Files
Verde-Web/app/Http/Controllers/Api/V1/Qr/MyQrCodeController.php
admin d458e74301 feat(backend): complete Module 7 — QR code system (core)
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>
2026-04-30 02:12:41 +08:00

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');
}
}