Download index.html from getreadystack/einvoice-mandate-lint: direct link, hf CLI and curl.
- Browser
- Download file 63.8 kB
-
https://huggingface.co/spaces/getreadystack/einvoice-mandate-lint/resolve/main/index.html
- Command line
-
hf download hf://spaces/getreadystack/einvoice-mandate-lint/index.html
-
curl -L -o index.html https://huggingface.co/spaces/getreadystack/einvoice-mandate-lint/resolve/main/index.html
63.8 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>EU einvoice 2026 requirements — check invoice xml free</title> | |
| <meta name="description" content="This browser tool checks UBL or CII invoice XML against EN 16931 and listed 2026 country mandates, and flags retired schema versions."> | |
| <style> | |
| :root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512} | |
| .wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px} | |
| textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box} | |
| .bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer} | |
| input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px} | |
| table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65} | |
| .sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600} | |
| .note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa} | |
| </style> | |
| <script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script> | |
| <!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/einvoice-mandate-lint"></head> | |
| <body> | |
| <div class="wrap"> | |
| <h1>EU einvoice 2026 requirements — check invoice xml free</h1> | |
| <p class="lede">Lints UBL/CII invoice XML against EN 16931 plus the 2026 country mandates (FR, DE, BE, PL, ES, IT) and flags retired schema versions. Runs entirely in your browser — nothing is uploaded.</p> | |
| <textarea id="src" spellcheck="false"><?xml version="1.0" encoding="UTF-8"?> | |
| <Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" | |
| xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" | |
| xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"> | |
| <cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.3</cbc:CustomizationID> | |
| <cbc:ID>FA-2026-0915</cbc:ID> | |
| <cbc:IssueDate>2026-09-11</cbc:IssueDate> | |
| <cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode> | |
| <cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode> | |
| <cac:AccountingSupplierParty> | |
| <cac:Party> | |
| <cac:PostalAddress> | |
| <cbc:StreetName>12 rue Oberkampf</cbc:StreetName> | |
| <cbc:CityName>Paris</cbc:CityName> | |
| <cac:Country><cbc:IdentificationCode>FR</cbc:IdentificationCode></cac:Country> | |
| </cac:PostalAddress> | |
| <cac:PartyTaxScheme> | |
| <cbc:CompanyID>FR40732829320</cbc:CompanyID> | |
| <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme> | |
| </cac:PartyTaxScheme> | |
| <cac:PartyLegalEntity> | |
| <cbc:RegistrationName>Atelier Bellevue SAS</cbc:RegistrationName> | |
| <cbc:CompanyID>FR 732 829 320</cbc:CompanyID> | |
| </cac:PartyLegalEntity> | |
| </cac:Party> | |
| </cac:AccountingSupplierParty> | |
| <cac:AccountingCustomerParty> | |
| <cac:Party> | |
| <cac:PostalAddress> | |
| <cbc:StreetName>8 avenue de Flandre</cbc:StreetName> | |
| <cbc:CityName>Lyon</cbc:CityName> | |
| <cac:Country><cbc:IdentificationCode>FR</cbc:IdentificationCode></cac:Country> | |
| </cac:PostalAddress> | |
| <cac:PartyLegalEntity> | |
| <cbc:RegistrationName>Groupe Verlaine SARL</cbc:RegistrationName> | |
| </cac:PartyLegalEntity> | |
| </cac:Party> | |
| </cac:AccountingCustomerParty> | |
| <cac:TaxTotal> | |
| <cbc:TaxAmount currencyID="EUR">1000.00</cbc:TaxAmount> | |
| </cac:TaxTotal> | |
| <cac:LegalMonetaryTotal> | |
| <cbc:LineExtensionAmount currencyID="EUR">4800.00</cbc:LineExtensionAmount> | |
| <cbc:TaxExclusiveAmount currencyID="EUR">5000.00</cbc:TaxExclusiveAmount> | |
| <cbc:TaxInclusiveAmount currencyID="EUR">5900.00</cbc:TaxInclusiveAmount> | |
| <cbc:PayableAmount currencyID="EUR">6000.00</cbc:PayableAmount> | |
| </cac:LegalMonetaryTotal> | |
| <cac:InvoiceLine> | |
| <cbc:ID>1</cbc:ID> | |
| <cbc:LineExtensionAmount currencyID="EUR">3000.00</cbc:LineExtensionAmount> | |
| <cac:Item><cbc:Name>Integration e-facturation</cbc:Name></cac:Item> | |
| </cac:InvoiceLine> | |
| <cac:InvoiceLine> | |
| <cbc:ID>2</cbc:ID> | |
| <cbc:LineExtensionAmount currencyID="EUR">2000.00</cbc:LineExtensionAmount> | |
| <cac:Item><cbc:Name>Recette et raccordement PA</cbc:Name></cac:Item> | |
| </cac:InvoiceLine> | |
| </Invoice></textarea> | |
| <div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div> | |
| <div id="out" aria-live="polite"></div> | |
| <div id="paid"><strong>Free here:</strong> Lint the invoice XML you have open - every EN 16931 core field plus the country mandate rules its CustomizationID selects, reported with line numbers.. <strong>In the editor:</strong> Scan every invoice XML in the workspace in one pass and export the findings as JSON, CSV or SARIF you keep and run in CI. — that part asks for a licence key.</div> | |
| <p class="note">Same engine as the VS Code extension, byte for byte.</p> | |
| </div> | |
| <script>var EINV_RULES = { | |
| "version": "2026.09.11", | |
| "syntaxes": { | |
| "Invoice": "UBL 2.1 Invoice", | |
| "CreditNote": "UBL 2.1 CreditNote", | |
| "CrossIndustryInvoice": "UN/CEFACT CII D16B" | |
| }, | |
| "profiles": { | |
| "urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0": { | |
| "label": "Peppol BIS Billing 3.0", "status": "current", "country": "*" | |
| }, | |
| "urn:cen.eu:en16931:2017": { | |
| "label": "EN 16931 core, no CIUS", "status": "current", "country": "*" | |
| }, | |
| "urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_3.0": { | |
| "label": "XRechnung 3.0 (KoSIT)", "status": "current", "country": "DE" | |
| }, | |
| "urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.3": { | |
| "label": "XRechnung 2.3", "status": "retired", "retired_on": "2025-02-06", | |
| "replaced_by": "urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_3.0", "country": "DE" | |
| }, | |
| "urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.2": { | |
| "label": "XRechnung 2.2", "status": "retired", "retired_on": "2024-02-01", | |
| "replaced_by": "urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_3.0", "country": "DE" | |
| }, | |
| "urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic": { | |
| "label": "Factur-X 1.0 BASIC", "status": "current", "country": "FR" | |
| }, | |
| "urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:en16931": { | |
| "label": "Factur-X 1.0 EN 16931 (COMFORT)", "status": "current", "country": "FR" | |
| }, | |
| "urn:factur-x.eu:1p0:minimum": { | |
| "label": "Factur-X MINIMUM", "status": "insufficient", "country": "FR", | |
| "note": "MINIMUM is not an EN 16931 compliant profile and is not a valid structured invoice for the French B2B mandate." | |
| } | |
| }, | |
| "mandates": [ | |
| {"country": "FR", "name": "France - Plateforme Agreee / Factur-X", | |
| "receive_from": "2026-09-01", "issue_from": "2026-09-01", | |
| "note": "Every VAT-registered business in France must be able to receive structured e-invoices from 2026-09-01; large and mid-sized companies must also issue from that date, SMEs and micro-enterprises from 2027-09-01. Accepted core formats: Factur-X, UBL 2.1, CII."}, | |
| {"country": "DE", "name": "Germany - E-Rechnung / XRechnung", | |
| "receive_from": "2025-01-01", "issue_from": "2027-01-01", | |
| "note": "Receipt of structured e-invoices is mandatory since 2025-01-01. Issuing becomes mandatory 2027-01-01 for sellers above EUR 800,000 prior-year turnover and 2028-01-01 for everyone else."}, | |
| {"country": "BE", "name": "Belgium - Peppol B2B", | |
| "receive_from": "2026-01-01", "issue_from": "2026-01-01", | |
| "note": "Domestic B2B structured e-invoicing is mandatory since 2026-01-01; the tolerance period ended 2026-03-31 and penalties are enforced. Peppol BIS Billing 3.0 is the reference format."}, | |
| {"country": "PL", "name": "Poland - KSeF FA(3)", | |
| "receive_from": "2026-02-01", "issue_from": "2026-02-01", | |
| "note": "FA(2) was replaced by the FA(3) XML structure on 2026-02-01. KSeF issuing is mandatory from 2026-02-01 above PLN 200m prior-year turnover, from 2026-04-01 for all other VAT payers, and from 2027-01-01 for micro-enterprises."}, | |
| {"country": "ES", "name": "Spain - Verifactu / SIF", | |
| "receive_from": "2027-01-01", "issue_from": "2027-01-01", | |
| "note": "Verifactu was postponed by one year: Corporate Income Tax payers from 2027-01-01 and other taxpayers, including autonomos, from 2027-07-01. A 2026 go-live date is out of date."}, | |
| {"country": "IT", "name": "Italy - FatturaPA / SdI", | |
| "receive_from": "2019-01-01", "issue_from": "2019-01-01", | |
| "note": "Domestic invoices clear through the Sistema di Interscambio in the FatturaPA XML format. UBL and CII are not accepted for domestic Italian invoices."} | |
| ], | |
| "rules": [ | |
| {"id": "EINV-SYNTAX", "kind": "syntax", "sev": "error", "bt": "-", "msg": "Root element is not an EN 16931 syntax", "fix": "Use a UBL 2.1 Invoice/CreditNote or a UN/CEFACT CrossIndustryInvoice document."}, | |
| {"id": "EINV-BT-24", "kind": "profile", "sev": "error", "bt": "BT-24", "msg": "Specification identifier (CustomizationID) is missing", "fix": "Add cbc:CustomizationID naming the EN 16931 CIUS this invoice follows."}, | |
| {"id": "EINV-PROFILE-UNKNOWN", "kind": "profile_unknown", "sev": "warn", "bt": "BT-24", "msg": "Specification identifier is not a profile this linter knows", "fix": "Check the identifier against the CIUS your receiving platform publishes."}, | |
| {"id": "EINV-PROFILE-RETIRED", "kind": "profile_retired", "sev": "error", "bt": "BT-24", "msg": "Specification identifier names a retired profile version", "fix": "Move to the replacement profile."}, | |
| {"id": "EINV-BT-1", "kind": "present", "sev": "error", "bt": "BT-1", "path": ["cbc:ID"], "msg": "Invoice number (BT-1) is missing", "fix": "Add cbc:ID at document level."}, | |
| {"id": "EINV-BT-2", "kind": "present", "sev": "error", "bt": "BT-2", "path": ["cbc:IssueDate"], "msg": "Invoice issue date (BT-2) is missing", "fix": "Add cbc:IssueDate in YYYY-MM-DD form."}, | |
| {"id": "EINV-BT-3", "kind": "present", "sev": "error", "bt": "BT-3", "path": ["cbc:InvoiceTypeCode"], "msg": "Invoice type code (BT-3) is missing", "fix": "Add cbc:InvoiceTypeCode, normally 380 for a commercial invoice."}, | |
| {"id": "EINV-BT-5", "kind": "present", "sev": "error", "bt": "BT-5", "path": ["cbc:DocumentCurrencyCode"], "msg": "Invoice currency code (BT-5) is missing", "fix": "Add cbc:DocumentCurrencyCode, for example EUR."}, | |
| {"id": "EINV-BT-27", "kind": "present", "sev": "error", "bt": "BT-27", "path": ["cac:AccountingSupplierParty", "cac:PartyLegalEntity", "cbc:RegistrationName"], "msg": "Seller legal name (BT-27) is missing", "fix": "Add cac:PartyLegalEntity/cbc:RegistrationName under the supplier party."}, | |
| {"id": "EINV-BT-31", "kind": "present", "sev": "error", "bt": "BT-31", "path": ["cac:AccountingSupplierParty", "cac:PartyTaxScheme", "cbc:CompanyID"], "msg": "Seller VAT identifier (BT-31) is missing", "fix": "Add cac:PartyTaxScheme/cbc:CompanyID with the VAT scheme for the seller."}, | |
| {"id": "EINV-BT-40", "kind": "present", "sev": "error", "bt": "BT-40", "path": ["cac:AccountingSupplierParty", "cac:PostalAddress", "cbc:IdentificationCode"], "msg": "Seller country code (BT-40) is missing", "fix": "Add cac:Country/cbc:IdentificationCode in the seller postal address."}, | |
| {"id": "EINV-BT-44", "kind": "present", "sev": "error", "bt": "BT-44", "path": ["cac:AccountingCustomerParty", "cac:PartyLegalEntity", "cbc:RegistrationName"], "msg": "Buyer legal name (BT-44) is missing", "fix": "Add cac:PartyLegalEntity/cbc:RegistrationName under the customer party."}, | |
| {"id": "EINV-BT-55", "kind": "present", "sev": "error", "bt": "BT-55", "path": ["cac:AccountingCustomerParty", "cac:PostalAddress", "cbc:IdentificationCode"], "msg": "Buyer country code (BT-55) is missing", "fix": "Add cac:Country/cbc:IdentificationCode in the buyer postal address."}, | |
| {"id": "EINV-BT-110", "kind": "present", "sev": "error", "bt": "BT-110", "path": ["cac:TaxTotal", "cbc:TaxAmount"], "msg": "Invoice total VAT amount (BT-110) is missing", "fix": "Add cac:TaxTotal/cbc:TaxAmount at document level."}, | |
| {"id": "EINV-BT-109", "kind": "present", "sev": "error", "bt": "BT-109", "path": ["cac:LegalMonetaryTotal", "cbc:TaxExclusiveAmount"], "msg": "Invoice total without VAT (BT-109) is missing", "fix": "Add cbc:TaxExclusiveAmount to cac:LegalMonetaryTotal."}, | |
| {"id": "EINV-BT-112", "kind": "present", "sev": "error", "bt": "BT-112", "path": ["cac:LegalMonetaryTotal", "cbc:TaxInclusiveAmount"], "msg": "Invoice total with VAT (BT-112) is missing", "fix": "Add cbc:TaxInclusiveAmount to cac:LegalMonetaryTotal."}, | |
| {"id": "EINV-BT-115", "kind": "present", "sev": "error", "bt": "BT-115", "path": ["cac:LegalMonetaryTotal", "cbc:PayableAmount"], "msg": "Amount due for payment (BT-115) is missing", "fix": "Add cbc:PayableAmount to cac:LegalMonetaryTotal."}, | |
| {"id": "EINV-LINE", "kind": "line_count", "sev": "error", "bt": "BG-25", "msg": "Invoice has no invoice line", "fix": "An EN 16931 invoice needs at least one cac:InvoiceLine."}, | |
| {"id": "EINV-BR-CO-10", "kind": "math_linesum", "sev": "error", "bt": "BT-106", "msg": "Sum of invoice line net amounts (BT-106) does not equal the lines", "fix": "Set cac:LegalMonetaryTotal/cbc:LineExtensionAmount to the sum of the line amounts."}, | |
| {"id": "EINV-BR-CO-15", "kind": "math_inclusive", "sev": "error", "bt": "BT-112", "msg": "Total with VAT (BT-112) does not equal total without VAT plus VAT", "fix": "BT-112 must equal BT-109 + BT-110."}, | |
| {"id": "EINV-BR-CO-16", "kind": "math_payable", "sev": "error", "bt": "BT-115", "msg": "Amount due (BT-115) does not equal total with VAT minus prepaid plus rounding", "fix": "BT-115 must equal BT-112 - BT-113 + BT-114."}, | |
| {"id": "EINV-BR-CO-25", "kind": "due_date", "sev": "warn", "bt": "BT-9", "msg": "Amount due is positive but neither a due date (BT-9) nor payment terms (BT-20) are given", "fix": "Add cbc:DueDate or cac:PaymentTerms/cbc:Note."}, | |
| {"id": "EINV-FR-SIREN", "kind": "country", "country": "FR", "sev": "error", "bt": "BT-30", "msg": "France: seller legal registration identifier (SIREN or SIRET) is missing or is not 9 or 14 digits", "fix": "Add cac:AccountingSupplierParty/cac:PartyLegalEntity/cbc:CompanyID with schemeID 0009 (SIRET) or 0002 (SIREN)."}, | |
| {"id": "EINV-FR-BUYER-SIREN", "kind": "country", "country": "FR", "sev": "error", "bt": "BT-47", "msg": "France: buyer legal registration identifier (SIREN or SIRET) is missing on a domestic B2B invoice", "fix": "Add cac:AccountingCustomerParty/cac:PartyLegalEntity/cbc:CompanyID with the buyer SIREN or SIRET."}, | |
| {"id": "EINV-FR-MINIMUM", "kind": "country", "country": "FR", "sev": "error", "bt": "BT-24", "msg": "France: the Factur-X MINIMUM profile is not an EN 16931 compliant invoice", "fix": "Use Factur-X BASIC or EN 16931 (COMFORT), or UBL 2.1 / CII."}, | |
| {"id": "EINV-DE-LEITWEG", "kind": "country", "country": "DE", "sev": "error", "bt": "BT-10", "msg": "XRechnung: buyer reference (BT-10, the Leitweg-ID) is missing", "fix": "Add cbc:BuyerReference with the Leitweg-ID the buyer gave you."}, | |
| {"id": "EINV-DE-CONTACT", "kind": "country", "country": "DE", "sev": "error", "bt": "BT-41", "msg": "XRechnung: seller contact name, telephone and email are required and one of them is missing", "fix": "Add cac:Contact with cbc:Name, cbc:Telephone and cbc:ElectronicMail under the supplier party."}, | |
| {"id": "EINV-DE-PAYMENT", "kind": "country", "country": "DE", "sev": "error", "bt": "BG-16", "msg": "XRechnung: payment instructions (BG-16) are missing", "fix": "Add cac:PaymentMeans with cbc:PaymentMeansCode."}, | |
| {"id": "EINV-BE-PEPPOL", "kind": "country", "country": "BE", "sev": "error", "bt": "BT-34", "msg": "Belgium: seller or buyer Peppol electronic address (cbc:EndpointID with a schemeID) is missing", "fix": "Add cbc:EndpointID schemeID=\"0208\" (Belgian enterprise number) to both parties."}, | |
| {"id": "EINV-BE-PROFILE", "kind": "country", "country": "BE", "sev": "warn", "bt": "BT-24", "msg": "Belgium: the reference format for the domestic B2B mandate is Peppol BIS Billing 3.0 and this invoice declares another profile", "fix": "Set CustomizationID to the Peppol BIS Billing 3.0 identifier."}, | |
| {"id": "EINV-PL-KSEF", "kind": "country", "country": "PL", "sev": "error", "bt": "-", "msg": "Poland: domestic invoices must be filed to KSeF in the FA(3) structure, not UBL or CII", "fix": "Generate the FA(3) XML for KSeF; keep UBL only for cross-border Peppol exchange."}, | |
| {"id": "EINV-PL-FA2", "kind": "pl_fa2", "sev": "error", "bt": "-", "msg": "Poland: this document uses the FA(2) KSeF structure, which was replaced by FA(3)", "fix": "Regenerate against the FA(3) schema."}, | |
| {"id": "EINV-IT-SDI", "kind": "country", "country": "IT", "sev": "error", "bt": "-", "msg": "Italy: domestic invoices clear through SdI in FatturaPA format, not UBL or CII", "fix": "Generate FatturaPA XML for domestic Italian invoices."}, | |
| {"id": "EINV-ES-VERIFACTU", "kind": "country", "country": "ES", "sev": "info", "bt": "-", "msg": "Spain: Verifactu was postponed by one year - Corporate Income Tax payers from 2027-01-01 and other taxpayers from 2027-07-01", "fix": "Check any 2026 Verifactu go-live date hard-coded in your billing configuration."}, | |
| {"id": "EINV-MANDATE-LIVE", "kind": "mandate_note", "sev": "info", "bt": "-", "msg": "Mandate status for the seller country", "fix": "No change needed; this line states the dates the other findings are measured against."} | |
| ] | |
| } | |
| ;</script> | |
| <script>/* E-Invoice Mandate Lint - engine | |
| * One brain, two homes: node (VS Code extension) and the browser (free web page). | |
| */ | |
| (function () { | |
| 'use strict'; | |
| var RULES = (typeof module !== 'undefined' && module.exports) | |
| ? require('./rules.json') | |
| : window.EINV_RULES; | |
| var byId = {}; | |
| RULES.rules.forEach(function (r) { byId[r.id] = r; }); | |
| // ---------- tiny XML reader (namespace-prefix tolerant) ---------- | |
| function localName(tag) { | |
| var i = tag.indexOf(':'); | |
| return i < 0 ? tag : tag.slice(i + 1); | |
| } | |
| function lineAt(text, index) { | |
| if (index < 0) return 1; | |
| return text.slice(0, index).split('\n').length; | |
| } | |
| // First <...:Name ...> ... </...:Name> block inside `text`. Returns null if absent. | |
| function block(text, name, from) { | |
| var ln = localName(name); | |
| var open = new RegExp('<(?:[A-Za-z0-9_.-]+:)?' + ln + '(\\s[^>]*)?(/)?>', 'g'); | |
| open.lastIndex = from || 0; | |
| var m = open.exec(text); | |
| if (!m) return null; | |
| if (m[2] === '/') return { inner: '', start: m.index, end: open.lastIndex, self: true }; | |
| var close = new RegExp('</(?:[A-Za-z0-9_.-]+:)?' + ln + '>', 'g'); | |
| close.lastIndex = open.lastIndex; | |
| var c = close.exec(text); | |
| if (!c) return null; | |
| return { | |
| inner: text.slice(open.lastIndex, c.index), | |
| attrs: m[1] || '', | |
| start: m.index, | |
| innerStart: open.lastIndex, | |
| end: close.lastIndex | |
| }; | |
| } | |
| // Walk a chain of element names. Each hop searches inside the previous hop. | |
| // Returns {value, line, attrs} or null. | |
| function pick(text, offset, chain) { | |
| var cur = text, base = offset || 0; | |
| for (var i = 0; i < chain.length; i++) { | |
| var b = block(cur, chain[i]); | |
| if (!b) return null; | |
| if (i === chain.length - 1) { | |
| return { | |
| value: b.inner.replace(/<[^>]*>/g, '').trim(), | |
| attrs: b.attrs || '', | |
| line: lineAt(text, base + b.start) + (base ? 0 : 0) | |
| }; | |
| } | |
| base = base + b.innerStart; | |
| cur = b.inner; | |
| } | |
| return null; | |
| } | |
| function allBlocks(text, name) { | |
| var out = [], from = 0, b; | |
| while ((b = block(text, name, from))) { | |
| out.push(b); | |
| from = b.end; | |
| if (out.length > 5000) break; | |
| } | |
| return out; | |
| } | |
| function num(s) { | |
| if (s === null || s === undefined || s === '') return null; | |
| var v = parseFloat(String(s).replace(/[^0-9.\-]/g, '')); | |
| return isNaN(v) ? null : v; | |
| } | |
| function round2(v) { return Math.round(v * 100) / 100; } | |
| // ---------- document facts ---------- | |
| function readDoc(text) { | |
| var d = { text: text, syntax: null, root: null, line: {} }; | |
| var rootMatch = /<(?:[A-Za-z0-9_.-]+:)?(Invoice|CreditNote|CrossIndustryInvoice|Faktura)(\s|>)/.exec(text); | |
| if (rootMatch) { | |
| d.root = rootMatch[1]; | |
| d.syntax = RULES.syntaxes[d.root] || null; | |
| d.rootLine = lineAt(text, rootMatch.index); | |
| } | |
| var cii = d.root === 'CrossIndustryInvoice'; | |
| function g(chain) { return pick(text, 0, chain); } | |
| d.customization = cii | |
| ? g(['GuidelineSpecifiedDocumentContextParameter', 'ID']) | |
| : g(['CustomizationID']); | |
| d.sellerCountry = g(['AccountingSupplierParty', 'PostalAddress', 'IdentificationCode']) | |
| || g(['SellerTradeParty', 'PostalTradeAddress', 'CountryID']); | |
| d.buyerCountry = g(['AccountingCustomerParty', 'PostalAddress', 'IdentificationCode']) | |
| || g(['BuyerTradeParty', 'PostalTradeAddress', 'CountryID']); | |
| d.sellerLegalId = g(['AccountingSupplierParty', 'PartyLegalEntity', 'CompanyID']); | |
| d.buyerLegalId = g(['AccountingCustomerParty', 'PartyLegalEntity', 'CompanyID']); | |
| d.buyerReference = g(['BuyerReference']); | |
| var sup = block(text, 'AccountingSupplierParty'); | |
| d.sellerContact = sup ? { | |
| name: pick(sup.inner, 0, ['Contact', 'Name']), | |
| tel: pick(sup.inner, 0, ['Contact', 'Telephone']), | |
| mail: pick(sup.inner, 0, ['Contact', 'ElectronicMail']) | |
| } : { name: null, tel: null, mail: null }; | |
| d.sellerEndpoint = sup ? pick(sup.inner, 0, ['EndpointID']) : null; | |
| var cus = block(text, 'AccountingCustomerParty'); | |
| d.buyerEndpoint = cus ? pick(cus.inner, 0, ['EndpointID']) : null; | |
| d.paymentMeans = block(text, 'PaymentMeans'); | |
| d.dueDate = g(['DueDate']); | |
| d.paymentTerms = block(text, 'PaymentTerms'); | |
| d.lines = allBlocks(text, cii ? 'IncludedSupplyChainTradeLineItem' : 'InvoiceLine'); | |
| d.lineSum = 0; | |
| d.lines.forEach(function (l) { | |
| var a = pick(l.inner, 0, ['LineExtensionAmount']) || pick(l.inner, 0, ['LineTotalAmount']); | |
| var v = a ? num(a.value) : null; | |
| if (v !== null) d.lineSum += v; | |
| }); | |
| d.lineSum = round2(d.lineSum); | |
| var tot = block(text, 'LegalMonetaryTotal') || block(text, 'SpecifiedTradeSettlementHeaderMonetarySummation'); | |
| d.totals = {}; | |
| ['LineExtensionAmount', 'TaxExclusiveAmount', 'TaxInclusiveAmount', 'PayableAmount', | |
| 'PrepaidAmount', 'PayableRoundingAmount'].forEach(function (k) { | |
| var p = tot ? pick(tot.inner, 0, [k]) : null; | |
| d.totals[k] = p ? { value: num(p.value), line: tot ? lineAt(text, tot.innerStart + p.line) : 1 } : null; | |
| }); | |
| if (tot) { | |
| d.totalsLine = lineAt(text, tot.start); | |
| // re-resolve each amount's line against the whole document for accurate anchors | |
| ['LineExtensionAmount', 'TaxExclusiveAmount', 'TaxInclusiveAmount', 'PayableAmount', | |
| 'PrepaidAmount', 'PayableRoundingAmount'].forEach(function (k) { | |
| if (!d.totals[k]) return; | |
| var b = block(tot.inner, k); | |
| d.totals[k].line = lineAt(text, tot.innerStart + b.start); | |
| }); | |
| } else { | |
| d.totalsLine = 1; | |
| } | |
| var tax = block(text, 'TaxTotal'); | |
| var ta = tax ? pick(tax.inner, 0, ['TaxAmount']) : null; | |
| d.taxAmount = ta ? { value: num(ta.value), line: lineAt(text, tax.innerStart + block(tax.inner, 'TaxAmount').start) } : null; | |
| d.profile = null; | |
| if (d.customization && d.customization.value) { | |
| d.profile = RULES.profiles[d.customization.value.trim()] || null; | |
| } | |
| d.country = (d.sellerCountry && d.sellerCountry.value) | |
| ? d.sellerCountry.value.trim().toUpperCase() | |
| : (d.profile && d.profile.country && d.profile.country !== '*' ? d.profile.country : null); | |
| d.domestic = !!(d.country && d.buyerCountry && | |
| d.buyerCountry.value.trim().toUpperCase() === d.country); | |
| d.mandate = null; | |
| for (var i = 0; i < RULES.mandates.length; i++) { | |
| if (RULES.mandates[i].country === d.country) { d.mandate = RULES.mandates[i]; break; } | |
| } | |
| return d; | |
| } | |
| // ---------- checks ---------- | |
| function check(text, opts) { | |
| opts = opts || {}; | |
| var today = opts.today || '2026-09-11'; | |
| text = String(text || ''); | |
| var findings = []; | |
| var d = readDoc(text); | |
| function add(id, extra, line) { | |
| var r = byId[id]; | |
| if (!r) return; | |
| findings.push({ | |
| check: r.id, | |
| sev: r.sev, | |
| msg: r.msg + (extra ? ' - ' + extra : ''), | |
| line: line || 1, | |
| bt: r.bt, | |
| fix: r.fix | |
| }); | |
| } | |
| if (!d.syntax) { | |
| add('EINV-SYNTAX', d.root ? 'found <' + d.root + '>' : 'no invoice root element found', d.rootLine || 1); | |
| if (d.root === 'Faktura') { | |
| // Polish KSeF document: check the schema variant and stop. | |
| var fa = /wersja\s*=\s*"?(1|2|3)/.exec(text) || /FA\s*\(?\s*([23])\s*\)?/.exec(text); | |
| var isFa2 = /http:\/\/crd\.gov\.pl\/wzor\/2023\/06\/29\/12648/.test(text) || /"FA\(2\)"/.test(text); | |
| if (isFa2) add('EINV-PL-FA2', 'FA(3) is required since ' + mandateDate('PL'), 1); | |
| } | |
| return finish(findings, d, today); | |
| } | |
| // profile | |
| if (!d.customization || !d.customization.value) { | |
| add('EINV-BT-24', null, d.rootLine || 1); | |
| } else if (!d.profile) { | |
| add('EINV-PROFILE-UNKNOWN', '"' + d.customization.value + '"', d.customization.line); | |
| } else if (d.profile.status === 'retired') { | |
| add('EINV-PROFILE-RETIRED', | |
| d.profile.label + ' was retired on ' + d.profile.retired_on + '; use ' + d.profile.replaced_by, | |
| d.customization.line); | |
| } | |
| // presence rules | |
| RULES.rules.forEach(function (r) { | |
| if (r.kind !== 'present') return; | |
| var p = pick(text, 0, r.path); | |
| if (!p || !p.value) add(r.id, null, d.rootLine || 1); | |
| }); | |
| if (d.lines.length === 0) add('EINV-LINE', null, d.rootLine || 1); | |
| // arithmetic | |
| var T = d.totals; | |
| if (T.LineExtensionAmount && d.lines.length && | |
| round2(T.LineExtensionAmount.value) !== d.lineSum) { | |
| add('EINV-BR-CO-10', | |
| 'declared ' + T.LineExtensionAmount.value.toFixed(2) + ', lines add up to ' + d.lineSum.toFixed(2), | |
| T.LineExtensionAmount.line); | |
| } | |
| if (T.TaxExclusiveAmount && T.TaxInclusiveAmount && d.taxAmount) { | |
| var expect = round2(T.TaxExclusiveAmount.value + d.taxAmount.value); | |
| if (round2(T.TaxInclusiveAmount.value) !== expect) { | |
| add('EINV-BR-CO-15', | |
| 'declared ' + T.TaxInclusiveAmount.value.toFixed(2) + ', ' + | |
| T.TaxExclusiveAmount.value.toFixed(2) + ' + ' + d.taxAmount.value.toFixed(2) + | |
| ' = ' + expect.toFixed(2), | |
| T.TaxInclusiveAmount.line); | |
| } | |
| } | |
| if (T.PayableAmount && T.TaxInclusiveAmount) { | |
| var pre = T.PrepaidAmount ? T.PrepaidAmount.value : 0; | |
| var rnd = T.PayableRoundingAmount ? T.PayableRoundingAmount.value : 0; | |
| var due = round2(T.TaxInclusiveAmount.value - pre + rnd); | |
| if (round2(T.PayableAmount.value) !== due) { | |
| add('EINV-BR-CO-16', | |
| 'declared ' + T.PayableAmount.value.toFixed(2) + ', expected ' + due.toFixed(2), | |
| T.PayableAmount.line); | |
| } | |
| if (due > 0 && !d.dueDate && !d.paymentTerms) { | |
| add('EINV-BR-CO-25', due.toFixed(2) + ' is due', T.PayableAmount.line); | |
| } | |
| } | |
| // country mandate rules | |
| var c = d.country; | |
| var live = mandateLive(c, today); | |
| if (c === 'FR' && live) { | |
| if (!d.sellerLegalId || !/^\d{9}(\d{5})?$/.test(d.sellerLegalId.value.replace(/\s/g, ''))) { | |
| add('EINV-FR-SIREN', | |
| d.sellerLegalId ? 'found "' + d.sellerLegalId.value + '"' : 'no cac:PartyLegalEntity/cbc:CompanyID', | |
| d.sellerLegalId ? d.sellerLegalId.line : d.rootLine); | |
| } | |
| if (d.domestic && (!d.buyerLegalId || !/^\d{9}(\d{5})?$/.test(d.buyerLegalId.value.replace(/\s/g, '')))) { | |
| add('EINV-FR-BUYER-SIREN', | |
| d.buyerLegalId ? 'found "' + d.buyerLegalId.value + '"' : 'no buyer SIREN/SIRET', | |
| d.buyerLegalId ? d.buyerLegalId.line : d.rootLine); | |
| } | |
| if (d.profile && d.profile.status === 'insufficient') { | |
| add('EINV-FR-MINIMUM', d.profile.note, d.customization.line); | |
| } | |
| } | |
| var isXR = !!(d.customization && /xrechnung/i.test(d.customization.value)); | |
| if (c === 'DE' || isXR) { | |
| if (!d.buyerReference || !d.buyerReference.value) { | |
| add('EINV-DE-LEITWEG', 'BR-DE-15', d.rootLine); | |
| } | |
| var miss = []; | |
| if (!d.sellerContact.name) miss.push('cbc:Name'); | |
| if (!d.sellerContact.tel) miss.push('cbc:Telephone'); | |
| if (!d.sellerContact.mail) miss.push('cbc:ElectronicMail'); | |
| if (miss.length) add('EINV-DE-CONTACT', 'missing ' + miss.join(', '), d.rootLine); | |
| if (!d.paymentMeans) add('EINV-DE-PAYMENT', 'BR-DE-1', d.rootLine); | |
| } | |
| if (c === 'BE' && live) { | |
| if (!d.sellerEndpoint || !d.buyerEndpoint || | |
| !/schemeID/i.test((d.sellerEndpoint.attrs || '') + (d.buyerEndpoint.attrs || ''))) { | |
| add('EINV-BE-PEPPOL', | |
| (d.sellerEndpoint ? '' : 'seller ') + (d.buyerEndpoint ? '' : 'buyer ') + 'endpoint missing', | |
| d.rootLine); | |
| } | |
| if (d.customization && !/peppol/i.test(d.customization.value)) { | |
| add('EINV-BE-PROFILE', 'declares "' + d.customization.value + '"', d.customization.line); | |
| } | |
| } | |
| if (c === 'PL' && d.domestic && live) { | |
| add('EINV-PL-KSEF', 'FA(3) mandatory since ' + mandateDate('PL'), d.rootLine); | |
| } | |
| if (c === 'IT' && d.domestic) { | |
| add('EINV-IT-SDI', null, d.rootLine); | |
| } | |
| if (c === 'ES') { | |
| add('EINV-ES-VERIFACTU', null, d.rootLine); | |
| } | |
| return finish(findings, d, today); | |
| } | |
| function mandateDate(country) { | |
| for (var i = 0; i < RULES.mandates.length; i++) { | |
| if (RULES.mandates[i].country === country) return RULES.mandates[i].issue_from; | |
| } | |
| return ''; | |
| } | |
| function mandateLive(country, today) { | |
| if (!country) return false; | |
| for (var i = 0; i < RULES.mandates.length; i++) { | |
| if (RULES.mandates[i].country === country) return RULES.mandates[i].receive_from <= today; | |
| } | |
| return false; | |
| } | |
| function finish(findings, d, today) { | |
| if (d.mandate) { | |
| var r = byId['EINV-MANDATE-LIVE']; | |
| var live = d.mandate.receive_from <= today; | |
| findings.push({ | |
| check: r.id, | |
| sev: 'info', | |
| msg: d.mandate.name + ': receive from ' + d.mandate.receive_from + | |
| ', issue from ' + d.mandate.issue_from + | |
| ' (' + (live ? 'live as of ' + today : 'not yet in force on ' + today) + ')', | |
| line: 1, | |
| bt: '-', | |
| fix: d.mandate.note | |
| }); | |
| } | |
| findings.sort(function (a, b) { return (a.line - b.line) || a.check.localeCompare(b.check); }); | |
| return { findings: findings }; | |
| } | |
| var api = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.rules.length }; | |
| if (typeof module !== 'undefined' && module.exports) module.exports = api; | |
| if (typeof window !== 'undefined') window.EINVENGINE = api; | |
| })(); | |
| </script> | |
| <script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다. | |
| ; | |
| function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); } | |
| function summaryLine(res) { | |
| const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length; | |
| return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.'; | |
| } | |
| function toText(res, label) { | |
| const f = (res && res.findings) || []; | |
| const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)]; | |
| f.forEach(function (x) { lines.push(' ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + ' ' + (x.line ? 'L' + x.line + ' ' : '') + (x.check || x.id || '') + ' ' + (x.msg || x.message || '')); }); | |
| return lines.join('\n'); | |
| } | |
| function toHtml(res) { | |
| const f = (res && res.findings) || []; | |
| if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>'; | |
| const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join(''); | |
| return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>'; | |
| } | |
| var EINVREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml }; | |
| if (typeof module !== 'undefined' && module.exports) module.exports = EINVREPORT; | |
| else window.EINVREPORT = EINVREPORT; | |
| </script> | |
| <script> | |
| (function(){ | |
| var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today'); | |
| today.value=new Date().toISOString().slice(0,10); | |
| function run(){ try { var res=window.EINVENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.EINVREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } } | |
| go.onclick=run; if (src.value.trim()) run(); | |
| })(); | |
| </script> | |
| <section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://marketplace.visualstudio.com/items?itemName=ReadyStack.einvoice-mandate-lint" rel="noopener" data-yj-install-door="mp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'einvoice-mandate-lint',src:'mp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">VS Code Marketplace</a><a href="https://open-vsx.org/extension/readystack/einvoice-mandate-lint" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'einvoice-mandate-lint',src:'ovsx',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><a href="https://www.npmjs.com/package/@readystack/einvoice-mandate-lint" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'einvoice-mandate-lint',src:'npm',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">npm</a><a href="https://www.npmjs.com/package/@readystack/einvoice-mandate-lint" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'einvoice-mandate-lint',src:'mcp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">MCP server (npx)</a><a href="https://hub.docker.com/r/getreadystack/einvoice-mandate-lint" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'einvoice-mandate-lint',src:'docker',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Docker Hub</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">ext install ReadyStack.einvoice-mandate-lint | |
| npx @readystack/einvoice-mandate-lint <file> | |
| npx @readystack/einvoice-mandate-lint --mcp</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/cl/polar_cl_l6iN1uWt0FwWu7tBsczD0jWpP2vxFM54Wdwqb3KPi1G" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section> | |
| <section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get one email when this rule changes</b><div style="opacity:.78;margin:.35rem 0 .8rem">We watch the regulation and vendor sources behind E-Invoice Mandate Lint - EU 2026 every day. When a rule changes, you get a single email with what changed and the updated check. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Notify me</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">You're on the list. One email when it changes.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"einvoice-mandate-lint",lang:"en",source:"rule_alert"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);})();</script></section> | |
| <section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">Lints UBL and CII invoice XML against EN 16931 and the national e-invoicing mandates that are live in 2026</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">An EN 16931 / Peppol integration consultant reviews an invoice mapping at about $170 an hour.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_V1RBWcrS00NsYLx6EMXeji1xvbix6LF1yO9qR4DduBz" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section> | |
| <!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&","<":"<",">":">",'"':"""}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div> | |
| <script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script> | |
| <script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script> | |
| <section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-12">Updated 2026-09-12</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-12","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku40981_result_card.jpg" alt="E-Invoice Mandate Lint - EU 2026" loading="lazy" style="max-width:100%;border-radius:6px"></p> | |
| <p>Nine errors and one warning, in a French B2B invoice that a Paris billing developer's XSD validator had called valid ten seconds earlier.</p> | |
| <p>The file was ordinary. A French seller, a French buyer, two service lines, EUR 6,000.00 payable. It came out of a mapping written two years ago for a German customer, and nobody had looked at the specification identifier since.</p> | |
| <p>Since 1 September 2026 that matters. Every VAT-registered business in France must be able to receive structured e-invoices, and large and mid-sized companies must issue them; SMEs and micro-enterprises follow on 1 September 2027. The invoice no longer travels to the buyer. It travels through an approved platform, and the platform accepts the whole document or rejects the whole document. There is no partially valid invoice, and a rejected invoice is an unpaid invoice.</p> | |
| <p>Here is what 35 rules found in that one file.</p> | |
| <p>The specification identifier still said xrechnung_2.3. XRechnung 2.3 was retired on 6 February 2025 and replaced by 3.0. The seller's legal registration identifier read "FR 732 829 320" - a SIREN is nine digits and a SIRET is fourteen, with no spaces and no country prefix. The buyer's PartyLegalEntity had no CompanyID at all, which a domestic French B2B invoice needs.</p> | |
| <p>Then the arithmetic. LineExtensionAmount was declared as 4800.00 while the two lines add up to 5000.00 (BR-CO-10). TaxInclusiveAmount was declared as 5900.00 while 5000.00 + 1000.00 = 6000.00 (BR-CO-15). PayableAmount was declared as 6000.00 while the amount due recomputed from the totals is 5900.00 (BR-CO-16).</p> | |
| <p>Because the file declared an XRechnung profile, the German CIUS rules applied as well: no BuyerReference, so no Leitweg-ID (BR-DE-15); no Contact block under the supplier party; no PaymentMeans. The single warning: 5900.00 is due and the file gives neither a due date (BT-9) nor payment terms (BT-20).</p> | |
| <p>Why does a chatbot miss this? Because the timeline moved underneath it. Poland replaced the FA(2) structure with FA(3) on 1 February 2026 and made KSeF mandatory for every remaining VAT payer on 1 April 2026. Belgium's domestic B2B mandate went live on 1 January 2026 and its tolerance period ended on 31 March 2026. And Spain's Verifactu, which nearly every article written in 2025 places on 1 January 2026, was postponed by a full year - corporate income tax payers from 1 January 2027, everyone else from 1 July 2027. Ask a general model in September 2026 and you will often get the 2026 date, because that is what most of the text it read says.</p> | |
| <p>A rule that is confident and out of date is worse than no rule, so the mandate dates and the profile identifiers live in rules.json as data rather than code. You can read exactly what the linter believes about France, Germany, Belgium, Poland, Spain and Italy before you trust a single finding, and you can see which rules stayed silent because their date has not arrived.</p> | |
| <p>Run the same linter on a corrected copy and it returns zero errors and one informational line stating the mandate dates it was measured against.</p> | |
| <p>Linting the file you have open is free and complete - all 35 rules, all six mandates, every finding shown, no watermark and no trial counter. It runs in VS Code and in the browser from the same engine file. The licence key is for a different job: scanning every invoice XML in a workspace in one pass and exporting the findings as JSON, CSV or SARIF that you keep and run in CI.</p> | |
| <p>An EN 16931 / Peppol integration consultant reviews an invoice mapping at about $170 an hour.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku40981_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku40981_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does E-Invoice Mandate Lint actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads a UBL 2.1 or UN/CEFACT CII invoice XML file, works out which national mandate applies from the CustomizationID and the seller country, and reports every missing business term, retired profile version and broken total against 35 rules, each anchored to the line it breaks on.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is it for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Billing and ERP developers who emit invoices into France's Plateforme Agreee network, Belgium's Peppol B2B channel, Germany's XRechnung, or Poland's KSeF. If you own the mapping code that turns an order into an invoice XML, this checks your output before a national platform does.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why is a free XSD validator or a chatbot not enough?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">An XSD validator only proves the shape is legal UBL; it passes files that France or Belgium reject. Chatbots answer from the old timeline - they still quote retired XRechnung 2.3, treat Poland's FA(2) as current, and place Spain's Verifactu in 2026 after it moved to 2027.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what needs a licence key?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Linting the file you have open is free and complete: all 35 rules, all six country mandates, no watermark, no trial limit, no locked findings. The licence key unlocks a different job - scanning every invoice XML in the workspace at once and exporting the findings as JSON, CSV or SARIF.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What would this cost as a person?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">An EN 16931 or Peppol integration consultant reviews an invoice mapping at roughly $170 an hour, and a mandate review of one country profile is rarely under half a day. The extension is $29 once, and the file-level lint is free forever.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just ask ChatGPT or another AI chat?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. E-Invoice Mandate Lint - EU 2026 checks the file you open against 35 rules from a rule set dated 2026-09-20, and points at the exact line with the fix. For a filing, an audit or a client you need that dated result on your own files.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does E-Invoice Mandate Lint actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads a UBL 2.1 or UN/CEFACT CII invoice XML file, works out which national mandate applies from the CustomizationID and the seller country, and reports every missing business term, retired profile version and broken total against 35 rules, each anchored to the line it breaks on."}}, {"@type": "Question", "name": "Who is it for?", "acceptedAnswer": {"@type": "Answer", "text": "Billing and ERP developers who emit invoices into France's Plateforme Agreee network, Belgium's Peppol B2B channel, Germany's XRechnung, or Poland's KSeF. If you own the mapping code that turns an order into an invoice XML, this checks your output before a national platform does."}}, {"@type": "Question", "name": "Why is a free XSD validator or a chatbot not enough?", "acceptedAnswer": {"@type": "Answer", "text": "An XSD validator only proves the shape is legal UBL; it passes files that France or Belgium reject. Chatbots answer from the old timeline - they still quote retired XRechnung 2.3, treat Poland's FA(2) as current, and place Spain's Verifactu in 2026 after it moved to 2027."}}, {"@type": "Question", "name": "What is free and what needs a licence key?", "acceptedAnswer": {"@type": "Answer", "text": "Linting the file you have open is free and complete: all 35 rules, all six country mandates, no watermark, no trial limit, no locked findings. The licence key unlocks a different job - scanning every invoice XML in the workspace at once and exporting the findings as JSON, CSV or SARIF."}}, {"@type": "Question", "name": "What would this cost as a person?", "acceptedAnswer": {"@type": "Answer", "text": "An EN 16931 or Peppol integration consultant reviews an invoice mapping at roughly $170 an hour, and a mandate review of one country profile is rarely under half a day. The extension is $29 once, and the file-level lint is free forever."}}, {"@type": "Question", "name": "Why not just ask ChatGPT or another AI chat?", "acceptedAnswer": {"@type": "Answer", "text": "A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. E-Invoice Mandate Lint - EU 2026 checks the file you open against 35 rules from a rule set dated 2026-09-20, and points at the exact line with the fix. For a filing, an audit or a client you need that dated result on your own files."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:40981,slug:"einvoice-mandate-lint",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/en16931-einvoice-lint"><span class="yj-rel-t">EN 16931 e-Invoice Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/france-einvoice-reception-lint"><span class="yj-rel-t">France E-Invoice Reception Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/myinvois-field-check"><span class="yj-rel-t">MyInvois Field Check — Malaysia e-Invoice (UBL 2.1)</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/belgium-einvoice-fine-vs-deduction-2026"><span class="yj-rel-t">Belgium E-Invoice Fine & 120% Check 2026</span><span class="yj-rel-p">$60</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/dashboard-ui-kit-eu-vat-billing"><span class="yj-rel-t">Dashboard UI Kit for EU SaaS Billing — VAT ID, Reverse Charge & OSS States (Fi</span><span class="yj-rel-p">$168</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/saas-dashboard-ui-kit-cancel-button"><span class="yj-rel-t">SaaS Dashboard UI Kit — Cancel Button §312k BGB & Billing Screens (Figma)</span><span class="yj-rel-p">$168</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body> | |
| </html> | |