Enhance Tax Export Command with Comprehensive Invoice Processing and Metrics

- Improve invoice processing with advanced error handling and tracking
- Add detailed volume metrics for USD and EUR
- Implement robust country detection with multiple fallback strategies
- Enhance logging and progress tracking during tax export generation
- Add performance timing and more granular invoice exclusion reporting
This commit is contained in:
Julien Nahum 2025-03-10 17:25:15 +08:00
parent 8894880cb9
commit 06328a47ab
1 changed files with 161 additions and 18 deletions

View File

@ -64,6 +64,9 @@ class GenerateTaxExport extends Command
*/ */
public function handle() public function handle()
{ {
// Start the processing timer
$startTime = microtime(true);
// iterate through all Stripe invoices // iterate through all Stripe invoices
$startDate = $this->option('start-date'); $startDate = $this->option('start-date');
$endDate = $this->option('end-date'); $endDate = $this->option('end-date');
@ -95,14 +98,16 @@ class GenerateTaxExport extends Command
// Create a progress bar // Create a progress bar
$queryOptions = [ $queryOptions = [
'limit' => 100, 'limit' => 100, // Maximum allowed by Stripe API
'expand' => [ 'expand' => [
'data.customer', 'data.customer',
'data.customer.address', 'data.customer.address',
'data.customer.tax_ids', 'data.customer.tax_ids',
'data.payment_intent', 'data.payment_intent',
'data.payment_intent.payment_method', 'data.payment_intent.payment_method',
'data.charge.balance_transaction' 'data.charge.balance_transaction',
'data.automatic_tax',
'data.total_tax_amounts'
], ],
'status' => 'paid', 'status' => 'paid',
]; ];
@ -116,29 +121,89 @@ class GenerateTaxExport extends Command
$invoices = Cashier::stripe()->invoices->all($queryOptions); $invoices = Cashier::stripe()->invoices->all($queryOptions);
$bar = $this->output->createProgressBar(); $bar = $this->output->createProgressBar();
$bar->start(); $bar->start();
// Improved counters for better tracking
$paymentNotSuccessfulCount = 0; $paymentNotSuccessfulCount = 0;
$refundedInvoicesCount = 0;
$missingDataInvoicesCount = 0;
$totalInvoice = 0; $totalInvoice = 0;
$processedInvoiceCount = 0;
$defaultedToFranceCount = 0;
$totalResults = 0;
// Volume metrics
$grossVolumeUsd = 0;
$netVolumeUsd = 0;
$taxTotalUsd = 0;
$grossVolumeEur = 0;
$netVolumeEur = 0;
$taxTotalEur = 0;
do { do {
$batchSize = count($invoices->data);
$totalResults += $batchSize;
foreach ($invoices as $invoice) { foreach ($invoices as $invoice) {
// Ignore if payment was refunded $totalInvoice++;
// Ignore if payment was refunded or not successful
if (($invoice->payment_intent->status ?? null) !== 'succeeded') { if (($invoice->payment_intent->status ?? null) !== 'succeeded') {
$paymentNotSuccessfulCount++; $paymentNotSuccessfulCount++;
continue; continue;
} }
$processedInvoices[] = $this->formatInvoice($invoice); // Check if invoice was refunded
$totalInvoice++; if (isset($invoice->charge) && isset($invoice->charge->refunded) && $invoice->charge->refunded) {
$refundedInvoicesCount++;
continue;
}
try {
$formattedInvoice = $this->formatInvoice($invoice);
// Check if we defaulted to France
if ($formattedInvoice['cust_country'] === 'FR' && $formattedInvoice['_defaulted_to_fr'] === true) {
$defaultedToFranceCount++;
}
// Remove the internal tracking field
unset($formattedInvoice['_defaulted_to_fr']);
$processedInvoices[] = $formattedInvoice;
$processedInvoiceCount++;
// Track volume metrics
$grossVolumeUsd += $formattedInvoice['total_usd'];
$netVolumeUsd += $formattedInvoice['total_after_tax_usd'];
$taxTotalUsd += $formattedInvoice['tax_total_usd'];
$grossVolumeEur += $formattedInvoice['total_eur'];
$netVolumeEur += $formattedInvoice['total_after_tax_eur'];
$taxTotalEur += $formattedInvoice['tax_total_eur'];
} catch (\Exception $e) {
$this->warn("Error processing invoice {$invoice->id}: {$e->getMessage()}");
$missingDataInvoicesCount++;
continue;
}
// Advance the progress bar // Advance the progress bar
$bar->advance(); $bar->advance();
} }
$queryOptions['starting_after'] = end($invoices->data)->id; // Safe pagination
try {
$lastInvoice = end($invoices->data);
if ($lastInvoice) {
$queryOptions['starting_after'] = $lastInvoice->id;
} else {
break;
}
sleep(5); // No need for sleep - Stripe API can handle the request rate
$invoices = $invoices->all($queryOptions); $invoices = Cashier::stripe()->invoices->all($queryOptions);
} catch (\Exception $e) {
$this->error("Error fetching next batch of invoices: {$e->getMessage()}");
break;
}
} while ($invoices->has_more); } while ($invoices->has_more);
$bar->finish(); $bar->finish();
@ -152,8 +217,32 @@ class GenerateTaxExport extends Command
$aggregatedReportFilePath = 'opnform-tax-export-aggregated_' . $startDate . '_' . $endDate . '.xlsx'; $aggregatedReportFilePath = 'opnform-tax-export-aggregated_' . $startDate . '_' . $endDate . '.xlsx';
$this->exportAsXlsx($aggregatedReport, $aggregatedReportFilePath); $this->exportAsXlsx($aggregatedReport, $aggregatedReportFilePath);
// Display the results // Calculate processing time
$this->info('Total invoices: ' . $totalInvoice . ' (with ' . $paymentNotSuccessfulCount . ' payment not successful or trial free invoice)'); $endTime = microtime(true);
$executionTime = round($endTime - $startTime, 2);
// Display the results with improved statistics
$this->info('Processing completed in ' . $executionTime . ' seconds');
$this->info('Total invoices found: ' . $totalInvoice);
$this->info('Processed invoices: ' . $processedInvoiceCount);
$this->info('Excluded invoices:');
$this->info(' - Payment not successful: ' . $paymentNotSuccessfulCount);
$this->info(' - Refunded: ' . $refundedInvoicesCount);
$this->info(' - Missing required data: ' . $missingDataInvoicesCount);
$this->info(' - Defaulted to France: ' . $defaultedToFranceCount);
// Display volume metrics
$this->line('');
$this->info('Volume Metrics (USD):');
$this->info(' - Gross volume: $' . number_format($grossVolumeUsd, 2));
$this->info(' - Tax collected: $' . number_format($taxTotalUsd, 2));
$this->info(' - Net volume: $' . number_format($netVolumeUsd, 2));
$this->line('');
$this->info('Volume Metrics (EUR):');
$this->info(' - Gross volume: €' . number_format($grossVolumeEur, 2));
$this->info(' - Tax collected: €' . number_format($taxTotalEur, 2));
$this->info(' - Net volume: €' . number_format($netVolumeEur, 2));
return Command::SUCCESS; return Command::SUCCESS;
} }
@ -205,28 +294,82 @@ class GenerateTaxExport extends Command
private function formatInvoice(Invoice $invoice): array private function formatInvoice(Invoice $invoice): array
{ {
$country = $invoice->customer->address->country ?? $invoice->payment_intent->payment_method->card->country ?? null; // Enhanced country detection logic with multiple fallbacks
$country = null;
$taxLocationFound = false;
$defaultedToFrance = false;
// Try to get country from customer's billing address
if (isset($invoice->customer->address) && !empty($invoice->customer->address->country)) {
$country = $invoice->customer->address->country;
$taxLocationFound = true;
}
// Try to get country from payment method
elseif (
isset($invoice->payment_intent) && isset($invoice->payment_intent->payment_method) &&
isset($invoice->payment_intent->payment_method->card) &&
!empty($invoice->payment_intent->payment_method->card->country)
) {
$country = $invoice->payment_intent->payment_method->card->country;
$taxLocationFound = true;
}
// Try to get country from automatic tax calculation
elseif (
isset($invoice->automatic_tax) && isset($invoice->automatic_tax->tax_location) &&
!empty($invoice->automatic_tax->tax_location->country)
) {
$country = $invoice->automatic_tax->tax_location->country;
$taxLocationFound = true;
}
// Try to get country from tax breakdown
elseif (isset($invoice->total_tax_amounts) && !empty($invoice->total_tax_amounts->data)) {
foreach ($invoice->total_tax_amounts->data as $taxAmount) {
if (isset($taxAmount->tax_rate) && isset($taxAmount->tax_rate->country)) {
$country = $taxAmount->tax_rate->country;
$taxLocationFound = true;
break;
}
}
}
// Default to France if no country found
if (!$taxLocationFound || is_null($country) || empty($country)) {
$country = 'FR';
$defaultedToFrance = true;
}
$vatId = null;
if (isset($invoice->customer->tax_ids) && !empty($invoice->customer->tax_ids->data)) {
$vatId = $invoice->customer->tax_ids->data[0]->value ?? null; $vatId = $invoice->customer->tax_ids->data[0]->value ?? null;
}
$taxRate = $this->computeTaxRate($country, $vatId); $taxRate = $this->computeTaxRate($country, $vatId);
$taxAmountCollectedUsd = $taxRate > 0 ? $invoice->total * $taxRate / ($taxRate + 100) : 0; // Safely calculate tax amounts
$totalEur = $invoice->charge->balance_transaction->amount; $total = $invoice->total ?? 0;
$taxAmountCollectedUsd = $taxRate > 0 ? $total * $taxRate / ($taxRate + 100) : 0;
$totalEur = 0;
if (isset($invoice->charge) && isset($invoice->charge->balance_transaction)) {
$totalEur = $invoice->charge->balance_transaction->amount ?? 0;
}
$taxAmountCollectedEur = $taxRate > 0 ? $totalEur * $taxRate / ($taxRate + 100) : 0; $taxAmountCollectedEur = $taxRate > 0 ? $totalEur * $taxRate / ($taxRate + 100) : 0;
return [ return [
'invoice_id' => $invoice->id, 'invoice_id' => $invoice->id,
'created_at' => Carbon::createFromTimestamp($invoice->created)->format('Y-m-d H:i:s'), 'created_at' => Carbon::createFromTimestamp($invoice->created)->format('Y-m-d H:i:s'),
'cust_id' => $invoice->customer->id, 'cust_id' => $invoice->customer->id ?? 'unknown',
'cust_vat_id' => $vatId, 'cust_vat_id' => $vatId,
'cust_country' => $country, 'cust_country' => $country,
'tax_rate' => $taxRate, 'tax_rate' => $taxRate,
'total_usd' => $invoice->total / 100, 'total_usd' => $total / 100,
'tax_total_usd' => $taxAmountCollectedUsd / 100, 'tax_total_usd' => $taxAmountCollectedUsd / 100,
'total_after_tax_usd' => ($invoice->total - $taxAmountCollectedUsd) / 100, 'total_after_tax_usd' => ($total - $taxAmountCollectedUsd) / 100,
'total_eur' => $totalEur / 100, 'total_eur' => $totalEur / 100,
'tax_total_eur' => $taxAmountCollectedEur / 100, 'tax_total_eur' => $taxAmountCollectedEur / 100,
'total_after_tax_eur' => ($totalEur - $taxAmountCollectedEur) / 100, 'total_after_tax_eur' => ($totalEur - $taxAmountCollectedEur) / 100,
'_defaulted_to_fr' => $defaultedToFrance,
]; ];
} }