Skip to content

Source

This page is generated from docs/mapping-reference-medusa.md at the pinned revision 5ab878035219. Edit it there, not here.

Mapping reference: Medusa v2 → BT/BG ​

Hand-written (unlike docs/mapping-reference.md, which is generated from packages/einvoice-cii/src/generated/plan.ts — do not confuse the two, and do not hand-edit that one, AGENTS.md §9). This file documents the other half of the adapter: which Medusa order/customer field, or which @normwerk/einvoice-medusa module option, a BT/BG number's value actually comes from. Every row is cross-checked against packages/einvoice-medusa/src/mapping/order-to-commerce-invoice-input.ts's real source (not memory) and, for the BT number itself, against docs/mapping-reference.md's own generated Model path column or packages/einvoice-model/src/generated/types.ts's own doc comments.

Re-derive this by hand after changing order-to-commerce-invoice-input.ts — there is no generator for this table (the field list here comes from real Medusa entities across several modules, not one typed plan tree the way the CII mapping does).

From the order itself (ORDER_QUERY_FIELDS) ​

BT/BGMedusa sourceNote
BT-2 (issue date)not from the order — new Date() at the moment the subscriber runsReal invoice date, not the order's own creation date.
BT-5 (currency code)order.currency_code, uppercased
BT-10 (buyer reference)order.customer.metadata.leitweg_id (passed as references.leitwegId), else order.customer.metadata.buyer_reference, else String(order.display_id)Both keys are this plugin's own convention — no built-in Medusa field carries either. Only leitweg_id is a Leitweg-ID: it is validated, check digits included, and routes the order to XRechnung. buyer_reference is free text, never read as a Leitweg-ID. Always populated (BR-DE-15).
BT-44 (buyer name)the billing address's company, else order.customer.company_name, else the billing address's first_name + last_name, else the customer's, else order.email/customer.emailFirst non-empty value wins, in that order. The billing address comes first because a guest checkout's customer record carries no name at all; the shipping address stands in when the order has no billing address.
BT-48 (buyer VAT-ID)order.customer.metadata.vat_idThis plugin's own convention — no built-in Medusa field carries a buyer VAT-ID either.
BT-49 (buyer electronic address)order.email, else order.customer.emailScheme fixed to "EM" (a plain email address). An order with no email at all is refused: XRechnung requires the buyer's electronic address (PEPPOL-EN16931-R010).
BT-50/51 (buyer address lines)order.billing_address.address_1/address_2, else .shipping_address.address_1/address_2Same address as BT-52/53. A blank line is left out. Without address_1 the invoice carries no buyer street, and buildInvoice warns above EUR 250 (§14 Abs. 4 Satz 1 Nr. 1 UStG; §33 UStDV).
BT-52 (buyer city)order.billing_address.city, else order.shipping_address.cityBilling address wins when both are present.
BT-53 (buyer post code)order.billing_address.postal_code, else .shipping_address.postal_code
BT-55 (buyer country code)order.billing_address.country_code, else .shipping_address.country_code, uppercasedThrows MissingBuyerCountryError if neither address has one — there is no further fallback (BT-55 has no source at all otherwise).
— buyer-is-business signal (taxContext.buyerIsBusiness)Boolean(order.customer.company_name)Not a BT itself — feeds decideVatCategory (einvoice-commerce), which BT-118 (VAT category code) then reflects.
— regime override (taxContext.regimeOverride)order.metadata.regime_overrideNot a BT itself — a whole RegimeOverride object (@normwerk/einvoice-commerce), carried straight through (Medusa's metadata is jsonb, no re-encoding needed). Feeds decideVatCategory's AE/E/Z branches (rows 5/6/8/12); the reflected BTs (BT-118, BT-120/121) depend on which kind.
— OSS destination-country rate (taxContext.ossRateOverride)order.metadata.oss_rate_overrideNot a BT itself — a plain decimal-percentage string (Amount), carried straight through, no lookup or validation here. Feeds decideVatCategory's row-7 (OSS) branch; decideVatCategory still refuses row 7 without it even when ossRegistered (below) is true.
— supply type, per line (lines[].supplyType) and whole-order (taxContext.supplyType)order.items[].requires_shipping, aggregated across all linesrequires_shipping === false → "services", else "goods"; more than one kind across lines → "mixed". Feeds decideVatCategory's rows 3/12/13 and the cross-border-mixed refusal. See resolveLineSupplyType/resolveOrderSupplyType's own doc comments for why this field, not a manual per-line override.
BT-72 (actual delivery date, BG-13)the invoiced fulfillment's created_at, as a date in Berlin; on a credit note, the corrected invoice's own BT-72, read back from its CII (its date when it states none)Each fulfillment is a supply of its own, dated the day it shipped (§14 Abs. 4 Nr. 6 UStG); a credit note corrects that supply. The rates of this day apply (germanVatRatesOn). Consulted by buildInvoice for category K (BR-IC-11).
BT-80 (deliver-to country code, BG-13)order.shipping_address.country_code, else the resolved buyer country (BT-55), uppercasedOnly ever consulted for category K (BR-IC-12).
BT-75/76 (deliver-to address lines, BG-13)order.shipping_address.address_1/address_2, else the buyer'sSame address as BT-77/78/80. A blank line is left out.
BT-77/78 (deliver-to city/post code, BG-13)order.shipping_address.city/.postal_code, else the resolved buyer city/post codeNot enforced by any Schematron rule — mapped for completeness alongside BT-80.
BT-126 (line identifier)1-based position among the invoiced fulfillment's lines, not a Medusa field
BT-129 (invoiced quantity)the invoiced fulfillment's lines (fulfillments[].items[].line_item_id), each with the units the order recorded for it: the FULFILL_ITEM action with the fulfillment's id (order_change.actions[], details.quantity); for the whole order, order.items[].detail.quantityAn invoice covers what its fulfillment shipped. Not the fulfillment item's quantity: for a variant with stock managed Medusa lists a fulfillment item per inventory item, its units times required_quantity — a table and four chairs would be five sets. Not OrderLineItem.quantity either — the line item's own DML model has no such field; the ordered quantity lives on the linked OrderItem row (detail). A shipped line without that record is refused (SHIPMENT_QUANTITY_UNKNOWN).
BT-130 (unit code)hardcoded "C62" (UN/ECE Rec 20, "piece")Medusa doesn't track a per-line unit-of-measure code by default — a real, documented v0.1 gap, not read from anywhere.
BT-146 (item net price)order.items[].unit_price — net as it is; when is_tax_inclusive, passed on as priceInclVat and the net price worked out by buildInvoiceA tax-inclusive price is not backed out here: buildInvoice takes each rate group's VAT out of its gross total, so the invoice totals what Medusa charged.
BG-27 / BT-136, BT-139 (line allowance: amount, reason)the line's share of order.items[].discount_subtotal — Medusa's own net discount for the line (discount_total, including tax, on a tax-inclusive line); reason = the line's items[].adjustments[].code values, else "Rabatt / Discount"Medusa states a line's discount for the units the buyer still has, so it is scaled back to the ordered quantity (detail.quantity less return_received_quantity and return_dismissed_quantity). Each fulfillment's invoice takes the share of its units; the one that ships a line's last units takes what is left, so a line's invoices add up to it. Rounded to 2 decimals.
BG-21 / BT-99, BT-104 (document-level charge: amount, reason)every order.shipping_methods[]: subtotal − discount_subtotal (net, after the shipping method's own discounts), summed — or, when every method is tax-inclusive, their total, passed on as amountInclVat; reason = "Versand / Shipping: " + the methods' namesOn the order's first invoice whose fulfillment still stands — the other invoices carry none. Omitted when shipping is free. Its VAT rate is buildInvoice's: the rate of the lines when they share one; with lines at different rates, one charge per rate, in proportion to the whole order's lines at each rate.
— (not an invoice field)for a fulfillment's invoice: its units at order.items[].total and .tax_total divided by the units the buyer kept, plus order.shipping_methods[].total and .tax_total on the invoice that carries the shipping; for the whole order, order.total + order.credit_line_total and order.tax_totalCompared with the invoice's BT-112 and BT-110 before a document number is taken — what Medusa charged for what this invoice covers. Less VAT on the invoice that explains the whole difference: issued with a notice; any other difference beyond rounding: not issued, the reason shown in the admin (docs/tax-semantics.md).
— (not an invoice field)order.shipping_methods[].tax_totalThe VAT Medusa charged on shipping — names shipping split across the order's rates as the cause of a notice.
— (not an invoice field)order.shipping_methods[].tax_lines[].rate (summed per method; the highest over the methods)The rate Medusa charged on shipping, passed on as a fact (shipping.chargedVatRate). Never the shipping's rate on the invoice — that follows the lines. Read only to tell a shop that charged no VAT at all — no line and no shipping above 0 % — which is refused as NO_VAT_CHARGED.
BT-113 (paid amount)the invoice's own total, when order.payment_collections[].captured_amount less refunded_amount covers order.totalAn order paid in full before it shipped: the invoice states its total as paid and the amount due (BT-115) is 0.00. Otherwise not stated — store credit and gift cards are not stated as paid either.
BT-151/152 (line VAT category/rate, indirectly)order.items[].tax_lines[].rate (summed), passed on as the rate Medusa charged; on a credit note, the rate the corrected invoice stated for the line (line_values[].rate, passed on as invoicedVatRate)Only consulted for category S — ignored for K/G/AE/E/Z, which are uniform for the whole document. Domestically the rate must be Germany's standard or reduced rate on the supply date (19 or 7; 16 or 5 from 2020-07-01 to 2020-12-31) and gives the line its rate; in an OSS sale it must equal oss_rate_override. A line charged at any other rate, or with no tax line on a domestic order, is refused rather than invoiced at a rate the buyer was not charged; one charged at another period's German rate is refused as RATE_NOT_IN_FORCE_ON_SUPPLY_DATE. A credit note's line takes its invoice's rate, whatever Medusa's tax line or today's rates say.
BT-153 (item name)order.items[].title

For a partial credit note (refund, cancellation) ​

Read by the credit-note path only, as the order's returns (return filtered by order_id: id, status, received_at, created_at, items[].item_id, items[].received_quantity). A received unit is valued at what the invoice stated for its line — stored with the invoice when it is issued (einvoice_document.line_values: the line item id, rate, quantity and gross amount of each line), not at Medusa's line totals, which Medusa recomputes once goods come back. What the invoice charged, and earlier credit notes credited, per rate is read back from the stored documents' BG-23 breakdown. Which returns a credit note paid for is stored with it (einvoice_document.covered_returns).

From merchant configuration (EinvoiceModuleOptions, medusa-config.ts) ​

Not order data at all — the same value on every document this plugin ever builds, set once in medusa-config.ts's plugins: [{ resolve: "@normwerk/einvoice-medusa", options: {...} }].

BT/BGOption
BT-27 (seller name)seller.name
BT-29 (seller identifier)not modeled — no seller.identifier option
BT-30 (seller legal reg. id)seller.legalRegistrationIdentifier
BT-31 (seller VAT-ID)seller.vatIdentifier — mandatory (assertValidOptions, no fallback)
BT-34 (seller electronic address)seller.electronicAddress — mandatory, with seller.electronicAddressScheme (BR-62)
BT-35/36 (seller address lines)seller.addressLine1 — mandatory (assertValidOptions: §14 Abs. 4 Satz 1 Nr. 1 UStG) / seller.addressLine2
BT-37/38/40 (seller city/postcode/country)seller.city/seller.postCode/seller.countryCode
BT-41/42/43 (seller contact)seller.contact.name/.telephone/.email — mandatory (assertValidOptions): every document declares the XRechnung 3.0 CIUS, whose BR-DE-2 requires seller contact
BG-16 (payment instructions)payment.means/.iban/.terms — mandatory (BR-DE-1)
— OSS registration (taxContext.ossRegistered)ossRegistered — optional, defaults to false. A standing fact about the merchant, true for every order, unlike ossRateOverride above.

Not sourced from Medusa, and not merchant-configurable either (v0.1 fixed defaults) ​

Worth knowing before assuming a value can be changed without code — none of these read from the order or from EinvoiceModuleOptions:

  • BT-130 (unit code) — always "C62" (see table above).
  • BT-158/BT-159 (HS code / country of origin, lines[].hsCode/.originCountry) — never populated by this mapping at all (no source field read for either); CommerceLine itself supports both, this adapter just doesn't wire them from an order yet.

Not read from Medusa's own field, by design ​

  • BT-48/BT-10's B2G source (buyer VAT-ID / Leitweg-ID) both come from order.customer.metadata, a free-form JSON bag — not a first-class Medusa field. docs/domain-glossary.md documents this convention (metadata.vat_id, metadata.leitweg_id, metadata.buyer_reference); a storefront or admin extension that collects these needs to write to that same metadata key for this plugin to ever see them.
  • regimeOverride's source (order.metadata.regime_override) is deliberately order.metadata, not customer.metadata. A reverse-charge/exempt/zero-rated call is a legal judgment about this transaction — putting it on the customer would make it silently apply to every order that customer ever places, not just the one it was actually true for.
  • OSS's two facts deliberately live at two different tiers, not one. ossRegistered is EinvoiceModuleOptions-level (merchant config, alongside seller/payment) because it's a standing fact about the merchant, true for every order they issue. ossRateOverride is order.metadata-level (alongside regime_override) because it's a fact about this order's buyer country, not the merchant — the same transaction-vs-standing-fact distinction regimeOverride's own bullet above draws. This package never maintains its own EU VAT rate table (a live, frequently-changing dataset with no vendored source) — the merchant, who already computed the rate at checkout, is the only honest source for it.