diff --git a/app/Console/Commands/UpdateLatestClientInvoicePaymentItemSpending.php b/app/Console/Commands/UpdateLatestClientInvoicePaymentItemSpending.php index c05236b..a4f1889 100644 --- a/app/Console/Commands/UpdateLatestClientInvoicePaymentItemSpending.php +++ b/app/Console/Commands/UpdateLatestClientInvoicePaymentItemSpending.php @@ -14,7 +14,7 @@ class UpdateLatestClientInvoicePaymentItemSpending extends Command protected $signature = 'customer:update-latest-invoice-item-spending {--dry-run : Calculate without saving changes}'; - protected $description = 'Update spending on each client\'s latest billing type 1 invoice payment item.'; + protected $description = 'Update spending on all billing type 1 invoice payment items.'; public function handle(GoogleAdsSpendService $spendService): int { @@ -36,66 +36,76 @@ public function handle(GoogleAdsSpendService $spendService): int &$failed, ) { foreach ($clients as $client) { - $item = ClientInvoicePaymentItem::query() + if (empty($client->customer_id)) { + $skipped++; + $this->warn("Skipping client {$client->id}: missing customer ID."); + + continue; + } + + ClientInvoicePaymentItem::query() ->whereHas( 'payment.invoice', fn (Builder $query) => $query->where('client_id', $client->id), ) ->where(fn (Builder $query) => $this->eligibleItems($query)) - ->latest('id') - ->first(); + ->orderBy('id') + ->chunkById(50, function ($items) use ( + $client, + $spendService, + $dryRun, + $today, + &$updated, + &$skipped, + &$failed, + ) { + foreach ($items as $item) { + $startDate = $item->start_date->format('Y-m-d'); + $endDate = $item->end_date?->format('Y-m-d') ?? $today; - if ($item === null || empty($client->customer_id)) { - $skipped++; - $this->warn("Skipping client {$client->id}: missing eligible item/customer ID."); + if ($endDate < $startDate) { + $skipped++; + $this->warn("Skipping item {$item->id}: end date is before start date."); - continue; - } + continue; + } - $startDate = $item->start_date->format('Y-m-d'); - $endDate = $item->end_date?->format('Y-m-d') ?? $today; + try { + $spending = $spendService->forDateRange( + $client->customer_id, + $startDate, + $endDate, + ); - if ($endDate < $startDate) { - $skipped++; - $this->warn("Skipping item {$item->id}: end date is before start date."); + if (! $dryRun) { + $item->forceFill(['spending' => $spending])->save(); + } - continue; - } + $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 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(), + ]); - 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->error("Failed item {$item->id}: {$exception->getMessage()}"); + } + } + }); } }); diff --git a/tests/Feature/UpdateLatestClientInvoicePaymentItemSpendingTest.php b/tests/Feature/UpdateLatestClientInvoicePaymentItemSpendingTest.php index 7f8c074..f3effd1 100644 --- a/tests/Feature/UpdateLatestClientInvoicePaymentItemSpendingTest.php +++ b/tests/Feature/UpdateLatestClientInvoicePaymentItemSpendingTest.php @@ -33,7 +33,7 @@ function createInvoiceItem( ]); } -test('it updates only the latest eligible invoice payment item for each client', function () { +test('it updates every eligible invoice payment item for each client', function () { Carbon::setTestNow('2026-06-25 10:00:00'); BillingItemType::query()->insert([ @@ -59,9 +59,17 @@ function createInvoiceItem( $adsService = Mockery::mock(GoogleAdsService::class); $adsService->shouldReceive('listCampaigns') - ->once() + ->twice() ->with('1111111111') ->andReturn([['id' => 101], ['id' => 102]]); + $adsService->shouldReceive('listCampaignsMetricsById') + ->once() + ->with('1111111111', '101', '2026-05-01', '2026-05-31') + ->andReturn([['actual_spend' => 3.25]]); + $adsService->shouldReceive('listCampaignsMetricsById') + ->once() + ->with('1111111111', '102', '2026-05-01', '2026-05-31') + ->andReturn([['actual_spend' => 4.75]]); $adsService->shouldReceive('listCampaignsMetricsById') ->once() ->with('1111111111', '101', '2026-06-01', '2026-06-25') @@ -82,10 +90,10 @@ function createInvoiceItem( app()->instance(GoogleAdsService::class, $adsService); $this->artisan('customer:update-latest-invoice-item-spending') - ->expectsOutputToContain('Done. 2 calculated, 0 skipped, 0 failed.') + ->expectsOutputToContain('Done. 3 calculated, 0 skipped, 0 failed.') ->assertSuccessful(); - expect((float) $olderItem->fresh()->spending)->toBe(10.0) + expect((float) $olderItem->fresh()->spending)->toBe(8.0) ->and((float) $latestItem->fresh()->spending)->toBe(20.0) ->and((float) $otherTypeItem->fresh()->spending)->toBe(20.0) ->and((float) $secondClientItem->fresh()->spending)->toBe(5.5);