qr_code_batches + qr_codes tables. State machine via
spatie/laravel-model-states (unassigned → allocated → active → used,
plus expired/voided). Serial format PH-{area}-{YYMM}-{batch}-{code}-
{checksum} with 2-char SHA-256-derived checksum. BatchGenerator
bulk-inserts in chunks. QrAllocator allocates free codes to verified
households (prefers area-targeted batch, falls back to any). Real
HouseholdVerified listener replaces the placeholder. QrBalanceLow
event for Module 11.
Admin: batch CRUD, mark-printed, void code, lifecycle search, PDF
print sheet (24/A4 via dompdf + endroid/qr-code + picqer/barcode).
Resident: list own codes, balance with low_balance flag, activate
allocated codes.
Removed explicit Event::listen for HouseholdVerified — Laravel 11
auto-discovers it via handle() type-hint, and double-registering was
causing double-dispatch.
130 feature tests passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
95 lines
3.2 KiB
PHP
95 lines
3.2 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\V1\Qr;
|
|
|
|
use App\Http\Controllers\Api\V1\ApiController;
|
|
use App\Http\Resources\QrCodeResource;
|
|
use App\Models\Household;
|
|
use App\Models\QrCode;
|
|
use App\Services\Qr\QrAllocator;
|
|
use App\States\QrCode\Active;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
|
|
class MyQrCodeController extends ApiController
|
|
{
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$request->validate([
|
|
'status' => ['nullable', 'in:unassigned,allocated,active,used,expired,voided'],
|
|
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
|
]);
|
|
|
|
$household = Household::where('head_user_id', $request->user()->id)->first();
|
|
if (! $household) {
|
|
return $this->ok([], 'No household yet');
|
|
}
|
|
|
|
$perPage = (int) $request->input('per_page', 50);
|
|
|
|
$codes = QrCode::query()
|
|
->where('assigned_to_household_id', $household->id)
|
|
->when($request->filled('status'), fn ($q) => $q->where('status', $request->string('status')))
|
|
->orderByDesc('id')
|
|
->paginate($perPage);
|
|
|
|
return $this->ok(
|
|
QrCodeResource::collection($codes),
|
|
null,
|
|
[
|
|
'page' => $codes->currentPage(),
|
|
'per_page' => $codes->perPage(),
|
|
'total' => $codes->total(),
|
|
],
|
|
);
|
|
}
|
|
|
|
public function balance(Request $request): JsonResponse
|
|
{
|
|
$household = Household::where('head_user_id', $request->user()->id)->first();
|
|
if (! $household) {
|
|
return $this->ok([
|
|
'active' => 0, 'allocated' => 0, 'used' => 0, 'expired' => 0,
|
|
'low_balance' => false,
|
|
]);
|
|
}
|
|
|
|
$counts = QrCode::query()
|
|
->where('assigned_to_household_id', $household->id)
|
|
->selectRaw('status, COUNT(*) as count')
|
|
->groupBy('status')
|
|
->pluck('count', 'status')
|
|
->all();
|
|
|
|
$active = (int) ($counts['active'] ?? 0);
|
|
$threshold = (int) config('qr.low_balance_threshold', 5);
|
|
|
|
return $this->ok([
|
|
'active' => $active,
|
|
'allocated' => (int) ($counts['allocated'] ?? 0),
|
|
'used' => (int) ($counts['used'] ?? 0),
|
|
'expired' => (int) ($counts['expired'] ?? 0),
|
|
'low_balance' => $active <= $threshold,
|
|
'low_balance_threshold' => $threshold,
|
|
]);
|
|
}
|
|
|
|
public function activate(Request $request, QrCode $qrCode, QrAllocator $allocator): JsonResponse
|
|
{
|
|
$household = Household::where('head_user_id', $request->user()->id)->first();
|
|
if (! $household || $qrCode->assigned_to_household_id !== $household->id) {
|
|
return $this->forbidden('This code is not assigned to your household');
|
|
}
|
|
|
|
if ($qrCode->status instanceof Active) {
|
|
return $this->ok(new QrCodeResource($qrCode), 'Code already active');
|
|
}
|
|
|
|
if (! $allocator->activate($qrCode)) {
|
|
return $this->fail('Code cannot be activated from its current state', null, 422);
|
|
}
|
|
|
|
return $this->ok(new QrCodeResource($qrCode->fresh()), 'Code activated');
|
|
}
|
|
}
|