Files
Verde-Web/tests/Feature/Api/V1/Payment/PaymentFlowTest.php
admin 15d56d0e98 feat(backend): finish Module 13 sub-modules + flow fixes
Notifications: notification_preferences + Laravel notifications inbox.
SmsChannel adapter for our SmsService. RoutesByPreferences trait reads
per-user toggles. HouseholdApproved/Rejected, QrBalanceLow, and
CodesPurchased notifications wired in via auto-discovered listeners
or direct dispatch from controllers/StoreOperations.

Payments: payments table + PaymentDriver interface. ManualPaymentDriver
works out of the box; PayMongoDriver activates when
PAYMONGO_SECRET_KEY is set, falls back to manual otherwise. Resident
initiates code-purchase, admin can mark paid manually, webhook applies
real provider events. Fulfillment runs StoreOperations::sellToHousehold.

Live tracking (HTTP polling): truck_location_history (with SPATIAL
INDEX + 7-day retention plan). Driver POST /driver/trucks/{uuid}/location
writes history, updates trucks.last_known_coordinates, caches in Redis,
flags geofence-trigger when entering active trip dumpsite. Admin
GET /admin/live/trucks returns active truck positions. Reverb broadcast
deferred.

Flow corrections:
- QrAllocator now idempotent — re-approving a household no longer
  re-dispenses free codes.
- arrive-dumpsite enforces dumpsite geofence via ST_Contains; can be
  bypassed with override_geofence: true.

171 feature tests passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:01:38 +08:00

99 lines
3.3 KiB
PHP

<?php
namespace Tests\Feature\Api\V1\Payment;
use App\Models\Household;
use App\Models\PartnerStore;
use App\Models\Payment;
use App\Models\QrCode;
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 PaymentFlowTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->seed(RoleSeeder::class);
}
public function test_resident_initiates_a_code_purchase_payment(): void
{
$resident = User::factory()->create(['role' => User::ROLE_RESIDENT, 'status' => 'active']);
Household::factory()->create(['head_user_id' => $resident->id]);
$store = PartnerStore::factory()->create(['status' => 'active']);
Sanctum::actingAs($resident);
$response = $this->postJson('/api/v1/me/payments/code-purchase', [
'store_id' => $store->uuid,
'quantity' => 5,
'retail_price_per_code_centavos' => 1000,
]);
$response->assertCreated()
->assertJsonPath('data.amount_centavos', 5000);
$this->assertDatabaseHas('payments', [
'payer_user_id' => $resident->id,
'amount_centavos' => 5000,
'status' => 'pending',
]);
}
public function test_admin_marking_paid_fulfills_the_purchase(): void
{
$admin = User::factory()->create(['role' => User::ROLE_ADMIN, 'status' => 'active']);
$resident = User::factory()->create(['role' => User::ROLE_RESIDENT, 'status' => 'active']);
$household = Household::factory()->create(['head_user_id' => $resident->id]);
$store = PartnerStore::factory()->create(['status' => 'active']);
app(StoreOperations::class)->issueWholesale($store, 20, 100000);
$payment = Payment::create([
'payer_user_id' => $resident->id,
'purpose' => Payment::PURPOSE_RESIDENT,
'amount_centavos' => 5000,
'status' => 'pending',
'metadata' => [
'store_id' => $store->id,
'household_id' => $household->id,
'quantity' => 5,
'retail_price_per_code_centavos' => 1000,
],
]);
Sanctum::actingAs($admin);
$response = $this->postJson("/api/v1/admin/live/payments/{$payment->uuid}/mark-paid");
$response->assertOk()->assertJsonPath('data.status', 'paid');
$active = QrCode::where('assigned_to_household_id', $household->id)
->where('status', 'active')->count();
$this->assertSame(5, $active);
}
public function test_resident_without_household_cannot_buy(): void
{
$resident = User::factory()->create(['role' => User::ROLE_RESIDENT, 'status' => 'active']);
$store = PartnerStore::factory()->create(['status' => 'active']);
Sanctum::actingAs($resident);
$this->postJson('/api/v1/me/payments/code-purchase', [
'store_id' => $store->uuid,
'quantity' => 1,
'retail_price_per_code_centavos' => 1000,
])->assertStatus(422);
}
public function test_paymongo_webhook_rejects_invalid_signature(): void
{
$this->postJson('/api/v1/webhooks/paymongo', ['data' => []])
->assertStatus(400);
}
}