@kreiseck/kasseneck-api 0.21.0 → 0.22.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +37 -2
  3. package/dist/cjs/client/errors.d.ts +0 -1
  4. package/dist/cjs/client/errors.js +24 -1
  5. package/dist/cjs/rechnung/api.d.ts +3 -1
  6. package/dist/cjs/rechnung/api.js +1 -0
  7. package/dist/cjs/rechnung/endpunkte.d.ts +12 -1
  8. package/dist/cjs/rechnung/endpunkte.js +56 -3
  9. package/dist/cjs/rechnung/index.d.ts +3 -2
  10. package/dist/cjs/rechnung/index.js +5 -1
  11. package/dist/cjs/rechnung/summen.d.ts +41 -0
  12. package/dist/cjs/rechnung/summen.js +81 -0
  13. package/dist/cjs/rechnung/texte.d.ts +4 -0
  14. package/dist/cjs/rechnung/texte.js +8 -0
  15. package/dist/cjs/rechnung/typen.d.ts +51 -7
  16. package/dist/cjs/rechnung/vertrag.js +6 -0
  17. package/dist/esm/client/errors.d.ts +0 -1
  18. package/dist/esm/client/errors.js +24 -1
  19. package/dist/esm/rechnung/api.d.ts +3 -1
  20. package/dist/esm/rechnung/api.js +2 -1
  21. package/dist/esm/rechnung/endpunkte.d.ts +12 -1
  22. package/dist/esm/rechnung/endpunkte.js +55 -3
  23. package/dist/esm/rechnung/index.d.ts +3 -2
  24. package/dist/esm/rechnung/index.js +2 -1
  25. package/dist/esm/rechnung/summen.d.ts +41 -0
  26. package/dist/esm/rechnung/summen.js +77 -0
  27. package/dist/esm/rechnung/texte.d.ts +4 -0
  28. package/dist/esm/rechnung/texte.js +8 -0
  29. package/dist/esm/rechnung/typen.d.ts +51 -7
  30. package/dist/esm/rechnung/vertrag.js +6 -0
  31. package/fixtures/hobex-hps-codes.json +1 -1
  32. package/fixtures/kasse-texte.json +1 -1
  33. package/fixtures/oberflaeche.json +9 -1
  34. package/fixtures/rechnung-api.schema.json +4 -1
  35. package/fixtures/rechnung-summen.json +154 -0
  36. package/fixtures/rechnung-texte.json +9 -1
  37. package/package.json +1 -1
@@ -164,6 +164,29 @@ function sieben(wert, geheimnisse, tiefe) {
164
164
  return raus;
165
165
  }
166
166
  /** Fachlicher Fehler: HTTP 200, aber `status: 'error'` im Rumpf. */
