Add invoice tax summary data
This commit is contained in:
parent
0d1625644f
commit
4ceb15773e
|
|
@ -0,0 +1,45 @@
|
|||
<?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\Casts;
|
||||
|
||||
use App\DataMapper\TransactionEventMetadata;
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
|
||||
class TransactionEventMetadataCast implements CastsAttributes
|
||||
{
|
||||
public function get($model, string $key, $value, array $attributes)
|
||||
{
|
||||
if (is_null($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = json_decode($value, true);
|
||||
|
||||
if (!is_array($data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new TransactionEventMetadata($data);
|
||||
}
|
||||
|
||||
public function set($model, string $key, $value, array $attributes)
|
||||
{
|
||||
if (is_null($value)) {
|
||||
return [$key => null];
|
||||
}
|
||||
|
||||
return [
|
||||
$key => json_encode($value->toArray())
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@ use App\Jobs\Ninja\BankTransactionSync;
|
|||
use App\Jobs\Cron\RecurringExpensesCron;
|
||||
use App\Jobs\Cron\RecurringInvoicesCron;
|
||||
use App\Jobs\EDocument\EInvoicePullDocs;
|
||||
use App\Jobs\Cron\InvoiceTaxSummary;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use App\Jobs\Invoice\InvoiceCheckLateWebhook;
|
||||
use App\Jobs\Subscription\CleanStaleInvoiceOrder;
|
||||
|
|
@ -72,6 +73,32 @@ class Kernel extends ConsoleKernel
|
|||
/* Checks for scheduled tasks */
|
||||
$schedule->job(new TaskScheduler())->hourlyAt(10)->withoutOverlapping()->name('task-scheduler-job')->onOneServer();
|
||||
|
||||
/* Generates the tax summary for invoices */
|
||||
$schedule->job(new InvoiceTaxSummary())->monthly('23:30')->withoutOverlapping()->name('invoice-tax-summary-job')->onOneServer();
|
||||
|
||||
// Run hourly over 26-hour period for complete timezone coverage
|
||||
$schedule->job(new InvoiceTaxSummary())
|
||||
->hourly()
|
||||
->when(function () {
|
||||
$now = now();
|
||||
$hour = $now->hour;
|
||||
|
||||
// Run for 26 hours starting from UTC 10:00 on last day of month
|
||||
// This covers the transition period when timezones move to next month
|
||||
if ($now->isLastOfMonth()) {
|
||||
// Start at UTC 10:00 (when UTC+14 moves to next day)
|
||||
return $hour >= 10;
|
||||
} elseif ($now->isFirstOfMonth()) {
|
||||
// Continue until UTC 12:00 (when UTC-12 moves to next day)
|
||||
return $hour <= 12;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
->withoutOverlapping()
|
||||
->name('invoice-tax-summary-26hour-coverage')
|
||||
->onOneServer();
|
||||
|
||||
/* Checks Rotessa Transactions */
|
||||
$schedule->job(new TransactionReport())->dailyAt('01:48')->withoutOverlapping()->name('rotessa-transaction-report')->onOneServer();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
<?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\DataMapper\TaxReport;
|
||||
|
||||
/**
|
||||
* Payment history for tracking partial payments across periods
|
||||
*/
|
||||
class PaymentHistory
|
||||
{
|
||||
public string $number;
|
||||
public string $date;
|
||||
public float $amount;
|
||||
public float $refunded;
|
||||
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
$this->number = $attributes['number'] ?? '';
|
||||
$this->date = $attributes['date'] ?? '';
|
||||
$this->amount = $attributes['amount'] ?? 0.0;
|
||||
$this->refunded = $attributes['refunded'] ?? 0.0;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'number' => $this->number,
|
||||
'date' => $this->date,
|
||||
'amount' => $this->amount,
|
||||
'refunded' => $this->refunded,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
<?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\DataMapper\TaxReport;
|
||||
|
||||
/**
|
||||
* Individual tax detail object with status tracking
|
||||
*/
|
||||
class TaxDetail
|
||||
{
|
||||
public string $tax_name; // e.g., Sales Tax
|
||||
public float $tax_rate = 0; //21%
|
||||
public string $nexus; // Tax jurisdiction nexus (e.g. "CA", "NY", "FL")
|
||||
public string $country_nexus; // Country nexus (e.g. "US", "UK", "CA")
|
||||
public float $taxable_amount; // net amount exclusive of taxes
|
||||
public float $tax_amount; // total tax amount
|
||||
public float $tax_amount_paid; // Amount actually paid (Based on the payment history)
|
||||
public float $tax_amount_remaining; // Amount still pending
|
||||
public string $tax_status; // "collected", "pending", "refundable", "partially_paid", "adjustment"
|
||||
|
||||
// Adjustment-specific fields (used when tax_status is "adjustment")
|
||||
public ?string $adjustment_reason; // "invoice_cancelled", "tax_rate_change", "exemption_applied", "correction"
|
||||
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
$this->tax_name = $attributes['tax_name'] ?? '';
|
||||
$this->tax_rate = $attributes['tax_rate'] ?? 0;
|
||||
$this->nexus = $attributes['nexus'] ?? '';
|
||||
$this->country_nexus = $attributes['country_nexus'] ?? '';
|
||||
$this->taxable_amount = $attributes['taxable_amount'] ?? 0.0;
|
||||
$this->tax_amount = $attributes['tax_amount'] ?? 0.0;
|
||||
$this->tax_amount_paid = $attributes['tax_amount_paid'] ?? 0.0;
|
||||
$this->tax_amount_remaining = $attributes['tax_amount_remaining'] ?? 0.0;
|
||||
$this->tax_status = $attributes['tax_status'] ?? 'pending';
|
||||
|
||||
// Adjustment fields
|
||||
$this->adjustment_reason = $attributes['adjustment_reason'] ?? null;
|
||||
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$data = [
|
||||
'tax_name' => $this->tax_name,
|
||||
'tax_rate' => $this->tax_rate,
|
||||
'nexus' => $this->nexus,
|
||||
'country_nexus' => $this->country_nexus,
|
||||
'taxable_amount' => $this->taxable_amount,
|
||||
'tax_amount' => $this->tax_amount,
|
||||
'tax_amount_paid' => $this->tax_amount_paid,
|
||||
'tax_amount_remaining' => $this->tax_amount_remaining,
|
||||
'tax_status' => $this->tax_status,
|
||||
'adjustment_reason' => $this->adjustment_reason,
|
||||
];
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?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\DataMapper\TaxReport;
|
||||
|
||||
use App\DataMapper\TaxReport\TaxDetail;
|
||||
use App\DataMapper\TaxReport\TaxSummary;
|
||||
|
||||
/**
|
||||
* Tax report object for InvoiceSync - tracks incremental tax history
|
||||
*/
|
||||
class TaxReport
|
||||
{
|
||||
public ?TaxSummary $tax_summary; // Summary totals
|
||||
public ?array $tax_details; // Array of TaxDetail objects (includes adjustments)
|
||||
public float $amount; // The total amount of the invoice
|
||||
public ?array $payment_history; // Array of PaymentHistory objects
|
||||
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
$this->tax_summary = isset($attributes['tax_summary'])
|
||||
? new TaxSummary($attributes['tax_summary'])
|
||||
: null;
|
||||
$this->tax_details = isset($attributes['tax_details'])
|
||||
? array_map(fn ($detail) => new TaxDetail($detail), $attributes['tax_details'])
|
||||
: null;
|
||||
$this->payment_history = isset($attributes['payment_history'])
|
||||
? array_map(fn ($payment) => new PaymentHistory($payment), $attributes['payment_history'])
|
||||
: null;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'tax_summary' => $this->tax_summary?->toArray(),
|
||||
'tax_details' => $this->tax_details ? array_map(fn ($detail) => $detail->toArray(), $this->tax_details) : null,
|
||||
'payment_history' => $this->payment_history ? array_map(fn ($payment) => $payment->toArray(), $this->payment_history) : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?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\DataMapper\TaxReport;
|
||||
|
||||
/**
|
||||
* Tax summary with totals for different tax states
|
||||
*/
|
||||
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 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';
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'total_taxes' => $this->total_taxes,
|
||||
'total_paid' => $this->total_paid,
|
||||
'status' => $this->status,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
<?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\DataMapper;
|
||||
|
||||
use App\Casts\TransactionEventMetadataCast;
|
||||
use App\DataMapper\TaxReport\TaxReport;
|
||||
use Illuminate\Contracts\Database\Eloquent\Castable;
|
||||
|
||||
/**
|
||||
* TransactionEventMetadata.
|
||||
*/
|
||||
class TransactionEventMetadata implements Castable
|
||||
{
|
||||
public TaxReport $tax_report;
|
||||
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
$this->tax_report = isset($attributes['tax_report'])
|
||||
? new TaxReport($attributes['tax_report'])
|
||||
: new TaxReport([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the caster class to use when casting from / to this cast target.
|
||||
*
|
||||
* @param array<string, mixed> $arguments
|
||||
*/
|
||||
public static function castUsing(array $arguments): string
|
||||
{
|
||||
return TransactionEventMetadataCast::class;
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self($data);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'tax_report' => $this->tax_report->toArray(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -241,14 +241,6 @@ class InvoiceController extends BaseController
|
|||
|
||||
event(new InvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars($user ? $user->id : null)));
|
||||
|
||||
$transaction = [
|
||||
'invoice' => $invoice->transaction_event(),
|
||||
'payment' => [],
|
||||
'client' => $invoice->client->transaction_event(),
|
||||
'credit' => [],
|
||||
'metadata' => [],
|
||||
];
|
||||
|
||||
return $this->itemResponse($invoice);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,162 @@
|
|||
<?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\Jobs\Cron;
|
||||
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Webhook;
|
||||
use App\Models\Company;
|
||||
use App\Models\Timezone;
|
||||
use App\Libraries\MultiDB;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use App\Jobs\Entity\EmailEntity;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Carbon\Carbon;
|
||||
use App\Listeners\Invoice\InvoiceTransactionEventEntry;
|
||||
|
||||
class InvoiceTaxSummary implements ShouldQueue
|
||||
{
|
||||
use Dispatchable;
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public $tries = 1;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$currentUtcHour = now()->hour;
|
||||
$transitioningTimezones = $this->getTransitioningTimezones($currentUtcHour);
|
||||
|
||||
foreach(MultiDB::$dbs as $db) {
|
||||
MultiDB::setDB($db);
|
||||
// Only process companies in timezones that just transitioned
|
||||
$companies = $this->getCompaniesInTimezones($transitioningTimezones);
|
||||
|
||||
foreach ($companies as $company) {
|
||||
|
||||
$this->processCompanyTaxSummary($company);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getTransitioningTimezones($utcHour)
|
||||
{
|
||||
$transitioningTimezones = [];
|
||||
|
||||
// Get all timezones from the database
|
||||
$timezones = app('timezones');
|
||||
|
||||
foreach ($timezones as $timezone) {
|
||||
// Calculate the current UTC offset for this timezone (accounting for DST)
|
||||
$currentOffset = $this->getCurrentUtcOffset($timezone->name);
|
||||
|
||||
// Calculate when this timezone transitions to the next day
|
||||
$transitionHour = $this->getTimezoneTransitionHour($currentOffset);
|
||||
|
||||
// If this timezone transitions at the current UTC hour, include it
|
||||
if ($transitionHour === $utcHour) {
|
||||
$transitioningTimezones[] = $timezone->id;
|
||||
}
|
||||
}
|
||||
|
||||
return $transitioningTimezones;
|
||||
}
|
||||
|
||||
private function getCurrentUtcOffset($timezoneName)
|
||||
{
|
||||
try {
|
||||
$dateTime = new \DateTime('now', new \DateTimeZone($timezoneName));
|
||||
return $dateTime->getOffset();
|
||||
} catch (\Exception $e) {
|
||||
// Fallback to UTC if timezone is invalid
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private function getTimezoneTransitionHour($utcOffset)
|
||||
{
|
||||
// Calculate which UTC hour this timezone transitions to the next day
|
||||
// A timezone with UTC offset +X transitions at UTC hour (24 - X)
|
||||
// For example: UTC+14 transitions at UTC 10:00 (24 - 14 = 10)
|
||||
// UTC-12 transitions at UTC 12:00 (24 - (-12) = 36, but we use modulo 24)
|
||||
|
||||
$transitionHour = (24 - ($utcOffset / 3600)) % 24;
|
||||
|
||||
// Handle negative offsets properly
|
||||
if ($transitionHour < 0) {
|
||||
$transitionHour += 24;
|
||||
}
|
||||
|
||||
return (int) $transitionHour;
|
||||
}
|
||||
|
||||
private function getCompaniesInTimezones($timezoneIds)
|
||||
{
|
||||
if (empty($timezoneIds)) {
|
||||
return collect(); // No companies to process
|
||||
}
|
||||
|
||||
// Get companies that have timezone_id in their JSON settings matching the transitioning timezones
|
||||
return Company::whereRaw("JSON_EXTRACT(settings, '$.timezone_id') IN (" . implode(',', $timezoneIds) . ")")->get();
|
||||
}
|
||||
|
||||
private function processCompanyTaxSummary($company)
|
||||
{
|
||||
// Your existing tax summary logic here
|
||||
// This will only run for companies in timezones that just transitioned
|
||||
|
||||
$startDate = now()->subMonth()->startOfMonth()->format('Y-m-d');
|
||||
$endDate = now()->subMonth()->endOfMonth()->format('Y-m-d');
|
||||
|
||||
// Process tax summary for the company
|
||||
$this->generateTaxSummary($company, $startDate, $endDate);
|
||||
}
|
||||
|
||||
private function generateTaxSummary($company, $startDate, $endDate)
|
||||
{
|
||||
$todayStart = now()->subHours(15)->timestamp;
|
||||
$todayEnd = now()->endOfDay()->timestamp;
|
||||
|
||||
Invoice::withTrashed()
|
||||
->with('payments')
|
||||
->where('company_id', $company->id)
|
||||
->whereIn('status_id', [2,3,4,5])
|
||||
->where('is_deleted', 0)
|
||||
->whereHas('client', function ($query) {
|
||||
$query->where('is_deleted', false);
|
||||
})
|
||||
->whereHas('company', function ($query) {
|
||||
$query->where('is_disabled', 0)
|
||||
->whereHas('account', function ($q) {
|
||||
$q->where('is_flagged', false);
|
||||
});
|
||||
})
|
||||
->whereBetween('date', [$startDate, $endDate])
|
||||
->whereDoesntHave('transaction_events', function ($query) use ($todayStart, $todayEnd) {
|
||||
$query->where('timestamp', '>=', $todayStart)
|
||||
->where('timestamp', '<=', $todayEnd);
|
||||
})
|
||||
->cursor()
|
||||
->each(function (Invoice $invoice) {
|
||||
(new InvoiceTransactionEventEntry())->run($invoice);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
<?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\Invoice;
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
private Collection $payments;
|
||||
|
||||
private float $paid_ratio;
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param Invoice $invoice
|
||||
* @return void
|
||||
*/
|
||||
public function run($invoice)
|
||||
{
|
||||
|
||||
$this->setPaidRatio($invoice);
|
||||
|
||||
$this->payments = $invoice->payments->flatMap(function ($payment) {
|
||||
return $payment->invoices()->get()->map(function ($invoice) use ($payment) {
|
||||
return [
|
||||
'number' => $payment->number,
|
||||
'amount' => $invoice->pivot->amount,
|
||||
'refunded' => $invoice->pivot->refunded,
|
||||
'date' => $invoice->pivot->created_at->format('Y-m-d'),
|
||||
];
|
||||
});
|
||||
});
|
||||
|
||||
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' => $invoice->is_deleted ? TransactionEvent::INVOICE_DELETED : TransactionEvent::INVOICE_UPDATED,
|
||||
'timestamp' => now()->timestamp,
|
||||
'metadata' => $this->getMetadata($invoice),
|
||||
]);
|
||||
}
|
||||
|
||||
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 getCancelledMetaData($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' => $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(),
|
||||
'tax_summary' => [
|
||||
'total_taxes' => $invoice->total_taxes,
|
||||
'total_paid' => $this->getTotalTaxPaid($invoice),
|
||||
'status' => 'cancelled',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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),0,
|
||||
'status' => 'deleted',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
private function getMetadata($invoice)
|
||||
{
|
||||
|
||||
if ($invoice->status_id == Invoice::STATUS_CANCELLED) {
|
||||
return $this->getCancelledMetaData($invoice);
|
||||
} elseif ($invoice->is_deleted) {
|
||||
return $this->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' => $tax['total'] - $this->calculateRatio($tax['total']),
|
||||
];
|
||||
$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),
|
||||
'status' => 'updated',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -983,18 +983,6 @@ class Client extends BaseModel implements HasLocalePreference
|
|||
return $offset;
|
||||
}
|
||||
|
||||
public function transaction_event()
|
||||
{
|
||||
$client = $this->fresh();
|
||||
|
||||
return [
|
||||
'client_id' => $client->id,
|
||||
'client_balance' => $client->balance ?: 0,
|
||||
'client_paid_to_date' => $client->paid_to_date ?: 0,
|
||||
'client_credit_balance' => $client->credit_balance ?: 0,
|
||||
];
|
||||
}
|
||||
|
||||
public function translate_entity(): string
|
||||
{
|
||||
return ctrans('texts.client');
|
||||
|
|
|
|||
|
|
@ -407,18 +407,6 @@ class Credit extends BaseModel
|
|||
});
|
||||
}
|
||||
|
||||
public function transaction_event()
|
||||
{
|
||||
$credit = $this->fresh();
|
||||
|
||||
return [
|
||||
'credit_id' => $credit->id,
|
||||
'credit_amount' => $credit->amount ?: 0,
|
||||
'credit_balance' => $credit->balance ?: 0,
|
||||
'credit_status' => $credit->status_id ?: 1,
|
||||
];
|
||||
}
|
||||
|
||||
public function translate_entity(): string
|
||||
{
|
||||
return ctrans('texts.credit');
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ use App\Utils\Number;
|
|||
* @property-read int|null $tasks_count
|
||||
* @property-read \App\Models\User $user
|
||||
* @property-read \App\Models\Vendor|null $vendor
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\TransactionEvent> $transaction_events
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Activity> $activities
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\CompanyLedger> $company_ledger
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Credit> $credits
|
||||
|
|
@ -349,6 +350,11 @@ class Invoice extends BaseModel
|
|||
return $this->hasMany(InvoiceInvitation::class);
|
||||
}
|
||||
|
||||
public function transaction_events(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(TransactionEvent::class);
|
||||
}
|
||||
|
||||
public function client(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Client::class)->withTrashed();
|
||||
|
|
@ -685,20 +691,6 @@ class Invoice extends BaseModel
|
|||
}
|
||||
}
|
||||
|
||||
public function transaction_event()
|
||||
{
|
||||
$invoice = $this->fresh();
|
||||
|
||||
return [
|
||||
'invoice_id' => $invoice->id,
|
||||
'invoice_amount' => $invoice->amount ?: 0,
|
||||
'invoice_partial' => $invoice->partial ?: 0,
|
||||
'invoice_balance' => $invoice->balance ?: 0,
|
||||
'invoice_paid_to_date' => $invoice->paid_to_date ?: 0,
|
||||
'invoice_status' => $invoice->status_id ?: 1,
|
||||
];
|
||||
}
|
||||
|
||||
public function expense_documents()
|
||||
{
|
||||
$line_items = $this->line_items;
|
||||
|
|
|
|||
|
|
@ -481,21 +481,6 @@ class Payment extends BaseModel
|
|||
return $domain.'/client/payment/'.$this->client->contacts()->first()->contact_key.'/'.$this->hashed_id.'?next=/client/payments/'.$this->hashed_id;
|
||||
}
|
||||
|
||||
public function transaction_event()
|
||||
{
|
||||
$payment = $this->fresh();
|
||||
|
||||
return [
|
||||
'payment_id' => $payment->id,
|
||||
'payment_amount' => $payment->amount ?: 0,
|
||||
'payment_applied' => $payment->applied ?: 0,
|
||||
'payment_refunded' => $payment->refunded ?: 0,
|
||||
'payment_status' => $payment->status_id ?: 1,
|
||||
'paymentables' => $payment->paymentables->toArray(),
|
||||
'payment_request' => [],
|
||||
];
|
||||
}
|
||||
|
||||
public function translate_entity(): string
|
||||
{
|
||||
return ctrans('texts.payment');
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\DataMapper\TransactionEventMetadata;
|
||||
|
||||
/**
|
||||
* Class Bank.
|
||||
*
|
||||
|
|
@ -42,34 +44,6 @@ namespace App\Models;
|
|||
* @property int|null $credit_status
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|StaticModel company()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|StaticModel exclude($columns)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent whereClientBalance($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent whereClientCreditBalance($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent whereClientId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent whereClientPaidToDate($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent whereCreditAmount($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent whereCreditBalance($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent whereCreditId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent whereCreditStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent whereEventId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent whereInvoiceAmount($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent whereInvoiceBalance($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent whereInvoiceId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent whereInvoicePaidToDate($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent whereInvoicePartial($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent whereInvoiceStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent whereMetadata($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent wherePaymentAmount($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent wherePaymentApplied($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent wherePaymentId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent wherePaymentRefunded($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent wherePaymentRequest($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent wherePaymentStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent wherePaymentables($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TransactionEvent whereTimestamp($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class TransactionEvent extends StaticModel
|
||||
|
|
@ -79,36 +53,14 @@ class TransactionEvent extends StaticModel
|
|||
public $guarded = ['id'];
|
||||
|
||||
public $casts = [
|
||||
'metadata' => 'array',
|
||||
'metadata' => TransactionEventMetadata::class,
|
||||
'payment_request' => 'array',
|
||||
'paymentables' => 'array',
|
||||
];
|
||||
|
||||
public const INVOICE_MARK_PAID = 1;
|
||||
public const INVOICE_UPDATED = 1;
|
||||
|
||||
public const INVOICE_UPDATED = 2;
|
||||
public const INVOICE_DELETED = 2;
|
||||
|
||||
public const INVOICE_DELETED = 3;
|
||||
|
||||
public const INVOICE_PAYMENT_APPLIED = 4;
|
||||
|
||||
public const INVOICE_CANCELLED = 5;
|
||||
|
||||
public const INVOICE_FEE_APPLIED = 6;
|
||||
|
||||
public const INVOICE_REVERSED = 7;
|
||||
|
||||
public const PAYMENT_MADE = 100;
|
||||
|
||||
public const PAYMENT_APPLIED = 101;
|
||||
|
||||
public const PAYMENT_REFUND = 102;
|
||||
|
||||
public const PAYMENT_FAILED = 103;
|
||||
|
||||
public const GATEWAY_PAYMENT_MADE = 104;
|
||||
|
||||
public const PAYMENT_DELETED = 105;
|
||||
|
||||
public const CLIENT_STATUS = 200;
|
||||
public const PAYMENT_DELETED = 3;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,6 +156,8 @@ class DeletePayment
|
|||
$paymentable_invoice->delete();
|
||||
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ class PaymentService
|
|||
->updatePaidToDate($invoice->pivot->amount * -1)
|
||||
->setStatus(Invoice::STATUS_SENT)
|
||||
->save();
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -309,6 +309,7 @@ class RefundPayment
|
|||
if ($invoice->is_deleted) {
|
||||
$invoice->delete();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -163,6 +163,8 @@ class UpdateInvoicePayment
|
|||
$invoice->service()
|
||||
->applyNumber()
|
||||
->save();
|
||||
|
||||
|
||||
}
|
||||
|
||||
/* Updates the company ledger */
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ use App\Export\CSV\BaseExport;
|
|||
use App\Utils\Traits\MakesDates;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use App\Services\Template\TemplateService;
|
||||
use App\Jobs\Invoice\InvoiceTaxReportUpdate;
|
||||
|
||||
class TaxSummaryReport extends BaseExport
|
||||
{
|
||||
|
|
|
|||
|
|
@ -37,48 +37,19 @@ class TaxReport
|
|||
|
||||
$this->spreadsheet = new Spreadsheet();
|
||||
|
||||
$this->updateTaxData();
|
||||
$this->buildData()
|
||||
->setCurrencyFormat()
|
||||
->createSummarySheet()
|
||||
->createInvoiceSummarySheetAccrual()
|
||||
->createInvoiceSummarySheetCash()
|
||||
->createInvoiceItemSummarySheetAccrual()
|
||||
->createInvoiceItemSummarySheetCash();
|
||||
|
||||
|
||||
// ->createGroupedTaxSummarySheetAccrual()
|
||||
// ->createGroupedTaxSummarySheetCash();
|
||||
|
||||
return $this;
|
||||
|
||||
}
|
||||
|
||||
private function postUpdateContinuation()
|
||||
{
|
||||
$this->buildData()
|
||||
->setCurrencyFormat()
|
||||
->createSummarySheet()
|
||||
->createInvoiceSummarySheetAccrual()
|
||||
->createInvoiceSummarySheetCash()
|
||||
->createInvoiceItemSummarySheetAccrual()
|
||||
->createInvoiceItemSummarySheetCash();
|
||||
}
|
||||
|
||||
private function updateTaxData()
|
||||
{
|
||||
|
||||
$batch_key = Str::uuid();
|
||||
|
||||
$updates = Invoice::withTrashed()
|
||||
->whereIn('id', $this->ids)
|
||||
->get()
|
||||
->map(function ($invoice) use ($batch_key) {
|
||||
return new InvoiceTaxReportUpdate($invoice, $this->company->db);
|
||||
})->toArray();
|
||||
|
||||
|
||||
$batch = Bus::batch($updates)
|
||||
->then(function (Batch $batch) {
|
||||
$this->postUpdateContinuation();
|
||||
})
|
||||
->name($batch_key)->dispatch();
|
||||
|
||||
}
|
||||
|
||||
public function setCurrencyFormat()
|
||||
{
|
||||
$currency = $this->company->currency();
|
||||
|
|
|
|||
Loading…
Reference in New Issue