inspiren-sem-tool/app/Console/Commands/CalculateClientInvoicePaymentItemSpending.php

122 lines
4.3 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Models\ClientInvoicePaymentItem;
use App\Services\GoogleAdsService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class CalculateClientInvoicePaymentItemSpending extends Command
{
protected $signature = 'customer:calculate-invoice-item-spending {--dry-run : Calculate without saving changes}';
protected $description = 'Calculate client invoice payment item spending from Google Ads spend by item date range.';
public function handle(GoogleAdsService $adsService): int
{
$dryRun = (bool) $this->option('dry-run');
$spendCache = [];
$updated = 0;
$skipped = 0;
$failed = 0;
ClientInvoicePaymentItem::query()
->with('payment.invoice.client')
->whereNotNull('start_date')
->whereNotNull('end_date')
->orderBy('id')
->chunkById(50, function ($items) use ($adsService, $dryRun, &$spendCache, &$updated, &$skipped, &$failed) {
foreach ($items as $item) {
$client = $item->payment?->invoice?->client;
if ($client === null || empty($client->customer_id)) {
$skipped++;
$this->warn("Skipping item {$item->id}: missing client/customer ID.");
continue;
}
$startDate = $item->start_date?->format('Y-m-d');
$endDate = $item->end_date?->format('Y-m-d');
if ($startDate === null || $endDate === null) {
$skipped++;
continue;
}
$cacheKey = implode('|', [$client->customer_id, $startDate, $endDate]);
try {
if (! array_key_exists($cacheKey, $spendCache)) {
$spendCache[$cacheKey] = $this->spendForDateRange(
$adsService,
$client->customer_id,
$startDate,
$endDate,
);
}
$spending = $spendCache[$cacheKey];
if (! $dryRun) {
$item->forceFill([
'spending' => $spending,
])->save();
}
$updated++;
$this->line(sprintf(
'%s item %d: RM %.2f (%s to %s)',
$dryRun ? 'Calculated' : 'Updated',
$item->id,
$spending,
$startDate,
$endDate,
));
} catch (\Throwable $exception) {
$failed++;
Log::error('Unable to calculate invoice payment item spending.', [
'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 spendForDateRange(
GoogleAdsService $adsService,
string $customerId,
string $startDate,
string $endDate,
): float {
$campaigns = $adsService->listCampaigns($customerId);
$spending = 0.0;
foreach ($campaigns as $campaign) {
$metrics = $adsService->listCampaignsMetricsById(
$customerId,
(string) $campaign['id'],
$startDate,
$endDate,
);
$spending += array_sum(array_map(
fn (array $metric): float => (float) ($metric['actual_spend'] ?? 0),
$metrics,
));
}
return round($spending, 6);
}
}