ksef-client-ts 0.5.2 → 0.6.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/dist/index.js CHANGED
@@ -298,21 +298,21 @@ var init_rest_request = __esm({
298
298
  _query = [];
299
299
  _presigned = false;
300
300
  _skipAuthRetry = false;
301
- constructor(method, path) {
301
+ constructor(method, path2) {
302
302
  this.method = method;
303
- this.path = path;
303
+ this.path = path2;
304
304
  }
305
- static get(path) {
306
- return new _RestRequest("GET", path);
305
+ static get(path2) {
306
+ return new _RestRequest("GET", path2);
307
307
  }
308
- static post(path) {
309
- return new _RestRequest("POST", path);
308
+ static post(path2) {
309
+ return new _RestRequest("POST", path2);
310
310
  }
311
- static put(path) {
312
- return new _RestRequest("PUT", path);
311
+ static put(path2) {
312
+ return new _RestRequest("PUT", path2);
313
313
  }
314
- static delete(path) {
315
- return new _RestRequest("DELETE", path);
314
+ static delete(path2) {
315
+ return new _RestRequest("DELETE", path2);
316
316
  }
317
317
  body(data) {
318
318
  this._body = data;
@@ -632,9 +632,9 @@ var init_rest_client = __esm({
632
632
  return response;
633
633
  }
634
634
  buildUrl(request) {
635
- const path = this.routeBuilder.build(request.path);
635
+ const path2 = this.routeBuilder.build(request.path);
636
636
  const base = this.options.baseUrl;
637
- const url = new URL(`${base}${path}`);
637
+ const url = new URL(`${base}${path2}`);
638
638
  const query = request.getQuery();
639
639
  for (const [key, value] of query) {
640
640
  url.searchParams.append(key, value);
@@ -2645,11 +2645,11 @@ async function validateSchema(xml, options, _parsed) {
2645
2645
  const prefix = rootElement ? `/${rootElement}/` : "/";
2646
2646
  const validationErrors = result.error.issues.map((issue) => {
2647
2647
  const zodPath = issue.path.join("/");
2648
- const path = zodPath ? `${prefix}${zodPath}` : rootElement ? `/${rootElement}` : void 0;
2648
+ const path2 = zodPath ? `${prefix}${zodPath}` : rootElement ? `/${rootElement}` : void 0;
2649
2649
  return {
2650
2650
  code: mapZodErrorCode(issue),
2651
2651
  message: issue.message,
2652
- path
2652
+ path: path2
2653
2653
  };
2654
2654
  });
2655
2655
  return { valid: false, schemaType, errors: validationErrors };
@@ -2689,9 +2689,9 @@ function validateBusinessRules(xml, _parsed) {
2689
2689
  collectDateErrors(object, rootElement, errors);
2690
2690
  return { valid: errors.length === 0, schemaType, errors };
2691
2691
  }
2692
- function collectNipPeselErrors(obj, path, errors) {
2692
+ function collectNipPeselErrors(obj, path2, errors) {
2693
2693
  for (const [key, value] of Object.entries(obj)) {
2694
- const currentPath = path ? `${path}/${key}` : key;
2694
+ const currentPath = path2 ? `${path2}/${key}` : key;
2695
2695
  if (key === "NIP" && typeof value === "string") {
2696
2696
  if (!isValidNip(value)) {
2697
2697
  errors.push({
@@ -2770,6 +2770,88 @@ var init_invoice_validator = __esm({
2770
2770
  }
2771
2771
  });
2772
2772
 
2773
+ // src/models/document-structures/types.ts
2774
+ var SystemCode, FORM_CODES, DEFAULT_FORM_CODE, INVOICE_TYPES_BY_SYSTEM_CODE, FORM_CODE_KEYS;
2775
+ var init_types = __esm({
2776
+ "src/models/document-structures/types.ts"() {
2777
+ "use strict";
2778
+ SystemCode = {
2779
+ FA_2: "FA (2)",
2780
+ FA_3: "FA (3)",
2781
+ PEF_3: "PEF (3)",
2782
+ PEF_KOR_3: "PEF_KOR (3)",
2783
+ FA_RR_1: "FA_RR (1)"
2784
+ };
2785
+ FORM_CODES = {
2786
+ FA_2: { systemCode: "FA (2)", schemaVersion: "1-0E", value: "FA" },
2787
+ FA_3: { systemCode: "FA (3)", schemaVersion: "1-0E", value: "FA" },
2788
+ PEF_3: { systemCode: "PEF (3)", schemaVersion: "2-1", value: "PEF" },
2789
+ PEF_KOR_3: { systemCode: "PEF_KOR (3)", schemaVersion: "2-1", value: "PEF" },
2790
+ FA_RR_1_LEGACY: { systemCode: "FA_RR (1)", schemaVersion: "1-0E", value: "RR" },
2791
+ FA_RR_1_TRANSITION: { systemCode: "FA_RR (1)", schemaVersion: "1-1E", value: "RR" },
2792
+ FA_RR_1: { systemCode: "FA_RR (1)", schemaVersion: "1-1E", value: "FA_RR" }
2793
+ };
2794
+ DEFAULT_FORM_CODE = FORM_CODES.FA_3;
2795
+ INVOICE_TYPES_BY_SYSTEM_CODE = {
2796
+ [SystemCode.FA_2]: ["Vat", "Zal", "Kor", "Roz", "Upr", "KorZal", "KorRoz"],
2797
+ [SystemCode.FA_3]: ["Vat", "Zal", "Kor", "Roz", "Upr", "KorZal", "KorRoz"],
2798
+ [SystemCode.PEF_3]: ["VatPef", "VatPefSp", "KorPef"],
2799
+ [SystemCode.PEF_KOR_3]: ["KorPef"],
2800
+ [SystemCode.FA_RR_1]: ["VatRr", "KorVatRr"]
2801
+ };
2802
+ FORM_CODE_KEYS = {
2803
+ FA2: FORM_CODES.FA_2,
2804
+ FA3: FORM_CODES.FA_3,
2805
+ PEF3: FORM_CODES.PEF_3,
2806
+ PEFKOR3: FORM_CODES.PEF_KOR_3,
2807
+ FARR1: FORM_CODES.FA_RR_1
2808
+ };
2809
+ }
2810
+ });
2811
+
2812
+ // src/models/document-structures/helpers.ts
2813
+ function getFormCode(systemCode) {
2814
+ return SYSTEM_CODE_TO_FORM_CODE[systemCode];
2815
+ }
2816
+ function parseFormCode(raw) {
2817
+ const match = ALL_FORM_CODES.find(
2818
+ (fc) => fc.systemCode === raw.systemCode && fc.schemaVersion === raw.schemaVersion && fc.value === raw.value
2819
+ );
2820
+ return match ?? raw;
2821
+ }
2822
+ function validateFormCodeForSession(formCode, sessionType) {
2823
+ if (sessionType === "online") return true;
2824
+ return !BATCH_DISALLOWED_SYSTEM_CODES.has(formCode.systemCode);
2825
+ }
2826
+ var SYSTEM_CODE_TO_FORM_CODE, ALL_FORM_CODES, BATCH_DISALLOWED_SYSTEM_CODES;
2827
+ var init_helpers = __esm({
2828
+ "src/models/document-structures/helpers.ts"() {
2829
+ "use strict";
2830
+ init_types();
2831
+ SYSTEM_CODE_TO_FORM_CODE = {
2832
+ [SystemCode.FA_2]: FORM_CODES.FA_2,
2833
+ [SystemCode.FA_3]: FORM_CODES.FA_3,
2834
+ [SystemCode.PEF_3]: FORM_CODES.PEF_3,
2835
+ [SystemCode.PEF_KOR_3]: FORM_CODES.PEF_KOR_3,
2836
+ [SystemCode.FA_RR_1]: FORM_CODES.FA_RR_1
2837
+ };
2838
+ ALL_FORM_CODES = Object.values(FORM_CODES);
2839
+ BATCH_DISALLOWED_SYSTEM_CODES = /* @__PURE__ */ new Set([
2840
+ SystemCode.PEF_3,
2841
+ SystemCode.PEF_KOR_3
2842
+ ]);
2843
+ }
2844
+ });
2845
+
2846
+ // src/models/document-structures/index.ts
2847
+ var init_document_structures = __esm({
2848
+ "src/models/document-structures/index.ts"() {
2849
+ "use strict";
2850
+ init_types();
2851
+ init_helpers();
2852
+ }
2853
+ });
2854
+
2773
2855
  // src/services/auth.ts
2774
2856
  var AuthService;
2775
2857
  var init_auth = __esm({
@@ -3335,20 +3417,20 @@ var init_lighthouse = __esm({
3335
3417
  this.lighthouseUrl = options.lighthouseUrl;
3336
3418
  this.timeout = options.timeout;
3337
3419
  }
3338
- async fetchJson(path) {
3420
+ async fetchJson(path2) {
3339
3421
  if (!this.lighthouseUrl) {
3340
3422
  throw new KSeFError(
3341
3423
  "Lighthouse API is not available for the DEMO environment. Use TEST or PROD instead."
3342
3424
  );
3343
3425
  }
3344
- const response = await fetch(`${this.lighthouseUrl}${path}`, {
3426
+ const response = await fetch(`${this.lighthouseUrl}${path2}`, {
3345
3427
  headers: { Accept: "application/json" },
3346
3428
  signal: AbortSignal.timeout(this.timeout)
3347
3429
  });
3348
3430
  if (!response.ok) {
3349
3431
  const body = await response.text();
3350
3432
  throw new KSeFError(
3351
- `Lighthouse ${path} failed: HTTP ${response.status} \u2014 ${body}`
3433
+ `Lighthouse ${path2} failed: HTTP ${response.status} \u2014 ${body}`
3352
3434
  );
3353
3435
  }
3354
3436
  return await response.json();
@@ -4328,6 +4410,391 @@ var init_zip = __esm({
4328
4410
  }
4329
4411
  });
4330
4412
 
4413
+ // src/offline/deadline.ts
4414
+ function getDefaultReason(mode) {
4415
+ switch (mode) {
4416
+ case "offline24":
4417
+ return "PLANNED";
4418
+ case "offline":
4419
+ return "SYSTEM_UNAVAILABLE";
4420
+ case "awaryjny":
4421
+ return "EMERGENCY";
4422
+ case "awaria_calkowita":
4423
+ return "TOTAL_FAILURE";
4424
+ }
4425
+ }
4426
+ function nextBusinessDay(from) {
4427
+ const d = new Date(from);
4428
+ d.setUTCDate(d.getUTCDate() + 1);
4429
+ while (d.getUTCDay() === 0 || d.getUTCDay() === 6) {
4430
+ d.setUTCDate(d.getUTCDate() + 1);
4431
+ }
4432
+ return d;
4433
+ }
4434
+ function addBusinessDays(from, days) {
4435
+ if (!Number.isInteger(days) || days < 0) {
4436
+ throw new Error(`days must be a non-negative integer, got ${days}`);
4437
+ }
4438
+ const d = new Date(from);
4439
+ let remaining = days;
4440
+ while (remaining > 0) {
4441
+ d.setUTCDate(d.getUTCDate() + 1);
4442
+ if (d.getUTCDay() !== 0 && d.getUTCDay() !== 6) {
4443
+ remaining--;
4444
+ }
4445
+ }
4446
+ return d;
4447
+ }
4448
+ function endOfDay(d) {
4449
+ const result = new Date(d);
4450
+ result.setUTCHours(23, 59, 59, 999);
4451
+ return result;
4452
+ }
4453
+ function getMaintenanceEndFallback(mw) {
4454
+ if (mw.endTime) {
4455
+ return new Date(mw.endTime);
4456
+ }
4457
+ const start = new Date(mw.startTime);
4458
+ start.setUTCDate(start.getUTCDate() + 7);
4459
+ return start;
4460
+ }
4461
+ function calculateOfflineDeadline(mode, invoiceDate, maintenanceWindow) {
4462
+ const date = typeof invoiceDate === "string" ? new Date(invoiceDate) : invoiceDate;
4463
+ switch (mode) {
4464
+ case "offline24": {
4465
+ return endOfDay(nextBusinessDay(date));
4466
+ }
4467
+ case "offline": {
4468
+ const base = maintenanceWindow ? getMaintenanceEndFallback(maintenanceWindow) : date;
4469
+ return endOfDay(nextBusinessDay(base));
4470
+ }
4471
+ case "awaryjny": {
4472
+ const base = maintenanceWindow ? getMaintenanceEndFallback(maintenanceWindow) : date;
4473
+ return endOfDay(addBusinessDays(base, 7));
4474
+ }
4475
+ case "awaria_calkowita": {
4476
+ return new Date(FAR_FUTURE);
4477
+ }
4478
+ }
4479
+ }
4480
+ function extendDeadlineForMaintenance(currentDeadline, maintenanceWindow, mode = "awaryjny") {
4481
+ const current = typeof currentDeadline === "string" ? new Date(currentDeadline) : new Date(currentDeadline);
4482
+ const mwEnd = getMaintenanceEndFallback(maintenanceWindow);
4483
+ if (mwEnd.getTime() > current.getTime()) {
4484
+ switch (mode) {
4485
+ case "offline24":
4486
+ case "offline":
4487
+ return endOfDay(nextBusinessDay(mwEnd));
4488
+ case "awaryjny":
4489
+ return endOfDay(addBusinessDays(mwEnd, 7));
4490
+ case "awaria_calkowita":
4491
+ return new Date(FAR_FUTURE);
4492
+ }
4493
+ }
4494
+ return current;
4495
+ }
4496
+ function isExpired(submitBy) {
4497
+ const deadline = typeof submitBy === "string" ? new Date(submitBy) : submitBy;
4498
+ return Date.now() > deadline.getTime();
4499
+ }
4500
+ function getTimeUntilDeadline(submitBy) {
4501
+ const deadline = typeof submitBy === "string" ? new Date(submitBy) : submitBy;
4502
+ return Math.max(0, deadline.getTime() - Date.now());
4503
+ }
4504
+ var FAR_FUTURE;
4505
+ var init_deadline = __esm({
4506
+ "src/offline/deadline.ts"() {
4507
+ "use strict";
4508
+ FAR_FUTURE = /* @__PURE__ */ new Date("9999-12-31T23:59:59Z");
4509
+ }
4510
+ });
4511
+
4512
+ // src/workflows/offline-invoice-workflow.ts
4513
+ import crypto6 from "crypto";
4514
+ var OfflineInvoiceWorkflow;
4515
+ var init_offline_invoice_workflow = __esm({
4516
+ "src/workflows/offline-invoice-workflow.ts"() {
4517
+ "use strict";
4518
+ init_ksef_api_error();
4519
+ init_deadline();
4520
+ init_document_structures();
4521
+ OfflineInvoiceWorkflow = class {
4522
+ constructor(qrService) {
4523
+ this.qrService = qrService;
4524
+ }
4525
+ async generate(input, options) {
4526
+ if (!input.invoiceXml || input.invoiceXml.trim().length === 0) {
4527
+ throw new Error("invoiceXml must not be empty");
4528
+ }
4529
+ if (!input.invoiceNumber) {
4530
+ throw new Error("invoiceNumber is required");
4531
+ }
4532
+ if (!input.sellerNip) {
4533
+ throw new Error("sellerNip is required");
4534
+ }
4535
+ const mode = options?.mode ?? "offline24";
4536
+ const reason = getDefaultReason(mode);
4537
+ const invoiceHashBase64 = crypto6.createHash("sha256").update(input.invoiceXml).digest("base64");
4538
+ const kod1Url = this.qrService.buildInvoiceVerificationUrl(
4539
+ input.sellerNip,
4540
+ input.invoiceDate,
4541
+ invoiceHashBase64
4542
+ );
4543
+ let kod2Url;
4544
+ if (options?.certificate) {
4545
+ kod2Url = this.qrService.buildCertificateVerificationUrl(
4546
+ input.sellerIdentifier.type,
4547
+ input.sellerIdentifier.value,
4548
+ input.sellerNip,
4549
+ options.certificate.certificateSerial,
4550
+ invoiceHashBase64,
4551
+ options.certificate.privateKeyPem
4552
+ );
4553
+ }
4554
+ const submitBy = options?.customDeadline ? typeof options.customDeadline === "string" ? options.customDeadline : options.customDeadline.toISOString() : calculateOfflineDeadline(mode, input.invoiceDate, options?.maintenanceWindow).toISOString();
4555
+ const metadata = {
4556
+ id: crypto6.randomUUID(),
4557
+ mode,
4558
+ reason,
4559
+ status: "GENERATED",
4560
+ invoiceNumber: input.invoiceNumber,
4561
+ invoiceDate: input.invoiceDate,
4562
+ invoiceXml: input.invoiceXml,
4563
+ sellerNip: input.sellerNip,
4564
+ sellerIdentifier: input.sellerIdentifier,
4565
+ buyerIdentifier: input.buyerIdentifier,
4566
+ totalAmount: input.totalAmount,
4567
+ currency: input.currency,
4568
+ kod1Url,
4569
+ kod2Url,
4570
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4571
+ submitBy,
4572
+ maintenanceWindowId: options?.maintenanceWindow?.id
4573
+ };
4574
+ if (options?.storage) {
4575
+ await options.storage.save(metadata);
4576
+ }
4577
+ return metadata;
4578
+ }
4579
+ async submit(client, options) {
4580
+ const { storage, checkExpiry = true } = options;
4581
+ let invoices;
4582
+ if (options.invoiceIds) {
4583
+ invoices = [];
4584
+ const notFound = [];
4585
+ for (const id of options.invoiceIds) {
4586
+ const inv = await storage.get(id);
4587
+ if (inv) {
4588
+ invoices.push(inv);
4589
+ } else {
4590
+ notFound.push(id);
4591
+ }
4592
+ }
4593
+ if (notFound.length > 0) {
4594
+ throw new Error(`Offline invoice(s) not found: ${notFound.join(", ")}`);
4595
+ }
4596
+ } else {
4597
+ invoices = await storage.list({ status: ["GENERATED", "QUEUED", "SUBMITTED"] });
4598
+ }
4599
+ const result = {
4600
+ total: invoices.length,
4601
+ submitted: 0,
4602
+ accepted: 0,
4603
+ rejected: 0,
4604
+ failed: 0,
4605
+ expired: 0,
4606
+ results: []
4607
+ };
4608
+ if (invoices.length === 0) return result;
4609
+ const pending = [];
4610
+ for (const inv of invoices) {
4611
+ if (checkExpiry && isExpired(inv.submitBy)) {
4612
+ await storage.update(inv.id, { status: "EXPIRED" });
4613
+ result.expired++;
4614
+ result.results.push({
4615
+ invoiceId: inv.id,
4616
+ invoiceNumber: inv.invoiceNumber,
4617
+ status: "EXPIRED"
4618
+ });
4619
+ } else {
4620
+ pending.push(inv);
4621
+ }
4622
+ }
4623
+ if (pending.length === 0) return result;
4624
+ await client.crypto.init();
4625
+ const encData = client.crypto.getEncryptionData();
4626
+ const formCode = options.formCode ?? DEFAULT_FORM_CODE;
4627
+ const openResp = await client.onlineSession.openSession(
4628
+ { formCode, encryption: encData.encryptionInfo }
4629
+ );
4630
+ const sessionRef = openResp.referenceNumber;
4631
+ try {
4632
+ for (const inv of pending) {
4633
+ await storage.update(inv.id, { status: "QUEUED" });
4634
+ try {
4635
+ const data = new TextEncoder().encode(inv.invoiceXml);
4636
+ const plainMeta = client.crypto.getFileMetadata(data);
4637
+ const encrypted = client.crypto.encryptAES256(data, encData.cipherKey, encData.cipherIv);
4638
+ const encMeta = client.crypto.getFileMetadata(encrypted);
4639
+ await storage.update(inv.id, { status: "SUBMITTED", submittedAt: (/* @__PURE__ */ new Date()).toISOString() });
4640
+ const resp = await client.onlineSession.sendInvoice(sessionRef, {
4641
+ invoiceHash: plainMeta.hashSHA,
4642
+ invoiceSize: plainMeta.fileSize,
4643
+ encryptedInvoiceHash: encMeta.hashSHA,
4644
+ encryptedInvoiceSize: encMeta.fileSize,
4645
+ encryptedInvoiceContent: Buffer.from(encrypted).toString("base64"),
4646
+ offlineMode: true
4647
+ });
4648
+ await storage.update(inv.id, {
4649
+ status: "ACCEPTED",
4650
+ acceptedAt: (/* @__PURE__ */ new Date()).toISOString(),
4651
+ ksefReferenceNumber: resp.referenceNumber
4652
+ });
4653
+ result.submitted++;
4654
+ result.accepted++;
4655
+ result.results.push({
4656
+ invoiceId: inv.id,
4657
+ invoiceNumber: inv.invoiceNumber,
4658
+ status: "ACCEPTED",
4659
+ ksefReferenceNumber: resp.referenceNumber
4660
+ });
4661
+ } catch (err) {
4662
+ const message = err instanceof Error ? err.message : String(err);
4663
+ if (err instanceof KSeFApiError) {
4664
+ await storage.update(inv.id, {
4665
+ status: "REJECTED",
4666
+ error: { code: err.statusCode, message }
4667
+ });
4668
+ result.submitted++;
4669
+ result.rejected++;
4670
+ result.results.push({
4671
+ invoiceId: inv.id,
4672
+ invoiceNumber: inv.invoiceNumber,
4673
+ status: "REJECTED",
4674
+ error: { code: err.statusCode, message }
4675
+ });
4676
+ } else {
4677
+ await storage.update(inv.id, {
4678
+ status: "QUEUED",
4679
+ error: { code: 0, message }
4680
+ });
4681
+ result.failed++;
4682
+ result.results.push({
4683
+ invoiceId: inv.id,
4684
+ invoiceNumber: inv.invoiceNumber,
4685
+ status: "QUEUED",
4686
+ error: { code: 0, message }
4687
+ });
4688
+ }
4689
+ }
4690
+ }
4691
+ } finally {
4692
+ try {
4693
+ await client.onlineSession.closeSession(sessionRef);
4694
+ } catch {
4695
+ }
4696
+ }
4697
+ return result;
4698
+ }
4699
+ // TODO(perf): Each correction opens a separate KSeF session.
4700
+ // For batch corrections, consider a correctBatch() sharing one session (like submit()).
4701
+ async correct(client, options) {
4702
+ const { storage, rejectedInvoiceId, correctedInvoiceXml } = options;
4703
+ const original = await storage.get(rejectedInvoiceId);
4704
+ if (!original) {
4705
+ throw new Error(`Offline invoice not found: ${rejectedInvoiceId}`);
4706
+ }
4707
+ if (original.status !== "REJECTED") {
4708
+ throw new Error(
4709
+ original.status === "CORRECTED" ? `Invoice ${rejectedInvoiceId} has already been corrected (by ${original.correctedBy})` : `Only rejected invoices can be corrected (current status: ${original.status})`
4710
+ );
4711
+ }
4712
+ const originalHash = crypto6.createHash("sha256").update(original.invoiceXml).digest("base64");
4713
+ await client.crypto.init();
4714
+ const encData = client.crypto.getEncryptionData();
4715
+ const formCode = options.formCode ?? DEFAULT_FORM_CODE;
4716
+ const openResp = await client.onlineSession.openSession(
4717
+ { formCode, encryption: encData.encryptionInfo }
4718
+ );
4719
+ const sessionRef = openResp.referenceNumber;
4720
+ try {
4721
+ const data = new TextEncoder().encode(correctedInvoiceXml);
4722
+ const plainMeta = client.crypto.getFileMetadata(data);
4723
+ const encrypted = client.crypto.encryptAES256(data, encData.cipherKey, encData.cipherIv);
4724
+ const encMeta = client.crypto.getFileMetadata(encrypted);
4725
+ const correctionId = crypto6.randomUUID();
4726
+ const submittedAt = (/* @__PURE__ */ new Date()).toISOString();
4727
+ const correctedHashBase64 = crypto6.createHash("sha256").update(correctedInvoiceXml).digest("base64");
4728
+ const kod1Url = this.qrService.buildInvoiceVerificationUrl(
4729
+ original.sellerNip,
4730
+ original.invoiceDate,
4731
+ correctedHashBase64
4732
+ );
4733
+ let kod2Url;
4734
+ if (options.certificate) {
4735
+ kod2Url = this.qrService.buildCertificateVerificationUrl(
4736
+ original.sellerIdentifier.type,
4737
+ original.sellerIdentifier.value,
4738
+ original.sellerNip,
4739
+ options.certificate.certificateSerial,
4740
+ correctedHashBase64,
4741
+ options.certificate.privateKeyPem
4742
+ );
4743
+ }
4744
+ const correctionMetadata = {
4745
+ id: correctionId,
4746
+ mode: original.mode,
4747
+ reason: original.reason,
4748
+ status: "SUBMITTED",
4749
+ invoiceNumber: original.invoiceNumber,
4750
+ invoiceDate: original.invoiceDate,
4751
+ invoiceXml: correctedInvoiceXml,
4752
+ sellerNip: original.sellerNip,
4753
+ sellerIdentifier: original.sellerIdentifier,
4754
+ buyerIdentifier: original.buyerIdentifier,
4755
+ kod1Url,
4756
+ kod2Url,
4757
+ generatedAt: submittedAt,
4758
+ submitBy: original.submitBy,
4759
+ submittedAt,
4760
+ correctedInvoiceId: rejectedInvoiceId
4761
+ };
4762
+ await storage.save(correctionMetadata);
4763
+ const resp = await client.onlineSession.sendInvoice(sessionRef, {
4764
+ invoiceHash: plainMeta.hashSHA,
4765
+ invoiceSize: plainMeta.fileSize,
4766
+ encryptedInvoiceHash: encMeta.hashSHA,
4767
+ encryptedInvoiceSize: encMeta.fileSize,
4768
+ encryptedInvoiceContent: Buffer.from(encrypted).toString("base64"),
4769
+ offlineMode: true,
4770
+ hashOfCorrectedInvoice: originalHash
4771
+ });
4772
+ await storage.update(correctionId, {
4773
+ status: "ACCEPTED",
4774
+ acceptedAt: (/* @__PURE__ */ new Date()).toISOString(),
4775
+ ksefReferenceNumber: resp.referenceNumber
4776
+ });
4777
+ await storage.update(rejectedInvoiceId, {
4778
+ status: "CORRECTED",
4779
+ correctedBy: correctionId
4780
+ });
4781
+ return {
4782
+ invoiceId: correctionId,
4783
+ invoiceNumber: correctionMetadata.invoiceNumber,
4784
+ status: "ACCEPTED",
4785
+ ksefReferenceNumber: resp.referenceNumber
4786
+ };
4787
+ } finally {
4788
+ try {
4789
+ await client.onlineSession.closeSession(sessionRef);
4790
+ } catch {
4791
+ }
4792
+ }
4793
+ }
4794
+ };
4795
+ }
4796
+ });
4797
+
4331
4798
  // src/client.ts
4332
4799
  var client_exports = {};
4333
4800
  __export(client_exports, {
@@ -4393,6 +4860,7 @@ var init_client = __esm({
4393
4860
  init_cryptography_service();
4394
4861
  init_verification_link_service();
4395
4862
  init_auth_xml_builder();
4863
+ init_offline_invoice_workflow();
4396
4864
  KSeFClient = class {
4397
4865
  auth;
4398
4866
  activeSessions;
@@ -4411,6 +4879,7 @@ var init_client = __esm({
4411
4879
  qr;
4412
4880
  options;
4413
4881
  authManager;
4882
+ _offline;
4414
4883
  constructor(options) {
4415
4884
  this.options = resolveOptions(options);
4416
4885
  const authManager = options?.authManager ?? new DefaultAuthManager(async () => {
@@ -4439,6 +4908,12 @@ var init_client = __esm({
4439
4908
  this.testData = new TestDataService(restClient, this.options.environmentName);
4440
4909
  this.qr = new VerificationLinkService(this.options.baseQrUrl);
4441
4910
  }
4911
+ get offline() {
4912
+ if (!this._offline) {
4913
+ this._offline = new OfflineInvoiceWorkflow(this.qr);
4914
+ }
4915
+ return this._offline;
4916
+ }
4442
4917
  async loginWithToken(token, nip) {
4443
4918
  const challenge = await this.auth.getChallenge();
4444
4919
  await this.crypto.init();
@@ -4522,65 +4997,8 @@ init_xml_to_object();
4522
4997
  init_schema_registry();
4523
4998
  init_invoice_validator();
4524
4999
 
4525
- // src/models/document-structures/types.ts
4526
- var SystemCode = {
4527
- FA_2: "FA (2)",
4528
- FA_3: "FA (3)",
4529
- PEF_3: "PEF (3)",
4530
- PEF_KOR_3: "PEF_KOR (3)",
4531
- FA_RR_1: "FA_RR (1)"
4532
- };
4533
- var FORM_CODES = {
4534
- FA_2: { systemCode: "FA (2)", schemaVersion: "1-0E", value: "FA" },
4535
- FA_3: { systemCode: "FA (3)", schemaVersion: "1-0E", value: "FA" },
4536
- PEF_3: { systemCode: "PEF (3)", schemaVersion: "2-1", value: "PEF" },
4537
- PEF_KOR_3: { systemCode: "PEF_KOR (3)", schemaVersion: "2-1", value: "PEF" },
4538
- FA_RR_1_LEGACY: { systemCode: "FA_RR (1)", schemaVersion: "1-0E", value: "RR" },
4539
- FA_RR_1_TRANSITION: { systemCode: "FA_RR (1)", schemaVersion: "1-1E", value: "RR" },
4540
- FA_RR_1: { systemCode: "FA_RR (1)", schemaVersion: "1-1E", value: "FA_RR" }
4541
- };
4542
- var DEFAULT_FORM_CODE = FORM_CODES.FA_3;
4543
- var INVOICE_TYPES_BY_SYSTEM_CODE = {
4544
- [SystemCode.FA_2]: ["Vat", "Zal", "Kor", "Roz", "Upr", "KorZal", "KorRoz"],
4545
- [SystemCode.FA_3]: ["Vat", "Zal", "Kor", "Roz", "Upr", "KorZal", "KorRoz"],
4546
- [SystemCode.PEF_3]: ["VatPef", "VatPefSp", "KorPef"],
4547
- [SystemCode.PEF_KOR_3]: ["KorPef"],
4548
- [SystemCode.FA_RR_1]: ["VatRr", "KorVatRr"]
4549
- };
4550
- var FORM_CODE_KEYS = {
4551
- FA2: FORM_CODES.FA_2,
4552
- FA3: FORM_CODES.FA_3,
4553
- PEF3: FORM_CODES.PEF_3,
4554
- PEFKOR3: FORM_CODES.PEF_KOR_3,
4555
- FARR1: FORM_CODES.FA_RR_1
4556
- };
4557
-
4558
- // src/models/document-structures/helpers.ts
4559
- var SYSTEM_CODE_TO_FORM_CODE = {
4560
- [SystemCode.FA_2]: FORM_CODES.FA_2,
4561
- [SystemCode.FA_3]: FORM_CODES.FA_3,
4562
- [SystemCode.PEF_3]: FORM_CODES.PEF_3,
4563
- [SystemCode.PEF_KOR_3]: FORM_CODES.PEF_KOR_3,
4564
- [SystemCode.FA_RR_1]: FORM_CODES.FA_RR_1
4565
- };
4566
- var ALL_FORM_CODES = Object.values(FORM_CODES);
4567
- var BATCH_DISALLOWED_SYSTEM_CODES = /* @__PURE__ */ new Set([
4568
- SystemCode.PEF_3,
4569
- SystemCode.PEF_KOR_3
4570
- ]);
4571
- function getFormCode(systemCode) {
4572
- return SYSTEM_CODE_TO_FORM_CODE[systemCode];
4573
- }
4574
- function parseFormCode(raw) {
4575
- const match = ALL_FORM_CODES.find(
4576
- (fc) => fc.systemCode === raw.systemCode && fc.schemaVersion === raw.schemaVersion && fc.value === raw.value
4577
- );
4578
- return match ?? raw;
4579
- }
4580
- function validateFormCodeForSession(formCode, sessionType) {
4581
- if (sessionType === "online") return true;
4582
- return !BATCH_DISALLOWED_SYSTEM_CODES.has(formCode.systemCode);
4583
- }
5000
+ // src/models/index.ts
5001
+ init_document_structures();
4584
5002
 
4585
5003
  // src/services/index.ts
4586
5004
  init_auth();
@@ -5332,6 +5750,9 @@ async function pollUntil(action, condition, options) {
5332
5750
  );
5333
5751
  }
5334
5752
 
5753
+ // src/workflows/online-session-workflow.ts
5754
+ init_document_structures();
5755
+
5335
5756
  // src/xml/upo-parser.ts
5336
5757
  init_ksef_validation_error();
5337
5758
  import { XMLParser } from "fast-xml-parser";
@@ -5450,6 +5871,46 @@ function parseUpoXml(xml) {
5450
5871
  };
5451
5872
  }
5452
5873
 
5874
+ // src/xml/invoice-field-extractor.ts
5875
+ import { XMLParser as XMLParser2 } from "fast-xml-parser";
5876
+ var invoiceParser = new XMLParser2({
5877
+ ignoreAttributes: false,
5878
+ attributeNamePrefix: "@_",
5879
+ parseTagValue: false,
5880
+ parseAttributeValue: false,
5881
+ removeNSPrefix: true,
5882
+ trimValues: false
5883
+ });
5884
+ function extractInvoiceFields(xml) {
5885
+ const parsed = invoiceParser.parse(xml);
5886
+ const root = parsed?.Faktura;
5887
+ if (!root || typeof root !== "object") {
5888
+ throw new Error(
5889
+ "Cannot extract invoice fields: missing <Faktura> root element. Ensure the XML is a valid KSeF invoice (FA_2, FA_3, or FA_RR)."
5890
+ );
5891
+ }
5892
+ if (root.Fa && typeof root.Fa === "object") {
5893
+ const invoiceDate = nonEmptyString(root.Fa.P_1);
5894
+ const invoiceNumber = nonEmptyString(root.Fa.P_2);
5895
+ if (invoiceDate && invoiceNumber) {
5896
+ return { invoiceNumber, invoiceDate };
5897
+ }
5898
+ }
5899
+ if (root.FakturaRR && typeof root.FakturaRR === "object") {
5900
+ const invoiceDate = nonEmptyString(root.FakturaRR.P_4B);
5901
+ const invoiceNumber = nonEmptyString(root.FakturaRR.P_4C);
5902
+ if (invoiceDate && invoiceNumber) {
5903
+ return { invoiceNumber, invoiceDate };
5904
+ }
5905
+ }
5906
+ throw new Error(
5907
+ "Cannot extract invoice number and date from XML. Expected <Fa><P_1>/<P_2> (FA format) or <FakturaRR><P_4B>/<P_4C> (RR format)."
5908
+ );
5909
+ }
5910
+ function nonEmptyString(value) {
5911
+ return typeof value === "string" && value.length > 0 ? value : void 0;
5912
+ }
5913
+
5453
5914
  // src/workflows/online-session-workflow.ts
5454
5915
  init_invoice_validator();
5455
5916
  init_ksef_validation_error();
@@ -5532,6 +5993,7 @@ async function openSendAndClose(client, invoices, options) {
5532
5993
  }
5533
5994
 
5534
5995
  // src/workflows/batch-session-workflow.ts
5996
+ init_document_structures();
5535
5997
  async function uploadBatch(client, zipData, options) {
5536
5998
  await client.crypto.init();
5537
5999
  if (options?.validate) {
@@ -5952,7 +6414,140 @@ async function authenticateWithPkcs12(client, options) {
5952
6414
  });
5953
6415
  }
5954
6416
 
6417
+ // src/offline/index.ts
6418
+ init_deadline();
6419
+
6420
+ // src/offline/storage.ts
6421
+ function matchesFilter(invoice, filter) {
6422
+ if (filter.status !== void 0) {
6423
+ const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
6424
+ if (!statuses.includes(invoice.status)) return false;
6425
+ }
6426
+ if (filter.mode !== void 0 && invoice.mode !== filter.mode) return false;
6427
+ if (filter.sellerNip !== void 0 && invoice.sellerNip !== filter.sellerNip) return false;
6428
+ if (filter.expiringBefore !== void 0) {
6429
+ const cutoff = typeof filter.expiringBefore === "string" ? new Date(filter.expiringBefore).getTime() : filter.expiringBefore.getTime();
6430
+ if (new Date(invoice.submitBy).getTime() >= cutoff) return false;
6431
+ }
6432
+ return true;
6433
+ }
6434
+ var InMemoryOfflineInvoiceStorage = class {
6435
+ store = /* @__PURE__ */ new Map();
6436
+ async save(invoice) {
6437
+ this.store.set(invoice.id, JSON.parse(JSON.stringify(invoice)));
6438
+ }
6439
+ async get(id) {
6440
+ const entry = this.store.get(id);
6441
+ return entry ? JSON.parse(JSON.stringify(entry)) : null;
6442
+ }
6443
+ async list(filter) {
6444
+ const all = [...this.store.values()];
6445
+ if (!filter) return all.map((i) => JSON.parse(JSON.stringify(i)));
6446
+ return all.filter((i) => matchesFilter(i, filter)).map((i) => JSON.parse(JSON.stringify(i)));
6447
+ }
6448
+ async update(id, updates) {
6449
+ const existing = this.store.get(id);
6450
+ if (!existing) throw new Error(`Offline invoice not found: ${id}`);
6451
+ this.store.set(id, JSON.parse(JSON.stringify({ ...existing, ...updates })));
6452
+ }
6453
+ async delete(id) {
6454
+ this.store.delete(id);
6455
+ }
6456
+ };
6457
+
6458
+ // src/offline/file-storage.ts
6459
+ import * as fs2 from "fs/promises";
6460
+ import * as path from "path";
6461
+ import * as os from "os";
6462
+ function resolveDir(dir) {
6463
+ if (dir === "~" || dir.startsWith("~/")) {
6464
+ return path.join(os.homedir(), dir.slice(1));
6465
+ }
6466
+ return dir;
6467
+ }
6468
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6469
+ function validateId(id) {
6470
+ if (!UUID_RE.test(id)) {
6471
+ throw new Error(`Invalid invoice ID: ${id}`);
6472
+ }
6473
+ }
6474
+ var FileOfflineInvoiceStorage = class {
6475
+ dir;
6476
+ constructor(directory) {
6477
+ this.dir = resolveDir(directory ?? "~/.ksef/offline");
6478
+ }
6479
+ async ensureDir() {
6480
+ await fs2.mkdir(this.dir, { recursive: true });
6481
+ }
6482
+ filePath(id) {
6483
+ validateId(id);
6484
+ return path.join(this.dir, `${id}.json`);
6485
+ }
6486
+ async save(invoice) {
6487
+ await this.ensureDir();
6488
+ const file = this.filePath(invoice.id);
6489
+ const tmp = `${file}.tmp`;
6490
+ await fs2.writeFile(tmp, JSON.stringify(invoice, null, 2));
6491
+ await fs2.rename(tmp, file);
6492
+ }
6493
+ async get(id) {
6494
+ const file = this.filePath(id);
6495
+ try {
6496
+ return JSON.parse(await fs2.readFile(file, "utf-8"));
6497
+ } catch (err) {
6498
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
6499
+ return null;
6500
+ }
6501
+ console.warn(`Warning: Failed to read offline invoice ${id}: ${err instanceof Error ? err.message : String(err)}`);
6502
+ return null;
6503
+ }
6504
+ }
6505
+ async list(filter) {
6506
+ let files;
6507
+ try {
6508
+ files = (await fs2.readdir(this.dir)).filter((f) => f.endsWith(".json"));
6509
+ } catch {
6510
+ return [];
6511
+ }
6512
+ const results = [];
6513
+ for (const file of files) {
6514
+ try {
6515
+ const data = JSON.parse(
6516
+ await fs2.readFile(path.join(this.dir, file), "utf-8")
6517
+ );
6518
+ if (!filter || matchesFilter(data, filter)) {
6519
+ results.push(data);
6520
+ }
6521
+ } catch (err) {
6522
+ console.warn(`Warning: Skipping corrupt offline invoice file ${file}: ${err instanceof Error ? err.message : String(err)}`);
6523
+ }
6524
+ }
6525
+ return results;
6526
+ }
6527
+ /**
6528
+ * Update invoice metadata (read-modify-write).
6529
+ *
6530
+ * Note: No file locking — concurrent updates to the same ID may cause
6531
+ * lost writes. Acceptable for CLI (single process). Library consumers
6532
+ * running parallel operations should use external locking.
6533
+ */
6534
+ async update(id, updates) {
6535
+ const existing = await this.get(id);
6536
+ if (!existing) throw new Error(`Offline invoice not found: ${id}`);
6537
+ await this.save({ ...existing, ...updates });
6538
+ }
6539
+ async delete(id) {
6540
+ const file = this.filePath(id);
6541
+ try {
6542
+ await fs2.unlink(file);
6543
+ } catch (e) {
6544
+ if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
6545
+ }
6546
+ }
6547
+ };
6548
+
5955
6549
  // src/index.ts
6550
+ init_offline_invoice_workflow();
5956
6551
  init_client();
5957
6552
  export {
5958
6553
  ActiveSessionsService,
@@ -5982,8 +6577,10 @@ export {
5982
6577
  FORM_CODES,
5983
6578
  FORM_CODE_KEYS,
5984
6579
  FileHwmStore,
6580
+ FileOfflineInvoiceStorage,
5985
6581
  INVOICE_TYPES_BY_SYSTEM_CODE,
5986
6582
  InMemoryHwmStore,
6583
+ InMemoryOfflineInvoiceStorage,
5987
6584
  InternalId,
5988
6585
  InvoiceDownloadService,
5989
6586
  InvoiceQueryFilterBuilder,
@@ -6007,6 +6604,7 @@ export {
6007
6604
  LimitsService,
6008
6605
  Nip,
6009
6606
  NipVatUe,
6607
+ OfflineInvoiceWorkflow,
6010
6608
  OnlineSessionService,
6011
6609
  PERMISSION_DESCRIPTION_MAX_LENGTH,
6012
6610
  PERMISSION_DESCRIPTION_MIN_LENGTH,
@@ -6036,6 +6634,7 @@ export {
6036
6634
  UpoVersion,
6037
6635
  VatUe,
6038
6636
  VerificationLinkService,
6637
+ addBusinessDays,
6039
6638
  authenticateWithCertificate,
6040
6639
  authenticateWithExternalSignature,
6041
6640
  authenticateWithPkcs12,
@@ -6043,6 +6642,7 @@ export {
6043
6642
  batchValidationDetails,
6044
6643
  buildUnsignedAuthTokenRequestXml,
6045
6644
  calculateBackoff,
6645
+ calculateOfflineDeadline,
6046
6646
  createZip,
6047
6647
  decodeJwtPayload,
6048
6648
  deduplicateByKsefNumber,
@@ -6052,9 +6652,14 @@ export {
6052
6652
  defaultTransport,
6053
6653
  exportAndDownload,
6054
6654
  exportInvoices,
6655
+ extendDeadlineForMaintenance,
6656
+ extractInvoiceFields,
6657
+ getDefaultReason,
6055
6658
  getEffectiveStartDate,
6056
6659
  getFormCode,
6660
+ getTimeUntilDeadline,
6057
6661
  incrementalExportAndDownload,
6662
+ isExpired,
6058
6663
  isRetryableError,
6059
6664
  isRetryableStatus,
6060
6665
  isValidBase64,
@@ -6072,6 +6677,7 @@ export {
6072
6677
  isValidReferenceNumber,
6073
6678
  isValidSha256Base64,
6074
6679
  isValidVatUe,
6680
+ nextBusinessDay,
6075
6681
  openOnlineSession,
6076
6682
  openSendAndClose,
6077
6683
  parseFormCode,