CoolCMS API

OrderCalculator
in package

FinalYes

Authoritative recalculation of order totals over the order snapshot.

Input is only the snapshot columns built by OrderSnapshotBuilder, never live price lists. Called from OrderManager::beforeSave(); stored totals are never trusted. All arithmetic is bcmath over decimal(10,3) strings (scale 6 intermediates, half-up round to 3), no floats.

Formula (binding; the invoice generator checks parity with the Invoicer InvoiceCalculator, tolerance 0.5 %): unitAfterDiscount = round3( unit x (1 - itemDiscount/100) x (1 - userDiscount/100) ) itemsTotal(WithVat) = sum of round3(unitAfterDiscount x quantity), base and with-VAT variants.

  • Fees (OrderItemFee) are informative: the set price already contains them, so they are not added.
  • Shipping and payment are added in both forms; free_from is already resolved by the builder.
  • Vouchers add up: p = min(100, sum of percents), fixed = sum of voucher.price (base without VAT), maxVat = highest item.vat (0 without items). discount = min(itemsTotal, round3(itemsTotal x p/100) + fixed) discountWithVat = min(itemsTotalWithVat, round3(itemsTotalWithVat x p/100) + round3(fixed x (1 + maxVat/100)))
  • A fixed amount has no rate of its own, so its VAT uses maxVat; the percent part is computed from items only, never from shipping or payment.
  • IMPORTANT: the discount never exceeds the goods price, otherwise a fixed voucher larger than the cart gives a negative total. Negative voucher values (bad configuration) count as 0.
  • The calculator also fills vat/priceWithVat on voucher snapshot rows (they depend on the final items).
  • totalPrice = itemsTotal + shipping.price + payment.price - discount totalPriceWithVat = itemsTotalWithVat + shipping.priceWithVat + payment.priceWithVat - discountWithVat Gifts do not enter the totals; a negative result is clipped to 0.

Rounding follows the order currency (currency.orderRoundingWhole): 1: roundedTotal = ceil(total) to whole units, rounding = rounded - total (always >= 0); roundingVat > 0 produces an extra rounding line on the invoice. 0: roundedTotal = half-up to currency.precision, no rounding line (see usesWholeRounding()). An order without a currency (half-built graph, unit tests) uses whole-unit ceil.

Tags
phpstan-type

TOrderGraph Order

Table of Contents

Constants

SCALE  : int = 3
WORK_SCALE  : int = 6

Methods

