@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/customer_deal_person_link/index.js +2 -0
- package/dist/generated/entities/customer_deal_person_link/index.js.map +2 -2
- package/dist/generated/entities/customer_user_invitation/index.js +2 -0
- package/dist/generated/entities/customer_user_invitation/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/auth/lib/backendChrome.js +10 -9
- package/dist/modules/auth/lib/backendChrome.js.map +2 -2
- package/dist/modules/auth/lib/tenantAccess.js +7 -0
- package/dist/modules/auth/lib/tenantAccess.js.map +2 -2
- package/dist/modules/business_rules/api/openmercato-call-options/route.js +85 -0
- package/dist/modules/business_rules/api/openmercato-call-options/route.js.map +7 -0
- package/dist/modules/business_rules/api/rules/route.js +71 -4
- package/dist/modules/business_rules/api/rules/route.js.map +2 -2
- package/dist/modules/business_rules/components/ActionBuilder.js +47 -4
- package/dist/modules/business_rules/components/ActionBuilder.js.map +3 -3
- package/dist/modules/business_rules/components/ActionRow.js +106 -16
- package/dist/modules/business_rules/components/ActionRow.js.map +2 -2
- package/dist/modules/business_rules/components/utils/actionValidation.js +6 -0
- package/dist/modules/business_rules/components/utils/actionValidation.js.map +2 -2
- package/dist/modules/business_rules/lib/action-executor.js +148 -0
- package/dist/modules/business_rules/lib/action-executor.js.map +2 -2
- package/dist/modules/business_rules/lib/openmercato-call-options-types.js +5 -0
- package/dist/modules/business_rules/lib/openmercato-call-options-types.js.map +7 -0
- package/dist/modules/business_rules/lib/openmercato-call-options.js +231 -0
- package/dist/modules/business_rules/lib/openmercato-call-options.js.map +7 -0
- package/dist/modules/business_rules/lib/rule-engine.js +5 -1
- package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
- package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js +11 -1
- package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js +11 -6
- package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js.map +2 -2
- package/dist/modules/configs/lib/upgrade-actions.js +12 -0
- package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users/[id].js +10 -1
- package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users-invite.js +61 -4
- package/dist/modules/customer_accounts/api/admin/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/users-invite.js +20 -2
- package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/data/entities.js +3 -0
- package/dist/modules/customer_accounts/data/entities.js.map +2 -2
- package/dist/modules/customer_accounts/data/validators.js +1 -0
- package/dist/modules/customer_accounts/data/validators.js.map +2 -2
- package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js +26 -0
- package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js.map +7 -0
- package/dist/modules/customer_accounts/lib/customerEntityOwnership.js +29 -3
- package/dist/modules/customer_accounts/lib/customerEntityOwnership.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerUrl.js +19 -10
- package/dist/modules/customer_accounts/lib/customerUrl.js.map +2 -2
- package/dist/modules/customer_accounts/lib/invitationEmail.js +35 -0
- package/dist/modules/customer_accounts/lib/invitationEmail.js.map +7 -0
- package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js +13 -0
- package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js.map +7 -0
- package/dist/modules/customer_accounts/services/customerInvitationService.js +32 -2
- package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
- package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js +47 -7
- package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection-table.js +1 -1
- package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/people/route.js +4 -2
- package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/route.js +7 -3
- package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +11 -1
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +14 -8
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +21 -6
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/commands/deals.js +48 -10
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/commands/people.js +2 -0
- package/dist/modules/customers/commands/people.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js +26 -6
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/DealDetailTabs.js +30 -8
- package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonDetailTabs.js +30 -8
- package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
- package/dist/modules/customers/data/entities.js +8 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/validators.js +2 -1
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260723210000_customers.js +15 -0
- package/dist/modules/customers/migrations/Migration20260723210000_customers.js.map +7 -0
- package/dist/modules/directory/api/tenants/route.js +8 -2
- package/dist/modules/directory/api/tenants/route.js.map +2 -2
- package/dist/modules/directory/commands/tenants.js +6 -0
- package/dist/modules/directory/commands/tenants.js.map +2 -2
- package/dist/modules/entities/api/entity-settings.js +59 -9
- package/dist/modules/entities/api/entity-settings.js.map +2 -2
- package/dist/modules/entities/backend/config/settings/page.js +10 -0
- package/dist/modules/entities/backend/config/settings/page.js.map +7 -0
- package/dist/modules/entities/backend/config/settings/page.meta.js +36 -0
- package/dist/modules/entities/backend/config/settings/page.meta.js.map +7 -0
- package/dist/modules/entities/backend/entities/user/create/page.js +16 -7
- package/dist/modules/entities/backend/entities/user/create/page.js.map +2 -2
- package/dist/modules/entities/components/EntitySettingsManager.js +136 -0
- package/dist/modules/entities/components/EntitySettingsManager.js.map +7 -0
- package/dist/modules/inbox_ops/api/webhook/inbound.js +14 -10
- package/dist/modules/inbox_ops/api/webhook/inbound.js.map +2 -2
- package/dist/modules/payment_gateways/api/sessions/route.js +1 -3
- package/dist/modules/payment_gateways/api/sessions/route.js.map +2 -2
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +11 -1
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/payment_gateways/data/entities.js +4 -0
- package/dist/modules/payment_gateways/data/entities.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +15 -6
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/session-idempotency.js +33 -0
- package/dist/modules/payment_gateways/lib/session-idempotency.js.map +2 -2
- package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js +18 -0
- package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js.map +7 -0
- package/dist/modules/payment_gateways/setup.js +47 -0
- package/dist/modules/payment_gateways/setup.js.map +2 -2
- package/dist/modules/payment_gateways/workers/session-initialization-prune.js +50 -0
- package/dist/modules/payment_gateways/workers/session-initialization-prune.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js +134 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js +11 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js.map +7 -0
- package/dist/modules/portal/lib/navigation.js +7 -0
- package/dist/modules/portal/lib/navigation.js.map +7 -0
- package/dist/modules/query_index/lib/engine.js +43 -0
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/query_index/lib/search-tokens.js +15 -5
- package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js +11 -1
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js.map +2 -2
- package/generated/entities/customer_deal_person_link/index.ts +1 -0
- package/generated/entities/customer_user_invitation/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +7 -7
- package/src/modules/auth/lib/backendChrome.tsx +15 -9
- package/src/modules/auth/lib/tenantAccess.ts +8 -0
- package/src/modules/business_rules/api/openmercato-call-options/route.ts +94 -0
- package/src/modules/business_rules/api/rules/route.ts +92 -4
- package/src/modules/business_rules/components/ActionBuilder.tsx +54 -5
- package/src/modules/business_rules/components/ActionRow.tsx +156 -18
- package/src/modules/business_rules/components/utils/actionValidation.ts +6 -0
- package/src/modules/business_rules/i18n/de.json +12 -0
- package/src/modules/business_rules/i18n/en.json +12 -0
- package/src/modules/business_rules/i18n/es.json +12 -0
- package/src/modules/business_rules/i18n/pl.json +12 -0
- package/src/modules/business_rules/lib/action-executor.ts +198 -0
- package/src/modules/business_rules/lib/openmercato-call-options-types.ts +31 -0
- package/src/modules/business_rules/lib/openmercato-call-options.ts +297 -0
- package/src/modules/business_rules/lib/rule-engine.ts +4 -0
- package/src/modules/communication_channels/api/post/webhook/[provider]/route.ts +11 -1
- package/src/modules/communication_channels/api/post/webhooks/gmail/route.ts +17 -11
- package/src/modules/configs/lib/upgrade-actions.ts +14 -0
- package/src/modules/customer_accounts/api/admin/users/[id].ts +16 -1
- package/src/modules/customer_accounts/api/admin/users-invite.ts +78 -3
- package/src/modules/customer_accounts/api/portal/users-invite.ts +22 -1
- package/src/modules/customer_accounts/data/entities.ts +3 -0
- package/src/modules/customer_accounts/data/validators.ts +1 -0
- package/src/modules/customer_accounts/emails/CustomerInvitationEmail.tsx +45 -0
- package/src/modules/customer_accounts/i18n/de.json +6 -0
- package/src/modules/customer_accounts/i18n/en.json +6 -0
- package/src/modules/customer_accounts/i18n/es.json +6 -0
- package/src/modules/customer_accounts/i18n/pl.json +6 -0
- package/src/modules/customer_accounts/lib/customerEntityOwnership.ts +68 -2
- package/src/modules/customer_accounts/lib/customerUrl.ts +27 -11
- package/src/modules/customer_accounts/lib/invitationEmail.ts +42 -0
- package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +16 -0
- package/src/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.ts +13 -0
- package/src/modules/customer_accounts/services/customerInvitationService.ts +56 -3
- package/src/modules/customer_accounts/subscribers/autoLinkCrm.ts +75 -8
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +2 -2
- package/src/modules/customer_accounts/widgets/injection-table.ts +4 -1
- package/src/modules/customers/api/deals/[id]/people/route.ts +3 -0
- package/src/modules/customers/api/deals/[id]/route.ts +10 -2
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +12 -1
- package/src/modules/customers/backend/customers/deals/[id]/hooks/types.ts +1 -0
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -9
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +24 -6
- package/src/modules/customers/commands/deals.ts +50 -9
- package/src/modules/customers/commands/people.ts +3 -0
- package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +30 -7
- package/src/modules/customers/components/detail/DealDetailTabs.tsx +34 -9
- package/src/modules/customers/components/detail/PersonDetailTabs.tsx +34 -9
- package/src/modules/customers/data/entities.ts +9 -1
- package/src/modules/customers/data/validators.ts +1 -0
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/customers/migrations/.snapshot-open-mercato.json +27 -1
- package/src/modules/customers/migrations/Migration20260723210000_customers.ts +15 -0
- package/src/modules/directory/api/tenants/route.ts +9 -1
- package/src/modules/directory/commands/tenants.ts +6 -0
- package/src/modules/entities/api/entity-settings.ts +66 -22
- package/src/modules/entities/backend/config/settings/page.meta.ts +34 -0
- package/src/modules/entities/backend/config/settings/page.tsx +12 -0
- package/src/modules/entities/backend/entities/user/create/page.tsx +16 -7
- package/src/modules/entities/components/EntitySettingsManager.tsx +160 -0
- package/src/modules/entities/i18n/de.json +9 -0
- package/src/modules/entities/i18n/en.json +9 -0
- package/src/modules/entities/i18n/es.json +9 -0
- package/src/modules/entities/i18n/pl.json +9 -0
- package/src/modules/inbox_ops/api/webhook/inbound.ts +14 -12
- package/src/modules/payment_gateways/api/sessions/route.ts +1 -3
- package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +13 -1
- package/src/modules/payment_gateways/data/entities.ts +5 -0
- package/src/modules/payment_gateways/i18n/de.json +5 -1
- package/src/modules/payment_gateways/i18n/en.json +5 -1
- package/src/modules/payment_gateways/i18n/es.json +5 -1
- package/src/modules/payment_gateways/i18n/pl.json +5 -1
- package/src/modules/payment_gateways/lib/gateway-service.ts +17 -8
- package/src/modules/payment_gateways/lib/session-idempotency.ts +36 -0
- package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +10 -1
- package/src/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.ts +17 -0
- package/src/modules/payment_gateways/setup.ts +62 -0
- package/src/modules/payment_gateways/workers/session-initialization-prune.ts +63 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.ts +9 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.tsx +164 -0
- package/src/modules/portal/i18n/de.json +18 -0
- package/src/modules/portal/i18n/en.json +18 -0
- package/src/modules/portal/i18n/es.json +18 -0
- package/src/modules/portal/i18n/pl.json +18 -0
- package/src/modules/portal/lib/navigation.ts +3 -0
- package/src/modules/query_index/lib/engine.ts +52 -0
- package/src/modules/query_index/lib/search-tokens.ts +16 -4
- 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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
selectedVersion
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
+
}
|