283 lines
11 KiB
PHP
283 lines
11 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Api\V1\Store;
|
|
|
|
use App\Models\Household;
|
|
use App\Models\PartnerStore;
|
|
use App\Models\QrCode;
|
|
use App\Models\StoreInventory;
|
|
use App\Models\User;
|
|
use App\Services\Store\StoreOperations;
|
|
use Database\Seeders\RoleSeeder;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Laravel\Sanctum\Sanctum;
|
|
use Tests\TestCase;
|
|
|
|
class PartnerStoreTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private User $admin;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->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'));
|
|
}
|
|
}
|