$lines * @param array{type:string, value:int}|null $adjustment * @return array{subtotal:int, lines_net:int, adjustment:int, net:int, tax:int, gross:int} */ public static function totals(array $lines, ?array $adjustment, int $rateBasisPoints): array { // The subtotal is the sum of the FULL line prices — the column the // reader can add up on the page. Discounts are collected separately and // shown as one figure beneath it. // // Printing each line already reduced was the previous attempt, and it // was worse: a line reading 202,50 next to "Rabatt 10 %" leaves the // reader unable to tell whether the discount is in that number or still // to come. Full price, then one subtraction, is a sum anybody can // follow. $subtotal = 0; $lineDiscounts = 0; foreach ($lines as $line) { $full = self::lineNet((int) $line['unit_net_cents'], (int) $line['quantity_milli']); $subtotal += $full; if (! empty($line['adjustment'])) { $lineDiscounts += self::adjustment( $full, (string) $line['adjustment']['type'], (int) $line['adjustment']['value'], ); } } // An adjustment on the whole document, on top of any on the lines. $wide = $adjustment === null ? 0 : self::adjustment($subtotal, (string) $adjustment['type'], (int) $adjustment['value']); $adjusted = $lineDiscounts + $wide; $net = $subtotal + $adjusted; $tax = self::tax($net, $rateBasisPoints); return [ 'subtotal' => $subtotal, 'lines_net' => $subtotal, // kept: older snapshots read this name 'adjustment' => $adjusted, 'net' => $net, 'tax' => $tax, 'gross' => $net + $tax, ]; } /** "1.234,56" — Austrian formatting, done here so no view invents its own. */ public static function money(int $cents): string { return number_format($cents / 100, 2, ',', '.'); } /** "12", "1,5", "0,25" — trailing zeroes dropped, because "12,000 Std." reads as a defect. */ public static function quantity(int $quantityMilli): string { $formatted = number_format($quantityMilli / 1000, 3, ',', '.'); return rtrim(rtrim($formatted, '0'), ','); } }