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, $householdId, ) { $tenantId = null; if ($householdId) { $tenantId = Household::find($householdId)?->tenant_id; } elseif ($targetArea) { $tenantId = $targetArea->tenant_id; } elseif ($targetStoreId) { $tenantId = PartnerStore::find($targetStoreId)?->tenant_id; } if (empty($tenantId)) { $tenantId = Tenancy::current()?->id; } $batch = QrCodeBatch::create([ 'tenant_id' => $tenantId, 'batch_number' => $batchNumber, 'quantity' => $quantity, 'purpose' => $purpose, 'target_area_id' => $targetArea?->id, 'target_store_id' => $targetStoreId, 'household_id' => $householdId, 'created_by_admin_id' => $createdBy?->id, 'expires_at' => $expiresAt, 'notes' => $notes, ]); $rows = []; $tsNow = $now->toDateTimeString(); // Bulk insert bypasses model events, so we have to set tenant_id // explicitly. Inherit from the just-created batch (which goes // through the HasTenant trait) so any cross-tenant subtleties // bubble up early. $tenantId = $batch->tenant_id ?? Tenancy::current()?->id; for ($i = 1; $i <= $quantity; $i++) { $serial = $this->serials->build($areaCode, $now, $batchSeq, $i); $rows[] = [ 'tenant_id' => $tenantId, 'serial' => $serial, 'barcode_value' => $serial, 'batch_id' => $batch->id, 'status' => $householdId ? Active::$name : Unassigned::$name, 'assigned_to_household_id' => $householdId, 'allocated_at' => $householdId ? $tsNow : null, 'activated_at' => $householdId ? $tsNow : null, '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-month batch sequence to match the 'ym' format in QR serials $startOfMonth = $when->copy()->startOfMonth(); $endOfMonth = $when->copy()->endOfMonth(); return QrCodeBatch::withTrashed()->whereBetween('created_at', [$startOfMonth, $endOfMonth])->count() + 1; } }