import type { Invoice } from "@e-invoice-eu/core"; import type { InvoiceOrder } from "../invoicePdf"; import type { CorrectionInvoiceKind, CorrectionInvoiceOrder } from "../correctionInvoicePdf"; import type { InvoiceSeller } from "../seller"; import { countryCode } from "./countryCode"; // @e-invoice-eu/core only re-exports the top-level `Invoice` type, not the // individual leaf field types (PaymentMeansTypeCode, SellerCountryCode, // ...) — derived here via indexed access instead of reaching into the // package's internal dist paths, which would be fragile against any // future restructuring on their end. type UblInvoice = Invoice["ubl:Invoice"]; type PaymentMeansTypeCode = NonNullable[number]["cbc:PaymentMeansCode"]; type SellerCountryCode = UblInvoice["cac:AccountingSupplierParty"]["cac:Party"]["cac:PostalAddress"]["cac:Country"]["cbc:IdentificationCode"]; type BuyerCountryCode = UblInvoice["cac:AccountingCustomerParty"]["cac:Party"]["cac:PostalAddress"]["cac:Country"]["cbc:IdentificationCode"]; // EN16931 VAT category — 'S' ("Standard rated") is the correct code for // *any* positive VAT rate under EN16931/Peppol BIS convention, not just // the statutory default rate; the actual percentage (19%, 7%, ...) goes // in the category's own `cbc:Percent`, not the code itself. This shop // only ever sells goods at a positive German VAT rate — no exports, // reverse-charge, or exempt sales exist yet — so every line and every // summed group uses this same code. const VAT_CATEGORY = "S"; // Which of this shop's three mutually-exclusive VAT treatments an // invoice/correction-invoice was issued under — 'standard' (normal // Regelbesteuerung), 'intra-community' (validated cross-border B2B // exemption, §4 Nr. 1b UStG), or 'kleinunternehmer' (§19 UStG small // business — the seller never charges VAT at all, on any sale). Derived // once per document from the order's own snapshotted // vatExempt/kleinunternehmer flags (see buildEInvoiceData/ // buildCorrectionEInvoiceData below) and threaded through every function // in this file that used to take a plain `vatExempt: boolean` — a // Kleinunternehmer sale is never also an intra-community exemption // (there's no VAT to exempt in the first place), so this is a 3-way // choice, not two independent booleans. type VatMode = "standard" | "intra-community" | "kleinunternehmer"; // UNTDID 5305 codes: 'K' — "VAT exempt for EEA intra-community supply of // goods and services" (§4 Nr. 1b UStG / Art. 138 VAT Directive). 'E' — // "Exempt from tax", used here for §19 UStG Kleinunternehmer — Germany's // small-business exemption is a national provision, not one of the // EU-wide reasons that has its own dedicated UNTDID/VATEX category, so // 'E' (the general exemption code) plus a free-text reason (BT-120, see // vatTaxSubtotalCategory below) is the correct EN16931 shape rather than // forcing it into 'K'. Distinct from 'S' (positive standard rate) and 'Z' // (zero-rated but still taxable) — neither applies to either exemption. function vatTaxCategory(rate: number, vatMode: VatMode): Record { const id = vatMode === "kleinunternehmer" ? "E" : vatMode === "intra-community" ? "K" : VAT_CATEGORY; return { "cbc:ID": id, "cbc:Percent": String(rate), "cac:TaxScheme": { "cbc:ID": "VAT" }, }; } // BT-120/BT-121 (VAT exemption reason text/code) are document-level fields // that only exist on cac:TaxTotal's own cac:TaxSubtotal.cac:TaxCategory — // confirmed empirically: @e-invoice-eu/core's ajv schema rejects // TaxExemptionReasonCode/TaxExemptionReason as "additional properties" the // moment they're placed on an cac:InvoiceLine's ClassifiedTaxCategory or a // cac:AllowanceCharge's TaxCategory (both use their own, more restrictive // generated type despite conceptually being "the same" UBL TaxCategory // complex type) — caught locally before ever reaching Mustang/CI, exactly // the kind of runtime-only-visible constraint this pipeline has hit before // (see amtCents()'s own comment). BR-K-10/BR-E-10 require either field // whenever category is 'K'/'E', satisfied here since this is the one place // they're actually allowed to live. §19 UStG has no EU-wide VATEX code (a // purely national exemption reason) — BR-E-10 accepts the free-text // TaxExemptionReason alone without a TaxExemptionReasonCode, so // kleinunternehmer only sets the text, unlike intra-community's VATEX-EU-IC. function vatTaxSubtotalCategory(rate: number, vatMode: VatMode): Record { return { ...vatTaxCategory(rate, vatMode), ...(vatMode === "intra-community" ? { "cbc:TaxExemptionReasonCode": "VATEX-EU-IC", "cbc:TaxExemptionReason": "Innergemeinschaftliche Lieferung" } : {}), ...(vatMode === "kleinunternehmer" ? { "cbc:TaxExemptionReason": "Gemäß § 19 UStG wird keine Umsatzsteuer berechnet." } : {}), }; } // UN/ECE Recommendation 20 unit-of-measure code for "one" (a countable // piece/unit) — this shop sells discrete products (ToDo-Karten sets, // notebooks, ...), never anything sold by weight/length/volume, so every // line uses the same code. const UNIT_CODE = "C62"; // UNCL4461 payment-means codes — only the ones this shop's actual // `payment-methods` collection can produce (see the Payload README's // "Per-product tax rates"-adjacent Commerce section). '1' ("Instrument // not defined") is the fallback for a payment method title this mapping // doesn't recognize, not an error — new payment methods get added in // Payload without a matching code deploy here otherwise breaking e-invoice // generation entirely. const PAYMENT_MEANS_CODE: Record = { Überweisung: "30", // SEPA credit transfer Kreditkarte: "48", // Bank card PayPal: "68", // Online payment service }; function paymentMeansCode(paymentMethodTitle: string): PaymentMeansTypeCode { return PAYMENT_MEANS_CODE[paymentMethodTitle] ?? "1"; } // Same definition as invoicePdf.tsx's own isPaidImmediately() — "Überweisung" // (bank transfer) is the only payment method on this shop that isn't // settled at checkout; everything else (Kreditkarte, PayPal, ...) captures // immediately, matching the visual PDF's "✓ Bereits beglichen" badge. function isPaidImmediately(paymentMethodTitle: string): boolean { return paymentMethodTitle !== "Überweisung"; } // EN16931 amount fields are XML-serialized strings, not numbers // (confirmed against @e-invoice-eu/core's own generated types — every // `*Amount`/`*Quantity`/`*Rate` type alias resolves to `string`). Every // `*Amount` field also turned out (at runtime, via the library's ajv // schema — not visible in the .d.ts types at all) to *require* a sibling // `*@currencyID` key the moment the amount itself is present. `amtCents()` // returns both keys at once via object spread, so call sites can't add one // without the other. // // Takes integer *cents*, not a float euro amount — every amount field in // this document is built from a shared integer-cents pipeline (see // `computeLines()`/`computeRateGroups()` below) specifically so that every // EN16931 "declared total = Σ its own parts" business rule (BR-CO-10, // BR-CO-12, BR-CO-13, BR-CO-14, BR-S-08) holds *exactly*, not just // approximately. Rounding a float sum once at the very end (the first cut // at this file) drifts from the sum of independently-rounded parts by a // cent as soon as an order combines a discount/shipping with more than one // VAT rate — caught by Mustang's Phase 4 CI check on the very first // fixture that did. function amtCents(key: string, cents: number): Record { return { [key]: (cents / 100).toFixed(2), [`${key}@currencyID`]: "EUR" }; } // Same "runtime-required sibling key" story as `amtCents()`, but for // `cbc:InvoicedQuantity@unitCode`. function qty(value: number): Record { return { "cbc:InvoicedQuantity": String(value), "cbc:InvoicedQuantity@unitCode": UNIT_CODE }; } function isoDate(iso: string): string { return iso.slice(0, 10); } // CII's SupplyChainTradeTransaction is a fixed Agreement/Delivery/ // Settlement element sequence — @e-invoice-eu/core's UBL→CII conversion // drops the whole `ram:ApplicableHeaderTradeDelivery` container unless at // least one of `cac:Delivery`'s own children actually resolves to a value // (an empty `{}` isn't enough — the converter recurses into children and // only vivifies the destination element if one of them produced output), // which fails schema validation (Mustang: "Invalid content ... // ApplicableHeaderTradeSettlement. One of ApplicableHeaderTradeDelivery is // expected", caught by the Phase 4 CI check on the very first real run — // an empty object there didn't fix it, this date is the actual fix). // // This shop doesn't track a separate delivery date distinct from the // invoice itself (no shipped-at timestamp exists at original-invoice time // — that's generated at checkout, before shipping even happens), so the // invoice's own issue date is used as the closest available proxy — BT-72 // is optional under EN16931, and Mustang only checks structural/schema // conformance here, not the business accuracy of the date's value. function delivery(issuedAtIso: string): UblInvoice["cac:Delivery"] { return { "cbc:ActualDeliveryDate": isoDate(issuedAtIso) }; } function paymentMeans(seller: InvoiceSeller, paymentMethodTitle: string): UblInvoice["cac:PaymentMeans"] { if (!seller.iban) return undefined; return [ { "cbc:PaymentMeansCode": paymentMeansCode(paymentMethodTitle), "cac:PayeeFinancialAccount": { "cbc:ID": seller.iban, ...(seller.bankName ? { "cbc:Name": seller.bankName } : {}), }, }, ]; } function sellerParty(seller: InvoiceSeller): UblInvoice["cac:AccountingSupplierParty"] { return { "cac:Party": { "cac:PostalAddress": { "cbc:StreetName": seller.sellerStreet, "cbc:CityName": seller.sellerCity, "cbc:PostalZone": seller.sellerZip, "cac:Country": { "cbc:IdentificationCode": countryCode(seller.sellerCountry) as SellerCountryCode }, }, "cac:PartyTaxScheme": [ { "cbc:CompanyID": seller.vatId, "cac:TaxScheme": { "cbc:ID": "VAT" }, }, ], "cac:PartyLegalEntity": { "cbc:RegistrationName": seller.sellerName }, "cac:Contact": { "cbc:ElectronicMail": seller.sellerEmail }, }, }; } function buyerParty(name: string, street: string | null | undefined, zip: string, city: string, country: string): UblInvoice["cac:AccountingCustomerParty"] { return { "cac:Party": { "cac:PostalAddress": { ...(street ? { "cbc:StreetName": street } : {}), "cbc:CityName": city, "cbc:PostalZone": zip, "cac:Country": { "cbc:IdentificationCode": countryCode(country) as BuyerCountryCode }, }, "cac:PartyLegalEntity": { "cbc:RegistrationName": name }, }, }; } type LineInput = { productName: string; variantName?: string | null; quantity: number; unitPrice: number; taxRatePercent: number }; type ComputedLine = { item: LineInput; netCents: number }; type RateGroup = { rate: number; lineNetCents: number; allowanceCents: number; chargeCents: number; taxableCents: number; taxCents: number }; // BT-131/BT-146 (line net amount / item net price) — `item.unitPrice` is // this shop's normal gross (VAT-inclusive) customer-facing price, same // value the visual PDF shows, but EN16931 invoice lines are always net; // de-grossed here per line's own rate, then rounded to the nearest cent // once — every other amount in this document is ultimately built from // summing these same per-line integer-cent values, never re-derived from // the original float unit price a second time. function computeLines(items: LineInput[]): ComputedLine[] { return items.map((item) => { const netUnitPrice = item.unitPrice / (1 + item.taxRatePercent / 100); return { item, netCents: Math.round(item.quantity * netUnitPrice * 100) }; }); } // Groups the already-rounded per-line net cents by VAT rate, then // allocates document-level discount/shipping across those groups // proportional to each group's own share of the total line net — the // EN16931-correct place for shipping/discount to live is an explicit // per-rate `cac:AllowanceCharge` (BT-92/BT-99), not folded silently into a // scaled net total the way the visual PDF's summary table gets away with. // Every field below (`taxableCents`, `taxCents`, and every total this feeds // into) is built purely from sums/rounds of the same per-line/per-group // integer cents, so BR-CO-10/12/13/14 and BR-S-08 all hold exactly by // construction — none of them are checking against the *original* // discountAmount/shippingCost float inputs, only that this document's own // declared totals equal the sum of its own declared parts. function computeRateGroups(lines: ComputedLine[], discountAmount: number, shippingCost: number): RateGroup[] { const byRate = new Map(); for (const { item, netCents } of lines) { byRate.set(item.taxRatePercent, (byRate.get(item.taxRatePercent) ?? 0) + netCents); } const totalNetCents = Array.from(byRate.values()).reduce((sum, c) => sum + c, 0); // `discountAmount`/`shippingCost` are this shop's normal GROSS // (VAT-inclusive) figures, same as `unitPrice` — allocated proportionally // per rate group in gross terms (same ratio basis as the line amounts), // then degrossed via that group's own rate, exactly like every other // amount in this document. Missing this degross step was a real, shipped // bug (2026-07-23): every allowance/charge came out ~19%/7% too large, // which BR-CO-12/13/14 couldn't catch (those only check this document's // own numbers reconcile with each other, not against the original // discountAmount/shippingCost) — surfaced instead as an oversized // PayableRoundingAmount (a real order showed >1 EUR, not a rounding-scale // difference) when an external checker flagged it. With degrossing // correctly applied, TaxInclusiveAmount reconciles to `order.total` // exactly and PayableRoundingAmount is 0 for a normal order. const discountCents = Math.round(discountAmount * 100); const shippingCents = Math.round(shippingCost * 100); return Array.from(byRate.entries()) .map(([rate, lineNetCents]) => { const ratio = totalNetCents > 0 ? lineNetCents / totalNetCents : 0; const allowanceCents = discountCents > 0 ? Math.round((discountCents * ratio) / (1 + rate / 100)) : 0; const chargeCents = shippingCents > 0 ? Math.round((shippingCents * ratio) / (1 + rate / 100)) : 0; const taxableCents = lineNetCents - allowanceCents + chargeCents; const taxCents = Math.round((taxableCents * rate) / 100); return { rate, lineNetCents, allowanceCents, chargeCents, taxableCents, taxCents }; }) .sort((a, b) => b.rate - a.rate); } function taxTotal(rateGroups: RateGroup[], vatMode: VatMode): UblInvoice["cac:TaxTotal"] { const totalTaxCents = rateGroups.reduce((sum, g) => sum + g.taxCents, 0); return [ { ...amtCents("cbc:TaxAmount", totalTaxCents), "cac:TaxSubtotal": rateGroups.map((g) => ({ ...amtCents("cbc:TaxableAmount", g.taxableCents), ...amtCents("cbc:TaxAmount", g.taxCents), "cac:TaxCategory": vatTaxSubtotalCategory(g.rate, vatMode), })), }, ] as unknown as UblInvoice["cac:TaxTotal"]; } function allowanceCharges(rateGroups: RateGroup[], vatMode: VatMode): UblInvoice["cac:AllowanceCharge"] { // Loosely typed here (build as Record, cast once at the // return boundary) — VAT_CATEGORY's literal type widens to plain // `string` the moment it's read through an intermediate variable, which // the library's generated VAT-category-code union type then rejects; // matches invoiceLines()/taxTotal()'s own established pattern for the // exact same widening issue. const entries: Record[] = []; for (const g of rateGroups) { const taxCategory = vatTaxCategory(g.rate, vatMode); if (g.allowanceCents > 0) { entries.push({ "cbc:ChargeIndicator": "false", "cbc:AllowanceChargeReason": "Rabatt", ...amtCents("cbc:Amount", g.allowanceCents), "cac:TaxCategory": taxCategory, }); } if (g.chargeCents > 0) { entries.push({ "cbc:ChargeIndicator": "true", "cbc:AllowanceChargeReason": "Versandkosten", ...amtCents("cbc:Amount", g.chargeCents), "cac:TaxCategory": taxCategory, }); } } return entries.length ? (entries as unknown as UblInvoice["cac:AllowanceCharge"]) : undefined; } function invoiceLines(lines: ComputedLine[], vatMode: VatMode): UblInvoice["cac:InvoiceLine"] { return lines.map(({ item, netCents }, i) => { const netUnitPrice = item.unitPrice / (1 + item.taxRatePercent / 100); return { "cbc:ID": String(i + 1), ...qty(item.quantity), ...amtCents("cbc:LineExtensionAmount", netCents), "cac:Item": { "cbc:Name": item.variantName ? `${item.productName} (${item.variantName})` : item.productName, "cac:ClassifiedTaxCategory": vatTaxCategory(item.taxRatePercent, vatMode), }, // Unit price, not a summed/reconciled total — kept as the plain // (unrounded-to-cent) net unit price for reference; BR-CO-10 only // checks LineExtensionAmount sums, never PriceAmount. "cac:Price": amtCents("cbc:PriceAmount", Math.round(netUnitPrice * 100)), }; }) as unknown as UblInvoice["cac:InvoiceLine"]; } // `chargedCents` is the amount actually charged (the original invoice's // own stored `order.total`, or — for a correction — the natural total // this same breakdown derives, see the two call sites). It can in // principle differ from this document's own internally-consistent // TaxInclusiveAmount by a cent or two (e.g. `order.total` comes from // checkout's own independent cart/discount math, not this per-rate // proportional allocation) — EN16931 has a dedicated field for exactly // that gap, `cbc:PayableRoundingAmount` (BT-114): BR-CO-16 requires // PayableAmount = TaxInclusiveAmount − PrepaidAmount + RoundingAmount, so // any gap is declared explicitly rather than either figure silently // disagreeing with where it actually comes from. // // `prepaid`: whether the order was already paid at checkout (Kreditkarte/ // PayPal, everything except Überweisung — same `isPaidImmediately()` // definition invoicePdf.tsx's own "✓ Bereits beglichen" badge uses). If // so, the whole charged amount is declared as PrepaidAmount (BT-113) and // PayableAmount (BT-115) becomes 0 — nothing outstanding, matching what // the visual PDF's badge already tells a human reader, now machine- // readable too. This also satisfies BR-CO-25 (a positive BT-115 needs a // due date or payment terms) by construction: an already-settled invoice // has a zero, not positive, amount due. function legalMonetaryTotal(rateGroups: RateGroup[], chargedCents: number, prepaid: boolean): UblInvoice["cac:LegalMonetaryTotal"] { const lineExtensionCents = rateGroups.reduce((sum, g) => sum + g.lineNetCents, 0); const allowanceTotalCents = rateGroups.reduce((sum, g) => sum + g.allowanceCents, 0); const chargeTotalCents = rateGroups.reduce((sum, g) => sum + g.chargeCents, 0); const taxExclusiveCents = lineExtensionCents - allowanceTotalCents + chargeTotalCents; const taxInclusiveCents = taxExclusiveCents + rateGroups.reduce((sum, g) => sum + g.taxCents, 0); const prepaidCents = prepaid ? chargedCents : 0; const payableCents = prepaid ? 0 : chargedCents; const roundingCents = payableCents - taxInclusiveCents + prepaidCents; return { ...amtCents("cbc:LineExtensionAmount", lineExtensionCents), ...(allowanceTotalCents > 0 ? amtCents("cbc:AllowanceTotalAmount", allowanceTotalCents) : {}), ...(chargeTotalCents > 0 ? amtCents("cbc:ChargeTotalAmount", chargeTotalCents) : {}), ...amtCents("cbc:TaxExclusiveAmount", taxExclusiveCents), ...amtCents("cbc:TaxInclusiveAmount", taxInclusiveCents), ...(prepaidCents > 0 ? amtCents("cbc:PrepaidAmount", prepaidCents) : {}), ...(roundingCents !== 0 ? amtCents("cbc:PayableRoundingAmount", roundingCents) : {}), ...amtCents("cbc:PayableAmount", payableCents), } as unknown as UblInvoice["cac:LegalMonetaryTotal"]; } // BR-CO-25: a positive Amount due for payment (BT-115) needs either a due // date or payment terms text. Applies to the not-already-paid case // (Überweisung on an original invoice, or any correction invoice — see // call sites) — this shop doesn't track a formal payment-term policy // anywhere, so a standard, safe default phrase is used rather than // inventing a due-date business rule that isn't actually modeled. function paymentTerms(note: string): UblInvoice["cac:PaymentTerms"] { return { "cbc:Note": note } as UblInvoice["cac:PaymentTerms"]; } // Full original invoice — one line per order item, positive amounts, // InvoiceTypeCode 380 ("Commercial invoice"). export function buildEInvoiceData(order: InvoiceOrder, seller: InvoiceSeller): Invoice { const vatMode: VatMode = order.kleinunternehmer ? "kleinunternehmer" : order.vatExempt ? "intra-community" : "standard"; const lines = computeLines(order.items); const rateGroups = computeRateGroups(lines, order.discountAmount, order.shippingCost); const allowanceChargeEntries = allowanceCharges(rateGroups, vatMode); const paid = isPaidImmediately(order.paymentMethodTitle); return { "ubl:Invoice": { "cbc:ID": order.invoiceNumber, "cbc:IssueDate": isoDate(order.invoiceIssuedAt), "cbc:InvoiceTypeCode": "380", "cbc:DocumentCurrencyCode": "EUR", "cac:AccountingSupplierParty": sellerParty(seller), "cac:AccountingCustomerParty": buyerParty( `${order.customerFirstName} ${order.customerLastName}`, order.deliveryMethod === "address" ? order.street : null, order.zip, order.city, order.country, ), "cac:Delivery": delivery(order.invoiceIssuedAt), "cac:PaymentMeans": paymentMeans(seller, order.paymentMethodTitle), // BR-CO-25: a positive Amount due (BT-115) needs a due date or // payment terms. Only needed on the not-already-paid path — a paid // invoice's BT-115 is 0 (see legalMonetaryTotal()'s `prepaid` param). ...(paid ? {} : { "cac:PaymentTerms": paymentTerms("Zahlbar sofort ohne Abzug.") }), ...(allowanceChargeEntries ? { "cac:AllowanceCharge": allowanceChargeEntries } : {}), "cac:TaxTotal": taxTotal(rateGroups, vatMode), // `order.total` is what the customer was actually charged at // checkout — any cent of drift against this breakdown's own // TaxInclusiveAmount is declared via PayableRoundingAmount inside // legalMonetaryTotal(), not silently absorbed into either figure. "cac:LegalMonetaryTotal": legalMonetaryTotal(rateGroups, Math.round(order.total * 100), paid), "cac:InvoiceLine": invoiceLines(lines, vatMode), }, }; } // Stornorechnung/Gutschrift — same UBL `Invoice` shape (this library has // no separate "credit note" type), but InvoiceTypeCode 381 ("Credit // note") instead of 380, so a receiving AP system reads it as a // reduction, not a second charge. Amounts stay positive (the credited // amount, not a negative number) — EN16931/UBL convention puts the // polarity in the type code, not the sign, same as the PDF's own visual // "-{amount}" is a *display* convention layered on top of positive // underlying numbers (see correctionInvoicePdf.tsx's groupByTaxRate()). // Mirrors resolveLineItems() in correctionInvoicePdf.tsx exactly — same // Stornorechnung-vs-Gutschrift policy (full reversal incl. shipping vs. // only returned quantities, no shipping, no discount reproration). export function buildCorrectionEInvoiceData(kind: CorrectionInvoiceKind, order: CorrectionInvoiceOrder, seller: InvoiceSeller): Invoice { const vatMode: VatMode = order.kleinunternehmer ? "kleinunternehmer" : order.vatExempt ? "intra-community" : "standard"; const effectiveItems = kind === "storno" ? order.items : order.items.filter((item) => (item.returnQuantity ?? 0) > 0).map((item) => ({ ...item, quantity: item.returnQuantity as number })); const lines = computeLines(effectiveItems); const rateGroups = computeRateGroups(lines, kind === "storno" ? order.discountAmount : 0, kind === "storno" ? order.shippingCost : 0); const allowanceChargeEntries = allowanceCharges(rateGroups, vatMode); // No separately-stored "charged total" exists for a correction event the // way `order.total` does for the original invoice — the reversal amount // *is* this breakdown's own TaxInclusiveAmount, so PayableAmount is // derived from it directly (computed inside legalMonetaryTotal()) rather // than passed in independently. const taxInclusiveCents = rateGroups.reduce((sum, g) => sum + g.lineNetCents - g.allowanceCents + g.chargeCents, 0) + rateGroups.reduce((sum, g) => sum + g.taxCents, 0); return { "ubl:Invoice": { "cbc:ID": order.correctionInvoiceNumber, "cbc:IssueDate": isoDate(order.correctionInvoiceIssuedAt), "cbc:InvoiceTypeCode": "381", "cbc:DocumentCurrencyCode": "EUR", "cac:BillingReference": [ { "cac:InvoiceDocumentReference": { "cbc:ID": order.invoiceNumber, "cbc:IssueDate": isoDate(order.invoiceIssuedAt), }, }, ], "cac:AccountingSupplierParty": sellerParty(seller), "cac:AccountingCustomerParty": buyerParty( `${order.customerFirstName} ${order.customerLastName}`, order.deliveryMethod === "address" ? order.street : null, order.zip, order.city, order.country, ), "cac:Delivery": delivery(order.correctionInvoiceIssuedAt), "cac:PaymentMeans": paymentMeans(seller, "Überweisung"), // BR-CO-25 — a credit note's "amount due" represents a refund owed to // the customer, not an open invoice payment, so there's no "already // paid"/PrepaidAmount concept here (unlike buildEInvoiceData's paid // branch above) — payment terms text satisfies the rule instead. "cac:PaymentTerms": paymentTerms("Der Rechnungsbetrag wird erstattet."), ...(allowanceChargeEntries ? { "cac:AllowanceCharge": allowanceChargeEntries } : {}), "cac:TaxTotal": taxTotal(rateGroups, vatMode), "cac:LegalMonetaryTotal": legalMonetaryTotal(rateGroups, taxInclusiveCents, false), "cac:InvoiceLine": invoiceLines(lines, vatMode), }, }; }