import { ReceiptType, type ReceiptTypeKey, KeckPaymentMethod } from '../enums/index.js'; import type { ReceiptItem } from '../models/receipt-item.js'; import type { Voucher } from '../models/voucher.js'; import type { PosVoucherSettings, VoucherCheck } from '../pos/gutschein.js'; import { type VoucherDueArticle, type VoucherDueInfo } from './gutschein-plan.js'; export { VOUCHER_ZERO_RATE_BUCKET_FROM, type VoucherDueArticle, type VoucherDueInfo } from './gutschein-plan.js'; /** * Zahlbetrag eines Belegs in ganzen Cent: Zwilling von `createReceipt` im * Backend (Nachtrag §8), also `beleg-toepfe.belegToepfe(...).zaehlerDeltaCents` * plus `zahlungen-core.wertgutscheinFlussCents`, mit den Trinkgeld-Positionen, * die `tip-core.buildTipItems` aus `tip` bzw. aus `payments[].tipCents` baut. * * `createReceipt` hat keinen Probelauf. Unter `/v3` ist `payments[]` Pflicht, * und die Summe der Zahlungen muss genau diesen Betrag treffen; wer die * Zahlungen baut (Kartenterminal, Rueckgeld), rechnet ihn hier vorher aus. * Trifft die Summe nicht, antwortet der Server mit `payments_sum_mismatch` und * `expectedCents`, ohne eine Belegnummer zu verbrauchen; das Paket wiederholt * nie selbst. * * **Bewusst dieselbe Rechnung wie der Server, nicht die exakte.** Das Backend * rechnet die RKSV-Toepfe in Euro-Gleitkomma (Einzelpreis `Math.round(cents)/100`, * je Topf `priceOne * amount` in Positionsreihenfolge, Trinkgeld-Positionen * hinten angehaengt, gerundet mit `Math.round(euro * 100)`, Rabatt-Deckel in * Euro). Diese Reihenfolge ist signiert und aendert sich nicht; ein exakter * Zwilling wiche bei Bruchmengen auf halbem Cent ab (0,5 x 29 ct: Server 14, * exakt 15). Darum steht hier dieselbe Arithmetik, Schritt fuer Schritt. * Eingaben und Ergebnis sind ganze Cent; die Gleitkommazahlen leben nur * innerhalb dieser Rechnung. Geprueft gegen die 20 Vertragsfaelle aus * `fixtures/v3/zahlbetrag-faelle.json` und gegen mehr als 1000 mit dem echten * Backend-Code erzeugte Faelle (`fixtures/receipt-due-generated.json`). * * **Trinkgeld-Empfaenger:** Ohne `recipients` bucht der Server das Trinkgeld * auf den angemeldeten Kassen-Benutzer, und dessen Inhaber-Kennzeichen * entscheidet: Inhaber-Trinkgeld ist Umsatz (anteilig auf die Saetze der Ware, * mitrabattiert), Personal-Trinkgeld ein durchlaufender Posten (nie * rabattiert). Das weiss nur die Kasse; darum ist `tipRecipient` Pflicht, * sobald Trinkgeld ohne `recipients` vorkommt. Ohne angemeldeten Benutzer * (Geraete-Schluessel) gilt `'staff'`. * * **Gutscheine der neuen Form** (Mehrzweck, Einzweck, Leistung, Aktion, * Auszahlung): der Server macht daraus Positionen und Eintraege * (`gutschein-beleg.plane`, Zwilling in `gutschein-plan.ts`). Was er dafuer * aus seinen Daten liest, gibt die Kasse mit: je Einloesung/Auszahlung die * Pruefung aus `checkVoucher` (`voucherInfo`, ueber den Code zugeordnet), fuer * einen Einzweck-Verkauf die Gutschein-Einstellung der Kasse * (`voucherSettings`, Satz), fuer einen Leistungsgutschein-Verkauf den Artikel * (`voucherArticles`, Preis und Satz) und `smallBusiness` fuer ein * Kleinunternehmer-Konto. Was fehlt oder was der Server abwiese, wirft * `invalid_voucher` mit dem erwarteten Server-Code in `voucherErrorCode`. * Ein Beleg nur aus Auszahlungen hat einen negativen Zahlbetrag (genau eine * Barzahlung ueber diesen Betrag). * * **Nicht rechenbar:** jede unbrauchbare Eingabe wirft [ReceiptDueError] * (`code: 'receipt_due_unavailable'`, `reason` im Einzelnen), nie still eine * falsche Zahl. Dann ist nichts gesendet. */ /** Wer das Trinkgeld ohne `recipients` bekommt (Kennzeichen des angemeldeten Kassen-Benutzers). */ export type ReceiptDueTipRecipient = 'owner' | 'staff'; /** Trinkgeld wie `tip` an `sellReceipt`: Betrag, optional mit Empfaengern samt Inhaber-Kennzeichen. */ export type ReceiptDueTip = number | { cents: number; recipients?: ReadonlyArray<{ cents: number; owner: boolean; }>; }; export interface ReceiptDueOptions { /** Trinkgeld als Parameter `tip`; nie zugleich mit `payments[].tipCents` (`tip_conflict`). */ tip?: ReceiptDueTip; /** * Die Zahlungen, soweit sie Trinkgeld tragen (`tipCents`). Der Server bucht * je Zahlart genau so, als waere `tip: { cents: Summe, paymentMethod }` * geschickt worden. Andere Felder zaehlen hier nicht. */ payments?: ReadonlyArray<{ method: KeckPaymentMethod | string; tipCents?: number; }>; /** Pflicht, sobald Trinkgeld ohne `recipients` vorkommt (siehe Modulkommentar). */ tipRecipient?: ReceiptDueTipRecipient; /** * Je Einloesung bzw. Auszahlung der neuen Form die Pruefung aus * `checkVoucher`, ueber den Code zugeordnet. Fehlt sie fuer einen Code, * wirft die Rechnung `invalid_voucher` mit `voucherErrorCode: * 'voucher_not_found'` (die Kasse hat den Code nicht geprueft). */ voucherInfo?: readonly VoucherDueInfo[]; /** * Gutschein-Einstellung der Kasse (`getKasseSettings`, Block `vouchers`): * Pflicht fuer jeden Verkauf eines Mehrzweck- oder Einzweckgutscheins (der * Server verkauft nur mit Einstellung). Fehlt sie oder ist keine Art * gewaehlt: `voucher_settings_missing`; eine andere Art als die * eingestellte: `voucher_invalid`. */ voucherSettings?: PosVoucherSettings; /** * Artikel fuer den Verkauf von Dienstleistungsgutscheinen (Preis und Satz, * wie im Artikelstamm). Fehlt der verkaufte Artikel hier: * `voucher_item_mismatch`, wie beim Server fuer einen unbekannten Artikel. */ voucherArticles?: readonly VoucherDueArticle[]; /** * Das Konto ist Kleinunternehmer (Konto-Fakten): Einzweck/Leistung werden zu * 0 % verkauft, eine Einzweck-Einloesung verteilt sich anteilig auf alle * Saetze des Korbs. Dasselbe gilt fuer einen befreit verkauften Gutschein * (`VoucherCheck.exempt`) in jedem Konto, wie der Server (`kuEingefroren || ku`). */ smallBusiness?: boolean; /** * Stichtag des 0-%-Topfs fuer Mehrzweck-Verkaeufe (`YYYY-MM-DD`). Vorgabe * [VOUCHER_ZERO_RATE_BUCKET_FROM], wie im Backend. Nur angeben, wenn der * Server ihn nachweislich anders fuehrt (Test-Umgebung). */ zeroRateBucketFrom?: string; /** Wiener Tag des Belegs (`YYYY-MM-DD`); Vorgabe: heute in Wien. */ today?: string; } /** Die sechs RKSV-Toepfe in Cent, Namen wie am Beleg des Backends. */ export interface ReceiptDueBuckets { amountRateStandard: number; amountRateReduced1: number; amountRateReduced2: number; amountRateZero: number; amountRateSpecial: number; amountRatOthers: number; } export interface ReceiptDueBreakdown { /** Was die Zahlungen zusammen ergeben muessen. */ dueCents: number; /** Delta des Umsatzzaehlers (Summe der Toepfe). */ counterDeltaCents: number; /** Anteil der Wertgutscheine am Zahlbetrag. */ valueVoucherFlowCents: number; /** Toepfe je auf Cent gerundet; `null` bei Start- und Nullbeleg. */ bucketsCents: ReceiptDueBuckets | null; } /** * Warum sich der Zahlbetrag nicht rechnen laesst. Fest wie ein Backend-Code; * dieselbe Liste im Dart-Zwilling und in `fixtures/receipt-due-errors.json`. */ export declare const RECEIPT_DUE_ERROR_REASONS: readonly ["unknown_receipt_type", "tip_not_allowed", "invalid_item", "invalid_voucher", "invalid_tip", "unknown_payment_method", "tip_conflict", "tip_recipient_missing", "tip_without_goods"]; export type ReceiptDueErrorReason = (typeof RECEIPT_DUE_ERROR_REASONS)[number]; /** * Der Zahlbetrag laesst sich aus dieser Eingabe nicht rechnen, etwa * Trinkgeld mit Betrag ohne Ware (`reason: 'tip_without_goods'`). * * Eindeutig **nicht gesendet**: die Rechnung laeuft ganz im Paket, vor jedem * Aufruf. `outcome` ist darum immer `'rejected'`; nichts ist geschehen, keine * Belegnummer verbraucht, keine Karte belastet. Die Kasse sagt das dem * Kassier, bevor sie ein Terminal anspricht. Entscheiden am `code` bzw. * `reason`, nie an `message` (die ist fuers Protokoll). * * Bis 1.0.0-rc.4 warf die Rechnung hier einen `RangeError` ohne Code. */ export declare class ReceiptDueError extends Error { readonly name = "ReceiptDueError"; /** Stabiler Code fuer jede Ursache. */ readonly code: "receipt_due_unavailable"; /** Die Ursache im Einzelnen. */ readonly reason: ReceiptDueErrorReason; /** Immer `'rejected'`: es ging nichts hinaus. */ readonly outcome: "rejected"; /** * Nur bei `invalid_voucher`: der Gutschein-Code, mit dem der Server * denselben Beleg abwiese (`voucher_rate_mismatch`, `voucher_exceeds_total`, * `voucher_not_found` …), fuer die Meldung an der Kasse. */ readonly voucherErrorCode?: string; constructor(reason: ReceiptDueErrorReason, message: string, voucherErrorCode?: string); } export declare function isReceiptDueError(error: unknown): error is ReceiptDueError; type BelegArt = ReceiptType | ReceiptTypeKey | string; /** Zahlbetrag in Cent, siehe Modulkommentar. */ export declare function receiptDueCents(items: readonly ReceiptItem[], vouchers: readonly Voucher[], receiptType: BelegArt, options?: ReceiptDueOptions): number; /** Wie [receiptDueCents], dazu Zaehler-Delta, Wertgutschein-Anteil und Toepfe. */ export declare function receiptDueBreakdown(items: readonly ReceiptItem[], vouchers: readonly Voucher[], receiptType: BelegArt, options?: ReceiptDueOptions): ReceiptDueBreakdown; /** Schon gewaehlte Gutscheine und Optionen des Belegs, vor dem Gutschein, der vorbelegt wird. */ export interface VoucherRedeemContext { vouchers?: readonly Voucher[]; options?: ReceiptDueOptions; } /** * Hoechstbetrag zum Vorbelegen einer Einloesung an der Kasse, in Cent, nie * negativ: der groesste Betrag, den der Server fuer diesen Gutschein annimmt. * * Obergrenze: Saldo, offener Betrag ohne Trinkgeld (ein Gutschein deckt nie * das Trinkgeld, Personal wie Inhaber); bei Einzweck die deckbaren Positionen im * Topf seines Satzes abzueglich der schon gewaehlten Einloesungen dort. Befreit * verkauft bzw. als Kleinunternehmer verteilt der Server anteilig auf alle * Saetze (`kuTeile`); dann begrenzt jeder Topf: sein Anteil plus die schon * gewaehlten Einloesungen dort darf seine Positionen nicht uebersteigen. * * Unter dieser Grenze wird jeder Kandidat mit genau der Rechnung von * [receiptDueCents] geprueft (Rundung halber Cent, groesster Rest), zuerst * Cent fuer Cent abwaerts (64 Schritte). Findet das nichts, sucht eine * Bisektion ueber [1, Grenze] und prueft danach 16 Cent aufwaerts: am Rand * des groessten Rests ist die Annahme nicht monoton. Leistung (wird ueber * Positionen eingeloest), Aktion (Rabatt rechnet der Server) und jeder nicht * aktive Gutschein: 0. Wirft nie [ReceiptDueError]: ist der Beleg ohne den * Gutschein nicht rechenbar (etwa `tip_without_goods`), ist das Ergebnis 0. */ export declare function voucherRedeemMaxCents(items: readonly ReceiptItem[], voucher: VoucherCheck, before?: VoucherRedeemContext): number;