Improvements for tax reporting
This commit is contained in:
parent
6c09ed8a47
commit
694f1476de
|
|
@ -27,8 +27,9 @@ class TaxDetail
|
|||
|
||||
// Adjustment-specific fields (used when tax_status is "adjustment")
|
||||
public ?string $adjustment_reason; // "invoice_cancelled", "tax_rate_change", "exemption_applied", "correction"
|
||||
public float $taxable_amount_adjustment;
|
||||
public float $tax_amount_adjustment;
|
||||
public float $line_total;
|
||||
public float $total_tax;
|
||||
|
||||
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
|
|
@ -42,8 +43,8 @@ class TaxDetail
|
|||
// Adjustment fields
|
||||
$this->adjustment_reason = $attributes['adjustment_reason'] ?? null;
|
||||
|
||||
$this->taxable_amount_adjustment = $attributes['taxable_amount_adjustment'] ?? 0.0;
|
||||
$this->tax_amount_adjustment = $attributes['tax_amount_adjustment'] ?? 0.0;
|
||||
$this->line_total = $attributes['line_total'] ?? 0.0;
|
||||
$this->total_tax = $attributes['total_tax'] ?? 0.0;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
|
|
@ -57,8 +58,8 @@ class TaxDetail
|
|||
'tax_amount' => $this->tax_amount,
|
||||
'tax_status' => $this->tax_status,
|
||||
'adjustment_reason' => $this->adjustment_reason,
|
||||
'taxable_amount_adjustment' => $this->taxable_amount_adjustment,
|
||||
'tax_amount_adjustment' => $this->tax_amount_adjustment,
|
||||
'line_total' => $this->line_total,
|
||||
'total_tax' => $this->total_tax,
|
||||
];
|
||||
|
||||
return $data;
|
||||
|
|
|
|||
|
|
@ -14,32 +14,37 @@ namespace App\DataMapper\TaxReport;
|
|||
|
||||
/**
|
||||
* Tax summary with totals for different tax states
|
||||
*
|
||||
* Represents the taxable amount and tax amount for an invoice in a specific period.
|
||||
* The meaning of these values depends on the status:
|
||||
*
|
||||
* - 'updated': Full invoice tax liability (accrual) or paid tax (cash)
|
||||
* - 'delta': Differential tax change from invoice updates
|
||||
* - 'adjustment': Tax change from payment refunds/deletions
|
||||
* - 'cancelled': Proportional tax on refunded/cancelled amount
|
||||
* - 'deleted': Full tax reversal
|
||||
* - 'reversed': Full tax reversal of credit note
|
||||
*/
|
||||
class TaxSummary
|
||||
{
|
||||
public float $taxable_amount;
|
||||
public float $total_taxes; // Tax collected and confirmed (ie. Invoice Paid)
|
||||
public string $status; // updated, deleted, cancelled, adjustment, reversed
|
||||
public float $adjustment;
|
||||
public float $tax_adjustment;
|
||||
public float $tax_amount;
|
||||
public string $status;
|
||||
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
$this->taxable_amount = $attributes['taxable_amount'] ?? 0.0;
|
||||
$this->total_taxes = $attributes['total_taxes'] ?? 0.0;
|
||||
// Support both old and new property names for backwards compatibility during migration
|
||||
$this->tax_amount = $attributes['tax_amount'] ?? $attributes['tax_adjustment'] ?? $attributes['total_taxes'] ?? 0.0;
|
||||
$this->status = $attributes['status'] ?? 'updated';
|
||||
$this->adjustment = $attributes['adjustment'] ?? 0.0;
|
||||
$this->tax_adjustment = $attributes['tax_adjustment'] ?? 0.0;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'taxable_amount' => $this->taxable_amount,
|
||||
'total_taxes' => $this->total_taxes,
|
||||
'tax_amount' => $this->tax_amount,
|
||||
'status' => $this->status,
|
||||
'adjustment' => $this->adjustment,
|
||||
'tax_adjustment' => $this->tax_adjustment,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -171,8 +171,8 @@ class InvoiceTaxSummary implements ShouldQueue
|
|||
// })
|
||||
->whereBetween('updated_at', [$startDateUtc, $endDateUtc])
|
||||
->cursor()
|
||||
->each(function (Invoice $invoice) {
|
||||
(new InvoiceTransactionEventEntry())->run($invoice);
|
||||
->each(function (Invoice $invoice) use ($endDate) {
|
||||
(new InvoiceTransactionEventEntry())->run($invoice, $endDate);
|
||||
});
|
||||
|
||||
Invoice::withTrashed()
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ class InvoiceTransactionEventEntry
|
|||
*/
|
||||
public function run(?Invoice $invoice, ?string $force_period = null)
|
||||
{
|
||||
|
||||
if(!$invoice)
|
||||
return;
|
||||
|
||||
|
|
@ -55,7 +56,13 @@ class InvoiceTransactionEventEntry
|
|||
|
||||
|
||||
$this->entry_type = 'delta';
|
||||
// nlog($event->period->format('Y-m-d') . " ". $period);
|
||||
|
||||
// if($force_period && $event->period->format('Y-m-d') == $force_period){
|
||||
// nlog("already have an event!!");
|
||||
// return;
|
||||
// }
|
||||
// else
|
||||
if($invoice->is_deleted && $event->metadata->tax_report->tax_summary->status == 'deleted'){
|
||||
// Invoice was previously deleted, and is still deleted... return early!!
|
||||
return;
|
||||
|
|
@ -82,7 +89,10 @@ class InvoiceTransactionEventEntry
|
|||
|
||||
}
|
||||
/** If the invoice hasn't changed its state... return early!! */
|
||||
else if(BcMath::comp($invoice->amount, $event->invoice_amount) == 0){
|
||||
else if(BcMath::comp($invoice->amount, $event->invoice_amount) == 0 || $event->period->format('Y-m-d') == $period){
|
||||
nlog("event period => {$period} => " . $event->period->format('Y-m-d'));
|
||||
nlog("invoice amount => {$invoice->amount} => " . $event->invoice_amount);
|
||||
nlog("apparently no change in amount or period");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -94,6 +104,7 @@ class InvoiceTransactionEventEntry
|
|||
|
||||
}
|
||||
|
||||
nlog("invoice amount => {$invoice->amount}");
|
||||
$this->payments = $invoice->payments->flatMap(function ($payment) {
|
||||
return $payment->invoices()->get()->map(function ($invoice) use ($payment) {
|
||||
return [
|
||||
|
|
@ -135,11 +146,6 @@ class InvoiceTransactionEventEntry
|
|||
return $this;
|
||||
}
|
||||
|
||||
private function calculateRatio(float $amount): float
|
||||
{
|
||||
return round($amount * $this->paid_ratio, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* calculateDeltaMetaData
|
||||
*
|
||||
|
|
@ -157,7 +163,6 @@ class InvoiceTransactionEventEntry
|
|||
$details = [];
|
||||
|
||||
$taxes = array_merge($calc->getTaxMap()->merge($calc->getTotalTaxMap())->toArray());
|
||||
|
||||
$previous_transaction_event = TransactionEvent::where('event_id', TransactionEvent::INVOICE_UPDATED)
|
||||
->where('invoice_id', $invoice->id)
|
||||
->orderBy('timestamp', 'desc')
|
||||
|
|
@ -168,31 +173,48 @@ class InvoiceTransactionEventEntry
|
|||
|
||||
foreach ($taxes as $tax) {
|
||||
$previousLine = collect($previous_tax_details)->where('tax_name', $tax['name'])->first() ?? null;
|
||||
nlog($previousLine);
|
||||
nlog($tax);
|
||||
|
||||
$tax_detail = [
|
||||
'tax_name' => $tax['name'],
|
||||
'tax_rate' => $tax['tax_rate'],
|
||||
'taxable_amount' => $tax['base_amount'] ?? $calc->getNetSubtotal(),
|
||||
'tax_amount' => $this->calculateRatio($tax['total']),
|
||||
'taxable_amount_adjustment' => ($tax['base_amount'] ?? $calc->getNetSubtotal()) - ($previousLine->taxable_amount ?? 0),
|
||||
'tax_amount_adjustment' => $tax['total'] - ($previousLine->tax_amount ?? 0),
|
||||
'line_total' => $tax['base_amount'],
|
||||
'total_tax' => $tax['total'],
|
||||
'taxable_amount' => ($tax['base_amount'] ?? $calc->getNetSubtotal()) - ($previousLine->line_total ?? 0),
|
||||
'tax_amount' => $tax['total'] - ($previousLine->total_tax ?? 0),
|
||||
// 'taxable_amount_adjustment' => ($tax['base_amount'] ?? $calc->getNetSubtotal()) - ($previousLine->taxable_amount ?? 0),
|
||||
// 'tax_amount_adjustment' => $tax['total'] - ($previousLine->tax_amount ?? 0),
|
||||
];
|
||||
|
||||
nlog($tax_detail);
|
||||
|
||||
$details[] = $tax_detail;
|
||||
}
|
||||
|
||||
$this->setPaidRatio($invoice);
|
||||
|
||||
// Calculate cumulative previous tax by summing all previous event tax amounts
|
||||
$all_events = TransactionEvent::where('event_id', TransactionEvent::INVOICE_UPDATED)
|
||||
->where('invoice_id', $invoice->id)
|
||||
->orderBy('timestamp', 'asc')
|
||||
->get();
|
||||
|
||||
$cumulative_tax = 0;
|
||||
$cumulative_taxable = 0;
|
||||
foreach ($all_events as $event) {
|
||||
$cumulative_tax += $event->metadata->tax_report->tax_summary->tax_amount ?? 0;
|
||||
$cumulative_taxable += $event->metadata->tax_report->tax_summary->taxable_amount ?? 0;
|
||||
}
|
||||
|
||||
return new TransactionEventMetadata([
|
||||
'tax_report' => [
|
||||
'tax_details' => $details,
|
||||
'payment_history' => $this->payments->toArray() ?? [], //@phpstan-ignore-line
|
||||
'tax_summary' => [
|
||||
'taxable_amount' => $calc->getNetSubtotal(),
|
||||
'total_taxes' => $calc->getTotalTaxes(),
|
||||
'taxable_amount' => $calc->getNetSubtotal() - $cumulative_taxable,
|
||||
'tax_amount' => round($calc->getTotalTaxes() - $cumulative_tax, 2),
|
||||
'status' => 'delta',
|
||||
'adjustment' => round($calc->getNetSubtotal() - $previous_transaction_event->metadata->tax_report->tax_summary->taxable_amount, 2),
|
||||
'tax_adjustment' => round($calc->getTotalTaxes() - $previous_transaction_event->metadata->tax_report->tax_summary->total_taxes,2)
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
|
@ -223,6 +245,8 @@ class InvoiceTransactionEventEntry
|
|||
'tax_rate' => $tax['tax_rate'],
|
||||
'taxable_amount' => ($tax['base_amount'] ?? $calc->getNetSubtotal()) * $this->paid_ratio * -1,
|
||||
'tax_amount' => ($tax['total'] * $this->paid_ratio * -1),
|
||||
'line_total' => $tax['base_amount'] * $this->paid_ratio * -1,
|
||||
'total_tax' => $tax['total'] * $this->paid_ratio * -1,
|
||||
];
|
||||
$details[] = $tax_detail;
|
||||
}
|
||||
|
|
@ -234,10 +258,8 @@ class InvoiceTransactionEventEntry
|
|||
'payment_history' => $this->payments->toArray() ?? [], //@phpstan-ignore-line
|
||||
'tax_summary' => [
|
||||
'taxable_amount' => $calc->getNetSubtotal() * $this->paid_ratio * -1,
|
||||
'total_taxes' => $calc->getTotalTaxes() * $this->paid_ratio * -1,
|
||||
'tax_amount' => $calc->getTotalTaxes() * $this->paid_ratio * -1,
|
||||
'status' => 'reversed',
|
||||
// 'adjustment' => round($calc->getNetSubtotal() - $previous_transaction_event->metadata->tax_report->tax_summary->taxable_amount, 2),
|
||||
// 'tax_adjustment' => round($calc->getTotalTaxes() - $previous_transaction_event->metadata->tax_report->tax_summary->total_taxes,2)
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
|
@ -274,6 +296,8 @@ class InvoiceTransactionEventEntry
|
|||
'tax_rate' => $tax['tax_rate'],
|
||||
'taxable_amount' => ($tax['base_amount'] ?? $calc->getNetSubtotal()) * $this->paid_ratio,
|
||||
'tax_amount' => ($tax['total'] * $this->paid_ratio),
|
||||
'line_total' => $tax['base_amount'] * $this->paid_ratio,
|
||||
'total_tax' => $tax['total'] * $this->paid_ratio,
|
||||
];
|
||||
$details[] = $tax_detail;
|
||||
}
|
||||
|
|
@ -285,10 +309,8 @@ class InvoiceTransactionEventEntry
|
|||
'payment_history' => $this->payments->toArray() ?? [], //@phpstan-ignore-line
|
||||
'tax_summary' => [
|
||||
'taxable_amount' => $calc->getNetSubtotal() * $this->paid_ratio,
|
||||
'total_taxes' => $calc->getTotalTaxes() * $this->paid_ratio,
|
||||
'tax_amount' => $calc->getTotalTaxes() * $this->paid_ratio,
|
||||
'status' => 'cancelled',
|
||||
// 'adjustment' => round($calc->getNetSubtotal() - $previous_transaction_event->metadata->tax_report->tax_summary->taxable_amount, 2),
|
||||
// 'tax_adjustment' => round($calc->getTotalTaxes() - $previous_transaction_event->metadata->tax_report->tax_summary->total_taxes,2)
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
|
@ -315,6 +337,8 @@ class InvoiceTransactionEventEntry
|
|||
'tax_rate' => $tax['tax_rate'],
|
||||
'taxable_amount' => ($tax['base_amount'] ?? $calc->getNetSubtotal()) * -1,
|
||||
'tax_amount' => $tax['total'] * -1,
|
||||
'line_total' => $tax['base_amount'] * -1,
|
||||
'total_tax' => $tax['total'] * -1,
|
||||
];
|
||||
$details[] = $tax_detail;
|
||||
}
|
||||
|
|
@ -324,8 +348,8 @@ class InvoiceTransactionEventEntry
|
|||
'tax_details' => $details,
|
||||
'payment_history' => $this->payments->toArray(),
|
||||
'tax_summary' => [
|
||||
'taxable_amount' => $calc->getNetSubtotal(),
|
||||
'total_taxes' => $calc->getTotalTaxes(),
|
||||
'taxable_amount' => $calc->getNetSubtotal() * -1,
|
||||
'tax_amount' => $calc->getTotalTaxes() * -1,
|
||||
'status' => 'deleted',
|
||||
],
|
||||
],
|
||||
|
|
@ -358,6 +382,8 @@ class InvoiceTransactionEventEntry
|
|||
'tax_rate' => $tax['tax_rate'],
|
||||
'taxable_amount' => $tax['base_amount'] ?? $calc->getNetSubtotal(),
|
||||
'tax_amount' => $tax['total'],
|
||||
'line_total' => $tax['base_amount'] ?? $calc->getNetSubtotal(),
|
||||
'total_tax' => $tax['total'],
|
||||
];
|
||||
$details[] = $tax_detail;
|
||||
}
|
||||
|
|
@ -368,7 +394,7 @@ class InvoiceTransactionEventEntry
|
|||
'payment_history' => $this->payments->toArray(),
|
||||
'tax_summary' => [
|
||||
'taxable_amount' => $calc->getNetSubtotal(),
|
||||
'total_taxes' => $calc->getTotalTaxes(),
|
||||
'tax_amount' => $calc->getTotalTaxes(),
|
||||
'status' => 'updated',
|
||||
],
|
||||
],
|
||||
|
|
|
|||
|
|
@ -117,11 +117,9 @@ class InvoiceTransactionEventEntryCash
|
|||
'tax_details' => $details,
|
||||
'payment_history' => $this->payments->toArray(),
|
||||
'tax_summary' => [
|
||||
'total_taxes' => $invoice->total_taxes * $this->paid_ratio,
|
||||
'tax_amount' => $invoice->total_taxes * $this->paid_ratio,
|
||||
'status' => 'updated',
|
||||
'taxable_amount' => $calc->getNetSubtotal() * $this->paid_ratio,
|
||||
'adjustment' => 0,
|
||||
'tax_adjustment' => 0,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -211,9 +211,9 @@ class PaymentTransactionEventEntry implements ShouldQueue
|
|||
|
||||
$taxes = array_merge($calc->getTaxMap()->merge($calc->getTotalTaxMap())->toArray());
|
||||
|
||||
// For full refunds, use the paid ratio (amount paid / invoice amount)
|
||||
// This gives us the correct tax adjustment
|
||||
$ratio = $this->paid_ratio;
|
||||
// Calculate refund ratio: refund_amount / invoice_amount
|
||||
// This gives us the pro-rata portion of taxes to refund
|
||||
$refund_ratio = $invoice->amount > 0 ? $this->invoice_adjustment / $invoice->amount : 0;
|
||||
|
||||
foreach ($taxes as $tax) {
|
||||
|
||||
|
|
@ -222,8 +222,8 @@ class PaymentTransactionEventEntry implements ShouldQueue
|
|||
$tax_detail = [
|
||||
'tax_name' => $tax['name'],
|
||||
'tax_rate' => $tax['tax_rate'],
|
||||
'taxable_amount' => round($base_amount * $ratio, 2) * -1,
|
||||
'tax_amount' => round($tax['total'] * $ratio, 2) * -1,
|
||||
'taxable_amount' => round($base_amount * $refund_ratio, 2) * -1,
|
||||
'tax_amount' => round($tax['total'] * $refund_ratio, 2) * -1,
|
||||
];
|
||||
$details[] = $tax_detail;
|
||||
}
|
||||
|
|
@ -233,11 +233,9 @@ class PaymentTransactionEventEntry implements ShouldQueue
|
|||
'tax_details' => $details,
|
||||
'payment_history' => $this->payments->toArray(),
|
||||
'tax_summary' => [
|
||||
'total_taxes' => round(($invoice->total_taxes - $this->getTotalTaxPaid($invoice)) * $ratio, 2) * -1,
|
||||
'tax_adjustment' => round(($invoice->total_taxes - $this->getTotalTaxPaid($invoice)) * $ratio, 2) * -1,
|
||||
'tax_amount' => round($invoice->total_taxes * $refund_ratio, 2) * -1,
|
||||
'status' => 'adjustment',
|
||||
'taxable_amount' => round($calc->getNetSubtotal() * $ratio, 2) * -1,
|
||||
'adjustment' => 0,
|
||||
'taxable_amount' => round($calc->getNetSubtotal() * $refund_ratio, 2) * -1,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
|
@ -278,10 +276,8 @@ class PaymentTransactionEventEntry implements ShouldQueue
|
|||
'tax_details' => $details,
|
||||
'payment_history' => $this->payments->toArray(),
|
||||
'tax_summary' => [
|
||||
'total_taxes' => $invoice->total_taxes * -1,
|
||||
'tax_amount' => round($invoice->total_taxes - $this->getTotalTaxPaid($invoice), 2) * -1,
|
||||
'taxable_amount' => $calc->getNetSubtotal() * -1,
|
||||
'adjustment' => 0,
|
||||
'tax_adjustment' => round($invoice->total_taxes - $this->getTotalTaxPaid($invoice), 2) * -1,
|
||||
'status' => 'adjustment',
|
||||
],
|
||||
],
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ use App\DataMapper\TransactionEventMetadata;
|
|||
* @property float $credit_balance
|
||||
* @property float $credit_amount
|
||||
* @property int|null $credit_status
|
||||
* @property Carbon|null $period
|
||||
* @property \Carbon\Carbon|null $period
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|StaticModel company()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|StaticModel exclude($columns)
|
||||
* @mixin \Eloquent
|
||||
|
|
|
|||
|
|
@ -77,12 +77,12 @@ class InvoiceItemReportRow
|
|||
$this->invoice->date,
|
||||
$this->tax_detail->tax_name,
|
||||
$this->tax_detail->tax_rate,
|
||||
$this->tax_detail->tax_amount_adjustment,
|
||||
$this->tax_detail->taxable_amount_adjustment,
|
||||
$this->tax_detail->tax_amount,
|
||||
$this->tax_detail->taxable_amount,
|
||||
$this->status->label(),
|
||||
];
|
||||
|
||||
return $this->appendRegionalColumns($row, $this->tax_detail->tax_amount_adjustment);
|
||||
return $this->appendRegionalColumns($row, $this->tax_detail->tax_amount);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class InvoiceReportRow
|
|||
ctrans('texts.invoice_date'),
|
||||
ctrans('texts.invoice_total'),
|
||||
ctrans('texts.paid'),
|
||||
ctrans('texts.total_taxes'),
|
||||
ctrans('texts.tax_amount'),
|
||||
ctrans('texts.taxable_amount'),
|
||||
ctrans('texts.notes'),
|
||||
];
|
||||
|
|
@ -60,14 +60,14 @@ class InvoiceReportRow
|
|||
$this->row_data = [
|
||||
$this->invoice->number,
|
||||
$this->invoice->date,
|
||||
$this->invoice->amount,
|
||||
$this->invoice->paid_to_date,
|
||||
$this->tax_summary->total_taxes,
|
||||
$this->event->invoice_amount,
|
||||
$this->event->invoice_paid_to_date,
|
||||
$this->tax_summary->tax_amount,
|
||||
$this->tax_summary->taxable_amount,
|
||||
$this->tax_summary->status->label(),
|
||||
];
|
||||
|
||||
$this->appendRegionalColumns($this->tax_summary->total_taxes);
|
||||
$this->appendRegionalColumns($this->tax_summary->tax_amount);
|
||||
|
||||
return $this->row_data;
|
||||
}
|
||||
|
|
@ -80,14 +80,14 @@ class InvoiceReportRow
|
|||
$this->row_data = [
|
||||
$this->invoice->number,
|
||||
$this->invoice->date,
|
||||
$this->tax_summary->adjustment,
|
||||
$this->event->invoice_amount,
|
||||
$this->event->metadata->tax_report->payment_history?->sum('amount') ?? 0,
|
||||
$this->tax_summary->tax_adjustment,
|
||||
$this->tax_summary->tax_amount,
|
||||
$this->tax_summary->taxable_amount,
|
||||
$this->tax_summary->status->label(),
|
||||
];
|
||||
|
||||
$this->appendRegionalColumns($this->tax_summary->tax_adjustment);
|
||||
$this->appendRegionalColumns($this->tax_summary->tax_amount);
|
||||
|
||||
return $this->row_data;
|
||||
}
|
||||
|
|
@ -100,14 +100,14 @@ class InvoiceReportRow
|
|||
$this->row_data = [
|
||||
$this->invoice->number,
|
||||
$this->invoice->date,
|
||||
$this->invoice->amount,
|
||||
$this->event->invoice_amount,
|
||||
$this->event->invoice_paid_to_date,
|
||||
$this->tax_summary->total_taxes,
|
||||
$this->event->invoice_paid_to_date - $this->invoice->amount, // Negative adjustment amount
|
||||
$this->tax_summary->tax_amount,
|
||||
$this->tax_summary->taxable_amount, // Negative adjustment amount
|
||||
$this->tax_summary->status->label(),
|
||||
];
|
||||
|
||||
$this->appendRegionalColumns($this->tax_summary->tax_adjustment);
|
||||
$this->appendRegionalColumns($this->tax_summary->tax_amount);
|
||||
|
||||
return $this->row_data;
|
||||
}
|
||||
|
|
@ -117,21 +117,17 @@ class InvoiceReportRow
|
|||
*/
|
||||
public function buildCancelledRow(): array
|
||||
{
|
||||
$paid_ratio = $this->event->invoice_amount > 0
|
||||
? $this->event->invoice_paid_to_date / $this->event->invoice_amount
|
||||
: 0;
|
||||
|
||||
$this->row_data = [
|
||||
$this->invoice->number,
|
||||
$this->invoice->date,
|
||||
$this->event->invoice_paid_to_date,
|
||||
$this->event->metadata->tax_report->payment_history?->sum('amount') ?? 0,
|
||||
$paid_ratio * $this->tax_summary->total_taxes,
|
||||
$this->tax_summary->tax_amount,
|
||||
$this->tax_summary->taxable_amount,
|
||||
$this->tax_summary->status->label(),
|
||||
];
|
||||
|
||||
$this->appendRegionalColumns($paid_ratio * $this->tax_summary->total_taxes);
|
||||
$this->appendRegionalColumns($this->tax_summary->tax_amount);
|
||||
|
||||
return $this->row_data;
|
||||
}
|
||||
|
|
@ -146,12 +142,12 @@ class InvoiceReportRow
|
|||
$this->invoice->date,
|
||||
$this->invoice->amount * -1,
|
||||
($this->event->metadata->tax_report->payment_history?->sum('amount') ?? 0) * -1,
|
||||
$this->tax_summary->total_taxes * -1,
|
||||
$this->tax_summary->taxable_amount * -1,
|
||||
$this->tax_summary->tax_amount,
|
||||
$this->tax_summary->taxable_amount,
|
||||
$this->tax_summary->status->label(),
|
||||
];
|
||||
|
||||
$this->appendRegionalColumns($this->tax_summary->total_taxes * -1);
|
||||
$this->appendRegionalColumns($this->tax_summary->tax_amount);
|
||||
|
||||
return $this->row_data;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,213 +0,0 @@
|
|||
# Tax Period Report - Refactoring Summary
|
||||
|
||||
## Overview
|
||||
|
||||
The TaxPeriodReport has been comprehensively refactored to provide a clean, extensible architecture for generating tax compliance reports across different regions and accounting methods.
|
||||
|
||||
## What Was Accomplished
|
||||
|
||||
### ✅ **Complete Architectural Refactor**
|
||||
|
||||
**Before**: 761 lines, mixed concerns, duplicated code
|
||||
**After**: 435 lines + 9 specialized classes
|
||||
|
||||
### ✅ **New Architecture Components**
|
||||
|
||||
#### **Core DTOs** (`app/Services/Report/TaxPeriod/`)
|
||||
- `TaxReportStatus.php` - Enum for status values (updated, delta, adjustment, cancelled, deleted)
|
||||
- `TaxSummary.php` - Tax summary data with calculated helpers
|
||||
- `TaxDetail.php` - Tax detail line items with calculated helpers
|
||||
|
||||
#### **Regional Tax Support**
|
||||
- `RegionalTaxCalculator.php` - Interface for region-specific implementations
|
||||
- `UsaTaxCalculator.php` - USA state/county/city/district tax breakdown
|
||||
- `GenericTaxCalculator.php` - Fallback for other regions
|
||||
- `RegionalTaxCalculatorFactory.php` - Auto-selects appropriate calculator
|
||||
|
||||
#### **Report Builders**
|
||||
- `InvoiceReportRow.php` - Builds invoice-level rows (handles all status types)
|
||||
- `InvoiceItemReportRow.php` - Builds tax detail rows
|
||||
|
||||
### ✅ **Metadata Optimization**
|
||||
|
||||
Removed redundant/unused fields:
|
||||
|
||||
**TaxSummary**:
|
||||
- ❌ Removed `total_paid` (now calculated on-demand)
|
||||
|
||||
**TaxDetail**:
|
||||
- ❌ Removed `tax_amount_paid` (calculated)
|
||||
- ❌ Removed `tax_amount_paid_adjustment` (always 0)
|
||||
- ❌ Removed `tax_amount_remaining_adjustment` (always 0)
|
||||
|
||||
**Storage Reduction**: ~36% per transaction event
|
||||
|
||||
### ✅ **Test Infrastructure**
|
||||
|
||||
Added `skip_initialization` parameter to TaxPeriodReport constructor for clean test isolation:
|
||||
|
||||
```php
|
||||
new TaxPeriodReport($company, $payload, skip_initialization: true)
|
||||
```
|
||||
|
||||
This prevents the prophylactic `initializeData()` from polluting test data.
|
||||
|
||||
## Test Status
|
||||
|
||||
### **Passing Tests (7/15)**
|
||||
✅ testSingleInvoiceTaxReportStructure
|
||||
✅ testInvoiceReportingOverMultiplePeriodsWithAccrualAccountingCheckAdjustmentsForIncreases
|
||||
✅ testInvoiceReportingOverMultiplePeriodsWithAccrualAccountingCheckAdjustmentsForDecreases
|
||||
✅ testInvoiceReportingOverMultiplePeriodsWithCashAccountingCheckAdjustments
|
||||
✅ testInvoiceWithRefundAndCashReportsAreCorrect
|
||||
✅ testInvoiceWithRefundAndCashReportsAreCorrectAcrossReportingPeriods
|
||||
✅ testCancelledInvoiceInSamePeriodAccrual
|
||||
|
||||
### **Pending Tests (8/15)** - Require Listener Fix
|
||||
|
||||
The following tests are scaffolded but require a fix to `InvoiceTransactionEventEntry`:
|
||||
|
||||
⏸️ testCancelledInvoiceInNextPeriodAccrual
|
||||
⏸️ testCancelledInvoiceWithPartialPaymentAccrual
|
||||
⏸️ testDeletedInvoiceInSamePeriodAccrual
|
||||
⏸️ testDeletedInvoiceInNextPeriodAccrual
|
||||
⏸️ testDeletedPaidInvoiceInNextPeriodAccrual
|
||||
⏸️ testPaymentDeletedInSamePeriodCash
|
||||
⏸️ testPaymentDeletedInNextPeriodCash
|
||||
⏸️ testPaymentDeletedInNextPeriodAccrual
|
||||
|
||||
## Known Issue: InvoiceTransactionEventEntry Listener
|
||||
|
||||
**Location**: `app/Listeners/Invoice/InvoiceTransactionEventEntry.php:68-70`
|
||||
|
||||
**Problem**: The listener returns early when invoice amount hasn't changed, even if the STATUS has changed:
|
||||
|
||||
```php
|
||||
else if(BcMath::comp($invoice->amount, $event->invoice_amount) == 0){
|
||||
return; // ❌ BUG: Returns even if status changed!
|
||||
}
|
||||
```
|
||||
|
||||
**Impact**: When an invoice is cancelled or has its status changed without amount changes, no new transaction event is created for that period.
|
||||
|
||||
**Fix Needed**: Add status change detection:
|
||||
|
||||
```php
|
||||
else if(BcMath::comp($invoice->amount, $event->invoice_amount) == 0 &&
|
||||
$invoice->status_id == $event->invoice_status){
|
||||
return; // ✅ Only return if BOTH amount AND status unchanged
|
||||
}
|
||||
```
|
||||
|
||||
**Workaround**: Tests can explicitly pass the `$force_period` parameter to bypass the early return logic.
|
||||
|
||||
## Benefits of Refactoring
|
||||
|
||||
### 1. **Regional Extensibility**
|
||||
Adding support for a new region (Canada, EU, Australia, etc.) is now trivial:
|
||||
|
||||
```php
|
||||
class CanadaTaxCalculator implements RegionalTaxCalculator {
|
||||
public function getHeaders(): array {
|
||||
return ['Province', 'GST', 'PST', 'HST'];
|
||||
}
|
||||
|
||||
public function calculateColumns(Invoice $invoice, float $amount): array {
|
||||
// Canadian-specific logic
|
||||
}
|
||||
|
||||
public static function supports(string $country_iso): bool {
|
||||
return $country_iso === 'CA';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Register in `RegionalTaxCalculatorFactory::CALCULATORS` and you're done!
|
||||
|
||||
### 2. **Type Safety**
|
||||
- Enums instead of magic strings
|
||||
- DTOs with typed properties
|
||||
- Clear method signatures
|
||||
|
||||
### 3. **Maintainability**
|
||||
- Single Responsibility Principle
|
||||
- DRY - no duplicated USA tax calculation code
|
||||
- Clear separation: orchestration vs presentation vs calculation
|
||||
|
||||
### 4. **Performance**
|
||||
- 36% reduction in metadata storage
|
||||
- Calculations only when needed (lazy)
|
||||
- Cleaner, faster queries
|
||||
|
||||
### 5. **Testability**
|
||||
- Each component can be unit tested independently
|
||||
- Clean test isolation with `skip_initialization` flag
|
||||
- Row builders use dependency injection
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Report Generation
|
||||
|
||||
```php
|
||||
$payload = [
|
||||
'start_date' => '2025-10-01',
|
||||
'end_date' => '2025-10-31',
|
||||
'date_range' => 'custom',
|
||||
'is_income_billed' => true, // true = accrual, false = cash
|
||||
];
|
||||
|
||||
$report = new TaxPeriodReport($company, $payload);
|
||||
$xlsx_content = $report->run();
|
||||
```
|
||||
|
||||
### Testing with Isolation
|
||||
|
||||
```php
|
||||
$report = new TaxPeriodReport($company, $payload, skip_initialization: true);
|
||||
$data = $report->boot()->getData();
|
||||
|
||||
// Now you have clean test data without interference from other tests
|
||||
```
|
||||
|
||||
### Using Calculated Methods
|
||||
|
||||
```php
|
||||
$tax_summary = TaxSummary::fromMetadata($event->metadata->tax_report->tax_summary);
|
||||
|
||||
// Calculate on-demand
|
||||
$paid = $tax_summary->calculateTotalPaid($invoice->amount, $invoice->paid_to_date);
|
||||
$remaining = $tax_summary->calculateTotalRemaining($invoice->amount, $invoice->paid_to_date);
|
||||
$ratio = $tax_summary->getPaymentRatio($invoice->amount, $invoice->paid_to_date);
|
||||
|
||||
$tax_detail = TaxDetail::fromMetadata($event->metadata->tax_report->tax_details[0]);
|
||||
$tax_paid = $tax_detail->calculateTaxPaid($ratio);
|
||||
```
|
||||
|
||||
## Migration Notes
|
||||
|
||||
### No Breaking Changes
|
||||
The refactored code is **100% backward compatible** with existing metadata structures. Old metadata with redundant fields will simply ignore them when creating DTOs.
|
||||
|
||||
### Production Deployment
|
||||
No migration required. The refactored report works with existing transaction events and metadata.
|
||||
|
||||
### Future Optimization
|
||||
Once InvoiceTransactionEventEntry is updated to use the streamlined metadata structure (removing redundant fields), storage savings will be realized automatically.
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `app/Services/Report/TaxPeriodReport.php` - Main orchestration class
|
||||
- Created 9 new support classes in `app/Services/Report/TaxPeriod/`
|
||||
- `tests/Feature/Export/TaxPeriodReportTest.php` - Added 8 new test scenarios
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Fix InvoiceTransactionEventEntry listener** to detect status changes
|
||||
2. **Enable pending tests** once listener is fixed
|
||||
3. **Add more regions** as needed (Canada, EU, etc.)
|
||||
4. **Update listeners** to use optimized metadata structure (optional)
|
||||
5. **Add summary sheet calculations** (currently empty placeholder)
|
||||
|
||||
## Questions?
|
||||
|
||||
This refactoring maintains all existing functionality while providing a solid foundation for future enhancements. All core business logic remains unchanged - this was purely a structural improvement.
|
||||
|
|
@ -22,8 +22,8 @@ class TaxDetail
|
|||
public float $tax_rate,
|
||||
public float $taxable_amount,
|
||||
public float $tax_amount,
|
||||
public float $tax_amount_adjustment = 0,
|
||||
public float $taxable_amount_adjustment = 0,
|
||||
public float $line_total = 0,
|
||||
public float $total_tax = 0,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
|
@ -36,8 +36,8 @@ class TaxDetail
|
|||
tax_rate: $metadata->tax_rate,
|
||||
taxable_amount: $metadata->taxable_amount ?? 0,
|
||||
tax_amount: $metadata->tax_amount ?? 0,
|
||||
tax_amount_adjustment: $metadata->tax_amount_adjustment ?? 0,
|
||||
taxable_amount_adjustment: $metadata->taxable_amount_adjustment ?? 0,
|
||||
line_total: $metadata->line_total ?? 0,
|
||||
total_tax: $metadata->total_tax ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -85,13 +85,6 @@ class TaxDetail
|
|||
return round($this->taxable_amount * (1 - $payment_ratio), 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this detail represents an adjustment (delta/refund)
|
||||
*/
|
||||
public function isAdjustment(): bool
|
||||
{
|
||||
return $this->tax_amount_adjustment != 0 || $this->taxable_amount_adjustment != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get effective tax rate as percentage string (e.g., "10%")
|
||||
|
|
@ -131,8 +124,8 @@ class TaxDetail
|
|||
'tax_rate' => $this->tax_rate,
|
||||
'taxable_amount' => $this->taxable_amount,
|
||||
'tax_amount' => $this->tax_amount,
|
||||
'tax_amount_adjustment' => $this->tax_amount_adjustment,
|
||||
'taxable_amount_adjustment' => $this->taxable_amount_adjustment,
|
||||
'line_total' => $this->line_total,
|
||||
'total_tax' => $this->total_tax,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,60 +14,43 @@ namespace App\Services\Report\TaxPeriod;
|
|||
|
||||
/**
|
||||
* Data Transfer Object for tax summary information
|
||||
*
|
||||
* Represents the taxable amount and tax amount for an invoice in a specific period.
|
||||
* The meaning of these values depends on the status:
|
||||
*
|
||||
* - 'updated': Full invoice tax liability (accrual) or paid tax (cash)
|
||||
* - 'delta': Differential tax change from invoice updates
|
||||
* - 'adjustment': Tax change from payment refunds/deletions
|
||||
* - 'cancelled': Proportional tax on refunded/cancelled amount
|
||||
* - 'deleted': Full tax reversal
|
||||
* - 'reversed': Full tax reversal of credit note
|
||||
*/
|
||||
class TaxSummary
|
||||
{
|
||||
public function __construct(
|
||||
public float $taxable_amount,
|
||||
public float $total_taxes,
|
||||
public float $tax_amount,
|
||||
public TaxReportStatus $status,
|
||||
public float $adjustment = 0,
|
||||
public float $tax_adjustment = 0,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from transaction event metadata
|
||||
*/
|
||||
public static function fromMetadata(object $metadata): self
|
||||
public static function fromMetadata($metadata): self
|
||||
{
|
||||
// Handle both object and array access
|
||||
$taxable_amount = is_array($metadata) ? ($metadata['taxable_amount'] ?? 0) : ($metadata->taxable_amount ?? 0);
|
||||
$tax_amount = is_array($metadata) ? ($metadata['tax_amount'] ?? $metadata['tax_adjustment'] ?? 0) : ($metadata->tax_amount ?? $metadata->tax_adjustment ?? 0);
|
||||
$status = is_array($metadata) ? ($metadata['status'] ?? 'updated') : ($metadata->status ?? 'updated');
|
||||
|
||||
return new self(
|
||||
taxable_amount: $metadata->taxable_amount ?? 0,
|
||||
total_taxes: $metadata->total_taxes ?? 0,
|
||||
status: TaxReportStatus::from($metadata->status ?? 'updated'),
|
||||
adjustment: $metadata->adjustment ?? 0,
|
||||
tax_adjustment: $metadata->tax_adjustment ?? 0,
|
||||
taxable_amount: $taxable_amount,
|
||||
tax_amount: $tax_amount,
|
||||
status: TaxReportStatus::from($status),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate total tax paid based on invoice payment ratio
|
||||
*
|
||||
* @param float $invoice_amount Total invoice amount
|
||||
* @param float $invoice_paid_to_date Amount paid on invoice
|
||||
* @return float Tax amount that has been paid
|
||||
*/
|
||||
public function calculateTotalPaid(float $invoice_amount, float $invoice_paid_to_date): float
|
||||
{
|
||||
if ($invoice_amount == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return round($this->total_taxes * ($invoice_paid_to_date / $invoice_amount), 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate total tax remaining
|
||||
*
|
||||
* @param float $invoice_amount Total invoice amount
|
||||
* @param float $invoice_paid_to_date Amount paid on invoice
|
||||
* @return float Tax amount still outstanding
|
||||
*/
|
||||
public function calculateTotalRemaining(float $invoice_amount, float $invoice_paid_to_date): float
|
||||
{
|
||||
return round($this->total_taxes - $this->calculateTotalPaid($invoice_amount, $invoice_paid_to_date), 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the payment ratio for this invoice
|
||||
*
|
||||
|
|
@ -87,10 +70,8 @@ class TaxSummary
|
|||
{
|
||||
return [
|
||||
'taxable_amount' => $this->taxable_amount,
|
||||
'total_taxes' => $this->total_taxes,
|
||||
'tax_amount' => $this->tax_amount,
|
||||
'status' => $this->status->value,
|
||||
'adjustment' => $this->adjustment,
|
||||
'tax_adjustment' => $this->tax_adjustment,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ class TaxPeriodReport extends BaseExport
|
|||
|
||||
public function run()
|
||||
{
|
||||
nlog($this->input);
|
||||
// nlog($this->input);
|
||||
MultiDB::setDb($this->company->db);
|
||||
App::forgetInstance('translator');
|
||||
App::setLocale($this->company->locale());
|
||||
|
|
@ -120,8 +120,6 @@ class TaxPeriodReport extends BaseExport
|
|||
{
|
||||
$this->cash_accounting = $this->input['is_income_billed'] ? false : true;
|
||||
|
||||
nlog("IS CASH ACCOUNTING ? => {$this->cash_accounting}");
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
@ -135,16 +133,21 @@ class TaxPeriodReport extends BaseExport
|
|||
*/
|
||||
private function initializeData(): self
|
||||
{
|
||||
|
||||
$q = Invoice::withTrashed()
|
||||
->where('company_id', $this->company->id)
|
||||
->whereIn('status_id', [2,3,4,5,6])
|
||||
->whereBetween('date', ['1970-01-01', now()->subMonth()->endOfMonth()->format('Y-m-d')])
|
||||
->whereDoesntHave('transaction_events');
|
||||
->whereBetween('date', ['1970-01-01', $this->end_date])
|
||||
// ->whereDoesntHave('transaction_events'); //filter by no transaction events for THIS month.
|
||||
->whereDoesntHave('transaction_events', function ($query) {
|
||||
$query->where('period', $this->end_date);
|
||||
});
|
||||
|
||||
$q->cursor()
|
||||
->each(function ($invoice) {
|
||||
|
||||
(new InvoiceTransactionEventEntry())->run($invoice, \Carbon\Carbon::parse($invoice->date)->endOfMonth()->format('Y-m-d'));
|
||||
(new InvoiceTransactionEventEntry())->run($invoice, $this->end_date);
|
||||
|
||||
|
||||
if (in_array($invoice->status_id, [Invoice::STATUS_PAID, Invoice::STATUS_PARTIAL])) {
|
||||
|
||||
|
|
@ -177,11 +180,19 @@ class TaxPeriodReport extends BaseExport
|
|||
->whereIn('metadata->tax_report->tax_summary->status', ['cancelled', 'deleted']);
|
||||
});
|
||||
|
||||
nlog("end date = {$this->end_date} =>" . $ii->count());
|
||||
|
||||
$ii->cursor()
|
||||
->each(function ($invoice) {
|
||||
|
||||
// Iterate through each month between start_date and end_date
|
||||
// $current_date = Carbon::parse($this->start_date);
|
||||
// $end_date_carbon = Carbon::parse($this->end_date);
|
||||
|
||||
// while ($current_date->lte($end_date_carbon)) {
|
||||
// $last_day_of_month = $current_date->copy()->endOfMonth()->format('Y-m-d');
|
||||
// (new InvoiceTransactionEventEntry())->run($invoice, $last_day_of_month);
|
||||
// $current_date->addMonth();
|
||||
// }
|
||||
|
||||
(new InvoiceTransactionEventEntry())->run($invoice, $this->end_date);
|
||||
|
||||
});
|
||||
|
|
@ -385,8 +396,6 @@ class TaxPeriodReport extends BaseExport
|
|||
|
||||
$query = $this->resolveQuery();
|
||||
|
||||
nlog($query->count(). " records to iterate");
|
||||
|
||||
// Initialize with headers
|
||||
$this->data['invoices'] = [InvoiceReportRow::getHeaders($this->regional_calculator)];
|
||||
$this->data['invoice_items'] = [InvoiceItemReportRow::getHeaders($this->regional_calculator)];
|
||||
|
|
@ -427,7 +436,6 @@ class TaxPeriodReport extends BaseExport
|
|||
{
|
||||
$tax_summary = TaxSummary::fromMetadata($event->metadata->tax_report->tax_summary);
|
||||
|
||||
nlog($event->metadata->toArray());
|
||||
// Build and add invoice row
|
||||
$invoice_row_builder = new InvoiceReportRow(
|
||||
$invoice,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue