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); }); function mountEditComponent(User $user, Voucher $voucher) { return Livewire::actingAs($user)->test(Edit::class, ['voucher' => $voucher]); } function createTestOutlets(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 () { $voucher = Voucher::factory()->create(); $voucher->outlets()->attach(createOutlets(2)->pluck('id')); mountEditComponent($this->user, $voucher) ->assertViewIs('livewire.studio.loyalty.voucher.form') ->assertViewHas('pageTitle', 'Ubah Voucher'); }); it('loads outlets on mount', function () { $voucher = Voucher::factory()->create(); $outlets = createOutlets(3); $component = mountEditComponent($this->user, $voucher); expect($component->outlets)->toHaveCount(3); expect($component->outlets)->toHaveKey($outlets->first()->id); }); it('loads voucher data into form on mount', function () { $outlets = createOutlets(2); $voucher = Voucher::factory()->create([ 'name' => 'Test Voucher', 'code' => 'TEST123', 'tags' => 'Promo', 'type' => VoucherType::FIXED->value, 'discount_amount' => 25000, 'min_purchase' => 100000, 'max_discount' => null, 'quota' => '50', 'limit_per_user' => '2', 'summary' => 'Test summary', 'start_date' => now()->addDay()->format('Y-m-d'), 'end_date' => now()->addMonth()->format('Y-m-d'), ]); $voucher->outlets()->attach($outlets->pluck('id')); $component = mountEditComponent($this->user, $voucher); expect($component->form->name)->toBe('Test Voucher'); expect($component->form->code)->toBe('TEST123'); expect($component->form->tags)->toBe('Promo'); expect($component->form->type)->toBe((string) VoucherType::FIXED->value); expect($component->form->summary)->toBe('Test summary'); expect($component->form->quota)->toBe('50'); expect($component->form->limit_per_user)->toBe('2'); expect($component->form->start_date)->toBe(now()->addDay()->format('Y-m-d')); expect($component->form->end_date)->toBe(now()->addMonth()->format('Y-m-d')); expect($component->form->outlet_ids)->toEqual($outlets->pluck('id')->toArray()); }); it('formats currency values correctly when loading voucher', function () { $voucher = Voucher::factory()->create([ 'discount_amount' => 50000, 'min_purchase' => 200000, 'max_discount' => 100000, ]); $voucher->outlets()->attach(createOutlets(1)->pluck('id')); $component = mountEditComponent($this->user, $voucher); // Note: replaceCurrency returns integer, so we check the raw value // The form will display formatted currency in the view expect($component->form->discount_amount)->toBe('50000'); expect($component->form->min_purchase)->toBe('200000'); expect($component->form->max_discount)->toBe('100000'); }); /* |-------------------------------------------------------------------------- | Update Voucher - Fixed Type |-------------------------------------------------------------------------- */ it('can update a fixed type voucher', function () { $outlets = createOutlets(3); $voucher = Voucher::factory()->create([ 'type' => VoucherType::FIXED->value, ]); $voucher->outlets()->attach($outlets->take(2)->pluck('id')); $newOutlets = createOutlets(2); $allOutlets = $outlets->merge($newOutlets); mountEditComponent($this->user, $voucher) ->set('form.name', 'Updated Voucher') ->set('form.code', 'UPDATED') ->set('form.tags', 'Updated Tag') ->set('form.discount_amount', '30000') ->set('form.min_purchase', '150000') ->set('form.quota', '75') ->set('form.limit_per_user', '3') ->set('form.summary', 'Updated summary') ->set('form.start_date', now()->addDays(2)->format('Y-m-d')) ->set('form.end_date', now()->addMonths(2)->format('Y-m-d')) ->set('form.outlet_ids', $allOutlets->pluck('id')->toArray()) ->call('save') ->assertHasNoErrors() ->assertRedirect(route('studio.loyalty.voucher.index')); $voucher->refresh(); expect($voucher->name)->toBe('Updated Voucher'); expect($voucher->code)->toBe('UPDATED'); expect($voucher->tags)->toBe('Updated Tag'); expect($voucher->discount_amount)->toBe(30000); expect($voucher->min_purchase)->toBe(150000); expect($voucher->quota)->toBe(75); expect($voucher->limit_per_user)->toBe(3); expect($voucher->outlets)->toHaveCount(5); }); it('can update voucher to remove optional fields', function () { $voucher = Voucher::factory()->create([ 'min_purchase' => 50000, 'quota' => 100, 'limit_per_user' => 2, 'end_date' => now()->addMonth()->format('Y-m-d'), ]); $voucher->outlets()->attach(createOutlets(1)->pluck('id')); mountEditComponent($this->user, $voucher) ->set('form.min_purchase', null) ->set('form.quota', null) ->set('form.limit_per_user', null) ->set('form.end_date', null) ->call('save') ->assertHasNoErrors(); $voucher->refresh(); expect($voucher->min_purchase)->toBeNull(); expect($voucher->quota)->toBeNull(); expect($voucher->limit_per_user)->toBeNull(); expect($voucher->end_date)->toBeNull(); }); /* |-------------------------------------------------------------------------- | Update Voucher - Percentage Type |-------------------------------------------------------------------------- */ it('can update voucher from fixed to percentage type', function () { $voucher = Voucher::factory()->create([ 'type' => VoucherType::FIXED->value, 'discount_amount' => 10000, 'max_discount' => null, ]); $voucher->outlets()->attach(createOutlets(1)->pluck('id')); mountEditComponent($this->user, $voucher) ->set('form.type', VoucherType::PERCENTAGE->value) ->set('form.discount_amount', '15') ->set('form.max_discount', '40000') ->call('save') ->assertHasNoErrors(); $voucher->refresh(); expect($voucher->type)->toBe(VoucherType::PERCENTAGE); expect($voucher->discount_amount)->toBe(15); expect($voucher->max_discount)->toBe(40000); }); it('can update voucher from percentage to fixed type', function () { $voucher = Voucher::factory()->create([ 'type' => VoucherType::PERCENTAGE->value, 'discount_amount' => 20, 'max_discount' => 50000, ]); $voucher->outlets()->attach(createOutlets(1)->pluck('id')); mountEditComponent($this->user, $voucher) ->set('form.type', VoucherType::FIXED->value) ->set('form.discount_amount', '25000') ->call('save') ->assertHasNoErrors(); $voucher->refresh(); expect($voucher->type)->toBe(VoucherType::FIXED); expect($voucher->discount_amount)->toBe(25000); expect($voucher->max_discount)->toBeNull(); }); it('can update percentage voucher with new max discount', function () { $voucher = Voucher::factory()->create([ 'type' => VoucherType::PERCENTAGE->value, 'discount_amount' => 10, 'max_discount' => 30000, ]); $voucher->outlets()->attach(createOutlets(1)->pluck('id')); mountEditComponent($this->user, $voucher) ->set('form.discount_amount', '25') ->set('form.max_discount', '60000') ->call('save') ->assertHasNoErrors(); $voucher->refresh(); expect($voucher->discount_amount)->toBe(25); expect($voucher->max_discount)->toBe(60000); }); /* |-------------------------------------------------------------------------- | Outlet Management |-------------------------------------------------------------------------- */ it('can update voucher outlets', function () { $originalOutlets = createOutlets(2); $voucher = Voucher::factory()->create(); $voucher->outlets()->attach($originalOutlets->pluck('id')); $newOutlets = createOutlets(3); mountEditComponent($this->user, $voucher) ->set('form.outlet_ids', $newOutlets->pluck('id')->toArray()) ->call('save') ->assertHasNoErrors(); $voucher->refresh(); expect($voucher->outlets)->toHaveCount(3); expect($voucher->outlets->pluck('id')->toArray())->toEqual($newOutlets->pluck('id')->toArray()); }); it('can add more outlets to existing voucher', function () { $originalOutlets = createOutlets(2); $voucher = Voucher::factory()->create(); $voucher->outlets()->attach($originalOutlets->pluck('id')); $additionalOutlets = createOutlets(2); $allOutlets = $originalOutlets->merge($additionalOutlets); mountEditComponent($this->user, $voucher) ->set('form.outlet_ids', $allOutlets->pluck('id')->toArray()) ->call('save') ->assertHasNoErrors(); $voucher->refresh(); expect($voucher->outlets)->toHaveCount(4); }); it('can remove all outlets from voucher', function () { $outlets = createOutlets(3); $voucher = Voucher::factory()->create(); $voucher->outlets()->attach($outlets->pluck('id')); mountEditComponent($this->user, $voucher) ->set('form.outlet_ids', []) ->call('save') ->assertHasErrors(['form.outlet_ids']); // Should fail validation }); it('can select all outlets when editing', function () { $outlets = createOutlets(5); $voucher = Voucher::factory()->create(); $voucher->outlets()->attach($outlets->take(2)->pluck('id')); $component = mountEditComponent($this->user, $voucher); $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 when editing', function () { $outlets = createOutlets(3); $voucher = Voucher::factory()->create(); $voucher->outlets()->attach($outlets->pluck('id')); $component = mountEditComponent($this->user, $voucher); expect($component->form->outlet_ids)->toHaveCount(3); $component->call('deselectAllOutlets'); expect($component->form->outlet_ids)->toHaveCount(0); }); /* |-------------------------------------------------------------------------- | Validation Tests |-------------------------------------------------------------------------- */ it('validates required fields on update', function () { $voucher = Voucher::factory()->create(); $voucher->outlets()->attach(createOutlets(1)->pluck('id')); mountEditComponent($this->user, $voucher) ->set('form.name', '') ->set('form.code', '') ->set('form.tags', '') ->set('form.summary', '') ->set('form.start_date', '') ->set('form.outlet_ids', []) ->call('save') ->assertHasErrors([ 'form.name', 'form.code', 'form.tags', 'form.summary', 'form.start_date', 'form.outlet_ids', ]); }); it('allows same code when updating voucher', function () { $voucher = Voucher::factory()->create(['code' => 'EXISTING']); $voucher->outlets()->attach(createOutlets(1)->pluck('id')); mountEditComponent($this->user, $voucher) ->set('form.name', 'Updated Name') ->set('form.code', 'EXISTING') // Same 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', [$voucher->outlets->first()->id]) ->call('save') ->assertHasNoErrors(); }); it('validates code uniqueness when changing to existing code', function () { $voucher1 = Voucher::factory()->create(['code' => 'VOUCHER1']); $voucher2 = Voucher::factory()->create(['code' => 'VOUCHER2']); $voucher1->outlets()->attach(createOutlets(1)->pluck('id')); mountEditComponent($this->user, $voucher1) ->set('form.code', 'VOUCHER2') // Trying to use existing code ->set('form.name', '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', [$voucher1->outlets->first()->id]) ->call('save') ->assertHasErrors(['form.code']); }); it('validates percentage discount requires max_discount on update', function () { $voucher = Voucher::factory()->create([ 'type' => VoucherType::PERCENTAGE->value, 'max_discount' => 50000, ]); $voucher->outlets()->attach(createOutlets(1)->pluck('id')); mountEditComponent($this->user, $voucher) ->set('form.max_discount', null) ->call('save') ->assertHasErrors(['form.max_discount']); }); /* |-------------------------------------------------------------------------- | Permissions |-------------------------------------------------------------------------- */ it('prevents update when user lacks update voucher permission', function () { $this->ownerRole->revokePermissionTo('update voucher'); Gate::define('update voucher', fn () => false); $voucher = Voucher::factory()->create(); $voucher->outlets()->attach(createOutlets(1)->pluck('id')); mountEditComponent($this->user, $voucher) ->set('form.name', 'Updated') ->set('form.code', 'UPDATED') ->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', [$voucher->outlets->first()->id]) ->call('save') ->assertForbidden(); }); it('allows update when user has update voucher permission', function () { $voucher = Voucher::factory()->create(); $voucher->outlets()->attach(createOutlets(1)->pluck('id')); mountEditComponent($this->user, $voucher) ->set('form.name', 'Updated') ->set('form.code', 'UPDATED') ->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', [$voucher->outlets->first()->id]) ->call('save') ->assertHasNoErrors(); }); /* |-------------------------------------------------------------------------- | Edge Cases |-------------------------------------------------------------------------- */ it('handles voucher with no outlets initially', function () { $voucher = Voucher::factory()->create(); // No outlets attached $outlets = createOutlets(2); mountEditComponent($this->user, $voucher) ->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->pluck('id')->toArray()) ->call('save') ->assertHasNoErrors(); $voucher->refresh(); expect($voucher->outlets)->toHaveCount(2); }); it('preserves available_count when updating quota', function () { $voucher = Voucher::factory()->create([ 'quota' => 100, 'available_count' => 50, // Some vouchers already used ]); $voucher->outlets()->attach(createOutlets(1)->pluck('id')); // Note: The form doesn't update available_count, only quota // This test verifies that available_count is not reset mountEditComponent($this->user, $voucher) ->set('form.quota', '200') ->call('save') ->assertHasNoErrors(); $voucher->refresh(); expect($voucher->quota)->toBe(200); // available_count should remain 50 (not reset to 200) expect($voucher->available_count)->toBe(50); });