mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-09-03 13:51:08 +00:00
127 lines
4.9 KiB
PHP
127 lines
4.9 KiB
PHP
<?php
|
|
|
|
namespace App\Domains\Sales\Application;
|
|
|
|
use App\Domains\Metadata\Contracts\CustomFieldValueWriter;
|
|
use App\Support\DocumentTotals;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
|
|
class DocumentItemService
|
|
{
|
|
public function __construct(
|
|
private readonly CustomFieldValueWriter $customFieldValueWriter,
|
|
) {}
|
|
|
|
/**
|
|
* Company-currency columns and the column each one is derived from.
|
|
*/
|
|
private const BASE_FIELDS = [
|
|
'base_price' => 'price',
|
|
'base_discount_val' => 'discount_val',
|
|
'base_tax' => 'tax',
|
|
'base_total' => 'total',
|
|
];
|
|
|
|
/**
|
|
* Persist the line items of a document.
|
|
*
|
|
* $recompute = false is for callers that have already decided every cent of
|
|
* the document and must not have it decided again: partial credit notes
|
|
* carry pro-rated integers from CreditNoteAmounts, and re-deriving the line
|
|
* total from price * quantity or the base_* columns from the exchange rate
|
|
* rounds a second time, drifts a cent, and breaks the telescoping invariant
|
|
* that makes a chain of partial credits add back up to the invoice. Such a
|
|
* caller still gets a base_* value derived here for any it did not supply.
|
|
*/
|
|
public function createItems(Model $document, array $items, bool $recompute = true): void
|
|
{
|
|
$exchangeRate = $document->exchange_rate;
|
|
|
|
foreach ($items as $item) {
|
|
$item['company_id'] = $document->company_id;
|
|
$item['exchange_rate'] = $exchangeRate;
|
|
|
|
if ($recompute) {
|
|
// Recompute the item total from price/quantity so a tampered item
|
|
// total can't desync from the recomputed document totals (GHSA-8c69).
|
|
$item['total'] = DocumentTotals::itemTotal($item, $document->discount_per_item === 'YES');
|
|
$item['base_price'] = $item['price'] * $exchangeRate;
|
|
$item['base_discount_val'] = $item['discount_val'] * $exchangeRate;
|
|
$item['base_tax'] = $item['tax'] * $exchangeRate;
|
|
$item['base_total'] = $item['total'] * $exchangeRate;
|
|
} else {
|
|
foreach (self::BASE_FIELDS as $baseField => $field) {
|
|
if (! array_key_exists($baseField, $item)) {
|
|
$item[$baseField] = ($item[$field] ?? 0) * $exchangeRate;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (array_key_exists('recurring_invoice_id', $item)) {
|
|
unset($item['recurring_invoice_id']);
|
|
}
|
|
|
|
$createdItem = $document->items()->create($item);
|
|
|
|
if (array_key_exists('taxes', $item) && $item['taxes']) {
|
|
foreach ($item['taxes'] as $tax) {
|
|
if (empty($tax['tax_type_id'])) {
|
|
// UI placeholder row for per-item tax mode; never persist.
|
|
continue;
|
|
}
|
|
|
|
$tax['company_id'] = $document->company_id;
|
|
$tax['exchange_rate'] = $document->exchange_rate;
|
|
$tax['currency_id'] = $document->currency_id;
|
|
|
|
if ($recompute || ! array_key_exists('base_amount', $tax)) {
|
|
$tax['base_amount'] = $tax['amount'] * $exchangeRate;
|
|
}
|
|
|
|
if (gettype($tax['amount']) !== 'NULL') {
|
|
// A row lifted off a recurring template still carries
|
|
// the template's key, which means nothing on the
|
|
// generated document. Dropping an absent key is a
|
|
// no-op, so it needs no guard.
|
|
unset($tax['recurring_invoice_id']);
|
|
|
|
$createdItem->taxes()->create($tax);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (array_key_exists('custom_fields', $item) && $item['custom_fields']) {
|
|
$this->customFieldValueWriter->attach($createdItem, $item['custom_fields']);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Persist the document-level tax rows.
|
|
*
|
|
* $recompute = false has the same meaning as in {@see createItems()}: the
|
|
* supplied base_amount is the caller's pro-rated integer and is kept as-is.
|
|
*/
|
|
public function createTaxes(Model $document, array $taxes, bool $recompute = true): void
|
|
{
|
|
$exchangeRate = $document->exchange_rate;
|
|
|
|
foreach ($taxes as $tax) {
|
|
$tax['company_id'] = $document->company_id;
|
|
$tax['exchange_rate'] = $document->exchange_rate;
|
|
$tax['currency_id'] = $document->currency_id;
|
|
|
|
if ($recompute || ! array_key_exists('base_amount', $tax)) {
|
|
$tax['base_amount'] = $tax['amount'] * $exchangeRate;
|
|
}
|
|
|
|
if (gettype($tax['amount']) !== 'NULL') {
|
|
// Same template key as in createItems(), dropped the same way.
|
|
unset($tax['recurring_invoice_id']);
|
|
|
|
$document->taxes()->create($tax);
|
|
}
|
|
}
|
|
}
|
|
}
|