From 0d991036f45e72e71738c7ba92e66bb904da2fe5 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 17 Nov 2025 15:12:01 +1100 Subject: [PATCH] Improve ability for adjustments to be made between reporting periods --- app/DataMapper/TaxReport/TaxSummary.php | 2 +- app/Jobs/Cron/InvoiceTaxSummary.php | 35 ++++--- .../Invoice/InvoiceTransactionEventEntry.php | 94 +++++++++++++++++-- 3 files changed, 109 insertions(+), 22 deletions(-) diff --git a/app/DataMapper/TaxReport/TaxSummary.php b/app/DataMapper/TaxReport/TaxSummary.php index e73678057c..58750535d0 100644 --- a/app/DataMapper/TaxReport/TaxSummary.php +++ b/app/DataMapper/TaxReport/TaxSummary.php @@ -19,7 +19,7 @@ class TaxSummary { public float $total_taxes; // Tax collected and confirmed (ie. Invoice Paid) public float $total_paid; // Tax pending collection (Outstanding tax of balance owing) - public string $status; + public string $status; // updated, deleted, cancelled public float $adjustment; public function __construct(array $attributes = []) { diff --git a/app/Jobs/Cron/InvoiceTaxSummary.php b/app/Jobs/Cron/InvoiceTaxSummary.php index 3c14b36aba..af8cd86953 100644 --- a/app/Jobs/Cron/InvoiceTaxSummary.php +++ b/app/Jobs/Cron/InvoiceTaxSummary.php @@ -137,11 +137,23 @@ class InvoiceTaxSummary implements ShouldQueue $todayStart = now()->subHours(15)->timestamp; $todayEnd = now()->endOfDay()->timestamp; + // Convert company timezone dates to UTC for database query + // $startDate and $endDate are in Y-m-d format (e.g., "2024-01-01") + $timezone = $company->timezone()->name ?? 'UTC'; + $startDateUtc = Carbon::createFromFormat('Y-m-d', $startDate, $timezone) + ->startOfDay() + ->setTimezone('UTC') + ->format('Y-m-d H:i:s'); + $endDateUtc = Carbon::createFromFormat('Y-m-d', $endDate, $timezone) + ->endOfDay() + ->setTimezone('UTC') + ->format('Y-m-d H:i:s'); + Invoice::withTrashed() - ->with('payments') + ->with('payments',) ->where('company_id', $company->id) ->whereIn('status_id', [2,3,4,5]) - ->where('is_deleted', 0) + // ->where('is_deleted', 0) I still need to assess deleted invoices, and ensure if there is an entry present, we reverse it!!! ->whereHas('client', function ($query) { $query->where('is_deleted', false); }) @@ -151,12 +163,13 @@ class InvoiceTaxSummary implements ShouldQueue $q->where('is_flagged', false); }); }) - ->whereBetween('date', [$startDate, $endDate]) - ->whereDoesntHave('transaction_events', function ($query) use ($todayStart, $todayEnd) { - $query->where('timestamp', '>=', $todayStart) - ->where('timestamp', '<=', $todayEnd) - ->where('event_id', TransactionEvent::INVOICE_UPDATED); - }) + // ->whereBetween('date', [$startDate, $endDate]) + // ->whereDoesntHave('transaction_events', function ($query) use ($todayStart, $todayEnd) { + // $query->where('timestamp', '>=', $todayStart) + // ->where('timestamp', '<=', $todayEnd) + // ->where('event_id', TransactionEvent::INVOICE_UPDATED); + // }) + ->whereBetween('updated_at', [$startDateUtc, $endDateUtc]) ->cursor() ->each(function (Invoice $invoice) { (new InvoiceTransactionEventEntry())->run($invoice); @@ -177,10 +190,10 @@ class InvoiceTaxSummary implements ShouldQueue $q->where('is_flagged', false); }); }) - ->whereHas('payments', function ($query) use ($startDate, $endDate) { - $query->whereHas('paymentables', function ($subQuery) use ($startDate, $endDate) { + ->whereHas('payments', function ($query) use ($startDateUtc, $endDateUtc) { + $query->whereHas('paymentables', function ($subQuery) use ($startDateUtc, $endDateUtc) { $subQuery->where('paymentable_type', Invoice::class) - ->whereBetween('created_at', [$startDate . ' 00:00:00', $endDate . ' 23:59:59']); + ->whereBetween('created_at', [$startDateUtc, $endDateUtc]); }); }) ->whereDoesntHave('transaction_events', function ($q) use ($todayStart, $todayEnd) { diff --git a/app/Listeners/Invoice/InvoiceTransactionEventEntry.php b/app/Listeners/Invoice/InvoiceTransactionEventEntry.php index 1154fe846c..41c867465f 100644 --- a/app/Listeners/Invoice/InvoiceTransactionEventEntry.php +++ b/app/Listeners/Invoice/InvoiceTransactionEventEntry.php @@ -12,18 +12,11 @@ namespace App\Listeners\Invoice; +use App\Utils\BcMath; use App\Models\Invoice; -use App\Models\Activity; use App\Models\TransactionEvent; use Illuminate\Support\Collection; -use App\DataMapper\TaxReport\TaxDetail; -use App\DataMapper\TaxReport\TaxReport; -use App\DataMapper\TaxReport\TaxSummary; -use App\Repositories\ActivityRepository; -use Illuminate\Contracts\Queue\ShouldQueue; use App\DataMapper\TransactionEventMetadata; -use Illuminate\Queue\Middleware\WithoutOverlapping; - class InvoiceTransactionEventEntry { @@ -31,6 +24,8 @@ class InvoiceTransactionEventEntry private float $paid_ratio; + private string $entry_type = 'updated'; + /** * Handle the event. * @@ -44,7 +39,37 @@ class InvoiceTransactionEventEntry $this->setPaidRatio($invoice); - $period = $force_period ?? now()->endOfMonth()->format('Y-m-d'); + $event = $invoice->transaction_events() + ->where('event_id', TransactionEvent::INVOICE_UPDATED) + ->orderBy('timestamp', 'desc') + ->first(); + + + if($event){ + + $this->entry_type = 'delta'; + + if($invoice->is_deleted && $event->metadata->tax_report->tax_summary->status == 'deleted'){ + // Invoice was previously deleted, and is still deleted... return early!! + return; + } + else if(in_array($invoice->status_id,[Invoice::STATUS_CANCELLED]) && $event->metadata->tax_report->tax_summary->status == 'cancelled'){ + // Invoice was previously cancelled, and is still cancelled... return early!! + return; + } + else if (!$invoice->is_deleted && $event->metadata->tax_report->tax_summary->status == 'deleted'){ + //restored invoice must be reported!!!! _do not return early!! + $this->entry_type = 'restored'; + } + /** If the invoice hasn't changed its state... return early!! */ + else if(BcMath::comp($invoice->amount, $event->invoice_amount) == 0){ + return; + } + + } + + //Long running tasks may spill over into the next day therefore month! + $period = $force_period ?? now()->endOfMonth()->subHours(5)->format('Y-m-d'); $this->payments = $invoice->payments->flatMap(function ($payment) { return $payment->invoices()->get()->map(function ($invoice) use ($payment) { @@ -91,6 +116,53 @@ class InvoiceTransactionEventEntry { return round($amount * $this->paid_ratio, 2); } + + /** + * calculateDeltaMetaData + * + * Calculates the differential between this period and the previous period. + * + * @param mixed $invoice + * + */ + private function calculateDeltaMetaData($invoice) + { + $calc = $invoice->calc(); + + $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') + ->first(); + + foreach ($taxes as $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']), + 'tax_amount_paid' => $this->calculateRatio($tax['total']), + 'tax_amount_remaining' => 0, + ]; + $details[] = $tax_detail; + } + + return new TransactionEventMetadata([ + 'tax_report' => [ + 'tax_details' => $details, + 'payment_history' => $this->payments->toArray() ?? [], //@phpstan-ignore-line + 'tax_summary' => [ + 'total_taxes' => $invoice->total_taxes, + 'total_paid' => $this->getTotalTaxPaid($invoice), + 'status' => 'updated', + 'adjustment' => round($invoice->amount - $previous_transaction_event->invoice_amount,2) + ], + ], + ]); + } /** * Existing tax details are not deleted, but pending taxes are set to 0 @@ -164,7 +236,7 @@ class InvoiceTransactionEventEntry 'payment_history' => $this->payments->toArray(), 'tax_summary' => [ 'total_taxes' => $invoice->total_taxes, - 'total_paid' => $this->getTotalTaxPaid($invoice),0, + 'total_paid' => $this->getTotalTaxPaid($invoice), 'status' => 'deleted', ], ], @@ -179,6 +251,8 @@ class InvoiceTransactionEventEntry return $this->getCancelledMetaData($invoice); } elseif ($invoice->is_deleted) { return $this->getDeletedMetaData($invoice); + } elseif ($this->entry_type == 'delta') { + return $this->calculateDeltaMetaData($invoice); } $calc = $invoice->calc();