@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,668 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { fetchPublicKey, clearPublicKeyCache, encryptKsefToken } from './ksefCrypto'
|
|
3
|
+
|
|
4
|
+
type KsefEnvironment = 'test' | 'production'
|
|
5
|
+
|
|
6
|
+
const BASE_URLS: Record<KsefEnvironment, string> = {
|
|
7
|
+
test: 'https://api-test.ksef.mf.gov.pl/v2',
|
|
8
|
+
production: 'https://api.ksef.mf.gov.pl/v2',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface KsefCredentials {
|
|
12
|
+
ksefToken: string
|
|
13
|
+
nip: string
|
|
14
|
+
environment: KsefEnvironment
|
|
15
|
+
tenantId?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface KsefTokenCache {
|
|
19
|
+
accessToken: string
|
|
20
|
+
accessTokenExp: Date
|
|
21
|
+
refreshToken: string
|
|
22
|
+
refreshTokenExp: Date
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const TOKEN_CACHE = new Map<string, KsefTokenCache>()
|
|
26
|
+
|
|
27
|
+
const ChallengeResponseSchema = z.object({
|
|
28
|
+
challenge: z.string(),
|
|
29
|
+
timestampMs: z.number().optional(),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const KsefTokenResponseSchema = z.object({
|
|
33
|
+
referenceNumber: z.string(),
|
|
34
|
+
authenticationToken: z.object({
|
|
35
|
+
token: z.string(),
|
|
36
|
+
validUntil: z.string().optional(),
|
|
37
|
+
}),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const AuthStatusSchema = z.object({
|
|
41
|
+
status: z.object({
|
|
42
|
+
code: z.number(),
|
|
43
|
+
description: z.string().optional(),
|
|
44
|
+
}),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const RedeemResponseSchema = z.object({
|
|
48
|
+
sessionToken: z.object({
|
|
49
|
+
token: z.string(),
|
|
50
|
+
generatedAt: z.string().optional(),
|
|
51
|
+
validUntil: z.string().optional(),
|
|
52
|
+
}).optional(),
|
|
53
|
+
accessToken: z.object({
|
|
54
|
+
token: z.string(),
|
|
55
|
+
validUntil: z.string().optional(),
|
|
56
|
+
}).optional(),
|
|
57
|
+
refreshToken: z.object({
|
|
58
|
+
token: z.string(),
|
|
59
|
+
validUntil: z.string().optional(),
|
|
60
|
+
}).optional(),
|
|
61
|
+
}).passthrough()
|
|
62
|
+
|
|
63
|
+
const RateLimitsSchema = z.object({
|
|
64
|
+
otherPerSecond: z.number().optional(),
|
|
65
|
+
otherPerMinute: z.number().optional(),
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
export type KsefRateLimits = z.infer<typeof RateLimitsSchema>
|
|
69
|
+
|
|
70
|
+
function cacheKey(credentials: KsefCredentials): string {
|
|
71
|
+
return `${credentials.environment}:${credentials.tenantId ?? ''}:${credentials.nip}`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function ksefFetch(url: string, options: RequestInit): Promise<Response> {
|
|
75
|
+
const response = await fetch(url, {
|
|
76
|
+
...options,
|
|
77
|
+
signal: AbortSignal.timeout(15_000),
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
if (response.status === 429) {
|
|
81
|
+
const retryAfter = response.headers.get('Retry-After')
|
|
82
|
+
const waitMs = Math.min(retryAfter ? parseInt(retryAfter, 10) * 1000 : 2000, 10_000)
|
|
83
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs))
|
|
84
|
+
return fetch(url, { ...options, signal: AbortSignal.timeout(15_000) })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return response
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function getValidAccessToken(credentials: KsefCredentials): Promise<string> {
|
|
91
|
+
const key = cacheKey(credentials)
|
|
92
|
+
const cached = TOKEN_CACHE.get(key)
|
|
93
|
+
const now = new Date()
|
|
94
|
+
|
|
95
|
+
if (cached) {
|
|
96
|
+
const refreshThreshold = new Date(cached.accessTokenExp.getTime() - 2 * 60 * 1000)
|
|
97
|
+
if (now < refreshThreshold) {
|
|
98
|
+
return cached.accessToken
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (now < cached.refreshTokenExp) {
|
|
102
|
+
const newAccessToken = await refreshAccessToken(cached.refreshToken, credentials.environment)
|
|
103
|
+
TOKEN_CACHE.set(key, {
|
|
104
|
+
...cached,
|
|
105
|
+
accessToken: newAccessToken,
|
|
106
|
+
accessTokenExp: new Date(Date.now() + 14 * 60 * 1000),
|
|
107
|
+
})
|
|
108
|
+
return newAccessToken
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return authenticate(credentials)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function authenticate(credentials: KsefCredentials): Promise<string> {
|
|
116
|
+
const baseUrl = BASE_URLS[credentials.environment]
|
|
117
|
+
|
|
118
|
+
const publicKeyPem = await fetchPublicKey(credentials.environment)
|
|
119
|
+
|
|
120
|
+
const challengeRes = await ksefFetch(`${baseUrl}/auth/challenge`, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: { 'Content-Type': 'application/json' },
|
|
123
|
+
body: JSON.stringify({}),
|
|
124
|
+
})
|
|
125
|
+
if (!challengeRes.ok) {
|
|
126
|
+
throw new KsefAuthError(`Challenge request failed: HTTP ${challengeRes.status}`, 'AUTH_CHALLENGE_FAILED')
|
|
127
|
+
}
|
|
128
|
+
const challengeData = ChallengeResponseSchema.parse(await challengeRes.json())
|
|
129
|
+
|
|
130
|
+
const timestampMs = challengeData.timestampMs ?? Date.now()
|
|
131
|
+
let encryptedToken: string
|
|
132
|
+
try {
|
|
133
|
+
encryptedToken = encryptKsefToken(credentials.ksefToken, timestampMs, publicKeyPem)
|
|
134
|
+
} catch {
|
|
135
|
+
clearPublicKeyCache(credentials.environment)
|
|
136
|
+
const freshKey = await fetchPublicKey(credentials.environment)
|
|
137
|
+
encryptedToken = encryptKsefToken(credentials.ksefToken, timestampMs, freshKey)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const ksefTokenRes = await ksefFetch(`${baseUrl}/auth/ksef-token`, {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
headers: { 'Content-Type': 'application/json' },
|
|
143
|
+
body: JSON.stringify({
|
|
144
|
+
challenge: challengeData.challenge,
|
|
145
|
+
contextIdentifier: { type: 'nip', value: credentials.nip },
|
|
146
|
+
encryptedToken,
|
|
147
|
+
}),
|
|
148
|
+
})
|
|
149
|
+
if (!ksefTokenRes.ok) {
|
|
150
|
+
const body = await ksefTokenRes.text().catch(() => '')
|
|
151
|
+
throw new KsefAuthError(
|
|
152
|
+
`KSeF token submission failed: HTTP ${ksefTokenRes.status}${body ? ` — ${body}` : ''}`,
|
|
153
|
+
ksefTokenRes.status === 401 ? 'AUTH_FAILED' : 'AUTH_KSEF_TOKEN_FAILED',
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
const tokenData = KsefTokenResponseSchema.parse(await ksefTokenRes.json())
|
|
157
|
+
const authToken = tokenData.authenticationToken.token
|
|
158
|
+
|
|
159
|
+
const authStatusData = await pollAuthStatus(baseUrl, tokenData.referenceNumber, authToken)
|
|
160
|
+
if (!authStatusData) {
|
|
161
|
+
throw new KsefAuthError('Authentication timed out waiting for KSeF status', 'AUTH_TIMEOUT')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const redeemRes = await ksefFetch(`${baseUrl}/auth/token/redeem`, {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
headers: {
|
|
167
|
+
'Content-Type': 'application/json',
|
|
168
|
+
'Authorization': `Bearer ${authToken}`,
|
|
169
|
+
},
|
|
170
|
+
body: JSON.stringify({}),
|
|
171
|
+
})
|
|
172
|
+
if (!redeemRes.ok) {
|
|
173
|
+
const body = await redeemRes.text().catch(() => '')
|
|
174
|
+
throw new KsefAuthError(`Token redeem failed: HTTP ${redeemRes.status}${body ? ` — ${body}` : ''}`, 'AUTH_REDEEM_FAILED')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const redeemRaw = await redeemRes.json() as Record<string, unknown>
|
|
178
|
+
const redeemData = RedeemResponseSchema.parse(redeemRaw)
|
|
179
|
+
const sessionTokenObj = redeemData.sessionToken ?? redeemData.accessToken
|
|
180
|
+
if (!sessionTokenObj) {
|
|
181
|
+
throw new KsefAuthError(
|
|
182
|
+
`Unexpected redeem response shape: ${JSON.stringify(redeemRaw)}`,
|
|
183
|
+
'AUTH_REDEEM_FAILED',
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
const accessToken = sessionTokenObj.token
|
|
187
|
+
const refreshTokenValue = redeemData.refreshToken?.token ?? accessToken
|
|
188
|
+
const refreshTokenExp = redeemData.refreshToken?.validUntil
|
|
189
|
+
? new Date(redeemData.refreshToken.validUntil)
|
|
190
|
+
: new Date(Date.now() + 60 * 60 * 1000)
|
|
191
|
+
const accessTokenExp = sessionTokenObj.validUntil
|
|
192
|
+
? new Date(sessionTokenObj.validUntil)
|
|
193
|
+
: new Date(Date.now() + 14 * 60 * 1000)
|
|
194
|
+
|
|
195
|
+
TOKEN_CACHE.set(cacheKey(credentials), {
|
|
196
|
+
accessToken,
|
|
197
|
+
accessTokenExp,
|
|
198
|
+
refreshToken: refreshTokenValue,
|
|
199
|
+
refreshTokenExp,
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
return accessToken
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function pollAuthStatus(baseUrl: string, referenceNumber: string, authToken: string): Promise<boolean> {
|
|
206
|
+
const deadline = Date.now() + 30_000
|
|
207
|
+
let delay = 1000
|
|
208
|
+
|
|
209
|
+
while (Date.now() < deadline) {
|
|
210
|
+
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
211
|
+
delay = Math.min(delay * 1.5, 5000)
|
|
212
|
+
|
|
213
|
+
const res = await ksefFetch(`${baseUrl}/auth/${referenceNumber}`, {
|
|
214
|
+
method: 'GET',
|
|
215
|
+
headers: {
|
|
216
|
+
'Accept': 'application/json',
|
|
217
|
+
'Authorization': `Bearer ${authToken}`,
|
|
218
|
+
},
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
if (res.ok) {
|
|
222
|
+
const data = AuthStatusSchema.safeParse(await res.json())
|
|
223
|
+
if (data.success) {
|
|
224
|
+
const code = data.data.status.code
|
|
225
|
+
if (code === 200) return true
|
|
226
|
+
if (code >= 400) {
|
|
227
|
+
throw new KsefAuthError(
|
|
228
|
+
`KSeF authentication failed: ${data.data.status.description ?? `status ${code}`}`,
|
|
229
|
+
'AUTH_FAILED',
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return false
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function refreshAccessToken(refreshToken: string, environment: KsefEnvironment): Promise<string> {
|
|
240
|
+
const baseUrl = BASE_URLS[environment]
|
|
241
|
+
const res = await ksefFetch(`${baseUrl}/auth/token/refresh`, {
|
|
242
|
+
method: 'POST',
|
|
243
|
+
headers: {
|
|
244
|
+
'Content-Type': 'application/json',
|
|
245
|
+
'Authorization': `Bearer ${refreshToken}`,
|
|
246
|
+
},
|
|
247
|
+
body: JSON.stringify({}),
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
if (!res.ok) {
|
|
251
|
+
throw new KsefAuthError(`Token refresh failed: HTTP ${res.status}`, 'AUTH_REFRESH_FAILED')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const raw = await res.json() as Record<string, unknown>
|
|
255
|
+
const data = RedeemResponseSchema.parse(raw)
|
|
256
|
+
const token = data.sessionToken?.token ?? data.accessToken?.token
|
|
257
|
+
if (!token) throw new KsefAuthError('Unexpected refresh response shape', 'AUTH_REFRESH_FAILED')
|
|
258
|
+
return token
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function verifyAccess(credentials: KsefCredentials): Promise<KsefRateLimits> {
|
|
262
|
+
const baseUrl = BASE_URLS[credentials.environment]
|
|
263
|
+
const key = cacheKey(credentials)
|
|
264
|
+
|
|
265
|
+
let accessToken: string
|
|
266
|
+
try {
|
|
267
|
+
accessToken = await getValidAccessToken(credentials)
|
|
268
|
+
} catch (err) {
|
|
269
|
+
TOKEN_CACHE.delete(key)
|
|
270
|
+
throw err
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const res = await ksefFetch(`${baseUrl}/rate-limits`, {
|
|
274
|
+
method: 'GET',
|
|
275
|
+
headers: {
|
|
276
|
+
'Accept': 'application/json',
|
|
277
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
278
|
+
},
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
if (res.status === 401) {
|
|
282
|
+
TOKEN_CACHE.delete(key)
|
|
283
|
+
const freshToken = await authenticate(credentials)
|
|
284
|
+
const retryRes = await ksefFetch(`${baseUrl}/rate-limits`, {
|
|
285
|
+
method: 'GET',
|
|
286
|
+
headers: {
|
|
287
|
+
'Accept': 'application/json',
|
|
288
|
+
'Authorization': `Bearer ${freshToken}`,
|
|
289
|
+
},
|
|
290
|
+
})
|
|
291
|
+
if (!retryRes.ok) {
|
|
292
|
+
throw new KsefNetworkError(`Rate limits check failed after re-auth: HTTP ${retryRes.status}`)
|
|
293
|
+
}
|
|
294
|
+
const parsed = RateLimitsSchema.safeParse(await retryRes.json())
|
|
295
|
+
return parsed.success ? parsed.data : {}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!res.ok) {
|
|
299
|
+
throw new KsefNetworkError(`Rate limits check failed: HTTP ${res.status}`)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const parsed = RateLimitsSchema.safeParse(await res.json())
|
|
303
|
+
return parsed.success ? parsed.data : {}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function clearTokenCache(tenantKey?: string): void {
|
|
307
|
+
if (tenantKey) {
|
|
308
|
+
TOKEN_CACHE.delete(tenantKey)
|
|
309
|
+
} else {
|
|
310
|
+
TOKEN_CACHE.clear()
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export class KsefAuthError extends Error {
|
|
315
|
+
readonly errorCode: string
|
|
316
|
+
constructor(message: string, errorCode: string) {
|
|
317
|
+
super(message)
|
|
318
|
+
this.name = 'KsefAuthError'
|
|
319
|
+
this.errorCode = errorCode
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export class KsefNetworkError extends Error {
|
|
324
|
+
readonly errorCode = 'NETWORK_ERROR'
|
|
325
|
+
constructor(message: string) {
|
|
326
|
+
super(message)
|
|
327
|
+
this.name = 'KsefNetworkError'
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const OpenSessionResponseSchema = z.object({
|
|
332
|
+
referenceNumber: z.string(),
|
|
333
|
+
validUntil: z.string(),
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
const SendInvoiceToSessionResponseSchema = z.object({
|
|
337
|
+
referenceNumber: z.string(),
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
const SessionStatusResponseSchema = z.object({
|
|
341
|
+
status: z.object({
|
|
342
|
+
code: z.number(),
|
|
343
|
+
description: z.string(),
|
|
344
|
+
details: z.array(z.string()).optional().nullable(),
|
|
345
|
+
}),
|
|
346
|
+
successfulInvoiceCount: z.number().optional().nullable(),
|
|
347
|
+
failedInvoiceCount: z.number().optional().nullable(),
|
|
348
|
+
}).passthrough()
|
|
349
|
+
|
|
350
|
+
export async function sendInvoice(
|
|
351
|
+
credentials: KsefCredentials,
|
|
352
|
+
payload: {
|
|
353
|
+
encryptedSymmetricKey: string
|
|
354
|
+
initializationVector: string
|
|
355
|
+
encryptedInvoiceContent: string
|
|
356
|
+
invoiceHash: string
|
|
357
|
+
invoiceSize: number
|
|
358
|
+
encryptedInvoiceHash: string
|
|
359
|
+
encryptedInvoiceSize: number
|
|
360
|
+
publicKeyId?: string
|
|
361
|
+
},
|
|
362
|
+
): Promise<{ sessionReferenceNumber: string; invoiceReferenceNumber: string }> {
|
|
363
|
+
const baseUrl = BASE_URLS[credentials.environment]
|
|
364
|
+
const accessToken = await getValidAccessToken(credentials)
|
|
365
|
+
|
|
366
|
+
const openRes = await ksefFetch(`${baseUrl}/sessions/online`, {
|
|
367
|
+
method: 'POST',
|
|
368
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` },
|
|
369
|
+
body: JSON.stringify({
|
|
370
|
+
formCode: { systemCode: 'FA (2)', schemaVersion: '1-0E', value: 'FA' },
|
|
371
|
+
encryption: {
|
|
372
|
+
encryptedSymmetricKey: payload.encryptedSymmetricKey,
|
|
373
|
+
initializationVector: payload.initializationVector,
|
|
374
|
+
...(payload.publicKeyId ? { publicKeyId: payload.publicKeyId } : {}),
|
|
375
|
+
},
|
|
376
|
+
}),
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
if (!openRes.ok) {
|
|
380
|
+
const body = await openRes.text().catch(() => '')
|
|
381
|
+
throw new KsefNetworkError(`Session open failed: HTTP ${openRes.status}${body ? ` — ${body}` : ''}`)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const openData = OpenSessionResponseSchema.parse(await openRes.json())
|
|
385
|
+
const sessionReferenceNumber = openData.referenceNumber
|
|
386
|
+
|
|
387
|
+
let invoiceReferenceNumber: string
|
|
388
|
+
try {
|
|
389
|
+
const sendRes = await ksefFetch(
|
|
390
|
+
`${baseUrl}/sessions/online/${encodeURIComponent(sessionReferenceNumber)}/invoices`,
|
|
391
|
+
{
|
|
392
|
+
method: 'POST',
|
|
393
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` },
|
|
394
|
+
body: JSON.stringify({
|
|
395
|
+
invoiceHash: payload.invoiceHash,
|
|
396
|
+
invoiceSize: payload.invoiceSize,
|
|
397
|
+
encryptedInvoiceHash: payload.encryptedInvoiceHash,
|
|
398
|
+
encryptedInvoiceSize: payload.encryptedInvoiceSize,
|
|
399
|
+
encryptedInvoiceContent: payload.encryptedInvoiceContent,
|
|
400
|
+
}),
|
|
401
|
+
},
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
if (!sendRes.ok) {
|
|
405
|
+
const body = await sendRes.text().catch(() => '')
|
|
406
|
+
throw new KsefNetworkError(`Invoice send failed: HTTP ${sendRes.status}${body ? ` — ${body}` : ''}`)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const sendData = SendInvoiceToSessionResponseSchema.parse(await sendRes.json())
|
|
410
|
+
invoiceReferenceNumber = sendData.referenceNumber
|
|
411
|
+
} catch (err) {
|
|
412
|
+
await ksefFetch(
|
|
413
|
+
`${baseUrl}/sessions/online/${encodeURIComponent(sessionReferenceNumber)}/close`,
|
|
414
|
+
{ method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}` } },
|
|
415
|
+
).catch(() => {})
|
|
416
|
+
throw err
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const closeRes = await ksefFetch(
|
|
420
|
+
`${baseUrl}/sessions/online/${encodeURIComponent(sessionReferenceNumber)}/close`,
|
|
421
|
+
{ method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}` } },
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
return { sessionReferenceNumber, invoiceReferenceNumber }
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export interface KsefReceivedInvoiceSummary {
|
|
429
|
+
ksefReferenceNumber: string
|
|
430
|
+
sessionReferenceNumber: string
|
|
431
|
+
issueDate: string | null
|
|
432
|
+
sellerNip: string | null
|
|
433
|
+
sellerName: string | null
|
|
434
|
+
grossAmount: string | null
|
|
435
|
+
netAmount: string | null
|
|
436
|
+
vatAmount: string | null
|
|
437
|
+
currency: string | null
|
|
438
|
+
invoiceNumber: string | null
|
|
439
|
+
upoDownloadUrl: string | null
|
|
440
|
+
invoiceDownloadUrl: string | null
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const SessionListResponseSchema = z.object({
|
|
444
|
+
sessions: z.array(z.object({
|
|
445
|
+
referenceNumber: z.string(),
|
|
446
|
+
}).passthrough()).default([]),
|
|
447
|
+
continuationToken: z.string().nullable().optional(),
|
|
448
|
+
}).passthrough()
|
|
449
|
+
|
|
450
|
+
const SessionInvoiceItemSchema = z.object({
|
|
451
|
+
referenceNumber: z.string().optional(),
|
|
452
|
+
ksefNumber: z.string().optional(),
|
|
453
|
+
ksefReferenceNumber: z.string().optional(),
|
|
454
|
+
issueDate: z.string().nullable().optional(),
|
|
455
|
+
subjectBy: z.object({
|
|
456
|
+
identifier: z.object({
|
|
457
|
+
type: z.string().optional(),
|
|
458
|
+
identifier: z.string().optional(),
|
|
459
|
+
}).optional(),
|
|
460
|
+
name: z.string().optional(),
|
|
461
|
+
}).passthrough().optional(),
|
|
462
|
+
grossAmount: z.union([z.string(), z.number()]).nullable().optional(),
|
|
463
|
+
netAmount: z.union([z.string(), z.number()]).nullable().optional(),
|
|
464
|
+
vatAmount: z.union([z.string(), z.number()]).nullable().optional(),
|
|
465
|
+
currency: z.string().nullable().optional(),
|
|
466
|
+
invoiceNumber: z.string().nullable().optional(),
|
|
467
|
+
upoDownloadUrl: z.string().nullable().optional(),
|
|
468
|
+
invoiceDownloadUrl: z.string().nullable().optional(),
|
|
469
|
+
}).passthrough()
|
|
470
|
+
|
|
471
|
+
const SessionInvoiceListResponseSchema = z.object({
|
|
472
|
+
invoices: z.array(SessionInvoiceItemSchema).default([]),
|
|
473
|
+
continuationToken: z.string().nullable().optional(),
|
|
474
|
+
}).passthrough()
|
|
475
|
+
|
|
476
|
+
async function fetchAllSessionInvoices(
|
|
477
|
+
baseUrl: string,
|
|
478
|
+
accessToken: string,
|
|
479
|
+
sessionRef: string,
|
|
480
|
+
): Promise<KsefReceivedInvoiceSummary[]> {
|
|
481
|
+
const results: KsefReceivedInvoiceSummary[] = []
|
|
482
|
+
let continuationToken: string | null | undefined = undefined
|
|
483
|
+
let isFirst = true
|
|
484
|
+
|
|
485
|
+
while (isFirst || continuationToken) {
|
|
486
|
+
isFirst = false
|
|
487
|
+
const url = new URL(`${baseUrl}/sessions/${encodeURIComponent(sessionRef)}/invoices`)
|
|
488
|
+
if (continuationToken) url.searchParams.set('continuationToken', continuationToken)
|
|
489
|
+
|
|
490
|
+
const res = await ksefFetch(url.toString(), {
|
|
491
|
+
method: 'GET',
|
|
492
|
+
headers: { 'Accept': 'application/json', 'Authorization': `Bearer ${accessToken}` },
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
if (!res.ok) break
|
|
496
|
+
|
|
497
|
+
const data = SessionInvoiceListResponseSchema.parse(await res.json())
|
|
498
|
+
continuationToken = data.continuationToken ?? null
|
|
499
|
+
|
|
500
|
+
for (const inv of data.invoices) {
|
|
501
|
+
const ksefRef = inv.ksefNumber ?? inv.ksefReferenceNumber
|
|
502
|
+
if (!ksefRef) continue
|
|
503
|
+
const grossRaw = inv.grossAmount
|
|
504
|
+
const netRaw = inv.netAmount
|
|
505
|
+
const vatRaw = inv.vatAmount
|
|
506
|
+
results.push({
|
|
507
|
+
ksefReferenceNumber: ksefRef,
|
|
508
|
+
sessionReferenceNumber: sessionRef,
|
|
509
|
+
issueDate: inv.issueDate ?? null,
|
|
510
|
+
sellerNip: inv.subjectBy?.identifier?.identifier ?? null,
|
|
511
|
+
sellerName: inv.subjectBy?.name ?? null,
|
|
512
|
+
grossAmount: grossRaw != null ? String(grossRaw) : null,
|
|
513
|
+
netAmount: netRaw != null ? String(netRaw) : null,
|
|
514
|
+
vatAmount: vatRaw != null ? String(vatRaw) : null,
|
|
515
|
+
currency: inv.currency ?? null,
|
|
516
|
+
invoiceNumber: inv.invoiceNumber ?? null,
|
|
517
|
+
upoDownloadUrl: inv.upoDownloadUrl ?? null,
|
|
518
|
+
invoiceDownloadUrl: inv.invoiceDownloadUrl ?? null,
|
|
519
|
+
})
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (!continuationToken) break
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return results
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export async function queryReceivedInvoices(
|
|
529
|
+
credentials: KsefCredentials,
|
|
530
|
+
params: {
|
|
531
|
+
dateFrom: string
|
|
532
|
+
dateTo: string
|
|
533
|
+
},
|
|
534
|
+
): Promise<{ items: KsefReceivedInvoiceSummary[]; totalCount: number }> {
|
|
535
|
+
const baseUrl = BASE_URLS[credentials.environment]
|
|
536
|
+
const accessToken = await getValidAccessToken(credentials)
|
|
537
|
+
const allItems: KsefReceivedInvoiceSummary[] = []
|
|
538
|
+
|
|
539
|
+
let continuationToken: string | null | undefined = undefined
|
|
540
|
+
let isFirst = true
|
|
541
|
+
|
|
542
|
+
while (isFirst || continuationToken) {
|
|
543
|
+
isFirst = false
|
|
544
|
+
const sessionUrl = new URL(`${baseUrl}/sessions`)
|
|
545
|
+
sessionUrl.searchParams.set('sessionType', 'online')
|
|
546
|
+
sessionUrl.searchParams.set('direction', 'received')
|
|
547
|
+
sessionUrl.searchParams.set('dateFrom', params.dateFrom)
|
|
548
|
+
sessionUrl.searchParams.set('dateTo', params.dateTo)
|
|
549
|
+
if (continuationToken) sessionUrl.searchParams.set('continuationToken', continuationToken)
|
|
550
|
+
|
|
551
|
+
const res = await ksefFetch(sessionUrl.toString(), {
|
|
552
|
+
method: 'GET',
|
|
553
|
+
headers: { 'Accept': 'application/json', 'Authorization': `Bearer ${accessToken}` },
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
if (!res.ok) {
|
|
557
|
+
const body = await res.text().catch(() => '')
|
|
558
|
+
throw new KsefNetworkError(`Query received sessions failed: HTTP ${res.status}${body ? ` — ${body}` : ''}`)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const sessionData = SessionListResponseSchema.parse(await res.json())
|
|
562
|
+
continuationToken = sessionData.continuationToken ?? null
|
|
563
|
+
|
|
564
|
+
for (const session of sessionData.sessions) {
|
|
565
|
+
const invoices = await fetchAllSessionInvoices(baseUrl, accessToken, session.referenceNumber)
|
|
566
|
+
allItems.push(...invoices)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (!continuationToken) break
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return { items: allItems, totalCount: allItems.length }
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async function downloadFromUrl(url: string): Promise<string> {
|
|
576
|
+
const res = await fetch(url, {
|
|
577
|
+
method: 'GET',
|
|
578
|
+
signal: AbortSignal.timeout(30_000),
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
if (!res.ok) {
|
|
582
|
+
const body = await res.text().catch(() => '')
|
|
583
|
+
throw new KsefNetworkError(`Invoice download failed: HTTP ${res.status}${body ? ` — ${body}` : ''}`)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return res.text()
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export async function downloadInvoiceFromUrl(url: string): Promise<string> {
|
|
590
|
+
return downloadFromUrl(url)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export async function downloadInvoice(
|
|
594
|
+
credentials: KsefCredentials,
|
|
595
|
+
ksefReferenceNumber: string,
|
|
596
|
+
): Promise<{ rawContent: string; upoDownloadUrl: string | null; invoiceDownloadUrl: string | null }> {
|
|
597
|
+
const baseUrl = BASE_URLS[credentials.environment]
|
|
598
|
+
const accessToken = await getValidAccessToken(credentials)
|
|
599
|
+
|
|
600
|
+
const res = await ksefFetch(`${baseUrl}/invoices/ksef/${encodeURIComponent(ksefReferenceNumber)}`, {
|
|
601
|
+
method: 'GET',
|
|
602
|
+
headers: { 'Accept': 'application/octet-stream', 'Authorization': `Bearer ${accessToken}` },
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
if (!res.ok) {
|
|
606
|
+
const body = await res.text().catch(() => '')
|
|
607
|
+
throw new KsefNetworkError(`Invoice fetch failed: HTTP ${res.status}${body ? ` — ${body}` : ''}`)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const contentType = res.headers.get('content-type') ?? ''
|
|
611
|
+
let rawContent: string
|
|
612
|
+
|
|
613
|
+
if (contentType.includes('application/json')) {
|
|
614
|
+
const data = await res.json() as Record<string, unknown>
|
|
615
|
+
const downloadUrl = (data['invoiceDownloadUrl'] ?? data['downloadUrl'] ?? data['url']) as string | undefined
|
|
616
|
+
if (downloadUrl) {
|
|
617
|
+
rawContent = await downloadFromUrl(downloadUrl)
|
|
618
|
+
} else if (typeof data['content'] === 'string') {
|
|
619
|
+
rawContent = data['content'] as string
|
|
620
|
+
} else {
|
|
621
|
+
throw new KsefNetworkError(`Unexpected JSON response from /invoices/ksef — keys: ${Object.keys(data).join(', ')}`)
|
|
622
|
+
}
|
|
623
|
+
return {
|
|
624
|
+
rawContent,
|
|
625
|
+
upoDownloadUrl: (data['upoDownloadUrl'] as string | undefined) ?? null,
|
|
626
|
+
invoiceDownloadUrl: (data['invoiceDownloadUrl'] as string | undefined) ?? null,
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
rawContent = await res.text()
|
|
631
|
+
return { rawContent, upoDownloadUrl: null, invoiceDownloadUrl: null }
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
export async function checkInvoiceStatus(
|
|
635
|
+
credentials: KsefCredentials,
|
|
636
|
+
referenceNumber: string,
|
|
637
|
+
): Promise<{ processingCode: number; ksefReferenceNumber?: string; errorDescription?: string }> {
|
|
638
|
+
const baseUrl = BASE_URLS[credentials.environment]
|
|
639
|
+
const accessToken = await getValidAccessToken(credentials)
|
|
640
|
+
|
|
641
|
+
const res = await ksefFetch(`${baseUrl}/sessions/${encodeURIComponent(referenceNumber)}`, {
|
|
642
|
+
method: 'GET',
|
|
643
|
+
headers: { 'Accept': 'application/json', 'Authorization': `Bearer ${accessToken}` },
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
if (!res.ok) {
|
|
647
|
+
const body = await res.text().catch(() => '')
|
|
648
|
+
throw new KsefNetworkError(`Session status check failed: HTTP ${res.status}${body ? ` — ${body}` : ''}`)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const data = SessionStatusResponseSchema.parse(await res.json())
|
|
652
|
+
const code = data.status.code
|
|
653
|
+
|
|
654
|
+
if (code === 200) {
|
|
655
|
+
if ((data.failedInvoiceCount ?? 0) > 0) {
|
|
656
|
+
const details = data.status.details?.join('; ') ?? ''
|
|
657
|
+
return { processingCode: 400, errorDescription: `KSeF rejected ${data.failedInvoiceCount} invoice(s): ${data.status.description}${details ? ` — ${details}` : ''}` }
|
|
658
|
+
}
|
|
659
|
+
return { processingCode: 200 }
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (code >= 400) {
|
|
663
|
+
const details = data.status.details?.join('; ') ?? ''
|
|
664
|
+
return { processingCode: code, errorDescription: `${data.status.description}${details ? ` — ${details}` : ''}` }
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return { processingCode: 100 }
|
|
668
|
+
}
|