@@ -34,14 +34,35 @@ function taxCategoryId(rate: number): string {
3434 return rate > 0 ? "S" : "Z" ;
3535}
3636
37- function splitAddressLines ( address ?: string ) : { lineOne ?: string ; lineTwo ?: string } {
37+ function splitAddressLines ( address ?: string ) : { lineOne ?: string ; lineTwo ?: string ; city ?: string ; zip ?: string } {
3838 if ( ! address ) return { } ;
3939 const parts = address . split ( / \r ? \n / ) . map ( ( part ) => part . trim ( ) ) . filter ( ( part ) => part . length > 0 ) ;
4040 if ( parts . length === 0 ) return { } ;
4141 const [ lineOne , ...rest ] = parts ;
42+ let lineTwo = rest . length ? rest . join ( ", " ) : undefined ;
43+
44+ // Simple heuristic: if lineTwo looks like "12345 City", try to extract it
45+ // This is a fallback if structured city/zip are missing
46+ let city : string | undefined ;
47+ let zip : string | undefined ;
48+
49+ if ( lineTwo && / ^ \d { 4 , 5 } \s + / . test ( lineTwo ) ) {
50+ const m = lineTwo . match ( / ^ ( \d { 4 , 5 } ) \s + ( .* ) $ / ) ;
51+ if ( m ) {
52+ zip = m [ 1 ] ;
53+ city = m [ 2 ] ;
54+ }
55+ } else if ( lineOne && / ^ \d { 4 , 5 } \s + / . test ( lineOne ) ) {
56+ // Sometimes address is just one line: "Street 1, 12345 City"
57+ // But here we split by newline. If lineOne is "12345 City", it's weird.
58+ // Let's just leave it for now.
59+ }
60+
4261 return {
4362 lineOne,
44- lineTwo : rest . length ? rest . join ( ", " ) : undefined ,
63+ lineTwo,
64+ city,
65+ zip,
4566 } ;
4667}
4768
@@ -67,8 +88,9 @@ export function generateFacturX22XML(
6788 const currency = safeCurrency ( business , invoice . currency ) ;
6889 const issue = fmtDateYYYYMMDD ( invoice . issueDate ) || fmtDateYYYYMMDD ( new Date ( ) ) || "" ;
6990
70- // BASIC profile - minimal yet compliant
71- const profileUrn = "urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic" ;
91+ // EN16931 (COMFORT) profile - required for XRechnung compliance
92+ // EN16931 (COMFORT) profile - required for XRechnung compliance
93+ const profileUrn = "urn:cen.eu:en16931:2017" ;
7294
7395 // Tax summary
7496 const taxes = ( invoice . taxes && invoice . taxes . length > 0 )
@@ -81,6 +103,9 @@ export function generateFacturX22XML(
81103
82104 const docContext = `
83105 <rsm:ExchangedDocumentContext>
106+ <ram:BusinessProcessSpecifiedDocumentContextParameter>
107+ <ram:ID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</ram:ID>
108+ </ram:BusinessProcessSpecifiedDocumentContextParameter>
84109 <ram:GuidelineSpecifiedDocumentContextParameter>
85110 <ram:ID>${ xmlEscape ( profileUrn ) } </ram:ID>
86111 </ram:GuidelineSpecifiedDocumentContextParameter>
@@ -99,11 +124,25 @@ export function generateFacturX22XML(
99124 const sellerParty = `
100125 <ram:SellerTradeParty>
101126 <ram:Name>${ xmlEscape ( business . companyName ) } </ram:Name>
127+ <ram:DefinedTradeContact>
128+ <ram:PersonName>${ xmlEscape ( business . companyName ) } </ram:PersonName>
129+ <ram:TelephoneUniversalCommunication>
130+ <ram:CompleteNumber>${ xmlEscape ( business . companyPhone || "+49 000 000000" ) } </ram:CompleteNumber>
131+ </ram:TelephoneUniversalCommunication>
132+ <ram:EmailURIUniversalCommunication>
133+ <ram:URIID>${ xmlEscape ( business . companyEmail || "[email protected] " ) } </ram:URIID> 134+ </ram:EmailURIUniversalCommunication>
135+ </ram:DefinedTradeContact>
102136 <ram:PostalTradeAddress>
137+ ${ ( business . companyPostalCode || sellerAddressLines . zip ) ? `<ram:PostcodeCode>${ xmlEscape ( business . companyPostalCode || sellerAddressLines . zip ) } </ram:PostcodeCode>` : "" }
103138 ${ sellerAddressLines . lineOne ? `<ram:LineOne>${ xmlEscape ( sellerAddressLines . lineOne ) } </ram:LineOne>` : "" }
104139 ${ sellerAddressLines . lineTwo ? `<ram:LineTwo>${ xmlEscape ( sellerAddressLines . lineTwo ) } </ram:LineTwo>` : "" }
140+ ${ ( business . companyCity || sellerAddressLines . city ) ? `<ram:CityName>${ xmlEscape ( business . companyCity || sellerAddressLines . city ) } </ram:CityName>` : "" }
105141 <ram:CountryID>${ xmlEscape ( business . companyCountryCode || opts . sellerCountryCode || "DE" ) } </ram:CountryID>
106142 </ram:PostalTradeAddress>
143+ <ram:URIUniversalCommunication>
144+ <ram:URIID schemeID="0088">${ xmlEscape ( business . companyTaxId || "0000000000000" ) } </ram:URIID>
145+ </ram:URIUniversalCommunication>
107146 ${ isLikelyVatId ( business . companyTaxId ) ? `
108147 <ram:SpecifiedTaxRegistration>
109148 <ram:ID schemeID="VA">${ xmlEscape ( ( business . companyCountryCode || opts . sellerCountryCode || "DE" ) + business . companyTaxId ! ) } </ram:ID>
@@ -117,12 +156,15 @@ export function generateFacturX22XML(
117156 <ram:BuyerTradeParty>
118157 <ram:Name>${ xmlEscape ( buyer . name ) } </ram:Name>
119158 <ram:PostalTradeAddress>
159+ ${ ( buyer . postalCode || buyerAddressLines . zip ) ? `<ram:PostcodeCode>${ xmlEscape ( buyer . postalCode || buyerAddressLines . zip ) } </ram:PostcodeCode>` : "" }
120160 ${ buyerAddressLines . lineOne ? `<ram:LineOne>${ xmlEscape ( buyerAddressLines . lineOne ) } </ram:LineOne>` : "" }
121161 ${ buyerAddressLines . lineTwo ? `<ram:LineTwo>${ xmlEscape ( buyerAddressLines . lineTwo ) } </ram:LineTwo>` : "" }
122- ${ buyer . postalCode ? `<ram:PostcodeCode>${ xmlEscape ( buyer . postalCode ) } </ram:PostcodeCode>` : "" }
123- ${ buyer . city ? `<ram:CityName>${ xmlEscape ( buyer . city ) } </ram:CityName>` : "" }
162+ ${ ( buyer . city || buyerAddressLines . city ) ? `<ram:CityName>${ xmlEscape ( buyer . city || buyerAddressLines . city ) } </ram:CityName>` : "" }
124163 <ram:CountryID>${ xmlEscape ( buyer . countryCode || opts . buyerCountryCode || "DE" ) } </ram:CountryID>
125164 </ram:PostalTradeAddress>
165+ <ram:URIUniversalCommunication>
166+ <ram:URIID schemeID="0088">${ xmlEscape ( buyer . taxId || "0000000000000" ) } </ram:URIID>
167+ </ram:URIUniversalCommunication>
126168 ${ isLikelyVatId ( buyer . taxId ) ? `
127169 <ram:SpecifiedTaxRegistration>
128170 <ram:ID schemeID="VA">${ xmlEscape ( ( buyer . countryCode || opts . buyerCountryCode || "DE" ) + buyer . taxId ! ) } </ram:ID>
@@ -131,9 +173,9 @@ export function generateFacturX22XML(
131173
132174 const headerAgreement = `
133175 <ram:ApplicableHeaderTradeAgreement>
176+ <ram:BuyerReference>${ xmlEscape ( invoice . customer . reference || "N/A" ) } </ram:BuyerReference>
134177 ${ sellerParty }
135178 ${ buyerParty }
136- ${ invoice . customer . reference ? `<ram:BuyerReference>${ xmlEscape ( invoice . customer . reference ) } </ram:BuyerReference>` : "" }
137179 ${ opts . orderReferenceId ? `<ram:BuyerOrderReferencedDocument><ram:IssuerAssignedID>${ xmlEscape ( opts . orderReferenceId ) } </ram:IssuerAssignedID></ram:BuyerOrderReferencedDocument>` : "" }
138180 </ram:ApplicableHeaderTradeAgreement>` ;
139181
@@ -210,7 +252,11 @@ export function generateFacturX22XML(
210252 <rsm:SupplyChainTradeTransaction>
211253 ${ linesXml }
212254 ${ headerAgreement }
213- <ram:ApplicableHeaderTradeDelivery />
255+ <ram:ApplicableHeaderTradeDelivery>
256+ <ram:ActualDeliverySupplyChainEvent>
257+ <ram:OccurrenceDateTime><udt:DateTimeString format="102">${ issue } </udt:DateTimeString></ram:OccurrenceDateTime>
258+ </ram:ActualDeliverySupplyChainEvent>
259+ </ram:ApplicableHeaderTradeDelivery>
214260 ${ headerSettlement }
215261 </rsm:SupplyChainTradeTransaction>` ;
216262
0 commit comments