mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-07-16 22:05:20 +00:00
v3 port. Invoice/estimate/recurring creation and update accepted total, sub_total, tax and due_amount straight from the request with no recalculation, letting a client persist financial totals that don't match the line items (and corrupt the invoice update due-amount/paid-amount logic which keyed off the client total). - Adds App\Support\DocumentTotals (mirrors the front-end calc) trusting only price/quantity/discounts/tax-line amounts. - getInvoicePayload/getEstimatePayload/getRecurringInvoicePayload override the client totals; the shared DocumentItemService::createItems recomputes each item total; InvoiceService::update keys its due-amount logic off the recomputed total. Adds DocumentTotals unit tests + a feature test proving a tampered invoice total is ignored; existing create/update tests no longer assert the now server-authoritative derived totals.
71 lines
2.4 KiB
PHP
71 lines
2.4 KiB
PHP
<?php
|
|
|
|
namespace App\Support;
|
|
|
|
/**
|
|
* Recomputes authoritative invoice/estimate/recurring-invoice totals from the
|
|
* submitted line items + document-level discount/taxes, mirroring the
|
|
* front-end calculation (resources/scripts admin invoice store +
|
|
* CreateItemRow).
|
|
*
|
|
* The client-supplied total / sub_total / tax / due_amount are NOT trusted
|
|
* (GHSA-8c69) — only price, quantity, discounts and tax-line amounts are.
|
|
* All amounts are integer minor units (cents).
|
|
*/
|
|
class DocumentTotals
|
|
{
|
|
/**
|
|
* @param array $items each item: price, quantity, discount_val?, taxes?[{amount}]
|
|
* @param array $taxes document-level taxes: [{amount}, ...]
|
|
* @return array{sub_total:int, tax:int, total:int}
|
|
*/
|
|
public static function compute(array $items, array $taxes, $discountVal, $taxPerItem, bool $taxIncluded, $discountPerItem = 'NO'): array
|
|
{
|
|
$perItemDiscount = is_string($discountPerItem) && strtoupper(trim($discountPerItem)) === 'YES';
|
|
$perItemTax = is_string($taxPerItem) && strtoupper(trim($taxPerItem)) === 'YES';
|
|
|
|
$subTotal = 0;
|
|
$itemTaxTotal = 0;
|
|
|
|
foreach ($items as $item) {
|
|
$subTotal += self::itemTotal($item, $perItemDiscount);
|
|
$itemTaxTotal += self::sumTaxAmounts($item['taxes'] ?? []);
|
|
}
|
|
|
|
$subtotalWithDiscount = $subTotal - (int) round((float) $discountVal);
|
|
|
|
$totalTax = $perItemTax ? $itemTaxTotal : self::sumTaxAmounts($taxes);
|
|
|
|
$total = $taxIncluded ? $subtotalWithDiscount : $subtotalWithDiscount + $totalTax;
|
|
|
|
return [
|
|
'sub_total' => $subTotal,
|
|
'tax' => $totalTax,
|
|
'total' => $total,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Authoritative per-item total: round(price * quantity) minus the item
|
|
* discount (only applied when discount is configured per item).
|
|
*/
|
|
public static function itemTotal(array $item, bool $perItemDiscount): int
|
|
{
|
|
$price = (float) ($item['price'] ?? 0);
|
|
$quantity = (float) ($item['quantity'] ?? 0);
|
|
$discount = $perItemDiscount ? (int) round((float) ($item['discount_val'] ?? 0)) : 0;
|
|
|
|
return (int) round($price * $quantity) - $discount;
|
|
}
|
|
|
|
protected static function sumTaxAmounts(array $taxes): int
|
|
{
|
|
$sum = 0;
|
|
foreach ($taxes as $tax) {
|
|
$sum += (int) round((float) ($tax['amount'] ?? 0));
|
|
}
|
|
|
|
return $sum;
|
|
}
|
|
}
|