seed(RoleSeeder::class); $this->admin = User::factory()->create(['role' => User::ROLE_ADMIN, 'status' => 'active']); } public function test_admin_can_create_partner_store(): void { Sanctum::actingAs($this->admin); $owner = User::factory()->create(['role' => User::ROLE_STORE_PARTNER]); $response = $this->postJson('/api/v1/admin/partner-stores', [ 'owner_user_id' => $owner->id, 'business_name' => 'Diliman Grocery', 'commission_rate_percent' => 15, 'status' => 'active', ]); $response->assertCreated() ->assertJsonPath('data.business_name', 'Diliman Grocery') ->assertJsonPath('data.commission_rate_percent', 15); } public function test_admin_can_issue_inventory_to_store(): void { Sanctum::actingAs($this->admin); $store = PartnerStore::factory()->create(['status' => 'active']); $response = $this->postJson("/api/v1/admin/partner-stores/{$store->uuid}/issue-inventory", [ 'quantity' => 50, 'wholesale_price_centavos' => 250000, ]); $response->assertCreated() ->assertJsonPath('data.quantity', 50) ->assertJsonPath('data.inventory_balance', 50); $allocated = QrCode::where('assigned_to_store_id', $store->id) ->where('status', 'allocated')->count(); $this->assertSame(50, $allocated); } public function test_inactive_store_cannot_receive_inventory(): void { Sanctum::actingAs($this->admin); $store = PartnerStore::factory()->create(['status' => 'pending_kyc']); $response = $this->postJson("/api/v1/admin/partner-stores/{$store->uuid}/issue-inventory", [ 'quantity' => 10, 'wholesale_price_centavos' => 50000, ]); $response->assertStatus(422); } public function test_admin_can_record_sale_and_codes_activate(): void { Sanctum::actingAs($this->admin); $store = PartnerStore::factory()->create(['status' => 'active', 'commission_rate_percent' => 10]); app(StoreOperations::class)->issueWholesale($store, 20, 100000); $household = Household::factory()->create(); $response = $this->postJson("/api/v1/admin/partner-stores/{$store->uuid}/sales", [ 'household_id' => $household->id, 'quantity' => 5, 'retail_price_per_code_centavos' => 1000, ]); $response->assertCreated() ->assertJsonPath('data.quantity', 5) ->assertJsonPath('data.retail_price_centavos', 5000) ->assertJsonPath('data.commission_centavos', 500); $active = QrCode::where('assigned_to_household_id', $household->id) ->where('status', 'active')->count(); $this->assertSame(5, $active); $balance = StoreInventory::where('store_id', $store->id)->value('current_code_balance'); $this->assertSame(15, (int) $balance); } public function test_oversold_request_fails(): void { Sanctum::actingAs($this->admin); $store = PartnerStore::factory()->create(['status' => 'active']); app(StoreOperations::class)->issueWholesale($store, 5, 25000); $household = Household::factory()->create(); $response = $this->postJson("/api/v1/admin/partner-stores/{$store->uuid}/sales", [ 'household_id' => $household->id, 'quantity' => 10, 'retail_price_per_code_centavos' => 1000, ]); $response->assertStatus(422); } public function test_sale_omitting_price_uses_global_srp(): void { Sanctum::actingAs($this->admin); $store = PartnerStore::factory()->create(['status' => 'active', 'commission_rate_percent' => 10]); app(StoreOperations::class)->issueWholesale($store, 10, 50000); $household = Household::factory()->create(); $response = $this->postJson("/api/v1/admin/partner-stores/{$store->uuid}/sales", [ 'household_id' => $household->id, 'quantity' => 5, ]); $response->assertCreated() ->assertJsonPath('data.quantity', 5) ->assertJsonPath('data.retail_price_centavos', 5000) ->assertJsonPath('data.commission_centavos', 500); } public function test_sale_with_incorrect_price_fails_validation(): void { Sanctum::actingAs($this->admin); $store = PartnerStore::factory()->create(['status' => 'active']); app(StoreOperations::class)->issueWholesale($store, 10, 50000); $household = Household::factory()->create(); $response = $this->postJson("/api/v1/admin/partner-stores/{$store->uuid}/sales", [ 'household_id' => $household->id, 'quantity' => 5, 'retail_price_per_code_centavos' => 1200, ]); $response->assertStatus(422) ->assertJsonValidationErrors(['retail_price_per_code_centavos']); } public function test_admin_can_view_analytics_and_history_endpoints(): void { Sanctum::actingAs($this->admin); $store = PartnerStore::factory()->create(['status' => 'active', 'commission_rate_percent' => 10]); // Issue wholesale (creates purchase) app(StoreOperations::class)->issueWholesale($store, 20, 100000); // Sell to household (creates sale) $household = Household::factory()->create(); app(StoreOperations::class)->sellToHousehold($store, $household, 5, 1000); // Test purchases endpoint $response = $this->getJson("/api/v1/admin/partner-stores/{$store->uuid}/purchases"); $response->assertOk() ->assertJsonStructure(['data', 'meta']) ->assertJsonCount(1, 'data') ->assertJsonPath('data.0.quantity', 20) ->assertJsonPath('data.0.wholesale_price_pesos', '1,000.00'); // Test sales-history endpoint $response = $this->getJson("/api/v1/admin/partner-stores/{$store->uuid}/sales-history"); $response->assertOk() ->assertJsonStructure(['data', 'meta']) ->assertJsonCount(1, 'data') ->assertJsonPath('data.0.quantity', 5) ->assertJsonPath('data.0.retail_price_pesos', '50.00') ->assertJsonPath('data.0.commission_pesos', '5.00'); // Test analytics endpoint $response = $this->getJson("/api/v1/admin/partner-stores/{$store->uuid}/analytics"); $response->assertOk() ->assertJson([ 'data' => [ 'total_wholesale_cost_pesos' => '1,000.00', 'total_retail_revenue_pesos' => '50.00', 'total_commission_pesos' => '5.00', 'total_settled_pesos' => '0.00', 'balance_due_pesos' => '45.00', 'total_issued_codes' => 20, 'total_sold_codes' => 5, ] ]); } public function test_admin_can_report_defective_qr_code(): void { Sanctum::actingAs($this->admin); $store = PartnerStore::factory()->create(['status' => 'active']); app(StoreOperations::class)->issueWholesale($store, 10, 50000); $qrCode = QrCode::where('assigned_to_store_id', $store->id)->first(); $response = $this->postJson("/api/v1/admin/partner-stores/{$store->uuid}/report-issue", [ 'serial' => $qrCode->serial, 'reason' => 'Unreadable barcode', ]); $response->assertOk() ->assertJsonPath('data.inventory_balance', 10); // 10 - 1 + 1 (replacement) = 10 $this->assertEquals('voided', (string) $qrCode->fresh()->status); $this->assertNull($qrCode->fresh()->assigned_to_store_id); $this->assertDatabaseHas('store_inventory_adjustments', [ 'store_id' => $store->id, 'type' => 'defective', 'quantity' => -1, 'qr_code_id' => $qrCode->id, ]); } public function test_admin_can_manually_adjust_inventory(): void { Sanctum::actingAs($this->admin); $store = PartnerStore::factory()->create(['status' => 'active']); app(StoreOperations::class)->issueWholesale($store, 10, 50000); $response = $this->postJson("/api/v1/admin/partner-stores/{$store->uuid}/adjust-stock", [ 'quantity' => 5, 'reason' => 'Physical audit correction', ]); $response->assertOk() ->assertJsonPath('data.inventory_balance', 15); $this->assertDatabaseHas('store_inventory_adjustments', [ 'store_id' => $store->id, 'type' => 'manual', 'quantity' => 5, 'reason' => 'Physical audit correction', 'adjusted_by_user_id' => $this->admin->id, ]); } public function test_admin_can_view_inventory_history_log(): void { Sanctum::actingAs($this->admin); $store = PartnerStore::factory()->create(['status' => 'active']); // 1. Issue (Purchase) app(StoreOperations::class)->issueWholesale($store, 50, 250000); // 2. Sale $household = Household::factory()->create(); app(StoreOperations::class)->sellToHousehold($store, $household, 5, 1000); // 3. Defective $qrCode = QrCode::where('assigned_to_store_id', $store->id)->first(); app(StoreOperations::class)->reportDefective($store, $qrCode->serial, 'Damaged', $this->admin); $response = $this->getJson("/api/v1/admin/partner-stores/{$store->uuid}/inventory-history"); $response->assertOk() ->assertJsonStructure(['data', 'meta']) ->assertJsonCount(4, 'data'); // Purchase, Sale, Defective, Replacement // Latest first (descending created_at) $this->assertEquals('manual', $response->json('data.0.type')); // Replacement $this->assertEquals(1, $response->json('data.0.quantity')); $this->assertEquals('defective', $response->json('data.1.type')); $this->assertEquals(-1, $response->json('data.1.quantity')); $this->assertEquals('sale', $response->json('data.2.type')); $this->assertEquals(-5, $response->json('data.2.quantity')); $this->assertEquals('purchase', $response->json('data.3.type')); $this->assertEquals(50, $response->json('data.3.quantity')); } }