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>
99 lines
3.3 KiB
PHP
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);
|
|
}
|
|
}
|