feat: Steuersatz je Kategorie und je Produkt, Rechnung weist mehrere Saetze aus
Ein einziger Satz fuer den ganzen Shop war fachlich falsch: der Steuersatz haengt in Deutschland an der Art der Ware, nicht am Betrieb. Honig ist ein Lebensmittel und ermaessigt besteuert (Anlage 2 zum UStG), eine Bienenwachskerze nicht. Der Satz wird jetzt in drei Stufen bestimmt, die erste gewinnt: 1. Produkt — neues Feld "Umsatzsteuersatz" unter Admin -> Produkte, fuer Ausnahmen, die sich nicht aus der Kategorie ergeben. 2. Kategorie — VAT_RATE_PERCENT_HONIG / _KERZEN / _PROPOLIS. 3. VAT_RATE_PERCENT als gemeinsamer Rueckfallwert. Die Kategorie-Stufe ist der Normalfall: aendern sich die gesetzlichen Saetze, genuegt eine Aenderung an der Konfiguration statt an jedem Produkt. Leer am Produkt heisst ausdruecklich "Satz der Kategorie" und NICHT 0 — 0 bedeutet steuerfrei nach § 19 UStG. Das Formularfeld hat dafuer einen eigenen Zweig im Validierungsschema, sonst haette z.coerce.number() aus dem leeren Feld eine 0 gemacht. Damit kann eine Rechnung mehrere Saetze tragen, sobald Honig und Kerze im selben Paket liegen. Sie weist deshalb je Satz eine eigene Netto- und Steuerzeile aus. Versand und Rabatt verteilen sich im Verhaeltnis der Warenwerte: der Versand ist umsatzsteuerlich eine Nebenleistung und teilt das Schicksal der Hauptleistung — gibt es mehrere, teilt er sich anteilig auf. Die Verteilung nutzt Hare-Niemeyer, damit dabei keine Cent verschwinden oder entstehen; zugesichert ist, dass die Summe aller Zeilen exakt den Rechnungsbetrag ergibt. Der Satz wandert von der Rechnung an die BESTELLPOSITION (OrderItem.vatRatePercent) und gehoert damit zum Snapshot wie der Preis: wird ein Produkt spaeter umkategorisiert, bleiben bestehende Rechnungen unveraendert. Invoice.vatRatePercent entfaellt wieder — ein einzelner Satz je Rechnung ist bei gemischten Saetzen nicht darstellbar. Die Spalte entstand in der vorigen Migration und wurde nie in Betrieb beschrieben. VAT_RATE_PERCENT_PROPOLIS steht auf 19, weil der volle Satz im Zweifel der sicherere ist — als Lebensmittel waeren 7 richtig, als Nahrungsergaenzungs- oder Kosmetikprodukt 19. Im README als zu pruefen vermerkt. Der Manifest-Test hat die neuen Variablen zunaechst als "liest niemand" gemeldet: lib/vat.ts bekommt die Umgebung als Parameter, damit sich die Saetze ohne globalen Zustand testen lassen, und greift auf die Kategorie-Saetze ueber einen berechneten Schluessel zu — beides kann eine Regex nicht sehen. Dafuer gibt es jetzt eine begruendete Ausnahmeliste. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
1 parent
7c59283ede
commit
625b73a8c8
20 files changed
+518
-70
No files matched your search
@@ -55,6 +55,22 @@ function envNamesUsedInCode(dir: string): Set<string> {
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schluessel, die die App zwar liest, aber nicht ueber ein woertliches
|
||||
* `process.env.NAME` — deshalb findet sie die Suche unten nicht.
|
||||
*
|
||||
* lib/vat.ts bekommt die Umgebung als Parameter uebergeben, damit sich die
|
||||
* Steuersaetze ohne globalen Zustand testen lassen, und greift auf die
|
||||
* Kategorie-Saetze ueber einen berechneten Schluessel zu. Beides ist Absicht;
|
||||
* eine Regex kann es nicht sehen.
|
||||
*/
|
||||
const READ_INDIRECTLY: Record<string, string> = {
|
||||
VAT_RATE_PERCENT: "lib/vat.ts — als Parameter uebergebene Umgebung (vatRateForCategory).",
|
||||
VAT_RATE_PERCENT_HONIG: "lib/vat.ts — berechneter Schluessel in CATEGORY_ENV_NAMES.",
|
||||
VAT_RATE_PERCENT_KERZEN: "lib/vat.ts — berechneter Schluessel in CATEGORY_ENV_NAMES.",
|
||||
VAT_RATE_PERCENT_PROPOLIS: "lib/vat.ts — berechneter Schluessel in CATEGORY_ENV_NAMES.",
|
||||
};
|
||||
|
||||
/**
|
||||
* Schluessel, die in den Manifesten stehen duerfen, ohne dass die App sie
|
||||
* liest. Wer hier etwas ergaenzt, muss benennen koennen, WER den Wert sonst
|
||||
@@ -98,7 +114,7 @@ describe("Kubernetes-Manifeste und Code", () => {
|
||||
|
||||
// Act
|
||||
const unused = [...manifestKeys(CONFIGMAP), ...manifestKeys(SECRET)]
|
||||
.filter((name) => !used.has(name) && !(name in USED_OUTSIDE_THE_APP))
|
||||
.filter((name) => !used.has(name) && !(name in USED_OUTSIDE_THE_APP) && !(name in READ_INDIRECTLY))
|
||||
.sort();
|
||||
|
||||
// Assert: toter Konfigurationsballast ist irrefuehrend — er sieht aus,
|
||||
|
||||
Reference in new issue
Block a user