/** * Der Rechenkern der Rechnung — exakt, in ganzen Zahlen. * * Eine Zeile ist ein Bruch: Preis (µ€) × Menge (Tausendstel) × Rabattanteil * (Hundertstel-Prozent). Multipliziert man die Skalen, ist die kleinste Einheit * 10⁻¹⁷ Cent — das passt in keine Gleitkommazahl und in keine 53-Bit-Ganzzahl, * also rechnet der Kern mit BigInt. Gerundet wird genau EINMAL je USt-Satz, auf * dem Bruch, kaufmaennisch (halbe Einheit vom Nullpunkt weg). * * Warum das so streng ist: Vier Umsetzungen (Server, dieses Paket, Dart, Panel) * mussten bisher dieselbe Reihenfolge von Gleitkomma-Schritten nachbauen, damit * Grenzfaelle gleich runden. Mit Ganzzahlen gibt es keine Reihenfolge mehr, an * der etwas auseinanderlaufen koennte. * * Dieser Unterpfad ist rein: kein Transport, kein Zugangsschluessel, keine * Abhaengigkeit ausser Typen. Er wird im Browser gebuendelt (Panel) und im * Server geladen. * * Spec: docs/specs/2026-09-17-rechnung-ganzzahlen-design.md § 5. */ import type { PriceMode, TaxScheme } from './vertrag.js'; /** Hoechster Betrag je Zeile und je Rechnung: 999.999.999,99 €. */ export declare const MAX_AMOUNT_CENTS = 99999999999; /** Steuerfaelle, in denen die Rechnung keine Steuer ausweist: jede Zeile zaehlt zu 0 %. */ export declare const ZERO_RATED_TAX_SCHEMES: readonly TaxScheme[]; /** Eine Position in gespeicherter Form — alle Werte ganzzahlig. */ export interface CalcItem { /** Einzelpreis in Millionstel Euro, 0 … 10¹². */ unitPriceMicros: number; /** Menge in Tausendstel; negativ = Abzugszeile, 0 = Textzeile. */ quantityMilli: number; /** Rabatt in Hundertstel-Prozent, 0 … 10000 (ohne Angabe 0). */ discountBp?: number; /** USt-Satz in Hundertstel-Prozent, 0 … 10000 (ohne Angabe 0). */ vatRateBp?: number; } export interface CalcOptions { priceMode: PriceMode; /** Ohne Angabe `normal`. */ taxScheme?: TaxScheme; } export interface RateTotal { rateBp: number; netCents: number; vatCents: number; grossCents: number; } export interface LineAmount { netCents: number; grossCents: number; /** Der Satz, mit dem die Zeile gerechnet wurde (bei steuerfreiem Fall 0). */ rateBp: number; } export interface CalcResult { netCents: number; vatCents: number; grossCents: number; /** Absteigend nach `rateBp`. */ byRate: RateTotal[]; /** Je Position, in der Reihenfolge der Eingabe. */ lines: LineAmount[]; } export type CalcErrorCode = 'amount_too_large' | 'not_integer' | 'out_of_range'; /** Fehler des Kerns — mit Code, Feld und Index, damit die API daraus einen Feldfehler machen kann. */ export declare class CalcError extends Error { readonly code: CalcErrorCode; readonly field?: string; readonly index?: number; constructor(code: CalcErrorCode, message: string, field?: string, index?: number); } /** * `a / b` kaufmaennisch gerundet, halbe Einheit vom Nullpunkt weg; `b` > 0. * * Ohne Gleitkomma: `(2·|a| + b) / (2·b)` ist genau dann eins groesser, wenn der * Rest mindestens die halbe Einheit betraegt. */ export declare function roundDiv(a: bigint, b: bigint): bigint; export declare function calculateInvoice(items: readonly CalcItem[], options: CalcOptions): CalcResult; export type ConversionReason = 'not_a_number' | 'too_many_decimals' | 'out_of_range'; export type ItemConversion = { ok: true; item: CalcItem & Record; } | { ok: false; field: 'unitPrice' | 'quantity' | 'vatRate' | 'discountPct'; reason: ConversionReason; }; /** * Euro-Position → Ganzzahl-Form, verlustfrei oder mit Grund abgelehnt. Die * EINE Stelle, die das tut: Panel (beim Lesen), SEPA-Lauf, Umrechnungsskript * und das Festschreiben benutzen alle diese Funktion, damit niemand eine * zweite Auslegung baut. * * Eine Position, die schon `unitPriceMicros` traegt, kommt unveraendert * zurueck — die Funktion ist die Formweiche, nicht eine blinde Umrechnung. * * Fehlende Menge und fehlender Satz gelten als 0, weil der Server bisher so * abgerechnet hat. Ein fehlender Preis dagegen ist NIE 0: sonst wuerde eine * Position, der nur der Preis fehlt, still zur 0-€-Zeile. */ export declare function itemFromEuro(item: Record): ItemConversion; /** * Anteiliger Preis fuer einen Teilzeitraum (erste Rechnung eines Abos). * War der Ursprungspreis centgenau, bleibt auch der Anteil centgenau — sonst * stuende auf der ersten Rechnung ein Preis mit sechs Stellen, wo bisher zwei * standen. */ export declare function proratedPriceMicros(unitPriceMicros: number, months: number, interval: number): number; /** * Schluessel eines USt-Satzes in den gespeicherten Maps (`creditedCents.byRate`, * `invoice_stats.vatByRate`): der Prozenttext mit Punkt, genau wie * `String(satz)` ihn bisher gebildet hat. NICHT fuer die Anzeige — dafuer gibt * es `formatVatRate`. */ export declare function vatRateMapKey(rateBp: number): string; /** Ein USt-Satz fuer die Anzeige: oesterreichisch mit Komma, ohne nachlaufende Nullen. */ export declare function formatVatRate(rateBp: number): string; /** * Ein Einzelpreis fuer die Anzeige: mindestens zwei, hoechstens sechs * Nachkommastellen, Tausenderpunkt, Komma als Trennzeichen — in jeder Sprache * oesterreichisch, wie die Betraege auf dem Blatt. */ export declare function formatUnitPrice(unitPriceMicros: number): string;