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(); } function getPurchaseItemsPayload(RawMaterialPrice $price, float $quantity = 2): array { $unit = $price->rawMaterial->unit; $unitVal = $unit instanceof \BackedEnum ? $unit->value : $unit; return [ [ 'raw_material_price_id' => $price->id, 'name' => $price->rawMaterial->name, 'unit' => $unitVal, 'variant' => $price->variant, 'quantity' => $quantity, 'price' => $price->price, ] ]; } 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', 'items' => getPurchaseItemsPayload($price, 2), ]) ->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, 'items' => getPurchaseItemsPayload($price, 2), ]) ->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, 'items' => getPurchaseItemsPayload($price, 2), ]) ->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' => getPurchaseItemsPayload($otherPrice, 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, 'items' => getPurchaseItemsPayload($price, 2), ]) ->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, 'items' => getPurchaseItemsPayload($price, 2), ]) ->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, 'items' => getPurchaseItemsPayload($price, 2), ]) ->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, 'items' => getPurchaseItemsPayload($price, 2), ]) ->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) ); }); }); // ─── Index ──────────────────────────────────────────────── describe('Purchase Index', function () { test('authenticated user with permission can view purchase index', function () { $user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW); $this->actingAs($user) ->get(route('admin.manage.purchases.index')) ->assertOk(); }); test('guest is redirected to login', function () { $this->get(route('admin.manage.purchases.index')) ->assertRedirect(route('login')); }); test('user without permission is forbidden', function () { $user = User::factory()->create(); $this->actingAs($user) ->get(route('admin.manage.purchases.index')) ->assertForbidden(); }); test('index can search purchases by supplier name', function () { $user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW); $supplier = Supplier::factory()->create(['name' => 'Supplier Kain']); $purchase = Purchase::factory()->create(['supplier_id' => $supplier->id, 'created_by_id' => $user->id]); $this->actingAs($user) ->get(route('admin.manage.purchases.index', ['search' => 'Kain'])) ->assertOk(); }); }); // ─── Create ─────────────────────────────────────────────── describe('Purchase Create', function () { test('authenticated user with permission can view create form', function () { $user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE); $this->actingAs($user) ->get(route('admin.manage.purchases.create')) ->assertOk(); }); test('guest is redirected to login', function () { $this->get(route('admin.manage.purchases.create')) ->assertRedirect(route('login')); }); test('user without create permission is forbidden', function () { $user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW); $this->actingAs($user) ->get(route('admin.manage.purchases.create')) ->assertForbidden(); }); }); // ─── Store Validation ───────────────────────────────────── describe('Purchase Store Validation', function () { test('supplier_id is required', function () { $user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE); $price = RawMaterialPrice::factory()->create(); $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' => '', 'discount' => 0, 'shipping_cost' => 0, 'items' => getPurchaseItemsPayload($price, 2), ]) ->assertSessionHasErrors('supplier_id'); }); test('supplier must exist', function () { $user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE); $price = RawMaterialPrice::factory()->create(); $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' => 99999, 'discount' => 0, 'shipping_cost' => 0, 'items' => getPurchaseItemsPayload($price, 2), ]) ->assertSessionHasErrors('supplier_id'); }); test('store fails without draft items', function () { $user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE); $supplier = Supplier::factory()->create(); $this->actingAs($user) ->post(route('admin.manage.purchases.store'), [ 'supplier_id' => $supplier->id, 'discount' => 0, 'shipping_cost' => 0, ]) ->assertSessionHasErrors('items'); }); }); // ─── Edit ───────────────────────────────────────────────── describe('Purchase Edit', function () { test('authenticated user with permission can view edit form', function () { $user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE, PermissionEnum::PURCHASES_UPDATE); $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, 'items' => getPurchaseItemsPayload($price, 2), ]); approveLatestPurchaseVerificationRequest($verifier); $purchase = Purchase::query()->latest()->firstOrFail(); $this->actingAs($user) ->get(route('admin.manage.purchases.edit', $purchase)) ->assertOk(); }); test('guest is redirected to login', function () { $purchase = Purchase::factory()->create(); $this->get(route('admin.manage.purchases.edit', $purchase)) ->assertRedirect(route('login')); }); test('user without update permission is forbidden', function () { $user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW); $purchase = Purchase::factory()->create(); $this->actingAs($user) ->get(route('admin.manage.purchases.edit', $purchase)) ->assertForbidden(); }); }); // ─── Update Validation ──────────────────────────────────── describe('Purchase Update Validation', function () { test('items is required on update', function () { $user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE, PermissionEnum::PURCHASES_UPDATE); $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, 'items' => getPurchaseItemsPayload($price, 2), ]); approveLatestPurchaseVerificationRequest($verifier); $purchase = Purchase::query()->latest()->firstOrFail(); $this->actingAs($user) ->put(route('admin.manage.purchases.update', $purchase), [ 'supplier_id' => $supplier->id, 'discount' => 0, 'shipping_cost' => 0, 'items' => [], ]) ->assertSessionHasErrors('items'); }); }); // ─── Destroy ────────────────────────────────────────────── describe('Purchase Destroy', function () { test('guest cannot delete a purchase', function () { $purchase = Purchase::factory()->create(); $this->delete(route('admin.manage.purchases.destroy', $purchase)) ->assertRedirect(route('login')); }); test('user without delete permission is forbidden', function () { $user = createPurchaseUserWithPermission(PermissionEnum::PURCHASES_VIEW); $purchase = Purchase::factory()->create(); $this->actingAs($user) ->delete(route('admin.manage.purchases.destroy', $purchase)) ->assertForbidden(); }); }); // ─── Owner Direct Actions ────────────────────────────────── describe('Owner Direct Actions', function () { test('owner can create purchase directly and increments stock', function () { $owner = createPurchaseUserWithPermission( PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE, PermissionEnum::OWNER_VERIFICATIONS_VERIFY, ); $supplier = Supplier::factory()->create(); $price = RawMaterialPrice::factory()->create(['stock' => 10]); $this->actingAs($owner) ->postJson(route('admin.manage.purchases.draft_items.store'), [ 'raw_material_price_id' => $price->id, 'quantity' => 3, ]) ->assertOk(); $this->actingAs($owner) ->post(route('admin.manage.purchases.store'), [ 'supplier_id' => $supplier->id, 'discount' => 0, 'shipping_cost' => 0, 'items' => getPurchaseItemsPayload($price, 3), ]) ->assertRedirect(route('admin.manage.purchases.index')); $purchase = Purchase::query()->latest()->first(); expect($purchase)->not->toBeNull(); $this->assertDatabaseMissing('owner_verification_requests', [ 'subject_id' => $purchase->id, 'subject_type' => Purchase::class, ]); expect((float) $price->fresh()->stock)->toBe(13.0); }); test('owner can delete purchase directly and decrements stock', function () { $owner = createPurchaseUserWithPermission( PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE, PermissionEnum::PURCHASES_DELETE, PermissionEnum::OWNER_VERIFICATIONS_VERIFY, ); $supplier = Supplier::factory()->create(); $price = RawMaterialPrice::factory()->create(['stock' => 10]); $this->actingAs($owner) ->postJson(route('admin.manage.purchases.draft_items.store'), [ 'raw_material_price_id' => $price->id, 'quantity' => 3, ]) ->assertOk(); $this->actingAs($owner) ->post(route('admin.manage.purchases.store'), [ 'supplier_id' => $supplier->id, 'discount' => 0, 'shipping_cost' => 0, 'items' => getPurchaseItemsPayload($price, 3), ]); expect((float) $price->fresh()->stock)->toBe(13.0); $purchase = Purchase::query()->latest()->firstOrFail(); $this->actingAs($owner) ->delete(route('admin.manage.purchases.destroy', $purchase)) ->assertRedirect(route('admin.manage.purchases.index')); expect(Purchase::query()->whereKey($purchase->id)->exists())->toBeFalse(); expect((float) $price->fresh()->stock)->toBe(10.0); }); }); // ─── Purchase Model ──────────────────────────────────────── describe('Purchase Model', function () { test('purchase uses soft deletes', function () { $purchase = Purchase::factory()->create(); $purchase->delete(); expect($purchase->trashed())->toBeTrue(); }); test('purchase has amounts cast to integer', function () { $purchase = Purchase::factory()->create([ 'subtotal' => 100000, 'discount' => 5000, 'shipping_cost' => 10000, 'total' => 105000, ]); expect($purchase->subtotal)->toBeInt(); expect($purchase->discount)->toBeInt(); expect($purchase->shipping_cost)->toBeInt(); expect($purchase->total)->toBeInt(); }); test('purchase has formatted amount accessors', function () { $purchase = Purchase::factory()->create([ 'subtotal' => 100000, 'discount' => 5000, 'shipping_cost' => 10000, 'total' => 105000, ]); expect($purchase->subtotal_formatted)->toBe('Rp 100.000'); expect($purchase->discount_formatted)->toBe('Rp 5.000'); expect($purchase->shipping_cost_formatted)->toBe('Rp 10.000'); expect($purchase->total_formatted)->toBe('Rp 105.000'); }); test('purchase belongs to supplier', function () { $supplier = Supplier::factory()->create(); $purchase = Purchase::factory()->create(['supplier_id' => $supplier->id]); expect($purchase->supplier)->not->toBeNull(); expect($purchase->supplier->id)->toBe($supplier->id); }); test('purchase belongs to created by user', function () { $user = User::factory()->create(); $purchase = Purchase::factory()->create(['created_by_id' => $user->id]); expect($purchase->createdBy)->not->toBeNull(); expect($purchase->createdBy->id)->toBe($user->id); }); test('purchase can have items', function () { $purchase = Purchase::factory()->create(); PurchaseItem::factory()->count(3)->create(['purchase_id' => $purchase->id]); expect($purchase->fresh()->items)->toHaveCount(3); }); }); // ─── PurchaseItem Model ──────────────────────────────────── describe('PurchaseItem Model', function () { test('purchase item has amounts cast to integer', function () { $purchase = Purchase::factory()->create(); $item = PurchaseItem::factory()->create([ 'purchase_id' => $purchase->id, 'unit_price' => 50000, 'subtotal' => 100000, ]); expect($item->unit_price)->toBeInt(); expect($item->subtotal)->toBeInt(); }); test('purchase item has formatted amount accessors', function () { $purchase = Purchase::factory()->create(); $item = PurchaseItem::factory()->create([ 'purchase_id' => $purchase->id, 'unit_price' => 50000, 'subtotal' => 100000, ]); expect($item->unit_price_formatted)->toBe('Rp 50.000'); expect($item->subtotal_formatted)->toBe('Rp 100.000'); }); test('purchase item belongs to purchase', function () { $purchase = Purchase::factory()->create(); $item = PurchaseItem::factory()->create(['purchase_id' => $purchase->id]); expect($item->purchase)->not->toBeNull(); expect($item->purchase->id)->toBe($purchase->id); }); test('purchase item uses soft deletes', function () { $purchase = Purchase::factory()->create(); $item = PurchaseItem::factory()->create(['purchase_id' => $purchase->id]); $item->delete(); expect($item->trashed())->toBeTrue(); }); }); describe('Purchase POS New Raw Material Form', function () { test('store new raw material adds draft item to cart and initializes stock to 0', function () { $user = createPurchaseUserWithPermission( PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE, ); $disk = config('filesystems.default') === 's3' ? 's3' : config('filesystems.default', 'public'); $key = 'raw-materials/'.\Illuminate\Support\Str::uuid().'.jpg'; $imageContent = \Illuminate\Http\UploadedFile::fake()->image("Standard.jpg", 100, 100)->get(); \Illuminate\Support\Facades\Storage::disk($disk)->put($key, $imageContent); $this->actingAs($user) ->postJson(route('admin.manage.purchases.draft_items.store_new_raw_material'), [ 'name' => 'Kain Toyobo Baru', 'unit' => 'yard', 'variant' => 'Standard', 'price' => 25000, 'quantity' => 10, 's3_keys' => [$key], ]) ->assertOk() ->assertJsonPath('item.raw_material_name', 'Kain Toyobo Baru') ->assertJsonPath('item.variant', 'Standard') ->assertJsonPath('item.unit_abbreviation', 'yard'); $this->assertDatabaseHas('raw_materials', [ 'name' => 'Kain Toyobo Baru', 'unit' => 'yard', ]); $this->assertDatabaseHas('raw_material_prices', [ 'variant' => 'Standard', 'price' => 25000, 'stock' => 0.0, ]); }); test('store new raw material fails if photo/s3_keys is missing', function () { $user = createPurchaseUserWithPermission( PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE, ); $this->actingAs($user) ->postJson(route('admin.manage.purchases.draft_items.store_new_raw_material'), [ 'name' => 'Kain Toyobo Baru', 'unit' => 'yard', 'variant' => 'Standard', 'price' => 25000, 'quantity' => 10, ]) ->assertStatus(422) ->assertJsonValidationErrors(['photos', 's3_keys']); }); test('store new raw material reuses existing raw material if name and unit match', function () { $user = createPurchaseUserWithPermission( PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE, ); $disk = config('filesystems.default') === 's3' ? 's3' : config('filesystems.default', 'public'); $key1 = 'raw-materials/'.\Illuminate\Support\Str::uuid().'.jpg'; $key2 = 'raw-materials/'.\Illuminate\Support\Str::uuid().'.jpg'; $imageContent = \Illuminate\Http\UploadedFile::fake()->image("Standard.jpg", 100, 100)->get(); \Illuminate\Support\Facades\Storage::disk($disk)->put($key1, $imageContent); \Illuminate\Support\Facades\Storage::disk($disk)->put($key2, $imageContent); // Add first variant $this->actingAs($user) ->postJson(route('admin.manage.purchases.draft_items.store_new_raw_material'), [ 'name' => 'Kain Toyobo Unik', 'unit' => 'yard', 'variant' => 'Standard', 'price' => 25000, 'quantity' => 10, 's3_keys' => [$key1], ]) ->assertOk(); // Add second variant with same name and unit $this->actingAs($user) ->postJson(route('admin.manage.purchases.draft_items.store_new_raw_material'), [ 'name' => 'Kain Toyobo Unik', 'unit' => 'yard', 'variant' => 'Premium', 'price' => 30000, 'quantity' => 5, 's3_keys' => [$key2], ]) ->assertOk(); // Check that raw_materials table only has 1 record for this name $rawMaterialsCount = \App\Models\RawMaterial::where('name', 'Kain Toyobo Unik')->count(); expect($rawMaterialsCount)->toBe(1); // Check that raw_material_prices has both variants for the same raw_material_id $rawMaterial = \App\Models\RawMaterial::where('name', 'Kain Toyobo Unik')->first(); $this->assertDatabaseHas('raw_material_prices', [ 'raw_material_id' => $rawMaterial->id, 'variant' => 'Standard', 'price' => 25000, ]); $this->assertDatabaseHas('raw_material_prices', [ 'raw_material_id' => $rawMaterial->id, 'variant' => 'Premium', 'price' => 30000, ]); }); test('store new raw material increments draft item quantity if same variant is added again', function () { $user = createPurchaseUserWithPermission( PermissionEnum::PURCHASES_VIEW, PermissionEnum::PURCHASES_CREATE, ); $disk = config('filesystems.default') === 's3' ? 's3' : config('filesystems.default', 'public'); $key = 'raw-materials/'.\Illuminate\Support\Str::uuid().'.jpg'; $imageContent = \Illuminate\Http\UploadedFile::fake()->image("Standard.jpg", 100, 100)->get(); \Illuminate\Support\Facades\Storage::disk($disk)->put($key, $imageContent); // Add standard variant first time (qty = 10) $this->actingAs($user) ->postJson(route('admin.manage.purchases.draft_items.store_new_raw_material'), [ 'name' => 'Kain Toyobo Sama', 'unit' => 'yard', 'variant' => 'Standard', 'price' => 25000, 'quantity' => 10, 's3_keys' => [$key], ]) ->assertOk() ->assertJsonPath('item.quantity', '10'); // Add same variant second time (qty = 5) $this->actingAs($user) ->postJson(route('admin.manage.purchases.draft_items.store_new_raw_material'), [ 'name' => 'Kain Toyobo Sama', 'unit' => 'yard', 'variant' => 'Standard', 'price' => 25000, 'quantity' => 5, 's3_keys' => [$key], ]) ->assertOk() ->assertJsonPath('item.quantity', '15'); // 10 + 5 }); });