@kreiseck/kasseneck-api 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -0
- package/README.md +69 -2
- package/dist/cjs/client/errors.d.ts +0 -1
- package/dist/cjs/client/errors.js +24 -1
- package/dist/cjs/rechnung/api.d.ts +3 -1
- package/dist/cjs/rechnung/api.js +1 -0
- package/dist/cjs/rechnung/endpunkte.d.ts +12 -1
- package/dist/cjs/rechnung/endpunkte.js +56 -3
- package/dist/cjs/rechnung/index.d.ts +4 -2
- package/dist/cjs/rechnung/index.js +16 -1
- package/dist/cjs/rechnung/rechnen.d.ts +123 -0
- package/dist/cjs/rechnung/rechnen.js +327 -0
- package/dist/cjs/rechnung/summen.d.ts +42 -0
- package/dist/cjs/rechnung/summen.js +74 -0
- package/dist/cjs/rechnung/texte.d.ts +4 -0
- package/dist/cjs/rechnung/texte.js +8 -0
- package/dist/cjs/rechnung/typen.d.ts +51 -7
- package/dist/cjs/rechnung/vertrag.d.ts +1 -1
- package/dist/cjs/rechnung/vertrag.js +8 -0
- package/dist/esm/client/errors.d.ts +0 -1
- package/dist/esm/client/errors.js +24 -1
- package/dist/esm/rechnung/api.d.ts +3 -1
- package/dist/esm/rechnung/api.js +2 -1
- package/dist/esm/rechnung/endpunkte.d.ts +12 -1
- package/dist/esm/rechnung/endpunkte.js +55 -3
- package/dist/esm/rechnung/index.d.ts +4 -2
- package/dist/esm/rechnung/index.js +3 -1
- package/dist/esm/rechnung/rechnen.d.ts +123 -0
- package/dist/esm/rechnung/rechnen.js +316 -0
- package/dist/esm/rechnung/summen.d.ts +42 -0
- package/dist/esm/rechnung/summen.js +71 -0
- package/dist/esm/rechnung/texte.d.ts +4 -0
- package/dist/esm/rechnung/texte.js +8 -0
- package/dist/esm/rechnung/typen.d.ts +51 -7
- package/dist/esm/rechnung/vertrag.d.ts +1 -1
- package/dist/esm/rechnung/vertrag.js +8 -0
- package/fixtures/hobex-hps-codes.json +1 -1
- package/fixtures/kasse-texte.json +1 -1
- package/fixtures/oberflaeche.json +12 -2
- package/fixtures/position-aus-euro.json +107 -0
- package/fixtures/rechnung-api.schema.json +7 -2
- package/fixtures/rechnung-rechnen-zufall.json +8451 -0
- package/fixtures/rechnung-rechnen.json +304 -0
- package/fixtures/rechnung-summen.json +154 -0
- package/fixtures/rechnung-texte.json +9 -1
- package/package.json +13 -2
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { type FetchLike } from '../client/transport.js';
|
|
8
8
|
import type { EInvoiceFormat, InvoiceLanguage } from './vertrag.js';
|
|
9
|
-
import type { Brand, CancelInvoiceRequest, CancelResult, CreditNoteRequest, CreditNoteResult, Customer, CustomerInput, CustomerPage, CustomerSearch, InvoiceDetail, InvoiceListQuery, InvoicePage, InvoiceSetupStatus, IssueInvoiceRequest, IssueResult, RecordPaymentRequest, RecordPaymentResult } from './typen.js';
|
|
9
|
+
import type { Brand, CancelInvoiceRequest, CancelResult, CreditNoteRequest, CreditNoteResult, Customer, CustomerInput, CustomerPage, CustomerSearch, InvoiceDetail, InvoiceListQuery, InvoicePage, InvoiceSetupStatus, IssueInvoiceRequest, IssueResult, PreviewResult, RecordPaymentRequest, RecordPaymentResult } from './typen.js';
|
|
10
10
|
export interface RechnungApiOptions {
|
|
11
11
|
/** `api_key` des Kontos (`kr_live_…` / `kr_test_…`). Gehoert auf einen Server. */
|
|
12
12
|
apiKey: string;
|
|
@@ -29,6 +29,8 @@ export interface RechnungApi {
|
|
|
29
29
|
updateCustomer(customerId: string, patch: Partial<CustomerInput>): Promise<Customer>;
|
|
30
30
|
searchCustomers(suche: CustomerSearch): Promise<CustomerPage>;
|
|
31
31
|
issueInvoice(anfrage: IssueInvoiceRequest): Promise<IssueResult>;
|
|
32
|
+
/** Probelauf: pruefen und rechnen wie `issueInvoice`, ohne auszustellen. */
|
|
33
|
+
previewInvoice(anfrage: IssueInvoiceRequest): Promise<PreviewResult>;
|
|
32
34
|
cancelInvoice(anfrage: CancelInvoiceRequest): Promise<CancelResult>;
|
|
33
35
|
createCreditNote(anfrage: CreditNoteRequest): Promise<CreditNoteResult>;
|
|
34
36
|
getInvoice(kennung: {
|
package/dist/esm/rechnung/api.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { createBinaryTransport, createTransport } from '../client/transport.js';
|
|
8
8
|
import { rechnungKeyAuth } from './auth.js';
|
|
9
|
-
import { cancelInvoice, createCreditNote, createCustomer, getCustomer, getInvoice, getInvoicePdf, getInvoiceSetupStatus, getInvoiceXml, issueInvoice, listBrands, listInvoices, recordInvoicePayment, searchCustomers, updateCustomer, } from './endpunkte.js';
|
|
9
|
+
import { cancelInvoice, createCreditNote, createCustomer, getCustomer, getInvoice, getInvoicePdf, getInvoiceSetupStatus, getInvoiceXml, issueInvoice, listBrands, previewInvoice, listInvoices, recordInvoicePayment, searchCustomers, updateCustomer, } from './endpunkte.js';
|
|
10
10
|
export function createRechnungApi(optionen) {
|
|
11
11
|
const transportOptionen = {
|
|
12
12
|
auth: rechnungKeyAuth({ apiKey: optionen.apiKey }),
|
|
@@ -22,6 +22,7 @@ export function createRechnungApi(optionen) {
|
|
|
22
22
|
updateCustomer: (id, patch) => updateCustomer(rufen, id, patch),
|
|
23
23
|
searchCustomers: (suche) => searchCustomers(rufen, suche),
|
|
24
24
|
issueInvoice: (anfrage) => issueInvoice(rufen, anfrage),
|
|
25
|
+
previewInvoice: (anfrage) => previewInvoice(rufen, anfrage),
|
|
25
26
|
cancelInvoice: (anfrage) => cancelInvoice(rufen, anfrage),
|
|
26
27
|
createCreditNote: (anfrage) => createCreditNote(rufen, anfrage),
|
|
27
28
|
getInvoice: (kennung) => getInvoice(rufen, kennung),
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import type { InternerBinaerTransport, InternerTransport } from '../client/aufrufe.js';
|
|
20
20
|
import type { EInvoiceFormat, InvoiceLanguage } from './vertrag.js';
|
|
21
|
-
import type { Brand, CancelInvoiceRequest, CancelResult, CreditNoteRequest, CreditNoteResult, Customer, CustomerInput, CustomerPage, CustomerSearch, InvoiceDetail, InvoiceListQuery, InvoicePage, InvoiceSetupStatus, IssueInvoiceRequest, IssueResult, RecordPaymentRequest, RecordPaymentResult } from './typen.js';
|
|
21
|
+
import type { Brand, CancelInvoiceRequest, CancelResult, CreditNoteRequest, CreditNoteResult, Customer, CustomerInput, CustomerPage, CustomerSearch, InvoiceDetail, InvoiceListQuery, InvoicePage, InvoiceSetupStatus, IssueInvoiceRequest, IssueResult, PreviewResult, RecordPaymentRequest, RecordPaymentResult } from './typen.js';
|
|
22
22
|
export declare function createCustomer(rufen: InternerTransport, customer: CustomerInput, optionen?: {
|
|
23
23
|
idempotencyKey?: string;
|
|
24
24
|
}): Promise<Customer>;
|
|
@@ -30,6 +30,17 @@ export declare function getCustomer(rufen: InternerTransport, kennung: {
|
|
|
30
30
|
export declare function updateCustomer(rufen: InternerTransport, customerId: string, patch: Partial<CustomerInput>): Promise<Customer>;
|
|
31
31
|
export declare function searchCustomers(rufen: InternerTransport, suche: CustomerSearch): Promise<CustomerPage>;
|
|
32
32
|
export declare function issueInvoice(rufen: InternerTransport, anfrage: IssueInvoiceRequest): Promise<IssueResult>;
|
|
33
|
+
/**
|
|
34
|
+
* Probelauf von `issueInvoice`: dieselbe Anfrage wird geprueft und gerechnet
|
|
35
|
+
* wie beim Ausstellen, aber nichts festgeschrieben. Die Antwort nennt Summen,
|
|
36
|
+
* Steuerfall, Sprache, Marke und die Hinweise — oder scheitert mit demselben
|
|
37
|
+
* Fehlercode, mit dem das Ausstellen scheitern wuerde.
|
|
38
|
+
*
|
|
39
|
+
* Der `idempotencyKey` wird nicht verbraucht: dieselbe Anfrage laesst sich
|
|
40
|
+
* danach unveraendert ausstellen. Verbindlich ist das Ausstellen — zwischen
|
|
41
|
+
* Probelauf und Ausstellen kann sich der Kunde oder das Konto aendern.
|
|
42
|
+
*/
|
|
43
|
+
export declare function previewInvoice(rufen: InternerTransport, anfrage: IssueInvoiceRequest): Promise<PreviewResult>;
|
|
33
44
|
export declare function cancelInvoice(rufen: InternerTransport, anfrage: CancelInvoiceRequest): Promise<CancelResult>;
|
|
34
45
|
export declare function createCreditNote(rufen: InternerTransport, anfrage: CreditNoteRequest): Promise<CreditNoteResult>;
|
|
35
46
|
export declare function getInvoice(rufen: InternerTransport, kennung: {
|
|
@@ -39,6 +39,33 @@ function seite(aufruf, daten, feld) {
|
|
|
39
39
|
return { eintraege: o[feld], nextCursor: typeof o['nextCursor'] === 'string' ? o['nextCursor'] : null };
|
|
40
40
|
}
|
|
41
41
|
const zahl = (wert) => (typeof wert === 'number' && Number.isFinite(wert) ? wert : 0);
|
|
42
|
+
/**
|
|
43
|
+
* Die Hinweise einer Antwort (`data.notice`) — immer eine Liste, `undefined`
|
|
44
|
+
* ohne Hinweis. Ein einzelnes Objekt (Server vor der Vereinheitlichung) wird
|
|
45
|
+
* zur Liste.
|
|
46
|
+
*
|
|
47
|
+
* Ein unbrauchbarer Eintrag wird uebergangen, nicht geworfen: der Aufruf hat
|
|
48
|
+
* schon gewirkt (die Rechnung ist ausgestellt, die Zahlung gebucht). Ein
|
|
49
|
+
* Fehler an dieser Stelle liesse den Aufrufer glauben, es sei nichts
|
|
50
|
+
* entstanden — und eine Wiederholung liefert die Hinweise nicht noch einmal.
|
|
51
|
+
*/
|
|
52
|
+
function hinweise(daten) {
|
|
53
|
+
const roh = objekt(daten)['notice'];
|
|
54
|
+
if (roh === undefined || roh === null)
|
|
55
|
+
return undefined;
|
|
56
|
+
const liste = (Array.isArray(roh) ? roh : [roh]).filter((h) => typeof objekt(h)['code'] === 'string' && typeof objekt(h)['message'] === 'string');
|
|
57
|
+
return liste.length ? liste : undefined;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Eine Kennung muss ein Objekt sein (`{ invoiceId }`, `{ customerId }` …). Ein
|
|
61
|
+
* blosser Text wuerde sonst Zeichen fuer Zeichen zu Feldern `0`, `1`, …
|
|
62
|
+
* zerlegt, und der Server koennte nur „Bitte Eingaben pruefen" antworten.
|
|
63
|
+
*/
|
|
64
|
+
function kennungVerlangen(aufruf, kennung, felder) {
|
|
65
|
+
if (kennung === null || typeof kennung !== 'object' || Array.isArray(kennung)) {
|
|
66
|
+
throw new KasseneckValidationError(aufruf, `Kennung als Objekt erwartet: ${felder}`, 'request');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
42
69
|
/** Anfrageobjekt als Nutzlast — flache Kopie, damit der Aufrufer sein Objekt behaelt. */
|
|
43
70
|
const nutzlast = (anfrage) => ({ ...anfrage });
|
|
44
71
|
// ---- Kunden -----------------------------------------------------------------
|
|
@@ -50,6 +77,7 @@ export async function createCustomer(rufen, customer, optionen = {}) {
|
|
|
50
77
|
return pflichtObjekt('createCustomer', daten, 'customer');
|
|
51
78
|
}
|
|
52
79
|
export async function getCustomer(rufen, kennung) {
|
|
80
|
+
kennungVerlangen('getCustomer', kennung, '{ customerId } oder { externalId }');
|
|
53
81
|
const daten = await rufen('getCustomer', nutzlast(kennung));
|
|
54
82
|
return pflichtObjekt('getCustomer', daten, 'customer');
|
|
55
83
|
}
|
|
@@ -65,10 +93,32 @@ export async function searchCustomers(rufen, suche) {
|
|
|
65
93
|
// ---- Rechnungen -------------------------------------------------------------
|
|
66
94
|
export async function issueInvoice(rufen, anfrage) {
|
|
67
95
|
const daten = await rufen('issueInvoice', nutzlast(anfrage));
|
|
68
|
-
|
|
96
|
+
const ergebnis = {
|
|
69
97
|
invoice: pflichtObjekt('issueInvoice', daten, 'invoice'),
|
|
70
98
|
replayed: objekt(daten)['replayed'] === true,
|
|
71
99
|
};
|
|
100
|
+
const notice = hinweise(daten);
|
|
101
|
+
if (notice)
|
|
102
|
+
ergebnis.notice = notice;
|
|
103
|
+
return ergebnis;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Probelauf von `issueInvoice`: dieselbe Anfrage wird geprueft und gerechnet
|
|
107
|
+
* wie beim Ausstellen, aber nichts festgeschrieben. Die Antwort nennt Summen,
|
|
108
|
+
* Steuerfall, Sprache, Marke und die Hinweise — oder scheitert mit demselben
|
|
109
|
+
* Fehlercode, mit dem das Ausstellen scheitern wuerde.
|
|
110
|
+
*
|
|
111
|
+
* Der `idempotencyKey` wird nicht verbraucht: dieselbe Anfrage laesst sich
|
|
112
|
+
* danach unveraendert ausstellen. Verbindlich ist das Ausstellen — zwischen
|
|
113
|
+
* Probelauf und Ausstellen kann sich der Kunde oder das Konto aendern.
|
|
114
|
+
*/
|
|
115
|
+
export async function previewInvoice(rufen, anfrage) {
|
|
116
|
+
const daten = await rufen('issueInvoice', { ...nutzlast(anfrage), dryRun: true });
|
|
117
|
+
const ergebnis = { preview: pflichtObjekt('issueInvoice', daten, 'preview') };
|
|
118
|
+
const notice = hinweise(daten);
|
|
119
|
+
if (notice)
|
|
120
|
+
ergebnis.notice = notice;
|
|
121
|
+
return ergebnis;
|
|
72
122
|
}
|
|
73
123
|
export async function cancelInvoice(rufen, anfrage) {
|
|
74
124
|
const daten = await rufen('cancelInvoice', nutzlast(anfrage));
|
|
@@ -88,6 +138,7 @@ export async function createCreditNote(rufen, anfrage) {
|
|
|
88
138
|
};
|
|
89
139
|
}
|
|
90
140
|
export async function getInvoice(rufen, kennung) {
|
|
141
|
+
kennungVerlangen('getInvoice', kennung, '{ invoiceId } oder { number }');
|
|
91
142
|
const daten = await rufen('getInvoice', nutzlast(kennung));
|
|
92
143
|
return pflichtObjekt('getInvoice', daten, 'invoice');
|
|
93
144
|
}
|
|
@@ -143,8 +194,9 @@ export async function recordInvoicePayment(rufen, anfrage) {
|
|
|
143
194
|
payment: pflichtObjekt('recordInvoicePayment', daten, 'payment'),
|
|
144
195
|
replayed: roh['replayed'] === true,
|
|
145
196
|
};
|
|
146
|
-
|
|
147
|
-
|
|
197
|
+
const notice = hinweise(daten);
|
|
198
|
+
if (notice)
|
|
199
|
+
ergebnis.notice = notice;
|
|
148
200
|
return ergebnis;
|
|
149
201
|
}
|
|
150
202
|
/** Die Marken des Kontos — die Kennung geht als `brandId` in `issueInvoice`. */
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
export { createRechnungApi, type RechnungApi, type RechnungApiOptions } from './api.js';
|
|
14
14
|
export { rechnungKeyAuth, type RechnungKeyAuthOptions } from './auth.js';
|
|
15
15
|
export { istRechnungFehler, rechnungFehlerCode, rechnungFeldFehler, type RechnungFeldFehler, } from './fehler.js';
|
|
16
|
-
export { cancelInvoice, createCreditNote, createCustomer, getCustomer, getInvoice, getInvoicePdf, getInvoiceSetupStatus, getInvoiceXml, issueInvoice, listBrands, listInvoices, recordInvoicePayment, searchCustomers, updateCustomer, } from './endpunkte.js';
|
|
17
|
-
export type { Brand, CancelInvoiceRequest, CancelResult, CreditNoteRequest, CreditNoteResult, Customer, CustomerInput, CustomerPage, CustomerSearch, EInvoiceStatus, Invoice, InvoiceDetail, InvoiceItem, InvoiceItemInput, InvoiceListQuery, InvoicePage, InvoiceRecipient, InvoiceSetupGap, InvoiceSetupStatus, InvoiceTotals, InvoiceNotice, InvoicePayment, IssueInvoiceRequest, IssueResult, PaymentInput, RecordPaymentRequest, RecordPaymentResult, } from './typen.js';
|
|
16
|
+
export { cancelInvoice, createCreditNote, createCustomer, getCustomer, getInvoice, getInvoicePdf, getInvoiceSetupStatus, getInvoiceXml, issueInvoice, listBrands, listInvoices, previewInvoice, recordInvoicePayment, searchCustomers, updateCustomer, } from './endpunkte.js';
|
|
17
|
+
export type { Brand, CancelInvoiceRequest, CancelResult, CreditNoteRequest, CreditNoteResult, Customer, CustomerInput, CustomerPage, CustomerSearch, EInvoiceStatus, Invoice, InvoiceDetail, InvoiceItem, InvoiceItemInput, InvoiceListQuery, InvoicePage, InvoiceRecipient, InvoiceSetupGap, InvoiceSetupStatus, InvoiceTotals, InvoiceNotice, InvoicePayment, InvoicePreview, InvoiceRateTotals, IssueInvoiceRequest, IssueResult, PaymentInput, PreviewResult, RecordPaymentRequest, RecordPaymentResult, } from './typen.js';
|
|
18
18
|
export { RECHNUNG_TEXTE, rechnungText, type RechnungTextSchluessel } from './texte.js';
|
|
19
|
+
export { rechnungSummen, STEUERFREIE_FAELLE, type SummenPosition } from './summen.js';
|
|
20
|
+
export { anteiligerPreis, BETRAG_GRENZE_CENTS, positionAusEuro, preisText, RechenFehler, rechnungRechnen, rund, satzSchluessel, satzText, type RechenErgebnis, type RechenFehlerCode, type RechenOptionen, type RechenPosition, type SatzSumme, type Umwandlung, type UmwandlungsGrund, type ZeilenBetrag, } from './rechnen.js';
|
|
19
21
|
export { RECHNUNG_VERTRAG_VERSION, RECHNUNG_AUFRUFE, INVOICE_ERROR_CODES, CREDIT_NOTE_REASONS, TAX_SCHEMES, PRICE_MODES, VAT_RATES, CUSTOMER_TYPES, INVOICE_LIST_STATUS, DOC_TYPES, EINVOICE_FORMATS, INVOICE_LANGUAGES, INVOICE_PAYMENT_METHODS, ITEM_KINDS, REVERSE_CHARGE_REASONS, INVOICE_NOTICE_CODES, INVOICE_UNITS, RECHNUNG_EINHEITEN_CODES, INVOICE_SETUP_REQUIREMENTS, KUNDE_FELDER, POSITION_FELDER, RECHNUNG_ANFRAGEN, RECHNUNG_GENAU_EINS, RECHNUNG_MINDESTENS_EINS, type RechnungAufruf, type InvoiceErrorCode, type CreditNoteReason, type TaxScheme, type PriceMode, type VatRatePercent, type CustomerType, type InvoiceListStatus, type DocType, type EInvoiceFormat, type InvoiceLanguage, type InvoicePaymentMethod, type ItemKind, type ReverseChargeReason, type InvoiceNoticeCode, type InvoiceUnit, type InvoiceSetupRequirement, type Format, type Feld, } from './vertrag.js';
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
export { createRechnungApi } from './api.js';
|
|
14
14
|
export { rechnungKeyAuth } from './auth.js';
|
|
15
15
|
export { istRechnungFehler, rechnungFehlerCode, rechnungFeldFehler, } from './fehler.js';
|
|
16
|
-
export { cancelInvoice, createCreditNote, createCustomer, getCustomer, getInvoice, getInvoicePdf, getInvoiceSetupStatus, getInvoiceXml, issueInvoice, listBrands, listInvoices, recordInvoicePayment, searchCustomers, updateCustomer, } from './endpunkte.js';
|
|
16
|
+
export { cancelInvoice, createCreditNote, createCustomer, getCustomer, getInvoice, getInvoicePdf, getInvoiceSetupStatus, getInvoiceXml, issueInvoice, listBrands, listInvoices, previewInvoice, recordInvoicePayment, searchCustomers, updateCustomer, } from './endpunkte.js';
|
|
17
17
|
export { RECHNUNG_TEXTE, rechnungText } from './texte.js';
|
|
18
|
+
export { rechnungSummen, STEUERFREIE_FAELLE } from './summen.js';
|
|
19
|
+
export { anteiligerPreis, BETRAG_GRENZE_CENTS, positionAusEuro, preisText, RechenFehler, rechnungRechnen, rund, satzSchluessel, satzText, } from './rechnen.js';
|
|
18
20
|
export { RECHNUNG_VERTRAG_VERSION, RECHNUNG_AUFRUFE, INVOICE_ERROR_CODES, CREDIT_NOTE_REASONS, TAX_SCHEMES, PRICE_MODES, VAT_RATES, CUSTOMER_TYPES, INVOICE_LIST_STATUS, DOC_TYPES, EINVOICE_FORMATS, INVOICE_LANGUAGES, INVOICE_PAYMENT_METHODS, ITEM_KINDS, REVERSE_CHARGE_REASONS, INVOICE_NOTICE_CODES, INVOICE_UNITS, RECHNUNG_EINHEITEN_CODES, INVOICE_SETUP_REQUIREMENTS, KUNDE_FELDER, POSITION_FELDER, RECHNUNG_ANFRAGEN, RECHNUNG_GENAU_EINS, RECHNUNG_MINDESTENS_EINS, } from './vertrag.js';
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Der Rechenkern der Rechnung — exakt, in ganzen Zahlen.
|
|
3
|
+
*
|
|
4
|
+
* Eine Zeile ist ein Bruch: Preis (µ€) × Menge (Tausendstel) × Rabattanteil
|
|
5
|
+
* (Hundertstel-Prozent). Multipliziert man die Skalen, ist die kleinste Einheit
|
|
6
|
+
* 10⁻¹⁷ Cent — das passt in keine Gleitkommazahl und in keine 53-Bit-Ganzzahl,
|
|
7
|
+
* also rechnet der Kern mit BigInt. Gerundet wird genau EINMAL je USt-Satz, auf
|
|
8
|
+
* dem Bruch, kaufmaennisch (halbe Einheit vom Nullpunkt weg).
|
|
9
|
+
*
|
|
10
|
+
* Warum das so streng ist: Vier Umsetzungen (Server, dieses Paket, Dart, Panel)
|
|
11
|
+
* mussten bisher dieselbe Reihenfolge von Gleitkomma-Schritten nachbauen, damit
|
|
12
|
+
* Grenzfaelle gleich runden. Mit Ganzzahlen gibt es keine Reihenfolge mehr, an
|
|
13
|
+
* der etwas auseinanderlaufen koennte.
|
|
14
|
+
*
|
|
15
|
+
* Dieser Unterpfad ist rein: kein Transport, kein Zugangsschluessel, keine
|
|
16
|
+
* Abhaengigkeit ausser Typen. Er wird im Browser gebuendelt (Panel) und im
|
|
17
|
+
* Server geladen.
|
|
18
|
+
*
|
|
19
|
+
* Spec: docs/specs/2026-09-17-rechnung-ganzzahlen-design.md § 5.
|
|
20
|
+
*/
|
|
21
|
+
import type { PriceMode, TaxScheme } from './vertrag.js';
|
|
22
|
+
/** Hoechster Betrag je Zeile und je Rechnung: 999.999.999,99 €. */
|
|
23
|
+
export declare const BETRAG_GRENZE_CENTS = 99999999999;
|
|
24
|
+
/** Steuerfaelle, in denen die Rechnung keine Steuer ausweist: jede Zeile zaehlt zu 0 %. */
|
|
25
|
+
export declare const STEUERFREIE_FAELLE: readonly TaxScheme[];
|
|
26
|
+
/** Eine Position in gespeicherter Form — alle Werte ganzzahlig. */
|
|
27
|
+
export interface RechenPosition {
|
|
28
|
+
/** Einzelpreis in Millionstel Euro, 0 … 10¹². */
|
|
29
|
+
unitPriceMicros: number;
|
|
30
|
+
/** Menge in Tausendstel; negativ = Abzugszeile, 0 = Textzeile. */
|
|
31
|
+
quantityMilli: number;
|
|
32
|
+
/** Rabatt in Hundertstel-Prozent, 0 … 10000 (ohne Angabe 0). */
|
|
33
|
+
discountBp?: number;
|
|
34
|
+
/** USt-Satz in Hundertstel-Prozent, 0 … 10000 (ohne Angabe 0). */
|
|
35
|
+
vatRateBp?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface RechenOptionen {
|
|
38
|
+
priceMode: PriceMode;
|
|
39
|
+
/** Ohne Angabe `normal`. */
|
|
40
|
+
taxScheme?: TaxScheme;
|
|
41
|
+
}
|
|
42
|
+
export interface SatzSumme {
|
|
43
|
+
rateBp: number;
|
|
44
|
+
netCents: number;
|
|
45
|
+
vatCents: number;
|
|
46
|
+
grossCents: number;
|
|
47
|
+
}
|
|
48
|
+
export interface ZeilenBetrag {
|
|
49
|
+
netCents: number;
|
|
50
|
+
grossCents: number;
|
|
51
|
+
/** Der Satz, mit dem die Zeile gerechnet wurde (bei steuerfreiem Fall 0). */
|
|
52
|
+
rateBp: number;
|
|
53
|
+
}
|
|
54
|
+
export interface RechenErgebnis {
|
|
55
|
+
netCents: number;
|
|
56
|
+
vatCents: number;
|
|
57
|
+
grossCents: number;
|
|
58
|
+
/** Absteigend nach `rateBp`. */
|
|
59
|
+
byRate: SatzSumme[];
|
|
60
|
+
/** Je Position, in der Reihenfolge der Eingabe. */
|
|
61
|
+
lines: ZeilenBetrag[];
|
|
62
|
+
}
|
|
63
|
+
export type RechenFehlerCode = 'amount_too_large' | 'kein_ganzzahlwert' | 'ausserhalb';
|
|
64
|
+
/** Fehler des Kerns — mit Code, Feld und Index, damit die API daraus einen Feldfehler machen kann. */
|
|
65
|
+
export declare class RechenFehler extends Error {
|
|
66
|
+
readonly code: RechenFehlerCode;
|
|
67
|
+
readonly feld?: string;
|
|
68
|
+
readonly index?: number;
|
|
69
|
+
constructor(code: RechenFehlerCode, nachricht: string, feld?: string, index?: number);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* `a / b` kaufmaennisch gerundet, halbe Einheit vom Nullpunkt weg; `b` > 0.
|
|
73
|
+
*
|
|
74
|
+
* Ohne Gleitkomma: `(2·|a| + b) / (2·b)` ist genau dann eins groesser, wenn der
|
|
75
|
+
* Rest mindestens die halbe Einheit betraegt.
|
|
76
|
+
*/
|
|
77
|
+
export declare function rund(a: bigint, b: bigint): bigint;
|
|
78
|
+
export declare function rechnungRechnen(positionen: readonly RechenPosition[], optionen: RechenOptionen): RechenErgebnis;
|
|
79
|
+
export type UmwandlungsGrund = 'kein_zahlwert' | 'nachkommastellen' | 'ausserhalb';
|
|
80
|
+
export type Umwandlung = {
|
|
81
|
+
ok: true;
|
|
82
|
+
position: RechenPosition & Record<string, unknown>;
|
|
83
|
+
} | {
|
|
84
|
+
ok: false;
|
|
85
|
+
feld: 'unitPrice' | 'quantity' | 'vatRate' | 'discountPct';
|
|
86
|
+
grund: UmwandlungsGrund;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Euro-Position → Ganzzahl-Form, verlustfrei oder mit Grund abgelehnt. Die
|
|
90
|
+
* EINE Stelle, die das tut: Panel (beim Lesen), SEPA-Lauf, Umrechnungsskript
|
|
91
|
+
* und das Festschreiben benutzen alle diese Funktion, damit niemand eine
|
|
92
|
+
* zweite Auslegung baut.
|
|
93
|
+
*
|
|
94
|
+
* Eine Position, die schon `unitPriceMicros` traegt, kommt unveraendert
|
|
95
|
+
* zurueck — die Funktion ist die Formweiche, nicht eine blinde Umrechnung.
|
|
96
|
+
*
|
|
97
|
+
* Fehlende Menge und fehlender Satz gelten als 0, weil der Server bisher so
|
|
98
|
+
* abgerechnet hat. Ein fehlender Preis dagegen ist NIE 0: sonst wuerde eine
|
|
99
|
+
* Position, der nur der Preis fehlt, still zur 0-€-Zeile.
|
|
100
|
+
*/
|
|
101
|
+
export declare function positionAusEuro(item: Record<string, unknown>): Umwandlung;
|
|
102
|
+
/**
|
|
103
|
+
* Anteiliger Preis fuer einen Teilzeitraum (erste Rechnung eines Abos).
|
|
104
|
+
* War der Ursprungspreis centgenau, bleibt auch der Anteil centgenau — sonst
|
|
105
|
+
* stuende auf der ersten Rechnung ein Preis mit sechs Stellen, wo bisher zwei
|
|
106
|
+
* standen.
|
|
107
|
+
*/
|
|
108
|
+
export declare function anteiligerPreis(unitPriceMicros: number, monate: number, intervall: number): number;
|
|
109
|
+
/**
|
|
110
|
+
* Schluessel eines USt-Satzes in den gespeicherten Maps (`creditedCents.byRate`,
|
|
111
|
+
* `invoice_stats.vatByRate`): der Prozenttext mit Punkt, genau wie
|
|
112
|
+
* `String(satz)` ihn bisher gebildet hat. NICHT fuer die Anzeige — dafuer gibt
|
|
113
|
+
* es `satzText`.
|
|
114
|
+
*/
|
|
115
|
+
export declare function satzSchluessel(rateBp: number): string;
|
|
116
|
+
/** Ein USt-Satz fuer die Anzeige: oesterreichisch mit Komma, ohne nachlaufende Nullen. */
|
|
117
|
+
export declare function satzText(rateBp: number): string;
|
|
118
|
+
/**
|
|
119
|
+
* Ein Einzelpreis fuer die Anzeige: mindestens zwei, hoechstens sechs
|
|
120
|
+
* Nachkommastellen, Tausenderpunkt, Komma als Trennzeichen — in jeder Sprache
|
|
121
|
+
* oesterreichisch, wie die Betraege auf dem Blatt.
|
|
122
|
+
*/
|
|
123
|
+
export declare function preisText(unitPriceMicros: number): string;
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Der Rechenkern der Rechnung — exakt, in ganzen Zahlen.
|
|
3
|
+
*
|
|
4
|
+
* Eine Zeile ist ein Bruch: Preis (µ€) × Menge (Tausendstel) × Rabattanteil
|
|
5
|
+
* (Hundertstel-Prozent). Multipliziert man die Skalen, ist die kleinste Einheit
|
|
6
|
+
* 10⁻¹⁷ Cent — das passt in keine Gleitkommazahl und in keine 53-Bit-Ganzzahl,
|
|
7
|
+
* also rechnet der Kern mit BigInt. Gerundet wird genau EINMAL je USt-Satz, auf
|
|
8
|
+
* dem Bruch, kaufmaennisch (halbe Einheit vom Nullpunkt weg).
|
|
9
|
+
*
|
|
10
|
+
* Warum das so streng ist: Vier Umsetzungen (Server, dieses Paket, Dart, Panel)
|
|
11
|
+
* mussten bisher dieselbe Reihenfolge von Gleitkomma-Schritten nachbauen, damit
|
|
12
|
+
* Grenzfaelle gleich runden. Mit Ganzzahlen gibt es keine Reihenfolge mehr, an
|
|
13
|
+
* der etwas auseinanderlaufen koennte.
|
|
14
|
+
*
|
|
15
|
+
* Dieser Unterpfad ist rein: kein Transport, kein Zugangsschluessel, keine
|
|
16
|
+
* Abhaengigkeit ausser Typen. Er wird im Browser gebuendelt (Panel) und im
|
|
17
|
+
* Server geladen.
|
|
18
|
+
*
|
|
19
|
+
* Spec: docs/specs/2026-09-17-rechnung-ganzzahlen-design.md § 5.
|
|
20
|
+
*/
|
|
21
|
+
/** Skala des Zeilenbruchs: 10⁻¹⁷ Cent. */
|
|
22
|
+
const E = 10n ** 17n;
|
|
23
|
+
/** Hoechster Betrag je Zeile und je Rechnung: 999.999.999,99 €. */
|
|
24
|
+
export const BETRAG_GRENZE_CENTS = 99_999_999_999;
|
|
25
|
+
/** Steuerfaelle, in denen die Rechnung keine Steuer ausweist: jede Zeile zaehlt zu 0 %. */
|
|
26
|
+
export const STEUERFREIE_FAELLE = Object.freeze([
|
|
27
|
+
'smallBusiness',
|
|
28
|
+
'reverseCharge',
|
|
29
|
+
'igLieferung',
|
|
30
|
+
'exportThirdCountry',
|
|
31
|
+
'domesticReverseCharge',
|
|
32
|
+
'outsideScope',
|
|
33
|
+
]);
|
|
34
|
+
/** Fehler des Kerns — mit Code, Feld und Index, damit die API daraus einen Feldfehler machen kann. */
|
|
35
|
+
export class RechenFehler extends Error {
|
|
36
|
+
code;
|
|
37
|
+
feld;
|
|
38
|
+
index;
|
|
39
|
+
constructor(code, nachricht, feld, index) {
|
|
40
|
+
super(nachricht);
|
|
41
|
+
this.name = 'RechenFehler';
|
|
42
|
+
this.code = code;
|
|
43
|
+
this.feld = feld;
|
|
44
|
+
this.index = index;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `a / b` kaufmaennisch gerundet, halbe Einheit vom Nullpunkt weg; `b` > 0.
|
|
49
|
+
*
|
|
50
|
+
* Ohne Gleitkomma: `(2·|a| + b) / (2·b)` ist genau dann eins groesser, wenn der
|
|
51
|
+
* Rest mindestens die halbe Einheit betraegt.
|
|
52
|
+
*/
|
|
53
|
+
export function rund(a, b) {
|
|
54
|
+
const negativ = a < 0n;
|
|
55
|
+
const betrag = negativ ? -a : a;
|
|
56
|
+
const ganz = (2n * betrag + b) / (2n * b);
|
|
57
|
+
return negativ ? -ganz : ganz;
|
|
58
|
+
}
|
|
59
|
+
const GRENZEN = {
|
|
60
|
+
unitPriceMicros: [0, 1_000_000_000_000],
|
|
61
|
+
quantityMilli: [-1_000_000_000_000, 1_000_000_000_000],
|
|
62
|
+
discountBp: [0, 10_000],
|
|
63
|
+
vatRateBp: [0, 10_000],
|
|
64
|
+
};
|
|
65
|
+
function ganzzahl(wert, feld, index) {
|
|
66
|
+
if (typeof wert !== 'number' || !Number.isInteger(wert)) {
|
|
67
|
+
throw new RechenFehler('kein_ganzzahlwert', `items[${index}].${feld} muss eine ganze Zahl sein`, feld, index);
|
|
68
|
+
}
|
|
69
|
+
const [min, max] = GRENZEN[feld];
|
|
70
|
+
if (wert < min || wert > max) {
|
|
71
|
+
throw new RechenFehler('ausserhalb', `items[${index}].${feld} liegt ausserhalb von ${min} … ${max}`, feld, index);
|
|
72
|
+
}
|
|
73
|
+
return wert;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Verteilt die Zeilenbetraege eines Satzes so, dass sie genau die Satzsumme
|
|
77
|
+
* ergeben. Startwert ist die kaufmaennisch gerundete Zeile; der Rest geht
|
|
78
|
+
* einzeln an die Zeilen, bei denen das Runden am meisten weggenommen (bzw.
|
|
79
|
+
* zugegeben) hat. Verglichen wird auf dem Bruch, nie in Gleitkomma; bei
|
|
80
|
+
* Gleichstand bekommt die fruehere Zeile den Cent. Eine Zeile ueber 0 bekommt
|
|
81
|
+
* nie einen Cent — sonst stuende auf dem Blatt ein Betrag, den die Position
|
|
82
|
+
* nicht hat.
|
|
83
|
+
*/
|
|
84
|
+
function verteile(gruppe, feld, faktor, nenner, ziel) {
|
|
85
|
+
const werte = gruppe.map((z) => {
|
|
86
|
+
const zaehler = z.L * faktor;
|
|
87
|
+
return { z, zaehler, wert: rund(zaehler, nenner) };
|
|
88
|
+
});
|
|
89
|
+
let rest = ziel - werte.reduce((s, w) => s + w.wert, 0n);
|
|
90
|
+
if (rest !== 0n) {
|
|
91
|
+
const schritt = rest > 0n ? 1n : -1n;
|
|
92
|
+
const kandidaten = werte
|
|
93
|
+
.filter((w) => w.zaehler !== 0n)
|
|
94
|
+
.sort((a, b) => {
|
|
95
|
+
const da = (a.zaehler - a.wert * nenner) * schritt;
|
|
96
|
+
const db = (b.zaehler - b.wert * nenner) * schritt;
|
|
97
|
+
if (da !== db)
|
|
98
|
+
return db > da ? 1 : -1;
|
|
99
|
+
return a.z.index - b.z.index;
|
|
100
|
+
});
|
|
101
|
+
for (let k = 0; rest !== 0n && kandidaten.length > 0; k = (k + 1) % kandidaten.length) {
|
|
102
|
+
kandidaten[k].wert += schritt;
|
|
103
|
+
rest -= schritt;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const w of werte)
|
|
107
|
+
w.z[feld] = Number(w.wert);
|
|
108
|
+
}
|
|
109
|
+
export function rechnungRechnen(positionen, optionen) {
|
|
110
|
+
const steuerfrei = STEUERFREIE_FAELLE.includes(optionen.taxScheme ?? 'normal');
|
|
111
|
+
const bruttoPreise = optionen.priceMode === 'gross' && !steuerfrei;
|
|
112
|
+
const grenze = BigInt(BETRAG_GRENZE_CENTS) * E;
|
|
113
|
+
const zeilen = positionen.map((p, index) => {
|
|
114
|
+
const preis = BigInt(ganzzahl(p.unitPriceMicros, 'unitPriceMicros', index));
|
|
115
|
+
const menge = BigInt(ganzzahl(p.quantityMilli, 'quantityMilli', index));
|
|
116
|
+
const rabatt = BigInt(ganzzahl(p.discountBp ?? 0, 'discountBp', index));
|
|
117
|
+
const satz = ganzzahl(p.vatRateBp ?? 0, 'vatRateBp', index);
|
|
118
|
+
const L = preis * menge * (10000n - rabatt) * 1000000n;
|
|
119
|
+
if ((L < 0n ? -L : L) > grenze) {
|
|
120
|
+
throw new RechenFehler('amount_too_large', `items[${index}] uebersteigt ${BETRAG_GRENZE_CENTS} Cent`, 'unitPriceMicros', index);
|
|
121
|
+
}
|
|
122
|
+
return { index, rateBp: steuerfrei ? 0 : satz, L, netCents: 0, grossCents: 0 };
|
|
123
|
+
});
|
|
124
|
+
const jeSatz = new Map();
|
|
125
|
+
for (const z of zeilen) {
|
|
126
|
+
const gruppe = jeSatz.get(z.rateBp);
|
|
127
|
+
if (gruppe)
|
|
128
|
+
gruppe.push(z);
|
|
129
|
+
else
|
|
130
|
+
jeSatz.set(z.rateBp, [z]);
|
|
131
|
+
}
|
|
132
|
+
const byRate = [];
|
|
133
|
+
for (const [rateBp, gruppe] of jeSatz) {
|
|
134
|
+
const r = BigInt(rateBp);
|
|
135
|
+
const S = gruppe.reduce((s, z) => s + z.L, 0n);
|
|
136
|
+
let netCents;
|
|
137
|
+
let vatCents;
|
|
138
|
+
let grossCents;
|
|
139
|
+
if (bruttoPreise) {
|
|
140
|
+
grossCents = rund(S, E);
|
|
141
|
+
netCents = rund(grossCents * 10000n, 10000n + r);
|
|
142
|
+
vatCents = grossCents - netCents;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
netCents = rund(S, E);
|
|
146
|
+
vatCents = rund(S * r, E * 10000n);
|
|
147
|
+
grossCents = netCents + vatCents;
|
|
148
|
+
}
|
|
149
|
+
if (bruttoPreise) {
|
|
150
|
+
verteile(gruppe, 'netCents', 10000n, E * (10000n + r), netCents);
|
|
151
|
+
verteile(gruppe, 'grossCents', 1n, E, grossCents);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
verteile(gruppe, 'netCents', 1n, E, netCents);
|
|
155
|
+
verteile(gruppe, 'grossCents', 10000n + r, E * 10000n, grossCents);
|
|
156
|
+
}
|
|
157
|
+
byRate.push({
|
|
158
|
+
rateBp,
|
|
159
|
+
netCents: Number(netCents),
|
|
160
|
+
vatCents: Number(vatCents),
|
|
161
|
+
grossCents: Number(grossCents),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
byRate.sort((a, b) => b.rateBp - a.rateBp);
|
|
165
|
+
const summe = (feld) => byRate.reduce((s, r) => s + r[feld], 0);
|
|
166
|
+
const ergebnis = {
|
|
167
|
+
netCents: summe('netCents'),
|
|
168
|
+
vatCents: summe('vatCents'),
|
|
169
|
+
grossCents: summe('grossCents'),
|
|
170
|
+
byRate,
|
|
171
|
+
lines: zeilen.map((z) => ({ netCents: z.netCents, grossCents: z.grossCents, rateBp: z.rateBp })),
|
|
172
|
+
};
|
|
173
|
+
for (const feld of ['netCents', 'vatCents', 'grossCents']) {
|
|
174
|
+
if (Math.abs(ergebnis[feld]) > BETRAG_GRENZE_CENTS) {
|
|
175
|
+
throw new RechenFehler('amount_too_large', `Die Rechnung uebersteigt ${BETRAG_GRENZE_CENTS} Cent (${feld})`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return ergebnis;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Wandelt eine Zahl ueber ihren kuerzesten Dezimaltext in eine Ganzzahl mit
|
|
182
|
+
* `stellen` Nachkommastellen. Kein Gleitkomma-Rechnen: `0.1 * 1000` ist
|
|
183
|
+
* 100.00000000000001, `String(0.1)` dagegen genau "0.1". Mehr Stellen als
|
|
184
|
+
* erlaubt ergeben `null` — auch Gleitkomma-Rauschen wie 0.30000000000000004,
|
|
185
|
+
* das mit 17 Stellen dasteht.
|
|
186
|
+
*/
|
|
187
|
+
function ganzAusDezimaltext(wert, stellen) {
|
|
188
|
+
if (typeof wert !== 'number' || !Number.isFinite(wert))
|
|
189
|
+
return null;
|
|
190
|
+
const treffer = /^(-?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(String(wert));
|
|
191
|
+
if (!treffer)
|
|
192
|
+
return null;
|
|
193
|
+
const [, vorzeichen = '', ganzTeil = '0', bruchTeil = '', hoch = '0'] = treffer;
|
|
194
|
+
const verschiebung = Number(hoch);
|
|
195
|
+
let ziffern = `${ganzTeil}${bruchTeil}`;
|
|
196
|
+
let nachkomma = bruchTeil.length - verschiebung;
|
|
197
|
+
if (nachkomma < 0) {
|
|
198
|
+
ziffern += '0'.repeat(-nachkomma);
|
|
199
|
+
nachkomma = 0;
|
|
200
|
+
}
|
|
201
|
+
if (nachkomma > stellen) {
|
|
202
|
+
// Nur echte Nullen am Ende duerfen wegfallen (1.20 bei 1 Stelle).
|
|
203
|
+
const zuviel = nachkomma - stellen;
|
|
204
|
+
if (!/^0+$/.test(ziffern.slice(-zuviel)))
|
|
205
|
+
return null;
|
|
206
|
+
ziffern = ziffern.slice(0, -zuviel);
|
|
207
|
+
nachkomma = stellen;
|
|
208
|
+
}
|
|
209
|
+
const skaliert = `${ziffern}${'0'.repeat(stellen - nachkomma)}`;
|
|
210
|
+
const zahl = Number(`${vorzeichen}${skaliert}`);
|
|
211
|
+
return Number.isSafeInteger(zahl) ? zahl : null;
|
|
212
|
+
}
|
|
213
|
+
const EURO_FELDER = ['unitPrice', 'quantity', 'vatRate', 'discountPct'];
|
|
214
|
+
/**
|
|
215
|
+
* Euro-Position → Ganzzahl-Form, verlustfrei oder mit Grund abgelehnt. Die
|
|
216
|
+
* EINE Stelle, die das tut: Panel (beim Lesen), SEPA-Lauf, Umrechnungsskript
|
|
217
|
+
* und das Festschreiben benutzen alle diese Funktion, damit niemand eine
|
|
218
|
+
* zweite Auslegung baut.
|
|
219
|
+
*
|
|
220
|
+
* Eine Position, die schon `unitPriceMicros` traegt, kommt unveraendert
|
|
221
|
+
* zurueck — die Funktion ist die Formweiche, nicht eine blinde Umrechnung.
|
|
222
|
+
*
|
|
223
|
+
* Fehlende Menge und fehlender Satz gelten als 0, weil der Server bisher so
|
|
224
|
+
* abgerechnet hat. Ein fehlender Preis dagegen ist NIE 0: sonst wuerde eine
|
|
225
|
+
* Position, der nur der Preis fehlt, still zur 0-€-Zeile.
|
|
226
|
+
*/
|
|
227
|
+
export function positionAusEuro(item) {
|
|
228
|
+
if (typeof item.unitPriceMicros === 'number') {
|
|
229
|
+
return { ok: true, position: item };
|
|
230
|
+
}
|
|
231
|
+
const roh = item.unitPrice;
|
|
232
|
+
if (typeof roh !== 'number' || !Number.isFinite(roh)) {
|
|
233
|
+
return { ok: false, feld: 'unitPrice', grund: 'kein_zahlwert' };
|
|
234
|
+
}
|
|
235
|
+
const micros = ganzAusDezimaltext(Math.abs(roh), 6);
|
|
236
|
+
if (micros === null)
|
|
237
|
+
return { ok: false, feld: 'unitPrice', grund: 'nachkommastellen' };
|
|
238
|
+
if (micros > GRENZEN.unitPriceMicros[1])
|
|
239
|
+
return { ok: false, feld: 'unitPrice', grund: 'ausserhalb' };
|
|
240
|
+
const zahl = (feld, stellen, max) => {
|
|
241
|
+
const w = item[feld];
|
|
242
|
+
if (w === undefined || w === null)
|
|
243
|
+
return { ok: true, wert: 0 };
|
|
244
|
+
if (typeof w !== 'number' || !Number.isFinite(w))
|
|
245
|
+
return { ok: false, grund: 'kein_zahlwert' };
|
|
246
|
+
const ganz = ganzAusDezimaltext(w, stellen);
|
|
247
|
+
if (ganz === null)
|
|
248
|
+
return { ok: false, grund: 'nachkommastellen' };
|
|
249
|
+
if (ganz < (feld === 'quantity' ? -max : 0) || ganz > max)
|
|
250
|
+
return { ok: false, grund: 'ausserhalb' };
|
|
251
|
+
return { ok: true, wert: ganz };
|
|
252
|
+
};
|
|
253
|
+
const menge = zahl('quantity', 3, GRENZEN.quantityMilli[1]);
|
|
254
|
+
if (!menge.ok)
|
|
255
|
+
return { ok: false, feld: 'quantity', grund: menge.grund };
|
|
256
|
+
const satz = zahl('vatRate', 2, 10_000);
|
|
257
|
+
if (!satz.ok)
|
|
258
|
+
return { ok: false, feld: 'vatRate', grund: satz.grund };
|
|
259
|
+
const rabatt = zahl('discountPct', 2, 10_000);
|
|
260
|
+
if (!rabatt.ok)
|
|
261
|
+
return { ok: false, feld: 'discountPct', grund: rabatt.grund };
|
|
262
|
+
const rest = { ...item };
|
|
263
|
+
for (const feld of EURO_FELDER)
|
|
264
|
+
delete rest[feld];
|
|
265
|
+
return {
|
|
266
|
+
ok: true,
|
|
267
|
+
position: {
|
|
268
|
+
...rest,
|
|
269
|
+
unitPriceMicros: micros,
|
|
270
|
+
quantityMilli: roh < 0 ? -menge.wert : menge.wert,
|
|
271
|
+
discountBp: rabatt.wert,
|
|
272
|
+
vatRateBp: satz.wert,
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Anteiliger Preis fuer einen Teilzeitraum (erste Rechnung eines Abos).
|
|
278
|
+
* War der Ursprungspreis centgenau, bleibt auch der Anteil centgenau — sonst
|
|
279
|
+
* stuende auf der ersten Rechnung ein Preis mit sechs Stellen, wo bisher zwei
|
|
280
|
+
* standen.
|
|
281
|
+
*/
|
|
282
|
+
export function anteiligerPreis(unitPriceMicros, monate, intervall) {
|
|
283
|
+
const micros = BigInt(unitPriceMicros);
|
|
284
|
+
const anteil = rund(micros * BigInt(monate), BigInt(intervall));
|
|
285
|
+
if (unitPriceMicros % 10_000 !== 0)
|
|
286
|
+
return Number(anteil);
|
|
287
|
+
return Number(rund(anteil, 10000n) * 10000n);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Schluessel eines USt-Satzes in den gespeicherten Maps (`creditedCents.byRate`,
|
|
291
|
+
* `invoice_stats.vatByRate`): der Prozenttext mit Punkt, genau wie
|
|
292
|
+
* `String(satz)` ihn bisher gebildet hat. NICHT fuer die Anzeige — dafuer gibt
|
|
293
|
+
* es `satzText`.
|
|
294
|
+
*/
|
|
295
|
+
export function satzSchluessel(rateBp) {
|
|
296
|
+
return String(rateBp / 100);
|
|
297
|
+
}
|
|
298
|
+
/** Ein USt-Satz fuer die Anzeige: oesterreichisch mit Komma, ohne nachlaufende Nullen. */
|
|
299
|
+
export function satzText(rateBp) {
|
|
300
|
+
return satzSchluessel(rateBp).replace('.', ',');
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Ein Einzelpreis fuer die Anzeige: mindestens zwei, hoechstens sechs
|
|
304
|
+
* Nachkommastellen, Tausenderpunkt, Komma als Trennzeichen — in jeder Sprache
|
|
305
|
+
* oesterreichisch, wie die Betraege auf dem Blatt.
|
|
306
|
+
*/
|
|
307
|
+
export function preisText(unitPriceMicros) {
|
|
308
|
+
const negativ = unitPriceMicros < 0;
|
|
309
|
+
const betrag = Math.abs(unitPriceMicros);
|
|
310
|
+
const ganz = Math.trunc(betrag / 1_000_000);
|
|
311
|
+
let bruch = String(betrag % 1_000_000).padStart(6, '0');
|
|
312
|
+
while (bruch.length > 2 && bruch.endsWith('0'))
|
|
313
|
+
bruch = bruch.slice(0, -1);
|
|
314
|
+
const mitPunkten = String(ganz).replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
|
315
|
+
return `${negativ ? '-' : ''}${mitPunkten},${bruch}`;
|
|
316
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Summen einer Rechnung vorab rechnen — genau so, wie der Server sie beim
|
|
3
|
+
* Ausstellen rechnet. Fuer Shops, die kassieren, bevor die Rechnung entsteht,
|
|
4
|
+
* und denselben Betrag brauchen, den die Rechnung spaeter ausweist.
|
|
5
|
+
*
|
|
6
|
+
* Die Regel (je USt-Satz, Betraege in Cent, kaufmaennisch gerundet):
|
|
7
|
+
*
|
|
8
|
+
* | Modus | Netto | USt | Brutto |
|
|
9
|
+
* |---------|--------------------------------|----------------------------|--------------------|
|
|
10
|
+
* | `net` | round(Σ Zeilen) | round(Σ Zeilen × Satz/100) | Netto + USt |
|
|
11
|
+
* | `gross` | round(B × 100 / (100 + Satz)) | B − Netto | B = round(Σ Zeilen)|
|
|
12
|
+
*
|
|
13
|
+
* Zeile = Einzelpreis × Menge × (1 − Rabatt/100), ungerundet. Im Brutto-Modus
|
|
14
|
+
* ist das Brutto der vereinbarte Preis und bleibt, wie die Zeilen es ergeben.
|
|
15
|
+
* Bei einem steuerfreien Steuerfall zaehlt jede Zeile zu 0 %.
|
|
16
|
+
*
|
|
17
|
+
* `fixtures/rechnung-summen.json` haelt die Prueffaelle; Backend und
|
|
18
|
+
* Dart-Zwilling pruefen gegen dieselbe Datei. Verbindlich bleibt, was der
|
|
19
|
+
* Server rechnet — mit `previewInvoice` laesst sich das vorab abfragen.
|
|
20
|
+
*/
|
|
21
|
+
import type { InvoiceTotals } from './typen.js';
|
|
22
|
+
import type { PriceMode, TaxScheme } from './vertrag.js';
|
|
23
|
+
import { STEUERFREIE_FAELLE } from './rechnen.js';
|
|
24
|
+
/** Aus dem Kern uebernommen, damit Kern und Huelle nie auseinanderlaufen. */
|
|
25
|
+
export { STEUERFREIE_FAELLE };
|
|
26
|
+
/** Was fuer die Summe zaehlt — `InvoiceItemInput` passt unveraendert. */
|
|
27
|
+
export interface SummenPosition {
|
|
28
|
+
quantity: number;
|
|
29
|
+
unitPriceCents: number;
|
|
30
|
+
vatRate: number;
|
|
31
|
+
discountPct?: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Netto, USt und Brutto einer Rechnung in Cent, je Satz absteigend — dieselbe
|
|
35
|
+
* Form wie `invoice.totals` in den Antworten.
|
|
36
|
+
*
|
|
37
|
+
* `taxScheme` ist der Steuerfall der Rechnung; ohne Angabe `normal`. Leitet der
|
|
38
|
+
* Server einen steuerfreien Fall ab (etwa eine ig. Lieferung), gehoert dieser
|
|
39
|
+
* Fall hierher — sonst rechnet die Funktion Steuer, die die Rechnung nicht
|
|
40
|
+
* ausweist.
|
|
41
|
+
*/
|
|
42
|
+
export declare function rechnungSummen(items: readonly SummenPosition[], priceMode: PriceMode, taxScheme?: TaxScheme): InvoiceTotals;
|