167
+ /**
168
+ * Die Feldfehler einer Pruefantwort (`errors: [{ field, message }]`) als
169
+ * Anhang der Fehlermeldung — sonst stuende im Log nur „Bitte Eingaben
170
+ * pruefen." und niemand wuesste, welches Feld gemeint ist. Hoechstens fuenf;
171
+ * vollstaendig bleiben sie in `details.errors`.
172
+ */
173
+ function feldHinweis(details) {
174
+ const roh = details['errors'];
175
+ if (!Array.isArray(roh))
176
+ return '';
177
+ const teile = [];
178
+ for (const e of roh) {
179
+ if (e === null || typeof e !== 'object')
180
+ continue;
181
+ const { field, message } = e;
182
+ if (typeof field === 'string' && typeof message === 'string')
183
+ teile.push(`${field}: ${message}`);
184
+ }
185
+ if (!teile.length)
186
+ return '';
187
+ const mehr = teile.length > 5 ? ` (+${teile.length - 5} weitere)` : '';
188
+ return ` [${teile.slice(0, 5).join('; ')}${mehr}]`;
189
+ }
167
190
  export class KasseneckApiError extends Error {
168
191
  name = 'KasseneckApiError';
169
192
  /** Aufgerufene Backend-Funktion, z. B. `createReceipt`. */
@@ -187,7 +210,7 @@ export class KasseneckApiError extends Error {
187
210
  */
188
211
  details;
189
212
  constructor(functionName, serverMessage, details = {}, code) {
190
- super(`${functionName} fehlgeschlagen: ${serverMessage}`);
213
+ super(`${functionName} fehlgeschlagen: ${serverMessage}${feldHinweis(details)}`);
191
214
  this.functionName = functionName;
192
215
  this.serverMessage = serverMessage;
193
216
  this.details = details;
@@ -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: {
@@ -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
- return {
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
- if (roh['notice'])
147
- ergebnis.notice = roh['notice'];
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,8 @@
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';
19
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, 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,7 @@
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';
18
19
  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,41 @@
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
+ /** Was fuer die Summe zaehlt — `InvoiceItemInput` passt unveraendert. */
24
+ export interface SummenPosition {
25
+ quantity: number;
26
+ unitPriceCents: number;
27
+ vatRate: number;
28
+ discountPct?: number;
29
+ }
30
+ /** Steuerfaelle, in denen die Rechnung keine Steuer ausweist: jede Zeile zaehlt zu 0 %. */
31
+ export declare const STEUERFREIE_FAELLE: readonly TaxScheme[];
32
+ /**
33
+ * Netto, USt und Brutto einer Rechnung in Cent, je Satz absteigend — dieselbe
34
+ * Form wie `invoice.totals` in den Antworten.
35
+ *
36
+ * `taxScheme` ist der Steuerfall der Rechnung; ohne Angabe `normal`. Leitet der
37
+ * Server einen steuerfreien Fall ab (etwa eine ig. Lieferung), gehoert dieser
38
+ * Fall hierher — sonst rechnet die Funktion Steuer, die die Rechnung nicht
39
+ * ausweist.
40
+ */
41
+ export declare function rechnungSummen(items: readonly SummenPosition[], priceMode: PriceMode, taxScheme?: TaxScheme): InvoiceTotals;
@@ -0,0 +1,77 @@
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
+ /** Steuerfaelle, in denen die Rechnung keine Steuer ausweist: jede Zeile zaehlt zu 0 %. */
22
+ export const STEUERFREIE_FAELLE = Object.freeze([
23
+ 'smallBusiness',
24
+ 'reverseCharge',
25
+ 'igLieferung',
26
+ 'exportThirdCountry',
27
+ 'domesticReverseCharge',
28
+ 'outsideScope',
29
+ ]);
30
+ /** Auf Cent gerundete Euro — dieselbe Rundung wie am Server (halber Cent vom Nullpunkt weg). */
31
+ function euroRund(n) {
32
+ return ((n < 0 ? -1 : 1) * Math.round((Math.abs(n) + Number.EPSILON) * 100)) / 100;
33
+ }
34
+ const euroZuCent = (euro) => Math.round(euroRund(euro) * 100) || 0;
35
+ /** Ganze Cent kaufmaennisch; ein Gleitkomma-Rest wie x,4999999… zaehlt als halber Cent. */
36
+ function centRund(x) {
37
+ const betrag = Math.round(Math.abs(x) * 1e6) / 1e6;
38
+ return (x < 0 ? -1 : 1) * Math.round(betrag) || 0;
39
+ }
40
+ /**
41
+ * Netto, USt und Brutto einer Rechnung in Cent, je Satz absteigend — dieselbe
42
+ * Form wie `invoice.totals` in den Antworten.
43
+ *
44
+ * `taxScheme` ist der Steuerfall der Rechnung; ohne Angabe `normal`. Leitet der
45
+ * Server einen steuerfreien Fall ab (etwa eine ig. Lieferung), gehoert dieser
46
+ * Fall hierher — sonst rechnet die Funktion Steuer, die die Rechnung nicht
47
+ * ausweist.
48
+ */
49
+ export function rechnungSummen(items, priceMode, taxScheme = 'normal') {
50
+ const steuerfrei = STEUERFREIE_FAELLE.includes(taxScheme);
51
+ const bruttoPreise = priceMode === 'gross' && !steuerfrei;
52
+ // Ungerundete Zeilen je Satz, in Euro wie am Server.
53
+ const jeSatz = new Map();
54
+ for (const p of items) {
55
+ const satz = steuerfrei ? 0 : p.vatRate;
56
+ const zeile = (p.unitPriceCents / 100) * p.quantity * (1 - (p.discountPct ?? 0) / 100);
57
+ jeSatz.set(satz, (jeSatz.get(satz) ?? 0) + zeile);
58
+ }
59
+ const byRate = [];
60
+ for (const [rate, roh] of jeSatz) {
61
+ let netCents;
62
+ let vatCents;
63
+ if (bruttoPreise) {
64
+ const grossCents = euroZuCent(roh);
65
+ netCents = centRund((grossCents * 100) / (100 + rate));
66
+ vatCents = grossCents - netCents;
67
+ }
68
+ else {
69
+ netCents = euroZuCent(roh);
70
+ vatCents = euroZuCent((roh * rate) / 100);
71
+ }
72
+ byRate.push({ rate, netCents, vatCents, grossCents: netCents + vatCents });
73
+ }
74
+ byRate.sort((a, b) => b.rate - a.rate);
75
+ const summe = (feld) => byRate.reduce((s, r) => s + r[feld], 0);
76
+ return { netCents: summe('netCents'), vatCents: summe('vatCents'), grossCents: summe('grossCents'), byRate };
77
+ }
@@ -35,8 +35,12 @@ declare const DE: {
35
35
  readonly 'pdf.tabelle.ust': "UST";
36
36
  readonly 'pdf.tabelle.einzelNetto': "EINZEL NETTO";
37
37
  readonly 'pdf.tabelle.betragNetto': "BETRAG NETTO";
38
+ readonly 'pdf.tabelle.einzelBrutto': "EINZEL BRUTTO";
39
+ readonly 'pdf.tabelle.betragBrutto': "BETRAG BRUTTO";
40
+ readonly 'pdf.position.rabatt': "abzüglich {prozent} % Rabatt";
38
41
  readonly 'pdf.summe.zwischensumme': "Zwischensumme netto";
39
42
  readonly 'pdf.summe.umsatzsteuer': "Umsatzsteuer {satz}%";
43
+ readonly 'pdf.summe.darinUst': "darin {satz}% USt von {netto} netto";
40
44
  readonly 'pdf.summe.gesamt.rechnung': "Gesamtbetrag";
41
45
  readonly 'pdf.summe.gesamt.gutschrift': "Gutschriftsbetrag";
42
46
  readonly 'steuer.kleinunternehmer': "Gemäß § 6 Abs. 1 Z 27 UStG wird keine Umsatzsteuer verrechnet (Kleinunternehmer).";
@@ -20,8 +20,12 @@ const DE = {
20
20
  'pdf.tabelle.ust': 'UST',
21
21
  'pdf.tabelle.einzelNetto': 'EINZEL NETTO',
22
22
  'pdf.tabelle.betragNetto': 'BETRAG NETTO',
23
+ 'pdf.tabelle.einzelBrutto': 'EINZEL BRUTTO',
24
+ 'pdf.tabelle.betragBrutto': 'BETRAG BRUTTO',
25
+ 'pdf.position.rabatt': 'abzüglich {prozent} % Rabatt',
23
26
  'pdf.summe.zwischensumme': 'Zwischensumme netto',
24
27
  'pdf.summe.umsatzsteuer': 'Umsatzsteuer {satz}%',
28
+ 'pdf.summe.darinUst': 'darin {satz}% USt von {netto} netto',
25
29
  'pdf.summe.gesamt.rechnung': 'Gesamtbetrag',
26
30
  'pdf.summe.gesamt.gutschrift': 'Gutschriftsbetrag',
27
31
  'steuer.kleinunternehmer': 'Gemäß § 6 Abs. 1 Z 27 UStG wird keine Umsatzsteuer verrechnet (Kleinunternehmer).',
@@ -226,8 +230,12 @@ const EN = {
226
230
  'pdf.tabelle.ust': 'VAT',
227
231
  'pdf.tabelle.einzelNetto': 'UNIT NET',
228
232
  'pdf.tabelle.betragNetto': 'AMOUNT NET',
233
+ 'pdf.tabelle.einzelBrutto': 'UNIT GROSS',
234
+ 'pdf.tabelle.betragBrutto': 'AMOUNT GROSS',
235
+ 'pdf.position.rabatt': 'less {prozent}% discount',
229
236
  'pdf.summe.zwischensumme': 'Subtotal net',
230
237
  'pdf.summe.umsatzsteuer': 'VAT {satz}%',
238
+ 'pdf.summe.darinUst': 'incl. {satz}% VAT on {netto} net',
231
239
  'pdf.summe.gesamt.rechnung': 'Total',
232
240
  'pdf.summe.gesamt.gutschrift': 'Credit total',
233
241
  'steuer.kleinunternehmer': 'No VAT charged pursuant to § 6 (1) no. 27 UStG (small business exemption).',
@@ -146,7 +146,7 @@ export interface InvoicePayment {
146
146
  }
147
147
  /**
148
148
  * Ein Hinweis zu einer erfolgreichen Antwort — kein Fehler, sondern etwas,
149
- * das der Aufrufer wissen sollte (heute nur `cash_receipt_required`).
149
+ * das der Aufrufer wissen sollte. Die Codes stehen in `INVOICE_NOTICE_CODES`.
150
150
  */
151
151
  export interface InvoiceNotice {
152
152
  code: InvoiceNoticeCode;
@@ -156,7 +156,11 @@ export interface RecordPaymentResult {
156
156
  invoice: Invoice;
157
157
  payment: InvoicePayment;
158
158
  replayed: boolean;
159
- notice?: InvoiceNotice;
159
+ /**
160
+ * Hinweise zu dieser Zahlung — eine **Liste** wie bei `issueInvoice`
161
+ * (bis 0.21.0 ein einzelnes Objekt). Fehlt, wenn es keinen gibt.
162
+ */
163
+ notice?: InvoiceNotice[];
160
164
  }
161
165
  export interface CancelInvoiceRequest {
162
166
  idempotencyKey: string;
@@ -167,15 +171,23 @@ export interface CancelInvoiceRequest {
167
171
  export interface CreditNoteRequest extends CancelInvoiceRequest {
168
172
  items: InvoiceItemInput[];
169
173
  }
174
+ /**
175
+ * Summen einer Rechnung in Cent, **immer positiv** — auch bei einer Gutschrift
176
+ * (`docType: 'GU'`); das Vorzeichen steht im Belegtyp, nicht im Betrag.
177
+ * Gerechnet wird je Satz wie in [rechnungSummen].
178
+ */
170
179
  export interface InvoiceTotals {
171
180
  netCents: number;
172
181
  vatCents: number;
173
182
  grossCents: number;
174
- byRate: {
175
- rate: number;
176
- netCents: number;
177
- vatCents: number;
178
- }[];
183
+ /** Je USt-Satz, absteigend. `grossCents` ist `netCents + vatCents` (seit 0.22.0 mitgeliefert). */
184
+ byRate: InvoiceRateTotals[];
185
+ }
186
+ export interface InvoiceRateTotals {
187
+ rate: number;
188
+ netCents: number;
189
+ vatCents: number;
190
+ grossCents: number;
179
191
  }
180
192
  export interface EInvoiceStatus {
181
193
  level: string;
@@ -299,6 +311,38 @@ export interface IssueResult {
299
311
  */
300
312
  notice?: InvoiceNotice[];
301
313
  }
314
+ /**
315
+ * Was `issueInvoice` mit dieser Anfrage ausstellen wuerde — ohne Nummer, ohne
316
+ * Dokument, ohne Zahlung. Der Probelauf prueft wie das Ausstellen (Einrichtung,
317
+ * Kunde, Steuerfall, Pflichtangaben, Marke, Zahlung) und scheitert mit
318
+ * denselben Fehlercodes.
319
+ */
320
+ export interface InvoicePreview {
321
+ docType: 'RE';
322
+ /** Heutiger Wiener Tag — der Tag, den eine sofort ausgestellte Rechnung truege. */
323
+ invoiceDate: string;
324
+ dueDate: string | null;
325
+ customerId: string | null;
326
+ /** Der abgeleitete Steuerfall. */
327
+ taxScheme: TaxScheme;
328
+ /** Warum dieser Fall gilt (z. B. `customer_country_eu_with_vat_id`). */
329
+ taxSchemeReason: string;
330
+ reverseChargeReason: ReverseChargeReason | null;
331
+ taxCountry: string;
332
+ priceMode: PriceMode;
333
+ totals: InvoiceTotals;
334
+ language: InvoiceLanguage;
335
+ brand: {
336
+ id: string | null;
337
+ name: string | null;
338
+ } | null;
339
+ einvoice: EInvoiceStatus;
340
+ }
341
+ export interface PreviewResult {
342
+ preview: InvoicePreview;
343
+ /** Dieselben Hinweise, die das Ausstellen liefern wuerde. */
344
+ notice?: InvoiceNotice[];
345
+ }
302
346
  export interface CancelResult {
303
347
  creditNote: Invoice;
304
348
  original: {
@@ -417,6 +417,12 @@ export const RECHNUNG_ANFRAGEN = Object.freeze({
417
417
  * und ein sofort geholtes PDF Zahlungsinformationen traegt.
418
418
  */
419
419
  payment: { typ: 'object', pflicht: false, felder: PAYMENT_FELDER },
420
+ /**
421
+ * Probelauf: alles pruefen und rechnen wie beim Ausstellen, aber nichts
422
+ * festschreiben — keine Nummer, kein Dokument, keine Zahlung, kein
423
+ * Idempotenz-Eintrag. Die Antwort traegt `preview` statt `invoice`.
424
+ */
425
+ dryRun: { typ: 'boolean', pflicht: false },
420
426
  },
421
427
  cancelInvoice: {
422
428
  idempotencyKey: idempotencyKey(true),
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.21.0",
2
+ "version": "0.22.0",
3
3
  "gemessenAn": {
4
4
  "tid": "3600335",
5
5
  "hpsVersion": "1.10.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.21.0",
2
+ "version": "0.22.0",
3
3
  "meldungen": {
4
4
  "netz.keine_verbindung": {
5
5
  "text": "Keine Verbindung zum Server. Bitte die Internetverbindung prüfen und erneut versuchen."
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.21.0",
2
+ "version": "0.22.0",
3
3
  "aufrufe": [
4
4
  "activateCashregister",
5
5
  "cancelInvoice",
@@ -519,6 +519,14 @@
519
519
  "listBrands",
520
520
  "recordInvoicePayment"
521
521
  ],
522
+ "steuerfreieFaelle": [
523
+ "smallBusiness",
524
+ "reverseCharge",
525
+ "igLieferung",
526
+ "exportThirdCountry",
527
+ "domesticReverseCharge",
528
+ "outsideScope"
529
+ ],
522
530
  "taxSchemes": [
523
531
  "normal",
524
532
  "smallBusiness",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "title": "Kasseneck Rechnungs-API",
4
4
  "version": 1,
5
- "paket": "0.21.0",
5
+ "paket": "0.22.0",
6
6
  "aufrufe": {
7
7
  "createCustomer": {
8
8
  "anfrage": {
@@ -523,6 +523,9 @@
523
523
  "method"
524
524
  ],
525
525
  "additionalProperties": false
526
+ },
527
+ "dryRun": {
528
+ "type": "boolean"
526
529
  }
527
530
  },
528
531
  "required": [