@open-mercato/core 0.6.7-develop.6795.1.8a3f27921c → 0.6.7-develop.6824.1.21fff90682

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 (228) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/customer_deal_person_link/index.js +2 -0
  3. package/dist/generated/entities/customer_deal_person_link/index.js.map +2 -2
  4. package/dist/generated/entities/customer_user_invitation/index.js +2 -0
  5. package/dist/generated/entities/customer_user_invitation/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +2 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/auth/lib/backendChrome.js +10 -9
  9. package/dist/modules/auth/lib/backendChrome.js.map +2 -2
  10. package/dist/modules/auth/lib/tenantAccess.js +7 -0
  11. package/dist/modules/auth/lib/tenantAccess.js.map +2 -2
  12. package/dist/modules/business_rules/api/openmercato-call-options/route.js +85 -0
  13. package/dist/modules/business_rules/api/openmercato-call-options/route.js.map +7 -0
  14. package/dist/modules/business_rules/api/rules/route.js +71 -4
  15. package/dist/modules/business_rules/api/rules/route.js.map +2 -2
  16. package/dist/modules/business_rules/components/ActionBuilder.js +47 -4
  17. package/dist/modules/business_rules/components/ActionBuilder.js.map +3 -3
  18. package/dist/modules/business_rules/components/ActionRow.js +106 -16
  19. package/dist/modules/business_rules/components/ActionRow.js.map +2 -2
  20. package/dist/modules/business_rules/components/utils/actionValidation.js +6 -0
  21. package/dist/modules/business_rules/components/utils/actionValidation.js.map +2 -2
  22. package/dist/modules/business_rules/lib/action-executor.js +148 -0
  23. package/dist/modules/business_rules/lib/action-executor.js.map +2 -2
  24. package/dist/modules/business_rules/lib/openmercato-call-options-types.js +5 -0
  25. package/dist/modules/business_rules/lib/openmercato-call-options-types.js.map +7 -0
  26. package/dist/modules/business_rules/lib/openmercato-call-options.js +231 -0
  27. package/dist/modules/business_rules/lib/openmercato-call-options.js.map +7 -0
  28. package/dist/modules/business_rules/lib/rule-engine.js +5 -1
  29. package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
  30. package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js +11 -1
  31. package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js.map +2 -2
  32. package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js +11 -6
  33. package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js.map +2 -2
  34. package/dist/modules/configs/lib/upgrade-actions.js +12 -0
  35. package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
  36. package/dist/modules/customer_accounts/api/admin/users/[id].js +10 -1
  37. package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
  38. package/dist/modules/customer_accounts/api/admin/users-invite.js +61 -4
  39. package/dist/modules/customer_accounts/api/admin/users-invite.js.map +2 -2
  40. package/dist/modules/customer_accounts/api/portal/users-invite.js +20 -2
  41. package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
  42. package/dist/modules/customer_accounts/data/entities.js +3 -0
  43. package/dist/modules/customer_accounts/data/entities.js.map +2 -2
  44. package/dist/modules/customer_accounts/data/validators.js +1 -0
  45. package/dist/modules/customer_accounts/data/validators.js.map +2 -2
  46. package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js +26 -0
  47. package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js.map +7 -0
  48. package/dist/modules/customer_accounts/lib/customerEntityOwnership.js +29 -3
  49. package/dist/modules/customer_accounts/lib/customerEntityOwnership.js.map +2 -2
  50. package/dist/modules/customer_accounts/lib/customerUrl.js +19 -10
  51. package/dist/modules/customer_accounts/lib/customerUrl.js.map +2 -2
  52. package/dist/modules/customer_accounts/lib/invitationEmail.js +35 -0
  53. package/dist/modules/customer_accounts/lib/invitationEmail.js.map +7 -0
  54. package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js +13 -0
  55. package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js.map +7 -0
  56. package/dist/modules/customer_accounts/services/customerInvitationService.js +32 -2
  57. package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
  58. package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js +47 -7
  59. package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js.map +2 -2
  60. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -2
  61. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
  62. package/dist/modules/customer_accounts/widgets/injection-table.js +1 -1
  63. package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
  64. package/dist/modules/customers/api/deals/[id]/people/route.js +4 -2
  65. package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
  66. package/dist/modules/customers/api/deals/[id]/route.js +7 -3
  67. package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
  68. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +11 -1
  69. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  70. package/dist/modules/customers/backend/customers/deals/[id]/page.js +14 -8
  71. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  72. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +21 -6
  73. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  74. package/dist/modules/customers/commands/deals.js +48 -10
  75. package/dist/modules/customers/commands/deals.js.map +2 -2
  76. package/dist/modules/customers/commands/people.js +2 -0
  77. package/dist/modules/customers/commands/people.js.map +2 -2
  78. package/dist/modules/customers/components/detail/CompanyDetailTabs.js +26 -6
  79. package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
  80. package/dist/modules/customers/components/detail/DealDetailTabs.js +30 -8
  81. package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
  82. package/dist/modules/customers/components/detail/PersonDetailTabs.js +30 -8
  83. package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
  84. package/dist/modules/customers/data/entities.js +8 -0
  85. package/dist/modules/customers/data/entities.js.map +2 -2
  86. package/dist/modules/customers/data/validators.js +2 -1
  87. package/dist/modules/customers/data/validators.js.map +2 -2
  88. package/dist/modules/customers/migrations/Migration20260723210000_customers.js +15 -0
  89. package/dist/modules/customers/migrations/Migration20260723210000_customers.js.map +7 -0
  90. package/dist/modules/directory/api/tenants/route.js +8 -2
  91. package/dist/modules/directory/api/tenants/route.js.map +2 -2
  92. package/dist/modules/directory/commands/tenants.js +6 -0
  93. package/dist/modules/directory/commands/tenants.js.map +2 -2
  94. package/dist/modules/entities/api/entity-settings.js +59 -9
  95. package/dist/modules/entities/api/entity-settings.js.map +2 -2
  96. package/dist/modules/entities/backend/config/settings/page.js +10 -0
  97. package/dist/modules/entities/backend/config/settings/page.js.map +7 -0
  98. package/dist/modules/entities/backend/config/settings/page.meta.js +36 -0
  99. package/dist/modules/entities/backend/config/settings/page.meta.js.map +7 -0
  100. package/dist/modules/entities/backend/entities/user/create/page.js +16 -7
  101. package/dist/modules/entities/backend/entities/user/create/page.js.map +2 -2
  102. package/dist/modules/entities/components/EntitySettingsManager.js +136 -0
  103. package/dist/modules/entities/components/EntitySettingsManager.js.map +7 -0
  104. package/dist/modules/inbox_ops/api/webhook/inbound.js +14 -10
  105. package/dist/modules/inbox_ops/api/webhook/inbound.js.map +2 -2
  106. package/dist/modules/payment_gateways/api/sessions/route.js +1 -3
  107. package/dist/modules/payment_gateways/api/sessions/route.js.map +2 -2
  108. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +11 -1
  109. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
  110. package/dist/modules/payment_gateways/data/entities.js +4 -0
  111. package/dist/modules/payment_gateways/data/entities.js.map +2 -2
  112. package/dist/modules/payment_gateways/lib/gateway-service.js +15 -6
  113. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  114. package/dist/modules/payment_gateways/lib/session-idempotency.js +33 -0
  115. package/dist/modules/payment_gateways/lib/session-idempotency.js.map +2 -2
  116. package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js +18 -0
  117. package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js.map +7 -0
  118. package/dist/modules/payment_gateways/setup.js +47 -0
  119. package/dist/modules/payment_gateways/setup.js.map +2 -2
  120. package/dist/modules/payment_gateways/workers/session-initialization-prune.js +50 -0
  121. package/dist/modules/payment_gateways/workers/session-initialization-prune.js.map +7 -0
  122. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js +134 -0
  123. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js.map +7 -0
  124. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js +11 -0
  125. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js.map +7 -0
  126. package/dist/modules/portal/lib/navigation.js +7 -0
  127. package/dist/modules/portal/lib/navigation.js.map +7 -0
  128. package/dist/modules/query_index/lib/engine.js +43 -0
  129. package/dist/modules/query_index/lib/engine.js.map +2 -2
  130. package/dist/modules/query_index/lib/search-tokens.js +15 -5
  131. package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
  132. package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js +11 -1
  133. package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js.map +2 -2
  134. package/generated/entities/customer_deal_person_link/index.ts +1 -0
  135. package/generated/entities/customer_user_invitation/index.ts +1 -0
  136. package/generated/entity-fields-registry.ts +2 -0
  137. package/package.json +7 -7
  138. package/src/modules/auth/lib/backendChrome.tsx +15 -9
  139. package/src/modules/auth/lib/tenantAccess.ts +8 -0
  140. package/src/modules/business_rules/api/openmercato-call-options/route.ts +94 -0
  141. package/src/modules/business_rules/api/rules/route.ts +92 -4
  142. package/src/modules/business_rules/components/ActionBuilder.tsx +54 -5
  143. package/src/modules/business_rules/components/ActionRow.tsx +156 -18
  144. package/src/modules/business_rules/components/utils/actionValidation.ts +6 -0
  145. package/src/modules/business_rules/i18n/de.json +12 -0
  146. package/src/modules/business_rules/i18n/en.json +12 -0
  147. package/src/modules/business_rules/i18n/es.json +12 -0
  148. package/src/modules/business_rules/i18n/pl.json +12 -0
  149. package/src/modules/business_rules/lib/action-executor.ts +198 -0
  150. package/src/modules/business_rules/lib/openmercato-call-options-types.ts +31 -0
  151. package/src/modules/business_rules/lib/openmercato-call-options.ts +297 -0
  152. package/src/modules/business_rules/lib/rule-engine.ts +4 -0
  153. package/src/modules/communication_channels/api/post/webhook/[provider]/route.ts +11 -1
  154. package/src/modules/communication_channels/api/post/webhooks/gmail/route.ts +17 -11
  155. package/src/modules/configs/lib/upgrade-actions.ts +14 -0
  156. package/src/modules/customer_accounts/api/admin/users/[id].ts +16 -1
  157. package/src/modules/customer_accounts/api/admin/users-invite.ts +78 -3
  158. package/src/modules/customer_accounts/api/portal/users-invite.ts +22 -1
  159. package/src/modules/customer_accounts/data/entities.ts +3 -0
  160. package/src/modules/customer_accounts/data/validators.ts +1 -0
  161. package/src/modules/customer_accounts/emails/CustomerInvitationEmail.tsx +45 -0
  162. package/src/modules/customer_accounts/i18n/de.json +6 -0
  163. package/src/modules/customer_accounts/i18n/en.json +6 -0
  164. package/src/modules/customer_accounts/i18n/es.json +6 -0
  165. package/src/modules/customer_accounts/i18n/pl.json +6 -0
  166. package/src/modules/customer_accounts/lib/customerEntityOwnership.ts +68 -2
  167. package/src/modules/customer_accounts/lib/customerUrl.ts +27 -11
  168. package/src/modules/customer_accounts/lib/invitationEmail.ts +42 -0
  169. package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +16 -0
  170. package/src/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.ts +13 -0
  171. package/src/modules/customer_accounts/services/customerInvitationService.ts +56 -3
  172. package/src/modules/customer_accounts/subscribers/autoLinkCrm.ts +75 -8
  173. package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +2 -2
  174. package/src/modules/customer_accounts/widgets/injection-table.ts +4 -1
  175. package/src/modules/customers/api/deals/[id]/people/route.ts +3 -0
  176. package/src/modules/customers/api/deals/[id]/route.ts +10 -2
  177. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +12 -1
  178. package/src/modules/customers/backend/customers/deals/[id]/hooks/types.ts +1 -0
  179. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -9
  180. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +24 -6
  181. package/src/modules/customers/commands/deals.ts +50 -9
  182. package/src/modules/customers/commands/people.ts +3 -0
  183. package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +30 -7
  184. package/src/modules/customers/components/detail/DealDetailTabs.tsx +34 -9
  185. package/src/modules/customers/components/detail/PersonDetailTabs.tsx +34 -9
  186. package/src/modules/customers/data/entities.ts +9 -1
  187. package/src/modules/customers/data/validators.ts +1 -0
  188. package/src/modules/customers/i18n/de.json +1 -0
  189. package/src/modules/customers/i18n/en.json +1 -0
  190. package/src/modules/customers/i18n/es.json +1 -0
  191. package/src/modules/customers/i18n/pl.json +1 -0
  192. package/src/modules/customers/migrations/.snapshot-open-mercato.json +27 -1
  193. package/src/modules/customers/migrations/Migration20260723210000_customers.ts +15 -0
  194. package/src/modules/directory/api/tenants/route.ts +9 -1
  195. package/src/modules/directory/commands/tenants.ts +6 -0
  196. package/src/modules/entities/api/entity-settings.ts +66 -22
  197. package/src/modules/entities/backend/config/settings/page.meta.ts +34 -0
  198. package/src/modules/entities/backend/config/settings/page.tsx +12 -0
  199. package/src/modules/entities/backend/entities/user/create/page.tsx +16 -7
  200. package/src/modules/entities/components/EntitySettingsManager.tsx +160 -0
  201. package/src/modules/entities/i18n/de.json +9 -0
  202. package/src/modules/entities/i18n/en.json +9 -0
  203. package/src/modules/entities/i18n/es.json +9 -0
  204. package/src/modules/entities/i18n/pl.json +9 -0
  205. package/src/modules/inbox_ops/api/webhook/inbound.ts +14 -12
  206. package/src/modules/payment_gateways/api/sessions/route.ts +1 -3
  207. package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +13 -1
  208. package/src/modules/payment_gateways/data/entities.ts +5 -0
  209. package/src/modules/payment_gateways/i18n/de.json +5 -1
  210. package/src/modules/payment_gateways/i18n/en.json +5 -1
  211. package/src/modules/payment_gateways/i18n/es.json +5 -1
  212. package/src/modules/payment_gateways/i18n/pl.json +5 -1
  213. package/src/modules/payment_gateways/lib/gateway-service.ts +17 -8
  214. package/src/modules/payment_gateways/lib/session-idempotency.ts +36 -0
  215. package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +10 -1
  216. package/src/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.ts +17 -0
  217. package/src/modules/payment_gateways/setup.ts +62 -0
  218. package/src/modules/payment_gateways/workers/session-initialization-prune.ts +63 -0
  219. package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.ts +9 -0
  220. package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.tsx +164 -0
  221. package/src/modules/portal/i18n/de.json +18 -0
  222. package/src/modules/portal/i18n/en.json +18 -0
  223. package/src/modules/portal/i18n/es.json +18 -0
  224. package/src/modules/portal/i18n/pl.json +18 -0
  225. package/src/modules/portal/lib/navigation.ts +3 -0
  226. package/src/modules/query_index/lib/engine.ts +52 -0
  227. package/src/modules/query_index/lib/search-tokens.ts +16 -4
  228. package/src/modules/shipping_carriers/api/webhook/[provider]/route.ts +11 -1
