option('dry-run'); $today = today()->toDateString(); $updated = 0; $skipped = 0; $failed = 0; Client::query() ->whereHas('invoices.payments.items', fn (Builder $query) => $this->eligibleItems($query)) ->orderBy('id') ->chunkById(50, function ($clients) use ( $spendService, $dryRun, $today, &$updated, &$skipped, &$failed, ) { foreach ($clients as $client) { $item = ClientInvoicePaymentItem::query() ->whereHas( 'payment.invoice', fn (Builder $query) => $query->where('client_id', $client->id), ) ->where(fn (Builder $query) => $this->eligibleItems($query)) ->latest('id') ->first(); if ($item === null || empty($client->customer_id)) { $skipped++; $this->warn("Skipping client {$client->id}: missing eligible item/customer ID."); continue; } $startDate = $item->start_date->format('Y-m-d'); $endDate = $item->end_date?->format('Y-m-d') ?? $today; if ($endDate < $startDate) { $skipped++; $this->warn("Skipping item {$item->id}: end date is before start date."); continue; } try { $spending = $spendService->forDateRange( $client->customer_id, $startDate, $endDate, ); if (! $dryRun) { $item->forceFill(['spending' => $spending])->save(); } $updated++; $this->line(sprintf( '%s client %d item %d: RM %.2f (%s to %s)', $dryRun ? 'Calculated' : 'Updated', $client->id, $item->id, $spending, $startDate, $endDate, )); } catch (\Throwable $exception) { $failed++; Log::error('Unable to update latest invoice payment item spending.', [ 'client_id' => $client->id, 'client_invoice_payment_item_id' => $item->id, 'customer_id' => $client->customer_id, 'start_date' => $startDate, 'end_date' => $endDate, 'message' => $exception->getMessage(), ]); $this->error("Failed item {$item->id}: {$exception->getMessage()}"); } } }); $this->info("Done. {$updated} calculated, {$skipped} skipped, {$failed} failed."); return $failed > 0 ? self::FAILURE : self::SUCCESS; } private function eligibleItems(Builder $query): Builder { return $query ->where('billing_item_types_id', 1) ->whereNotNull('start_date'); } }