Files
website/website/tests/Feature/Admin/CouponRedemptionTest.php
Claude Dev b4ef90465c feat: complete pre-launch audit — frontend polish, churn prevention, login history, financial reports, configurable checkout
Includes all work from phases 6-9+ and frontend polish rounds 1 & 2:

- Login history with device trust, new device notifications, session management
- Churn prevention: cancellation surveys, winback campaigns with email sequences
- Financial reports: revenue, P&L, tax, aging, refund, subscription reports with PDF/CSV/JSON export
- Configurable checkout: plan config groups/options, build-your-own VPS
- Frontend polish: fix broken legal links, add SEO meta tags, favicon, font display=swap,
  Head titles on all 14 marketing pages, mobile responsive fixes, AuthLayout legal footer,
  remove false 24/7 claims, hide empty stats, correct uptime SLA to 99.9%,
  GameServers notify buttons linked to /contact, 301 redirects for /terms and /privacy
- WHMCS migration scripts
- Update legal page effective dates to March 16, 2026

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 11:39:25 -04:00

336 lines
11 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\Coupon;
use App\Models\CouponRedemption;
use App\Models\User;
use Database\Seeders\RoleAndPermissionSeeder;
// DatabaseTransactions is applied globally in Pest.php
use Laravel\Cashier\Subscription;
// uses DatabaseTransactions from Pest.php
beforeEach(function (): void {
$this->seed(RoleAndPermissionSeeder::class);
$this->admin = User::factory()->admin()->create();
$this->actingAs($this->admin);
});
test('admin can view coupon redemptions page', function (): void {
$response = $this->get('http://admin.ezscale.dev/coupons/redemptions');
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Admin/Coupons/Redemptions')
->has('redemptions')
->has('coupons')
->has('stats')
->has('filters')
);
});
test('redemptions page displays all redemptions', function (): void {
$coupon = Coupon::factory()->create(['code' => 'TEST50']);
$user = User::factory()->create();
CouponRedemption::factory()
->for($coupon)
->for($user)
->create(['discount_amount' => '10.00']);
CouponRedemption::factory()
->for($coupon)
->for($user)
->create(['discount_amount' => '15.00']);
$response = $this->get('http://admin.ezscale.dev/coupons/redemptions');
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Admin/Coupons/Redemptions')
->where('redemptions.total', 2)
->where('stats.total_redemptions', 2)
->where('stats.total_discount', 25)
);
});
test('redemptions can be filtered by coupon', function (): void {
$coupon1 = Coupon::factory()->create(['code' => 'COUPON1']);
$coupon2 = Coupon::factory()->create(['code' => 'COUPON2']);
$user = User::factory()->create();
CouponRedemption::factory()->for($coupon1)->for($user)->create();
CouponRedemption::factory()->for($coupon2)->for($user)->create();
CouponRedemption::factory()->for($coupon2)->for($user)->create();
$response = $this->get("http://admin.ezscale.dev/coupons/redemptions?coupon_id={$coupon2->id}");
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Admin/Coupons/Redemptions')
->where('redemptions.total', 2)
->where('filters.coupon_id', (string) $coupon2->id)
);
});
test('redemptions can be filtered by customer name', function (): void {
$coupon = Coupon::factory()->create();
$user1 = User::factory()->create(['name' => 'John Doe']);
$user2 = User::factory()->create(['name' => 'Jane Smith']);
CouponRedemption::factory()->for($coupon)->for($user1)->create();
CouponRedemption::factory()->for($coupon)->for($user2)->create();
$response = $this->get('http://admin.ezscale.dev/coupons/redemptions?customer=John');
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Admin/Coupons/Redemptions')
->where('redemptions.total', 1)
->where('filters.customer', 'John')
);
});
test('redemptions can be filtered by customer email', function (): void {
$coupon = Coupon::factory()->create();
$user1 = User::factory()->create(['email' => 'john@example.com']);
$user2 = User::factory()->create(['email' => 'jane@example.com']);
CouponRedemption::factory()->for($coupon)->for($user1)->create();
CouponRedemption::factory()->for($coupon)->for($user2)->create();
$response = $this->get('http://admin.ezscale.dev/coupons/redemptions?customer=john@example.com');
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Admin/Coupons/Redemptions')
->where('redemptions.total', 1)
);
});
test('redemptions can be filtered by date range', function (): void {
$coupon = Coupon::factory()->create();
$user = User::factory()->create();
// Create redemptions on different dates
CouponRedemption::factory()
->for($coupon)
->for($user)
->create(['created_at' => now()->subDays(10)]);
CouponRedemption::factory()
->for($coupon)
->for($user)
->create(['created_at' => now()->subDays(5)]);
CouponRedemption::factory()
->for($coupon)
->for($user)
->create(['created_at' => now()->subDay()]);
$dateFrom = now()->subDays(6)->format('Y-m-d');
$dateTo = now()->format('Y-m-d');
$response = $this->get("http://admin.ezscale.dev/coupons/redemptions?date_from={$dateFrom}&date_to={$dateTo}");
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Admin/Coupons/Redemptions')
->where('redemptions.total', 2)
);
});
test('redemptions page calculates correct stats', function (): void {
$coupon1 = Coupon::factory()->create();
$coupon2 = Coupon::factory()->create();
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$user3 = User::factory()->create();
// Create multiple redemptions
CouponRedemption::factory()->for($coupon1)->for($user1)->create(['discount_amount' => '10.00']);
CouponRedemption::factory()->for($coupon1)->for($user2)->create(['discount_amount' => '15.00']);
CouponRedemption::factory()->for($coupon2)->for($user3)->create(['discount_amount' => '20.00']);
CouponRedemption::factory()->for($coupon2)->for($user1)->create(['discount_amount' => '25.00']);
$response = $this->get('http://admin.ezscale.dev/coupons/redemptions');
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Admin/Coupons/Redemptions')
->where('stats.total_redemptions', 4)
->where('stats.total_discount', 70)
->where('stats.unique_customers', 3)
->where('stats.unique_coupons', 2)
);
});
test('stats are filtered correctly when filters are applied', function (): void {
$coupon1 = Coupon::factory()->create();
$coupon2 = Coupon::factory()->create();
$user1 = User::factory()->create();
$user2 = User::factory()->create();
CouponRedemption::factory()->for($coupon1)->for($user1)->create(['discount_amount' => '10.00']);
CouponRedemption::factory()->for($coupon1)->for($user2)->create(['discount_amount' => '15.00']);
CouponRedemption::factory()->for($coupon2)->for($user1)->create(['discount_amount' => '20.00']);
$response = $this->get("http://admin.ezscale.dev/coupons/redemptions?coupon_id={$coupon1->id}");
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Admin/Coupons/Redemptions')
->where('stats.total_redemptions', 2)
->where('stats.total_discount', 25)
->where('stats.unique_customers', 2)
->where('stats.unique_coupons', 1)
);
});
test('redemptions page eager loads relationships', function (): void {
$coupon = Coupon::factory()->create();
$user = User::factory()->create();
$subscription = Subscription::factory()
->for($user)
->create();
CouponRedemption::factory()
->for($coupon)
->for($user)
->for($subscription)
->create();
$response = $this->get('http://admin.ezscale.dev/coupons/redemptions');
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Admin/Coupons/Redemptions')
->where('redemptions.data.0.coupon.code', $coupon->code)
->where('redemptions.data.0.user.name', $user->name)
->has('redemptions.data.0.subscription')
);
});
test('redemptions are paginated correctly', function (): void {
$coupon = Coupon::factory()->create();
$user = User::factory()->create();
// Create 30 redemptions
CouponRedemption::factory()
->count(30)
->for($coupon)
->for($user)
->create();
$response = $this->get('http://admin.ezscale.dev/coupons/redemptions');
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Admin/Coupons/Redemptions')
->where('redemptions.total', 30)
->where('redemptions.last_page', 2)
->where('redemptions.from', 1)
->where('redemptions.to', 25)
);
});
test('non-admin users cannot access redemptions page', function (): void {
$customer = User::factory()->create();
$customer->assignRole('customer');
$this->actingAs($customer)
->get('http://admin.ezscale.dev/coupons/redemptions')
->assertForbidden();
});
test('guests cannot access redemptions page', function (): void {
auth()->logout();
$this->get('http://admin.ezscale.dev/coupons/redemptions')
->assertRedirect();
});
test('coupons list is available for filter dropdown', function (): void {
Coupon::factory()->create(['code' => 'ALPHA']);
Coupon::factory()->create(['code' => 'BETA']);
Coupon::factory()->create(['code' => 'GAMMA']);
$response = $this->get('http://admin.ezscale.dev/coupons/redemptions');
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Admin/Coupons/Redemptions')
->has('coupons', 3)
->where('coupons.0.code', 'ALPHA')
->where('coupons.1.code', 'BETA')
->where('coupons.2.code', 'GAMMA')
);
});
test('redemptions with deleted users are cascade deleted', function (): void {
$coupon = Coupon::factory()->create();
$user = User::factory()->create();
$redemption = CouponRedemption::factory()
->for($coupon)
->for($user)
->create();
// Delete the user (cascades to redemption)
$user->delete();
$response = $this->get('http://admin.ezscale.dev/coupons/redemptions');
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Admin/Coupons/Redemptions')
->where('redemptions.total', 0)
);
});
test('redemptions can be exported to CSV', function (): void {
$coupon = Coupon::factory()->create(['code' => 'TEST50', 'type' => 'percentage', 'value' => '50.00']);
$user = User::factory()->create(['name' => 'John Doe', 'email' => 'john@example.com']);
CouponRedemption::factory()
->for($coupon)
->for($user)
->create(['discount_amount' => '25.00']);
$response = $this->get('http://admin.ezscale.dev/coupons/redemptions?export=csv');
$response->assertOk();
$response->assertHeader('Content-Type', 'text/csv; charset=UTF-8');
$response->assertHeader('Content-Disposition');
$content = $response->streamedContent();
expect($content)->toContain('Redemption ID')
->and($content)->toContain('Coupon Code')
->and($content)->toContain('TEST50')
->and($content)->toContain('percentage')
->and($content)->toContain('John Doe')
->and($content)->toContain('john@example.com')
->and($content)->toContain('25.00');
});
test('CSV export respects filters', function (): void {
$coupon1 = Coupon::factory()->create(['code' => 'COUPON1']);
$coupon2 = Coupon::factory()->create(['code' => 'COUPON2']);
$user = User::factory()->create();
CouponRedemption::factory()->for($coupon1)->for($user)->create();
CouponRedemption::factory()->for($coupon2)->for($user)->create();
$response = $this->get("http://admin.ezscale.dev/coupons/redemptions?export=csv&coupon_id={$coupon1->id}");
$response->assertOk();
$content = $response->streamedContent();
expect($content)->toContain('COUPON1')
->and($content)->not->toContain('COUPON2');
});