@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,15 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260519210000_integration_ksef_direct extends Migration {
4
+
5
+ override up(): void | Promise<void> {
6
+ this.addSql(`create table "ksef_direct_connections" ("id" uuid not null default gen_random_uuid(), "organization_id" uuid not null, "tenant_id" uuid not null, "status" text not null default 'unconfigured', "last_checked_at" timestamptz null, "error_message" text null, "error_code" text null, "created_at" timestamptz not null, "updated_at" timestamptz not null, primary key ("id"));`);
7
+ this.addSql(`create index "ksef_direct_connections_status_updated_at_index" on "ksef_direct_connections" ("status", "updated_at");`);
8
+ this.addSql(`alter table "ksef_direct_connections" add constraint "ksef_direct_connections_organization_id_tenant_id_unique" unique ("organization_id", "tenant_id");`);
9
+ }
10
+
11
+ override down(): void | Promise<void> {
12
+ this.addSql(`drop table if exists "ksef_direct_connections";`);
13
+ }
14
+
15
+ }
@@ -0,0 +1,17 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260520120000_ksef_direct_documents extends Migration {
4
+
5
+ override up(): void | Promise<void> {
6
+ this.addSql(`create table "ksef_direct_documents" ("id" uuid not null default gen_random_uuid(), "organization_id" uuid not null, "tenant_id" uuid not null, "source" text not null default 'manual', "status" text not null default 'draft', "ksef_reference_number" text null, "seller_nip" text not null, "buyer_nip" text not null, "buyer_name" text null, "invoice_number" text not null, "issue_date" timestamptz not null, "sale_date" timestamptz null, "net_amount" numeric(15,2) not null, "vat_amount" numeric(15,2) not null, "gross_amount" numeric(15,2) not null, "currency" text not null default 'PLN', "line_items" jsonb not null default '[]', "notes" text null, "error_message" text null, "created_at" timestamptz not null, "updated_at" timestamptz not null, primary key ("id"));`);
7
+ this.addSql(`create index "ksef_direct_documents_organization_id_tenant_id_index" on "ksef_direct_documents" ("organization_id", "tenant_id");`);
8
+ this.addSql(`create index "ksef_direct_documents_organization_id_tenant_id_status_index" on "ksef_direct_documents" ("organization_id", "tenant_id", "status");`);
9
+ this.addSql(`create index "ksef_direct_documents_organization_id_tenant_id_source_index" on "ksef_direct_documents" ("organization_id", "tenant_id", "source");`);
10
+ this.addSql(`create index "ksef_direct_documents_ksef_reference_number_index" on "ksef_direct_documents" ("ksef_reference_number");`);
11
+ }
12
+
13
+ override down(): void | Promise<void> {
14
+ this.addSql(`drop table if exists "ksef_direct_documents";`);
15
+ }
16
+
17
+ }
@@ -0,0 +1,15 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260520220000_ksef_direct_send_queue extends Migration {
4
+
5
+ override up(): void | Promise<void> {
6
+ this.addSql(`alter table "ksef_direct_documents" add column "ksef_processing_reference_number" text null;`);
7
+ this.addSql(`alter table "ksef_direct_documents" add column "seller_name" text null;`);
8
+ }
9
+
10
+ override down(): void | Promise<void> {
11
+ this.addSql(`alter table "ksef_direct_documents" drop column "ksef_processing_reference_number";`);
12
+ this.addSql(`alter table "ksef_direct_documents" drop column "seller_name";`);
13
+ }
14
+
15
+ }
@@ -0,0 +1,17 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260520230000_ksef_direct_seller_per_document extends Migration {
4
+
5
+ override up(): void | Promise<void> {
6
+ this.addSql(`alter table "ksef_direct_documents" add column "seller_address_l1" text null;`);
7
+ this.addSql(`alter table "ksef_direct_documents" add column "seller_city" text null;`);
8
+ this.addSql(`alter table "ksef_direct_documents" add column "seller_country" text null;`);
9
+ }
10
+
11
+ override down(): void | Promise<void> {
12
+ this.addSql(`alter table "ksef_direct_documents" drop column "seller_address_l1";`);
13
+ this.addSql(`alter table "ksef_direct_documents" drop column "seller_city";`);
14
+ this.addSql(`alter table "ksef_direct_documents" drop column "seller_country";`);
15
+ }
16
+
17
+ }
@@ -0,0 +1,16 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260521120000_ksef_direct_received_documents extends Migration {
4
+
5
+ override up(): void | Promise<void> {
6
+ this.addSql(`create table "ksef_direct_received_documents" ("id" uuid not null default gen_random_uuid(), "organization_id" uuid not null, "tenant_id" uuid not null, "ksef_reference_number" text not null, "raw_xml" text null, "invoice_number" text null, "seller_nip" text null, "seller_name" text null, "issue_date" date null, "currency" text null, "net_amount" numeric(15,2) null, "vat_amount" numeric(15,2) null, "gross_amount" numeric(15,2) null, "status" text not null default 'pending_download', "error_message" text null, "upo_download_url" text null, "invoice_download_url" text null, "synced_at" timestamptz null, "created_at" timestamptz not null, "updated_at" timestamptz not null, primary key ("id"));`);
7
+ this.addSql(`create index "ksef_direct_received_documents_organization_id_tenant_id_index" on "ksef_direct_received_documents" ("organization_id", "tenant_id");`);
8
+ this.addSql(`create index "ksef_direct_received_documents_organization_id_tenant_id_status_index" on "ksef_direct_received_documents" ("organization_id", "tenant_id", "status");`);
9
+ this.addSql(`alter table "ksef_direct_received_documents" add constraint "ksef_direct_received_documents_organization_id_ksef_reference_number_unique" unique ("organization_id", "ksef_reference_number");`);
10
+ }
11
+
12
+ override down(): void | Promise<void> {
13
+ this.addSql(`drop table if exists "ksef_direct_received_documents";`);
14
+ }
15
+
16
+ }
@@ -0,0 +1,15 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260521130000_ksef_received_download_urls extends Migration {
4
+
5
+ override up(): void | Promise<void> {
6
+ this.addSql(`alter table "ksef_direct_received_documents" add column if not exists "upo_download_url" text null;`);
7
+ this.addSql(`alter table "ksef_direct_received_documents" add column if not exists "invoice_download_url" text null;`);
8
+ }
9
+
10
+ override down(): void | Promise<void> {
11
+ this.addSql(`alter table "ksef_direct_received_documents" drop column if exists "upo_download_url";`);
12
+ this.addSql(`alter table "ksef_direct_received_documents" drop column if exists "invoice_download_url";`);
13
+ }
14
+
15
+ }
@@ -0,0 +1,9 @@
1
+ import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'
2
+
3
+ export const setup: ModuleSetupConfig = {
4
+ defaultRoleFeatures: {
5
+ admin: ['integration_ksef_direct.*'],
6
+ },
7
+ }
8
+
9
+ export default setup
@@ -0,0 +1,21 @@
1
+ import { enqueueKsefDirectDocument } from '../commands/enqueue-ksef-direct-document'
2
+
3
+ export const metadata = {
4
+ event: 'ksef_direct.document.created',
5
+ persistent: true,
6
+ id: 'ksef_direct.auto_enqueue_document',
7
+ }
8
+
9
+ export default async function handler(
10
+ payload: { documentId: string; organizationId: string; tenantId: string },
11
+ ctx: { resolve: <T = unknown>(name: string) => T },
12
+ ) {
13
+ const em = ctx.resolve<{ fork: () => unknown }>('em')?.fork() as any
14
+ if (!em) return
15
+
16
+ try {
17
+ await enqueueKsefDirectDocument(em, payload.tenantId, payload.organizationId, payload.documentId)
18
+ } catch {
19
+ // If document is already queued or in a non-draft state, ignore — idempotent
20
+ }
21
+ }
@@ -0,0 +1,129 @@
1
+ import type { QueuedJob, WorkerMeta } from '@open-mercato/queue'
2
+ import { createModuleQueue } from '@open-mercato/queue'
3
+
4
+ export const metadata: WorkerMeta = {
5
+ queue: 'ksef_direct_check',
6
+ id: 'ksef-direct-check',
7
+ concurrency: 5,
8
+ }
9
+
10
+ type HandlerContext = { resolve: <T = unknown>(name: string) => T }
11
+
12
+ type CheckJobPayload = {
13
+ documentId: string
14
+ organizationId: string
15
+ tenantId: string
16
+ attempt: number
17
+ }
18
+
19
+ export default async function handler(
20
+ job: QueuedJob<CheckJobPayload>,
21
+ ctx: HandlerContext,
22
+ ): Promise<void> {
23
+ const payload = job.payload
24
+ const em = (ctx.resolve('em') as { fork: () => unknown })?.fork() as any
25
+ if (!em) return
26
+
27
+ const { KsefDirectDocument } = await import('../data/entities')
28
+ const doc = await em.findOne(KsefDirectDocument, {
29
+ id: payload.documentId,
30
+ organizationId: payload.organizationId,
31
+ tenantId: payload.tenantId,
32
+ })
33
+
34
+ if (!doc || doc.status !== 'sending') {
35
+ return
36
+ }
37
+
38
+ if (!doc.ksefProcessingReferenceNumber) {
39
+ doc.status = 'failed'
40
+ doc.errorMessage = 'Missing KSeF processing reference number'
41
+ doc.updatedAt = new Date()
42
+ await em.flush()
43
+ return
44
+ }
45
+
46
+ const credentialsService = ctx.resolve('integrationCredentialsService') as any
47
+ const rawCreds = credentialsService
48
+ ? await credentialsService.resolve('integration_ksef_direct', {
49
+ tenantId: payload.tenantId,
50
+ organizationId: payload.organizationId,
51
+ })
52
+ : null
53
+
54
+ const { KsefDirectCredentialsSchema } = await import('../data/validators')
55
+ const credsParsed = KsefDirectCredentialsSchema.safeParse(rawCreds)
56
+ if (!credsParsed.success) {
57
+ doc.status = 'failed'
58
+ doc.errorMessage = 'KSeF Direct credentials not configured'
59
+ doc.updatedAt = new Date()
60
+ await em.flush()
61
+ return
62
+ }
63
+
64
+ const credentials = {
65
+ ksefToken: credsParsed.data.ksef_token,
66
+ nip: credsParsed.data.nip,
67
+ environment: credsParsed.data.environment,
68
+ tenantId: payload.tenantId,
69
+ }
70
+
71
+ const { checkInvoiceStatus } = await import('../lib/ksefClient')
72
+ const { emitKsefDirectEvent } = await import('../events')
73
+
74
+ let result: { processingCode: number; ksefReferenceNumber?: string; errorDescription?: string }
75
+ try {
76
+ result = await checkInvoiceStatus(credentials, doc.ksefProcessingReferenceNumber)
77
+ } catch (err) {
78
+ // Re-throw transient errors so BullMQ retries; do not permanently fail the document
79
+ throw err
80
+ }
81
+
82
+ if (result.processingCode === 200) {
83
+ doc.status = 'sent'
84
+ doc.updatedAt = new Date()
85
+ await em.flush()
86
+
87
+ await emitKsefDirectEvent('ksef_direct.document.sent', {
88
+ documentId: doc.id,
89
+ ksefReferenceNumber: doc.ksefReferenceNumber ?? undefined,
90
+ organizationId: payload.organizationId,
91
+ tenantId: payload.tenantId,
92
+ })
93
+ } else if (result.processingCode === 100) {
94
+ if (payload.attempt >= 10) {
95
+ doc.status = 'failed'
96
+ doc.errorMessage = 'Timeout: KSeF did not confirm the document within 10 minutes'
97
+ doc.updatedAt = new Date()
98
+ await em.flush()
99
+
100
+ await emitKsefDirectEvent('ksef_direct.document.failed', {
101
+ documentId: doc.id,
102
+ errorMessage: doc.errorMessage,
103
+ organizationId: payload.organizationId,
104
+ tenantId: payload.tenantId,
105
+ })
106
+ } else {
107
+ const checkQueue = createModuleQueue<CheckJobPayload>('ksef_direct_check')
108
+ await checkQueue.enqueue(
109
+ { ...payload, attempt: payload.attempt + 1 },
110
+ { delayMs: 60_000 },
111
+ )
112
+ }
113
+ } else {
114
+ const errorMessage = result.errorDescription
115
+ ?? `KSeF processing error: code ${result.processingCode}`
116
+ doc.status = 'failed'
117
+ doc.errorMessage = errorMessage
118
+ doc.ksefReferenceNumber = null
119
+ doc.updatedAt = new Date()
120
+ await em.flush()
121
+
122
+ await emitKsefDirectEvent('ksef_direct.document.failed', {
123
+ documentId: doc.id,
124
+ errorMessage,
125
+ organizationId: payload.organizationId,
126
+ tenantId: payload.tenantId,
127
+ })
128
+ }
129
+ }
@@ -0,0 +1,137 @@
1
+ import type { QueuedJob, WorkerMeta } from '@open-mercato/queue'
2
+ import { createModuleQueue } from '@open-mercato/queue'
3
+
4
+ export const metadata: WorkerMeta = {
5
+ queue: 'ksef_direct_send',
6
+ id: 'ksef-direct-send',
7
+ concurrency: 5,
8
+ }
9
+
10
+ type HandlerContext = { resolve: <T = unknown>(name: string) => T }
11
+
12
+ type SendJobPayload = {
13
+ documentId: string
14
+ organizationId: string
15
+ tenantId: string
16
+ }
17
+
18
+ type CheckJobPayload = {
19
+ documentId: string
20
+ organizationId: string
21
+ tenantId: string
22
+ attempt: number
23
+ }
24
+
25
+ export default async function handler(
26
+ job: QueuedJob<SendJobPayload>,
27
+ ctx: HandlerContext,
28
+ ): Promise<void> {
29
+ const payload = job.payload
30
+ const em = (ctx.resolve('em') as { fork: () => unknown })?.fork() as any
31
+ if (!em) return
32
+
33
+ const { KsefDirectDocument } = await import('../data/entities')
34
+ const doc = await em.findOne(KsefDirectDocument, {
35
+ id: payload.documentId,
36
+ organizationId: payload.organizationId,
37
+ tenantId: payload.tenantId,
38
+ })
39
+
40
+ if (!doc || doc.status !== 'queued') {
41
+ return
42
+ }
43
+
44
+ if (!doc.sellerName?.trim()) {
45
+ doc.status = 'failed'
46
+ doc.errorMessage = 'Seller name is required to send the document to KSeF'
47
+ doc.updatedAt = new Date()
48
+ await em.flush()
49
+
50
+ const { emitKsefDirectEvent } = await import('../events')
51
+ await emitKsefDirectEvent('ksef_direct.document.failed', {
52
+ documentId: doc.id,
53
+ errorMessage: doc.errorMessage,
54
+ organizationId: payload.organizationId,
55
+ tenantId: payload.tenantId,
56
+ })
57
+ return
58
+ }
59
+
60
+ const credentialsService = ctx.resolve('integrationCredentialsService') as any
61
+ const rawCreds = credentialsService
62
+ ? await credentialsService.resolve('integration_ksef_direct', {
63
+ tenantId: payload.tenantId,
64
+ organizationId: payload.organizationId,
65
+ })
66
+ : null
67
+
68
+ const { KsefDirectCredentialsSchema } = await import('../data/validators')
69
+ const credsParsed = KsefDirectCredentialsSchema.safeParse(rawCreds)
70
+
71
+ if (!credsParsed.success) {
72
+ doc.status = 'failed'
73
+ doc.errorMessage = 'KSeF Direct credentials not configured'
74
+ doc.updatedAt = new Date()
75
+ await em.flush()
76
+
77
+ const { emitKsefDirectEvent } = await import('../events')
78
+ await emitKsefDirectEvent('ksef_direct.document.failed', {
79
+ documentId: doc.id,
80
+ errorMessage: doc.errorMessage,
81
+ organizationId: payload.organizationId,
82
+ tenantId: payload.tenantId,
83
+ })
84
+ return
85
+ }
86
+
87
+ const creds = credsParsed.data
88
+ const credentials = {
89
+ ksefToken: creds.ksef_token,
90
+ nip: creds.nip,
91
+ environment: creds.environment,
92
+ tenantId: payload.tenantId,
93
+ }
94
+
95
+ try {
96
+ const { fetchInvoicePublicKey, prepareInvoicePayload } = await import('../lib/ksefCrypto')
97
+ const { generateFa2Xml } = await import('../lib/ksefFa2Xml')
98
+ const { sendInvoice } = await import('../lib/ksefClient')
99
+
100
+ const xml = generateFa2Xml(doc, {
101
+ sellerName: doc.sellerName,
102
+ sellerAddressL1: doc.sellerAddressL1 ?? undefined,
103
+ sellerCity: doc.sellerCity ?? undefined,
104
+ sellerCountry: doc.sellerCountry ?? undefined,
105
+ })
106
+
107
+ const { publicKeyPem, publicKeyId } = await fetchInvoicePublicKey(credentials.environment)
108
+ const invoicePayload = prepareInvoicePayload(xml, publicKeyPem)
109
+ const { sessionReferenceNumber, invoiceReferenceNumber } = await sendInvoice(credentials, { ...invoicePayload, publicKeyId })
110
+
111
+ doc.status = 'sending'
112
+ doc.ksefProcessingReferenceNumber = sessionReferenceNumber
113
+ doc.ksefReferenceNumber = invoiceReferenceNumber
114
+ doc.updatedAt = new Date()
115
+ await em.flush()
116
+
117
+ const checkQueue = createModuleQueue<CheckJobPayload>('ksef_direct_check')
118
+ await checkQueue.enqueue(
119
+ { documentId: doc.id, organizationId: payload.organizationId, tenantId: payload.tenantId, attempt: 1 },
120
+ { delayMs: 30_000 },
121
+ )
122
+ } catch (err) {
123
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error during KSeF document send'
124
+ doc.status = 'failed'
125
+ doc.errorMessage = errorMessage
126
+ doc.updatedAt = new Date()
127
+ await em.flush()
128
+
129
+ const { emitKsefDirectEvent } = await import('../events')
130
+ await emitKsefDirectEvent('ksef_direct.document.failed', {
131
+ documentId: doc.id,
132
+ errorMessage,
133
+ organizationId: payload.organizationId,
134
+ tenantId: payload.tenantId,
135
+ })
136
+ }
137
+ }
@@ -0,0 +1,171 @@
1
+ import type { QueuedJob, WorkerMeta } from '@open-mercato/queue'
2
+
3
+ export const metadata: WorkerMeta = {
4
+ queue: 'ksef_direct_receive',
5
+ id: 'ksef-direct-receive',
6
+ concurrency: 3,
7
+ }
8
+
9
+ type HandlerContext = { resolve: <T = unknown>(name: string) => T }
10
+
11
+ export type SyncReceivedDocumentsPayload = {
12
+ organizationId: string
13
+ tenantId: string
14
+ dateFrom: string
15
+ dateTo: string
16
+ }
17
+
18
+ export default async function handler(
19
+ job: QueuedJob<SyncReceivedDocumentsPayload>,
20
+ ctx: HandlerContext,
21
+ ): Promise<void> {
22
+ const payload = job.payload
23
+
24
+ const em = (ctx.resolve('em') as { fork: () => unknown })?.fork() as any
25
+ if (!em) return
26
+
27
+ const credentialsService = ctx.resolve('integrationCredentialsService') as any
28
+ const rawCreds = credentialsService
29
+ ? await credentialsService.resolve('integration_ksef_direct', {
30
+ tenantId: payload.tenantId,
31
+ organizationId: payload.organizationId,
32
+ })
33
+ : null
34
+ const { KsefDirectCredentialsSchema } = await import('../data/validators')
35
+ const credsParsed = KsefDirectCredentialsSchema.safeParse(rawCreds)
36
+ if (!credsParsed.success) return
37
+
38
+ const credentials = {
39
+ ksefToken: credsParsed.data.ksef_token,
40
+ nip: credsParsed.data.nip,
41
+ environment: credsParsed.data.environment,
42
+ tenantId: payload.tenantId,
43
+ }
44
+
45
+ const { queryReceivedInvoices, downloadInvoiceFromUrl } = await import('../lib/ksefClient')
46
+ const { KsefDirectReceivedDocument } = await import('../data/entities')
47
+ const { parseReceivedInvoiceXml } = await import('../lib/ksefXmlParser')
48
+ const { emitKsefDirectEvent } = await import('../events')
49
+
50
+ // Phase 1: enumerate all received invoices for the date range and upsert summaries
51
+ const result = await queryReceivedInvoices(credentials, {
52
+ dateFrom: payload.dateFrom,
53
+ dateTo: payload.dateTo,
54
+ })
55
+
56
+ const collectedRefs: string[] = []
57
+
58
+ for (const summary of result.items) {
59
+ collectedRefs.push(summary.ksefReferenceNumber)
60
+
61
+ const existing = await em.findOne(KsefDirectReceivedDocument, {
62
+ organizationId: payload.organizationId,
63
+ ksefReferenceNumber: summary.ksefReferenceNumber,
64
+ })
65
+
66
+ if (!existing) {
67
+ const record = em.create(KsefDirectReceivedDocument, {
68
+ organizationId: payload.organizationId,
69
+ tenantId: payload.tenantId,
70
+ ksefReferenceNumber: summary.ksefReferenceNumber,
71
+ sellerNip: summary.sellerNip,
72
+ sellerName: summary.sellerName,
73
+ issueDate: summary.issueDate,
74
+ grossAmount: summary.grossAmount,
75
+ netAmount: summary.netAmount,
76
+ vatAmount: summary.vatAmount,
77
+ currency: summary.currency,
78
+ invoiceNumber: summary.invoiceNumber,
79
+ upoDownloadUrl: summary.upoDownloadUrl,
80
+ invoiceDownloadUrl: summary.invoiceDownloadUrl,
81
+ status: 'pending_download',
82
+ })
83
+ em.persist(record)
84
+ } else {
85
+ // Keep URL fields fresh in case they changed (pre-signed URLs rotate)
86
+ if (summary.upoDownloadUrl) existing.upoDownloadUrl = summary.upoDownloadUrl
87
+ if (summary.invoiceDownloadUrl) existing.invoiceDownloadUrl = summary.invoiceDownloadUrl
88
+ if (existing.status === 'failed') {
89
+ existing.status = 'pending_download'
90
+ existing.errorMessage = null
91
+ existing.updatedAt = new Date()
92
+ }
93
+ }
94
+ }
95
+
96
+ await em.flush()
97
+
98
+ if (collectedRefs.length === 0) {
99
+ await emitKsefDirectEvent('ksef_direct.received_document.synced', {
100
+ organizationId: payload.organizationId,
101
+ tenantId: payload.tenantId,
102
+ syncedCount: 0,
103
+ failedCount: 0,
104
+ dateFrom: payload.dateFrom,
105
+ dateTo: payload.dateTo,
106
+ })
107
+ return
108
+ }
109
+
110
+ // Phase 2: download XML content for all pending records using stored URLs
111
+ const pendingRecords = await em.find(KsefDirectReceivedDocument, {
112
+ organizationId: payload.organizationId,
113
+ tenantId: payload.tenantId,
114
+ status: 'pending_download',
115
+ ksefReferenceNumber: { $in: collectedRefs },
116
+ })
117
+
118
+ let syncedCount = 0
119
+ let failedCount = 0
120
+
121
+ for (const record of pendingRecords) {
122
+ try {
123
+ const downloadUrl = record.invoiceDownloadUrl ?? record.upoDownloadUrl
124
+ if (!downloadUrl) {
125
+ throw new Error('No download URL available for this invoice')
126
+ }
127
+
128
+ const rawXml = await downloadInvoiceFromUrl(downloadUrl)
129
+ const parsed = parseReceivedInvoiceXml(rawXml)
130
+
131
+ record.rawXml = rawXml
132
+ // Only override metadata fields with parsed values when non-null (prefer KSeF API metadata)
133
+ if (parsed.invoiceNumber) record.invoiceNumber = parsed.invoiceNumber
134
+ if (parsed.sellerNip) record.sellerNip = parsed.sellerNip
135
+ if (parsed.sellerName) record.sellerName = parsed.sellerName
136
+ if (parsed.issueDate) record.issueDate = parsed.issueDate
137
+ if (parsed.currency) record.currency = parsed.currency
138
+ if (parsed.netAmount) record.netAmount = parsed.netAmount
139
+ if (parsed.vatAmount) record.vatAmount = parsed.vatAmount
140
+ if (parsed.grossAmount) record.grossAmount = parsed.grossAmount
141
+ record.status = 'downloaded'
142
+ record.syncedAt = new Date()
143
+ record.updatedAt = new Date()
144
+ await em.flush()
145
+ syncedCount++
146
+ } catch (err) {
147
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error during XML download'
148
+ record.status = 'failed'
149
+ record.errorMessage = errorMessage
150
+ record.updatedAt = new Date()
151
+ await em.flush()
152
+ failedCount++
153
+
154
+ await emitKsefDirectEvent('ksef_direct.received_document.failed', {
155
+ organizationId: payload.organizationId,
156
+ tenantId: payload.tenantId,
157
+ ksefReferenceNumber: record.ksefReferenceNumber,
158
+ errorMessage,
159
+ })
160
+ }
161
+ }
162
+
163
+ await emitKsefDirectEvent('ksef_direct.received_document.synced', {
164
+ organizationId: payload.organizationId,
165
+ tenantId: payload.tenantId,
166
+ syncedCount,
167
+ failedCount,
168
+ dateFrom: payload.dateFrom,
169
+ dateTo: payload.dateTo,
170
+ })
171
+ }
@@ -0,0 +1 @@
1
+ declare module '@open-mercato/core/*'