feat: Loop all invoice to get spending
This commit is contained in:
parent
38fe43414f
commit
e467c60288
@ -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()}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user