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>
87 lines
2.8 KiB
PHP
87 lines
2.8 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Qr;
|
|
|
|
use App\Models\QrCode;
|
|
use App\Models\QrCodeBatch;
|
|
use App\Models\ServiceArea;
|
|
use App\Models\User;
|
|
use App\States\QrCode\Unassigned;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class BatchGenerator
|
|
{
|
|
public function __construct(private readonly QrSerialGenerator $serials) {}
|
|
|
|
/**
|
|
* Generate a new batch with $quantity codes in the unassigned state.
|
|
*/
|
|
public function generate(
|
|
int $quantity,
|
|
string $purpose,
|
|
?ServiceArea $targetArea = null,
|
|
?int $targetStoreId = null,
|
|
?User $createdBy = null,
|
|
?\DateTimeInterface $expiresAt = null,
|
|
?string $notes = null,
|
|
): QrCodeBatch {
|
|
$now = Carbon::now();
|
|
$areaCode = $targetArea?->code ?? 'PH';
|
|
$batchSeq = $this->nextBatchSeq($now);
|
|
$batchNumber = sprintf('B%s%04d', $now->format('ymd'), $batchSeq);
|
|
|
|
return DB::transaction(function () use (
|
|
$batchNumber, $quantity, $purpose, $targetArea, $targetStoreId,
|
|
$createdBy, $expiresAt, $notes, $now, $areaCode, $batchSeq,
|
|
) {
|
|
$batch = QrCodeBatch::create([
|
|
'batch_number' => $batchNumber,
|
|
'quantity' => $quantity,
|
|
'purpose' => $purpose,
|
|
'target_area_id' => $targetArea?->id,
|
|
'target_store_id' => $targetStoreId,
|
|
'created_by_admin_id' => $createdBy?->id,
|
|
'expires_at' => $expiresAt,
|
|
'notes' => $notes,
|
|
]);
|
|
|
|
$rows = [];
|
|
$tsNow = $now->toDateTimeString();
|
|
for ($i = 1; $i <= $quantity; $i++) {
|
|
$serial = $this->serials->build($areaCode, $now, $batchSeq, $i);
|
|
$rows[] = [
|
|
'serial' => $serial,
|
|
'barcode_value' => $serial,
|
|
'batch_id' => $batch->id,
|
|
'status' => Unassigned::$name,
|
|
'expires_at' => $expiresAt,
|
|
'created_at' => $tsNow,
|
|
'updated_at' => $tsNow,
|
|
];
|
|
|
|
// Bulk insert in chunks to keep memory bounded for big batches.
|
|
if (count($rows) >= 1000) {
|
|
QrCode::insert($rows);
|
|
$rows = [];
|
|
}
|
|
}
|
|
|
|
if (! empty($rows)) {
|
|
QrCode::insert($rows);
|
|
}
|
|
|
|
return $batch->fresh();
|
|
});
|
|
}
|
|
|
|
private function nextBatchSeq(Carbon $when): int
|
|
{
|
|
// Per-day batch sequence: count today's batches, +1.
|
|
$today = $when->copy()->startOfDay();
|
|
$tomorrow = $when->copy()->endOfDay();
|
|
|
|
return QrCodeBatch::whereBetween('created_at', [$today, $tomorrow])->count() + 1;
|
|
}
|
|
}
|