seed(RolePermissionSeeder::class); $this->user = User::factory() ->active() ->has(Employee::factory()) ->create(); $this->ownerRole = Role::where('name', 'Owner')->first(); $this->user->roles()->attach($this->ownerRole->id); Bus::fake(); }); function mountCreateComponent(User $user) { return Livewire::actingAs($user)->test(Create::class); } function createOutlets(int $count = 3) { return Outlet::factory() ->count($count) ->has(OpeningHour::factory()->count(7)) ->has(Facility::factory()->count(5)) ->create(); } /* |-------------------------------------------------------------------------- | Component Rendering & Mount |-------------------------------------------------------------------------- */ it('renders page successfully', function () { mountCreateComponent($this->user) ->assertViewIs('livewire.studio.loyalty.voucher.form') ->assertViewHas('pageTitle', 'Tambah Voucher'); }); it('loads outlets on mount', function () { $outlets = createOutlets(3); $component = mountCreateComponent($this->user); expect($component->outlets)->toHaveCount(3); expect($component->outlets)->toHaveKey($outlets->first()->id); expect($component->outlets[$outlets->first()->id])->toBe($outlets->first()->name); }); it('initializes form with default values', function () { $component = mountCreateComponent($this->user); expect($component->form->name)->toBe(''); expect($component->form->code)->toBe(''); expect($component->form->tags)->toBe(''); expect($component->form->type)->toBe('1'); expect($component->form->discount_amount)->toBe(''); expect($component->form->outlet_ids)->toBe([]); }); /* |-------------------------------------------------------------------------- | Create Voucher - Fixed Type |-------------------------------------------------------------------------- */ it('can create a fixed type voucher with all fields', function () { $outlets = createOutlets(3); $outletIds = $outlets->pluck('id')->toArray(); $component = mountCreateComponent($this->user) ->set('form.name', 'Voucher Diskon 10rb') ->set('form.code', 'DISKON10') ->set('form.tags', 'Member Baru') ->set('form.type', VoucherType::FIXED->value) ->set('form.discount_amount', '10000') ->set('form.min_purchase', '50000') ->set('form.quota', '100') ->set('form.limit_per_user', '2') ->set('form.summary', 'Diskon 10rb untuk pembelian minimal 50rb') ->set('form.start_date', now()->addDay()->format('Y-m-d')) ->set('form.end_date', now()->addMonth()->format('Y-m-d')) ->set('form.outlet_ids', $outletIds) ->call('save'); $component->assertHasNoErrors() ->assertRedirect(route('studio.loyalty.voucher.index')); $this->assertDatabaseHas('vouchers', [ 'name' => 'Voucher Diskon 10rb', 'code' => 'DISKON10', 'tags' => 'Member Baru', 'type' => VoucherType::FIXED->value, 'discount_amount' => 10000, 'min_purchase' => 50000, 'quota' => 100, 'available_count' => 100, 'limit_per_user' => 2, 'summary' => 'Diskon 10rb untuk pembelian minimal 50rb', 'max_discount' => null, ]); $voucher = Voucher::where('code', 'DISKON10')->first(); expect($voucher->outlets)->toHaveCount(3); expect($voucher->outlets->pluck('id')->toArray())->toEqual($outletIds); }); it('can create a fixed type voucher without optional fields', function () { $outlets = createOutlets(1); mountCreateComponent($this->user) ->set('form.name', 'Voucher Gratis') ->set('form.code', 'GRATIS') ->set('form.tags', 'Promo') ->set('form.type', VoucherType::FIXED->value) ->set('form.discount_amount', '5000') ->set('form.summary', 'Voucher gratis') ->set('form.start_date', now()->addDay()->format('Y-m-d')) ->set('form.outlet_ids', [$outlets->first()->id]) ->call('save') ->assertHasNoErrors(); $this->assertDatabaseHas('vouchers', [ 'code' => 'GRATIS', 'min_purchase' => null, 'max_discount' => null, 'quota' => null, 'available_count' => 0, 'limit_per_user' => null, 'end_date' => null, ]); }); it('can create a fixed type voucher with currency formatted amounts', function () { $outlets = createOutlets(1); mountCreateComponent($this->user) ->set('form.name', 'Voucher Test') ->set('form.code', 'TEST01') ->set('form.tags', 'Test') ->set('form.type', VoucherType::FIXED->value) ->set('form.discount_amount', '25.000') ->set('form.min_purchase', '100.000') ->set('form.summary', 'Test voucher') ->set('form.start_date', now()->addDay()->format('Y-m-d')) ->set('form.outlet_ids', [$outlets->first()->id]) ->call('save') ->assertHasNoErrors(); $this->assertDatabaseHas('vouchers', [ 'code' => 'TEST01', 'discount_amount' => 25000, 'min_purchase' => 100000, ]); }); /* |-------------------------------------------------------------------------- | Create Voucher - Percentage Type |-------------------------------------------------------------------------- */ it('can create a percentage type voucher with max discount', function () { $outlets = createOutlets(2); mountCreateComponent($this->user) ->set('form.name', 'Diskon 20%') ->set('form.code', 'DISKON20') ->set('form.tags', 'Promo') ->set('form.type', VoucherType::PERCENTAGE->value) ->set('form.discount_amount', '20') ->set('form.max_discount', '50000') ->set('form.min_purchase', '100000') ->set('form.quota', '200') ->set('form.limit_per_user', '1') ->set('form.summary', 'Diskon 20% maksimal 50rb') ->set('form.start_date', now()->addDay()->format('Y-m-d')) ->set('form.end_date', now()->addMonths(2)->format('Y-m-d')) ->set('form.outlet_ids', $outlets->pluck('id')->toArray()) ->call('save') ->assertHasNoErrors(); $this->assertDatabaseHas('vouchers', [ 'code' => 'DISKON20', 'type' => VoucherType::PERCENTAGE->value, 'discount_amount' => 20, 'max_discount' => 50000, 'quota' => 200, 'available_count' => 200, ]); }); it('requires max_discount for percentage type voucher', function () { $outlets = createOutlets(1); mountCreateComponent($this->user) ->set('form.name', 'Diskon Test') ->set('form.code', 'TEST02') ->set('form.tags', 'Test') ->set('form.type', VoucherType::PERCENTAGE->value) ->set('form.discount_amount', '15') ->set('form.summary', 'Test') ->set('form.start_date', now()->addDay()->format('Y-m-d')) ->set('form.outlet_ids', [$outlets->first()->id]) ->call('save') ->assertHasErrors(['form.max_discount']); }); /* |-------------------------------------------------------------------------- | Validation Tests |-------------------------------------------------------------------------- */ it('validates required fields', function () { mountCreateComponent($this->user) ->set('form.type', '') ->call('save') ->assertHasErrors([ 'form.name', 'form.code', 'form.tags', 'form.type', 'form.discount_amount', 'form.summary', 'form.start_date', 'form.outlet_ids', ]); }); it('validates name max length', function () { $outlets = createOutlets(1); mountCreateComponent($this->user) ->set('form.name', str_repeat('a', 51)) ->set('form.code', 'TEST') ->set('form.tags', 'Test') ->set('form.type', VoucherType::FIXED->value) ->set('form.discount_amount', '10000') ->set('form.summary', 'Test') ->set('form.start_date', now()->addDay()->format('Y-m-d')) ->set('form.outlet_ids', [$outlets->first()->id]) ->call('save') ->assertHasErrors(['form.name']); }); it('validates code uniqueness', function () { $voucher = Voucher::factory()->create(['code' => 'EXISTING']); $outlets = createOutlets(1); mountCreateComponent($this->user) ->set('form.name', 'New Voucher') ->set('form.code', 'EXISTING') ->set('form.tags', 'Test') ->set('form.type', VoucherType::FIXED->value) ->set('form.discount_amount', '10000') ->set('form.summary', 'Test') ->set('form.start_date', now()->addDay()->format('Y-m-d')) ->set('form.outlet_ids', [$outlets->first()->id]) ->call('save') ->assertHasErrors(['form.code']); }); it('validates code format (alpha_num)', function () { $outlets = createOutlets(1); mountCreateComponent($this->user) ->set('form.name', 'Test') ->set('form.code', 'TEST-CODE') ->set('form.tags', 'Test') ->set('form.type', VoucherType::FIXED->value) ->set('form.discount_amount', '10000') ->set('form.summary', 'Test') ->set('form.start_date', now()->addDay()->format('Y-m-d')) ->set('form.outlet_ids', [$outlets->first()->id]) ->call('save') ->assertHasErrors(['form.code']); }); it('validates tags max length', function () { $outlets = createOutlets(1); mountCreateComponent($this->user) ->set('form.name', 'Test') ->set('form.code', 'TEST') ->set('form.tags', str_repeat('a', 21)) ->set('form.type', VoucherType::FIXED->value) ->set('form.discount_amount', '10000') ->set('form.summary', 'Test') ->set('form.start_date', now()->addDay()->format('Y-m-d')) ->set('form.outlet_ids', [$outlets->first()->id]) ->call('save') ->assertHasErrors(['form.tags']); }); it('validates percentage discount amount range', function () { $outlets = createOutlets(1); mountCreateComponent($this->user) ->set('form.name', 'Test') ->set('form.code', 'TEST') ->set('form.tags', 'Test') ->set('form.type', VoucherType::PERCENTAGE->value) ->set('form.discount_amount', '101') ->set('form.max_discount', '50000') ->set('form.summary', 'Test') ->set('form.start_date', now()->addDay()->format('Y-m-d')) ->set('form.outlet_ids', [$outlets->first()->id]) ->call('save') ->assertHasErrors(['form.discount_amount']); mountCreateComponent($this->user) ->set('form.name', 'Test') ->set('form.code', 'TEST2') ->set('form.tags', 'Test') ->set('form.type', VoucherType::PERCENTAGE->value) ->set('form.discount_amount', '-1') ->set('form.max_discount', '50000') ->set('form.summary', 'Test') ->set('form.start_date', now()->addDay()->format('Y-m-d')) ->set('form.outlet_ids', [$outlets->first()->id]) ->call('save') ->assertHasErrors(['form.discount_amount']); }); it('validates start date is after yesterday', function () { $outlets = createOutlets(1); mountCreateComponent($this->user) ->set('form.name', 'Test') ->set('form.code', 'TEST') ->set('form.tags', 'Test') ->set('form.type', VoucherType::FIXED->value) ->set('form.discount_amount', '10000') ->set('form.summary', 'Test') ->set('form.start_date', now()->subDays(2)->format('Y-m-d')) ->set('form.outlet_ids', [$outlets->first()->id]) ->call('save') ->assertHasErrors(['form.start_date']); }); it('validates end date is after or equal to start date', function () { $outlets = createOutlets(1); mountCreateComponent($this->user) ->set('form.name', 'Test') ->set('form.code', 'TEST') ->set('form.tags', 'Test') ->set('form.type', VoucherType::FIXED->value) ->set('form.discount_amount', '10000') ->set('form.summary', 'Test') ->set('form.start_date', now()->addDays(5)->format('Y-m-d')) ->set('form.end_date', now()->addDays(3)->format('Y-m-d')) ->set('form.outlet_ids', [$outlets->first()->id]) ->call('save') ->assertHasErrors(['form.end_date']); }); it('validates outlet_ids is required and has at least one', function () { mountCreateComponent($this->user) ->set('form.name', 'Test') ->set('form.code', 'TEST') ->set('form.tags', 'Test') ->set('form.type', VoucherType::FIXED->value) ->set('form.discount_amount', '10000') ->set('form.summary', 'Test') ->set('form.start_date', now()->addDay()->format('Y-m-d')) ->set('form.outlet_ids', []) ->call('save') ->assertHasErrors(['form.outlet_ids']); mountCreateComponent($this->user) ->set('form.name', 'Test') ->set('form.code', 'TEST2') ->set('form.tags', 'Test') ->set('form.type', VoucherType::FIXED->value) ->set('form.discount_amount', '10000') ->set('form.summary', 'Test') ->set('form.start_date', now()->addDay()->format('Y-m-d')) ->call('save') ->assertHasErrors(['form.outlet_ids']); }); it('validates outlet_ids exist', function () { mountCreateComponent($this->user) ->set('form.name', 'Test') ->set('form.code', 'TEST') ->set('form.tags', 'Test') ->set('form.type', VoucherType::FIXED->value) ->set('form.discount_amount', '10000') ->set('form.summary', 'Test') ->set('form.start_date', now()->addDay()->format('Y-m-d')) ->set('form.outlet_ids', [99999]) ->call('save') ->assertHasErrors(['form.outlet_ids.0']); }); it('validates summary max length', function () { $outlets = createOutlets(1); mountCreateComponent($this->user) ->set('form.name', 'Test') ->set('form.code', 'TEST') ->set('form.tags', 'Test') ->set('form.type', VoucherType::FIXED->value) ->set('form.discount_amount', '10000') ->set('form.summary', str_repeat('a', 201)) ->set('form.start_date', now()->addDay()->format('Y-m-d')) ->set('form.outlet_ids', [$outlets->first()->id]) ->call('save') ->assertHasErrors(['form.summary']); }); /* |-------------------------------------------------------------------------- | Outlet Selection |-------------------------------------------------------------------------- */ it('can select all outlets', function () { $outlets = createOutlets(5); $component = mountCreateComponent($this->user); $component->call('selectAllOutlets'); expect($component->form->outlet_ids)->toHaveCount(5); expect($component->form->outlet_ids)->toEqual($outlets->pluck('id')->toArray()); }); it('can deselect all outlets', function () { $outlets = createOutlets(3); $component = mountCreateComponent($this->user) ->set('form.outlet_ids', $outlets->pluck('id')->toArray()); expect($component->form->outlet_ids)->toHaveCount(3); $component->call('deselectAllOutlets'); expect($component->form->outlet_ids)->toHaveCount(0); expect($component->form->outlet_ids)->toBe([]); }); /* |-------------------------------------------------------------------------- | Permissions |-------------------------------------------------------------------------- */ it('prevents creation when user lacks create voucher permission', function () { $this->ownerRole->revokePermissionTo('create voucher'); Gate::define('create voucher', fn () => false); $outlets = createOutlets(1); mountCreateComponent($this->user) ->set('form.name', 'Test') ->set('form.code', 'TEST') ->set('form.tags', 'Test') ->set('form.type', VoucherType::FIXED->value) ->set('form.discount_amount', '10000') ->set('form.summary', 'Test') ->set('form.start_date', now()->addDay()->format('Y-m-d')) ->set('form.outlet_ids', [$outlets->first()->id]) ->call('save') ->assertForbidden(); }); it('allows creation when user has create voucher permission', function () { $outlets = createOutlets(1); mountCreateComponent($this->user) ->set('form.name', 'Test') ->set('form.code', 'TEST') ->set('form.tags', 'Test') ->set('form.type', VoucherType::FIXED->value) ->set('form.discount_amount', '10000') ->set('form.summary', 'Test') ->set('form.start_date', now()->addDay()->format('Y-m-d')) ->set('form.outlet_ids', [$outlets->first()->id]) ->call('save') ->assertHasNoErrors(); }); /* |-------------------------------------------------------------------------- | Push Notification |-------------------------------------------------------------------------- */ it('dispatches push notification job when creating voucher', function () { $users = User::factory()->count(3)->create(); foreach ($users as $user) { PushNotification::create([ 'user_id' => $user->id, 'subscriptions' => json_encode(['endpoint' => 'test', 'keys' => ['p256dh' => 'test', 'auth' => 'test']]), ]); } $outlets = createOutlets(1); mountCreateComponent($this->user) ->set('form.name', 'Test') ->set('form.code', 'TEST') ->set('form.tags', 'Test') ->set('form.type', VoucherType::FIXED->value) ->set('form.discount_amount', '10000') ->set('form.summary', 'Test') ->set('form.start_date', now()->addDay()->format('Y-m-d')) ->set('form.outlet_ids', [$outlets->first()->id]) ->call('save'); Bus::assertDispatched(StorePushNotification::class, function ($job) { return $job->payload['title'] === '🎁 Voucher Baru!' && $job->payload['body'] === 'Ada voucher baru untuk kamu! Jangan sampai kelewatan, yuk cek sekarang dan nikmati penawarannya ✨🎉'; }); }); /* |-------------------------------------------------------------------------- | Edge Cases |-------------------------------------------------------------------------- */ it('handles null optional fields correctly', function () { $outlets = createOutlets(1); mountCreateComponent($this->user) ->set('form.name', 'Test') ->set('form.code', 'TEST') ->set('form.tags', 'Test') ->set('form.type', VoucherType::FIXED->value) ->set('form.discount_amount', '10000') ->set('form.min_purchase', null) ->set('form.quota', null) ->set('form.limit_per_user', null) ->set('form.summary', 'Test') ->set('form.start_date', now()->addDay()->format('Y-m-d')) ->set('form.end_date', null) ->set('form.outlet_ids', [$outlets->first()->id]) ->call('save') ->assertHasNoErrors(); $voucher = Voucher::where('code', 'TEST')->first(); expect($voucher->min_purchase)->toBeNull(); expect($voucher->quota)->toBeNull(); expect($voucher->limit_per_user)->toBeNull(); expect($voucher->end_date)->toBeNull(); expect($voucher->available_count)->toBe(0); }); it('sets available_count equal to quota when quota is provided', function () { $outlets = createOutlets(1); mountCreateComponent($this->user) ->set('form.name', 'Test') ->set('form.code', 'TEST') ->set('form.tags', 'Test') ->set('form.type', VoucherType::FIXED->value) ->set('form.discount_amount', '10000') ->set('form.quota', '150') ->set('form.summary', 'Test') ->set('form.start_date', now()->addDay()->format('Y-m-d')) ->set('form.outlet_ids', [$outlets->first()->id]) ->call('save') ->assertHasNoErrors(); $voucher = Voucher::where('code', 'TEST')->first(); expect($voucher->quota)->toBe(150); expect($voucher->available_count)->toBe(150); });