feat(voucher): implementasi testing
This commit is contained in:
parent
b2069964f5
commit
a97d99d58d
@ -1,53 +1,596 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\VoucherType;
|
||||
use App\Jobs\StorePushNotification;
|
||||
use App\Livewire\Studio\Loyalty\Voucher\Create;
|
||||
use App\Livewire\Studio\Loyalty\Voucher\Index;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Facility;
|
||||
use App\Models\OpeningHour;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\PushNotification;
|
||||
use App\Models\User;
|
||||
use App\Models\Voucher;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('renders successfully', function () {
|
||||
Livewire::test(Create::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);
|
||||
|
||||
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('can create a voucher', function () {
|
||||
$data = Voucher::factory()
|
||||
->active()
|
||||
->raw();
|
||||
it('loads outlets on mount', function () {
|
||||
$outlets = createOutlets(3);
|
||||
|
||||
$outletIds = Outlet::factory()
|
||||
->count(4)
|
||||
->has(OpeningHour::factory()->count(7))
|
||||
->has(Facility::factory()->count(5))
|
||||
->create();
|
||||
$component = mountCreateComponent($this->user);
|
||||
|
||||
Livewire::test(Create::class)
|
||||
->set('form.name', $data['name'])
|
||||
->set('form.code', $data['code'])
|
||||
->set('form.min_purchase', $data['min_purchase'])
|
||||
->set('form.quota', $data['quota'])
|
||||
->set('form.limit_per_user', $data['limit_per_user'])
|
||||
->set('form.start_date', $data['start_date'])
|
||||
->set('form.end_date', $data['end_date'])
|
||||
->set('form.type', $data['type'])
|
||||
->set('form.discount_amount', $data['discount_amount'])
|
||||
->set('form.max_discount', $data['max_discount'])
|
||||
->set('form.is_active', $data['is_active']->value)
|
||||
->set('form.outlet_ids', $outletIds->pluck('id')->toArray())
|
||||
->call('save')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('refreshDatatable')
|
||||
->assertRedirect(Index::class);
|
||||
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' => $data['name'],
|
||||
'code' => $data['code'],
|
||||
'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);
|
||||
});
|
||||
|
||||
@ -1,61 +1,495 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\IsActive;
|
||||
use App\Enums\VoucherType;
|
||||
use App\Livewire\Studio\Loyalty\Voucher\Edit;
|
||||
use App\Livewire\Studio\Loyalty\Voucher\Index;
|
||||
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);
|
||||
|
||||
it('renders successfully', function () {
|
||||
$voucher = Voucher::factory()->active()->create();
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
Livewire::test(Edit::class, ['voucher' => $voucher])
|
||||
$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 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 () {
|
||||
$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('can update a voucher', function () {
|
||||
$voucher = Voucher::factory()->active()->create();
|
||||
it('loads outlets on mount', function () {
|
||||
$voucher = Voucher::factory()->create();
|
||||
$outlets = createOutlets(3);
|
||||
|
||||
$data = Voucher::factory()
|
||||
->state(fn () => [
|
||||
'is_active' => fake()->randomElement(IsActive::cases()),
|
||||
])
|
||||
->raw();
|
||||
$component = mountEditComponent($this->user, $voucher);
|
||||
|
||||
$outletIds = Outlet::factory()
|
||||
->count(4)
|
||||
->has(OpeningHour::factory()->count(7))
|
||||
->has(Facility::factory()->count(5))
|
||||
->create();
|
||||
expect($component->outlets)->toHaveCount(3);
|
||||
expect($component->outlets)->toHaveKey($outlets->first()->id);
|
||||
});
|
||||
|
||||
Livewire::test(Edit::class, ['voucher' => $voucher])
|
||||
->set('form.name', $data['name'])
|
||||
->set('form.code', $data['code'])
|
||||
->set('form.min_purchase', $data['min_purchase'])
|
||||
->set('form.quota', $data['quota'])
|
||||
->set('form.limit_per_user', $data['limit_per_user'])
|
||||
->set('form.start_date', $data['start_date'])
|
||||
->set('form.end_date', $data['end_date'])
|
||||
->set('form.type', $data['type'])
|
||||
->set('form.discount_amount', $data['discount_amount'])
|
||||
->set('form.max_discount', $data['max_discount'])
|
||||
->set('form.is_active', $data['is_active']->value)
|
||||
->set('form.outlet_ids', $outletIds->pluck('id')->toArray())
|
||||
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()
|
||||
->assertDispatched('refreshDatatable')
|
||||
->assertRedirect(Index::class);
|
||||
->assertRedirect(route('studio.loyalty.voucher.index'));
|
||||
|
||||
$this->assertDatabaseHas('vouchers', [
|
||||
'id' => $voucher->id,
|
||||
'name' => $data['name'],
|
||||
'code' => $data['code'],
|
||||
]);
|
||||
$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);
|
||||
});
|
||||
|
||||
@ -1,43 +1,312 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\IsActive;
|
||||
use App\Livewire\Studio\Loyalty\Voucher\Index;
|
||||
use App\Models\Employee;
|
||||
use App\Models\User;
|
||||
use App\Models\Voucher;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('renders successfully', function () {
|
||||
Livewire::test(Index::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 mountIndexComponent(User $user)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(Index::class);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders page successfully', function () {
|
||||
mountIndexComponent($this->user)
|
||||
->assertViewIs('livewire.studio.loyalty.voucher.index')
|
||||
->assertViewHas('pageTitle', 'Voucher');
|
||||
});
|
||||
|
||||
it('displays all vouchers', function () {
|
||||
Voucher::factory()
|
||||
->count(10)
|
||||
->state(fn () => [
|
||||
'is_active' => fake()->randomElement(IsActive::cases()),
|
||||
])
|
||||
->create();
|
||||
Voucher::factory()->count(10)->create();
|
||||
|
||||
$voucher = Voucher::first();
|
||||
|
||||
Livewire::test(Index::class)->assertSee($voucher->name);
|
||||
mountIndexComponent($this->user)
|
||||
->assertSee($voucher->name);
|
||||
});
|
||||
|
||||
it('can delete a voucher', function () {
|
||||
$voucher = Voucher::factory()
|
||||
->state(fn () => [
|
||||
'is_active' => fake()->randomElement(IsActive::cases()),
|
||||
])
|
||||
->create();
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Statistics
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
Livewire::test(Index::class)
|
||||
it('displays correct total voucher count', function () {
|
||||
Voucher::factory()->count(5)->create();
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->stats)->toHaveCount(4);
|
||||
expect($component->stats[0]['title'])->toBe('Total Voucher');
|
||||
expect($component->stats[0]['value'])->toBe(5);
|
||||
});
|
||||
|
||||
it('displays correct active voucher count', function () {
|
||||
// Active vouchers: start_date <= now AND (end_date is null OR end_date >= now)
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->subDay()->format('Y-m-d'),
|
||||
'end_date' => now()->addDay()->format('Y-m-d'),
|
||||
]);
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->subDay()->format('Y-m-d'),
|
||||
'end_date' => null,
|
||||
]);
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->subWeek()->format('Y-m-d'),
|
||||
'end_date' => now()->addWeek()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
// Inactive vouchers
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->subWeek()->format('Y-m-d'),
|
||||
'end_date' => now()->subDay()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
// Upcoming vouchers
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->addDay()->format('Y-m-d'),
|
||||
'end_date' => now()->addWeek()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->stats[1]['title'])->toBe('Voucher Aktif');
|
||||
expect($component->stats[1]['value'])->toBe(3);
|
||||
});
|
||||
|
||||
it('displays correct inactive voucher count', function () {
|
||||
// Inactive vouchers: end_date < now
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->subWeek()->format('Y-m-d'),
|
||||
'end_date' => now()->subDay()->format('Y-m-d'),
|
||||
]);
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->subMonth()->format('Y-m-d'),
|
||||
'end_date' => now()->subWeek()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
// Active vouchers
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->subDay()->format('Y-m-d'),
|
||||
'end_date' => now()->addDay()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
// Upcoming vouchers
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->addDay()->format('Y-m-d'),
|
||||
'end_date' => now()->addWeek()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->stats[2]['title'])->toBe('Voucher Tidak Aktif');
|
||||
expect($component->stats[2]['value'])->toBe(2);
|
||||
});
|
||||
|
||||
it('displays correct upcoming voucher count', function () {
|
||||
// Upcoming vouchers: start_date > now
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->addDay()->format('Y-m-d'),
|
||||
'end_date' => now()->addWeek()->format('Y-m-d'),
|
||||
]);
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->addWeek()->format('Y-m-d'),
|
||||
'end_date' => now()->addMonth()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
// Active vouchers
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->subDay()->format('Y-m-d'),
|
||||
'end_date' => now()->addDay()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
// Inactive vouchers
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->subWeek()->format('Y-m-d'),
|
||||
'end_date' => now()->subDay()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->stats[3]['title'])->toBe('Voucher Akan Datang');
|
||||
expect($component->stats[3]['value'])->toBe(2);
|
||||
});
|
||||
|
||||
it('calculates stats correctly with mixed voucher statuses', function () {
|
||||
// Create vouchers with different statuses
|
||||
Voucher::factory()->count(3)->create([
|
||||
'start_date' => now()->subDay()->format('Y-m-d'),
|
||||
'end_date' => now()->addDay()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
Voucher::factory()->count(2)->create([
|
||||
'start_date' => now()->subWeek()->format('Y-m-d'),
|
||||
'end_date' => now()->subDay()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
Voucher::factory()->count(4)->create([
|
||||
'start_date' => now()->addDay()->format('Y-m-d'),
|
||||
'end_date' => now()->addWeek()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->stats[0]['value'])->toBe(9); // Total
|
||||
expect($component->stats[1]['value'])->toBe(3); // Active
|
||||
expect($component->stats[2]['value'])->toBe(2); // Inactive
|
||||
expect($component->stats[3]['value'])->toBe(4); // Upcoming
|
||||
});
|
||||
|
||||
it('handles vouchers with null end_date in stats', function () {
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->subDay()->format('Y-m-d'),
|
||||
'end_date' => null, // Never expires
|
||||
]);
|
||||
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->addDay()->format('Y-m-d'),
|
||||
'end_date' => null, // Upcoming, never expires
|
||||
]);
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->stats[1]['value'])->toBe(1); // Active (started, no end date)
|
||||
expect($component->stats[3]['value'])->toBe(1); // Upcoming (not started yet)
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Delete Voucher
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('can delete a voucher', function () {
|
||||
$voucher = Voucher::factory()->create();
|
||||
|
||||
mountIndexComponent($this->user)
|
||||
->call('delete', $voucher)
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
expect(Voucher::count())->toBe(0);
|
||||
$this->assertSoftDeleted('vouchers', ['id' => $voucher->id]);
|
||||
});
|
||||
|
||||
it('can delete multiple vouchers', function () {
|
||||
$vouchers = Voucher::factory()->count(5)->create();
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
foreach ($vouchers as $voucher) {
|
||||
$component->call('delete', $voucher);
|
||||
}
|
||||
|
||||
expect(Voucher::count())->toBe(0);
|
||||
});
|
||||
|
||||
it('soft deletes voucher when deleting', function () {
|
||||
$voucher = Voucher::factory()->create();
|
||||
|
||||
mountIndexComponent($this->user)
|
||||
->call('delete', $voucher);
|
||||
|
||||
expect(Voucher::withTrashed()->count())->toBe(1);
|
||||
expect(Voucher::count())->toBe(0);
|
||||
expect($voucher->fresh()->trashed())->toBeTrue();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Permissions
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('displays create button when user has create voucher permission', function () {
|
||||
mountIndexComponent($this->user)
|
||||
->assertSee('Tambah');
|
||||
});
|
||||
|
||||
it('hides create button when user lacks create voucher permission', function () {
|
||||
$this->ownerRole->revokePermissionTo('create voucher');
|
||||
Gate::define('create voucher', fn () => false);
|
||||
|
||||
mountIndexComponent($this->user)
|
||||
->assertDontSee('Tambah');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Edge Cases
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('handles empty voucher list', function () {
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->stats[0]['value'])->toBe(0);
|
||||
expect($component->stats[1]['value'])->toBe(0);
|
||||
expect($component->stats[2]['value'])->toBe(0);
|
||||
expect($component->stats[3]['value'])->toBe(0);
|
||||
});
|
||||
|
||||
it('handles voucher with same start and end date', function () {
|
||||
$today = now()->format('Y-m-d');
|
||||
|
||||
Voucher::factory()->create([
|
||||
'start_date' => $today,
|
||||
'end_date' => $today,
|
||||
]);
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
// Should be counted as active (start_date <= now AND end_date >= now)
|
||||
expect($component->stats[1]['value'])->toBe(1);
|
||||
});
|
||||
|
||||
it('updates stats when vouchers are deleted', function () {
|
||||
$voucher1 = Voucher::factory()->create([
|
||||
'start_date' => now()->subDay()->format('Y-m-d'),
|
||||
'end_date' => now()->addDay()->format('Y-m-d'),
|
||||
]);
|
||||
$voucher2 = Voucher::factory()->create([
|
||||
'start_date' => now()->addDay()->format('Y-m-d'),
|
||||
'end_date' => now()->addWeek()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
expect($component->stats[0]['value'])->toBe(2);
|
||||
expect($component->stats[1]['value'])->toBe(1);
|
||||
expect($component->stats[3]['value'])->toBe(1);
|
||||
|
||||
$component->call('delete', $voucher1);
|
||||
|
||||
// Note: Stats are calculated in mount(), so they won't update automatically
|
||||
// This test verifies the delete works, but stats would need remount to update
|
||||
expect(Voucher::count())->toBe(1);
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user