@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.
Files changed (140) hide show
  1. package/README.md +36 -0
  2. package/dist/index.js +2 -0
  3. package/dist/index.js.map +7 -0
  4. package/dist/modules/integration_ksef_direct/acl.js +13 -0
  5. package/dist/modules/integration_ksef_direct/acl.js.map +7 -0
  6. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/documents/[id].js +92 -0
  7. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/documents/[id].js.map +7 -0
  8. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/documents.js +105 -0
  9. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/documents.js.map +7 -0
  10. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/health.js +158 -0
  11. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/health.js.map +7 -0
  12. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/received-documents/[id].js +86 -0
  13. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/received-documents/[id].js.map +7 -0
  14. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/received-documents.js +112 -0
  15. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/received-documents.js.map +7 -0
  16. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/seller-info.js +54 -0
  17. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/seller-info.js.map +7 -0
  18. package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/documents/[id]/send.js +64 -0
  19. package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/documents/[id]/send.js.map +7 -0
  20. package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/documents.js +104 -0
  21. package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/documents.js.map +7 -0
  22. package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/invoice-numbers.js +41 -0
  23. package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/invoice-numbers.js.map +7 -0
  24. package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/received-documents/fetch.js +172 -0
  25. package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/received-documents/fetch.js.map +7 -0
  26. package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/received-documents/sync.js +80 -0
  27. package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/received-documents/sync.js.map +7 -0
  28. package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/new/page.js +441 -0
  29. package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/new/page.js.map +7 -0
  30. package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/new/page.meta.js +8 -0
  31. package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/new/page.meta.js.map +7 -0
  32. package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/page.js +193 -0
  33. package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/page.js.map +7 -0
  34. package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/received-documents/page.js +314 -0
  35. package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/received-documents/page.js.map +7 -0
  36. package/dist/modules/integration_ksef_direct/backend/page.js +154 -0
  37. package/dist/modules/integration_ksef_direct/backend/page.js.map +7 -0
  38. package/dist/modules/integration_ksef_direct/commands/create-ksef-direct-document.js +80 -0
  39. package/dist/modules/integration_ksef_direct/commands/create-ksef-direct-document.js.map +7 -0
  40. package/dist/modules/integration_ksef_direct/commands/enqueue-ksef-direct-document.js +43 -0
  41. package/dist/modules/integration_ksef_direct/commands/enqueue-ksef-direct-document.js.map +7 -0
  42. package/dist/modules/integration_ksef_direct/data/entities.js +224 -0
  43. package/dist/modules/integration_ksef_direct/data/entities.js.map +7 -0
  44. package/dist/modules/integration_ksef_direct/data/validators.js +103 -0
  45. package/dist/modules/integration_ksef_direct/data/validators.js.map +7 -0
  46. package/dist/modules/integration_ksef_direct/di.js +11 -0
  47. package/dist/modules/integration_ksef_direct/di.js.map +7 -0
  48. package/dist/modules/integration_ksef_direct/events.js +21 -0
  49. package/dist/modules/integration_ksef_direct/events.js.map +7 -0
  50. package/dist/modules/integration_ksef_direct/index.js +10 -0
  51. package/dist/modules/integration_ksef_direct/index.js.map +7 -0
  52. package/dist/modules/integration_ksef_direct/integration.js +56 -0
  53. package/dist/modules/integration_ksef_direct/integration.js.map +7 -0
  54. package/dist/modules/integration_ksef_direct/lib/health.js +32 -0
  55. package/dist/modules/integration_ksef_direct/lib/health.js.map +7 -0
  56. package/dist/modules/integration_ksef_direct/lib/invoiceNumberFormat.js +23 -0
  57. package/dist/modules/integration_ksef_direct/lib/invoiceNumberFormat.js.map +7 -0
  58. package/dist/modules/integration_ksef_direct/lib/ksefClient.js +523 -0
  59. package/dist/modules/integration_ksef_direct/lib/ksefClient.js.map +7 -0
  60. package/dist/modules/integration_ksef_direct/lib/ksefCrypto.js +103 -0
  61. package/dist/modules/integration_ksef_direct/lib/ksefCrypto.js.map +7 -0
  62. package/dist/modules/integration_ksef_direct/lib/ksefFa2Xml.js +123 -0
  63. package/dist/modules/integration_ksef_direct/lib/ksefFa2Xml.js.map +7 -0
  64. package/dist/modules/integration_ksef_direct/lib/ksefXmlParser.js +76 -0
  65. package/dist/modules/integration_ksef_direct/lib/ksefXmlParser.js.map +7 -0
  66. package/dist/modules/integration_ksef_direct/migrations/Migration20260519210000_integration_ksef_direct.js +15 -0
  67. package/dist/modules/integration_ksef_direct/migrations/Migration20260519210000_integration_ksef_direct.js.map +7 -0
  68. package/dist/modules/integration_ksef_direct/migrations/Migration20260520120000_ksef_direct_documents.js +17 -0
  69. package/dist/modules/integration_ksef_direct/migrations/Migration20260520120000_ksef_direct_documents.js.map +7 -0
  70. package/dist/modules/integration_ksef_direct/migrations/Migration20260520220000_ksef_direct_send_queue.js +15 -0
  71. package/dist/modules/integration_ksef_direct/migrations/Migration20260520220000_ksef_direct_send_queue.js.map +7 -0
  72. package/dist/modules/integration_ksef_direct/migrations/Migration20260520230000_ksef_direct_seller_per_document.js +17 -0
  73. package/dist/modules/integration_ksef_direct/migrations/Migration20260520230000_ksef_direct_seller_per_document.js.map +7 -0
  74. package/dist/modules/integration_ksef_direct/migrations/Migration20260521120000_ksef_direct_received_documents.js +16 -0
  75. package/dist/modules/integration_ksef_direct/migrations/Migration20260521120000_ksef_direct_received_documents.js.map +7 -0
  76. package/dist/modules/integration_ksef_direct/migrations/Migration20260521130000_ksef_received_download_urls.js +15 -0
  77. package/dist/modules/integration_ksef_direct/migrations/Migration20260521130000_ksef_received_download_urls.js.map +7 -0
  78. package/dist/modules/integration_ksef_direct/setup.js +11 -0
  79. package/dist/modules/integration_ksef_direct/setup.js.map +7 -0
  80. package/dist/modules/integration_ksef_direct/subscribers/auto-enqueue-ksef-document.js +19 -0
  81. package/dist/modules/integration_ksef_direct/subscribers/auto-enqueue-ksef-document.js.map +7 -0
  82. package/dist/modules/integration_ksef_direct/workers/check-ksef-document-status.js +103 -0
  83. package/dist/modules/integration_ksef_direct/workers/check-ksef-document-status.js.map +7 -0
  84. package/dist/modules/integration_ksef_direct/workers/send-ksef-document.js +104 -0
  85. package/dist/modules/integration_ksef_direct/workers/send-ksef-document.js.map +7 -0
  86. package/dist/modules/integration_ksef_direct/workers/sync-received-documents.js +137 -0
  87. package/dist/modules/integration_ksef_direct/workers/sync-received-documents.js.map +7 -0
  88. package/dist/types/declarations.d.js +1 -0
  89. package/dist/types/declarations.d.js.map +7 -0
  90. package/package.json +98 -0
  91. package/src/index.ts +1 -0
  92. package/src/modules/integration_ksef_direct/__tests__/invoiceNumberFormat.test.ts +42 -0
  93. package/src/modules/integration_ksef_direct/__tests__/ksefFa2Xml.test.ts +407 -0
  94. package/src/modules/integration_ksef_direct/__tests__/ksefXmlParser.test.ts +230 -0
  95. package/src/modules/integration_ksef_direct/acl.ts +9 -0
  96. package/src/modules/integration_ksef_direct/api/get/integration-ksef-direct/documents/[id].ts +94 -0
  97. package/src/modules/integration_ksef_direct/api/get/integration-ksef-direct/documents.ts +111 -0
  98. package/src/modules/integration_ksef_direct/api/get/integration-ksef-direct/health.ts +194 -0
  99. package/src/modules/integration_ksef_direct/api/get/integration-ksef-direct/received-documents/[id].ts +88 -0
  100. package/src/modules/integration_ksef_direct/api/get/integration-ksef-direct/received-documents.ts +119 -0
  101. package/src/modules/integration_ksef_direct/api/get/integration-ksef-direct/seller-info.ts +62 -0
  102. package/src/modules/integration_ksef_direct/api/post/integration-ksef-direct/documents/[id]/send.ts +64 -0
  103. package/src/modules/integration_ksef_direct/api/post/integration-ksef-direct/documents.ts +109 -0
  104. package/src/modules/integration_ksef_direct/api/post/integration-ksef-direct/invoice-numbers.ts +40 -0
  105. package/src/modules/integration_ksef_direct/api/post/integration-ksef-direct/received-documents/fetch.ts +185 -0
  106. package/src/modules/integration_ksef_direct/api/post/integration-ksef-direct/received-documents/sync.ts +86 -0
  107. package/src/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/new/page.meta.ts +4 -0
  108. package/src/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/new/page.tsx +470 -0
  109. package/src/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/page.tsx +233 -0
  110. package/src/modules/integration_ksef_direct/backend/integration-ksef-direct/received-documents/page.tsx +415 -0
  111. package/src/modules/integration_ksef_direct/backend/page.tsx +183 -0
  112. package/src/modules/integration_ksef_direct/commands/create-ksef-direct-document.ts +93 -0
  113. package/src/modules/integration_ksef_direct/commands/enqueue-ksef-direct-document.ts +57 -0
  114. package/src/modules/integration_ksef_direct/data/entities.ts +195 -0
  115. package/src/modules/integration_ksef_direct/data/validators.ts +115 -0
  116. package/src/modules/integration_ksef_direct/di.ts +9 -0
  117. package/src/modules/integration_ksef_direct/events.ts +18 -0
  118. package/src/modules/integration_ksef_direct/i18n/en.json +115 -0
  119. package/src/modules/integration_ksef_direct/i18n/pl.json +115 -0
  120. package/src/modules/integration_ksef_direct/index.ts +6 -0
  121. package/src/modules/integration_ksef_direct/integration.ts +54 -0
  122. package/src/modules/integration_ksef_direct/lib/health.ts +43 -0
  123. package/src/modules/integration_ksef_direct/lib/invoiceNumberFormat.ts +23 -0
  124. package/src/modules/integration_ksef_direct/lib/ksefClient.ts +668 -0
  125. package/src/modules/integration_ksef_direct/lib/ksefCrypto.ts +138 -0
  126. package/src/modules/integration_ksef_direct/lib/ksefFa2Xml.ts +147 -0
  127. package/src/modules/integration_ksef_direct/lib/ksefXmlParser.ts +97 -0
  128. package/src/modules/integration_ksef_direct/migrations/.snapshot-open-mercato.json +1028 -0
  129. package/src/modules/integration_ksef_direct/migrations/Migration20260519210000_integration_ksef_direct.ts +15 -0
  130. package/src/modules/integration_ksef_direct/migrations/Migration20260520120000_ksef_direct_documents.ts +17 -0
  131. package/src/modules/integration_ksef_direct/migrations/Migration20260520220000_ksef_direct_send_queue.ts +15 -0
  132. package/src/modules/integration_ksef_direct/migrations/Migration20260520230000_ksef_direct_seller_per_document.ts +17 -0
  133. package/src/modules/integration_ksef_direct/migrations/Migration20260521120000_ksef_direct_received_documents.ts +16 -0
  134. package/src/modules/integration_ksef_direct/migrations/Migration20260521130000_ksef_received_download_urls.ts +15 -0
  135. package/src/modules/integration_ksef_direct/setup.ts +9 -0
  136. package/src/modules/integration_ksef_direct/subscribers/auto-enqueue-ksef-document.ts +21 -0
  137. package/src/modules/integration_ksef_direct/workers/check-ksef-document-status.ts +129 -0
  138. package/src/modules/integration_ksef_direct/workers/send-ksef-document.ts +137 -0
  139. package/src/modules/integration_ksef_direct/workers/sync-received-documents.ts +171 -0
  140. package/src/types/declarations.d.ts +1 -0
