parfum/tests/Feature/Livewire/Studio/Loyalty/Voucher/EditTest.php

496 lines
17 KiB
PHP

<?php
use App\Enums\VoucherType;
use App\Livewire\Studio\Loyalty\Voucher\Edit;
use App\Models\Employee;
use App\Models\Facility;
use App\Models\OpeningHour;
use App\Models\Outlet;
use App\Models\User;
use App\Models\Voucher;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->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: parseRupiahToInt 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);
});