seed(RolePermissionSeeder::class); }); function createPurchaseUserWithPermission(PermissionEnum ...$permissions): User { $user = User::factory()->create(); $user->givePermissionTo( array_merge( [PermissionEnum::DASHBOARD_VIEW->value], array_map(fn (PermissionEnum $p) => $p->value, $permissions) ) ); $user->forgetCachedPermissions(); return $user; } function createPurchaseVerifierUser(): User { $user = User::factory()->create(); $user->givePermissionTo([ PermissionEnum::DASHBOARD_VIEW->value, PermissionEnum::OWNER_VERIFICATIONS_VIEW->value, PermissionEnum::OWNER_VERIFICATIONS_VERIFY->value, PermissionEnum::OWNER_VERIFICATIONS_REJECT->value, ]); $user->forgetCachedPermissions(); return $user; } function approveLatestPurchaseVerificationRequest(User $verifier): OwnerVerificationRequest { $verificationRequest = OwnerVerificationRequest::query()->pending()->latest()->firstOrFail(); test()->actingAs($verifier) ->post(route('admin.manage.owner_verifications.approve_request', $verificationRequest)) ->assertRedirect(); return $verificationRequest->fresh(); } describe('Purchase Owner Verification', function () { test('create purchase submits verification without changing stock', function () { $user = createPurchaseUserWithPermission( PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE, ); $supplier = Supplier::factory()->create(); $price = RawMaterialPrice::factory()->create(['stock' => 10]); $initialStock = (float) $price->stock; $this->actingAs($user) ->postJson(route('admin.manage.purchases.draft_items.store'), [ 'raw_material_price_id' => $price->id, 'quantity' => 2, ]) ->assertOk(); $this->actingAs($user) ->post(route('admin.manage.purchases.store'), [ 'supplier_id' => $supplier->id, 'discount' => 0, 'shipping_cost' => 0, 'notes' => 'Belanja test', ]) ->assertRedirect(route('admin.manage.purchases.index')); $purchase = Purchase::query()->latest()->first(); expect($purchase)->not->toBeNull(); $this->assertDatabaseHas('owner_verification_requests', [ 'subject_type' => Purchase::class, 'subject_id' => $purchase->id, 'action' => OwnerVerificationAction::CREATE->value, 'status' => OwnerVerificationStatus::PENDING->value, ]); expect((float) $price->fresh()->stock)->toBe($initialStock); }); test('approving create purchase increments stock', function () { $user = createPurchaseUserWithPermission( PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE, ); $verifier = createPurchaseVerifierUser(); $supplier = Supplier::factory()->create(); $price = RawMaterialPrice::factory()->create(['stock' => 10]); $this->actingAs($user) ->postJson(route('admin.manage.purchases.draft_items.store'), [ 'raw_material_price_id' => $price->id, 'quantity' => 2, ]) ->assertOk(); $this->actingAs($user) ->post(route('admin.manage.purchases.store'), [ 'supplier_id' => $supplier->id, 'discount' => 0, 'shipping_cost' => 0, ]) ->assertRedirect(route('admin.manage.purchases.index')); approveLatestPurchaseVerificationRequest($verifier); expect((float) $price->fresh()->stock)->toBe(12.0); }); test('update purchase submits verification request only', function () { $user = createPurchaseUserWithPermission( PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE, PermissionEnum::PURCHASES_UPDATE, ); $verifier = createPurchaseVerifierUser(); $supplier = Supplier::factory()->create(); $price = RawMaterialPrice::factory()->create(['stock' => 10, 'price' => 50_000]); $otherPrice = RawMaterialPrice::factory()->create(['stock' => 5, 'price' => 30_000]); $this->actingAs($user) ->postJson(route('admin.manage.purchases.draft_items.store'), [ 'raw_material_price_id' => $price->id, 'quantity' => 2, ]) ->assertOk(); $this->actingAs($user) ->post(route('admin.manage.purchases.store'), [ 'supplier_id' => $supplier->id, 'discount' => 0, 'shipping_cost' => 0, ]) ->assertRedirect(route('admin.manage.purchases.index')); approveLatestPurchaseVerificationRequest($verifier); $purchase = Purchase::query()->latest()->firstOrFail(); $originalTotal = $purchase->total; $this->actingAs($user) ->put(route('admin.manage.purchases.update', $purchase), [ 'supplier_id' => $supplier->id, 'discount' => 1000, 'shipping_cost' => 0, 'notes' => 'Ubah belanja', 'items' => [ ['raw_material_price_id' => $otherPrice->id, 'quantity' => 1], ], ]) ->assertRedirect(route('admin.manage.purchases.index')); $this->assertDatabaseHas('owner_verification_requests', [ 'subject_type' => Purchase::class, 'subject_id' => $purchase->id, 'action' => OwnerVerificationAction::UPDATE->value, 'status' => OwnerVerificationStatus::PENDING->value, ]); expect($purchase->fresh()->total)->toBe($originalTotal); }); test('delete purchase submits verification request only', function () { $user = createPurchaseUserWithPermission( PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE, PermissionEnum::PURCHASES_DELETE, ); $verifier = createPurchaseVerifierUser(); $supplier = Supplier::factory()->create(); $price = RawMaterialPrice::factory()->create(['stock' => 10]); $this->actingAs($user) ->postJson(route('admin.manage.purchases.draft_items.store'), [ 'raw_material_price_id' => $price->id, 'quantity' => 2, ]) ->assertOk(); $this->actingAs($user) ->post(route('admin.manage.purchases.store'), [ 'supplier_id' => $supplier->id, 'discount' => 0, 'shipping_cost' => 0, ]) ->assertRedirect(route('admin.manage.purchases.index')); approveLatestPurchaseVerificationRequest($verifier); $purchase = Purchase::query()->latest()->firstOrFail(); $this->actingAs($user) ->delete(route('admin.manage.purchases.destroy', $purchase)) ->assertRedirect(route('admin.manage.purchases.index')); $this->assertDatabaseHas('owner_verification_requests', [ 'subject_type' => Purchase::class, 'subject_id' => $purchase->id, 'action' => OwnerVerificationAction::DELETE->value, 'status' => OwnerVerificationStatus::PENDING->value, ]); expect(Purchase::query()->whereKey($purchase->id)->exists())->toBeTrue(); expect((float) $price->fresh()->stock)->toBe(12.0); }); test('rejecting create purchase deletes purchase without changing stock', function () { $user = createPurchaseUserWithPermission( PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE, ); $verifier = createPurchaseVerifierUser(); $supplier = Supplier::factory()->create(); $price = RawMaterialPrice::factory()->create(['stock' => 10]); $this->actingAs($user) ->postJson(route('admin.manage.purchases.draft_items.store'), [ 'raw_material_price_id' => $price->id, 'quantity' => 2, ]) ->assertOk(); $this->actingAs($user) ->post(route('admin.manage.purchases.store'), [ 'supplier_id' => $supplier->id, 'discount' => 0, 'shipping_cost' => 0, ]) ->assertRedirect(route('admin.manage.purchases.index')); $purchase = Purchase::query()->latest()->firstOrFail(); $verificationRequest = OwnerVerificationRequest::query()->pending()->latest()->firstOrFail(); $this->actingAs($verifier) ->post(route('admin.manage.owner_verifications.reject_request', $verificationRequest), [ 'reason' => 'Tidak sesuai', ]) ->assertRedirect(); expect(Purchase::query()->whereKey($purchase->id)->exists())->toBeFalse(); expect(PurchaseItem::query()->where('purchase_id', $purchase->id)->exists())->toBeFalse(); expect((float) $price->fresh()->stock)->toBe(10.0); }); test('pending purchase blocks edit route', function () { $user = createPurchaseUserWithPermission( PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE, PermissionEnum::PURCHASES_UPDATE, ); $supplier = Supplier::factory()->create(); $price = RawMaterialPrice::factory()->create(['stock' => 10]); $this->actingAs($user) ->postJson(route('admin.manage.purchases.draft_items.store'), [ 'raw_material_price_id' => $price->id, 'quantity' => 2, ]) ->assertOk(); $this->actingAs($user) ->post(route('admin.manage.purchases.store'), [ 'supplier_id' => $supplier->id, 'discount' => 0, 'shipping_cost' => 0, ]) ->assertRedirect(route('admin.manage.purchases.index')); $purchase = Purchase::query()->latest()->firstOrFail(); $this->actingAs($user) ->get(route('admin.manage.purchases.edit', $purchase)) ->assertRedirect(route('admin.manage.purchases.index')); }); test('purchase index exposes pending verification state', function () { $user = createPurchaseUserWithPermission( PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE, ); $supplier = Supplier::factory()->create(); $price = RawMaterialPrice::factory()->create(['stock' => 10]); $this->actingAs($user) ->postJson(route('admin.manage.purchases.draft_items.store'), [ 'raw_material_price_id' => $price->id, 'quantity' => 2, ]) ->assertOk(); $this->actingAs($user) ->post(route('admin.manage.purchases.store'), [ 'supplier_id' => $supplier->id, 'discount' => 0, 'shipping_cost' => 0, ]) ->assertRedirect(route('admin.manage.purchases.index')); $purchase = Purchase::query()->latest()->firstOrFail(); $response = $this->actingAs($user) ->get(route('admin.manage.purchases.index')); $response->assertOk(); $response->assertInertia(fn ($page) => $page ->where('purchases.data.0.id', $purchase->id) ->where('purchases.data.0.has_pending_request', true) ->where('purchases.data.0.pending_request_action', OwnerVerificationAction::CREATE->value) ); }); });