@@ -0,0 +1,12 @@
1
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
2
+ import { EntitySettingsManager } from '../../../components/EntitySettingsManager'
3
+
4
+ export default function EntitySettingsPage() {
5
+ return (
6
+ <Page>
7
+ <PageBody className="space-y-8">
8
+ <EntitySettingsManager />
9
+ </PageBody>
10
+ </Page>
11
+ )
12
+ }
@@ -10,6 +10,7 @@ import { useRouter } from 'next/navigation'
10
10
  import { pushWithFlash } from '@open-mercato/ui/backend/utils/flash'
11
11
  import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
12
12
  import { LoadingMessage } from '@open-mercato/ui/backend/detail'
13
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
13
14
 
14
15
  const schema = upsertCustomEntitySchema
15
16
 
@@ -24,15 +25,23 @@ export default function CreateEntityPage() {
24
25
  React.useEffect(() => {
25
26
  let active = true
26
27
  ;(async () => {
27
- const data = await readApiResultOrThrow<{ newEntitiesRestrictedByDefault?: boolean }>(
28
- '/api/entities/entity-settings',
29
- undefined,
30
- { errorMessage: '[internal] Failed to load entity settings', fallback: { newEntitiesRestrictedByDefault: false } },
31
- ).catch(() => ({ newEntitiesRestrictedByDefault: false }))
32
- if (active) setDefaultRestricted(data?.newEntitiesRestrictedByDefault === true)
28
+ try {
29
+ const data = await readApiResultOrThrow<{ newEntitiesRestrictedByDefault?: boolean }>(
30
+ '/api/entities/entity-settings',
31
+ undefined,
32
+ {
33
+ errorMessage: t('entities.userEntities.errors.settingsLoadFailed', 'Could not load default restriction policy; using default'),
34
+ fallback: { newEntitiesRestrictedByDefault: false },
35
+ },
36
+ )
37
+ if (active) setDefaultRestricted(data?.newEntitiesRestrictedByDefault === true)
38
+ } catch {
39
+ flash(t('entities.userEntities.errors.settingsLoadFailed', 'Could not load default restriction policy; using default'), 'warning')
40
+ if (active) setDefaultRestricted(false)
41
+ }
33
42
  })()
34
43
  return () => { active = false }
35
- }, [])
44
+ }, [t])
36
45
  const fields = React.useMemo<CrudField[]>(() => ([
37
46
  {
38
47
  id: 'entityId',
@@ -0,0 +1,160 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Button } from '@open-mercato/ui/primitives/button'
5
+ import { Checkbox } from '@open-mercato/ui/primitives/checkbox'
6
+ import { Label } from '@open-mercato/ui/primitives/label'
7
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
8
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
9
+ import { apiCall, apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
10
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
11
+ import { ErrorMessage } from '@open-mercato/ui/backend/detail'
12
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
13
+ import { buildOptimisticLockHeader, extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'
14
+ import { withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
15
+
16
+ export function EntitySettingsManager(): React.ReactElement {
17
+ const t = useT()
18
+ const [loading, setLoading] = React.useState(true)
19
+ const [loadError, setLoadError] = React.useState(false)
20
+ const [saving, setSaving] = React.useState(false)
21
+ const [restricted, setRestricted] = React.useState(false)
22
+ const [updatedAt, setUpdatedAt] = React.useState<string | null>(null)
23
+
24
+ const { runMutation } = useGuardedMutation({
25
+ contextId: 'custom-entities-settings',
26
+ })
27
+
28
+ const loadSettings = React.useCallback(async () => {
29
+ setLoading(true)
30
+ setLoadError(false)
31
+ try {
32
+ const data = await readApiResultOrThrow<{ newEntitiesRestrictedByDefault?: boolean; updatedAt?: string | null }>(
33
+ '/api/entities/entity-settings',
34
+ undefined,
35
+ {
36
+ errorMessage: t('entities.settings.errors.loadFailed', 'Failed to load settings'),
37
+ }
38
+ )
39
+ setRestricted(data.newEntitiesRestrictedByDefault === true)
40
+ setUpdatedAt(data.updatedAt ?? null)
41
+ } catch {
42
+ setLoadError(true)
43
+ } finally {
44
+ setLoading(false)
45
+ }
46
+ }, [t])
47
+
48
+ React.useEffect(() => {
49
+ void loadSettings()
50
+ }, [loadSettings])
51
+
52
+ const handleSubmit = async (e: React.FormEvent) => {
53
+ e.preventDefault()
54
+ setSaving(true)
55
+ try {
56
+ const headers = buildOptimisticLockHeader(updatedAt)
57
+ await runMutation({
58
+ operation: () =>
59
+ withScopedApiRequestHeaders(headers, async () => {
60
+ const res = await apiCallOrThrow<{ ok: boolean; newEntitiesRestrictedByDefault: boolean; updatedAt: string | null }>(
61
+ '/api/entities/entity-settings',
62
+ {
63
+ method: 'PUT',
64
+ headers: { 'content-type': 'application/json' },
65
+ body: JSON.stringify({
66
+ newEntitiesRestrictedByDefault: restricted,
67
+ expectedUpdatedAt: updatedAt ?? undefined,
68
+ }),
69
+ },
70
+ {
71
+ errorMessage: t('entities.settings.errors.saveFailed', 'Failed to save settings'),
72
+ }
73
+ )
74
+
75
+ if (res.ok && res.result) {
76
+ setUpdatedAt(res.result.updatedAt)
77
+ flash(t('entities.settings.flash.saved', 'Settings saved'), 'success')
78
+ }
79
+ return res
80
+ }),
81
+ context: {},
82
+ mutationPayload: {
83
+ newEntitiesRestrictedByDefault: restricted,
84
+ },
85
+ })
86
+ } catch (err) {
87
+ const conflict = extractOptimisticLockConflict(err)
88
+ if (conflict) {
89
+ setLoading(true)
90
+ await loadSettings()
91
+ } else {
92
+ flash(t('entities.settings.errors.saveFailed', 'Failed to save settings'), 'error')
93
+ }
94
+ } finally {
95
+ setSaving(false)
96
+ }
97
+ }
98
+
99
+ if (loading) {
100
+ return (
101
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
102
+ <Spinner className="h-4 w-4" />
103
+ <span>{t('entities.userEntities.form.loading', 'Loading…')}</span>
104
+ </div>
105
+ )
106
+ }
107
+
108
+ if (loadError) {
109
+ return (
110
+ <ErrorMessage
111
+ label={t('entities.settings.errors.loadFailed', 'Failed to load settings')}
112
+ action={(
113
+ <Button type="button" variant="outline" size="sm" onClick={() => void loadSettings()}>
114
+ {t('common.retry', 'Retry')}
115
+ </Button>
116
+ )}
117
+ />
118
+ )
119
+ }
120
+
121
+ return (
122
+ <form onSubmit={(e) => void handleSubmit(e)} className="space-y-6 max-w-2xl">
123
+ <div className="space-y-1">
124
+ <h3 className="text-base font-semibold">
125
+ {t('entities.settings.title', 'Custom Entities')}
126
+ </h3>
127
+ <p className="text-sm text-muted-foreground">
128
+ {t('entities.settings.description', 'Manage global custom entity configurations.')}
129
+ </p>
130
+ </div>
131
+
132
+ <div className="rounded-lg border bg-card p-4 space-y-4">
133
+ <div className="flex items-start gap-3">
134
+ <Checkbox
135
+ id="newEntitiesRestrictedByDefault"
136
+ checked={restricted}
137
+ onCheckedChange={(checked) => setRestricted(checked === true)}
138
+ disabled={saving}
139
+ className="mt-1"
140
+ />
141
+ <div className="space-y-1">
142
+ <Label htmlFor="newEntitiesRestrictedByDefault" className="font-medium cursor-pointer">
143
+ {t('entities.settings.newEntitiesRestrictedByDefault.label', 'Restrict record access by default')}
144
+ </Label>
145
+ <p className="text-sm text-muted-foreground">
146
+ {t('entities.settings.newEntitiesRestrictedByDefault.description', 'Require explicit per-entity permissions for records of newly created custom entities by default.')}
147
+ </p>
148
+ </div>
149
+ </div>
150
+ </div>
151
+
152
+ <div className="flex items-center justify-end gap-3 border-t pt-4">
153
+ <Button type="submit" disabled={saving}>
154
+ {saving ? <Spinner className="mr-2 h-4 w-4" /> : null}
155
+ {t('common.save', 'Save')}
156
+ </Button>
157
+ </div>
158
+ </form>
159
+ )
160
+ }
@@ -196,6 +196,14 @@
196
196
  "entities.nav.systemEntities": "Systementitäten",
197
197
  "entities.nav.userEntities": "Benutzerentitäten",
198
198
  "entities.nav.userEntities.create": "Entität erstellen",
199
+ "entities.settings.description": "Globale Konfigurationen für benutzerdefinierte Entitäten verwalten.",
200
+ "entities.settings.errors.conflict": "Die Einstellungen wurden von einem anderen Benutzer geändert. Bitte neu laden.",
201
+ "entities.settings.errors.loadFailed": "Laden der Einstellungen fehlgeschlagen",
202
+ "entities.settings.errors.saveFailed": "Speichern der Einstellungen fehlgeschlagen",
203
+ "entities.settings.flash.saved": "Einstellungen gespeichert",
204
+ "entities.settings.newEntitiesRestrictedByDefault.description": "Für Datensätze neu erstellter benutzerdefinierter Entitäten standardmäßig explizite Berechtigungen pro Entität erfordern.",
205
+ "entities.settings.newEntitiesRestrictedByDefault.label": "Datensatzzugriff standardmäßig einschränken",
206
+ "entities.settings.title": "Benutzerdefinierte Entitäten",
199
207
  "entities.userEntities.edit.errors.deleteEntityFailed": "Entität konnte nicht gelöscht werden",
200
208
  "entities.userEntities.edit.errors.invalidEntityDescription": "Die angeforderte Entitäts-ID fehlt oder ist ungültig.",
201
209
  "entities.userEntities.edit.errors.invalidEntityId": "Ungültige Entitäts-ID",
@@ -221,6 +229,7 @@
221
229
  "entities.userEntities.errors.entityIdExists": "Entitäts-ID ist bereits vorhanden",
222
230
  "entities.userEntities.errors.entityIdRequired": "Entitäts-ID ist erforderlich",
223
231
  "entities.userEntities.errors.loadFailed": "Entitäten konnten nicht geladen werden",
232
+ "entities.userEntities.errors.settingsLoadFailed": "Standardeinschränkungsrichtlinie konnte nicht geladen werden; Standardwert wird verwendet",
224
233
  "entities.userEntities.flash.created": "Entität erstellt",
225
234
  "entities.userEntities.form.accessRestricted.help": "Erfordert eine explizite entitätsspezifische Berechtigung zum Anzeigen oder Verwalten der Datensätze dieser Entität. Deaktiviert lassen, um allen Benutzern mit der allgemeinen Datensatzberechtigung Zugriff zu gewähren.",
226
235
  "entities.userEntities.form.accessRestricted.label": "Datensatzzugriff einschränken",
@@ -196,6 +196,14 @@
196
196
  "entities.nav.systemEntities": "System Entities",
197
197
  "entities.nav.userEntities": "User Entities",
198
198
  "entities.nav.userEntities.create": "Create Entity",
199
+ "entities.settings.description": "Manage global custom entity configurations.",
200
+ "entities.settings.errors.conflict": "Settings were modified by another user. Please reload.",
201
+ "entities.settings.errors.loadFailed": "Failed to load settings",
202
+ "entities.settings.errors.saveFailed": "Failed to save settings",
203
+ "entities.settings.flash.saved": "Settings saved",
204
+ "entities.settings.newEntitiesRestrictedByDefault.description": "Require explicit per-entity permissions for records of newly created custom entities by default.",
205
+ "entities.settings.newEntitiesRestrictedByDefault.label": "Restrict record access by default",
206
+ "entities.settings.title": "Custom Entities",
199
207
  "entities.userEntities.edit.errors.deleteEntityFailed": "Failed to delete entity",
200
208
  "entities.userEntities.edit.errors.invalidEntityDescription": "The requested entity ID is missing or invalid.",
201
209
  "entities.userEntities.edit.errors.invalidEntityId": "Invalid entity ID",
@@ -221,6 +229,7 @@
221
229
  "entities.userEntities.errors.entityIdExists": "Entity ID already exists",
222
230
  "entities.userEntities.errors.entityIdRequired": "Entity ID is required",
223
231
  "entities.userEntities.errors.loadFailed": "Failed to load entities",
232
+ "entities.userEntities.errors.settingsLoadFailed": "Could not load default restriction policy; using default",
224
233
  "entities.userEntities.flash.created": "Entity created",
225
234
  "entities.userEntities.form.accessRestricted.help": "Require an explicit per-entity permission to view or manage this entity’s records. Leave off to allow anyone with the general records permission.",
226
235
  "entities.userEntities.form.accessRestricted.label": "Restrict record access",
@@ -196,6 +196,14 @@
196
196
  "entities.nav.systemEntities": "Entidades del sistema",
197
197
  "entities.nav.userEntities": "Entidades de usuario",
198
198
  "entities.nav.userEntities.create": "Crear entidad",
199
+ "entities.settings.description": "Administrar configuraciones globales de entidades personalizadas.",
200
+ "entities.settings.errors.conflict": "La configuración fue modificada por otro usuario. Por favor recarga.",
201
+ "entities.settings.errors.loadFailed": "Error al cargar la configuración",
202
+ "entities.settings.errors.saveFailed": "Error al guardar la configuración",
203
+ "entities.settings.flash.saved": "Configuración guardada",
204
+ "entities.settings.newEntitiesRestrictedByDefault.description": "Requerir permisos explícitos por entidad para registros de nuevas entidades creadas por defecto.",
205
+ "entities.settings.newEntitiesRestrictedByDefault.label": "Restringir acceso a registros por defecto",
206
+ "entities.settings.title": "Entidades personalizadas",
199
207
  "entities.userEntities.edit.errors.deleteEntityFailed": "No se pudo eliminar la entidad",
200
208
  "entities.userEntities.edit.errors.invalidEntityDescription": "Falta el ID de la entidad solicitada o no es válido.",
201
209
  "entities.userEntities.edit.errors.invalidEntityId": "ID de entidad no válido",
@@ -221,6 +229,7 @@
221
229
  "entities.userEntities.errors.entityIdExists": "El ID de la entidad ya existe",
222
230
  "entities.userEntities.errors.entityIdRequired": "El ID de la entidad es obligatorio",
223
231
  "entities.userEntities.errors.loadFailed": "No se pudieron cargar las entidades",
232
+ "entities.userEntities.errors.settingsLoadFailed": "No se pudo cargar la política de restricción por defecto; usando valor predeterminado",
224
233
  "entities.userEntities.flash.created": "Entidad creada",
225
234
  "entities.userEntities.form.accessRestricted.help": "Requiere un permiso explícito por entidad para ver o gestionar los registros de esta entidad. Déjalo desactivado para permitir el acceso a cualquiera con el permiso general de registros.",
226
235
  "entities.userEntities.form.accessRestricted.label": "Restringir acceso a registros",
@@ -196,6 +196,14 @@
196
196
  "entities.nav.systemEntities": "Encje systemowe",
197
197
  "entities.nav.userEntities": "Encje użytkownika",
198
198
  "entities.nav.userEntities.create": "Utwórz encję",
199
+ "entities.settings.description": "Zarządzaj globalnymi ustawieniami niestandardowych encji.",
200
+ "entities.settings.errors.conflict": "Ustawienia zostały zmodyfikowane przez innego użytkownika. Proszę przeładować.",
201
+ "entities.settings.errors.loadFailed": "Nie udało się załadować ustawień",
202
+ "entities.settings.errors.saveFailed": "Nie udało się zapisać ustawień",
203
+ "entities.settings.flash.saved": "Ustawienia zostały zapisane",
204
+ "entities.settings.newEntitiesRestrictedByDefault.description": "Domyślnie wymagaj jawnych uprawnień dla poszczególnych encji do rekordów nowo tworzonych niestandardowych encji.",
205
+ "entities.settings.newEntitiesRestrictedByDefault.label": "Domyślnie ograniczaj dostęp do rekordów",
206
+ "entities.settings.title": "Niestandardowe encje",
199
207
  "entities.userEntities.edit.errors.deleteEntityFailed": "Nie udało się usunąć encji",
200
208
  "entities.userEntities.edit.errors.invalidEntityDescription": "Brakuje identyfikatora encji albo jest on nieprawidłowy.",
201
209
  "entities.userEntities.edit.errors.invalidEntityId": "Nieprawidłowy identyfikator encji",
@@ -221,6 +229,7 @@
221
229
  "entities.userEntities.errors.entityIdExists": "Identyfikator encji już istnieje",
222
230
  "entities.userEntities.errors.entityIdRequired": "Identyfikator encji jest wymagany",
223
231
  "entities.userEntities.errors.loadFailed": "Nie udało się wczytać encji",
232
+ "entities.userEntities.errors.settingsLoadFailed": "Nie udało się załadować domyślnej polityki ograniczeń; używanie wartości domyślnej",
224
233
  "entities.userEntities.flash.created": "Encja została utworzona",
225
234
  "entities.userEntities.form.accessRestricted.help": "Wymaga jawnego uprawnienia dla tej encji, aby przeglądać lub zarządzać jej rekordami. Pozostaw wyłączone, aby zezwolić na dostęp każdemu z ogólnym uprawnieniem do rekordów.",
226
235
  "entities.userEntities.form.accessRestricted.label": "Ogranicz dostęp do rekordów",
@@ -13,6 +13,11 @@ import { parseInboundEmail } from '../../lib/emailParser'
13
13
  import { checkRateLimit } from '../../lib/rateLimiter'
14
14
  import { emitInboxOpsEvent } from '../../events'
15
15
  import { createLogger } from '@open-mercato/shared/lib/logger'
16
+ import {
17
+ readBoundedRequestBody,
18
+ resolveWebhookBodyLimitBytes,
19
+ WebhookBodyTooLargeError,
20
+ } from '@open-mercato/shared/lib/webhooks'
16
21
 
17
22
  const logger = createLogger('inbox_ops').child({ component: 'webhook' })
18
23
 
@@ -20,7 +25,10 @@ export const metadata = {
20
25
  POST: { requireAuth: false },
21
26
  }
22
27
 
23
- const MAX_PAYLOAD_SIZE = 2 * 1024 * 1024 // 2MB
28
+ const MAX_PAYLOAD_SIZE = resolveWebhookBodyLimitBytes(
29
+ process.env.INBOX_OPS_WEBHOOK_MAX_BODY_BYTES ?? process.env.OM_WEBHOOK_MAX_BODY_BYTES,
30
+ 2 * 1024 * 1024,
31
+ )
24
32
  const REPLAY_WINDOW_MS = 5 * 60 * 1000 // 5 minutes
25
33
 
26
34
  function isTimestampFresh(timestamp: string): boolean {
@@ -211,22 +219,16 @@ export async function POST(req: Request) {
211
219
  return NextResponse.json({ error: 'Service unavailable' }, { status: 503 })
212
220
  }
213
221
 
214
- const contentLength = parseInt(req.headers.get('content-length') || '0', 10)
215
- if (contentLength > MAX_PAYLOAD_SIZE) {
216
- return NextResponse.json({ error: 'Payload too large' }, { status: 413 })
217
- }
218
-
219
222
  let rawBody: string
220
223
  try {
221
- rawBody = await req.text()
222
- } catch {
224
+ rawBody = await readBoundedRequestBody(req, { maxBytes: MAX_PAYLOAD_SIZE })
225
+ } catch (error) {
226
+ if (error instanceof WebhookBodyTooLargeError) {
227
+ return NextResponse.json({ error: 'Payload too large' }, { status: 413 })
228
+ }
223
229
  return NextResponse.json({ error: 'Bad request' }, { status: 400 })
224
230
  }
225
231
 
226
- if (rawBody.length > MAX_PAYLOAD_SIZE) {
227
- return NextResponse.json({ error: 'Payload too large' }, { status: 413 })
228
- }
229
-
230
232
  const verified = await verifyAndParse(req, rawBody)
231
233
  if (!verified.ok) {
232
234
  return verified.response
@@ -102,9 +102,7 @@ export async function POST(req: Request) {
102
102
  if (isCrudHttpError(err)) {
103
103
  return NextResponse.json(err.body, { status: err.status })
104
104
  }
105
- const message = err instanceof Error ? err.message : 'Failed to create payment session'
106
- const status = message.includes('No gateway adapter') ? 422 : 502
107
- return NextResponse.json({ error: message }, { status })
105
+ return NextResponse.json({ error: 'Failed to create payment session' }, { status: 502 })
108
106
  }
109
107
  }
110
108
 
@@ -14,6 +14,7 @@ import { getPaymentGatewayQueue } from '../../../lib/queue'
14
14
  import { processPaymentGatewayWebhookJob } from '../../../lib/webhook-processor'
15
15
  import { paymentGatewaysTag } from '../../openapi'
16
16
  import { createLogger } from '@open-mercato/shared/lib/logger'
17
+ import { readBoundedRequestBody, WebhookBodyTooLargeError } from '@open-mercato/shared/lib/webhooks'
17
18
 
18
19
  const logger = createLogger('payment_gateways').child({ component: 'webhook' })
19
20
 
@@ -42,7 +43,17 @@ export async function POST(req: Request, { params }: { params: Promise<{ provide
42
43
  const rateLimitResponse = await checkProviderWebhookRateLimit(container, req, providerKey)
43
44
  if (rateLimitResponse) return rateLimitResponse
44
45
 
45
- const rawBody = await req.text()
46
+ let rawBody: string
47
+ try {
48
+ rawBody = registration.maxBodyBytes === undefined
49
+ ? await req.text()
50
+ : await readBoundedRequestBody(req, { maxBytes: registration.maxBodyBytes })
51
+ } catch (error) {
52
+ if (error instanceof WebhookBodyTooLargeError) {
53
+ return NextResponse.json({ error: 'Webhook payload too large' }, { status: 413 })
54
+ }
55
+ throw error
56
+ }
46
57
  const headers: Record<string, string> = {}
47
58
  req.headers.forEach((value, key) => {
48
59
  headers[key] = value
@@ -164,6 +175,7 @@ export const openApi = {
164
175
  responses: [
165
176
  { status: 202, description: 'Webhook accepted for async processing' },
166
177
  { status: 401, description: 'Signature verification failed' },
178
+ { status: 413, description: 'Webhook payload too large' },
167
179
  { status: 404, description: 'Unknown provider' },
168
180
  ],
169
181
  },
@@ -145,6 +145,11 @@ export class GatewayPaymentOperation {
145
145
  name: 'gateway_session_initializations_scope_operation_unique',
146
146
  properties: ['operationKey', 'providerKey', 'organizationId', 'tenantId'],
147
147
  })
148
+ @Index({
149
+ name: 'gateway_session_initializations_prune_idx',
150
+ expression:
151
+ 'create index "gateway_session_initializations_prune_idx" on "gateway_session_initializations" ("tenant_id", "organization_id", "updated_at") where "gateway_transaction_id" is not null',
152
+ })
148
153
  export class GatewaySessionInitialization {
149
154
  [OptionalProps]?: 'claimToken' | 'claimedAt' | 'gatewayTransactionId' | 'createdAt' | 'updatedAt'
150
155
 
@@ -79,5 +79,9 @@
79
79
  "payment_gateways.transactions.success.capture": "Payment erfasst",
80
80
  "payment_gateways.transactions.success.refreshStatus": "Der Transaktionsstatus wurde aktualisiert",
81
81
  "payment_gateways.transactions.tableTitle": "Transaktionen",
82
- "payment_gateways.transactions.title": "Zahlungstransaktionen"
82
+ "payment_gateways.transactions.title": "Zahlungstransaktionen",
83
+ "payment_gateways.upgradeActions.sessionInitializationPrune.cta": "Bereinigung der Ansprüche planen",
84
+ "payment_gateways.upgradeActions.sessionInitializationPrune.loading": "Bereinigung der Ansprüche wird geplant…",
85
+ "payment_gateways.upgradeActions.sessionInitializationPrune.message": "Registrieren Sie den täglichen Bereinigungsplan, der abgeschlossene Zahlungssitzungsansprüche nach ihrem 24-stündigen Wiederholungsfenster entfernt.",
86
+ "payment_gateways.upgradeActions.sessionInitializationPrune.success": "Die Bereinigung der Zahlungssitzungsansprüche ist geplant."
83
87
  }
@@ -79,5 +79,9 @@
79
79
  "payment_gateways.transactions.success.capture": "Payment captured",
80
80
  "payment_gateways.transactions.success.refreshStatus": "Transaction status refreshed",
81
81
  "payment_gateways.transactions.tableTitle": "Transactions",
82
- "payment_gateways.transactions.title": "Payment Transactions"
82
+ "payment_gateways.transactions.title": "Payment Transactions",
83
+ "payment_gateways.upgradeActions.sessionInitializationPrune.cta": "Schedule claim cleanup",
84
+ "payment_gateways.upgradeActions.sessionInitializationPrune.loading": "Scheduling claim cleanup…",
85
+ "payment_gateways.upgradeActions.sessionInitializationPrune.message": "Register the daily cleanup schedule that removes completed payment-session claims after their 24-hour replay window.",
86
+ "payment_gateways.upgradeActions.sessionInitializationPrune.success": "Payment-session claim cleanup is scheduled."
83
87
  }
@@ -79,5 +79,9 @@
79
79
  "payment_gateways.transactions.success.capture": "Pago capturado",
80
80
  "payment_gateways.transactions.success.refreshStatus": "El estado de la transaccion se actualizo",
81
81
  "payment_gateways.transactions.tableTitle": "Transacciones",
82
- "payment_gateways.transactions.title": "Transacciones de pago"
82
+ "payment_gateways.transactions.title": "Transacciones de pago",
83
+ "payment_gateways.upgradeActions.sessionInitializationPrune.cta": "Programar limpieza de reclamaciones",
84
+ "payment_gateways.upgradeActions.sessionInitializationPrune.loading": "Programando limpieza de reclamaciones…",
85
+ "payment_gateways.upgradeActions.sessionInitializationPrune.message": "Registra la limpieza diaria que elimina las reclamaciones de sesiones de pago completadas después de su ventana de repetición de 24 horas.",
86
+ "payment_gateways.upgradeActions.sessionInitializationPrune.success": "La limpieza de reclamaciones de sesiones de pago está programada."
83
87
  }
@@ -79,5 +79,9 @@
79
79
  "payment_gateways.transactions.success.capture": "Płatność została przechwycona",
80
80
  "payment_gateways.transactions.success.refreshStatus": "Status transakcji odświeżony",
81
81
  "payment_gateways.transactions.tableTitle": "Transakcje",
82
- "payment_gateways.transactions.title": "Transakcje płatności"
82
+ "payment_gateways.transactions.title": "Transakcje płatności",
83
+ "payment_gateways.upgradeActions.sessionInitializationPrune.cta": "Zaplanuj czyszczenie roszczeń",
84
+ "payment_gateways.upgradeActions.sessionInitializationPrune.loading": "Planowanie czyszczenia roszczeń…",
85
+ "payment_gateways.upgradeActions.sessionInitializationPrune.message": "Zarejestruj codzienny harmonogram usuwający zakończone roszczenia inicjalizacji sesji płatności po 24-godzinnym oknie ponowienia.",
86
+ "payment_gateways.upgradeActions.sessionInitializationPrune.success": "Czyszczenie roszczeń inicjalizacji sesji płatności zostało zaplanowane."
83
87
  }
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto'
2
2
  import type { EntityManager } from '@mikro-orm/postgresql'
3
3
  import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
4
+ import { createLogger } from '@open-mercato/shared/lib/logger'
4
5
  import {
5
6
  getGatewayAdapter,
6
7
  type CreateSessionInput,
@@ -17,7 +18,7 @@ import {
17
18
  import type { CredentialsService } from '../../integrations/lib/credentials-service'
18
19
  import type { IntegrationStateService } from '../../integrations/lib/state-service'
19
20
  import type { IntegrationLogService } from '../../integrations/lib/log-service'
20
- import { conflict } from '@open-mercato/shared/lib/crud/errors'
21
+ import { conflict, CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
21
22
  import { GatewayPaymentOperation, GatewaySessionInitialization, GatewayTransaction } from '../data/entities'
22
23
  import { canApplyManualAction, isValidTransition, type ManualGatewayAction } from './status-machine'
23
24
  import { emitPaymentGatewayEvent } from '../events'
@@ -48,6 +49,7 @@ import {
48
49
 
49
50
  const PAYMENT_SESSION_CLAIM_STALE_MS = 30_000
50
51
  const PAYMENT_SESSION_WAIT_INTERVAL_MS = 25
52
+ const logger = createLogger('payment_gateways').child({ component: 'gateway-service' })
51
53
 
52
54
  function assertManualActionAllowed(action: ManualGatewayAction, transaction: GatewayTransaction): void {
53
55
  const current = transaction.unifiedStatus as UnifiedPaymentStatus
@@ -189,11 +191,10 @@ export function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {
189
191
  : undefined
190
192
  const adapter = getGatewayAdapter(providerKey, selectedVersion)
191
193
  if (!adapter) {
192
- throw new Error(
193
- selectedVersion
194
- ? `No gateway adapter registered for provider: ${providerKey} (version: ${selectedVersion})`
195
- : `No gateway adapter registered for provider: ${providerKey}`,
196
- )
194
+ const message = selectedVersion
195
+ ? `No gateway adapter registered for provider: ${providerKey} (version: ${selectedVersion})`
196
+ : `No gateway adapter registered for provider: ${providerKey}`
197
+ throw new CrudHttpError(422, { error: message })
197
198
  }
198
199
  const credentials = await integrationCredentialsService.resolve(integrationId, scope) ?? {}
199
200
 
@@ -469,7 +470,7 @@ export function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {
469
470
  scope,
470
471
  )
471
472
  if (!transaction) {
472
- throw new Error('Completed payment session is missing its gateway transaction')
473
+ throw new Error('[internal] Completed payment session is missing its gateway transaction')
473
474
  }
474
475
  return { transaction, session: restoreSession(transaction) }
475
476
  }
@@ -490,7 +491,15 @@ export function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {
490
491
  try {
491
492
  session = await adapter.createSession({ ...sessionInput, idempotencyKey: operationKey })
492
493
  } catch (error) {
493
- await releasePaymentSessionInitialization(em, ownership, scope).catch(() => undefined)
494
+ try {
495
+ await releasePaymentSessionInitialization(em, ownership, scope)
496
+ } catch (releaseError) {
497
+ logger.warn('Failed to release payment session initialization claim', {
498
+ claimId: ownership.id,
499
+ providerKey: input.providerKey,
500
+ err: releaseError,
501
+ })
502
+ }
494
503
  throw error
495
504
  } finally {
496
505
  await stopHeartbeat()
@@ -6,6 +6,11 @@ import { GatewaySessionInitialization } from '../data/entities'
6
6
 
7
7
  type Scope = { organizationId: string; tenantId: string }
8
8
 
9
+ export const PAYMENT_SESSION_REPLAY_WINDOW_MS = 24 * 60 * 60 * 1000
10
+ export const PAYMENT_SESSION_INITIALIZATION_PRUNE_QUEUE =
11
+ 'payment-gateway-session-initialization-prune'
12
+ export const PAYMENT_SESSION_INITIALIZATION_PRUNE_BATCH_SIZE = 1_000
13
+
9
14
  export type OwnedPaymentSessionInitialization = {
10
15
  id: string
11
16
  claimToken: string
@@ -140,3 +145,34 @@ export async function refreshPaymentSessionInitialization(
140
145
  )
141
146
  return refreshedRows > 0
142
147
  }
148
+
149
+ export async function pruneCompletedPaymentSessionInitializations(
150
+ em: EntityManager,
151
+ scope: Scope,
152
+ options: { now?: Date; batchSize?: number } = {},
153
+ ): Promise<number> {
154
+ const now = options.now ?? new Date()
155
+ const batchSize = options.batchSize ?? PAYMENT_SESSION_INITIALIZATION_PRUNE_BATCH_SIZE
156
+ if (!Number.isInteger(batchSize) || batchSize < 1) {
157
+ throw new Error('[internal] Payment-session initialization prune batch size must be a positive integer')
158
+ }
159
+ const cutoff = new Date(now.getTime() - PAYMENT_SESSION_REPLAY_WINDOW_MS)
160
+ const connection = em.getConnection()
161
+ const result = await connection.execute(
162
+ `
163
+ delete from gateway_session_initializations
164
+ where id in (
165
+ select id from gateway_session_initializations
166
+ where tenant_id = ?
167
+ and organization_id = ?
168
+ and gateway_transaction_id is not null
169
+ and updated_at < ?
170
+ order by updated_at asc
171
+ limit ?
172
+ )
173
+ `,
174
+ [scope.tenantId, scope.organizationId, cutoff, batchSize],
175
+ 'run',
176
+ ) as { affectedRows?: number; rowCount?: number } | undefined
177
+ return result?.affectedRows ?? result?.rowCount ?? 0
178
+ }
@@ -537,6 +537,15 @@
537
537
  "primary": true,
538
538
  "unique": true
539
539
  },
540
+ {
541
+ "columnNames": [],
542
+ "composite": false,
543
+ "constraint": false,
544
+ "keyName": "gateway_session_initializations_prune_idx",
545
+ "primary": false,
546
+ "unique": false,
547
+ "expression": "create index \"gateway_session_initializations_prune_idx\" on \"gateway_session_initializations\" (\"tenant_id\", \"organization_id\", \"updated_at\") where \"gateway_transaction_id\" is not null"
548
+ },
540
549
  {
541
550
  "columnNames": [
542
551
  "operation_key",
@@ -1162,4 +1171,4 @@
1162
1171
  ],
1163
1172
  "views": [],
1164
1173
  "nativeEnums": {}
1165
- }
1174
+ }
@@ -0,0 +1,17 @@
1
+ import { Migration } from '@mikro-orm/migrations'
2
+
3
+ export class Migration20260803103035_payment_gateways extends Migration {
4
+ override isTransactional(): boolean {
5
+ return false
6
+ }
7
+
8
+ override up(): void | Promise<void> {
9
+ this.addSql(
10
+ 'create index concurrently if not exists "gateway_session_initializations_prune_idx" on "gateway_session_initializations" ("tenant_id", "organization_id", "updated_at") where "gateway_transaction_id" is not null;',
11
+ )
12
+ }
13
+
14
+ override down(): void | Promise<void> {
15
+ this.addSql('drop index concurrently if exists "gateway_session_initializations_prune_idx";')
16
+ }
17
+ }