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_taxes; // Tax collected and confirmed (ie. Invoice Paid)
|
||||||
public float $total_paid; // Tax pending collection (Outstanding tax of balance owing)
|
public float $total_paid; // Tax pending collection (Outstanding tax of balance owing)
|
||||||
public string $status;
|
public string $status;
|
||||||
|
public float $adjustment;
|
||||||
public function __construct(array $attributes = [])
|
public function __construct(array $attributes = [])
|
||||||
{
|
{
|
||||||
$this->total_taxes = $attributes['total_taxes'] ?? 0.0;
|
$this->total_taxes = $attributes['total_taxes'] ?? 0.0;
|
||||||
$this->total_paid = $attributes['total_paid'] ?? 0.0;
|
$this->total_paid = $attributes['total_paid'] ?? 0.0;
|
||||||
$this->status = $attributes['status'] ?? 'updated';
|
$this->status = $attributes['status'] ?? 'updated';
|
||||||
|
$this->adjustment = $attributes['adjustment'] ?? 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toArray(): array
|
public function toArray(): array
|
||||||
|
|
@ -34,6 +35,7 @@ class TaxSummary
|
||||||
'total_taxes' => $this->total_taxes,
|
'total_taxes' => $this->total_taxes,
|
||||||
'total_paid' => $this->total_paid,
|
'total_paid' => $this->total_paid,
|
||||||
'status' => $this->status,
|
'status' => $this->status,
|
||||||
|
'adjustment' => $this->adjustment,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ class InvoiceTransactionEventEntry
|
||||||
'event_id' => $invoice->is_deleted ? TransactionEvent::INVOICE_DELETED : TransactionEvent::INVOICE_UPDATED,
|
'event_id' => $invoice->is_deleted ? TransactionEvent::INVOICE_DELETED : TransactionEvent::INVOICE_UPDATED,
|
||||||
'timestamp' => now()->timestamp,
|
'timestamp' => now()->timestamp,
|
||||||
'metadata' => $this->getMetadata($invoice),
|
'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_balance
|
||||||
* @property string $credit_amount
|
* @property string $credit_amount
|
||||||
* @property int|null $credit_status
|
* @property int|null $credit_status
|
||||||
|
* @property Carbon|null $period
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|StaticModel company()
|
* @method static \Illuminate\Database\Eloquent\Builder|StaticModel company()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|StaticModel exclude($columns)
|
* @method static \Illuminate\Database\Eloquent\Builder|StaticModel exclude($columns)
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
|
|
@ -56,11 +57,12 @@ class TransactionEvent extends StaticModel
|
||||||
'metadata' => TransactionEventMetadata::class,
|
'metadata' => TransactionEventMetadata::class,
|
||||||
'payment_request' => 'array',
|
'payment_request' => 'array',
|
||||||
'paymentables' => 'array',
|
'paymentables' => 'array',
|
||||||
|
'period' => 'date',
|
||||||
];
|
];
|
||||||
|
|
||||||
public const INVOICE_UPDATED = 1;
|
public const INVOICE_UPDATED = 1;
|
||||||
|
|
||||||
public const INVOICE_DELETED = 2;
|
public const PAYMENT_REFUNDED = 2;
|
||||||
|
|
||||||
public const PAYMENT_DELETED = 3;
|
public const PAYMENT_DELETED = 3;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,11 @@
|
||||||
|
|
||||||
namespace App\Services\Payment;
|
namespace App\Services\Payment;
|
||||||
|
|
||||||
use App\Models\BankTransaction;
|
|
||||||
use App\Models\Credit;
|
use App\Models\Credit;
|
||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
use App\Models\Payment;
|
use App\Models\Payment;
|
||||||
|
use App\Models\BankTransaction;
|
||||||
|
use App\Listeners\Payment\PaymentTransactionEventEntry;
|
||||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||||
|
|
||||||
class DeletePayment
|
class DeletePayment
|
||||||
|
|
@ -87,6 +88,9 @@ class DeletePayment
|
||||||
$this->_paid_to_date_deleted = 0;
|
$this->_paid_to_date_deleted = 0;
|
||||||
|
|
||||||
if ($this->payment->invoices()->exists()) {
|
if ($this->payment->invoices()->exists()) {
|
||||||
|
|
||||||
|
$invoice_ids = $this->payment->invoices()->pluck('id');
|
||||||
|
|
||||||
$this->payment->invoices()->each(function ($paymentable_invoice) {
|
$this->payment->invoices()->each(function ($paymentable_invoice) {
|
||||||
$net_deletable = $paymentable_invoice->pivot->amount - $paymentable_invoice->pivot->refunded;
|
$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.
|
//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;
|
namespace App\Services\Payment;
|
||||||
|
|
||||||
use App\Exceptions\PaymentRefundFailed;
|
use stdClass;
|
||||||
use App\Jobs\Payment\EmailRefundPayment;
|
use App\Utils\Ninja;
|
||||||
use App\Models\Activity;
|
|
||||||
use App\Models\Credit;
|
use App\Models\Credit;
|
||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
use App\Models\Payment;
|
use App\Models\Payment;
|
||||||
|
use App\Models\Activity;
|
||||||
|
use App\Exceptions\PaymentRefundFailed;
|
||||||
|
use App\Jobs\Payment\EmailRefundPayment;
|
||||||
use App\Repositories\ActivityRepository;
|
use App\Repositories\ActivityRepository;
|
||||||
use App\Utils\Ninja;
|
use App\Listeners\Payment\PaymentTransactionEventEntry;
|
||||||
use stdClass;
|
|
||||||
|
|
||||||
class RefundPayment
|
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 {
|
} 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.
|
//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 Illuminate\Database\Eloquent\Builder;
|
||||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
|
use App\Listeners\Invoice\InvoiceTransactionEventEntry;
|
||||||
|
|
||||||
class TaxReport
|
class TaxReport
|
||||||
{
|
{
|
||||||
|
|
@ -189,53 +190,16 @@ class TaxReport
|
||||||
/** @var Invoice $invoice */
|
/** @var Invoice $invoice */
|
||||||
foreach($this->query->cursor() as $invoice){
|
foreach($this->query->cursor() as $invoice){
|
||||||
|
|
||||||
$calc = $invoice->calc();
|
if($invoice->transaction_events->count() == 0){
|
||||||
//Combine the line taxes with invoice taxes here to get a total tax amount
|
(new InvoiceTransactionEventEntry())->run($invoice);
|
||||||
$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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$payment_amount = round($payment_amount,2);
|
//get the invoice state as at the end of the current period.
|
||||||
$invoice_amount = round($invoice->amount,2);
|
$invoice->transaction_events->each(function($event){
|
||||||
|
|
||||||
$total_taxes = round($invoice->total_taxes,2);
|
|
||||||
|
|
||||||
$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'][] = [
|
//anything period the reporting period is considered an ADJUSTMENT
|
||||||
$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'] ?? '',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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