@open-mercato/core 0.10.1-develop.7736.1.88e07906bf → 0.10.1-develop.7748.1.d745ba61aa
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/api_key/index.js +2 -0
- package/dist/generated/entities/api_key/index.js.map +2 -2
- package/dist/generated/entities/attachment_partition/index.js +2 -0
- package/dist/generated/entities/attachment_partition/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/helpers/integration/attachmentAccessFixtures.js +144 -0
- package/dist/helpers/integration/attachmentAccessFixtures.js.map +7 -0
- package/dist/modules/api_keys/data/entities.js +7 -0
- package/dist/modules/api_keys/data/entities.js.map +2 -2
- package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js +30 -0
- package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js.map +7 -0
- package/dist/modules/api_keys/services/apiKeyService.js +31 -9
- package/dist/modules/api_keys/services/apiKeyService.js.map +2 -2
- package/dist/modules/attachments/api/file/[id]/route.js +16 -10
- package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +15 -9
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/attachments/api/library/[id]/route.js +100 -56
- package/dist/modules/attachments/api/library/[id]/route.js.map +3 -3
- package/dist/modules/attachments/api/library/route.js +56 -17
- package/dist/modules/attachments/api/library/route.js.map +2 -2
- package/dist/modules/attachments/api/partitions/route.js +2 -0
- package/dist/modules/attachments/api/partitions/route.js.map +2 -2
- package/dist/modules/attachments/api/route.js +74 -34
- package/dist/modules/attachments/api/route.js.map +3 -3
- package/dist/modules/attachments/api/transfer/route.js +63 -37
- package/dist/modules/attachments/api/transfer/route.js.map +2 -2
- package/dist/modules/attachments/data/entities.js +3 -0
- package/dist/modules/attachments/data/entities.js.map +2 -2
- package/dist/modules/attachments/data/validators.js +41 -0
- package/dist/modules/attachments/data/validators.js.map +7 -0
- package/dist/modules/attachments/di.js +5 -2
- package/dist/modules/attachments/di.js.map +2 -2
- package/dist/modules/attachments/generators.js +29 -0
- package/dist/modules/attachments/generators.js.map +7 -0
- package/dist/modules/attachments/index.js +8 -1
- package/dist/modules/attachments/index.js.map +2 -2
- package/dist/modules/attachments/lib/access-errors.js +35 -0
- package/dist/modules/attachments/lib/access-errors.js.map +7 -0
- package/dist/modules/attachments/lib/access-mutation.js +23 -0
- package/dist/modules/attachments/lib/access-mutation.js.map +7 -0
- package/dist/modules/attachments/lib/access-protection.js +73 -0
- package/dist/modules/attachments/lib/access-protection.js.map +7 -0
- package/dist/modules/attachments/lib/access-query.js +74 -0
- package/dist/modules/attachments/lib/access-query.js.map +7 -0
- package/dist/modules/attachments/lib/access-registry.js +87 -0
- package/dist/modules/attachments/lib/access-registry.js.map +7 -0
- package/dist/modules/attachments/lib/access-runner.js +161 -0
- package/dist/modules/attachments/lib/access-runner.js.map +7 -0
- package/dist/modules/attachments/lib/access-types.js +1 -0
- package/dist/modules/attachments/lib/access-types.js.map +7 -0
- package/dist/modules/attachments/lib/attachment-service.js +14 -6
- package/dist/modules/attachments/lib/attachment-service.js.map +2 -2
- package/dist/modules/attachments/lib/createFromBuffer.js +2 -0
- package/dist/modules/attachments/lib/createFromBuffer.js.map +2 -2
- package/dist/modules/attachments/lib/partitions.js +3 -2
- package/dist/modules/attachments/lib/partitions.js.map +2 -2
- package/dist/modules/attachments/lib/scoped-upload-service.js +2 -0
- package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
- package/dist/modules/attachments/lib/target-access-service.js +12 -3
- package/dist/modules/attachments/lib/target-access-service.js.map +2 -2
- package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js +38 -0
- package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js.map +7 -0
- package/dist/modules/attachments/setup.js +10 -0
- package/dist/modules/attachments/setup.js.map +2 -2
- package/dist/modules/notifications/api/[id]/action/route.js +8 -0
- package/dist/modules/notifications/api/[id]/action/route.js.map +2 -2
- package/dist/modules/notifications/api/route.js +17 -0
- package/dist/modules/notifications/api/route.js.map +2 -2
- package/dist/modules/notifications/data/validators.js +1 -1
- package/dist/modules/notifications/data/validators.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +45 -3
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/notifications/lib/routeHelpers.js +27 -0
- package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +50 -34
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/status-machine.js +10 -1
- package/dist/modules/payment_gateways/lib/status-machine.js.map +2 -2
- package/dist/modules/payment_gateways/lib/webhook-processor.js +1 -1
- package/dist/modules/payment_gateways/lib/webhook-processor.js.map +2 -2
- package/dist/modules/staff/notifications.js +4 -2
- package/dist/modules/staff/notifications.js.map +2 -2
- package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js +1 -1
- package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js.map +2 -2
- package/dist/modules/workflows/api/tasks/[id]/complete/route.js +7 -1
- package/dist/modules/workflows/api/tasks/[id]/complete/route.js.map +2 -2
- package/dist/modules/workflows/backend/tasks/[id]/page.js +2 -1
- package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
- package/dist/modules/workflows/data/entities.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js +18 -4
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/lib/activity-types.js +2 -2
- package/dist/modules/workflows/lib/activity-types.js.map +2 -2
- package/dist/modules/workflows/lib/condition-handler.js +7 -1
- package/dist/modules/workflows/lib/condition-handler.js.map +2 -2
- package/dist/modules/workflows/lib/duration.js +13 -0
- package/dist/modules/workflows/lib/duration.js.map +2 -2
- package/dist/modules/workflows/lib/instance-status.js +16 -0
- package/dist/modules/workflows/lib/instance-status.js.map +7 -0
- package/dist/modules/workflows/lib/step-handler.js +9 -2
- package/dist/modules/workflows/lib/step-handler.js.map +2 -2
- package/dist/modules/workflows/lib/task-handler.js +16 -8
- package/dist/modules/workflows/lib/task-handler.js.map +2 -2
- package/dist/modules/workflows/lib/task-quick-action.js +13 -10
- package/dist/modules/workflows/lib/task-quick-action.js.map +2 -2
- package/dist/modules/workflows/lib/task-sla.js +10 -0
- package/dist/modules/workflows/lib/task-sla.js.map +2 -2
- package/dist/modules/workflows/lib/transition-handler.js +2 -1
- package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
- package/dist/modules/workflows/lib/workflow-executor.js +29 -1
- package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
- package/dist/modules/workflows/notifications.js +4 -0
- package/dist/modules/workflows/notifications.js.map +2 -2
- package/generated/entities/api_key/index.ts +1 -0
- package/generated/entities/attachment_partition/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +7 -7
- package/src/helpers/integration/attachmentAccessFixtures.ts +141 -0
- package/src/modules/api_keys/data/entities.ts +25 -1
- package/src/modules/api_keys/migrations/.snapshot-open-mercato.json +234 -68
- package/src/modules/api_keys/migrations/Migration20261012202110_api_keys.ts +47 -0
- package/src/modules/api_keys/services/apiKeyService.ts +55 -9
- package/src/modules/attachments/api/file/[id]/route.ts +12 -9
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +10 -8
- package/src/modules/attachments/api/library/[id]/route.ts +87 -58
- package/src/modules/attachments/api/library/route.ts +40 -11
- package/src/modules/attachments/api/partitions/route.ts +2 -0
- package/src/modules/attachments/api/route.ts +68 -35
- package/src/modules/attachments/api/transfer/route.ts +65 -42
- package/src/modules/attachments/data/entities.ts +4 -0
- package/src/modules/attachments/data/validators.ts +36 -0
- package/src/modules/attachments/di.ts +4 -1
- package/src/modules/attachments/generators.ts +27 -0
- package/src/modules/attachments/i18n/de.json +4 -0
- package/src/modules/attachments/i18n/en.json +4 -0
- package/src/modules/attachments/i18n/es.json +4 -0
- package/src/modules/attachments/i18n/ko.json +4 -0
- package/src/modules/attachments/i18n/pl.json +4 -0
- package/src/modules/attachments/index.ts +18 -0
- package/src/modules/attachments/lib/access-errors.ts +32 -0
- package/src/modules/attachments/lib/access-mutation.ts +26 -0
- package/src/modules/attachments/lib/access-protection.ts +83 -0
- package/src/modules/attachments/lib/access-query.ts +86 -0
- package/src/modules/attachments/lib/access-registry.ts +117 -0
- package/src/modules/attachments/lib/access-runner.ts +183 -0
- package/src/modules/attachments/lib/access-types.ts +78 -0
- package/src/modules/attachments/lib/attachment-service.ts +8 -4
- package/src/modules/attachments/lib/createFromBuffer.ts +3 -0
- package/src/modules/attachments/lib/partitions.ts +3 -2
- package/src/modules/attachments/lib/scoped-upload-service.ts +2 -0
- package/src/modules/attachments/lib/target-access-service.ts +10 -3
- package/src/modules/attachments/migrations/.snapshot-open-mercato.json +17 -0
- package/src/modules/attachments/migrations/Migration20261001120000_attachments.ts +35 -0
- package/src/modules/attachments/setup.ts +10 -0
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/notifications/api/[id]/action/route.ts +8 -0
- package/src/modules/notifications/api/route.ts +17 -0
- package/src/modules/notifications/data/validators.ts +1 -1
- package/src/modules/notifications/i18n/de.json +2 -0
- package/src/modules/notifications/i18n/en.json +2 -0
- package/src/modules/notifications/i18n/es.json +2 -0
- package/src/modules/notifications/i18n/ko.json +2 -0
- package/src/modules/notifications/i18n/pl.json +2 -0
- package/src/modules/notifications/lib/notificationService.ts +67 -1
- package/src/modules/notifications/lib/routeHelpers.ts +38 -0
- package/src/modules/payment_gateways/lib/gateway-service.ts +50 -33
- package/src/modules/payment_gateways/lib/status-machine.ts +19 -0
- package/src/modules/payment_gateways/lib/webhook-processor.ts +1 -1
- package/src/modules/staff/notifications.ts +2 -0
- package/src/modules/workflows/AGENTS.md +2 -2
- package/src/modules/workflows/api/portal/tasks/[id]/complete/route.ts +5 -1
- package/src/modules/workflows/api/tasks/[id]/complete/route.ts +7 -1
- package/src/modules/workflows/backend/tasks/[id]/page.tsx +6 -2
- package/src/modules/workflows/data/entities.ts +6 -0
- package/src/modules/workflows/i18n/de.json +2 -0
- package/src/modules/workflows/i18n/en.json +2 -0
- package/src/modules/workflows/i18n/es.json +2 -0
- package/src/modules/workflows/i18n/ko.json +2 -0
- package/src/modules/workflows/i18n/pl.json +2 -0
- package/src/modules/workflows/lib/activity-executor.ts +36 -4
- package/src/modules/workflows/lib/activity-types.ts +2 -2
- package/src/modules/workflows/lib/condition-handler.ts +16 -1
- package/src/modules/workflows/lib/duration.ts +25 -0
- package/src/modules/workflows/lib/instance-status.ts +29 -0
- package/src/modules/workflows/lib/step-handler.ts +16 -1
- package/src/modules/workflows/lib/task-handler.ts +18 -10
- package/src/modules/workflows/lib/task-quick-action.ts +19 -10
- package/src/modules/workflows/lib/task-sla.ts +13 -0
- package/src/modules/workflows/lib/transition-handler.ts +2 -1
- package/src/modules/workflows/lib/workflow-executor.ts +44 -1
- package/src/modules/workflows/notifications.ts +4 -0
|
@@ -2,7 +2,7 @@ import type { EntityManager } from '@mikro-orm/postgresql'
|
|
|
2
2
|
import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
|
|
3
3
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
4
4
|
import { Attachment, AttachmentPartition } from '../data/entities'
|
|
5
|
-
import {
|
|
5
|
+
import { evaluateAttachmentAccess, type AttachmentAccessContext } from './access-runner'
|
|
6
6
|
import { readAttachmentMetadata } from './metadata'
|
|
7
7
|
|
|
8
8
|
export type AttachmentRecordTarget = {
|
|
@@ -19,7 +19,10 @@ export type AttachmentTargetAccessInput = {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export class AttachmentTargetAccessService {
|
|
22
|
-
constructor(
|
|
22
|
+
constructor(
|
|
23
|
+
private readonly em: EntityManager,
|
|
24
|
+
private readonly resolveAccessContext?: (() => AttachmentAccessContext) | null,
|
|
25
|
+
) {}
|
|
23
26
|
|
|
24
27
|
async canAccessLinkedTarget(input: AttachmentTargetAccessInput): Promise<boolean> {
|
|
25
28
|
if (!input.targets.length) return false
|
|
@@ -44,7 +47,11 @@ export class AttachmentTargetAccessService {
|
|
|
44
47
|
],
|
|
45
48
|
})
|
|
46
49
|
if (!partition) return false
|
|
47
|
-
|
|
50
|
+
const access = await evaluateAttachmentAccess({
|
|
51
|
+
auth: input.auth, attachment, partition, action: 'read',
|
|
52
|
+
context: this.resolveAccessContext?.(), requireAuthForPublic: true,
|
|
53
|
+
})
|
|
54
|
+
if (!access.ok) return false
|
|
48
55
|
|
|
49
56
|
const metadata = readAttachmentMetadata(attachment.storageMetadata)
|
|
50
57
|
return input.targets.some((target) => (
|
|
@@ -8,6 +8,23 @@
|
|
|
8
8
|
"name": "attachment_partitions",
|
|
9
9
|
"schema": "public",
|
|
10
10
|
"columns": {
|
|
11
|
+
"access_resolver_requirements": {
|
|
12
|
+
"name": "access_resolver_requirements",
|
|
13
|
+
"type": "jsonb",
|
|
14
|
+
"unsigned": false,
|
|
15
|
+
"autoincrement": false,
|
|
16
|
+
"primary": false,
|
|
17
|
+
"nullable": true,
|
|
18
|
+
"unique": false,
|
|
19
|
+
"length": null,
|
|
20
|
+
"precision": null,
|
|
21
|
+
"scale": null,
|
|
22
|
+
"default": null,
|
|
23
|
+
"comment": null,
|
|
24
|
+
"collation": null,
|
|
25
|
+
"enumItems": [],
|
|
26
|
+
"mappedType": "json"
|
|
27
|
+
},
|
|
11
28
|
"code": {
|
|
12
29
|
"name": "code",
|
|
13
30
|
"type": "text",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations'
|
|
2
|
+
|
|
3
|
+
export class Migration20261001120000_attachments extends Migration {
|
|
4
|
+
override up(): void {
|
|
5
|
+
this.addSql('alter table "attachment_partitions" add column if not exists "access_resolver_requirements" jsonb null;')
|
|
6
|
+
this.addSql(`
|
|
7
|
+
update attachment_partitions as partition
|
|
8
|
+
set access_resolver_requirements = coalesce(partition.access_resolver_requirements, '[]'::jsonb)
|
|
9
|
+
|| '[{"resolverId":"documents.document-attachments","targetEntity":"documents:document"}]'::jsonb,
|
|
10
|
+
updated_at = now()
|
|
11
|
+
where (partition.access_resolver_requirements is null or jsonb_typeof(partition.access_resolver_requirements) = 'array')
|
|
12
|
+
and not coalesce(partition.access_resolver_requirements, '[]'::jsonb)
|
|
13
|
+
@> '[{"resolverId":"documents.document-attachments","targetEntity":"documents:document"}]'::jsonb
|
|
14
|
+
and exists (
|
|
15
|
+
select 1 from attachments as attachment
|
|
16
|
+
where attachment.partition_code = partition.code
|
|
17
|
+
and (
|
|
18
|
+
btrim(attachment.entity_id) = 'documents:document'
|
|
19
|
+
or exists (
|
|
20
|
+
select 1
|
|
21
|
+
from jsonb_array_elements(
|
|
22
|
+
case jsonb_typeof(attachment.storage_metadata->'assignments')
|
|
23
|
+
when 'array' then attachment.storage_metadata->'assignments'
|
|
24
|
+
when 'object' then jsonb_build_array(attachment.storage_metadata->'assignments')
|
|
25
|
+
else '[]'::jsonb
|
|
26
|
+
end
|
|
27
|
+
) as assignment
|
|
28
|
+
where jsonb_typeof(assignment) = 'object'
|
|
29
|
+
and btrim(assignment->>'type') = 'documents:document'
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
`)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'
|
|
2
|
+
import { syncAttachmentAccessProtection } from './lib/access-protection'
|
|
3
|
+
import { ensureDefaultPartitions } from './lib/partitions'
|
|
2
4
|
|
|
3
5
|
export const setup: ModuleSetupConfig = {
|
|
6
|
+
async onTenantCreated({ em }) {
|
|
7
|
+
await ensureDefaultPartitions(em)
|
|
8
|
+
await syncAttachmentAccessProtection(em)
|
|
9
|
+
},
|
|
10
|
+
async seedDefaults({ em }) {
|
|
11
|
+
await ensureDefaultPartitions(em)
|
|
12
|
+
await syncAttachmentAccessProtection(em)
|
|
13
|
+
},
|
|
4
14
|
defaultRoleFeatures: {
|
|
5
15
|
admin: ['attachments.*', 'attachments.view', 'attachments.manage'],
|
|
6
16
|
employee: ['attachments.view'],
|
|
@@ -228,6 +228,8 @@
|
|
|
228
228
|
"auth.acl.features.forms.submissions.export": "Einreichungsdaten exportieren (Datenübertragbarkeit, DSGVO Art. 15/20)",
|
|
229
229
|
"auth.acl.features.forms.submissions.manage": "Einreichungen verwalten (erneut öffnen, Beteiligte zuweisen, exportieren)",
|
|
230
230
|
"auth.acl.features.forms.view": "Formulare und Einreichungen ansehen",
|
|
231
|
+
"auth.acl.features.gateway_autopay.configure": "Configure Autopay gateway settings",
|
|
232
|
+
"auth.acl.features.gateway_autopay.view": "View Autopay gateway configuration",
|
|
231
233
|
"auth.acl.features.gateway_stripe.configure": "Configure Stripe gateway settings",
|
|
232
234
|
"auth.acl.features.gateway_stripe.view": "View Stripe gateway configuration",
|
|
233
235
|
"auth.acl.features.gateway_tpay.configure": "Tpay-Gateway-Einstellungen konfigurieren",
|
|
@@ -459,6 +461,7 @@
|
|
|
459
461
|
"auth.acl.modules.example": "Example Module",
|
|
460
462
|
"auth.acl.modules.example_customers_sync": "Example Customers Sync",
|
|
461
463
|
"auth.acl.modules.feature_toggles": "Feature Toggles",
|
|
464
|
+
"auth.acl.modules.gateway_autopay": "Autopay Payment Gateway",
|
|
462
465
|
"auth.acl.modules.gateway_stripe": "Stripe Payment Gateway",
|
|
463
466
|
"auth.acl.modules.gateway_tpay": "Tpay-Zahlungsgateway",
|
|
464
467
|
"auth.acl.modules.inbox_ops": "InboxOps — Email-to-ERP Agent",
|
|
@@ -228,6 +228,8 @@
|
|
|
228
228
|
"auth.acl.features.forms.submissions.export": "Export submission data (GDPR Art. 15/20 portability)",
|
|
229
229
|
"auth.acl.features.forms.submissions.manage": "Manage submissions (reopen, assign actors, export)",
|
|
230
230
|
"auth.acl.features.forms.view": "View forms and submissions",
|
|
231
|
+
"auth.acl.features.gateway_autopay.configure": "Configure Autopay gateway settings",
|
|
232
|
+
"auth.acl.features.gateway_autopay.view": "View Autopay gateway configuration",
|
|
231
233
|
"auth.acl.features.gateway_stripe.configure": "Configure Stripe gateway settings",
|
|
232
234
|
"auth.acl.features.gateway_stripe.view": "View Stripe gateway configuration",
|
|
233
235
|
"auth.acl.features.gateway_tpay.configure": "Configure Tpay gateway settings",
|
|
@@ -459,6 +461,7 @@
|
|
|
459
461
|
"auth.acl.modules.example": "Example Module",
|
|
460
462
|
"auth.acl.modules.example_customers_sync": "Example Customers Sync",
|
|
461
463
|
"auth.acl.modules.feature_toggles": "Feature Toggles",
|
|
464
|
+
"auth.acl.modules.gateway_autopay": "Autopay Payment Gateway",
|
|
462
465
|
"auth.acl.modules.gateway_stripe": "Stripe Payment Gateway",
|
|
463
466
|
"auth.acl.modules.gateway_tpay": "Tpay Payment Gateway",
|
|
464
467
|
"auth.acl.modules.inbox_ops": "InboxOps — Email-to-ERP Agent",
|
|
@@ -228,6 +228,8 @@
|
|
|
228
228
|
"auth.acl.features.forms.submissions.export": "Exportar datos de envíos (portabilidad, RGPD art. 15/20)",
|
|
229
229
|
"auth.acl.features.forms.submissions.manage": "Gestionar envíos (reabrir, asignar participantes, exportar)",
|
|
230
230
|
"auth.acl.features.forms.view": "Ver formularios y envíos",
|
|
231
|
+
"auth.acl.features.gateway_autopay.configure": "Configure Autopay gateway settings",
|
|
232
|
+
"auth.acl.features.gateway_autopay.view": "View Autopay gateway configuration",
|
|
231
233
|
"auth.acl.features.gateway_stripe.configure": "Configure Stripe gateway settings",
|
|
232
234
|
"auth.acl.features.gateway_stripe.view": "View Stripe gateway configuration",
|
|
233
235
|
"auth.acl.features.gateway_tpay.configure": "Configurar los ajustes de la pasarela Tpay",
|
|
@@ -459,6 +461,7 @@
|
|
|
459
461
|
"auth.acl.modules.example": "Example Module",
|
|
460
462
|
"auth.acl.modules.example_customers_sync": "Example Customers Sync",
|
|
461
463
|
"auth.acl.modules.feature_toggles": "Feature Toggles",
|
|
464
|
+
"auth.acl.modules.gateway_autopay": "Autopay Payment Gateway",
|
|
462
465
|
"auth.acl.modules.gateway_stripe": "Stripe Payment Gateway",
|
|
463
466
|
"auth.acl.modules.gateway_tpay": "Pasarela de pago Tpay",
|
|
464
467
|
"auth.acl.modules.inbox_ops": "InboxOps — Email-to-ERP Agent",
|
|
@@ -228,6 +228,8 @@
|
|
|
228
228
|
"auth.acl.features.forms.submissions.export": "Export submission data (GDPR Art. 15/20 portability)",
|
|
229
229
|
"auth.acl.features.forms.submissions.manage": "Manage submissions (reopen, assign actors, export)",
|
|
230
230
|
"auth.acl.features.forms.view": "View forms and submissions",
|
|
231
|
+
"auth.acl.features.gateway_autopay.configure": "Configure Autopay gateway settings",
|
|
232
|
+
"auth.acl.features.gateway_autopay.view": "View Autopay gateway configuration",
|
|
231
233
|
"auth.acl.features.gateway_stripe.configure": "Configure Stripe gateway settings",
|
|
232
234
|
"auth.acl.features.gateway_stripe.view": "View Stripe gateway configuration",
|
|
233
235
|
"auth.acl.features.gateway_tpay.configure": "Tpay 게이트웨이 설정 구성",
|
|
@@ -459,6 +461,7 @@
|
|
|
459
461
|
"auth.acl.modules.example": "Example Module",
|
|
460
462
|
"auth.acl.modules.example_customers_sync": "Example Customers Sync",
|
|
461
463
|
"auth.acl.modules.feature_toggles": "Feature Toggles",
|
|
464
|
+
"auth.acl.modules.gateway_autopay": "Autopay Payment Gateway",
|
|
462
465
|
"auth.acl.modules.gateway_stripe": "Stripe Payment Gateway",
|
|
463
466
|
"auth.acl.modules.gateway_tpay": "Tpay 결제 게이트웨이",
|
|
464
467
|
"auth.acl.modules.inbox_ops": "InboxOps — Email-to-ERP Agent",
|
|
@@ -228,6 +228,8 @@
|
|
|
228
228
|
"auth.acl.features.forms.submissions.export": "Eksport danych zgłoszeń (przenoszenie danych, RODO art. 15/20)",
|
|
229
229
|
"auth.acl.features.forms.submissions.manage": "Zarządzanie zgłoszeniami (ponowne otwieranie, przypisywanie uczestników, eksport)",
|
|
230
230
|
"auth.acl.features.forms.view": "Przeglądanie formularzy i zgłoszeń",
|
|
231
|
+
"auth.acl.features.gateway_autopay.configure": "Konfiguruj ustawienia bramki Autopay",
|
|
232
|
+
"auth.acl.features.gateway_autopay.view": "Wyświetl konfigurację bramki Autopay",
|
|
231
233
|
"auth.acl.features.gateway_stripe.configure": "Konfiguruj ustawienia bramki Stripe",
|
|
232
234
|
"auth.acl.features.gateway_stripe.view": "Wyświetl konfigurację bramki Stripe",
|
|
233
235
|
"auth.acl.features.gateway_tpay.configure": "Konfiguruj ustawienia bramki Tpay",
|
|
@@ -459,6 +461,7 @@
|
|
|
459
461
|
"auth.acl.modules.example": "Moduł przykładowy",
|
|
460
462
|
"auth.acl.modules.example_customers_sync": "Przykładowa synchronizacja klientów",
|
|
461
463
|
"auth.acl.modules.feature_toggles": "Przełączniki funkcji",
|
|
464
|
+
"auth.acl.modules.gateway_autopay": "Bramka płatnicza Autopay",
|
|
462
465
|
"auth.acl.modules.gateway_stripe": "Bramka płatnicza Stripe",
|
|
463
466
|
"auth.acl.modules.gateway_tpay": "Bramka płatnicza Tpay",
|
|
464
467
|
"auth.acl.modules.inbox_ops": "InboxOps — agent e-mail do ERP",
|
|
@@ -97,6 +97,14 @@ export const openApi = {
|
|
|
97
97
|
},
|
|
98
98
|
},
|
|
99
99
|
},
|
|
100
|
+
403: {
|
|
101
|
+
description: 'The stored action command is not declared by the notification type with the same action id and command, or the user lacks the features the declared action requires (`code: notification_action_forbidden`)',
|
|
102
|
+
content: {
|
|
103
|
+
'application/json': {
|
|
104
|
+
schema: actionConflictResponseSchema,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
100
108
|
400: {
|
|
101
109
|
description: 'Action not found or failed',
|
|
102
110
|
content: {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
resolveCrudCache,
|
|
10
10
|
} from '@open-mercato/shared/lib/crud/cache'
|
|
11
11
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi/types'
|
|
12
|
+
import { NOTIFICATION_ACTION_COMMAND_NOT_ALLOWED_ERROR_CODE } from '@open-mercato/shared/modules/notifications/types'
|
|
12
13
|
import { Notification } from '../data/entities'
|
|
13
14
|
import { listNotificationsSchema, createNotificationSchema } from '../data/validators'
|
|
14
15
|
import { toNotificationDto } from '../lib/notificationMapper'
|
|
@@ -21,12 +22,14 @@ import {
|
|
|
21
22
|
NOTIFICATION_RESOURCE_KIND,
|
|
22
23
|
notificationCrudErrorResponse,
|
|
23
24
|
notificationValidationErrorResponse,
|
|
25
|
+
rejectNotificationActionCommands,
|
|
24
26
|
resolveGuardedNotificationContext,
|
|
25
27
|
resolveNotificationContext,
|
|
26
28
|
runGuardedNotificationWrite,
|
|
27
29
|
TENANT_SCOPE_REQUIRED_ERROR_CODE,
|
|
28
30
|
} from '../lib/routeHelpers'
|
|
29
31
|
import {
|
|
32
|
+
actionConflictResponseSchema,
|
|
30
33
|
buildNotificationsCrudOpenApi,
|
|
31
34
|
createPagedListResponseSchema,
|
|
32
35
|
notificationItemSchema,
|
|
@@ -216,6 +219,8 @@ export async function POST(req: Request) {
|
|
|
216
219
|
if (!parsed.success) {
|
|
217
220
|
return notificationValidationErrorResponse(parsed.error)
|
|
218
221
|
}
|
|
222
|
+
const commandRejection = await rejectNotificationActionCommands(parsed.data)
|
|
223
|
+
if (commandRejection) return commandRejection
|
|
219
224
|
|
|
220
225
|
try {
|
|
221
226
|
const guarded = await runGuardedNotificationWrite(
|
|
@@ -251,6 +256,7 @@ const notificationsCrudOpenApi = buildNotificationsCrudOpenApi({
|
|
|
251
256
|
})
|
|
252
257
|
|
|
253
258
|
const notificationsCrudGet = notificationsCrudOpenApi.methods?.GET ?? {}
|
|
259
|
+
const notificationsCrudPost = notificationsCrudOpenApi.methods?.POST ?? {}
|
|
254
260
|
|
|
255
261
|
// The CRUD factory documents errors only for DELETE, and POST already gets an auto-generated 403
|
|
256
262
|
// from its `requireFeatures` metadata. GET is authenticated-only, so its tenant-scope rejection has
|
|
@@ -270,5 +276,16 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
270
276
|
},
|
|
271
277
|
],
|
|
272
278
|
},
|
|
279
|
+
POST: {
|
|
280
|
+
...notificationsCrudPost,
|
|
281
|
+
errors: [
|
|
282
|
+
...(notificationsCrudPost.errors ?? []),
|
|
283
|
+
{
|
|
284
|
+
status: 400,
|
|
285
|
+
description: `Invalid payload, or an action carries a \`commandId\` (code: ${NOTIFICATION_ACTION_COMMAND_NOT_ALLOWED_ERROR_CODE})`,
|
|
286
|
+
schema: actionConflictResponseSchema,
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
},
|
|
273
290
|
},
|
|
274
291
|
}
|
|
@@ -15,7 +15,7 @@ export const notificationActionSchema = z.object({
|
|
|
15
15
|
labelKey: z.string().optional(),
|
|
16
16
|
variant: z.enum(['default', 'secondary', 'destructive', 'outline', 'ghost']).optional(),
|
|
17
17
|
icon: z.string().optional(),
|
|
18
|
-
commandId: z.string().optional(),
|
|
18
|
+
commandId: z.string().optional().describe('Refused by the HTTP create routes (400 notification_action_command_not_allowed); command actions are attached by server code from the notification type definition'),
|
|
19
19
|
href: safeRelativeHrefSchema.optional(),
|
|
20
20
|
confirmRequired: z.boolean().optional(),
|
|
21
21
|
confirmMessage: z.string().optional(),
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
"notifications.delivery.settings.invalid": "Ungültige Zustelleinstellungen",
|
|
15
15
|
"notifications.empty": "Keine Benachrichtigungen",
|
|
16
16
|
"notifications.error.action": "Aktion konnte nicht ausgeführt werden",
|
|
17
|
+
"notifications.error.actionCommandNotAllowed": "Über die API erstellte Benachrichtigungsaktionen dürfen keine Befehle ausführen",
|
|
18
|
+
"notifications.error.actionForbidden": "Sie dürfen diese Benachrichtigungsaktion nicht ausführen",
|
|
17
19
|
"notifications.error.load": "Benachrichtigungen konnten nicht geladen werden",
|
|
18
20
|
"notifications.filters.actionRequired": "Aktion erforderlich",
|
|
19
21
|
"notifications.filters.all": "Alle",
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
"notifications.delivery.settings.invalid": "Invalid delivery settings",
|
|
15
15
|
"notifications.empty": "No notifications",
|
|
16
16
|
"notifications.error.action": "Failed to execute action",
|
|
17
|
+
"notifications.error.actionCommandNotAllowed": "Notification actions created through the API cannot run commands",
|
|
18
|
+
"notifications.error.actionForbidden": "You are not allowed to run this notification action",
|
|
17
19
|
"notifications.error.load": "Failed to load notifications",
|
|
18
20
|
"notifications.filters.actionRequired": "Action Required",
|
|
19
21
|
"notifications.filters.all": "All",
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
"notifications.delivery.settings.invalid": "Configuración de entrega no válida",
|
|
15
15
|
"notifications.empty": "Sin notificaciones",
|
|
16
16
|
"notifications.error.action": "No se pudo ejecutar la acción",
|
|
17
|
+
"notifications.error.actionCommandNotAllowed": "Las acciones de notificación creadas mediante la API no pueden ejecutar comandos",
|
|
18
|
+
"notifications.error.actionForbidden": "No tienes permiso para ejecutar esta acción de notificación",
|
|
17
19
|
"notifications.error.load": "No se pudieron cargar las notificaciones",
|
|
18
20
|
"notifications.filters.actionRequired": "Acción requerida",
|
|
19
21
|
"notifications.filters.all": "Todas",
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
"notifications.delivery.settings.invalid": "잘못된 전달 설정입니다",
|
|
15
15
|
"notifications.empty": "알림 없음",
|
|
16
16
|
"notifications.error.action": "작업을 실행하지 못했습니다",
|
|
17
|
+
"notifications.error.actionCommandNotAllowed": "API로 생성한 알림 작업은 명령을 실행할 수 없습니다",
|
|
18
|
+
"notifications.error.actionForbidden": "이 알림 작업을 실행할 권한이 없습니다",
|
|
17
19
|
"notifications.error.load": "알림을 불러오지 못했습니다",
|
|
18
20
|
"notifications.filters.actionRequired": "조치 필요",
|
|
19
21
|
"notifications.filters.all": "전체",
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
"notifications.delivery.settings.invalid": "Nieprawidłowe ustawienia dostarczania",
|
|
15
15
|
"notifications.empty": "Brak powiadomień",
|
|
16
16
|
"notifications.error.action": "Nie udało się wykonać akcji",
|
|
17
|
+
"notifications.error.actionCommandNotAllowed": "Akcje powiadomień utworzone przez API nie mogą uruchamiać poleceń",
|
|
18
|
+
"notifications.error.actionForbidden": "Nie masz uprawnień do wykonania tej akcji powiadomienia",
|
|
17
19
|
"notifications.error.load": "Nie udało się załadować powiadomień",
|
|
18
20
|
"notifications.filters.actionRequired": "Wymagana akcja",
|
|
19
21
|
"notifications.filters.all": "Wszystkie",
|
|
@@ -6,8 +6,13 @@ import { Notification, type NotificationStatus } from '../data/entities'
|
|
|
6
6
|
import type { CreateNotificationInput, CreateBatchNotificationInput, CreateRoleNotificationInput, CreateFeatureNotificationInput, ExecuteActionInput } from '../data/validators'
|
|
7
7
|
import {
|
|
8
8
|
NOTIFICATION_ACTION_ALREADY_EXECUTED_ERROR_CODE,
|
|
9
|
+
NOTIFICATION_ACTION_FORBIDDEN_ERROR_CODE,
|
|
10
|
+
type NotificationAction,
|
|
9
11
|
type NotificationPollData,
|
|
12
|
+
type NotificationTypeAction,
|
|
10
13
|
} from '@open-mercato/shared/modules/notifications/types'
|
|
14
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
15
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
11
16
|
import { NOTIFICATION_EVENTS, NOTIFICATION_SSE_EVENTS } from './events'
|
|
12
17
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
13
18
|
import {
|
|
@@ -97,6 +102,27 @@ function actionAlreadyExecuted(): CrudHttpError {
|
|
|
97
102
|
})
|
|
98
103
|
}
|
|
99
104
|
|
|
105
|
+
async function actionForbidden(): Promise<CrudHttpError> {
|
|
106
|
+
const { translate } = await resolveTranslations()
|
|
107
|
+
return new CrudHttpError(403, {
|
|
108
|
+
error: translate('notifications.error.actionForbidden', 'You are not allowed to run this notification action'),
|
|
109
|
+
code: NOTIFICATION_ACTION_FORBIDDEN_ERROR_CODE,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The command action the notification's registered type declares for a stored action: same action
|
|
115
|
+
* id (across `actions` and `optionalActions`) and same `commandId`. Anything else is not dispatched.
|
|
116
|
+
*/
|
|
117
|
+
function findDeclaredCommandAction(type: string, action: NotificationAction): NotificationTypeAction | null {
|
|
118
|
+
const definition = getNotificationType(type)
|
|
119
|
+
if (!definition) return null
|
|
120
|
+
const declared = [...definition.actions, ...(definition.optionalActions ?? [])]
|
|
121
|
+
.find((candidate) => candidate.id === action.id)
|
|
122
|
+
if (!declared?.commandId || declared.commandId !== action.commandId) return null
|
|
123
|
+
return declared
|
|
124
|
+
}
|
|
125
|
+
|
|
100
126
|
function applyNotificationContent(
|
|
101
127
|
notification: Notification,
|
|
102
128
|
input: NotificationContentInput,
|
|
@@ -285,6 +311,39 @@ export interface NotificationServiceDeps {
|
|
|
285
311
|
export function createNotificationService(deps: NotificationServiceDeps): NotificationService {
|
|
286
312
|
const { em: rootEm, eventBus, commandBus, container } = deps
|
|
287
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Whether the clicking user holds every feature a declared notification action requires.
|
|
316
|
+
* Fails closed: no user, no container, or a failing RBAC check denies the action.
|
|
317
|
+
*/
|
|
318
|
+
const userHoldsActionFeatures = async (
|
|
319
|
+
requiredFeatures: string[] | undefined,
|
|
320
|
+
scopeCtx: NotificationServiceContext,
|
|
321
|
+
): Promise<boolean> => {
|
|
322
|
+
if (!requiredFeatures || requiredFeatures.length === 0) return true
|
|
323
|
+
if (!scopeCtx.userId || !container) return false
|
|
324
|
+
try {
|
|
325
|
+
const rbacService = container.resolve('rbacService') as {
|
|
326
|
+
userHasAllFeatures: (
|
|
327
|
+
userId: string,
|
|
328
|
+
features: string[],
|
|
329
|
+
scope: { tenantId: string | null; organizationId: string | null },
|
|
330
|
+
) => Promise<boolean>
|
|
331
|
+
}
|
|
332
|
+
const allowed = await rbacService.userHasAllFeatures(
|
|
333
|
+
scopeCtx.userId,
|
|
334
|
+
requiredFeatures,
|
|
335
|
+
{ tenantId: scopeCtx.tenantId, organizationId: scopeCtx.organizationId ?? null },
|
|
336
|
+
)
|
|
337
|
+
return allowed === true
|
|
338
|
+
} catch (err) {
|
|
339
|
+
getTelemetryRuntime()?.reportError(err, {
|
|
340
|
+
module: 'notifications',
|
|
341
|
+
code: 'notifications.action_feature_check_failed',
|
|
342
|
+
})
|
|
343
|
+
return false
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
288
347
|
/**
|
|
289
348
|
* Resolves the authoritative delivery-channel set for one recipient at create time — the single
|
|
290
349
|
* gate that folds per-send target, per-type eligibility, registered strategies, and the
|
|
@@ -696,6 +755,13 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
|
|
|
696
755
|
throw actionAlreadyExecuted()
|
|
697
756
|
}
|
|
698
757
|
|
|
758
|
+
if (action.commandId) {
|
|
759
|
+
const declared = findDeclaredCommandAction(notification.type, action)
|
|
760
|
+
if (!declared || !(await userHoldsActionFeatures(declared.requiredFeatures, ctx))) {
|
|
761
|
+
throw await actionForbidden()
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
699
765
|
const actionedAt = new Date()
|
|
700
766
|
const previousStatus = notification.status
|
|
701
767
|
const previousActionedAt = notification.actionedAt ?? null
|
|
@@ -749,8 +815,8 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
|
|
|
749
815
|
|
|
750
816
|
if (action.commandId && commandBus && container) {
|
|
751
817
|
const commandInput = {
|
|
752
|
-
id: notification.sourceEntityId,
|
|
753
818
|
...input.payload,
|
|
819
|
+
id: notification.sourceEntityId,
|
|
754
820
|
}
|
|
755
821
|
|
|
756
822
|
// Build a CommandRuntimeContext from the notification service context
|
|
@@ -5,7 +5,9 @@ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/d
|
|
|
5
5
|
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
6
6
|
import { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'
|
|
7
7
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
8
|
+
import { NOTIFICATION_ACTION_COMMAND_NOT_ALLOWED_ERROR_CODE } from '@open-mercato/shared/modules/notifications/types'
|
|
8
9
|
import { resolveNotificationService, type NotificationService } from './notificationService'
|
|
10
|
+
import { actionConflictResponseSchema } from '../api/openapi'
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Mutation-guard resource kind for notification rows.
|
|
@@ -60,6 +62,32 @@ export async function notificationValidationErrorResponse(error: z.ZodError): Pr
|
|
|
60
62
|
)
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
export type NotificationActionsPayload = { actions?: Array<{ commandId?: string | null }> | null }
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Reject an API-supplied action that carries a `commandId`.
|
|
69
|
+
*
|
|
70
|
+
* Command-backed actions run with the clicking user's identity, so only server code may attach
|
|
71
|
+
* them, from the notification type definition. Called after payload validation and before the
|
|
72
|
+
* mutation guard or the service write.
|
|
73
|
+
*/
|
|
74
|
+
export async function rejectNotificationActionCommands(
|
|
75
|
+
data: NotificationActionsPayload,
|
|
76
|
+
): Promise<Response | null> {
|
|
77
|
+
if (!data.actions?.some((action) => action.commandId)) return null
|
|
78
|
+
const { t } = await resolveTranslations()
|
|
79
|
+
return Response.json(
|
|
80
|
+
{
|
|
81
|
+
error: t(
|
|
82
|
+
'notifications.error.actionCommandNotAllowed',
|
|
83
|
+
'Notification actions created through the API cannot run commands',
|
|
84
|
+
),
|
|
85
|
+
code: NOTIFICATION_ACTION_COMMAND_NOT_ALLOWED_ERROR_CODE,
|
|
86
|
+
},
|
|
87
|
+
{ status: 400 },
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
63
91
|
export function notificationCrudErrorResponse(error: unknown): Response | null {
|
|
64
92
|
if (!isCrudHttpError(error)) return null
|
|
65
93
|
return Response.json(error.body ?? { error: 'Notification request failed' }, { status: error.status })
|
|
@@ -235,6 +263,8 @@ export function createBulkNotificationRoute<TSchema extends z.ZodTypeAny>(
|
|
|
235
263
|
if (!parsed.success) {
|
|
236
264
|
return notificationValidationErrorResponse(parsed.error)
|
|
237
265
|
}
|
|
266
|
+
const commandRejection = await rejectNotificationActionCommands(parsed.data as NotificationActionsPayload)
|
|
267
|
+
if (commandRejection) return commandRejection
|
|
238
268
|
|
|
239
269
|
try {
|
|
240
270
|
const guarded = await runGuardedNotificationWrite(
|
|
@@ -298,6 +328,14 @@ export function createBulkNotificationOpenApi<TSchema extends z.ZodTypeAny>(
|
|
|
298
328
|
},
|
|
299
329
|
},
|
|
300
330
|
},
|
|
331
|
+
400: {
|
|
332
|
+
description: `Invalid payload, or an action carries a \`commandId\` (code: ${NOTIFICATION_ACTION_COMMAND_NOT_ALLOWED_ERROR_CODE})`,
|
|
333
|
+
content: {
|
|
334
|
+
'application/json': {
|
|
335
|
+
schema: actionConflictResponseSchema,
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
},
|
|
301
339
|
},
|
|
302
340
|
},
|
|
303
341
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import { QueryOrder } from '@mikro-orm/core'
|
|
2
|
+
import { LockMode, QueryOrder } from '@mikro-orm/core'
|
|
3
3
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
4
4
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
5
5
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
@@ -21,7 +21,7 @@ import type { IntegrationStateService } from '../../integrations/lib/state-servi
|
|
|
21
21
|
import type { IntegrationLogService } from '../../integrations/lib/log-service'
|
|
22
22
|
import { conflict, CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
23
23
|
import { GatewayPaymentOperation, GatewaySessionInitialization, GatewayTransaction } from '../data/entities'
|
|
24
|
-
import { canApplyManualAction, isValidTransition, type ManualGatewayAction } from './status-machine'
|
|
24
|
+
import { canApplyManualAction, isValidTransition, resolveImpliedCaptureStatus, type ManualGatewayAction } from './status-machine'
|
|
25
25
|
import { emitPaymentGatewayEvent } from '../events'
|
|
26
26
|
import { readGatewayMetadata, readWebhookLog } from './transaction-fields'
|
|
27
27
|
import { reconcileSessionAmountWithOrder } from './order-amount-reconciliation'
|
|
@@ -124,6 +124,7 @@ export function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {
|
|
|
124
124
|
transactionId: string,
|
|
125
125
|
scope: { organizationId: string; tenantId: string },
|
|
126
126
|
targetEm: EntityManager = em,
|
|
127
|
+
options?: { lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
127
128
|
): Promise<GatewayTransaction> {
|
|
128
129
|
const transaction = await findOneWithDecryption(
|
|
129
130
|
targetEm,
|
|
@@ -134,7 +135,7 @@ export function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {
|
|
|
134
135
|
tenantId: scope.tenantId,
|
|
135
136
|
deletedAt: null,
|
|
136
137
|
},
|
|
137
|
-
|
|
138
|
+
options,
|
|
138
139
|
scope,
|
|
139
140
|
)
|
|
140
141
|
if (!transaction) {
|
|
@@ -765,42 +766,57 @@ export function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {
|
|
|
765
766
|
receivedAt?: string
|
|
766
767
|
}
|
|
767
768
|
}, scope: { organizationId: string; tenantId: string }): Promise<void> {
|
|
768
|
-
const transaction = await
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
769
|
+
const { transaction, previousStatus, impliedCaptureStatus, shouldApplyStatus } = await em.fork().transactional(async (tx) => {
|
|
770
|
+
const locked = await findTransactionOrThrow(transactionId, scope, tx, { lockMode: LockMode.PESSIMISTIC_WRITE })
|
|
771
|
+
const currentStatus = locked.unifiedStatus as UnifiedPaymentStatus
|
|
772
|
+
const implied = resolveImpliedCaptureStatus(currentStatus, update.unifiedStatus)
|
|
773
|
+
const canTransition = isValidTransition(currentStatus, update.unifiedStatus) || implied !== null
|
|
774
|
+
const applies = canTransition && update.unifiedStatus !== currentStatus
|
|
775
|
+
if (applies) {
|
|
776
|
+
if (implied) alignCapturedAmountWithStatus(locked, implied)
|
|
777
|
+
locked.unifiedStatus = update.unifiedStatus
|
|
778
|
+
alignCapturedAmountWithStatus(locked, update.unifiedStatus)
|
|
779
|
+
}
|
|
780
|
+
if (update.providerStatus) {
|
|
781
|
+
locked.gatewayStatus = update.providerStatus
|
|
782
|
+
}
|
|
783
|
+
if (update.providerData) {
|
|
784
|
+
locked.gatewayMetadata = { ...readGatewayMetadata(locked.gatewayMetadata), ...update.providerData }
|
|
785
|
+
}
|
|
786
|
+
if (update.webhookEvent) {
|
|
787
|
+
const webhookLog = readWebhookLog(locked.webhookLog)
|
|
788
|
+
webhookLog.push({
|
|
789
|
+
eventType: update.webhookEvent.eventType,
|
|
790
|
+
receivedAt: update.webhookEvent.receivedAt ?? new Date().toISOString(),
|
|
791
|
+
idempotencyKey: update.webhookEvent.idempotencyKey,
|
|
792
|
+
unifiedStatus: update.unifiedStatus,
|
|
793
|
+
processed: update.webhookEvent.processed,
|
|
794
|
+
})
|
|
795
|
+
locked.webhookLog = webhookLog
|
|
796
|
+
}
|
|
797
|
+
locked.lastWebhookAt = new Date()
|
|
798
|
+
await tx.flush()
|
|
799
|
+
return {
|
|
800
|
+
transaction: locked,
|
|
801
|
+
previousStatus: currentStatus,
|
|
802
|
+
impliedCaptureStatus: applies ? implied : null,
|
|
803
|
+
shouldApplyStatus: applies,
|
|
804
|
+
}
|
|
805
|
+
})
|
|
796
806
|
if (shouldApplyStatus) {
|
|
797
|
-
|
|
807
|
+
const eventPayload = {
|
|
798
808
|
transactionId: transaction.id,
|
|
799
809
|
paymentId: transaction.paymentId,
|
|
800
810
|
providerKey: transaction.providerKey,
|
|
801
|
-
previousStatus,
|
|
802
811
|
organizationId: transaction.organizationId,
|
|
803
812
|
tenantId: transaction.tenantId,
|
|
813
|
+
}
|
|
814
|
+
if (impliedCaptureStatus) {
|
|
815
|
+
await emitStatusEvent(impliedCaptureStatus, { ...eventPayload, previousStatus })
|
|
816
|
+
}
|
|
817
|
+
await emitStatusEvent(update.unifiedStatus, {
|
|
818
|
+
...eventPayload,
|
|
819
|
+
previousStatus: impliedCaptureStatus ?? previousStatus,
|
|
804
820
|
})
|
|
805
821
|
}
|
|
806
822
|
await writeTransactionLog(
|
|
@@ -811,6 +827,7 @@ export function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {
|
|
|
811
827
|
shouldApplyStatus ? 'Payment status synchronized from webhook' : 'Webhook received with no status transition',
|
|
812
828
|
{
|
|
813
829
|
previousStatus,
|
|
830
|
+
...(impliedCaptureStatus ? { impliedStatus: impliedCaptureStatus } : {}),
|
|
814
831
|
nextStatus: update.unifiedStatus,
|
|
815
832
|
providerStatus: update.providerStatus ?? null,
|
|
816
833
|
eventType: update.webhookEvent?.eventType ?? null,
|