diff --git a/app/DataMapper/TaxReport/TaxSummary.php b/app/DataMapper/TaxReport/TaxSummary.php index f8a360e96a..e73678057c 100644 --- a/app/DataMapper/TaxReport/TaxSummary.php +++ b/app/DataMapper/TaxReport/TaxSummary.php @@ -20,12 +20,13 @@ 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 float $adjustment; public function __construct(array $attributes = []) { $this->total_taxes = $attributes['total_taxes'] ?? 0.0; $this->total_paid = $attributes['total_paid'] ?? 0.0; $this->status = $attributes['status'] ?? 'updated'; + $this->adjustment = $attributes['adjustment'] ?? 0.0; } public function toArray(): array @@ -34,6 +35,7 @@ class TaxSummary 'total_taxes' => $this->total_taxes, 'total_paid' => $this->total_paid, 'status' => $this->status, + 'adjustment' => $this->adjustment, ]; } } diff --git a/app/Listeners/Invoice/InvoiceTransactionEventEntry.php b/app/Listeners/Invoice/InvoiceTransactionEventEntry.php index 0a9f1a1f00..6580b047ef 100644 --- a/app/Listeners/Invoice/InvoiceTransactionEventEntry.php +++ b/app/Listeners/Invoice/InvoiceTransactionEventEntry.php @@ -67,6 +67,7 @@ class InvoiceTransactionEventEntry 'event_id' => $invoice->is_deleted ? TransactionEvent::INVOICE_DELETED : TransactionEvent::INVOICE_UPDATED, 'timestamp' => now()->timestamp, 'metadata' => $this->getMetadata($invoice), + 'period' => now()->endOfMonth()->format('Y-m-d'), ]); } diff --git a/app/Listeners/Payment/PaymentTransactionEventEntry.php b/app/Listeners/Payment/PaymentTransactionEventEntry.php new file mode 100644 index 0000000000..0e213f53d1 --- /dev/null +++ b/app/Listeners/Payment/PaymentTransactionEventEntry.php @@ -0,0 +1,234 @@ +db); + + $this->payments = $this->payment + ->invoices() + ->get() + ->filter(function($invoice){ + //only insert adjustment entries if we are after the end of the month!! + return Carbon::parse($invoice->date)->endOfMonth()->isBefore(now()->addSeconds($this->payment->company->timezone_offset())); + }) + ->map(function ($invoice) { + return [ + 'number' => $this->payment->number, + 'amount' => $invoice->pivot->amount, + 'refunded' => $invoice->pivot->refunded, + 'date' => $invoice->pivot->created_at->format('Y-m-d'), + ]; + }); + + Invoice::withTrashed() + ->whereIn('id', $this->invoice_ids) + ->get() + ->filter(function($invoice){ + //only insert adjustment entries if we are after the end of the month!! + return Carbon::parse($invoice->date)->endOfMonth()->isBefore(now()->addSeconds($this->payment->company->timezone_offset())); + }) + ->each(function($invoice){ + + $this->setPaidRatio($invoice); + + TransactionEvent::create([ + 'invoice_id' => $invoice->id, + 'client_id' => $invoice->client_id, + 'client_balance' => $invoice->client->balance, + 'client_paid_to_date' => $invoice->client->paid_to_date, + 'client_credit_balance' => $invoice->client->credit_balance, + 'invoice_balance' => $invoice->balance ?? 0, + 'invoice_amount' => $invoice->amount ?? 0 , + 'invoice_partial' => $invoice->partial ?? 0, + 'invoice_paid_to_date' => $invoice->paid_to_date ?? 0, + 'invoice_status' => $invoice->is_deleted ? 7 : $invoice->status_id, + 'event_id' => $this->payment->is_deleted ? TransactionEvent::PAYMENT_DELETED : TransactionEvent::PAYMENT_REFUNDED, + 'timestamp' => now()->timestamp, + 'metadata' => $this->getMetadata($invoice), + 'period' => now()->endOfMonth()->format('Y-m-d'), + 'payment_id' => $this->payment->id, + 'payment_amount' => $this->payment->amount, + 'payment_refunded' => $this->payment->refunded, + 'payment_applied' => $this->payment->applied, + 'payment_status' => $this->payment->status_id, + ]); + }); + } + + private function setPaidRatio(Invoice $invoice): self + { + if ($invoice->amount == 0) { + $this->paid_ratio = 0; + return $this; + } + + $this->paid_ratio = $invoice->paid_to_date / $invoice->amount; + + return $this; + } + + private function calculateRatio(float $amount): float + { + return round($amount * $this->paid_ratio, 2); + } + + /** + * Existing tax details are not deleted, but pending taxes are set to 0 + * + * @param mixed $invoice + */ + private function getRefundedMetaData($invoice) + { + + $calc = $invoice->calc(); + + $details = []; + + $taxes = array_merge($calc->getTaxMap()->merge($calc->getTotalTaxMap())->toArray()); + + foreach ($taxes as $tax) { + $tax_detail = [ + 'tax_name' => $tax['name'], + 'tax_rate' => $tax['tax_rate'], + 'taxable_amount' => $tax['base_amount'] ?? $calc->getNetSubtotal(), + 'tax_amount' => $tax['total'], + 'tax_amount_paid' => $this->calculateRatio($tax['total']), + 'tax_amount_remaining' => round($tax['total'] - $this->calculateRatio($tax['total']), 2), + ]; + $details[] = $tax_detail; + } + + return new TransactionEventMetadata([ + 'tax_report' => [ + 'tax_details' => $details, + 'payment_history' => $this->payments->toArray(), + 'tax_summary' => [ + 'total_taxes' => $invoice->total_taxes, + 'total_paid' => $this->getTotalTaxPaid($invoice), + 'adjustment' => round($invoice->total_taxes - $this->getTotalTaxPaid($invoice), 2) * -1, + 'status' => 'adjustment', + ], + ], + ]); + + } + + /** + * Set all tax details to 0 + * + * @param mixed $invoice + */ + private function getDeletedMetaData($invoice) + { + + $calc = $invoice->calc(); + + $details = []; + + $taxes = array_merge($calc->getTaxMap()->merge($calc->getTotalTaxMap())->toArray()); + + foreach ($taxes as $tax) { + $tax_detail = [ + 'tax_name' => $tax['name'], + 'tax_rate' => $tax['tax_rate'], + 'taxable_amount' => $tax['base_amount'] ?? $calc->getNetSubtotal(), + 'tax_amount' => $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(), + 'tax_summary' => [ + 'total_taxes' => $invoice->total_taxes, + 'total_paid' => $this->getTotalTaxPaid($invoice), + 'adjustment' => round($invoice->total_taxes - $this->getTotalTaxPaid($invoice), 2) * -1, + 'status' => 'adjustment', + ], + ], + ]); + + } + + private function getMetadata($invoice) + { + + if ($this->payment->is_deleted) { + return $this->getDeletedMetaData($invoice); + } else { + return $this->getRefundedMetaData($invoice); + } + + } + + private function getTotalTaxPaid($invoice) + { + if ($invoice->amount == 0) { + return 0; + } + + $total_paid = $this->payments->sum('amount') - $this->payments->sum('refunded'); + + return round($invoice->total_taxes * ($total_paid / $invoice->amount), 2); + + } + + public function middleware() + { + return [(new WithoutOverlapping("payment_transaction_event_entry_".$this->payment->id.'_'.$this->db))->releaseAfter(10)->expireAfter(30)]; + } +} diff --git a/app/Models/TransactionEvent.php b/app/Models/TransactionEvent.php index cccf52e904..38ff4aafaa 100644 --- a/app/Models/TransactionEvent.php +++ b/app/Models/TransactionEvent.php @@ -42,6 +42,7 @@ use App\DataMapper\TransactionEventMetadata; * @property string $credit_balance * @property string $credit_amount * @property int|null $credit_status + * @property Carbon|null $period * @method static \Illuminate\Database\Eloquent\Builder|StaticModel company() * @method static \Illuminate\Database\Eloquent\Builder|StaticModel exclude($columns) * @mixin \Eloquent @@ -56,11 +57,12 @@ class TransactionEvent extends StaticModel 'metadata' => TransactionEventMetadata::class, 'payment_request' => 'array', 'paymentables' => 'array', + 'period' => 'date', ]; public const INVOICE_UPDATED = 1; - public const INVOICE_DELETED = 2; + public const PAYMENT_REFUNDED = 2; public const PAYMENT_DELETED = 3; } diff --git a/app/Services/Payment/DeletePayment.php b/app/Services/Payment/DeletePayment.php index df679a8bf3..907c0e9edc 100644 --- a/app/Services/Payment/DeletePayment.php +++ b/app/Services/Payment/DeletePayment.php @@ -12,10 +12,11 @@ namespace App\Services\Payment; -use App\Models\BankTransaction; use App\Models\Credit; use App\Models\Invoice; use App\Models\Payment; +use App\Models\BankTransaction; +use App\Listeners\Payment\PaymentTransactionEventEntry; use Illuminate\Contracts\Container\BindingResolutionException; class DeletePayment @@ -87,6 +88,9 @@ class DeletePayment $this->_paid_to_date_deleted = 0; if ($this->payment->invoices()->exists()) { + + $invoice_ids = $this->payment->invoices()->pluck('id'); + $this->payment->invoices()->each(function ($paymentable_invoice) { $net_deletable = $paymentable_invoice->pivot->amount - $paymentable_invoice->pivot->refunded; @@ -159,6 +163,8 @@ class DeletePayment }); + + PaymentTransactionEventEntry::dispatch($this->payment, $invoice_ids, $this->payment->company->db); } //sometimes the payment is NOT created properly, this catches the payment and prevents the paid to date reducing inappropriately. diff --git a/app/Services/Payment/RefundPayment.php b/app/Services/Payment/RefundPayment.php index 701ed5f79b..2f0e54cead 100644 --- a/app/Services/Payment/RefundPayment.php +++ b/app/Services/Payment/RefundPayment.php @@ -12,15 +12,16 @@ namespace App\Services\Payment; -use App\Exceptions\PaymentRefundFailed; -use App\Jobs\Payment\EmailRefundPayment; -use App\Models\Activity; +use stdClass; +use App\Utils\Ninja; use App\Models\Credit; use App\Models\Invoice; use App\Models\Payment; +use App\Models\Activity; +use App\Exceptions\PaymentRefundFailed; +use App\Jobs\Payment\EmailRefundPayment; use App\Repositories\ActivityRepository; -use App\Utils\Ninja; -use stdClass; +use App\Listeners\Payment\PaymentTransactionEventEntry; class RefundPayment { @@ -312,6 +313,8 @@ class RefundPayment } + PaymentTransactionEventEntry::dispatch($this->payment, array_column($this->refund_data['invoices'], 'invoice_id'), $this->payment->company->db); + } else { //if we are refunding and no payments have been tagged, then we need to decrement the client->paid_to_date by the total refund amount. diff --git a/app/Services/Report/XLS/TaxReport.php b/app/Services/Report/XLS/TaxReport.php index 0e58d0aef7..958345516f 100644 --- a/app/Services/Report/XLS/TaxReport.php +++ b/app/Services/Report/XLS/TaxReport.php @@ -11,6 +11,7 @@ use App\Services\Report\TaxSummaryReport; use Illuminate\Database\Eloquent\Builder; use PhpOffice\PhpSpreadsheet\Spreadsheet; use App\Models\Invoice; +use App\Listeners\Invoice\InvoiceTransactionEventEntry; class TaxReport { @@ -189,53 +190,16 @@ class TaxReport /** @var Invoice $invoice */ foreach($this->query->cursor() as $invoice){ - $calc = $invoice->calc(); - //Combine the line taxes with invoice taxes here to get a total tax amount - $taxes = array_merge($calc->getTaxMap()->merge($calc->getTotalTaxMap())->toArray()); - - $payment_amount = 0; - - foreach ($invoice->payments()->get() as $payment) { - - if($payment->pivot->created_at->addSeconds($offset)->isBetween($start_date_instance,$end_date_instance)){ - $payment_amount += ($payment->pivot->amount - $payment->pivot->refunded); - } + if($invoice->transaction_events->count() == 0){ + (new InvoiceTransactionEventEntry())->run($invoice); } - $payment_amount = round($payment_amount,2); - $invoice_amount = round($invoice->amount,2); - - $total_taxes = round($invoice->total_taxes,2); + //get the invoice state as at the end of the current period. + $invoice->transaction_events->each(function($event){ - $pro_rata_payment_ratio = $payment_amount != 0 ? ($payment_amount/$invoice_amount) : 0; - $total_taxes = $payment_amount != 0 ? ($payment_amount/$invoice_amount) * $invoice->total_taxes : 0; - $taxable_amount = $calc->getNetSubtotal(); + }); - $this->data['invoices'][] = [ - $invoice->number, - $invoice->date, - $invoice_amount, - $payment_amount, - $total_taxes, - $taxable_amount, - ]; - - - foreach($taxes as $tax){ - - $this->data['invoice_items'][] = [ - $invoice->number, - $invoice->date, - $invoice_amount, - $payment_amount, - $tax['name'], - $tax['tax_rate'], - $tax['total'], - $tax['total'] * $pro_rata_payment_ratio, - $tax['base_amount'] ?? $calc->getNetSubtotal(), - $tax['nexus'] ?? '', - ]; - } + //anything period the reporting period is considered an ADJUSTMENT } diff --git a/database/migrations/2025_08_05_023807_add_date_period_to_transaction_events_table.php b/database/migrations/2025_08_05_023807_add_date_period_to_transaction_events_table.php new file mode 100644 index 0000000000..bf79aec457 --- /dev/null +++ b/database/migrations/2025_08_05_023807_add_date_period_to_transaction_events_table.php @@ -0,0 +1,25 @@ +date('period')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + } +};