@open-mercato/core 0.6.6 → 0.6.7-develop.6552.1.5208a19369

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 (268) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/gateway_session_initialization/index.js +23 -0
  3. package/dist/generated/entities/gateway_session_initialization/index.js.map +7 -0
  4. package/dist/generated/entities.ids.generated.js +1 -0
  5. package/dist/generated/entities.ids.generated.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +12 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/api_keys/api/keys/route.js +30 -13
  9. package/dist/modules/api_keys/api/keys/route.js.map +2 -2
  10. package/dist/modules/api_keys/cli.js +2 -1
  11. package/dist/modules/api_keys/cli.js.map +2 -2
  12. package/dist/modules/attachments/api/file/[id]/route.js +9 -7
  13. package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
  14. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +9 -7
  15. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
  16. package/dist/modules/attachments/api/route.js +21 -17
  17. package/dist/modules/attachments/api/route.js.map +2 -2
  18. package/dist/modules/attachments/components/AttachmentContentPreview.js +6 -35
  19. package/dist/modules/attachments/components/AttachmentContentPreview.js.map +2 -2
  20. package/dist/modules/attachments/lib/reconcileOrganization.js +110 -0
  21. package/dist/modules/attachments/lib/reconcileOrganization.js.map +7 -0
  22. package/dist/modules/attachments/lib/requestScope.js +10 -0
  23. package/dist/modules/attachments/lib/requestScope.js.map +7 -0
  24. package/dist/modules/audit_logs/backend/audit-logs/page.js +14 -24
  25. package/dist/modules/audit_logs/backend/audit-logs/page.js.map +2 -2
  26. package/dist/modules/auth/api/autologin.js +85 -0
  27. package/dist/modules/auth/api/autologin.js.map +7 -0
  28. package/dist/modules/auth/api/users/route.js +6 -2
  29. package/dist/modules/auth/api/users/route.js.map +2 -2
  30. package/dist/modules/auth/cli.js +3 -2
  31. package/dist/modules/auth/cli.js.map +2 -2
  32. package/dist/modules/auth/lib/autologin.js +19 -0
  33. package/dist/modules/auth/lib/autologin.js.map +7 -0
  34. package/dist/modules/configs/lib/upgrade-actions.js +22 -0
  35. package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
  36. package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js +1 -0
  37. package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js.map +2 -2
  38. package/dist/modules/customer_accounts/api/admin/roles/[id].js +17 -3
  39. package/dist/modules/customer_accounts/api/admin/roles/[id].js.map +2 -2
  40. package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js +6 -1
  41. package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js.map +2 -2
  42. package/dist/modules/customer_accounts/api/portal/users-invite.js +6 -1
  43. package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
  44. package/dist/modules/customer_accounts/services/customerInvitationService.js +1 -0
  45. package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
  46. package/dist/modules/customer_accounts/services/customerRbacService.js +6 -1
  47. package/dist/modules/customer_accounts/services/customerRbacService.js.map +2 -2
  48. package/dist/modules/customer_accounts/widgets/injection-table.js +0 -2
  49. package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
  50. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +7 -1
  51. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  52. package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js +2 -2
  53. package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js.map +2 -2
  54. package/dist/modules/customers/components/detail/CompanyDetailTabs.js +37 -43
  55. package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
  56. package/dist/modules/customers/components/detail/DealDetailTabs.js +23 -35
  57. package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
  58. package/dist/modules/customers/components/detail/DetailTabsLayout.js +6 -21
  59. package/dist/modules/customers/components/detail/DetailTabsLayout.js.map +2 -2
  60. package/dist/modules/customers/components/detail/MobilePersonDetail.js +26 -32
  61. package/dist/modules/customers/components/detail/MobilePersonDetail.js.map +2 -2
  62. package/dist/modules/customers/components/detail/PersonDetailTabs.js +29 -39
  63. package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
  64. package/dist/modules/customers/components/detail/useDealsAccess.js +16 -0
  65. package/dist/modules/customers/components/detail/useDealsAccess.js.map +7 -0
  66. package/dist/modules/dashboards/api/roles/widgets/route.js +22 -3
  67. package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
  68. package/dist/modules/dashboards/api/users/widgets/route.js +22 -3
  69. package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
  70. package/dist/modules/dashboards/cli.js +4 -3
  71. package/dist/modules/dashboards/cli.js.map +2 -2
  72. package/dist/modules/dashboards/lib/widgetAssignmentScope.js +9 -1
  73. package/dist/modules/dashboards/lib/widgetAssignmentScope.js.map +2 -2
  74. package/dist/modules/dictionaries/commands/entries.js +7 -14
  75. package/dist/modules/dictionaries/commands/entries.js.map +2 -2
  76. package/dist/modules/dictionaries/commands/entry-operations.js +7 -16
  77. package/dist/modules/dictionaries/commands/entry-operations.js.map +2 -2
  78. package/dist/modules/dictionaries/commands/factory.js +9 -3
  79. package/dist/modules/dictionaries/commands/factory.js.map +2 -2
  80. package/dist/modules/dictionaries/fields/dictionary.js +56 -41
  81. package/dist/modules/dictionaries/fields/dictionary.js.map +2 -2
  82. package/dist/modules/directory/utils/organizationScope.js +10 -3
  83. package/dist/modules/directory/utils/organizationScope.js.map +2 -2
  84. package/dist/modules/entities/api/records.js +5 -4
  85. package/dist/modules/entities/api/records.js.map +2 -2
  86. package/dist/modules/entities/backend/entities/system/[entityId]/page.meta.js +1 -0
  87. package/dist/modules/entities/backend/entities/system/[entityId]/page.meta.js.map +2 -2
  88. package/dist/modules/entities/backend/entities/user/[entityId]/page.js +49 -49
  89. package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
  90. package/dist/modules/entities/backend/entities/user/[entityId]/page.meta.js +1 -0
  91. package/dist/modules/entities/backend/entities/user/[entityId]/page.meta.js.map +2 -2
  92. package/dist/modules/entities/cli.js +2 -1
  93. package/dist/modules/entities/cli.js.map +2 -2
  94. package/dist/modules/feature_toggles/api/check/boolean/route.js +0 -14
  95. package/dist/modules/feature_toggles/api/check/boolean/route.js.map +2 -2
  96. package/dist/modules/feature_toggles/defaults.json +8 -0
  97. package/dist/modules/integrations/backend/integrations/[id]/page.js +9 -80
  98. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  99. package/dist/modules/payment_gateways/data/entities.js +45 -0
  100. package/dist/modules/payment_gateways/data/entities.js.map +2 -2
  101. package/dist/modules/payment_gateways/lib/gateway-service.js +173 -38
  102. package/dist/modules/payment_gateways/lib/gateway-service.js.map +3 -3
  103. package/dist/modules/payment_gateways/lib/session-idempotency.js +104 -0
  104. package/dist/modules/payment_gateways/lib/session-idempotency.js.map +7 -0
  105. package/dist/modules/payment_gateways/migrations/Migration20260709220938_payment_gateways.js +11 -0
  106. package/dist/modules/payment_gateways/migrations/Migration20260709220938_payment_gateways.js.map +7 -0
  107. package/dist/modules/planner/backend/planner/availability-rulesets/[id]/page.js +16 -14
  108. package/dist/modules/planner/backend/planner/availability-rulesets/[id]/page.js.map +2 -2
  109. package/dist/modules/planner/components/AvailabilityRulesEditor.js +18 -30
  110. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  111. package/dist/modules/query_index/api/status.js +2 -8
  112. package/dist/modules/query_index/api/status.js.map +2 -2
  113. package/dist/modules/resources/backend/resources/resources/[id]/page.js +24 -24
  114. package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
  115. package/dist/modules/sales/backend/sales/channels/create/page.js +6 -0
  116. package/dist/modules/sales/backend/sales/channels/create/page.js.map +2 -2
  117. package/dist/modules/sales/backend/sales/channels/create/page.meta.js +2 -0
  118. package/dist/modules/sales/backend/sales/channels/create/page.meta.js.map +2 -2
  119. package/dist/modules/sales/backend/sales/channels/offers/page.js +6 -0
  120. package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
  121. package/dist/modules/sales/backend/sales/channels/offers/page.meta.js +2 -0
  122. package/dist/modules/sales/backend/sales/channels/offers/page.meta.js.map +2 -2
  123. package/dist/modules/sales/backend/sales/channels/page.js +6 -0
  124. package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
  125. package/dist/modules/sales/backend/sales/channels/page.meta.js +2 -0
  126. package/dist/modules/sales/backend/sales/channels/page.meta.js.map +2 -2
  127. package/dist/modules/sales/backend/sales/documents/[id]/page.js +6 -4
  128. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  129. package/dist/modules/sales/commands/documents.js +44 -0
  130. package/dist/modules/sales/commands/documents.js.map +2 -2
  131. package/dist/modules/sales/commands/returns.js +2 -27
  132. package/dist/modules/sales/commands/returns.js.map +2 -2
  133. package/dist/modules/sales/components/SalesChannelsDisabledNotice.js +15 -0
  134. package/dist/modules/sales/components/SalesChannelsDisabledNotice.js.map +7 -0
  135. package/dist/modules/sales/components/documents/AddressesSection.js +6 -2
  136. package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
  137. package/dist/modules/sales/components/documents/ItemsSection.js +1 -0
  138. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  139. package/dist/modules/sales/components/documents/LineItemDialog.js +9 -0
  140. package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
  141. package/dist/modules/sales/components/documents/SalesDocumentForm.js +51 -3
  142. package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
  143. package/dist/modules/sales/components/documents/SalesDocumentsTable.js +10 -8
  144. package/dist/modules/sales/components/documents/SalesDocumentsTable.js.map +2 -2
  145. package/dist/modules/sales/components/documents/ShipmentDialog.js +59 -38
  146. package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
  147. package/dist/modules/sales/components/useSalesChannelsEnabled.js +44 -0
  148. package/dist/modules/sales/components/useSalesChannelsEnabled.js.map +7 -0
  149. package/dist/modules/sales/lib/salesChannelsToggle.js +18 -0
  150. package/dist/modules/sales/lib/salesChannelsToggle.js.map +7 -0
  151. package/dist/modules/sales/lib/salesChannelsToggleId.js +5 -0
  152. package/dist/modules/sales/lib/salesChannelsToggleId.js.map +7 -0
  153. package/dist/modules/sales/lib/shipments/snapshots.js +27 -1
  154. package/dist/modules/sales/lib/shipments/snapshots.js.map +2 -2
  155. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +22 -25
  156. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  157. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +12 -14
  158. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
  159. package/dist/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.js +10 -29
  160. package/dist/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.js.map +2 -2
  161. package/dist/modules/sync_excel/lib/adapters/customers.js +19 -20
  162. package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
  163. package/dist/modules/sync_excel/lib/parser.js +185 -0
  164. package/dist/modules/sync_excel/lib/parser.js.map +2 -2
  165. package/dist/modules/sync_excel/lib/upload-storage.js +20 -1
  166. package/dist/modules/sync_excel/lib/upload-storage.js.map +2 -2
  167. package/generated/entities/gateway_session_initialization/index.ts +10 -0
  168. package/generated/entities.ids.generated.ts +1 -0
  169. package/generated/entity-fields-registry.ts +12 -0
  170. package/package.json +9 -8
  171. package/src/modules/api_keys/api/keys/route.ts +36 -13
  172. package/src/modules/api_keys/cli.ts +2 -3
  173. package/src/modules/attachments/api/file/[id]/route.ts +11 -7
  174. package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +11 -7
  175. package/src/modules/attachments/api/route.ts +26 -21
  176. package/src/modules/attachments/components/AttachmentContentPreview.tsx +9 -44
  177. package/src/modules/attachments/lib/reconcileOrganization.ts +188 -0
  178. package/src/modules/attachments/lib/requestScope.ts +27 -0
  179. package/src/modules/audit_logs/backend/audit-logs/page.tsx +12 -22
  180. package/src/modules/auth/api/autologin.ts +100 -0
  181. package/src/modules/auth/api/users/route.ts +11 -2
  182. package/src/modules/auth/cli.ts +3 -2
  183. package/src/modules/auth/lib/autologin.ts +36 -0
  184. package/src/modules/configs/i18n/de.json +5 -1
  185. package/src/modules/configs/i18n/en.json +5 -1
  186. package/src/modules/configs/i18n/es.json +5 -1
  187. package/src/modules/configs/i18n/pl.json +5 -1
  188. package/src/modules/configs/lib/upgrade-actions.ts +24 -0
  189. package/src/modules/customer_accounts/AGENTS.md +8 -6
  190. package/src/modules/customer_accounts/api/admin/roles/[id]/acl.ts +1 -0
  191. package/src/modules/customer_accounts/api/admin/roles/[id].ts +17 -3
  192. package/src/modules/customer_accounts/api/portal/users/[id]/roles.ts +6 -1
  193. package/src/modules/customer_accounts/api/portal/users-invite.ts +6 -1
  194. package/src/modules/customer_accounts/services/customerInvitationService.ts +1 -0
  195. package/src/modules/customer_accounts/services/customerRbacService.ts +9 -2
  196. package/src/modules/customer_accounts/widgets/injection-table.ts +0 -2
  197. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +11 -1
  198. package/src/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.tsx +6 -2
  199. package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +39 -55
  200. package/src/modules/customers/components/detail/DealDetailTabs.tsx +24 -49
  201. package/src/modules/customers/components/detail/DetailTabsLayout.tsx +13 -24
  202. package/src/modules/customers/components/detail/MobilePersonDetail.tsx +25 -31
  203. package/src/modules/customers/components/detail/PersonDetailTabs.tsx +29 -50
  204. package/src/modules/customers/components/detail/useDealsAccess.ts +19 -0
  205. package/src/modules/dashboards/api/roles/widgets/route.ts +22 -3
  206. package/src/modules/dashboards/api/users/widgets/route.ts +22 -3
  207. package/src/modules/dashboards/cli.ts +4 -9
  208. package/src/modules/dashboards/lib/widgetAssignmentScope.ts +18 -0
  209. package/src/modules/dictionaries/commands/entries.ts +7 -16
  210. package/src/modules/dictionaries/commands/entry-operations.ts +8 -18
  211. package/src/modules/dictionaries/commands/factory.ts +8 -2
  212. package/src/modules/dictionaries/fields/dictionary.tsx +55 -32
  213. package/src/modules/dictionaries/i18n/de.json +2 -0
  214. package/src/modules/dictionaries/i18n/en.json +2 -0
  215. package/src/modules/dictionaries/i18n/es.json +2 -0
  216. package/src/modules/dictionaries/i18n/pl.json +2 -0
  217. package/src/modules/directory/utils/organizationScope.ts +36 -2
  218. package/src/modules/entities/api/records.ts +5 -7
  219. package/src/modules/entities/backend/entities/system/[entityId]/page.meta.ts +1 -0
  220. package/src/modules/entities/backend/entities/user/[entityId]/page.meta.ts +1 -0
  221. package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +51 -49
  222. package/src/modules/entities/cli.ts +2 -1
  223. package/src/modules/entities/i18n/de.json +34 -0
  224. package/src/modules/entities/i18n/en.json +34 -0
  225. package/src/modules/entities/i18n/es.json +34 -0
  226. package/src/modules/entities/i18n/pl.json +34 -0
  227. package/src/modules/feature_toggles/api/check/boolean/route.ts +0 -15
  228. package/src/modules/feature_toggles/defaults.json +8 -0
  229. package/src/modules/integrations/backend/integrations/[id]/page.tsx +16 -59
  230. package/src/modules/payment_gateways/data/entities.ts +39 -0
  231. package/src/modules/payment_gateways/lib/gateway-service.ts +211 -40
  232. package/src/modules/payment_gateways/lib/session-idempotency.ts +142 -0
  233. package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +236 -0
  234. package/src/modules/payment_gateways/migrations/Migration20260709220938_payment_gateways.ts +10 -0
  235. package/src/modules/planner/backend/planner/availability-rulesets/[id]/page.tsx +14 -20
  236. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +16 -34
  237. package/src/modules/query_index/api/status.ts +2 -8
  238. package/src/modules/resources/backend/resources/resources/[id]/page.tsx +27 -37
  239. package/src/modules/sales/backend/sales/channels/create/page.meta.ts +3 -0
  240. package/src/modules/sales/backend/sales/channels/create/page.tsx +7 -0
  241. package/src/modules/sales/backend/sales/channels/offers/page.meta.ts +3 -0
  242. package/src/modules/sales/backend/sales/channels/offers/page.tsx +7 -0
  243. package/src/modules/sales/backend/sales/channels/page.meta.ts +4 -0
  244. package/src/modules/sales/backend/sales/channels/page.tsx +7 -0
  245. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +7 -5
  246. package/src/modules/sales/commands/documents.ts +65 -0
  247. package/src/modules/sales/commands/returns.ts +2 -32
  248. package/src/modules/sales/components/SalesChannelsDisabledNotice.tsx +21 -0
  249. package/src/modules/sales/components/documents/AddressesSection.tsx +6 -2
  250. package/src/modules/sales/components/documents/ItemsSection.tsx +5 -0
  251. package/src/modules/sales/components/documents/LineItemDialog.tsx +10 -0
  252. package/src/modules/sales/components/documents/SalesDocumentForm.tsx +53 -3
  253. package/src/modules/sales/components/documents/SalesDocumentsTable.tsx +10 -8
  254. package/src/modules/sales/components/documents/ShipmentDialog.tsx +65 -40
  255. package/src/modules/sales/components/useSalesChannelsEnabled.ts +59 -0
  256. package/src/modules/sales/i18n/de.json +5 -0
  257. package/src/modules/sales/i18n/en.json +5 -0
  258. package/src/modules/sales/i18n/es.json +5 -0
  259. package/src/modules/sales/i18n/pl.json +5 -0
  260. package/src/modules/sales/lib/salesChannelsToggle.ts +26 -0
  261. package/src/modules/sales/lib/salesChannelsToggleId.ts +1 -0
  262. package/src/modules/sales/lib/shipments/snapshots.ts +30 -0
  263. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +25 -34
  264. package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +11 -17
  265. package/src/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.tsx +11 -28
  266. package/src/modules/sync_excel/lib/adapters/customers.ts +20 -22
  267. package/src/modules/sync_excel/lib/parser.ts +245 -0
  268. package/src/modules/sync_excel/lib/upload-storage.ts +23 -1
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from 'node:crypto'
1
2
  import type { EntityManager } from '@mikro-orm/postgresql'
2
3
  import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
3
4
  import {
@@ -15,10 +16,21 @@ import type { CredentialsService } from '../../integrations/lib/credentials-serv
15
16
  import type { IntegrationStateService } from '../../integrations/lib/state-service'
16
17
  import type { IntegrationLogService } from '../../integrations/lib/log-service'
17
18
  import { conflict } from '@open-mercato/shared/lib/crud/errors'
18
- import { GatewayTransaction } from '../data/entities'
19
+ import { GatewaySessionInitialization, GatewayTransaction } from '../data/entities'
19
20
  import { canApplyManualAction, isValidTransition, type ManualGatewayAction } from './status-machine'
20
21
  import { emitPaymentGatewayEvent } from '../events'
21
22
  import { readGatewayMetadata, readWebhookLog } from './transaction-fields'
23
+ import {
24
+ buildPaymentSessionOperationKey,
25
+ claimPaymentSessionInitialization,
26
+ findPaymentSessionInitialization,
27
+ reclaimPaymentSessionInitialization,
28
+ refreshPaymentSessionInitialization,
29
+ releasePaymentSessionInitialization,
30
+ } from './session-idempotency'
31
+
32
+ const PAYMENT_SESSION_CLAIM_STALE_MS = 30_000
33
+ const PAYMENT_SESSION_WAIT_INTERVAL_MS = 25
22
34
 
23
35
  function assertManualActionAllowed(action: ManualGatewayAction, transaction: GatewayTransaction): void {
24
36
  const current = transaction.unifiedStatus as UnifiedPaymentStatus
@@ -48,11 +60,17 @@ export interface PaymentGatewayServiceDeps {
48
60
  integrationCredentialsService: CredentialsService
49
61
  integrationStateService?: IntegrationStateService
50
62
  integrationLogService?: IntegrationLogService
63
+ sessionClaimOptions?: {
64
+ staleAfterMs?: number
65
+ heartbeatIntervalMs?: number
66
+ pollIntervalMs?: number
67
+ }
51
68
  }
52
69
 
53
70
  export interface CreatePaymentSessionInput {
54
71
  providerKey: string
55
72
  paymentId: string
73
+ idempotencyKey?: string
56
74
  orderId?: string
57
75
  amount: number
58
76
  currencyCode: string
@@ -69,6 +87,12 @@ export interface CreatePaymentSessionInput {
69
87
 
70
88
  export function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {
71
89
  const { em, integrationCredentialsService, integrationLogService } = deps
90
+ const claimStaleAfterMs = Math.max(1, deps.sessionClaimOptions?.staleAfterMs ?? PAYMENT_SESSION_CLAIM_STALE_MS)
91
+ const claimHeartbeatIntervalMs = Math.max(
92
+ 1,
93
+ deps.sessionClaimOptions?.heartbeatIntervalMs ?? Math.floor(claimStaleAfterMs / 3),
94
+ )
95
+ const claimPollIntervalMs = Math.max(1, deps.sessionClaimOptions?.pollIntervalMs ?? PAYMENT_SESSION_WAIT_INTERVAL_MS)
72
96
 
73
97
  async function findTransactionOrThrow(
74
98
  transactionId: string,
@@ -152,6 +176,109 @@ export function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {
152
176
  return { adapter, credentials }
153
177
  }
154
178
 
179
+ function createGatewayTransaction(
180
+ manager: EntityManager,
181
+ input: CreatePaymentSessionInput,
182
+ session: CreateSessionResult,
183
+ id?: string,
184
+ ): GatewayTransaction {
185
+ const data = {
186
+ paymentId: input.paymentId,
187
+ providerKey: input.providerKey,
188
+ providerSessionId: session.sessionId,
189
+ unifiedStatus: session.status,
190
+ redirectUrl: session.redirectUrl
191
+ ?? (session.clientSession?.type === 'redirect' ? session.clientSession.redirectUrl : null),
192
+ clientSecret: session.clientSecret ?? null,
193
+ amount: String(input.amount),
194
+ currencyCode: input.currencyCode,
195
+ gatewayMetadata: {
196
+ ...(session.providerData ?? {}),
197
+ ...(session.clientSession ? { clientSession: session.clientSession } : {}),
198
+ },
199
+ organizationId: input.organizationId,
200
+ tenantId: input.tenantId,
201
+ deletedAt: null,
202
+ }
203
+ return id
204
+ ? manager.create(GatewayTransaction, { id, ...data })
205
+ : manager.create(GatewayTransaction, data)
206
+ }
207
+
208
+ function restoreSession(transaction: GatewayTransaction): CreateSessionResult {
209
+ const metadata = readGatewayMetadata(transaction.gatewayMetadata)
210
+ const { clientSession, ...providerData } = metadata
211
+ return {
212
+ sessionId: readProviderSessionId(transaction),
213
+ status: transaction.unifiedStatus as UnifiedPaymentStatus,
214
+ ...(transaction.clientSecret ? { clientSecret: transaction.clientSecret } : {}),
215
+ ...(transaction.redirectUrl ? { redirectUrl: transaction.redirectUrl } : {}),
216
+ ...(Object.keys(providerData).length > 0 ? { providerData } : {}),
217
+ ...(clientSession && typeof clientSession === 'object'
218
+ ? { clientSession: clientSession as CreateSessionResult['clientSession'] }
219
+ : {}),
220
+ }
221
+ }
222
+
223
+ async function recordCreatedSession(
224
+ transaction: GatewayTransaction,
225
+ input: CreatePaymentSessionInput,
226
+ scope: { organizationId: string; tenantId: string },
227
+ ): Promise<void> {
228
+ await emitPaymentGatewayEvent('payment_gateways.session.created', {
229
+ transactionId: transaction.id,
230
+ paymentId: transaction.paymentId,
231
+ providerKey: transaction.providerKey,
232
+ status: transaction.unifiedStatus,
233
+ organizationId: transaction.organizationId,
234
+ tenantId: transaction.tenantId,
235
+ })
236
+ await writeTransactionLog(
237
+ transaction.providerKey,
238
+ scope,
239
+ transaction.id,
240
+ 'info',
241
+ 'Payment session created',
242
+ {
243
+ paymentId: transaction.paymentId,
244
+ providerSessionId: transaction.providerSessionId,
245
+ status: transaction.unifiedStatus,
246
+ amount: input.amount,
247
+ currencyCode: input.currencyCode,
248
+ },
249
+ )
250
+ }
251
+
252
+ function startClaimHeartbeat(
253
+ ownership: { id: string; claimToken: string },
254
+ scope: { organizationId: string; tenantId: string },
255
+ ): () => Promise<void> {
256
+ let stopped = false
257
+ let timer: ReturnType<typeof setTimeout> | null = null
258
+ let pendingRefresh = Promise.resolve()
259
+
260
+ const schedule = () => {
261
+ timer = setTimeout(() => {
262
+ pendingRefresh = refreshPaymentSessionInitialization(em, ownership, scope, new Date())
263
+ .then(
264
+ (refreshed) => {
265
+ if (refreshed && !stopped) schedule()
266
+ },
267
+ () => {
268
+ if (!stopped) schedule()
269
+ },
270
+ )
271
+ }, claimHeartbeatIntervalMs)
272
+ }
273
+ schedule()
274
+
275
+ return async () => {
276
+ stopped = true
277
+ if (timer) clearTimeout(timer)
278
+ await pendingRefresh
279
+ }
280
+ }
281
+
155
282
  return {
156
283
  async createPaymentSession(input: CreatePaymentSessionInput): Promise<{ transaction: GatewayTransaction; session: CreateSessionResult }> {
157
284
  const scope = { organizationId: input.organizationId, tenantId: input.tenantId }
@@ -160,6 +287,7 @@ export function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {
160
287
  const sessionInput: CreateSessionInput = {
161
288
  paymentId: input.paymentId,
162
289
  orderId: input.orderId,
290
+ idempotencyKey: input.idempotencyKey,
163
291
  tenantId: input.tenantId,
164
292
  organizationId: input.organizationId,
165
293
  amount: input.amount,
@@ -174,50 +302,93 @@ export function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {
174
302
  credentials,
175
303
  }
176
304
 
177
- const session = await adapter.createSession(sessionInput)
305
+ if (!input.idempotencyKey) {
306
+ const session = await adapter.createSession(sessionInput)
307
+ const transaction = createGatewayTransaction(em, input, session)
308
+ await em.persist(transaction).flush()
309
+ await recordCreatedSession(transaction, input, scope)
310
+ return { transaction, session }
311
+ }
178
312
 
179
- const transaction = em.create(GatewayTransaction, {
313
+ const operationKey = buildPaymentSessionOperationKey({
314
+ idempotencyKey: input.idempotencyKey,
180
315
  paymentId: input.paymentId,
181
316
  providerKey: input.providerKey,
182
- providerSessionId: session.sessionId,
183
- unifiedStatus: session.status,
184
- redirectUrl: session.redirectUrl
185
- ?? (session.clientSession?.type === 'redirect' ? session.clientSession.redirectUrl : null),
186
- clientSecret: session.clientSecret ?? null,
187
- amount: String(input.amount),
188
- currencyCode: input.currencyCode,
189
- gatewayMetadata: {
190
- ...(session.providerData ?? {}),
191
- ...(session.clientSession ? { clientSession: session.clientSession } : {}),
192
- },
193
- organizationId: input.organizationId,
194
- tenantId: input.tenantId,
195
- })
196
- await em.persist(transaction).flush()
197
- await emitPaymentGatewayEvent('payment_gateways.session.created', {
198
- transactionId: transaction.id,
199
- paymentId: transaction.paymentId,
200
- providerKey: transaction.providerKey,
201
- status: transaction.unifiedStatus,
202
- organizationId: transaction.organizationId,
203
- tenantId: transaction.tenantId,
204
- })
205
- await writeTransactionLog(
206
- transaction.providerKey,
207
317
  scope,
208
- transaction.id,
209
- 'info',
210
- 'Payment session created',
211
- {
212
- paymentId: transaction.paymentId,
213
- providerSessionId: transaction.providerSessionId,
214
- status: transaction.unifiedStatus,
215
- amount: input.amount,
216
- currencyCode: input.currencyCode,
217
- },
218
- )
318
+ })
319
+ while (true) {
320
+ const existing = await findPaymentSessionInitialization(em, operationKey, input.providerKey, scope)
321
+ if (existing?.gatewayTransactionId) {
322
+ const transaction = await findOneWithDecryption(
323
+ em.fork(),
324
+ GatewayTransaction,
325
+ {
326
+ id: existing.gatewayTransactionId,
327
+ organizationId: scope.organizationId,
328
+ tenantId: scope.tenantId,
329
+ deletedAt: null,
330
+ },
331
+ undefined,
332
+ scope,
333
+ )
334
+ if (!transaction) {
335
+ throw new Error('Completed payment session is missing its gateway transaction')
336
+ }
337
+ return { transaction, session: restoreSession(transaction) }
338
+ }
339
+
340
+ const claimedAt = new Date()
341
+ const staleBefore = new Date(claimedAt.getTime() - claimStaleAfterMs)
342
+ const ownership = existing
343
+ ? await reclaimPaymentSessionInitialization(em, existing, scope, claimedAt, staleBefore)
344
+ : await claimPaymentSessionInitialization(em, operationKey, input.providerKey, scope, claimedAt)
345
+
346
+ if (!ownership) {
347
+ await new Promise((resolve) => setTimeout(resolve, claimPollIntervalMs))
348
+ continue
349
+ }
350
+
351
+ let session: CreateSessionResult
352
+ const stopHeartbeat = startClaimHeartbeat(ownership, scope)
353
+ try {
354
+ session = await adapter.createSession({ ...sessionInput, idempotencyKey: operationKey })
355
+ } catch (error) {
356
+ await releasePaymentSessionInitialization(em, ownership, scope).catch(() => undefined)
357
+ throw error
358
+ } finally {
359
+ await stopHeartbeat()
360
+ }
361
+
362
+ const transactionId = randomUUID()
363
+ const transaction = await em.fork().transactional(async (transactionEm) => {
364
+ const finalizedRows = await transactionEm.nativeUpdate(
365
+ GatewaySessionInitialization,
366
+ {
367
+ id: ownership.id,
368
+ claimToken: ownership.claimToken,
369
+ gatewayTransactionId: null,
370
+ organizationId: scope.organizationId,
371
+ tenantId: scope.tenantId,
372
+ },
373
+ {
374
+ gatewayTransactionId: transactionId,
375
+ claimToken: null,
376
+ claimedAt: null,
377
+ updatedAt: new Date(),
378
+ },
379
+ )
380
+ if (finalizedRows === 0) return null
381
+ const created = createGatewayTransaction(transactionEm, input, session, transactionId)
382
+ await transactionEm.persist(created).flush()
383
+ return created
384
+ })
219
385
 
220
- return { transaction, session }
386
+ if (!transaction) {
387
+ continue
388
+ }
389
+ await recordCreatedSession(transaction, input, scope)
390
+ return { transaction, session }
391
+ }
221
392
  },
222
393
 
223
394
  async capturePayment(transactionId: string, amount: number | undefined, scope: { organizationId: string; tenantId: string }): Promise<CaptureResult> {
@@ -0,0 +1,142 @@
1
+ import { createHash, randomUUID } from 'node:crypto'
2
+ import { UniqueConstraintViolationException } from '@mikro-orm/core'
3
+ import type { EntityManager } from '@mikro-orm/postgresql'
4
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
5
+ import { GatewaySessionInitialization } from '../data/entities'
6
+
7
+ type Scope = { organizationId: string; tenantId: string }
8
+
9
+ export type OwnedPaymentSessionInitialization = {
10
+ id: string
11
+ claimToken: string
12
+ }
13
+
14
+ export function buildPaymentSessionOperationKey(input: {
15
+ idempotencyKey: string
16
+ paymentId: string
17
+ providerKey: string
18
+ scope: Scope
19
+ }): string {
20
+ const digest = createHash('sha256')
21
+ .update(JSON.stringify([
22
+ input.scope.tenantId,
23
+ input.scope.organizationId,
24
+ input.providerKey,
25
+ input.paymentId,
26
+ input.idempotencyKey,
27
+ ]))
28
+ .digest('hex')
29
+ return `om-payment-session:${digest}`
30
+ }
31
+
32
+ export async function findPaymentSessionInitialization(
33
+ em: EntityManager,
34
+ operationKey: string,
35
+ providerKey: string,
36
+ scope: Scope,
37
+ ): Promise<GatewaySessionInitialization | null> {
38
+ return findOneWithDecryption(
39
+ em.fork(),
40
+ GatewaySessionInitialization,
41
+ {
42
+ operationKey,
43
+ providerKey,
44
+ organizationId: scope.organizationId,
45
+ tenantId: scope.tenantId,
46
+ },
47
+ undefined,
48
+ scope,
49
+ )
50
+ }
51
+
52
+ export async function claimPaymentSessionInitialization(
53
+ em: EntityManager,
54
+ operationKey: string,
55
+ providerKey: string,
56
+ scope: Scope,
57
+ claimedAt: Date,
58
+ ): Promise<OwnedPaymentSessionInitialization | null> {
59
+ const claimEm = em.fork()
60
+ const claimToken = randomUUID()
61
+ const record = claimEm.create(GatewaySessionInitialization, {
62
+ operationKey,
63
+ providerKey,
64
+ claimToken,
65
+ claimedAt,
66
+ gatewayTransactionId: null,
67
+ organizationId: scope.organizationId,
68
+ tenantId: scope.tenantId,
69
+ })
70
+ try {
71
+ await claimEm.persist(record).flush()
72
+ return { id: record.id, claimToken }
73
+ } catch (error: unknown) {
74
+ if (error instanceof UniqueConstraintViolationException) return null
75
+ throw error
76
+ }
77
+ }
78
+
79
+ export async function reclaimPaymentSessionInitialization(
80
+ em: EntityManager,
81
+ claim: GatewaySessionInitialization,
82
+ scope: Scope,
83
+ claimedAt: Date,
84
+ staleBefore: Date,
85
+ ): Promise<OwnedPaymentSessionInitialization | null> {
86
+ const claimToken = randomUUID()
87
+ const claimedRows = await em.fork().nativeUpdate(
88
+ GatewaySessionInitialization,
89
+ {
90
+ id: claim.id,
91
+ operationKey: claim.operationKey,
92
+ providerKey: claim.providerKey,
93
+ organizationId: scope.organizationId,
94
+ tenantId: scope.tenantId,
95
+ gatewayTransactionId: null,
96
+ $or: [
97
+ { claimToken: null },
98
+ { claimedAt: { $lt: staleBefore } },
99
+ ],
100
+ },
101
+ { claimToken, claimedAt, updatedAt: claimedAt },
102
+ )
103
+ return claimedRows > 0 ? { id: claim.id, claimToken } : null
104
+ }
105
+
106
+ export async function releasePaymentSessionInitialization(
107
+ em: EntityManager,
108
+ ownership: OwnedPaymentSessionInitialization,
109
+ scope: Scope,
110
+ ): Promise<void> {
111
+ await em.fork().nativeUpdate(
112
+ GatewaySessionInitialization,
113
+ {
114
+ id: ownership.id,
115
+ claimToken: ownership.claimToken,
116
+ organizationId: scope.organizationId,
117
+ tenantId: scope.tenantId,
118
+ gatewayTransactionId: null,
119
+ },
120
+ { claimToken: null, claimedAt: null, updatedAt: new Date() },
121
+ )
122
+ }
123
+
124
+ export async function refreshPaymentSessionInitialization(
125
+ em: EntityManager,
126
+ ownership: OwnedPaymentSessionInitialization,
127
+ scope: Scope,
128
+ claimedAt: Date,
129
+ ): Promise<boolean> {
130
+ const refreshedRows = await em.fork().nativeUpdate(
131
+ GatewaySessionInitialization,
132
+ {
133
+ id: ownership.id,
134
+ claimToken: ownership.claimToken,
135
+ organizationId: scope.organizationId,
136
+ tenantId: scope.tenantId,
137
+ gatewayTransactionId: null,
138
+ },
139
+ { claimedAt, updatedAt: claimedAt },
140
+ )
141
+ return refreshedRows > 0
142
+ }