Padding tax reports
This commit is contained in:
parent
3b9962fb22
commit
33c5b90dce
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,234 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Listeners\Payment;
|
||||
|
||||
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 App\Libraries\MultiDB;
|
||||
use App\Models\Payment;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class PaymentTransactionEventEntry implements ShouldQueue
|
||||
{
|
||||
use Dispatchable;
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public $tries = 1;
|
||||
|
||||
private float $paid_ratio;
|
||||
|
||||
private Collection $payments;
|
||||
/**
|
||||
*/
|
||||
public function __construct(private Payment $payment, private array $invoice_ids, private string $db)
|
||||
{}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
nlog("PaymentTransactionEventEntry::handle");
|
||||
//payment vs refunded
|
||||
MultiDB::setDb($this->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)];
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('transaction_events', function (Blueprint $table) {
|
||||
$table->date('period')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
}
|
||||
};
|
||||
Loading…
Reference in New Issue