ancillaryLines()  : array<int, array{vat: numeric-string, base: numeric-string, withVat: numeric-string}>
Returns the rows of one ancillary snapshot (shipping or payment) for the recap and the invoice: with an engine snapshot (`tax_treatment`) split by item rates, for an older order a single row with the stored rate. The sum always matches `price`/`priceWithVat` of the snapshot.
ancillaryShares()  : array<int, array{vat: numeric-string, base: numeric-string, withVat: numeric-string}>
Splits an ancillary supply (shipping, payment surcharge) like the main supply: the base is spread proportionally to item bases per rate and the rounding remainder goes to the group with the largest base (same algorithm as {@see \App\UI\Api\System\Models\Helpers\TaxAmountCalculator::allocate()}, but at the precision of `OrderCalculator` so the snapshot matches the totals).
ancillaryTotals()  : array{vat: numeric-string, priceWithVat: numeric-string}
Returns the rate and the price with VAT of an ancillary supply for a 1:1 snapshot (shipping and payment carry one row): `vat` is the rate of the largest group (informative, the split is done by {@see ancillaryShares()}), `priceWithVat` is the sum of the amounts actually charged in all groups.
applyLineDiscounts()  : numeric-string
Returns the unit price after both discounts. Public input for the VAT engine: the row base for {@see \App\UI\Api\Eshop\Models\Managers\OrderTaxManager}, so budget weights and the choice of the main supply are computed from the same numbers as the totals.
itemsTotals()  : array{base: numeric-string, withVat: numeric-string, maxVat: numeric-string}
Returns item totals after discounts (see the formula) and the highest VAT rate. Public: for free_from thresholds of shipping/payment {@see \App\UI\Api\Eshop\Models\Services\OrderSnapshotBuilder} takes `cartTotal` from `withVat` (cart value = items with VAT after discounts, without shipping, payment and vouchers).
recalculate()  : void
Recalculates and writes the totals on the order and fills `vat`/`priceWithVat` on the voucher snapshot rows. Does not flush; call it before saving.
roundToPrecision()  : numeric-string
Rounds half-up (away from zero) to `$precision` decimals, the result at scale 3; the same algorithm as `PriceTrait::roundToPrecision()` for payments (one rounding of the unrounded sum).
usesWholeRounding()  : bool
Tells whether the payable amount is rounded up to whole units with a rounding line. The order currency decides (`currency.orderRoundingWhole`); an order without a currency (or with an entity that does not know the flag) keeps the legacy behavior, i.e. yes.
vatRecap()  : array<int, array{vat: numeric-string, base: numeric-string, withVat: numeric-string, vatAmount: numeric-string}>
Returns the VAT recap per rate (order confirmation e-mail): the sum of `withVat` over rates equals `totalPriceWithVat` and the sum of `base` equals `totalPrice` (before rounding), exactly the sums of {@see recalculate()}.
voucherDiscounts()  : array<int, array{percents: int, base: numeric-string, withVat: numeric-string}>
Returns the actual discount of each order voucher (in collection order), the base for the cart/checkout summary where every discount code has its own line.
withoutVat()  : numeric-string
Returns the base from a price with VAT and a rate in % (round3 half-up).
withVat()  : numeric-string
Returns the price with VAT from a base and a rate in % (round3 half-up).
addToVatGroup()  : void
applyDiscounts()  : numeric-string
Applies both discounts multiplicatively on the unit price, round3 (see the formula).
ceilToWhole()  : numeric-string
Rounds up to whole currency units (legacy `ceil()` parity), correct for negative numbers too.
currencyPrecision()  : int
Returns the order currency `precision` (2 for euro); 2 without a currency, used only for the branch without whole units.
discountTotals()  : array{base: numeric-string, withVat: numeric-string}
Returns the total voucher discount (see the formula): percents capped at 100 of items plus fixed amounts.
itemBasesByVat()  : array<string|int, numeric-string>
Returns item bases after discounts grouped by rate (the weights for spreading an ancillary supply).
minAmount()  : numeric-string
nonNegative()  : numeric-string
norm()  : numeric-string
round3()  : numeric-string
Rounds half-up to 3 decimals (bcmath truncates on its own, hence the explicit rounding).

Constants

Methods

ancillaryLines()

Returns the rows of one ancillary snapshot (shipping or payment) for the recap and the invoice: with an engine snapshot (`tax_treatment`) split by item rates, for an older order a single row with the stored rate. The sum always matches `price`/`priceWithVat` of the snapshot.

public ancillaryLines(TOrderGraph $order, OrderShipper|OrderPaymentMethod $line) : array<int, array{vat: numeric-string, base: numeric-string, withVat: numeric-string}>
Parameters
$order : TOrderGraph
$line : OrderShipper|OrderPaymentMethod
Return values
array<int, array{vat: numeric-string, base: numeric-string, withVat: numeric-string}>

ancillaryShares()

Splits an ancillary supply (shipping, payment surcharge) like the main supply: the base is spread proportionally to item bases per rate and the rounding remainder goes to the group with the largest base (same algorithm as {@see \App\UI\Api\System\Models\Helpers\TaxAmountCalculator::allocate()}, but at the precision of `OrderCalculator` so the snapshot matches the totals).

public ancillaryShares(TOrderGraph $order, numeric-string $amount) : array<int, array{vat: numeric-string, base: numeric-string, withVat: numeric-string}>

