feat: Loop all invoice to get spending

This commit is contained in:
brian-inspiren 2026-07-06 09:43:31 +08:00
parent 38fe43414f
commit e467c60288
2 changed files with 72 additions and 54 deletions

View File

@ -14,7 +14,7 @@ class UpdateLatestClientInvoicePaymentItemSpending extends Command
protected $signature = 'customer:update-latest-invoice-item-spending protected $signature = 'customer:update-latest-invoice-item-spending
{--dry-run : Calculate without saving changes}'; {--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 public function handle(GoogleAdsSpendService $spendService): int
{ {
@ -36,66 +36,76 @@ public function handle(GoogleAdsSpendService $spendService): int
&$failed, &$failed,
) { ) {
foreach ($clients as $client) { 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( ->whereHas(
'payment.invoice', 'payment.invoice',
fn (Builder $query) => $query->where('client_id', $client->id), fn (Builder $query) => $query->where('client_id', $client->id),
) )
->where(fn (Builder $query) => $this->eligibleItems($query)) ->where(fn (Builder $query) => $this->eligibleItems($query))
->latest('id') ->orderBy('id')
->first(); ->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)) { if ($endDate < $startDate) {
$skipped++; $skipped++;
$this->warn("Skipping client {$client->id}: missing eligible item/customer ID."); $this->warn("Skipping item {$item->id}: end date is before start date.");
continue; continue;
} }
$startDate = $item->start_date->format('Y-m-d'); try {
$endDate = $item->end_date?->format('Y-m-d') ?? $today; $spending = $spendService->forDateRange(
$client->customer_id,
$startDate,
$endDate,
);
if ($endDate < $startDate) { if (! $dryRun) {
$skipped++; $item->forceFill(['spending' => $spending])->save();
$this->warn("Skipping item {$item->id}: end date is before start date."); }
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 { $this->error("Failed item {$item->id}: {$exception->getMessage()}");
$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()}");
}
} }
}); });

View File

@ -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'); Carbon::setTestNow('2026-06-25 10:00:00');
BillingItemType::query()->insert([ BillingItemType::query()->insert([
@ -59,9 +59,17 @@ function createInvoiceItem(
$adsService = Mockery::mock(GoogleAdsService::class); $adsService = Mockery::mock(GoogleAdsService::class);
$adsService->shouldReceive('listCampaigns') $adsService->shouldReceive('listCampaigns')
->once() ->twice()
->with('1111111111') ->with('1111111111')
->andReturn([['id' => 101], ['id' => 102]]); ->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') $adsService->shouldReceive('listCampaignsMetricsById')
->once() ->once()
->with('1111111111', '101', '2026-06-01', '2026-06-25') ->with('1111111111', '101', '2026-06-01', '2026-06-25')
@ -82,10 +90,10 @@ function createInvoiceItem(
app()->instance(GoogleAdsService::class, $adsService); app()->instance(GoogleAdsService::class, $adsService);
$this->artisan('customer:update-latest-invoice-item-spending') $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(); ->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) $latestItem->fresh()->spending)->toBe(20.0)
->and((float) $otherTypeItem->fresh()->spending)->toBe(20.0) ->and((float) $otherTypeItem->fresh()->spending)->toBe(20.0)
->and((float) $secondClientItem->fresh()->spending)->toBe(5.5); ->and((float) $secondClientItem->fresh()->spending)->toBe(5.5);