user = User::factory()->create(); $this->workspace = Workspace::factory()->create(); $this->workspace->users()->attach($this->user->id, [ 'role' => 'owner', 'is_default' => true, ]); // Use existing seeded package $this->package = Package::where('code', 'creator')->first(); // Create test coupons $this->percentCoupon = Coupon::create([ 'code' => 'SAVE20', 'name' => '20% Off', 'type' => 'percentage', 'value' => 20.00, 'applies_to' => 'all', 'is_active' => true, 'max_uses' => 100, 'max_uses_per_workspace' => 1, 'used_count' => 0, ]); $this->fixedCoupon = Coupon::create([ 'code' => 'FLAT10', 'name' => '£10 Off', 'type' => 'fixed_amount', 'value' => 10.00, 'applies_to' => 'all', 'is_active' => true, 'max_uses_per_workspace' => 1, ]); $this->service = app(CouponService::class); }); describe('CouponService', function () { describe('findByCode() method', function () { it('finds coupon by code (case insensitive)', function () { $coupon = $this->service->findByCode('save20'); expect($coupon)->not->toBeNull() ->and($coupon->code)->toBe('SAVE20'); }); it('returns null for non-existent code', function () { $coupon = $this->service->findByCode('NOTREAL'); expect($coupon)->toBeNull(); }); }); describe('validate() method', function () { it('validates active coupon', function () { $result = $this->service->validate( $this->percentCoupon, $this->workspace, $this->package ); expect($result->isValid())->toBeTrue(); }); it('rejects inactive coupon', function () { $this->percentCoupon->update(['is_active' => false]); $result = $this->service->validate( $this->percentCoupon, $this->workspace, $this->package ); expect($result->isValid())->toBeFalse(); }); it('rejects expired coupon', function () { $this->percentCoupon->update(['valid_until' => now()->subDay()]); $result = $this->service->validate( $this->percentCoupon, $this->workspace, $this->package ); expect($result->isValid())->toBeFalse(); }); it('rejects coupon before start date', function () { $this->percentCoupon->update(['valid_from' => now()->addDay()]); $result = $this->service->validate( $this->percentCoupon, $this->workspace, $this->package ); expect($result->isValid())->toBeFalse(); }); it('rejects coupon that has reached max uses', function () { $this->percentCoupon->update([ 'max_uses' => 5, 'used_count' => 5, ]); $result = $this->service->validate( $this->percentCoupon, $this->workspace, $this->package ); expect($result->isValid())->toBeFalse(); }); it('rejects coupon already used by workspace', function () { // Need an order for coupon usage $order = Order::create([ 'workspace_id' => $this->workspace->id, 'order_number' => 'ORD-001', 'status' => 'paid', 'subtotal' => 19.00, 'total' => 15.20, 'currency' => 'GBP', ]); CouponUsage::create([ 'coupon_id' => $this->percentCoupon->id, 'workspace_id' => $this->workspace->id, 'order_id' => $order->id, 'discount_amount' => 3.80, ]); $result = $this->service->validate( $this->percentCoupon, $this->workspace, $this->package ); expect($result->isValid())->toBeFalse(); }); it('validates coupon restricted to specific packages', function () { // Set applies_to to 'packages' and provide package IDs $this->percentCoupon->update([ 'applies_to' => 'packages', 'package_ids' => [$this->package->id], ]); $result = $this->service->validate( $this->percentCoupon, $this->workspace, $this->package ); expect($result->isValid())->toBeTrue(); // Use existing seeded agency package $otherPackage = Package::where('code', 'agency')->first(); $result = $this->service->validate( $this->percentCoupon, $this->workspace, $otherPackage ); expect($result->isValid())->toBeFalse(); }); it('validates minimum purchase amount', function () { $this->fixedCoupon->update(['min_amount' => 50.00]); // When min_amount is set, the validation in CouponService doesn't check it // The calculateDiscount method returns 0 for amounts below min_amount // So this test should check the discount calculation instead $discount = $this->fixedCoupon->calculateDiscount(19.00); expect($discount)->toBe(0.0); }); }); describe('recordUsage() method', function () { it('records coupon usage', function () { $order = Order::create([ 'workspace_id' => $this->workspace->id, 'order_number' => 'ORD-001', 'status' => 'paid', 'subtotal' => 19.00, 'total' => 15.20, 'currency' => 'GBP', ]); $usage = $this->service->recordUsage( $this->percentCoupon, $this->workspace, $order, 3.80 ); expect($usage)->toBeInstanceOf(CouponUsage::class) ->and($usage->coupon_id)->toBe($this->percentCoupon->id) ->and($usage->workspace_id)->toBe($this->workspace->id) ->and($usage->order_id)->toBe($order->id) ->and((float) $usage->discount_amount)->toBe(3.80); // Check used_count was incremented $this->percentCoupon->refresh(); expect($this->percentCoupon->used_count)->toBe(1); }); }); }); describe('Coupon model', function () { describe('calculateDiscount() method', function () { it('calculates percentage discount', function () { $discount = $this->percentCoupon->calculateDiscount(100.00); expect($discount)->toBe(20.00); }); it('calculates fixed discount', function () { $discount = $this->fixedCoupon->calculateDiscount(100.00); expect($discount)->toBe(10.00); }); it('caps fixed discount at subtotal', function () { $discount = $this->fixedCoupon->calculateDiscount(5.00); expect($discount)->toBe(5.00); // Can't discount more than subtotal }); it('respects max discount amount', function () { $this->percentCoupon->update(['max_discount' => 15.00]); $discount = $this->percentCoupon->calculateDiscount(100.00); expect($discount)->toBe(15.00); // Capped at max }); }); describe('isValid() method', function () { it('returns true for valid coupon', function () { expect($this->percentCoupon->isValid())->toBeTrue(); }); it('returns false for inactive coupon', function () { $this->percentCoupon->update(['is_active' => false]); expect($this->percentCoupon->isValid())->toBeFalse(); }); it('returns false for expired coupon', function () { $this->percentCoupon->update(['valid_until' => now()->subHour()]); expect($this->percentCoupon->isValid())->toBeFalse(); }); it('returns true within date range', function () { $this->percentCoupon->update([ 'valid_from' => now()->subDay(), 'valid_until' => now()->addDay(), ]); expect($this->percentCoupon->isValid())->toBeTrue(); }); }); describe('hasReachedMaxUses() method', function () { it('returns false when under limit', function () { $this->percentCoupon->update([ 'max_uses' => 100, 'used_count' => 50, ]); expect($this->percentCoupon->hasReachedMaxUses())->toBeFalse(); }); it('returns true when at limit', function () { $this->percentCoupon->update([ 'max_uses' => 100, 'used_count' => 100, ]); expect($this->percentCoupon->hasReachedMaxUses())->toBeTrue(); }); it('returns false when no limit set', function () { $this->percentCoupon->update([ 'max_uses' => null, 'used_count' => 1000, ]); expect($this->percentCoupon->hasReachedMaxUses())->toBeFalse(); }); }); describe('isRestrictedToPackage() method', function () { it('returns false when no package restrictions', function () { expect($this->percentCoupon->isRestrictedToPackage('creator'))->toBeFalse(); }); it('returns true for allowed package', function () { $this->percentCoupon->update(['package_ids' => ['creator', 'agency']]); expect($this->percentCoupon->isRestrictedToPackage('creator'))->toBeTrue() ->and($this->percentCoupon->isRestrictedToPackage('agency'))->toBeTrue(); }); it('returns false for restricted package', function () { $this->percentCoupon->update(['package_ids' => ['creator']]); expect($this->percentCoupon->isRestrictedToPackage('agency'))->toBeFalse(); }); }); describe('scopes', function () { it('scopes to active coupons', function () { Coupon::create([ 'code' => 'INACTIVE', 'name' => 'Inactive', 'type' => 'percentage', 'value' => 10.00, 'is_active' => false, ]); $active = Coupon::active()->get(); expect($active->pluck('code')->toArray())->toContain('SAVE20', 'FLAT10') ->and($active->pluck('code')->toArray())->not->toContain('INACTIVE'); }); it('scopes to valid coupons', function () { Coupon::create([ 'code' => 'EXPIRED', 'name' => 'Expired', 'type' => 'percentage', 'value' => 10.00, 'is_active' => true, 'valid_until' => now()->subDay(), ]); $valid = Coupon::valid()->get(); expect($valid->pluck('code')->toArray())->toContain('SAVE20', 'FLAT10') ->and($valid->pluck('code')->toArray())->not->toContain('EXPIRED'); }); }); });