@fast-white-cat/integration-ksef-direct 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/integration_ksef_direct/acl.js +13 -0
- package/dist/modules/integration_ksef_direct/acl.js.map +7 -0
- package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/documents/[id].js +92 -0
- package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/documents/[id].js.map +7 -0
- package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/documents.js +105 -0
- package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/documents.js.map +7 -0
- package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/health.js +158 -0
- package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/health.js.map +7 -0
- package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/received-documents/[id].js +86 -0
- package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/received-documents/[id].js.map +7 -0
- package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/received-documents.js +112 -0
- package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/received-documents.js.map +7 -0
- package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/seller-info.js +54 -0
- package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/seller-info.js.map +7 -0
- package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/documents/[id]/send.js +64 -0
- package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/documents/[id]/send.js.map +7 -0
- package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/documents.js +104 -0
- package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/documents.js.map +7 -0
- package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/invoice-numbers.js +41 -0
- package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/invoice-numbers.js.map +7 -0
- package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/received-documents/fetch.js +172 -0
- package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/received-documents/fetch.js.map +7 -0
- package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/received-documents/sync.js +80 -0
- package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/received-documents/sync.js.map +7 -0
- package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/new/page.js +441 -0
- package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/new/page.js.map +7 -0
- package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/new/page.meta.js +8 -0
- package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/new/page.meta.js.map +7 -0
- package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/page.js +193 -0
- package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/page.js.map +7 -0
- package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/received-documents/page.js +314 -0
- package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/received-documents/page.js.map +7 -0
- package/dist/modules/integration_ksef_direct/backend/page.js +154 -0
- package/dist/modules/integration_ksef_direct/backend/page.js.map +7 -0
- package/dist/modules/integration_ksef_direct/commands/create-ksef-direct-document.js +80 -0
- package/dist/modules/integration_ksef_direct/commands/create-ksef-direct-document.js.map +7 -0
- package/dist/modules/integration_ksef_direct/commands/enqueue-ksef-direct-document.js +43 -0
- package/dist/modules/integration_ksef_direct/commands/enqueue-ksef-direct-document.js.map +7 -0
- package/dist/modules/integration_ksef_direct/data/entities.js +224 -0
- package/dist/modules/integration_ksef_direct/data/entities.js.map +7 -0
- package/dist/modules/integration_ksef_direct/data/validators.js +103 -0
- package/dist/modules/integration_ksef_direct/data/validators.js.map +7 -0
- package/dist/modules/integration_ksef_direct/di.js +11 -0
- package/dist/modules/integration_ksef_direct/di.js.map +7 -0
- package/dist/modules/integration_ksef_direct/events.js +21 -0
- package/dist/modules/integration_ksef_direct/events.js.map +7 -0
- package/dist/modules/integration_ksef_direct/index.js +10 -0
- package/dist/modules/integration_ksef_direct/index.js.map +7 -0
- package/dist/modules/integration_ksef_direct/integration.js +56 -0
- package/dist/modules/integration_ksef_direct/integration.js.map +7 -0
- package/dist/modules/integration_ksef_direct/lib/health.js +32 -0
- package/dist/modules/integration_ksef_direct/lib/health.js.map +7 -0
- package/dist/modules/integration_ksef_direct/lib/invoiceNumberFormat.js +23 -0
- package/dist/modules/integration_ksef_direct/lib/invoiceNumberFormat.js.map +7 -0
- package/dist/modules/integration_ksef_direct/lib/ksefClient.js +523 -0
- package/dist/modules/integration_ksef_direct/lib/ksefClient.js.map +7 -0
- package/dist/modules/integration_ksef_direct/lib/ksefCrypto.js +103 -0
- package/dist/modules/integration_ksef_direct/lib/ksefCrypto.js.map +7 -0
- package/dist/modules/integration_ksef_direct/lib/ksefFa2Xml.js +123 -0
- package/dist/modules/integration_ksef_direct/lib/ksefFa2Xml.js.map +7 -0
- package/dist/modules/integration_ksef_direct/lib/ksefXmlParser.js +76 -0
- package/dist/modules/integration_ksef_direct/lib/ksefXmlParser.js.map +7 -0
- package/dist/modules/integration_ksef_direct/migrations/Migration20260519210000_integration_ksef_direct.js +15 -0
- package/dist/modules/integration_ksef_direct/migrations/Migration20260519210000_integration_ksef_direct.js.map +7 -0
- package/dist/modules/integration_ksef_direct/migrations/Migration20260520120000_ksef_direct_documents.js +17 -0
- package/dist/modules/integration_ksef_direct/migrations/Migration20260520120000_ksef_direct_documents.js.map +7 -0
- package/dist/modules/integration_ksef_direct/migrations/Migration20260520220000_ksef_direct_send_queue.js +15 -0
- package/dist/modules/integration_ksef_direct/migrations/Migration20260520220000_ksef_direct_send_queue.js.map +7 -0
- package/dist/modules/integration_ksef_direct/migrations/Migration20260520230000_ksef_direct_seller_per_document.js +17 -0
- package/dist/modules/integration_ksef_direct/migrations/Migration20260520230000_ksef_direct_seller_per_document.js.map +7 -0
- package/dist/modules/integration_ksef_direct/migrations/Migration20260521120000_ksef_direct_received_documents.js +16 -0
- package/dist/modules/integration_ksef_direct/migrations/Migration20260521120000_ksef_direct_received_documents.js.map +7 -0
- package/dist/modules/integration_ksef_direct/migrations/Migration20260521130000_ksef_received_download_urls.js +15 -0
- package/dist/modules/integration_ksef_direct/migrations/Migration20260521130000_ksef_received_download_urls.js.map +7 -0
- package/dist/modules/integration_ksef_direct/setup.js +11 -0
- package/dist/modules/integration_ksef_direct/setup.js.map +7 -0
- package/dist/modules/integration_ksef_direct/subscribers/auto-enqueue-ksef-document.js +19 -0
- package/dist/modules/integration_ksef_direct/subscribers/auto-enqueue-ksef-document.js.map +7 -0
- package/dist/modules/integration_ksef_direct/workers/check-ksef-document-status.js +103 -0
- package/dist/modules/integration_ksef_direct/workers/check-ksef-document-status.js.map +7 -0
- package/dist/modules/integration_ksef_direct/workers/send-ksef-document.js +104 -0
- package/dist/modules/integration_ksef_direct/workers/send-ksef-document.js.map +7 -0
- package/dist/modules/integration_ksef_direct/workers/sync-received-documents.js +137 -0
- package/dist/modules/integration_ksef_direct/workers/sync-received-documents.js.map +7 -0
- package/dist/types/declarations.d.js +1 -0
- package/dist/types/declarations.d.js.map +7 -0
- package/package.json +98 -0
- package/src/index.ts +1 -0
- package/src/modules/integration_ksef_direct/__tests__/invoiceNumberFormat.test.ts +42 -0
- package/src/modules/integration_ksef_direct/__tests__/ksefFa2Xml.test.ts +407 -0
- package/src/modules/integration_ksef_direct/__tests__/ksefXmlParser.test.ts +230 -0
- package/src/modules/integration_ksef_direct/acl.ts +9 -0
- package/src/modules/integration_ksef_direct/api/get/integration-ksef-direct/documents/[id].ts +94 -0
- package/src/modules/integration_ksef_direct/api/get/integration-ksef-direct/documents.ts +111 -0
- package/src/modules/integration_ksef_direct/api/get/integration-ksef-direct/health.ts +194 -0
- package/src/modules/integration_ksef_direct/api/get/integration-ksef-direct/received-documents/[id].ts +88 -0
- package/src/modules/integration_ksef_direct/api/get/integration-ksef-direct/received-documents.ts +119 -0
- package/src/modules/integration_ksef_direct/api/get/integration-ksef-direct/seller-info.ts +62 -0
- package/src/modules/integration_ksef_direct/api/post/integration-ksef-direct/documents/[id]/send.ts +64 -0
- package/src/modules/integration_ksef_direct/api/post/integration-ksef-direct/documents.ts +109 -0
- package/src/modules/integration_ksef_direct/api/post/integration-ksef-direct/invoice-numbers.ts +40 -0
- package/src/modules/integration_ksef_direct/api/post/integration-ksef-direct/received-documents/fetch.ts +185 -0
- package/src/modules/integration_ksef_direct/api/post/integration-ksef-direct/received-documents/sync.ts +86 -0
- package/src/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/new/page.meta.ts +4 -0
- package/src/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/new/page.tsx +470 -0
- package/src/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/page.tsx +233 -0
- package/src/modules/integration_ksef_direct/backend/integration-ksef-direct/received-documents/page.tsx +415 -0
- package/src/modules/integration_ksef_direct/backend/page.tsx +183 -0
- package/src/modules/integration_ksef_direct/commands/create-ksef-direct-document.ts +93 -0
- package/src/modules/integration_ksef_direct/commands/enqueue-ksef-direct-document.ts +57 -0
- package/src/modules/integration_ksef_direct/data/entities.ts +195 -0
- package/src/modules/integration_ksef_direct/data/validators.ts +115 -0
- package/src/modules/integration_ksef_direct/di.ts +9 -0
- package/src/modules/integration_ksef_direct/events.ts +18 -0
- package/src/modules/integration_ksef_direct/i18n/en.json +115 -0
- package/src/modules/integration_ksef_direct/i18n/pl.json +115 -0
- package/src/modules/integration_ksef_direct/index.ts +6 -0
- package/src/modules/integration_ksef_direct/integration.ts +54 -0
- package/src/modules/integration_ksef_direct/lib/health.ts +43 -0
- package/src/modules/integration_ksef_direct/lib/invoiceNumberFormat.ts +23 -0
- package/src/modules/integration_ksef_direct/lib/ksefClient.ts +668 -0
- package/src/modules/integration_ksef_direct/lib/ksefCrypto.ts +138 -0
- package/src/modules/integration_ksef_direct/lib/ksefFa2Xml.ts +147 -0
- package/src/modules/integration_ksef_direct/lib/ksefXmlParser.ts +97 -0
- package/src/modules/integration_ksef_direct/migrations/.snapshot-open-mercato.json +1028 -0
- package/src/modules/integration_ksef_direct/migrations/Migration20260519210000_integration_ksef_direct.ts +15 -0
- package/src/modules/integration_ksef_direct/migrations/Migration20260520120000_ksef_direct_documents.ts +17 -0
- package/src/modules/integration_ksef_direct/migrations/Migration20260520220000_ksef_direct_send_queue.ts +15 -0
- package/src/modules/integration_ksef_direct/migrations/Migration20260520230000_ksef_direct_seller_per_document.ts +17 -0
- package/src/modules/integration_ksef_direct/migrations/Migration20260521120000_ksef_direct_received_documents.ts +16 -0
- package/src/modules/integration_ksef_direct/migrations/Migration20260521130000_ksef_received_download_urls.ts +15 -0
- package/src/modules/integration_ksef_direct/setup.ts +9 -0
- package/src/modules/integration_ksef_direct/subscribers/auto-enqueue-ksef-document.ts +21 -0
- package/src/modules/integration_ksef_direct/workers/check-ksef-document-status.ts +129 -0
- package/src/modules/integration_ksef_direct/workers/send-ksef-document.ts +137 -0
- package/src/modules/integration_ksef_direct/workers/sync-received-documents.ts +171 -0
- package/src/types/declarations.d.ts +1 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
type KsefEnvironment = 'test' | 'production'
|
|
4
|
+
|
|
5
|
+
const BASE_URLS: Record<KsefEnvironment, string> = {
|
|
6
|
+
test: 'https://api-test.ksef.mf.gov.pl/v2',
|
|
7
|
+
production: 'https://api.ksef.mf.gov.pl/v2',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface PublicKeyCacheEntry {
|
|
11
|
+
publicKeyPem: string
|
|
12
|
+
fetchedAt: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const PUBLIC_KEY_CACHE = new Map<KsefEnvironment, PublicKeyCacheEntry>()
|
|
16
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000
|
|
17
|
+
|
|
18
|
+
interface PublicKeyCertificateEntry {
|
|
19
|
+
certificate: string
|
|
20
|
+
certificateId: string
|
|
21
|
+
publicKeyId?: string
|
|
22
|
+
validFrom?: string
|
|
23
|
+
validTo?: string
|
|
24
|
+
usage?: string[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function fetchCertificates(environment: KsefEnvironment): Promise<PublicKeyCertificateEntry[]> {
|
|
28
|
+
const url = `${BASE_URLS[environment]}/security/public-key-certificates`
|
|
29
|
+
const response = await fetch(url, {
|
|
30
|
+
method: 'GET',
|
|
31
|
+
headers: { 'Accept': 'application/json' },
|
|
32
|
+
signal: AbortSignal.timeout(10_000),
|
|
33
|
+
})
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
throw new Error(`Failed to fetch KSeF public key certificates: HTTP ${response.status}`)
|
|
36
|
+
}
|
|
37
|
+
const data = await response.json() as PublicKeyCertificateEntry[]
|
|
38
|
+
if (!Array.isArray(data)) {
|
|
39
|
+
throw new Error('KSeF public key response is not an array')
|
|
40
|
+
}
|
|
41
|
+
return data
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function certToPem(entry: PublicKeyCertificateEntry): string {
|
|
45
|
+
const derBuffer = Buffer.from(entry.certificate, 'base64')
|
|
46
|
+
const x509 = new crypto.X509Certificate(derBuffer)
|
|
47
|
+
return x509.publicKey.export({ type: 'spki', format: 'pem' }).toString()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function fetchPublicKey(environment: KsefEnvironment): Promise<string> {
|
|
51
|
+
const cached = PUBLIC_KEY_CACHE.get(environment)
|
|
52
|
+
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
|
|
53
|
+
return cached.publicKeyPem
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const certs = await fetchCertificates(environment)
|
|
57
|
+
const tokenCert = certs.find(entry => entry.usage?.includes('KsefTokenEncryption'))
|
|
58
|
+
if (!tokenCert?.certificate) {
|
|
59
|
+
throw new Error('KSeF public key response missing KsefTokenEncryption certificate')
|
|
60
|
+
}
|
|
61
|
+
const publicKeyPem = certToPem(tokenCert)
|
|
62
|
+
PUBLIC_KEY_CACHE.set(environment, { publicKeyPem, fetchedAt: Date.now() })
|
|
63
|
+
return publicKeyPem
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function fetchInvoicePublicKey(
|
|
67
|
+
environment: KsefEnvironment,
|
|
68
|
+
): Promise<{ publicKeyPem: string; publicKeyId?: string }> {
|
|
69
|
+
const certs = await fetchCertificates(environment)
|
|
70
|
+
const invoiceCert = certs.find(entry => entry.usage?.includes('SymmetricKeyEncryption'))
|
|
71
|
+
?? certs.find(entry => entry.usage?.includes('KsefTokenEncryption'))
|
|
72
|
+
if (!invoiceCert?.certificate) {
|
|
73
|
+
throw new Error('KSeF public key response missing invoice encryption certificate')
|
|
74
|
+
}
|
|
75
|
+
return { publicKeyPem: certToPem(invoiceCert), publicKeyId: invoiceCert.publicKeyId }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function clearPublicKeyCache(environment?: KsefEnvironment): void {
|
|
79
|
+
if (environment) {
|
|
80
|
+
PUBLIC_KEY_CACHE.delete(environment)
|
|
81
|
+
} else {
|
|
82
|
+
PUBLIC_KEY_CACHE.clear()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function encryptKsefToken(token: string, timestampMs: number, publicKeyPem: string): string {
|
|
87
|
+
const plaintext = `${token}|${timestampMs}`
|
|
88
|
+
const encrypted = crypto.publicEncrypt(
|
|
89
|
+
{
|
|
90
|
+
key: publicKeyPem,
|
|
91
|
+
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
|
|
92
|
+
oaepHash: 'sha256',
|
|
93
|
+
},
|
|
94
|
+
Buffer.from(plaintext, 'utf-8'),
|
|
95
|
+
)
|
|
96
|
+
return encrypted.toString('base64')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function generateSymmetricKey(): { key: Buffer; iv: Buffer } {
|
|
100
|
+
return {
|
|
101
|
+
key: crypto.randomBytes(32),
|
|
102
|
+
iv: crypto.randomBytes(16),
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function prepareInvoicePayload(
|
|
107
|
+
xmlString: string,
|
|
108
|
+
publicKeyPem: string,
|
|
109
|
+
): {
|
|
110
|
+
encryptedSymmetricKey: string
|
|
111
|
+
initializationVector: string
|
|
112
|
+
encryptedInvoiceContent: string
|
|
113
|
+
invoiceHash: string
|
|
114
|
+
invoiceSize: number
|
|
115
|
+
encryptedInvoiceHash: string
|
|
116
|
+
encryptedInvoiceSize: number
|
|
117
|
+
} {
|
|
118
|
+
const { key, iv } = generateSymmetricKey()
|
|
119
|
+
const xmlBytes = Buffer.from(xmlString, 'utf-8')
|
|
120
|
+
|
|
121
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv)
|
|
122
|
+
const encryptedContent = Buffer.concat([cipher.update(xmlBytes), cipher.final()])
|
|
123
|
+
|
|
124
|
+
const encryptedSymmetricKey = crypto.publicEncrypt(
|
|
125
|
+
{ key: publicKeyPem, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha256' },
|
|
126
|
+
key,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
encryptedSymmetricKey: encryptedSymmetricKey.toString('base64'),
|
|
131
|
+
initializationVector: iv.toString('base64'),
|
|
132
|
+
encryptedInvoiceContent: encryptedContent.toString('base64'),
|
|
133
|
+
invoiceHash: crypto.createHash('sha256').update(xmlBytes).digest('base64'),
|
|
134
|
+
invoiceSize: xmlBytes.length,
|
|
135
|
+
encryptedInvoiceHash: crypto.createHash('sha256').update(encryptedContent).digest('base64'),
|
|
136
|
+
encryptedInvoiceSize: encryptedContent.length,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { KsefDirectDocument } from '../data/entities'
|
|
2
|
+
|
|
3
|
+
export class KsefXmlGenerationError extends Error {
|
|
4
|
+
constructor(message: string) {
|
|
5
|
+
super(message)
|
|
6
|
+
this.name = 'KsefXmlGenerationError'
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type SellerInfo = {
|
|
11
|
+
sellerName: string
|
|
12
|
+
sellerAddressL1?: string
|
|
13
|
+
sellerCity?: string
|
|
14
|
+
sellerCountry?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function escapeXml(str: string): string {
|
|
18
|
+
return str
|
|
19
|
+
.replace(/&/g, '&')
|
|
20
|
+
.replace(/</g, '<')
|
|
21
|
+
.replace(/>/g, '>')
|
|
22
|
+
.replace(/"/g, '"')
|
|
23
|
+
.replace(/'/g, ''')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function formatDate(date: Date): string {
|
|
27
|
+
return date.toISOString().split('T')[0]!
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// vatRate → { suffix for P_13/P_14, p12Code for FaWiersz.P_12, whether VAT amount applies }
|
|
31
|
+
const VAT_RATE_GROUPS: Record<string, { suffix: string; p12Code: string; hasVat: boolean }> = {
|
|
32
|
+
'23': { suffix: '1', p12Code: '23', hasVat: true },
|
|
33
|
+
'8': { suffix: '2', p12Code: '8', hasVat: true },
|
|
34
|
+
'5': { suffix: '3', p12Code: '5', hasVat: true },
|
|
35
|
+
'0': { suffix: '4', p12Code: '0', hasVat: false },
|
|
36
|
+
'ZW': { suffix: '5', p12Code: 'zw', hasVat: false },
|
|
37
|
+
'NP': { suffix: '6', p12Code: 'np', hasVat: false },
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function generateFa2Xml(document: KsefDirectDocument, seller: SellerInfo): string {
|
|
41
|
+
if (!seller.sellerName.trim()) {
|
|
42
|
+
throw new KsefXmlGenerationError('sellerName is required for FA_VAT 2.0 XML generation')
|
|
43
|
+
}
|
|
44
|
+
if (!seller.sellerAddressL1?.trim()) {
|
|
45
|
+
throw new KsefXmlGenerationError('sellerAddressL1 is required for FA_VAT 2.0 XML generation')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const now = new Date()
|
|
49
|
+
const issueDate = formatDate(document.issueDate)
|
|
50
|
+
const saleDate = document.saleDate ? formatDate(document.saleDate) : issueDate
|
|
51
|
+
|
|
52
|
+
const lineItemsXml = document.lineItems.map((item, index) => {
|
|
53
|
+
const group = VAT_RATE_GROUPS[item.vatRate]
|
|
54
|
+
const p12 = group?.p12Code ?? item.vatRate
|
|
55
|
+
return `
|
|
56
|
+
<FaWiersz>
|
|
57
|
+
<NrWierszaFa>${index + 1}</NrWierszaFa>
|
|
58
|
+
<P_7>${escapeXml(item.description)}</P_7>
|
|
59
|
+
<P_8A>${escapeXml(item.unit)}</P_8A>
|
|
60
|
+
<P_8B>${item.quantity}</P_8B>
|
|
61
|
+
<P_9A>${item.unitNetPrice.toFixed(2)}</P_9A>
|
|
62
|
+
<P_11>${item.netAmount.toFixed(2)}</P_11>
|
|
63
|
+
<P_12>${escapeXml(p12)}</P_12>
|
|
64
|
+
</FaWiersz>`
|
|
65
|
+
}).join('')
|
|
66
|
+
|
|
67
|
+
// Group by VAT rate, accumulate net + vat amounts
|
|
68
|
+
const groups = new Map<string, { net: number; vat: number; hasVat: boolean }>()
|
|
69
|
+
for (const item of document.lineItems) {
|
|
70
|
+
const group = VAT_RATE_GROUPS[item.vatRate]
|
|
71
|
+
if (!group) continue
|
|
72
|
+
const existing = groups.get(group.suffix) ?? { net: 0, vat: 0, hasVat: group.hasVat }
|
|
73
|
+
groups.set(group.suffix, {
|
|
74
|
+
net: existing.net + item.netAmount,
|
|
75
|
+
vat: existing.vat + item.vatAmount,
|
|
76
|
+
hasVat: group.hasVat,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Sort suffix numerically for deterministic output
|
|
81
|
+
const sortedSuffixes = [...groups.keys()].sort()
|
|
82
|
+
const vatSummaryXml = sortedSuffixes.map((suffix) => {
|
|
83
|
+
const g = groups.get(suffix)!
|
|
84
|
+
const net = `\n <P_13_${suffix}>${g.net.toFixed(2)}</P_13_${suffix}>`
|
|
85
|
+
const vat = g.hasVat ? `\n <P_14_${suffix}>${g.vat.toFixed(2)}</P_14_${suffix}>` : ''
|
|
86
|
+
return net + vat
|
|
87
|
+
}).join('')
|
|
88
|
+
|
|
89
|
+
// TAdres: KodKraju → AdresL1 → [AdresL2] — city goes in AdresL2, no Miejscowosc element
|
|
90
|
+
const adresL2 = seller.sellerCity?.trim()
|
|
91
|
+
? `\n <AdresL2>${escapeXml(seller.sellerCity)}</AdresL2>`
|
|
92
|
+
: ''
|
|
93
|
+
const adresXml = `
|
|
94
|
+
<Adres>
|
|
95
|
+
<KodKraju>${escapeXml(seller.sellerCountry ?? 'PL')}</KodKraju>
|
|
96
|
+
<AdresL1>${escapeXml(seller.sellerAddressL1)}</AdresL1>${adresL2}
|
|
97
|
+
</Adres>`
|
|
98
|
+
|
|
99
|
+
const grossAmount = parseFloat(String(document.grossAmount)).toFixed(2)
|
|
100
|
+
|
|
101
|
+
// FA(2) v1-0E Fa sequence: KodWaluty → P_1 → P_2A → P_6 → VAT summaries → P_15 → Adnotacje → RodzajFaktury → FaWiersz
|
|
102
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
103
|
+
<Faktura xmlns="http://crd.gov.pl/wzor/2023/06/29/12648/">
|
|
104
|
+
<Naglowek>
|
|
105
|
+
<KodFormularza kodSystemowy="FA (2)" wersjaSchemy="1-0E">FA</KodFormularza>
|
|
106
|
+
<WariantFormularza>2</WariantFormularza>
|
|
107
|
+
<DataWytworzeniaFa>${now.toISOString()}</DataWytworzeniaFa>
|
|
108
|
+
<SystemInfo>OpenMercato</SystemInfo>
|
|
109
|
+
</Naglowek>
|
|
110
|
+
<Podmiot1>
|
|
111
|
+
<DaneIdentyfikacyjne>
|
|
112
|
+
<NIP>${escapeXml(document.sellerNip)}</NIP>
|
|
113
|
+
<Nazwa>${escapeXml(seller.sellerName)}</Nazwa>
|
|
114
|
+
</DaneIdentyfikacyjne>${adresXml}
|
|
115
|
+
</Podmiot1>
|
|
116
|
+
<Podmiot2>
|
|
117
|
+
<DaneIdentyfikacyjne>
|
|
118
|
+
<NIP>${escapeXml(document.buyerNip)}</NIP>
|
|
119
|
+
<Nazwa>${escapeXml(document.buyerName ?? '')}</Nazwa>
|
|
120
|
+
</DaneIdentyfikacyjne>
|
|
121
|
+
</Podmiot2>
|
|
122
|
+
<Fa>
|
|
123
|
+
<KodWaluty>${escapeXml(document.currency)}</KodWaluty>
|
|
124
|
+
<P_1>${issueDate}</P_1>
|
|
125
|
+
<P_2>${escapeXml(document.invoiceNumber)}</P_2>
|
|
126
|
+
<P_6>${saleDate}</P_6>${vatSummaryXml}
|
|
127
|
+
<P_15>${grossAmount}</P_15>
|
|
128
|
+
<Adnotacje>
|
|
129
|
+
<P_16>2</P_16>
|
|
130
|
+
<P_17>2</P_17>
|
|
131
|
+
<P_18>2</P_18>
|
|
132
|
+
<P_18A>2</P_18A>
|
|
133
|
+
<Zwolnienie>
|
|
134
|
+
<P_19N>1</P_19N>
|
|
135
|
+
</Zwolnienie>
|
|
136
|
+
<NoweSrodkiTransportu>
|
|
137
|
+
<P_22N>1</P_22N>
|
|
138
|
+
</NoweSrodkiTransportu>
|
|
139
|
+
<P_23>2</P_23>
|
|
140
|
+
<PMarzy>
|
|
141
|
+
<P_PMarzyN>1</P_PMarzyN>
|
|
142
|
+
</PMarzy>
|
|
143
|
+
</Adnotacje>
|
|
144
|
+
<RodzajFaktury>VAT</RodzajFaktury>${lineItemsXml}
|
|
145
|
+
</Fa>
|
|
146
|
+
</Faktura>`
|
|
147
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export interface ParsedReceivedInvoice {
|
|
2
|
+
invoiceNumber: string | null
|
|
3
|
+
sellerNip: string | null
|
|
4
|
+
sellerName: string | null
|
|
5
|
+
issueDate: string | null
|
|
6
|
+
currency: string | null
|
|
7
|
+
netAmount: string | null
|
|
8
|
+
vatAmount: string | null
|
|
9
|
+
grossAmount: string | null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function extractSection(xml: string, tag: string): string | null {
|
|
13
|
+
const match = xml.match(new RegExp(`<${tag}[\\s\\S]*?>([\\s\\S]*?)<\\/${tag}>`, 'i'))
|
|
14
|
+
return match ? match[1]! : null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function extractTag(xml: string, tag: string): string | null {
|
|
18
|
+
const match = xml.match(new RegExp(`<${tag}[^>]*>([^<]*)<\\/${tag}>`, 'i'))
|
|
19
|
+
return match ? match[1]!.trim() || null : null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sumTagPattern(xml: string, tagPattern: RegExp): number {
|
|
23
|
+
let sum = 0
|
|
24
|
+
for (const match of xml.matchAll(new RegExp(`<(${tagPattern.source})>([^<]*)<\\/\\1>`, 'gi'))) {
|
|
25
|
+
sum += parseFloat(match[2] ?? '0') || 0
|
|
26
|
+
}
|
|
27
|
+
return sum
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseFa2Xml(xml: string): ParsedReceivedInvoice {
|
|
31
|
+
const podmiot1 = extractSection(xml, 'Podmiot1')
|
|
32
|
+
const sellerNip = podmiot1 ? extractTag(podmiot1, 'NIP') : null
|
|
33
|
+
const sellerName = podmiot1
|
|
34
|
+
? (extractTag(podmiot1, 'PelnaNazwa') ?? extractTag(podmiot1, 'Nazwa'))
|
|
35
|
+
: null
|
|
36
|
+
|
|
37
|
+
const fa = extractSection(xml, 'Fa')
|
|
38
|
+
const issueDate = fa ? extractTag(fa, 'P_1') : null
|
|
39
|
+
const invoiceNumber = fa ? (extractTag(fa, 'P_2A') ?? extractTag(fa, 'P_2')) : null
|
|
40
|
+
const currency = fa ? extractTag(fa, 'KodWaluty') : null
|
|
41
|
+
const grossAmountRaw = fa ? extractTag(fa, 'P_15') : null
|
|
42
|
+
|
|
43
|
+
const netAmountNum = fa ? sumTagPattern(fa, /P_13_\d+/) : 0
|
|
44
|
+
const vatAmountNum = fa ? sumTagPattern(fa, /P_14_\d+/) : 0
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
invoiceNumber,
|
|
48
|
+
sellerNip,
|
|
49
|
+
sellerName,
|
|
50
|
+
issueDate,
|
|
51
|
+
currency,
|
|
52
|
+
netAmount: netAmountNum > 0 ? netAmountNum.toFixed(2) : null,
|
|
53
|
+
vatAmount: vatAmountNum > 0 ? vatAmountNum.toFixed(2) : null,
|
|
54
|
+
grossAmount: grossAmountRaw,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseUpoXml(xml: string): ParsedReceivedInvoice {
|
|
59
|
+
// UPO (Potwierdzenie) format from KSeF — xmlns="http://upo.schematy.mf.gov.pl/KSeF/..."
|
|
60
|
+
// Contains metadata about an accepted invoice but not full financial data.
|
|
61
|
+
const dokument = extractSection(xml, 'Dokument')
|
|
62
|
+
const sellerNip = dokument ? extractTag(dokument, 'NipSprzedawcy') : null
|
|
63
|
+
const invoiceNumber = dokument ? extractTag(dokument, 'NumerFaktury') : null
|
|
64
|
+
const issueDate = dokument ? extractTag(dokument, 'DataWystawieniaFaktury') : null
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
invoiceNumber,
|
|
68
|
+
sellerNip,
|
|
69
|
+
sellerName: null,
|
|
70
|
+
issueDate,
|
|
71
|
+
currency: null,
|
|
72
|
+
netAmount: null,
|
|
73
|
+
vatAmount: null,
|
|
74
|
+
grossAmount: null,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function parseReceivedInvoiceXml(xml: string): ParsedReceivedInvoice {
|
|
79
|
+
try {
|
|
80
|
+
// Detect UPO format by namespace
|
|
81
|
+
if (xml.includes('upo.schematy.mf.gov.pl') || xml.includes('<Potwierdzenie')) {
|
|
82
|
+
return parseUpoXml(xml)
|
|
83
|
+
}
|
|
84
|
+
return parseFa2Xml(xml)
|
|
85
|
+
} catch {
|
|
86
|
+
return {
|
|
87
|
+
invoiceNumber: null,
|
|
88
|
+
sellerNip: null,
|
|
89
|
+
sellerName: null,
|
|
90
|
+
issueDate: null,
|
|
91
|
+
currency: null,
|
|
92
|
+
netAmount: null,
|
|
93
|
+
vatAmount: null,
|
|
94
|
+
grossAmount: null,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|