parfum/tests/Feature/Livewire/Studio/Loyalty/Voucher/CreateTest.php
2025-12-08 11:08:27 +07:00

597 lines
20 KiB
PHP

<?php
use App\Enums\VoucherType;
use App\Jobs\StorePushNotification;
use App\Livewire\Studio\Loyalty\Voucher\Create;
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);
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('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);
});