An order without VAT (withVat = false) or without items has a single group with rate 0.

Parameters
$order : TOrderGraph
$amount : numeric-string

base of the ancillary supply (after free_from)

Return values
array<int, array{vat: numeric-string, base: numeric-string, withVat: numeric-string}>

ancillaryTotals()

Returns the rate and the price with VAT of an ancillary supply for a 1:1 snapshot (shipping and payment carry one row): `vat` is the rate of the largest group (informative, the split is done by {@see ancillaryShares()}), `priceWithVat` is the sum of the amounts actually charged in all groups.

public ancillaryTotals(TOrderGraph $order, numeric-string $amount) : array{vat: numeric-string, priceWithVat: numeric-string}
Parameters
$order : TOrderGraph
$amount : numeric-string
Return values
array{vat: numeric-string, priceWithVat: numeric-string}

applyLineDiscounts()

Returns the unit price after both discounts. Public input for the VAT engine: the row base for {@see \App\UI\Api\Eshop\Models\Managers\OrderTaxManager}, so budget weights and the choice of the main supply are computed from the same numbers as the totals.

public applyLineDiscounts(numeric-string $unit, numeric-string $itemDiscount, numeric-string $userDiscount) : numeric-string
Parameters
$unit : numeric-string
$itemDiscount : numeric-string
$userDiscount : numeric-string
Return values
numeric-string

itemsTotals()

Returns item totals after discounts (see the formula) and the highest VAT rate. Public: for free_from thresholds of shipping/payment {@see \App\UI\Api\Eshop\Models\Services\OrderSnapshotBuilder} takes `cartTotal` from `withVat` (cart value = items with VAT after discounts, without shipping, payment and vouchers).

public itemsTotals(TOrderGraph $order) : array{base: numeric-string, withVat: numeric-string, maxVat: numeric-string}
Parameters
$order : TOrderGraph
Return values
array{base: numeric-string, withVat: numeric-string, maxVat: numeric-string}

recalculate()

Recalculates and writes the totals on the order and fills `vat`/`priceWithVat` on the voucher snapshot rows. Does not flush; call it before saving.

public recalculate(TOrderGraph $order) : void
Parameters
$order : TOrderGraph

roundToPrecision()

Rounds half-up (away from zero) to `$precision` decimals, the result at scale 3; the same algorithm as `PriceTrait::roundToPrecision()` for payments (one rounding of the unrounded sum).

public roundToPrecision(string $value, int $precision) : numeric-string
Parameters
$value : string
$precision : int
Return values
numeric-string

usesWholeRounding()

Tells whether the payable amount is rounded up to whole units with a rounding line. The order currency decides (`currency.orderRoundingWhole`); an order without a currency (or with an entity that does not know the flag) keeps the legacy behavior, i.e. yes.

public usesWholeRounding(TOrderGraph $order) : bool

Public: the invoice (OrderInvoiceGenerator, rounding line) and the cart/e-mail summaries (show the line or not) need the same decision.

Parameters
$order : TOrderGraph
Return values
bool

vatRecap()

Returns the VAT recap per rate (order confirmation e-mail): the sum of `withVat` over rates equals `totalPriceWithVat` and the sum of `base` equals `totalPrice` (before rounding), exactly the sums of {@see recalculate()}.

public vatRecap(TOrderGraph $order) : array<int, array{vat: numeric-string, base: numeric-string, withVat: numeric-string, vatAmount: numeric-string}>

Voucher discount breakdown (a discount has no rate of its own):

  • the percent part is split between rates proportionally to the items of that rate (an item discount lowers the base and tax of exactly that rate); the rounding remainder goes to the highest rate;
  • the fixed part goes entirely to the highest item rate (the same maxVat as in the calculation). An order without VAT (withVat=false) has no recap and returns [].
