20, 'BE' => 21, 'BG' => 20, 'HR' => 25, 'CY' => 19, 'CZ' => 21, 'DK' => 25, 'EE' => 20, 'FI' => 24, 'FR' => 20, 'DE' => 19, 'GR' => 24, 'HU' => 27, 'IE' => 23, 'IT' => 22, 'LV' => 21, 'LT' => 21, 'LU' => 17, 'MT' => 18, 'NL' => 21, 'PL' => 23, 'PT' => 23, 'RO' => 19, 'SK' => 20, 'SI' => 22, 'ES' => 21, 'SE' => 25, ]; /** * Execute the console command. * * @return int */ public function handle() { // iterate through all Stripe invoices $startDate = $this->option('start-date'); $endDate = $this->option('end-date'); // Validate the date format if ($startDate && ! Carbon::createFromFormat('Y-m-d', $startDate)) { $this->error('Invalid start date format. Use YYYY-MM-DD.'); return Command::FAILURE; } if ($endDate && ! Carbon::createFromFormat('Y-m-d', $endDate)) { $this->error('Invalid end date format. Use YYYY-MM-DD.'); return Command::FAILURE; } elseif (! $endDate && $this->option('full-month')) { $endDate = Carbon::parse($startDate)->endOfMonth()->endOfDay()->format('Y-m-d'); } $this->info('Start date: '.$startDate); $this->info('End date: '.$endDate); $processedInvoices = []; // Create a progress bar $queryOptions = [ 'limit' => 100, 'expand' => ['data.customer', 'data.customer.address', 'data.customer.tax_ids', 'data.payment_intent', 'data.payment_intent.payment_method', 'data.charge.balance_transaction'], 'status' => 'paid', ]; if ($startDate) { $queryOptions['created']['gte'] = Carbon::parse($startDate)->startOfDay()->timestamp; } if ($endDate) { $queryOptions['created']['lte'] = Carbon::parse($endDate)->endOfDay()->timestamp; } $invoices = Cashier::stripe()->invoices->all($queryOptions); $bar = $this->output->createProgressBar(); $bar->start(); $paymentNotSuccessfulCount = 0; $totalInvoice = 0; do { foreach ($invoices as $invoice) { // Ignore if payment was refunded if (($invoice->payment_intent->status ?? null) !== 'succeeded') { $paymentNotSuccessfulCount++; continue; } $processedInvoices[] = $this->formatInvoice($invoice); $totalInvoice++; // Advance the progress bar $bar->advance(); } $queryOptions['starting_after'] = end($invoices->data)->id; sleep(5); $invoices = $invoices->all($queryOptions); } while ($invoices->has_more); $bar->finish(); $this->line(''); $aggregatedReport = $this->aggregateReport($processedInvoices); $filePath = 'opnform-tax-export-per-invoice_'.$startDate.'_'.$endDate.'.xlsx'; $this->exportAsXlsx($processedInvoices, $filePath); $aggregatedReportFilePath = 'opnform-tax-export-aggregated_'.$startDate.'_'.$endDate.'.xlsx'; $this->exportAsXlsx($aggregatedReport, $aggregatedReportFilePath); // Display the results $this->info('Total invoices: '.$totalInvoice.' (with '.$paymentNotSuccessfulCount.' payment not successful or trial free invoice)'); return Command::SUCCESS; } private function aggregateReport($invoices): array { // Sum invoices per country $aggregatedReport = []; foreach ($invoices as $invoice) { $country = $invoice['cust_country']; $customerType = is_null($invoice['cust_vat_id']) && $this->isEuropeanCountry($country) ? 'individual' : 'business'; if (! isset($aggregatedReport[$country])) { $defaultVal = [ 'count' => 0, 'total_usd' => 0, 'tax_total_usd' => 0, 'total_after_tax_usd' => 0, 'total_eur' => 0, 'tax_total_eur' => 0, 'total_after_tax_eur' => 0, ]; $aggregatedReport[$country] = [ 'individual' => $defaultVal, 'business' => $defaultVal, ]; } $aggregatedReport[$country][$customerType]['count']++; $aggregatedReport[$country][$customerType]['total_usd'] = ($aggregatedReport[$country][$customerType]['total_usd'] ?? 0) + $invoice['total_usd']; $aggregatedReport[$country][$customerType]['tax_total_usd'] = ($aggregatedReport[$country][$customerType]['tax_total_usd'] ?? 0) + $invoice['tax_total_usd']; $aggregatedReport[$country][$customerType]['total_after_tax_usd'] = ($aggregatedReport[$country][$customerType]['total_after_tax_usd'] ?? 0) + $invoice['total_after_tax_usd']; $aggregatedReport[$country][$customerType]['total_eur'] = ($aggregatedReport[$country][$customerType]['total_eur'] ?? 0) + $invoice['total_eur']; $aggregatedReport[$country][$customerType]['tax_total_eur'] = ($aggregatedReport[$country][$customerType]['tax_total_eur'] ?? 0) + $invoice['tax_total_eur']; $aggregatedReport[$country][$customerType]['total_after_tax_eur'] = ($aggregatedReport[$country][$customerType]['total_after_tax_eur'] ?? 0) + $invoice['total_after_tax_eur']; } $finalReport = []; foreach ($aggregatedReport as $country => $data) { foreach ($data as $customerType => $aggData) { $finalReport[] = [ 'country' => $country, 'customer_type' => $customerType, ...$aggData, ]; } } return $finalReport; } private function formatInvoice(Invoice $invoice): array { $country = $invoice->customer->address->country ?? $invoice->payment_intent->payment_method->card->country ?? null; $vatId = $invoice->customer->tax_ids->data[0]->value ?? null; $taxRate = $this->computeTaxRate($country, $vatId); $taxAmountCollectedUsd = $taxRate > 0 ? $invoice->total * $taxRate / ($taxRate + 100) : 0; $totalEur = $invoice->charge->balance_transaction->amount; $taxAmountCollectedEur = $taxRate > 0 ? $totalEur * $taxRate / ($taxRate + 100) : 0; return [ 'invoice_id' => $invoice->id, 'created_at' => Carbon::createFromTimestamp($invoice->created)->format('Y-m-d H:i:s'), 'cust_id' => $invoice->customer->id, 'cust_vat_id' => $vatId, 'cust_country' => $country, 'tax_rate' => $taxRate, 'total_usd' => $invoice->total / 100, 'tax_total_usd' => $taxAmountCollectedUsd / 100, 'total_after_tax_usd' => ($invoice->total - $taxAmountCollectedUsd) / 100, 'total_eur' => $totalEur / 100, 'tax_total_eur' => $taxAmountCollectedEur / 100, 'total_after_tax_eur' => ($totalEur - $taxAmountCollectedEur) / 100, ]; } private function computeTaxRate($countryCode, $vatId) { // Since we're a French company, for France, always apply 20% VAT if ($countryCode == 'FR' || is_null($countryCode) || empty($countryCode)) { return self::EU_TAX_RATES['FR']; } if ($taxRate = (self::EU_TAX_RATES[$countryCode] ?? null)) { // If VAT ID is provided, then TAX is 0% if (! $vatId) { return $taxRate; } } return 0; } private function isEuropeanCountry($countryCode) { return isset(self::EU_TAX_RATES[$countryCode]); } private function exportAsXlsx($data, $filename) { if (count($data) == 0) { $this->info('Empty data. No file generated.'); return; } (new ArrayExport($data))->store($filename, 'local', \Maatwebsite\Excel\Excel::XLSX); $this->line('File generated: '.storage_path('app/'.$filename)); } }