@@ -0,0 +1,407 @@
1
+ import { generateFa2Xml, KsefXmlGenerationError, type SellerInfo } from '../lib/ksefFa2Xml'
2
+ import type { KsefDirectDocument, KsefDirectStoredLineItem } from '../data/entities'
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Helpers
6
+ // ---------------------------------------------------------------------------
7
+
8
+ function makeLineItem(overrides: Partial<KsefDirectStoredLineItem> = {}): KsefDirectStoredLineItem {
9
+ return {
10
+ description: 'Test service',
11
+ quantity: 1,
12
+ unit: 'szt',
13
+ unitNetPrice: 100,
14
+ vatRate: '23',
15
+ netAmount: 100,
16
+ vatAmount: 23,
17
+ grossAmount: 123,
18
+ ...overrides,
19
+ }
20
+ }
21
+
22
+ function makeDocument(overrides: Partial<KsefDirectDocument> = {}): KsefDirectDocument {
23
+ return {
24
+ id: 'doc-uuid-1',
25
+ organizationId: 'org-uuid-1',
26
+ tenantId: 'tenant-uuid-1',
27
+ source: 'manual',
28
+ status: 'draft',
29
+ sellerNip: '1234567890',
30
+ buyerNip: '0987654321',
31
+ buyerName: 'Buyer Sp. z o.o.',
32
+ invoiceNumber: 'FV/2025/01/TESTTEST',
33
+ issueDate: new Date('2025-01-15'),
34
+ saleDate: null,
35
+ netAmount: '100.00',
36
+ vatAmount: '23.00',
37
+ grossAmount: '123.00',
38
+ currency: 'PLN',
39
+ lineItems: [makeLineItem()],
40
+ notes: null,
41
+ ksefReferenceNumber: null,
42
+ ksefProcessingReferenceNumber: null,
43
+ sellerName: null,
44
+ sellerAddressL1: null,
45
+ sellerCity: null,
46
+ sellerCountry: null,
47
+ errorMessage: null,
48
+ createdAt: new Date('2025-01-01'),
49
+ updatedAt: new Date('2025-01-01'),
50
+ ...overrides,
51
+ } as KsefDirectDocument
52
+ }
53
+
54
+ const DEFAULT_SELLER: SellerInfo = {
55
+ sellerName: 'Seller Sp. z o.o.',
56
+ sellerAddressL1: 'ul. Testowa 1',
57
+ sellerCity: 'Warszawa',
58
+ sellerCountry: 'PL',
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Tests
63
+ // ---------------------------------------------------------------------------
64
+
65
+ describe('generateFa2Xml', () => {
66
+ describe('validation — required seller fields', () => {
67
+ it('throws KsefXmlGenerationError when sellerName is empty', () => {
68
+ const doc = makeDocument()
69
+ expect(() => generateFa2Xml(doc, { ...DEFAULT_SELLER, sellerName: '' })).toThrow(
70
+ KsefXmlGenerationError,
71
+ )
72
+ })
73
+
74
+ it('throws KsefXmlGenerationError when sellerName is whitespace-only', () => {
75
+ const doc = makeDocument()
76
+ expect(() => generateFa2Xml(doc, { ...DEFAULT_SELLER, sellerName: ' ' })).toThrow(
77
+ KsefXmlGenerationError,
78
+ )
79
+ })
80
+
81
+ it('throws KsefXmlGenerationError when sellerAddressL1 is absent', () => {
82
+ const doc = makeDocument()
83
+ const seller: SellerInfo = { sellerName: 'Seller', sellerAddressL1: undefined }
84
+ expect(() => generateFa2Xml(doc, seller)).toThrow(KsefXmlGenerationError)
85
+ })
86
+
87
+ it('throws KsefXmlGenerationError when sellerAddressL1 is whitespace-only', () => {
88
+ const doc = makeDocument()
89
+ expect(() => generateFa2Xml(doc, { ...DEFAULT_SELLER, sellerAddressL1: ' ' })).toThrow(
90
+ KsefXmlGenerationError,
91
+ )
92
+ })
93
+
94
+ it('error name is "KsefXmlGenerationError"', () => {
95
+ const doc = makeDocument()
96
+ try {
97
+ generateFa2Xml(doc, { ...DEFAULT_SELLER, sellerName: '' })
98
+ fail('expected to throw')
99
+ } catch (err) {
100
+ expect((err as Error).name).toBe('KsefXmlGenerationError')
101
+ }
102
+ })
103
+ })
104
+
105
+ describe('XML structure', () => {
106
+ let xml: string
107
+
108
+ beforeEach(() => {
109
+ xml = generateFa2Xml(makeDocument(), DEFAULT_SELLER)
110
+ })
111
+
112
+ it('produces a string starting with the XML declaration', () => {
113
+ expect(xml).toMatch(/^<\?xml version="1\.0"/)
114
+ })
115
+
116
+ it('uses the FA(2) namespace', () => {
117
+ expect(xml).toContain('xmlns="http://crd.gov.pl/wzor/2023/06/29/12648/"')
118
+ })
119
+
120
+ it('includes the KodFormularza element with value FA', () => {
121
+ expect(xml).toContain('<KodFormularza kodSystemowy="FA (2)" wersjaSchemy="1-0E">FA</KodFormularza>')
122
+ })
123
+
124
+ it('includes WariantFormularza = 2', () => {
125
+ expect(xml).toContain('<WariantFormularza>2</WariantFormularza>')
126
+ })
127
+
128
+ it('includes SystemInfo = OpenMercato', () => {
129
+ expect(xml).toContain('<SystemInfo>OpenMercato</SystemInfo>')
130
+ })
131
+
132
+ it('includes RodzajFaktury = VAT', () => {
133
+ expect(xml).toContain('<RodzajFaktury>VAT</RodzajFaktury>')
134
+ })
135
+ })
136
+
137
+ describe('seller (Podmiot1) data', () => {
138
+ it('places seller NIP inside Podmiot1', () => {
139
+ const xml = generateFa2Xml(makeDocument(), DEFAULT_SELLER)
140
+ expect(xml).toContain('<NIP>1234567890</NIP>')
141
+ })
142
+
143
+ it('places seller name inside Podmiot1', () => {
144
+ const xml = generateFa2Xml(makeDocument(), DEFAULT_SELLER)
145
+ expect(xml).toContain('<Nazwa>Seller Sp. z o.o.</Nazwa>')
146
+ })
147
+
148
+ it('includes sellerAddressL1 inside Adres/AdresL1', () => {
149
+ const xml = generateFa2Xml(makeDocument(), DEFAULT_SELLER)
150
+ expect(xml).toContain('<AdresL1>ul. Testowa 1</AdresL1>')
151
+ })
152
+
153
+ it('includes sellerCity inside Adres/AdresL2', () => {
154
+ const xml = generateFa2Xml(makeDocument(), DEFAULT_SELLER)
155
+ expect(xml).toContain('<AdresL2>Warszawa</AdresL2>')
156
+ })
157
+
158
+ it('defaults KodKraju to PL when sellerCountry is not provided', () => {
159
+ const sellerNoCountry: SellerInfo = {
160
+ sellerName: 'Seller',
161
+ sellerAddressL1: 'ul. X 1',
162
+ }
163
+ const xml = generateFa2Xml(makeDocument(), sellerNoCountry)
164
+ expect(xml).toContain('<KodKraju>PL</KodKraju>')
165
+ })
166
+
167
+ it('uses the provided sellerCountry code when given', () => {
168
+ const xml = generateFa2Xml(makeDocument(), { ...DEFAULT_SELLER, sellerCountry: 'DE' })
169
+ expect(xml).toContain('<KodKraju>DE</KodKraju>')
170
+ })
171
+
172
+ it('omits AdresL2 when sellerCity is absent', () => {
173
+ const sellerNoCity: SellerInfo = {
174
+ sellerName: 'Seller',
175
+ sellerAddressL1: 'ul. X 1',
176
+ }
177
+ const xml = generateFa2Xml(makeDocument(), sellerNoCity)
178
+ expect(xml).not.toContain('<AdresL2>')
179
+ })
180
+ })
181
+
182
+ describe('buyer (Podmiot2) data', () => {
183
+ it('places buyer NIP inside Podmiot2', () => {
184
+ const xml = generateFa2Xml(makeDocument(), DEFAULT_SELLER)
185
+ expect(xml).toContain('<NIP>0987654321</NIP>')
186
+ })
187
+
188
+ it('places buyer name inside Podmiot2', () => {
189
+ const xml = generateFa2Xml(makeDocument(), DEFAULT_SELLER)
190
+ expect(xml).toContain('<Nazwa>Buyer Sp. z o.o.</Nazwa>')
191
+ })
192
+
193
+ it('outputs empty Nazwa when buyerName is null', () => {
194
+ const doc = makeDocument({ buyerName: null })
195
+ const xml = generateFa2Xml(doc, DEFAULT_SELLER)
196
+ expect(xml).toContain('<Nazwa></Nazwa>')
197
+ })
198
+ })
199
+
200
+ describe('Fa / invoice fields', () => {
201
+ it('outputs KodWaluty matching the document currency', () => {
202
+ const xml = generateFa2Xml(makeDocument(), DEFAULT_SELLER)
203
+ expect(xml).toContain('<KodWaluty>PLN</KodWaluty>')
204
+ })
205
+
206
+ it('outputs P_1 as the formatted issue date (YYYY-MM-DD)', () => {
207
+ const xml = generateFa2Xml(makeDocument(), DEFAULT_SELLER)
208
+ expect(xml).toContain('<P_1>2025-01-15</P_1>')
209
+ })
210
+
211
+ it('outputs P_2 with the invoice number', () => {
212
+ const xml = generateFa2Xml(makeDocument(), DEFAULT_SELLER)
213
+ expect(xml).toContain('<P_2>FV/2025/01/TESTTEST</P_2>')
214
+ })
215
+
216
+ it('outputs P_6 as the sale date when provided', () => {
217
+ const doc = makeDocument({ saleDate: new Date('2025-01-20') })
218
+ const xml = generateFa2Xml(doc, DEFAULT_SELLER)
219
+ expect(xml).toContain('<P_6>2025-01-20</P_6>')
220
+ })
221
+
222
+ it('falls back P_6 to issueDate when saleDate is null', () => {
223
+ const xml = generateFa2Xml(makeDocument(), DEFAULT_SELLER) // saleDate: null
224
+ expect(xml).toContain('<P_6>2025-01-15</P_6>')
225
+ })
226
+
227
+ it('outputs P_15 as the gross amount (2 decimal places)', () => {
228
+ const xml = generateFa2Xml(makeDocument(), DEFAULT_SELLER)
229
+ expect(xml).toContain('<P_15>123.00</P_15>')
230
+ })
231
+ })
232
+
233
+ describe('line items (FaWiersz)', () => {
234
+ it('outputs a FaWiersz element for each line item', () => {
235
+ const doc = makeDocument({
236
+ lineItems: [
237
+ makeLineItem({ description: 'Item A' }),
238
+ makeLineItem({ description: 'Item B' }),
239
+ ],
240
+ })
241
+ const xml = generateFa2Xml(doc, DEFAULT_SELLER)
242
+ const matches = xml.match(/<FaWiersz>/g)
243
+ expect(matches).toHaveLength(2)
244
+ })
245
+
246
+ it('sets NrWierszaFa starting from 1', () => {
247
+ const xml = generateFa2Xml(makeDocument(), DEFAULT_SELLER)
248
+ expect(xml).toContain('<NrWierszaFa>1</NrWierszaFa>')
249
+ })
250
+
251
+ it('outputs P_7 with the item description', () => {
252
+ const xml = generateFa2Xml(makeDocument(), DEFAULT_SELLER)
253
+ expect(xml).toContain('<P_7>Test service</P_7>')
254
+ })
255
+
256
+ it('outputs P_8A with the unit', () => {
257
+ const xml = generateFa2Xml(makeDocument(), DEFAULT_SELLER)
258
+ expect(xml).toContain('<P_8A>szt</P_8A>')
259
+ })
260
+
261
+ it('outputs P_8B with the quantity', () => {
262
+ const xml = generateFa2Xml(makeDocument(), DEFAULT_SELLER)
263
+ expect(xml).toContain('<P_8B>1</P_8B>')
264
+ })
265
+
266
+ it('outputs P_9A with the unit net price (2 decimal places)', () => {
267
+ const xml = generateFa2Xml(makeDocument(), DEFAULT_SELLER)
268
+ expect(xml).toContain('<P_9A>100.00</P_9A>')
269
+ })
270
+
271
+ it('outputs P_11 with the net amount (2 decimal places)', () => {
272
+ const xml = generateFa2Xml(makeDocument(), DEFAULT_SELLER)
273
+ expect(xml).toContain('<P_11>100.00</P_11>')
274
+ })
275
+
276
+ it('uses the correct P_12 code for 23% VAT rate', () => {
277
+ const xml = generateFa2Xml(makeDocument(), DEFAULT_SELLER)
278
+ expect(xml).toContain('<P_12>23</P_12>')
279
+ })
280
+
281
+ it('uses the correct P_12 code (zw) for ZW exempt rate', () => {
282
+ const doc = makeDocument({
283
+ lineItems: [makeLineItem({ vatRate: 'ZW', vatAmount: 0 })],
284
+ })
285
+ const xml = generateFa2Xml(doc, DEFAULT_SELLER)
286
+ expect(xml).toContain('<P_12>zw</P_12>')
287
+ })
288
+
289
+ it('uses the correct P_12 code (np) for NP rate', () => {
290
+ const doc = makeDocument({
291
+ lineItems: [makeLineItem({ vatRate: 'NP', vatAmount: 0 })],
292
+ })
293
+ const xml = generateFa2Xml(doc, DEFAULT_SELLER)
294
+ expect(xml).toContain('<P_12>np</P_12>')
295
+ })
296
+ })
297
+
298
+ describe('VAT summary (P_13/P_14 grouping)', () => {
299
+ it('outputs P_13_1 (net) for 23% VAT rate', () => {
300
+ const xml = generateFa2Xml(makeDocument(), DEFAULT_SELLER)
301
+ expect(xml).toContain('<P_13_1>100.00</P_13_1>')
302
+ })
303
+
304
+ it('outputs P_14_1 (VAT) for 23% VAT rate', () => {
305
+ const xml = generateFa2Xml(makeDocument(), DEFAULT_SELLER)
306
+ expect(xml).toContain('<P_14_1>23.00</P_14_1>')
307
+ })
308
+
309
+ it('does not output P_14_4 (VAT) for 0% rate (hasVat = false)', () => {
310
+ const doc = makeDocument({
311
+ lineItems: [makeLineItem({ vatRate: '0', vatAmount: 0 })],
312
+ })
313
+ const xml = generateFa2Xml(doc, DEFAULT_SELLER)
314
+ expect(xml).not.toContain('<P_14_4>')
315
+ expect(xml).toContain('<P_13_4>')
316
+ })
317
+
318
+ it('does not output P_14_5 for ZW (exempt) rate', () => {
319
+ const doc = makeDocument({
320
+ lineItems: [makeLineItem({ vatRate: 'ZW', vatAmount: 0 })],
321
+ })
322
+ const xml = generateFa2Xml(doc, DEFAULT_SELLER)
323
+ expect(xml).not.toContain('<P_14_5>')
324
+ expect(xml).toContain('<P_13_5>')
325
+ })
326
+
327
+ it('accumulates multiple lines with the same VAT rate into a single group', () => {
328
+ const doc = makeDocument({
329
+ lineItems: [
330
+ makeLineItem({ vatRate: '23', netAmount: 100, vatAmount: 23 }),
331
+ makeLineItem({ vatRate: '23', netAmount: 200, vatAmount: 46 }),
332
+ ],
333
+ grossAmount: '369.00',
334
+ })
335
+ const xml = generateFa2Xml(doc, DEFAULT_SELLER)
336
+ expect(xml).toContain('<P_13_1>300.00</P_13_1>')
337
+ expect(xml).toContain('<P_14_1>69.00</P_14_1>')
338
+ })
339
+
340
+ it('outputs separate P_13 groups for different VAT rates', () => {
341
+ const doc = makeDocument({
342
+ lineItems: [
343
+ makeLineItem({ vatRate: '23', netAmount: 100, vatAmount: 23, grossAmount: 123 }),
344
+ makeLineItem({ vatRate: '8', netAmount: 200, vatAmount: 16, grossAmount: 216 }),
345
+ ],
346
+ grossAmount: '339.00',
347
+ })
348
+ const xml = generateFa2Xml(doc, DEFAULT_SELLER)
349
+ expect(xml).toContain('<P_13_1>100.00</P_13_1>') // 23% group
350
+ expect(xml).toContain('<P_14_1>23.00</P_14_1>')
351
+ expect(xml).toContain('<P_13_2>200.00</P_13_2>') // 8% group
352
+ expect(xml).toContain('<P_14_2>16.00</P_14_2>')
353
+ })
354
+ })
355
+
356
+ describe('XML escaping', () => {
357
+ it('escapes & in description', () => {
358
+ const doc = makeDocument({
359
+ lineItems: [makeLineItem({ description: 'Services & Goods' })],
360
+ })
361
+ const xml = generateFa2Xml(doc, DEFAULT_SELLER)
362
+ expect(xml).toContain('<P_7>Services &amp; Goods</P_7>')
363
+ })
364
+
365
+ it('escapes < in description', () => {
366
+ const doc = makeDocument({
367
+ lineItems: [makeLineItem({ description: '1<2' })],
368
+ })
369
+ const xml = generateFa2Xml(doc, DEFAULT_SELLER)
370
+ expect(xml).toContain('<P_7>1&lt;2</P_7>')
371
+ })
372
+
373
+ it('escapes > in description', () => {
374
+ const doc = makeDocument({
375
+ lineItems: [makeLineItem({ description: '2>1' })],
376
+ })
377
+ const xml = generateFa2Xml(doc, DEFAULT_SELLER)
378
+ expect(xml).toContain('<P_7>2&gt;1</P_7>')
379
+ })
380
+
381
+ it('escapes " in seller address', () => {
382
+ const xml = generateFa2Xml(
383
+ makeDocument(),
384
+ { ...DEFAULT_SELLER, sellerAddressL1: 'ul. "Testowa" 1' },
385
+ )
386
+ expect(xml).toContain('<AdresL1>ul. &quot;Testowa&quot; 1</AdresL1>')
387
+ })
388
+
389
+ it("escapes ' in seller name", () => {
390
+ const xml = generateFa2Xml(
391
+ makeDocument(),
392
+ { ...DEFAULT_SELLER, sellerName: "Seller's Ltd" },
393
+ )
394
+ expect(xml).toContain("<Nazwa>Seller&apos;s Ltd</Nazwa>")
395
+ })
396
+ })
397
+
398
+ describe('Adnotacje block', () => {
399
+ it('includes the static Adnotacje block', () => {
400
+ const xml = generateFa2Xml(makeDocument(), DEFAULT_SELLER)
401
+ expect(xml).toContain('<Adnotacje>')
402
+ expect(xml).toContain('<P_16>2</P_16>')
403
+ expect(xml).toContain('<P_17>2</P_17>')
404
+ expect(xml).toContain('<P_23>2</P_23>')
405
+ })
406
+ })
407
+ })
@@ -0,0 +1,230 @@
1
+ import { parseReceivedInvoiceXml, type ParsedReceivedInvoice } from '../lib/ksefXmlParser'
2
+
3
+ // Minimal FA(2) invoice XML based on the structure produced by ksefFa2Xml.ts
4
+ const FA2_XML = `<?xml version="1.0" encoding="UTF-8"?>
5
+ <Faktura xmlns="http://crd.gov.pl/wzor/2023/06/29/12648/">
6
+ <Podmiot1>
7
+ <DaneIdentyfikacyjne>
8
+ <NIP>1234567890</NIP>
9
+ <Nazwa>Seller Sp. z o.o.</Nazwa>
10
+ </DaneIdentyfikacyjne>
11
+ </Podmiot1>
12
+ <Podmiot2>
13
+ <DaneIdentyfikacyjne>
14
+ <NIP>0987654321</NIP>
15
+ <Nazwa>Buyer S.A.</Nazwa>
16
+ </DaneIdentyfikacyjne>
17
+ </Podmiot2>
18
+ <Fa>
19
+ <KodWaluty>PLN</KodWaluty>
20
+ <P_1>2025-01-15</P_1>
21
+ <P_2>FV/2025/01/ABCD1234</P_2>
22
+ <P_6>2025-01-15</P_6>
23
+ <P_13_1>1000.00</P_13_1>
24
+ <P_14_1>230.00</P_14_1>
25
+ <P_13_2>500.00</P_13_2>
26
+ <P_14_2>40.00</P_14_2>
27
+ <P_15>1770.00</P_15>
28
+ </Fa>
29
+ </Faktura>`
30
+
31
+ // FA(2) XML using P_2A instead of P_2 for invoice number
32
+ const FA2_XML_WITH_P2A = `<?xml version="1.0" encoding="UTF-8"?>
33
+ <Faktura xmlns="http://crd.gov.pl/wzor/2023/06/29/12648/">
34
+ <Podmiot1>
35
+ <DaneIdentyfikacyjne>
36
+ <NIP>1111111111</NIP>
37
+ <PelnaNazwa>Full Name GmbH</PelnaNazwa>
38
+ </DaneIdentyfikacyjne>
39
+ </Podmiot1>
40
+ <Fa>
41
+ <KodWaluty>EUR</KodWaluty>
42
+ <P_1>2024-06-30</P_1>
43
+ <P_2A>FV/2024/06/XXXXXXXX</P_2A>
44
+ <P_6>2024-06-30</P_6>
45
+ <P_13_1>200.00</P_13_1>
46
+ <P_14_1>46.00</P_14_1>
47
+ <P_15>246.00</P_15>
48
+ </Fa>
49
+ </Faktura>`
50
+
51
+ // FA(2) with only exempt (no VAT) rates — no P_14 elements
52
+ const FA2_XML_EXEMPT_ONLY = `<?xml version="1.0" encoding="UTF-8"?>
53
+ <Faktura xmlns="http://crd.gov.pl/wzor/2023/06/29/12648/">
54
+ <Podmiot1>
55
+ <DaneIdentyfikacyjne>
56
+ <NIP>5555555555</NIP>
57
+ <Nazwa>Exempt Seller</Nazwa>
58
+ </DaneIdentyfikacyjne>
59
+ </Podmiot1>
60
+ <Fa>
61
+ <KodWaluty>PLN</KodWaluty>
62
+ <P_1>2025-03-01</P_1>
63
+ <P_2>FV/2025/03/ZERORAT1</P_2>
64
+ <P_6>2025-03-01</P_6>
65
+ <P_13_5>800.00</P_13_5>
66
+ <P_15>800.00</P_15>
67
+ </Fa>
68
+ </Faktura>`
69
+
70
+ // UPO (Potwierdzenie) format from KSeF
71
+ const UPO_XML = `<?xml version="1.0" encoding="UTF-8"?>
72
+ <Potwierdzenie xmlns="http://upo.schematy.mf.gov.pl/KSeF/2023">
73
+ <Dokument>
74
+ <NipSprzedawcy>9999999999</NipSprzedawcy>
75
+ <NumerFaktury>FV/2025/05/UPOTEST1</NumerFaktury>
76
+ <DataWystawieniaFaktury>2025-05-10</DataWystawieniaFaktury>
77
+ </Dokument>
78
+ </Potwierdzenie>`
79
+
80
+ // Entirely malformed / empty XML
81
+ const EMPTY_XML = ''
82
+ const GARBAGE_XML = '<not-an-invoice>garbage</not-an-invoice>'
83
+
84
+ describe('parseReceivedInvoiceXml', () => {
85
+ describe('FA(2) format — standard invoice', () => {
86
+ let result: ParsedReceivedInvoice
87
+
88
+ beforeEach(() => {
89
+ result = parseReceivedInvoiceXml(FA2_XML)
90
+ })
91
+
92
+ it('extracts the seller NIP from Podmiot1', () => {
93
+ expect(result.sellerNip).toBe('1234567890')
94
+ })
95
+
96
+ it('extracts the seller name (Nazwa) from Podmiot1', () => {
97
+ expect(result.sellerName).toBe('Seller Sp. z o.o.')
98
+ })
99
+
100
+ it('extracts the issue date (P_1)', () => {
101
+ expect(result.issueDate).toBe('2025-01-15')
102
+ })
103
+
104
+ it('extracts the invoice number from P_2 when P_2A is absent', () => {
105
+ expect(result.invoiceNumber).toBe('FV/2025/01/ABCD1234')
106
+ })
107
+
108
+ it('extracts the currency (KodWaluty)', () => {
109
+ expect(result.currency).toBe('PLN')
110
+ })
111
+
112
+ it('sums all P_13_* elements as net amount', () => {
113
+ // 1000.00 + 500.00 = 1500.00
114
+ expect(result.netAmount).toBe('1500.00')
115
+ })
116
+
117
+ it('sums all P_14_* elements as VAT amount', () => {
118
+ // 230.00 + 40.00 = 270.00
119
+ expect(result.vatAmount).toBe('270.00')
120
+ })
121
+
122
+ it('extracts the gross amount from P_15', () => {
123
+ expect(result.grossAmount).toBe('1770.00')
124
+ })
125
+ })
126
+
127
+ describe('FA(2) format — P_2A takes precedence over P_2 for invoice number', () => {
128
+ it('uses P_2A as invoice number when present', () => {
129
+ const result = parseReceivedInvoiceXml(FA2_XML_WITH_P2A)
130
+ expect(result.invoiceNumber).toBe('FV/2024/06/XXXXXXXX')
131
+ })
132
+
133
+ it('extracts PelnaNazwa as seller name when Nazwa is absent', () => {
134
+ const result = parseReceivedInvoiceXml(FA2_XML_WITH_P2A)
135
+ expect(result.sellerName).toBe('Full Name GmbH')
136
+ })
137
+
138
+ it('reads EUR currency', () => {
139
+ const result = parseReceivedInvoiceXml(FA2_XML_WITH_P2A)
140
+ expect(result.currency).toBe('EUR')
141
+ })
142
+ })
143
+
144
+ describe('FA(2) format — exempt-only rates (no P_14 elements)', () => {
145
+ let result: ParsedReceivedInvoice
146
+
147
+ beforeEach(() => {
148
+ result = parseReceivedInvoiceXml(FA2_XML_EXEMPT_ONLY)
149
+ })
150
+
151
+ it('returns null for vatAmount when there are no P_14_* elements', () => {
152
+ expect(result.vatAmount).toBeNull()
153
+ })
154
+
155
+ it('returns the correct net amount from P_13_5', () => {
156
+ expect(result.netAmount).toBe('800.00')
157
+ })
158
+
159
+ it('returns the correct gross amount', () => {
160
+ expect(result.grossAmount).toBe('800.00')
161
+ })
162
+ })
163
+
164
+ describe('UPO (Potwierdzenie) format', () => {
165
+ let result: ParsedReceivedInvoice
166
+
167
+ beforeEach(() => {
168
+ result = parseReceivedInvoiceXml(UPO_XML)
169
+ })
170
+
171
+ it('detects UPO format and extracts NipSprzedawcy', () => {
172
+ expect(result.sellerNip).toBe('9999999999')
173
+ })
174
+
175
+ it('extracts NumerFaktury as invoiceNumber', () => {
176
+ expect(result.invoiceNumber).toBe('FV/2025/05/UPOTEST1')
177
+ })
178
+
179
+ it('extracts DataWystawieniaFaktury as issueDate', () => {
180
+ expect(result.issueDate).toBe('2025-05-10')
181
+ })
182
+
183
+ it('returns null for financial fields not present in UPO', () => {
184
+ expect(result.sellerName).toBeNull()
185
+ expect(result.currency).toBeNull()
186
+ expect(result.netAmount).toBeNull()
187
+ expect(result.vatAmount).toBeNull()
188
+ expect(result.grossAmount).toBeNull()
189
+ })
190
+ })
191
+
192
+ describe('empty or malformed input', () => {
193
+ it('returns all-null result for empty string', () => {
194
+ const result = parseReceivedInvoiceXml(EMPTY_XML)
195
+ expect(result.invoiceNumber).toBeNull()
196
+ expect(result.sellerNip).toBeNull()
197
+ expect(result.sellerName).toBeNull()
198
+ expect(result.issueDate).toBeNull()
199
+ expect(result.currency).toBeNull()
200
+ expect(result.netAmount).toBeNull()
201
+ expect(result.vatAmount).toBeNull()
202
+ expect(result.grossAmount).toBeNull()
203
+ })
204
+
205
+ it('returns all-null result for garbage XML that has no recognized structure', () => {
206
+ const result = parseReceivedInvoiceXml(GARBAGE_XML)
207
+ expect(result.invoiceNumber).toBeNull()
208
+ expect(result.sellerNip).toBeNull()
209
+ })
210
+ })
211
+
212
+ describe('return shape', () => {
213
+ it('always returns an object with the expected keys', () => {
214
+ const result = parseReceivedInvoiceXml(FA2_XML)
215
+ const expectedKeys: (keyof ParsedReceivedInvoice)[] = [
216
+ 'invoiceNumber',
217
+ 'sellerNip',
218
+ 'sellerName',
219
+ 'issueDate',
220
+ 'currency',
221
+ 'netAmount',
222
+ 'vatAmount',
223
+ 'grossAmount',
224
+ ]
225
+ for (const key of expectedKeys) {
226
+ expect(result).toHaveProperty(key)
227
+ }
228
+ })
229
+ })
230
+ })
@@ -0,0 +1,9 @@
1
+ export const features = [
2
+ { id: 'integration_ksef_direct.view', title: 'View KSeF Direct connection status', module: 'integration_ksef_direct' },
3
+ { id: 'integration_ksef_direct.manage', title: 'Manage KSeF Direct integration', module: 'integration_ksef_direct' },
4
+ { id: 'integration_ksef_direct.documents.view', title: 'View KSeF Direct documents', module: 'integration_ksef_direct' },
5
+ { id: 'integration_ksef_direct.documents.create', title: 'Create KSeF Direct documents manually', module: 'integration_ksef_direct' },
6
+ { id: 'integration_ksef_direct.documents.send', title: 'Send KSeF Direct documents to KSeF', module: 'integration_ksef_direct' },
7
+ { id: 'integration_ksef_direct.received_documents.view', title: 'View KSeF Direct received documents', module: 'integration_ksef_direct' },
8
+ { id: 'integration_ksef_direct.received_documents.sync', title: 'Sync KSeF Direct received documents', module: 'integration_ksef_direct' },
9
+ ]