Parameters
$order : TOrderGraph
Return values
array<int, array{vat: numeric-string, base: numeric-string, withVat: numeric-string, vatAmount: numeric-string}>

voucherDiscounts()

Returns the actual discount of each order voucher (in collection order), the base for the cart/checkout summary where every discount code has its own line.

public voucherDiscounts(TOrderGraph $order) : array<int, array{percents: int, base: numeric-string, withVat: numeric-string}>

OrderVoucher.price/priceWithVat carry only the fixed part of a voucher; a percent voucher has zero there, so a summary listing them would show "discount -0" although the total included the discount.

The breakdown matches the total of recalculate() to the cent: percents are allocated in order with a cap of 100 % (a voucher gets nothing that no longer fits) and the last voucher gets the remainder, so the sum of the rows equals discountBase/discountWithVat even after rounding.

Parameters
$order : TOrderGraph
Return values
array<int, array{percents: int, base: numeric-string, withVat: numeric-string}>

withoutVat()

Returns the base from a price with VAT and a rate in % (round3 half-up).

public withoutVat(string $withVat, string $vatPercents) : numeric-string
Parameters
$withVat : string
$vatPercents : string
Return values
numeric-string

withVat()

Returns the price with VAT from a base and a rate in % (round3 half-up).

public withVat(string $base, string $vatPercents) : numeric-string
Parameters
$base : string
$vatPercents : string
Return values
numeric-string

addToVatGroup()

private addToVatGroup(array<string, array{base: numeric-string, withVat: numeric-string}> &$groups, string $rate, numeric-string $base, numeric-string $withVat) : void
Parameters
$groups : array<string, array{base: numeric-string, withVat: numeric-string}>
$rate : string
$base : numeric-string
$withVat : numeric-string

applyDiscounts()

Applies both discounts multiplicatively on the unit price, round3 (see the formula).

private applyDiscounts(numeric-string $unit, numeric-string $itemDiscount, numeric-string $userDiscount) : numeric-string
Parameters
$unit : numeric-string
$itemDiscount : numeric-string
$userDiscount : numeric-string
Return values
numeric-string

ceilToWhole()

Rounds up to whole currency units (legacy `ceil()` parity), correct for negative numbers too.

private ceilToWhole(string $value) : numeric-string
Parameters
$value : string
Return values
numeric-string

currencyPrecision()

Returns the order currency `precision` (2 for euro); 2 without a currency, used only for the branch without whole units.

private currencyPrecision(TOrderGraph $order) : int
Parameters
$order : TOrderGraph
Return values
int

discountTotals()

Returns the total voucher discount (see the formula): percents capped at 100 of items plus fixed amounts.

private discountTotals(TOrderGraph $order, numeric-string $itemsTotal, numeric-string $itemsTotalWithVat, numeric-string $maxVat) : array{base: numeric-string, withVat: numeric-string}
Parameters
$order : TOrderGraph
$itemsTotal : numeric-string
$itemsTotalWithVat : numeric-string
$maxVat : numeric-string
Return values
array{base: numeric-string, withVat: numeric-string}

itemBasesByVat()

Returns item bases after discounts grouped by rate (the weights for spreading an ancillary supply).

private itemBasesByVat(TOrderGraph $order) : array<string|int, numeric-string>
Parameters
$order : TOrderGraph
Return values
array<string|int, numeric-string>

minAmount()

private minAmount(numeric-string $a, numeric-string $b) : numeric-string
Parameters
$a : numeric-string
$b : numeric-string
Return values
numeric-string

nonNegative()

private nonNegative(numeric-string $value) : numeric-string
Parameters
$value : numeric-string
Return values
numeric-string

norm()

private norm(string $value) : numeric-string
Parameters
$value : string
Return values
numeric-string

round3()

Rounds half-up to 3 decimals (bcmath truncates on its own, hence the explicit rounding).

private round3(numeric-string $value) : numeric-string
Parameters
$value : numeric-string
Return values
numeric-string
On this page

Search results