seed(RolePermissionSeeder::class); }); function createRestockUserWithPermission(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 createRestockOwnerUser(PermissionEnum ...$permissions): User { $user = User::factory()->create(); $user->givePermissionTo( array_merge( [ PermissionEnum::DASHBOARD_VIEW->value, PermissionEnum::OWNER_VERIFICATIONS_VERIFY->value, ], array_map(fn (PermissionEnum $p) => $p->value, $permissions) ) ); $user->forgetCachedPermissions(); return $user; } function createRestockVerifierUser(): 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 approveLatestRestockVerificationRequest(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 getRestockItemsPayload(ProductVariant $variant, int $quantity = 2, int $unitPrice = 50000): array { return [ [ 'product_variant_id' => $variant->id, 'quantity' => $quantity, 'unit_price' => $unitPrice, ], ]; } describe('Restock CRUD', function () { test('owner can create restock and stock is incremented immediately', function () { $user = createRestockOwnerUser( PermissionEnum::RESTOCKS_VIEW, PermissionEnum::RESTOCKS_CREATE, ); $variant = ProductVariant::factory()->create(['stock' => 10]); $initialStock = $variant->stock; $this->actingAs($user) ->post(route('admin.manage.restocks.store'), [ 'notes' => 'Restock test', 'stock_type' => 'good', 'items' => getRestockItemsPayload($variant, 3), ]) ->assertRedirect(route('admin.manage.restocks.index')); $restock = Restock::query()->latest()->first(); expect($restock)->not->toBeNull(); expect($restock->notes)->toBe('Restock test'); expect($restock->stock_type)->toBe(ProductStockQuality::GOOD); expect($variant->fresh()->stock)->toBe($initialStock + 3); }); test('owner can create restock with retail stock type', function () { $user = createRestockOwnerUser( PermissionEnum::RESTOCKS_VIEW, PermissionEnum::RESTOCKS_CREATE, ); $variant = ProductVariant::factory()->create(['retail_stock' => 5]); $this->actingAs($user) ->post(route('admin.manage.restocks.store'), [ 'notes' => 'Restock eceran', 'stock_type' => 'retail', 'items' => getRestockItemsPayload($variant, 2), ]) ->assertRedirect(route('admin.manage.restocks.index')); expect($variant->fresh()->retail_stock)->toBe(7); }); test('owner can create restock with reject stock type', function () { $user = createRestockOwnerUser( PermissionEnum::RESTOCKS_VIEW, PermissionEnum::RESTOCKS_CREATE, ); $variant = ProductVariant::factory()->create(['reject_stock' => 3]); $this->actingAs($user) ->post(route('admin.manage.restocks.store'), [ 'notes' => 'Restock reject', 'stock_type' => 'reject', 'items' => getRestockItemsPayload($variant, 4), ]) ->assertRedirect(route('admin.manage.restocks.index')); expect($variant->fresh()->reject_stock)->toBe(7); }); test('owner can delete restock and stock is decremented', function () { $user = createRestockOwnerUser( PermissionEnum::RESTOCKS_VIEW, PermissionEnum::RESTOCKS_CREATE, PermissionEnum::RESTOCKS_DELETE, ); $variant = ProductVariant::factory()->create(['stock' => 10]); $this->actingAs($user) ->post(route('admin.manage.restocks.store'), [ 'stock_type' => 'good', 'items' => getRestockItemsPayload($variant, 3), ]) ->assertRedirect(); expect($variant->fresh()->stock)->toBe(13); $restock = Restock::query()->latest()->first(); $this->actingAs($user) ->delete(route('admin.manage.restocks.destroy', $restock)) ->assertRedirect(); expect($variant->fresh()->stock)->toBe(10); }); test('index page returns ok', function () { $user = createRestockOwnerUser(PermissionEnum::RESTOCKS_VIEW); $this->actingAs($user) ->get(route('admin.manage.restocks.index')) ->assertOk(); }); test('create page returns ok', function () { $user = createRestockOwnerUser( PermissionEnum::RESTOCKS_VIEW, PermissionEnum::RESTOCKS_CREATE, ); $this->actingAs($user) ->get(route('admin.manage.restocks.create')) ->assertOk(); }); test('edit page returns ok', function () { $user = createRestockOwnerUser( PermissionEnum::RESTOCKS_VIEW, PermissionEnum::RESTOCKS_CREATE, PermissionEnum::RESTOCKS_UPDATE, ); $variant = ProductVariant::factory()->create(); $this->actingAs($user) ->post(route('admin.manage.restocks.store'), [ 'stock_type' => 'good', 'items' => getRestockItemsPayload($variant), ]) ->assertRedirect(); $restock = Restock::query()->latest()->first(); $this->actingAs($user) ->get(route('admin.manage.restocks.edit', $restock)) ->assertOk(); }); }); describe('Restock Owner Verification', function () { test('non-owner create restock submits verification without changing stock', function () { $user = createRestockUserWithPermission( PermissionEnum::RESTOCKS_VIEW, PermissionEnum::RESTOCKS_CREATE, ); $variant = ProductVariant::factory()->create(['stock' => 10]); $initialStock = $variant->stock; $this->actingAs($user) ->post(route('admin.manage.restocks.store'), [ 'stock_type' => 'good', 'items' => getRestockItemsPayload($variant, 3), ]) ->assertRedirect(route('admin.manage.restocks.index')); $restock = Restock::query()->latest()->first(); expect($restock)->not->toBeNull(); $this->assertDatabaseHas('owner_verification_requests', [ 'subject_type' => Restock::class, 'subject_id' => $restock->id, 'action' => OwnerVerificationAction::CREATE->value, 'status' => OwnerVerificationStatus::PENDING->value, ]); expect($variant->fresh()->stock)->toBe($initialStock); }); test('approving create restock increments stock', function () { $user = createRestockUserWithPermission( PermissionEnum::RESTOCKS_VIEW, PermissionEnum::RESTOCKS_CREATE, ); $verifier = createRestockVerifierUser(); $variant = ProductVariant::factory()->create(['stock' => 10]); $this->actingAs($user) ->post(route('admin.manage.restocks.store'), [ 'stock_type' => 'good', 'items' => getRestockItemsPayload($variant, 3), ]) ->assertRedirect(); approveLatestRestockVerificationRequest($verifier); expect($variant->fresh()->stock)->toBe(13); }); test('rejecting create restock does not change stock and deletes restock', function () { $user = createRestockUserWithPermission( PermissionEnum::RESTOCKS_VIEW, PermissionEnum::RESTOCKS_CREATE, ); $verifier = createRestockVerifierUser(); $variant = ProductVariant::factory()->create(['stock' => 10]); $this->actingAs($user) ->post(route('admin.manage.restocks.store'), [ 'stock_type' => 'good', 'items' => getRestockItemsPayload($variant, 3), ]) ->assertRedirect(); $restock = Restock::query()->latest()->first(); $restockId = $restock->id; $verificationRequest = OwnerVerificationRequest::query()->pending()->latest()->firstOrFail(); $this->actingAs($verifier) ->post(route('admin.manage.owner_verifications.reject_request', $verificationRequest)) ->assertRedirect(); expect($variant->fresh()->stock)->toBe(10); expect(Restock::find($restockId))->toBeNull(); expect(Restock::withTrashed()->find($restockId))->not->toBeNull(); }); test('non-owner delete restock submits verification request', function () { $user = createRestockUserWithPermission( PermissionEnum::RESTOCKS_VIEW, PermissionEnum::RESTOCKS_CREATE, PermissionEnum::RESTOCKS_DELETE, ); $verifier = createRestockVerifierUser(); $variant = ProductVariant::factory()->create(['stock' => 10]); $this->actingAs($user) ->post(route('admin.manage.restocks.store'), [ 'stock_type' => 'good', 'items' => getRestockItemsPayload($variant, 3), ]) ->assertRedirect(); $restock = Restock::query()->latest()->first(); approveLatestRestockVerificationRequest($verifier); expect($variant->fresh()->stock)->toBe(13); $this->actingAs($user) ->delete(route('admin.manage.restocks.destroy', $restock)) ->assertRedirect(); $this->assertDatabaseHas('owner_verification_requests', [ 'subject_type' => Restock::class, 'subject_id' => $restock->id, 'action' => OwnerVerificationAction::DELETE->value, 'status' => OwnerVerificationStatus::PENDING->value, ]); expect($variant->fresh()->stock)->toBe(13); }); test('approving delete restock decrements stock', function () { $user = createRestockUserWithPermission( PermissionEnum::RESTOCKS_VIEW, PermissionEnum::RESTOCKS_CREATE, PermissionEnum::RESTOCKS_DELETE, ); $verifier = createRestockVerifierUser(); $variant = ProductVariant::factory()->create(['stock' => 10]); $this->actingAs($user) ->post(route('admin.manage.restocks.store'), [ 'stock_type' => 'good', 'items' => getRestockItemsPayload($variant, 3), ]) ->assertRedirect(); $restock = Restock::query()->latest()->first(); approveLatestRestockVerificationRequest($verifier); expect($variant->fresh()->stock)->toBe(13); $this->actingAs($user) ->delete(route('admin.manage.restocks.destroy', $restock)) ->assertRedirect(); approveLatestRestockVerificationRequest($verifier); expect($variant->fresh()->stock)->toBe(10); }); test('non-owner update restock submits verification request', function () { $user = createRestockUserWithPermission( PermissionEnum::RESTOCKS_VIEW, PermissionEnum::RESTOCKS_CREATE, PermissionEnum::RESTOCKS_UPDATE, ); $verifier = createRestockVerifierUser(); $variant = ProductVariant::factory()->create(['stock' => 10]); $otherVariant = ProductVariant::factory()->create(['stock' => 5]); $this->actingAs($user) ->post(route('admin.manage.restocks.store'), [ 'stock_type' => 'good', 'items' => getRestockItemsPayload($variant, 2), ]) ->assertRedirect(); $restock = Restock::query()->latest()->first(); approveLatestRestockVerificationRequest($verifier); expect($variant->fresh()->stock)->toBe(12); $this->actingAs($user) ->put(route('admin.manage.restocks.update', $restock), [ 'stock_type' => 'good', 'items' => getRestockItemsPayload($otherVariant, 4), ]) ->assertRedirect(); $this->assertDatabaseHas('owner_verification_requests', [ 'subject_type' => Restock::class, 'subject_id' => $restock->id, 'action' => OwnerVerificationAction::UPDATE->value, 'status' => OwnerVerificationStatus::PENDING->value, ]); }); }); describe('Restock Draft Items', function () { test('draft item can be created and deleted', function () { $user = createRestockOwnerUser( PermissionEnum::RESTOCKS_VIEW, PermissionEnum::RESTOCKS_CREATE, ); $variant = ProductVariant::factory()->create(); $this->actingAs($user) ->postJson(route('admin.manage.restocks.draft_items.store'), [ 'product_variant_id' => $variant->id, 'quantity' => 5, 'unit_price' => 50000, ]) ->assertOk(); $this->assertDatabaseHas('restock_items', [ 'user_id' => $user->id, 'product_variant_id' => $variant->id, 'quantity' => 5, 'unit_price' => 50000, 'restock_id' => null, ]); $itemId = RestockItem::where('user_id', $user->id) ->where('product_variant_id', $variant->id) ->whereNull('restock_id') ->value('id'); $this->actingAs($user) ->deleteJson(route('admin.manage.restocks.draft_items.destroy', ['productVariant' => $variant->id])) ->assertOk(); $this->assertDatabaseMissing('restock_items', ['id' => $itemId]); }); test('draft items are cleared when opening create page', function () { $user = createRestockOwnerUser( PermissionEnum::RESTOCKS_VIEW, PermissionEnum::RESTOCKS_CREATE, ); $variant = ProductVariant::factory()->create(); $this->actingAs($user) ->postJson(route('admin.manage.restocks.draft_items.store'), [ 'product_variant_id' => $variant->id, 'quantity' => 5, 'unit_price' => 50000, ]) ->assertOk(); $itemId = RestockItem::where('user_id', $user->id) ->where('product_variant_id', $variant->id) ->whereNull('restock_id') ->value('id'); expect($itemId)->not->toBeNull(); $this->actingAs($user) ->get(route('admin.manage.restocks.create')) ->assertOk(); expect(RestockItem::find($itemId))->toBeNull(); }); });