@open-mercato/core 0.10.1-develop.7954.1.48da4a7d13 → 0.10.1-develop.7972.1.03f813eaaf
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/modules/auth/api/logout.js +3 -3
- package/dist/modules/auth/api/logout.js.map +2 -2
- package/dist/modules/auth/commands/roles.js +42 -31
- package/dist/modules/auth/commands/roles.js.map +2 -2
- package/dist/modules/communication_channels/setup.js +49 -45
- package/dist/modules/communication_channels/setup.js.map +2 -2
- package/dist/modules/customer_accounts/services/domainMappingService.js +55 -29
- package/dist/modules/customer_accounts/services/domainMappingService.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesSection.js +8 -3
- package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityCard.js +3 -3
- package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityHistorySection.js +19 -7
- package/dist/modules/customers/components/detail/ActivityHistorySection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimeline.js +5 -5
- package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
- package/dist/modules/directory/subscribers/seedOrganizationDefaults.js +67 -0
- package/dist/modules/directory/subscribers/seedOrganizationDefaults.js.map +7 -0
- package/dist/modules/ecommerce/api/storefront/categories/[slug]/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/categories/[slug]/route.js.map +2 -2
- package/dist/modules/ecommerce/api/storefront/categories/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/categories/route.js.map +2 -2
- package/dist/modules/ecommerce/api/storefront/context/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/context/route.js.map +2 -2
- package/dist/modules/ecommerce/api/storefront/products/[idOrHandle]/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/products/[idOrHandle]/route.js.map +2 -2
- package/dist/modules/ecommerce/api/storefront/products/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/products/route.js.map +2 -2
- package/dist/modules/ecommerce/api/storefront/search/suggest/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/search/suggest/route.js.map +2 -2
- package/dist/modules/ecommerce/lib/cacheInvalidation.js +3 -0
- package/dist/modules/ecommerce/lib/cacheInvalidation.js.map +2 -2
- package/dist/modules/ecommerce/lib/cacheKeys.js +4 -0
- package/dist/modules/ecommerce/lib/cacheKeys.js.map +2 -2
- package/dist/modules/ecommerce/lib/storeContext.js +36 -12
- package/dist/modules/ecommerce/lib/storeContext.js.map +2 -2
- package/dist/modules/ecommerce/subscribers/sales-channel-cache-invalidation.js +14 -0
- package/dist/modules/ecommerce/subscribers/sales-channel-cache-invalidation.js.map +7 -0
- package/dist/modules/integrations/setup.js +34 -24
- package/dist/modules/integrations/setup.js.map +2 -2
- package/dist/modules/messages/components/MessagesInboxPageClient.js +1 -1
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/MainMessageHeader.js +3 -3
- package/dist/modules/messages/components/message-detail/panels/MainMessageHeader.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/body-panel.js +2 -2
- package/dist/modules/messages/components/message-detail/panels/body-panel.js.map +2 -2
- package/dist/modules/payment_gateways/setup.js +3 -0
- package/dist/modules/payment_gateways/setup.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js +30 -11
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js.map +3 -3
- package/dist/modules/warranty_claims/setup.js +34 -30
- package/dist/modules/warranty_claims/setup.js.map +2 -2
- package/dist/modules/workflows/setup.js +7 -0
- package/dist/modules/workflows/setup.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/logout.ts +3 -3
- package/src/modules/auth/commands/roles.ts +43 -32
- package/src/modules/communication_channels/setup.ts +59 -53
- package/src/modules/customer_accounts/services/domainMappingService.ts +57 -29
- package/src/modules/customers/components/detail/ActivitiesSection.tsx +8 -3
- package/src/modules/customers/components/detail/ActivityCard.tsx +3 -3
- package/src/modules/customers/components/detail/ActivityHistorySection.tsx +20 -8
- package/src/modules/customers/components/detail/ActivityTimeline.tsx +7 -7
- 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/ko.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/directory/subscribers/seedOrganizationDefaults.ts +84 -0
- package/src/modules/ecommerce/api/storefront/categories/[slug]/route.ts +1 -1
- package/src/modules/ecommerce/api/storefront/categories/route.ts +1 -1
- package/src/modules/ecommerce/api/storefront/context/route.ts +1 -1
- package/src/modules/ecommerce/api/storefront/products/[idOrHandle]/route.ts +1 -1
- package/src/modules/ecommerce/api/storefront/products/route.ts +1 -1
- package/src/modules/ecommerce/api/storefront/search/suggest/route.ts +1 -1
- package/src/modules/ecommerce/lib/cacheInvalidation.ts +4 -0
- package/src/modules/ecommerce/lib/cacheKeys.ts +4 -0
- package/src/modules/ecommerce/lib/storeContext.ts +56 -10
- package/src/modules/ecommerce/subscribers/sales-channel-cache-invalidation.ts +12 -0
- package/src/modules/integrations/setup.ts +41 -24
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +1 -1
- package/src/modules/messages/components/message-detail/panels/MainMessageHeader.tsx +3 -3
- package/src/modules/messages/components/message-detail/panels/body-panel.tsx +2 -2
- package/src/modules/payment_gateways/setup.ts +4 -0
- package/src/modules/warranty_claims/backend/warranty_claims/[id]/page.tsx +55 -28
- package/src/modules/warranty_claims/setup.ts +40 -30
- package/src/modules/workflows/setup.ts +8 -0
|
@@ -42,6 +42,36 @@ function stableScheduleUuid(stableKey) {
|
|
|
42
42
|
function stableSlaSweepScheduleId(organizationId) {
|
|
43
43
|
return stableScheduleUuid(`warranty_claims:sla-sweep:${organizationId}`);
|
|
44
44
|
}
|
|
45
|
+
async function ensureSlaSweepSchedule(ctx) {
|
|
46
|
+
const cradle = ctx.container;
|
|
47
|
+
if (typeof cradle.hasRegistration !== "function" || !cradle.hasRegistration("schedulerService")) return;
|
|
48
|
+
const schedulerService = ctx.container.resolve("schedulerService");
|
|
49
|
+
try {
|
|
50
|
+
await schedulerService.register({
|
|
51
|
+
id: stableSlaSweepScheduleId(ctx.organizationId),
|
|
52
|
+
name: "Warranty claims SLA sweep",
|
|
53
|
+
description: "Enqueues tenant-scoped warranty claim SLA escalation sweeps every 900 seconds.",
|
|
54
|
+
scopeType: "organization",
|
|
55
|
+
organizationId: ctx.organizationId,
|
|
56
|
+
tenantId: ctx.tenantId,
|
|
57
|
+
scheduleType: "interval",
|
|
58
|
+
scheduleValue: "900s",
|
|
59
|
+
timezone: "UTC",
|
|
60
|
+
targetType: "queue",
|
|
61
|
+
targetQueue: "warranty_claims.sla_sweep",
|
|
62
|
+
targetPayload: {
|
|
63
|
+
scope: { tenantId: ctx.tenantId, organizationId: ctx.organizationId }
|
|
64
|
+
},
|
|
65
|
+
sourceType: "module",
|
|
66
|
+
sourceModule: "warranty_claims",
|
|
67
|
+
isEnabled: true
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
logger.warn("[warranty_claims] Failed to register SLA sweep schedule", {
|
|
71
|
+
error: error instanceof Error ? error.message : error
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
45
75
|
const setup = {
|
|
46
76
|
defaultRoleFeatures: {
|
|
47
77
|
admin: adminFeatures,
|
|
@@ -81,36 +111,10 @@ const setup = {
|
|
|
81
111
|
},
|
|
82
112
|
async seedDefaults({ em, tenantId, organizationId, container }) {
|
|
83
113
|
await seedWarrantyClaimDictionaries(em, { tenantId, organizationId });
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
const schedulerService = container.resolve("schedulerService");
|
|
89
|
-
try {
|
|
90
|
-
await schedulerService.register({
|
|
91
|
-
id: stableSlaSweepScheduleId(organizationId),
|
|
92
|
-
name: "Warranty claims SLA sweep",
|
|
93
|
-
description: "Enqueues tenant-scoped warranty claim SLA escalation sweeps every 900 seconds.",
|
|
94
|
-
scopeType: "organization",
|
|
95
|
-
organizationId,
|
|
96
|
-
tenantId,
|
|
97
|
-
scheduleType: "interval",
|
|
98
|
-
scheduleValue: "900s",
|
|
99
|
-
timezone: "UTC",
|
|
100
|
-
targetType: "queue",
|
|
101
|
-
targetQueue: "warranty_claims.sla_sweep",
|
|
102
|
-
targetPayload: {
|
|
103
|
-
scope: { tenantId, organizationId }
|
|
104
|
-
},
|
|
105
|
-
sourceType: "module",
|
|
106
|
-
sourceModule: "warranty_claims",
|
|
107
|
-
isEnabled: true
|
|
108
|
-
});
|
|
109
|
-
} catch (error) {
|
|
110
|
-
logger.warn("[warranty_claims] Failed to register SLA sweep schedule", {
|
|
111
|
-
error: error instanceof Error ? error.message : error
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
+
await ensureSlaSweepSchedule({ container, tenantId, organizationId });
|
|
115
|
+
},
|
|
116
|
+
async onOrganizationCreated({ container, tenantId, organizationId }) {
|
|
117
|
+
await ensureSlaSweepSchedule({ container, tenantId, organizationId });
|
|
114
118
|
}
|
|
115
119
|
};
|
|
116
120
|
var setup_default = setup;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/warranty_claims/setup.ts"],
|
|
4
|
-
"sourcesContent": ["import { createHash } from 'node:crypto'\nimport type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport { CLAIM_TYPES } from './data/validators'\nimport { WarrantyClaimSequence } from './data/entities'\nimport { seedWarrantyClaimDictionaries } from './lib/dictionaries'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('warranty_claims')\n\ntype SchedulerServiceLike = {\n register: (registration: {\n id: string\n name: string\n scopeType: 'system' | 'organization' | 'tenant'\n organizationId?: string\n tenantId?: string\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone?: string\n targetType: 'queue' | 'command'\n targetQueue?: string\n targetPayload?: unknown\n sourceType?: 'user' | 'module'\n sourceModule?: string\n isEnabled?: boolean\n description?: string\n }) => Promise<void>\n}\n\n// Cross-module READ grants the desk genuinely needs: the intake order picker and\n// the product picker call the sales and catalog list APIs directly. Assignee names\n// are resolved through this module's own `warranty_claims.claim.view`-gated\n// `/api/warranty_claims/assignees` endpoint, so no `auth.*` grant is required here \u2014\n// granting one would hand every role that can see a claim the tenant's full user\n// directory, including emails and role assignments.\nconst connectedIntakeFeatures = ['sales.orders.view', 'catalog.products.view']\n\nconst adminFeatures = [\n 'warranty_claims.*',\n 'warranty_claims.claim.view',\n 'warranty_claims.claim.create',\n 'warranty_claims.claim.manage',\n 'warranty_claims.claim.delete',\n 'warranty_claims.settings.manage',\n 'warranty_claims.external.submit',\n 'warranty_claims.external.view',\n 'warranty_claims.registration.view',\n 'warranty_claims.registration.manage',\n 'warranty_claims.vendor_policy.manage',\n 'warranty_claims.troubleshooting.manage',\n 'warranty_claims.receiving.manage',\n ...connectedIntakeFeatures,\n]\n\nconst ownerFeatures = [\n 'warranty_claims.*',\n 'warranty_claims.claim.view',\n 'warranty_claims.claim.create',\n 'warranty_claims.claim.manage',\n 'warranty_claims.claim.delete',\n 'warranty_claims.settings.manage',\n 'warranty_claims.registration.view',\n 'warranty_claims.registration.manage',\n 'warranty_claims.vendor_policy.manage',\n 'warranty_claims.troubleshooting.manage',\n 'warranty_claims.receiving.manage',\n ...connectedIntakeFeatures,\n]\n\nfunction stableScheduleUuid(stableKey: string): string {\n const hex = createHash('sha256').update(stableKey).digest('hex')\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`\n}\n\nfunction stableSlaSweepScheduleId(organizationId: string): string {\n return stableScheduleUuid(`warranty_claims:sla-sweep:${organizationId}`)\n}\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n admin: adminFeatures,\n owner: ownerFeatures,\n employee: [\n 'warranty_claims.claim.view',\n 'warranty_claims.claim.create',\n 'warranty_claims.claim.manage',\n 'warranty_claims.registration.view',\n 'warranty_claims.registration.manage',\n 'warranty_claims.receiving.manage',\n ...connectedIntakeFeatures,\n ],\n },\n\n async onTenantCreated({ em, tenantId, organizationId }) {\n const now = new Date()\n for (const claimType of CLAIM_TYPES) {\n const sequence = await em.findOne(WarrantyClaimSequence, {\n tenantId,\n organizationId,\n claimType,\n })\n if (!sequence) {\n em.persist(\n em.create(WarrantyClaimSequence, {\n tenantId,\n organizationId,\n claimType,\n nextNumber: 1,\n createdAt: now,\n updatedAt: now,\n })\n )\n }\n }\n await em.flush()\n },\n\n async seedDefaults({ em, tenantId, organizationId, container }) {\n await seedWarrantyClaimDictionaries(em, { tenantId, organizationId })\n
|
|
5
|
-
"mappings": "AAAA,SAAS,kBAAkB;AAE3B,SAAS,mBAAmB;AAC5B,SAAS,6BAA6B;AACtC,SAAS,qCAAqC;AAC9C,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,iBAAiB;AA4B7C,MAAM,0BAA0B,CAAC,qBAAqB,uBAAuB;AAE7E,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL;AAEA,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL;AAEA,SAAS,mBAAmB,WAA2B;AACrD,QAAM,MAAM,WAAW,QAAQ,EAAE,OAAO,SAAS,EAAE,OAAO,KAAK;AAC/D,SAAO,GAAG,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,MAAM,GAAG,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC;AAC9G;AAEA,SAAS,yBAAyB,gBAAgC;AAChE,SAAO,mBAAmB,6BAA6B,cAAc,EAAE;AACzE;AAEO,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA,IACnB,OAAO;AAAA,IACP,OAAO;AAAA,IACP,UAAU;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,GAAG;AAAA,IACL;AAAA,EACF;AAAA,EAEA,MAAM,gBAAgB,EAAE,IAAI,UAAU,eAAe,GAAG;AACtD,UAAM,MAAM,oBAAI,KAAK;AACrB,eAAW,aAAa,aAAa;AACnC,YAAM,WAAW,MAAM,GAAG,QAAQ,uBAAuB;AAAA,QACvD;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AACD,UAAI,CAAC,UAAU;AACb,WAAG;AAAA,UACD,GAAG,OAAO,uBAAuB;AAAA,YAC/B;AAAA,YACA;AAAA,YACA;AAAA,YACA,YAAY;AAAA,YACZ,WAAW;AAAA,YACX,WAAW;AAAA,UACb,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AACA,UAAM,GAAG,MAAM;AAAA,EACjB;AAAA,EAEA,MAAM,aAAa,EAAE,IAAI,UAAU,gBAAgB,UAAU,GAAG;AAC9D,UAAM,8BAA8B,IAAI,EAAE,UAAU,eAAe,CAAC;AACpE,UAAM,
|
|
4
|
+
"sourcesContent": ["import { createHash } from 'node:crypto'\nimport type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport { CLAIM_TYPES } from './data/validators'\nimport { WarrantyClaimSequence } from './data/entities'\nimport { seedWarrantyClaimDictionaries } from './lib/dictionaries'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('warranty_claims')\n\ntype SchedulerServiceLike = {\n register: (registration: {\n id: string\n name: string\n scopeType: 'system' | 'organization' | 'tenant'\n organizationId?: string\n tenantId?: string\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone?: string\n targetType: 'queue' | 'command'\n targetQueue?: string\n targetPayload?: unknown\n sourceType?: 'user' | 'module'\n sourceModule?: string\n isEnabled?: boolean\n description?: string\n }) => Promise<void>\n}\n\n// Cross-module READ grants the desk genuinely needs: the intake order picker and\n// the product picker call the sales and catalog list APIs directly. Assignee names\n// are resolved through this module's own `warranty_claims.claim.view`-gated\n// `/api/warranty_claims/assignees` endpoint, so no `auth.*` grant is required here \u2014\n// granting one would hand every role that can see a claim the tenant's full user\n// directory, including emails and role assignments.\nconst connectedIntakeFeatures = ['sales.orders.view', 'catalog.products.view']\n\nconst adminFeatures = [\n 'warranty_claims.*',\n 'warranty_claims.claim.view',\n 'warranty_claims.claim.create',\n 'warranty_claims.claim.manage',\n 'warranty_claims.claim.delete',\n 'warranty_claims.settings.manage',\n 'warranty_claims.external.submit',\n 'warranty_claims.external.view',\n 'warranty_claims.registration.view',\n 'warranty_claims.registration.manage',\n 'warranty_claims.vendor_policy.manage',\n 'warranty_claims.troubleshooting.manage',\n 'warranty_claims.receiving.manage',\n ...connectedIntakeFeatures,\n]\n\nconst ownerFeatures = [\n 'warranty_claims.*',\n 'warranty_claims.claim.view',\n 'warranty_claims.claim.create',\n 'warranty_claims.claim.manage',\n 'warranty_claims.claim.delete',\n 'warranty_claims.settings.manage',\n 'warranty_claims.registration.view',\n 'warranty_claims.registration.manage',\n 'warranty_claims.vendor_policy.manage',\n 'warranty_claims.troubleshooting.manage',\n 'warranty_claims.receiving.manage',\n ...connectedIntakeFeatures,\n]\n\nfunction stableScheduleUuid(stableKey: string): string {\n const hex = createHash('sha256').update(stableKey).digest('hex')\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`\n}\n\nfunction stableSlaSweepScheduleId(organizationId: string): string {\n return stableScheduleUuid(`warranty_claims:sla-sweep:${organizationId}`)\n}\n\nasync function ensureSlaSweepSchedule(ctx: {\n container: import('awilix').AwilixContainer\n tenantId: string\n organizationId: string\n}): Promise<void> {\n const cradle = ctx.container as { hasRegistration?: (name: string) => boolean }\n if (typeof cradle.hasRegistration !== 'function' || !cradle.hasRegistration('schedulerService')) return\n\n const schedulerService = ctx.container.resolve('schedulerService') as SchedulerServiceLike\n try {\n await schedulerService.register({\n id: stableSlaSweepScheduleId(ctx.organizationId),\n name: 'Warranty claims SLA sweep',\n description: 'Enqueues tenant-scoped warranty claim SLA escalation sweeps every 900 seconds.',\n scopeType: 'organization',\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n scheduleType: 'interval',\n scheduleValue: '900s',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'warranty_claims.sla_sweep',\n targetPayload: {\n scope: { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n },\n sourceType: 'module',\n sourceModule: 'warranty_claims',\n isEnabled: true,\n })\n } catch (error) {\n logger.warn('[warranty_claims] Failed to register SLA sweep schedule', {\n error: error instanceof Error ? error.message : error,\n })\n }\n}\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n admin: adminFeatures,\n owner: ownerFeatures,\n employee: [\n 'warranty_claims.claim.view',\n 'warranty_claims.claim.create',\n 'warranty_claims.claim.manage',\n 'warranty_claims.registration.view',\n 'warranty_claims.registration.manage',\n 'warranty_claims.receiving.manage',\n ...connectedIntakeFeatures,\n ],\n },\n\n async onTenantCreated({ em, tenantId, organizationId }) {\n const now = new Date()\n for (const claimType of CLAIM_TYPES) {\n const sequence = await em.findOne(WarrantyClaimSequence, {\n tenantId,\n organizationId,\n claimType,\n })\n if (!sequence) {\n em.persist(\n em.create(WarrantyClaimSequence, {\n tenantId,\n organizationId,\n claimType,\n nextNumber: 1,\n createdAt: now,\n updatedAt: now,\n })\n )\n }\n }\n await em.flush()\n },\n\n async seedDefaults({ em, tenantId, organizationId, container }) {\n await seedWarrantyClaimDictionaries(em, { tenantId, organizationId })\n await ensureSlaSweepSchedule({ container, tenantId, organizationId })\n },\n\n async onOrganizationCreated({ container, tenantId, organizationId }) {\n await ensureSlaSweepSchedule({ container, tenantId, organizationId })\n },\n}\n\nexport default setup\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,kBAAkB;AAE3B,SAAS,mBAAmB;AAC5B,SAAS,6BAA6B;AACtC,SAAS,qCAAqC;AAC9C,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,iBAAiB;AA4B7C,MAAM,0BAA0B,CAAC,qBAAqB,uBAAuB;AAE7E,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL;AAEA,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL;AAEA,SAAS,mBAAmB,WAA2B;AACrD,QAAM,MAAM,WAAW,QAAQ,EAAE,OAAO,SAAS,EAAE,OAAO,KAAK;AAC/D,SAAO,GAAG,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,MAAM,GAAG,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC;AAC9G;AAEA,SAAS,yBAAyB,gBAAgC;AAChE,SAAO,mBAAmB,6BAA6B,cAAc,EAAE;AACzE;AAEA,eAAe,uBAAuB,KAIpB;AAChB,QAAM,SAAS,IAAI;AACnB,MAAI,OAAO,OAAO,oBAAoB,cAAc,CAAC,OAAO,gBAAgB,kBAAkB,EAAG;AAEjG,QAAM,mBAAmB,IAAI,UAAU,QAAQ,kBAAkB;AACjE,MAAI;AACF,UAAM,iBAAiB,SAAS;AAAA,MAC9B,IAAI,yBAAyB,IAAI,cAAc;AAAA,MAC/C,MAAM;AAAA,MACN,aAAa;AAAA,MACb,WAAW;AAAA,MACX,gBAAgB,IAAI;AAAA,MACpB,UAAU,IAAI;AAAA,MACd,cAAc;AAAA,MACd,eAAe;AAAA,MACf,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,eAAe;AAAA,QACb,OAAO,EAAE,UAAU,IAAI,UAAU,gBAAgB,IAAI,eAAe;AAAA,MACtE;AAAA,MACA,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,WAAW;AAAA,IACb,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,KAAK,2DAA2D;AAAA,MACrE,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAClD,CAAC;AAAA,EACH;AACF;AAEO,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA,IACnB,OAAO;AAAA,IACP,OAAO;AAAA,IACP,UAAU;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,GAAG;AAAA,IACL;AAAA,EACF;AAAA,EAEA,MAAM,gBAAgB,EAAE,IAAI,UAAU,eAAe,GAAG;AACtD,UAAM,MAAM,oBAAI,KAAK;AACrB,eAAW,aAAa,aAAa;AACnC,YAAM,WAAW,MAAM,GAAG,QAAQ,uBAAuB;AAAA,QACvD;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AACD,UAAI,CAAC,UAAU;AACb,WAAG;AAAA,UACD,GAAG,OAAO,uBAAuB;AAAA,YAC/B;AAAA,YACA;AAAA,YACA;AAAA,YACA,YAAY;AAAA,YACZ,WAAW;AAAA,YACX,WAAW;AAAA,UACb,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AACA,UAAM,GAAG,MAAM;AAAA,EACjB;AAAA,EAEA,MAAM,aAAa,EAAE,IAAI,UAAU,gBAAgB,UAAU,GAAG;AAC9D,UAAM,8BAA8B,IAAI,EAAE,UAAU,eAAe,CAAC;AACpE,UAAM,uBAAuB,EAAE,WAAW,UAAU,eAAe,CAAC;AAAA,EACtE;AAAA,EAEA,MAAM,sBAAsB,EAAE,WAAW,UAAU,eAAe,GAAG;AACnE,UAAM,uBAAuB,EAAE,WAAW,UAAU,eAAe,CAAC;AAAA,EACtE;AACF;AAEA,IAAO,gBAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -45,6 +45,13 @@ const setup = {
|
|
|
45
45
|
organizationId: ctx.organizationId
|
|
46
46
|
});
|
|
47
47
|
},
|
|
48
|
+
onOrganizationCreated: async (ctx) => {
|
|
49
|
+
await ensureMetricRollupSchedule({
|
|
50
|
+
container: ctx.container,
|
|
51
|
+
tenantId: ctx.tenantId,
|
|
52
|
+
organizationId: ctx.organizationId
|
|
53
|
+
});
|
|
54
|
+
},
|
|
48
55
|
/**
|
|
49
56
|
* Portal grants for the §6.4 portal task surface.
|
|
50
57
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/workflows/setup.ts"],
|
|
4
|
-
"sourcesContent": ["import { createHash } from 'node:crypto'\nimport type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport { resolveBusinessRuleDiscoveryCache } from '@open-mercato/core/modules/business_rules/lib/rule-engine'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { seedExampleWorkflows } from './lib/seeds'\nimport { WORKFLOW_DEFINITION_METRIC_ROLLUP_INTERVAL } from './lib/metrics/rollup-schedule'\n\nconst logger = createLogger('workflows')\n\ntype SchedulerServiceLike = {\n register: (registration: {\n id: string\n name: string\n description?: string\n scopeType: 'system' | 'organization' | 'tenant'\n organizationId?: string\n tenantId?: string\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone?: string\n targetType: 'queue'\n targetQueue: string\n targetPayload?: Record<string, unknown>\n sourceType?: string\n sourceModule?: string\n isEnabled?: boolean\n }) => Promise<void>\n}\n\n/**\n * `scheduled_jobs.id` is a uuid column, so a module-owned schedule's stable\n * registration key must be hashed into a uuid rather than used verbatim \u2014 this\n * keeps `schedulerService.register()` an idempotent upsert across re-runs of\n * `seedDefaults` instead of trying to insert a raw string into the uuid PK.\n */\nfunction stableScheduleUuid(stableKey: string): string {\n const hex = createHash('sha256').update(stableKey).digest('hex')\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`\n}\n\n/**\n * Register the per-organization KPI rollup tick (spec \u00A78.5).\n *\n * Per ORGANIZATION rather than one system-wide tick that enumerates tenants:\n * the pass would otherwise need an unscoped read to discover which\n * organizations exist, and this module's rule is that every query carries both\n * scope columns. One schedule row per organization is the price of that.\n *\n * Best-effort in both directions: `@open-mercato/scheduler` is an optional peer\n * (no registration in the container \u21D2 silent no-op), and a failure to register\n * must never abort tenant setup \u2014 the read API live-computes when no rollup row\n * exists, so a missing schedule degrades freshness, not correctness.\n */\nasync function ensureMetricRollupSchedule(ctx: {\n container: { resolve: (name: string) => unknown; hasRegistration?: (name: string) => boolean }\n tenantId: string\n organizationId: string\n}): Promise<void> {\n const cradle = ctx.container as { hasRegistration?: (name: string) => boolean }\n if (typeof cradle.hasRegistration !== 'function' || !cradle.hasRegistration('schedulerService')) return\n try {\n const schedulerService = ctx.container.resolve('schedulerService') as SchedulerServiceLike\n await schedulerService.register({\n id: stableScheduleUuid(`workflows:definition-metric-rollup:${ctx.organizationId}`),\n name: 'Workflow definition metric rollup',\n description:\n 'Recomputes per-definition run, success-rate, duration-percentile and task-SLA KPIs into workflow_definition_metric_rollups.',\n scopeType: 'organization',\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n scheduleType: 'interval',\n scheduleValue: WORKFLOW_DEFINITION_METRIC_ROLLUP_INTERVAL,\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'workflow-definition-metric-rollup',\n targetPayload: { scope: { tenantId: ctx.tenantId, organizationId: ctx.organizationId } },\n sourceType: 'module',\n sourceModule: 'workflows',\n isEnabled: true,\n })\n } catch (error) {\n logger.warn('[internal] workflows: failed to register the metric-rollup schedule', { err: error })\n }\n}\n\nexport const setup: ModuleSetupConfig = {\n seedDefaults: async (ctx) => {\n const scope = { tenantId: ctx.tenantId, organizationId: ctx.organizationId }\n const cache = resolveBusinessRuleDiscoveryCache(ctx.container.resolve.bind(ctx.container))\n await seedExampleWorkflows(ctx.em, scope, { cache })\n await ensureMetricRollupSchedule({\n container: ctx.container as never,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n },\n\n /**\n * Portal grants for the \u00A76.4 portal task surface.\n *\n * Declared HERE rather than in `customer_accounts` because a module ships its\n * own portal features the same way it ships its own backoffice ones \u2014\n * `syncDefaultCustomerRoleAcls` collects `defaultCustomerRoleFeatures` from\n * every enabled module and merges them into the seeded customer roles.\n * Workflows is the first shipped consumer of that seam.\n *\n * `portal_admin` is deliberately absent: it already carries `portal.*`, and a\n * wildcard is exactly what the portal branch of the predicate refuses to treat\n * as ownership. Granting it explicitly would suggest the wildcard means\n * something it does not.\n *\n * Two rollout caveats, neither a bug:\n * - The merge is additive into EXISTING roles and runs during tenant setup,\n * so an already-provisioned tenant picks these up only after\n * `mercato customer_accounts sync-customer-role-acls`.\n * - `CustomerRbacService` caches ACLs for 5 minutes, so a fresh grant is not\n * immediately visible to a signed-in portal user.\n */\n defaultCustomerRoleFeatures: {\n buyer: ['portal.tasks.view', 'portal.tasks.complete'],\n viewer: ['portal.tasks.view'],\n },\n\n defaultRoleFeatures: {\n // `workflows.*` already grants every concrete id, including the spec \u00A78.4\n // recovery features `workflows.instances.rerun_step` and\n // `workflows.instances.bulk_ops`, the spec \u00A78.5 ops feature\n // `workflows.metrics.view`, and the execution-identity feature\n // `workflows.definitions.grant_features` (safe to hold under a wildcard: a\n // grant can never exceed the granting user's OWN current features, so an\n // admin can only delegate powers they already have). Existing tenants pick\n // up newly declared features only after `yarn mercato auth sync-role-acls`.\n admin: ['workflows.*'],\n employee: [\n 'workflows.view',\n 'workflows.view_tasks',\n 'workflows.tasks.view',\n 'workflows.tasks.claim',\n 'workflows.tasks.complete',\n 'workflows.instances.view',\n ],\n },\n}\n\nexport default setup\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,kBAAkB;AAE3B,SAAS,yCAAyC;AAClD,SAAS,oBAAoB;AAC7B,SAAS,4BAA4B;AACrC,SAAS,kDAAkD;AAE3D,MAAM,SAAS,aAAa,WAAW;AA4BvC,SAAS,mBAAmB,WAA2B;AACrD,QAAM,MAAM,WAAW,QAAQ,EAAE,OAAO,SAAS,EAAE,OAAO,KAAK;AAC/D,SAAO,GAAG,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,MAAM,GAAG,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC;AAC9G;AAeA,eAAe,2BAA2B,KAIxB;AAChB,QAAM,SAAS,IAAI;AACnB,MAAI,OAAO,OAAO,oBAAoB,cAAc,CAAC,OAAO,gBAAgB,kBAAkB,EAAG;AACjG,MAAI;AACF,UAAM,mBAAmB,IAAI,UAAU,QAAQ,kBAAkB;AACjE,UAAM,iBAAiB,SAAS;AAAA,MAC9B,IAAI,mBAAmB,sCAAsC,IAAI,cAAc,EAAE;AAAA,MACjF,MAAM;AAAA,MACN,aACE;AAAA,MACF,WAAW;AAAA,MACX,gBAAgB,IAAI;AAAA,MACpB,UAAU,IAAI;AAAA,MACd,cAAc;AAAA,MACd,eAAe;AAAA,MACf,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,eAAe,EAAE,OAAO,EAAE,UAAU,IAAI,UAAU,gBAAgB,IAAI,eAAe,EAAE;AAAA,MACvF,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,WAAW;AAAA,IACb,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,KAAK,uEAAuE,EAAE,KAAK,MAAM,CAAC;AAAA,EACnG;AACF;AAEO,MAAM,QAA2B;AAAA,EACtC,cAAc,OAAO,QAAQ;AAC3B,UAAM,QAAQ,EAAE,UAAU,IAAI,UAAU,gBAAgB,IAAI,eAAe;AAC3E,UAAM,QAAQ,kCAAkC,IAAI,UAAU,QAAQ,KAAK,IAAI,SAAS,CAAC;AACzF,UAAM,qBAAqB,IAAI,IAAI,OAAO,EAAE,MAAM,CAAC;AACnD,UAAM,2BAA2B;AAAA,MAC/B,WAAW,IAAI;AAAA,MACf,UAAU,IAAI;AAAA,MACd,gBAAgB,IAAI;AAAA,IACtB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuBA,6BAA6B;AAAA,IAC3B,OAAO,CAAC,qBAAqB,uBAAuB;AAAA,IACpD,QAAQ,CAAC,mBAAmB;AAAA,EAC9B;AAAA,EAEA,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASnB,OAAO,CAAC,aAAa;AAAA,IACrB,UAAU;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,gBAAQ;",
|
|
4
|
+
"sourcesContent": ["import { createHash } from 'node:crypto'\nimport type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport { resolveBusinessRuleDiscoveryCache } from '@open-mercato/core/modules/business_rules/lib/rule-engine'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { seedExampleWorkflows } from './lib/seeds'\nimport { WORKFLOW_DEFINITION_METRIC_ROLLUP_INTERVAL } from './lib/metrics/rollup-schedule'\n\nconst logger = createLogger('workflows')\n\ntype SchedulerServiceLike = {\n register: (registration: {\n id: string\n name: string\n description?: string\n scopeType: 'system' | 'organization' | 'tenant'\n organizationId?: string\n tenantId?: string\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone?: string\n targetType: 'queue'\n targetQueue: string\n targetPayload?: Record<string, unknown>\n sourceType?: string\n sourceModule?: string\n isEnabled?: boolean\n }) => Promise<void>\n}\n\n/**\n * `scheduled_jobs.id` is a uuid column, so a module-owned schedule's stable\n * registration key must be hashed into a uuid rather than used verbatim \u2014 this\n * keeps `schedulerService.register()` an idempotent upsert across re-runs of\n * `seedDefaults` instead of trying to insert a raw string into the uuid PK.\n */\nfunction stableScheduleUuid(stableKey: string): string {\n const hex = createHash('sha256').update(stableKey).digest('hex')\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`\n}\n\n/**\n * Register the per-organization KPI rollup tick (spec \u00A78.5).\n *\n * Per ORGANIZATION rather than one system-wide tick that enumerates tenants:\n * the pass would otherwise need an unscoped read to discover which\n * organizations exist, and this module's rule is that every query carries both\n * scope columns. One schedule row per organization is the price of that.\n *\n * Best-effort in both directions: `@open-mercato/scheduler` is an optional peer\n * (no registration in the container \u21D2 silent no-op), and a failure to register\n * must never abort tenant setup \u2014 the read API live-computes when no rollup row\n * exists, so a missing schedule degrades freshness, not correctness.\n */\nasync function ensureMetricRollupSchedule(ctx: {\n container: { resolve: (name: string) => unknown; hasRegistration?: (name: string) => boolean }\n tenantId: string\n organizationId: string\n}): Promise<void> {\n const cradle = ctx.container as { hasRegistration?: (name: string) => boolean }\n if (typeof cradle.hasRegistration !== 'function' || !cradle.hasRegistration('schedulerService')) return\n try {\n const schedulerService = ctx.container.resolve('schedulerService') as SchedulerServiceLike\n await schedulerService.register({\n id: stableScheduleUuid(`workflows:definition-metric-rollup:${ctx.organizationId}`),\n name: 'Workflow definition metric rollup',\n description:\n 'Recomputes per-definition run, success-rate, duration-percentile and task-SLA KPIs into workflow_definition_metric_rollups.',\n scopeType: 'organization',\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n scheduleType: 'interval',\n scheduleValue: WORKFLOW_DEFINITION_METRIC_ROLLUP_INTERVAL,\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'workflow-definition-metric-rollup',\n targetPayload: { scope: { tenantId: ctx.tenantId, organizationId: ctx.organizationId } },\n sourceType: 'module',\n sourceModule: 'workflows',\n isEnabled: true,\n })\n } catch (error) {\n logger.warn('[internal] workflows: failed to register the metric-rollup schedule', { err: error })\n }\n}\n\nexport const setup: ModuleSetupConfig = {\n seedDefaults: async (ctx) => {\n const scope = { tenantId: ctx.tenantId, organizationId: ctx.organizationId }\n const cache = resolveBusinessRuleDiscoveryCache(ctx.container.resolve.bind(ctx.container))\n await seedExampleWorkflows(ctx.em, scope, { cache })\n await ensureMetricRollupSchedule({\n container: ctx.container as never,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n },\n\n onOrganizationCreated: async (ctx) => {\n await ensureMetricRollupSchedule({\n container: ctx.container as never,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n },\n\n /**\n * Portal grants for the \u00A76.4 portal task surface.\n *\n * Declared HERE rather than in `customer_accounts` because a module ships its\n * own portal features the same way it ships its own backoffice ones \u2014\n * `syncDefaultCustomerRoleAcls` collects `defaultCustomerRoleFeatures` from\n * every enabled module and merges them into the seeded customer roles.\n * Workflows is the first shipped consumer of that seam.\n *\n * `portal_admin` is deliberately absent: it already carries `portal.*`, and a\n * wildcard is exactly what the portal branch of the predicate refuses to treat\n * as ownership. Granting it explicitly would suggest the wildcard means\n * something it does not.\n *\n * Two rollout caveats, neither a bug:\n * - The merge is additive into EXISTING roles and runs during tenant setup,\n * so an already-provisioned tenant picks these up only after\n * `mercato customer_accounts sync-customer-role-acls`.\n * - `CustomerRbacService` caches ACLs for 5 minutes, so a fresh grant is not\n * immediately visible to a signed-in portal user.\n */\n defaultCustomerRoleFeatures: {\n buyer: ['portal.tasks.view', 'portal.tasks.complete'],\n viewer: ['portal.tasks.view'],\n },\n\n defaultRoleFeatures: {\n // `workflows.*` already grants every concrete id, including the spec \u00A78.4\n // recovery features `workflows.instances.rerun_step` and\n // `workflows.instances.bulk_ops`, the spec \u00A78.5 ops feature\n // `workflows.metrics.view`, and the execution-identity feature\n // `workflows.definitions.grant_features` (safe to hold under a wildcard: a\n // grant can never exceed the granting user's OWN current features, so an\n // admin can only delegate powers they already have). Existing tenants pick\n // up newly declared features only after `yarn mercato auth sync-role-acls`.\n admin: ['workflows.*'],\n employee: [\n 'workflows.view',\n 'workflows.view_tasks',\n 'workflows.tasks.view',\n 'workflows.tasks.claim',\n 'workflows.tasks.complete',\n 'workflows.instances.view',\n ],\n },\n}\n\nexport default setup\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,kBAAkB;AAE3B,SAAS,yCAAyC;AAClD,SAAS,oBAAoB;AAC7B,SAAS,4BAA4B;AACrC,SAAS,kDAAkD;AAE3D,MAAM,SAAS,aAAa,WAAW;AA4BvC,SAAS,mBAAmB,WAA2B;AACrD,QAAM,MAAM,WAAW,QAAQ,EAAE,OAAO,SAAS,EAAE,OAAO,KAAK;AAC/D,SAAO,GAAG,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,MAAM,GAAG,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC;AAC9G;AAeA,eAAe,2BAA2B,KAIxB;AAChB,QAAM,SAAS,IAAI;AACnB,MAAI,OAAO,OAAO,oBAAoB,cAAc,CAAC,OAAO,gBAAgB,kBAAkB,EAAG;AACjG,MAAI;AACF,UAAM,mBAAmB,IAAI,UAAU,QAAQ,kBAAkB;AACjE,UAAM,iBAAiB,SAAS;AAAA,MAC9B,IAAI,mBAAmB,sCAAsC,IAAI,cAAc,EAAE;AAAA,MACjF,MAAM;AAAA,MACN,aACE;AAAA,MACF,WAAW;AAAA,MACX,gBAAgB,IAAI;AAAA,MACpB,UAAU,IAAI;AAAA,MACd,cAAc;AAAA,MACd,eAAe;AAAA,MACf,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,eAAe,EAAE,OAAO,EAAE,UAAU,IAAI,UAAU,gBAAgB,IAAI,eAAe,EAAE;AAAA,MACvF,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,WAAW;AAAA,IACb,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,KAAK,uEAAuE,EAAE,KAAK,MAAM,CAAC;AAAA,EACnG;AACF;AAEO,MAAM,QAA2B;AAAA,EACtC,cAAc,OAAO,QAAQ;AAC3B,UAAM,QAAQ,EAAE,UAAU,IAAI,UAAU,gBAAgB,IAAI,eAAe;AAC3E,UAAM,QAAQ,kCAAkC,IAAI,UAAU,QAAQ,KAAK,IAAI,SAAS,CAAC;AACzF,UAAM,qBAAqB,IAAI,IAAI,OAAO,EAAE,MAAM,CAAC;AACnD,UAAM,2BAA2B;AAAA,MAC/B,WAAW,IAAI;AAAA,MACf,UAAU,IAAI;AAAA,MACd,gBAAgB,IAAI;AAAA,IACtB,CAAC;AAAA,EACH;AAAA,EAEA,uBAAuB,OAAO,QAAQ;AACpC,UAAM,2BAA2B;AAAA,MAC/B,WAAW,IAAI;AAAA,MACf,UAAU,IAAI;AAAA,MACd,gBAAgB,IAAI;AAAA,IACtB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuBA,6BAA6B;AAAA,IAC3B,OAAO,CAAC,qBAAqB,uBAAuB;AAAA,IACpD,QAAQ,CAAC,mBAAmB;AAAA,EAC9B;AAAA,EAEA,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASnB,OAAO,CAAC,aAAa;AAAA,IACrB,UAAU;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,gBAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.7972.1.03f813eaaf",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -266,16 +266,16 @@
|
|
|
266
266
|
"zod": "^4.6.6"
|
|
267
267
|
},
|
|
268
268
|
"peerDependencies": {
|
|
269
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
270
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
271
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
269
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7972.1.03f813eaaf",
|
|
270
|
+
"@open-mercato/shared": "0.10.1-develop.7972.1.03f813eaaf",
|
|
271
|
+
"@open-mercato/ui": "0.10.1-develop.7972.1.03f813eaaf",
|
|
272
272
|
"react": "^19.0.0",
|
|
273
273
|
"react-dom": "^19.0.0"
|
|
274
274
|
},
|
|
275
275
|
"devDependencies": {
|
|
276
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
277
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
278
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
276
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7972.1.03f813eaaf",
|
|
277
|
+
"@open-mercato/shared": "0.10.1-develop.7972.1.03f813eaaf",
|
|
278
|
+
"@open-mercato/ui": "0.10.1-develop.7972.1.03f813eaaf",
|
|
279
279
|
"@testing-library/dom": "^10.4.1",
|
|
280
280
|
"@testing-library/jest-dom": "^7.0.1",
|
|
281
281
|
"@testing-library/react": "^16.3.3",
|
|
@@ -55,7 +55,7 @@ function extractAuthTokenClaims(token: string | null): AuthTokenClaims {
|
|
|
55
55
|
export async function POST(req: Request) {
|
|
56
56
|
const sessToken = parseCookie(req, 'session_token')
|
|
57
57
|
const authToken = parseCookie(req, 'auth_token')
|
|
58
|
-
if (!sessToken && !authToken) return buildSafeRedirectResponse(req, '/login')
|
|
58
|
+
if (!sessToken && !authToken) return buildSafeRedirectResponse(req, '/login', 303)
|
|
59
59
|
const claims = extractAuthTokenClaims(authToken)
|
|
60
60
|
let sessionRevoked = false
|
|
61
61
|
if (sessToken || claims.sessionId) {
|
|
@@ -83,7 +83,7 @@ export async function POST(req: Request) {
|
|
|
83
83
|
at: new Date().toISOString(),
|
|
84
84
|
}, { persistent: true }).catch(() => undefined)
|
|
85
85
|
}
|
|
86
|
-
const res = buildSafeRedirectResponse(req, '/login')
|
|
86
|
+
const res = buildSafeRedirectResponse(req, '/login', 303)
|
|
87
87
|
res.cookies.set('auth_token', '', { path: '/', maxAge: 0 })
|
|
88
88
|
res.cookies.set('session_token', '', { path: '/', maxAge: 0 })
|
|
89
89
|
res.cookies.set('om_selected_org', '', { path: '/', maxAge: 0 })
|
|
@@ -103,7 +103,7 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
103
103
|
summary: 'Invalidate session and redirect',
|
|
104
104
|
description: 'Revokes the session identified by the `auth_token` sid claim and the `session_token` cookie, clears both cookies together with the organization-scope cookies (`om_selected_org`, `om_selected_tenant`), and redirects the browser to the login page. A valid access token is not required, so a logout submitted after the access token expired still revokes the remember-me refresh session. A request that carries neither session cookie is only redirected.',
|
|
105
105
|
responses: [
|
|
106
|
-
{ status:
|
|
106
|
+
{ status: 303, description: 'See Other: navigate to login with GET after logout', mediaType: 'text/html' },
|
|
107
107
|
],
|
|
108
108
|
},
|
|
109
109
|
},
|
|
@@ -415,41 +415,52 @@ const updateRoleCommand: CommandHandler<Record<string, unknown>, Role> = {
|
|
|
415
415
|
const de = (ctx.container.resolve('dataEngine') as DataEngine)
|
|
416
416
|
const redoLogEntry = (ctx as RoleReplayRuntimeContext)[ROLE_REDO_LOG]
|
|
417
417
|
let role: Role | null = null
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
418
|
+
try {
|
|
419
|
+
await withAtomicFlush(em, [async () => {
|
|
420
|
+
if (redoLogEntry) {
|
|
421
|
+
await authorizeRoleReplayAtMutation('update', {
|
|
422
|
+
operation: 'redo',
|
|
423
|
+
input: rawInput,
|
|
424
|
+
ctx,
|
|
425
|
+
logEntry: redoLogEntry,
|
|
426
|
+
})
|
|
427
|
+
}
|
|
428
|
+
await lockRoleWriterAuthorizationState(em, [parsed.id])
|
|
429
|
+
if (wantsTenantChange) {
|
|
430
|
+
await em.nativeDelete(RoleAcl, { role: parsed.id as unknown as Role })
|
|
431
|
+
}
|
|
432
|
+
role = await de.updateOrmEntity({
|
|
433
|
+
entity: Role,
|
|
434
|
+
where: buildScopedRoleFilter(parsed.id, scope),
|
|
435
|
+
apply: (entity) => {
|
|
436
|
+
if (parsed.name !== undefined) entity.name = parsed.name
|
|
437
|
+
if (parsed.tenantId !== undefined && scope.isSuperAdmin) entity.tenantId = parsed.tenantId
|
|
438
|
+
},
|
|
425
439
|
})
|
|
426
|
-
|
|
427
|
-
await lockRoleWriterAuthorizationState(em, [parsed.id])
|
|
428
|
-
if (wantsTenantChange) {
|
|
429
|
-
await em.nativeDelete(RoleAcl, { role: parsed.id as unknown as Role })
|
|
430
|
-
}
|
|
431
|
-
role = await de.updateOrmEntity({
|
|
432
|
-
entity: Role,
|
|
433
|
-
where: buildScopedRoleFilter(parsed.id, scope),
|
|
434
|
-
apply: (entity) => {
|
|
435
|
-
if (parsed.name !== undefined) entity.name = parsed.name
|
|
436
|
-
if (parsed.tenantId !== undefined && scope.isSuperAdmin) entity.tenantId = parsed.tenantId
|
|
437
|
-
},
|
|
438
|
-
})
|
|
439
|
-
if (!role) throw new CrudHttpError(404, { error: 'Role not found' })
|
|
440
|
+
if (!role) throw new CrudHttpError(404, { error: 'Role not found' })
|
|
440
441
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
442
|
+
await setCustomFieldsIfAny({
|
|
443
|
+
dataEngine: de,
|
|
444
|
+
entityId: E.auth.role,
|
|
445
|
+
recordId: String(role.id),
|
|
446
|
+
organizationId: null,
|
|
447
|
+
tenantId: role.tenantId ? String(role.tenantId) : null,
|
|
448
|
+
values: custom,
|
|
449
|
+
})
|
|
450
|
+
}], {
|
|
451
|
+
transaction: true,
|
|
452
|
+
label: redoLogEntry ? 'auth.roles.update.redo' : 'auth.roles.update',
|
|
448
453
|
})
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
454
|
+
} catch (error) {
|
|
455
|
+
if (!isUniqueViolation(error, 'roles_tenant_id_name_unique')) throw error
|
|
456
|
+
const { translate } = await resolveTranslations()
|
|
457
|
+
const message = translate('auth.roles.errors.nameExists', 'A role with this name already exists')
|
|
458
|
+
throw new CrudHttpError(400, {
|
|
459
|
+
error: message,
|
|
460
|
+
fieldErrors: { name: message },
|
|
461
|
+
details: [{ path: ['name'], message, code: 'duplicate', origin: 'validation' }],
|
|
462
|
+
})
|
|
463
|
+
}
|
|
453
464
|
const updatedRole = role as Role | null
|
|
454
465
|
if (!updatedRole) throw new CrudHttpError(404, { error: 'Role not found' })
|
|
455
466
|
|
|
@@ -71,6 +71,60 @@ function stablePollTickScheduleId(organizationId: string): string {
|
|
|
71
71
|
return stableScheduleUuid(`communication_channels:poll-tick:${organizationId}`)
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
async function ensureCommunicationChannelSchedules(ctx: {
|
|
75
|
+
container: import('awilix').AwilixContainer
|
|
76
|
+
tenantId: string
|
|
77
|
+
organizationId: string
|
|
78
|
+
}): Promise<void> {
|
|
79
|
+
const cradle = ctx.container as { hasRegistration?: (name: string) => boolean }
|
|
80
|
+
if (typeof cradle.hasRegistration !== 'function' || !cradle.hasRegistration('schedulerService')) return
|
|
81
|
+
|
|
82
|
+
const schedulerService = ctx.container.resolve('schedulerService') as SchedulerServiceLike
|
|
83
|
+
try {
|
|
84
|
+
await schedulerService.register({
|
|
85
|
+
id: stablePollTickScheduleId(ctx.organizationId),
|
|
86
|
+
name: 'Communication channels poll tick',
|
|
87
|
+
description:
|
|
88
|
+
`Enumerates active polling channels every ${POLL_TICK_INTERVAL_SECONDS}s and enqueues per-channel poll jobs.`,
|
|
89
|
+
scopeType: 'organization',
|
|
90
|
+
organizationId: ctx.organizationId,
|
|
91
|
+
tenantId: ctx.tenantId,
|
|
92
|
+
scheduleType: 'interval',
|
|
93
|
+
scheduleValue: `${POLL_TICK_INTERVAL_SECONDS}s`,
|
|
94
|
+
timezone: 'UTC',
|
|
95
|
+
targetType: 'queue',
|
|
96
|
+
targetQueue: COMMUNICATION_CHANNELS_QUEUES.pollTick,
|
|
97
|
+
targetPayload: {
|
|
98
|
+
scope: { tenantId: ctx.tenantId, organizationId: ctx.organizationId },
|
|
99
|
+
},
|
|
100
|
+
sourceType: 'module',
|
|
101
|
+
sourceModule: 'communication_channels',
|
|
102
|
+
isEnabled: true,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
await schedulerService.register({
|
|
106
|
+
id: stableScheduleUuid(`communication_channels:gmail-renew-watch:${ctx.organizationId}`),
|
|
107
|
+
name: 'Gmail watch renewal',
|
|
108
|
+
description:
|
|
109
|
+
'Daily 04:00 UTC. Re-issues gmail.users.watch for channels within OM_PUSH_RENEWAL_GMAIL_LEAD_HOURS of expiry.',
|
|
110
|
+
scopeType: 'organization',
|
|
111
|
+
organizationId: ctx.organizationId,
|
|
112
|
+
tenantId: ctx.tenantId,
|
|
113
|
+
scheduleType: 'cron',
|
|
114
|
+
scheduleValue: '0 4 * * *',
|
|
115
|
+
timezone: 'UTC',
|
|
116
|
+
targetType: 'queue',
|
|
117
|
+
targetQueue: COMMUNICATION_CHANNELS_QUEUES.gmailRenewWatch,
|
|
118
|
+
targetPayload: { scope: { tenantId: ctx.tenantId, organizationId: ctx.organizationId } },
|
|
119
|
+
sourceType: 'module',
|
|
120
|
+
sourceModule: 'communication_channels',
|
|
121
|
+
isEnabled: true,
|
|
122
|
+
})
|
|
123
|
+
} catch (error) {
|
|
124
|
+
logger.warn('Failed to register module schedules', { err: error })
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
74
128
|
export const setup: ModuleSetupConfig = {
|
|
75
129
|
defaultRoleFeatures: {
|
|
76
130
|
superadmin: [
|
|
@@ -195,59 +249,11 @@ export const setup: ModuleSetupConfig = {
|
|
|
195
249
|
* scheduler module isn't enabled — keeps the hub usable in scheduler-less
|
|
196
250
|
* test harnesses.
|
|
197
251
|
*/
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
// Best-effort: a scheduler failure must not abort tenant initialization for
|
|
204
|
-
// every other module (mirrors the ai_assistant setup pattern). The schedule
|
|
205
|
-
// ids are deterministic uuids so re-runs upsert idempotently.
|
|
206
|
-
try {
|
|
207
|
-
await schedulerService.register({
|
|
208
|
-
id: stablePollTickScheduleId(organizationId),
|
|
209
|
-
name: 'Communication channels poll tick',
|
|
210
|
-
description:
|
|
211
|
-
`Enumerates active polling channels every ${POLL_TICK_INTERVAL_SECONDS}s and enqueues per-channel poll jobs.`,
|
|
212
|
-
scopeType: 'organization',
|
|
213
|
-
organizationId,
|
|
214
|
-
tenantId,
|
|
215
|
-
scheduleType: 'interval',
|
|
216
|
-
scheduleValue: `${POLL_TICK_INTERVAL_SECONDS}s`,
|
|
217
|
-
timezone: 'UTC',
|
|
218
|
-
targetType: 'queue',
|
|
219
|
-
targetQueue: COMMUNICATION_CHANNELS_QUEUES.pollTick,
|
|
220
|
-
targetPayload: {
|
|
221
|
-
scope: { tenantId, organizationId },
|
|
222
|
-
},
|
|
223
|
-
sourceType: 'module',
|
|
224
|
-
sourceModule: 'communication_channels',
|
|
225
|
-
isEnabled: true,
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
// Spec C § Phase C4 — Gmail watch renewal cron, per-org so multi-tenant
|
|
229
|
-
// deploys schedule independently.
|
|
230
|
-
await schedulerService.register({
|
|
231
|
-
id: stableScheduleUuid(`communication_channels:gmail-renew-watch:${organizationId}`),
|
|
232
|
-
name: 'Gmail watch renewal',
|
|
233
|
-
description:
|
|
234
|
-
'Daily 04:00 UTC. Re-issues gmail.users.watch for channels within OM_PUSH_RENEWAL_GMAIL_LEAD_HOURS of expiry.',
|
|
235
|
-
scopeType: 'organization',
|
|
236
|
-
organizationId,
|
|
237
|
-
tenantId,
|
|
238
|
-
scheduleType: 'cron',
|
|
239
|
-
scheduleValue: '0 4 * * *',
|
|
240
|
-
timezone: 'UTC',
|
|
241
|
-
targetType: 'queue',
|
|
242
|
-
targetQueue: COMMUNICATION_CHANNELS_QUEUES.gmailRenewWatch,
|
|
243
|
-
targetPayload: { scope: { tenantId, organizationId } },
|
|
244
|
-
sourceType: 'module',
|
|
245
|
-
sourceModule: 'communication_channels',
|
|
246
|
-
isEnabled: true,
|
|
247
|
-
})
|
|
248
|
-
} catch (error) {
|
|
249
|
-
logger.warn('Failed to register module schedules', { err: error })
|
|
250
|
-
}
|
|
252
|
+
await ensureCommunicationChannelSchedules({ container, tenantId, organizationId })
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
async onOrganizationCreated({ container, tenantId, organizationId }) {
|
|
256
|
+
await ensureCommunicationChannelSchedules({ container, tenantId, organizationId })
|
|
251
257
|
},
|
|
252
258
|
}
|
|
253
259
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { Resolver, promises as dnsPromises } from 'node:dns'
|
|
2
2
|
import { request as httpsRequest } from 'node:https'
|
|
3
|
+
import type { ClientRequest } from 'node:http'
|
|
4
|
+
import { resolveSafeHostAddress } from '@open-mercato/shared/lib/host-pinning'
|
|
5
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
3
6
|
import { setTimeout as delay } from 'node:timers/promises'
|
|
4
7
|
import { EntityManager } from '@mikro-orm/postgresql'
|
|
5
8
|
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
@@ -99,36 +102,61 @@ const defaultDnsResolver: DnsResolverContract = {
|
|
|
99
102
|
|
|
100
103
|
const defaultHealthCheck: HealthCheckContract = (hostname, timeoutMs) =>
|
|
101
104
|
new Promise((resolve) => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
105
|
+
let req: ClientRequest | undefined
|
|
106
|
+
let settled = false
|
|
107
|
+
const failure = { ok: false, originHeaderPresent: false, reason: 'TLS health check failed' }
|
|
108
|
+
const finish = (result: Awaited<ReturnType<HealthCheckContract>>) => {
|
|
109
|
+
if (settled) return
|
|
110
|
+
settled = true
|
|
111
|
+
clearTimeout(deadline)
|
|
112
|
+
resolve(result)
|
|
113
|
+
}
|
|
114
|
+
const deadline = setTimeout(() => {
|
|
115
|
+
finish(failure)
|
|
116
|
+
req?.destroy()
|
|
117
|
+
}, timeoutMs)
|
|
118
|
+
|
|
119
|
+
void resolveSafeHostAddress(hostname).then(({ host }) => {
|
|
120
|
+
if (settled) return
|
|
121
|
+
const headerName = (process.env.CUSTOMER_DOMAIN_ORIGIN_HEADER ?? 'X-Open-Mercato-Origin').toLowerCase()
|
|
122
|
+
req = httpsRequest(
|
|
123
|
+
{
|
|
124
|
+
host,
|
|
125
|
+
servername: hostname,
|
|
126
|
+
rejectUnauthorized: true,
|
|
127
|
+
agent: false,
|
|
128
|
+
port: 443,
|
|
129
|
+
path: '/api/customer_accounts/domain-check',
|
|
130
|
+
method: 'GET',
|
|
131
|
+
headers: { Host: hostname, 'X-Domain-Check-Secret': process.env.DOMAIN_CHECK_SECRET ?? '' },
|
|
132
|
+
timeout: timeoutMs,
|
|
133
|
+
},
|
|
134
|
+
(res) => {
|
|
135
|
+
const headerValue = res.headers[headerName]
|
|
136
|
+
const originHeader = Array.isArray(headerValue) ? headerValue[0] : headerValue
|
|
137
|
+
const originHeaderPresent = typeof originHeader === 'string' && originHeader === '1'
|
|
138
|
+
const status = res.statusCode ?? 0
|
|
139
|
+
res.resume()
|
|
140
|
+
finish({
|
|
141
|
+
ok: status >= 200 && status < 400,
|
|
142
|
+
originHeaderPresent,
|
|
143
|
+
reason: status >= 200 && status < 400 ? undefined : `HTTP ${status}`,
|
|
144
|
+
})
|
|
145
|
+
},
|
|
146
|
+
)
|
|
147
|
+
req.on('timeout', () => {
|
|
148
|
+
finish(failure)
|
|
149
|
+
req?.destroy()
|
|
150
|
+
})
|
|
151
|
+
req.on('error', () => finish(failure))
|
|
152
|
+
req.end()
|
|
153
|
+
}).catch(() => {
|
|
154
|
+
getTelemetryRuntime()?.reportError(new Error('[internal] Domain TLS health check failed'), {
|
|
155
|
+
module: 'customer_accounts',
|
|
156
|
+
code: 'customer_accounts.domain_health_check_failed',
|
|
157
|
+
})
|
|
158
|
+
finish(failure)
|
|
130
159
|
})
|
|
131
|
-
req.end()
|
|
132
160
|
})
|
|
133
161
|
|
|
134
162
|
export class DomainMappingService {
|
|
@@ -94,8 +94,12 @@ function sortTimelineActivities(items: InteractionSummary[]): InteractionSummary
|
|
|
94
94
|
return leftScheduled - rightScheduled
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
const leftTime = left.occurredAt ?? left.
|
|
98
|
-
const rightTime = right.occurredAt ?? right.
|
|
97
|
+
const leftTime = left.occurredAt ?? left.scheduledAt ?? null
|
|
98
|
+
const rightTime = right.occurredAt ?? right.scheduledAt ?? null
|
|
99
|
+
if (!leftTime || !rightTime) {
|
|
100
|
+
if (leftTime !== rightTime) return leftTime ? -1 : 1
|
|
101
|
+
return right.id.localeCompare(left.id)
|
|
102
|
+
}
|
|
99
103
|
const compare = rightTime.localeCompare(leftTime)
|
|
100
104
|
if (compare !== 0) return compare
|
|
101
105
|
return right.id.localeCompare(left.id)
|
|
@@ -242,7 +246,8 @@ export function ActivitiesSection({
|
|
|
242
246
|
}
|
|
243
247
|
const legacyFiltered = legacyItems.filter((entry) => {
|
|
244
248
|
if (filterTypes.length > 0 && !filterTypes.includes(entry.interactionType)) return false
|
|
245
|
-
const dateOnly = toDateOnly(entry.occurredAt ?? entry.
|
|
249
|
+
const dateOnly = toDateOnly(entry.occurredAt ?? entry.scheduledAt)
|
|
250
|
+
if ((filterDateFrom || filterDateTo) && !dateOnly) return false
|
|
246
251
|
if (filterDateFrom && dateOnly < filterDateFrom) return false
|
|
247
252
|
if (filterDateTo && dateOnly > filterDateTo) return false
|
|
248
253
|
return true
|
|
@@ -116,7 +116,7 @@ function buildEmailCardWidgetData(activity: InteractionSummary): EmailCardWidget
|
|
|
116
116
|
|
|
117
117
|
export function ActivityCard({ activity, onOpen, onChanged, runMutation }: ActivityCardProps) {
|
|
118
118
|
const t = useT()
|
|
119
|
-
const timestamp = activity.occurredAt ?? activity.scheduledAt
|
|
119
|
+
const timestamp = activity.occurredAt ?? activity.scheduledAt
|
|
120
120
|
const TypeIcon = TYPE_ICONS[activity.interactionType] ?? StickyNote
|
|
121
121
|
const titleBase = activity.title ?? activity.body ?? activity.interactionType
|
|
122
122
|
const title = activity.duration ? `${titleBase} (${activity.duration} min)` : titleBase
|
|
@@ -179,8 +179,8 @@ export function ActivityCard({ activity, onOpen, onChanged, runMutation }: Activ
|
|
|
179
179
|
} : undefined}
|
|
180
180
|
>
|
|
181
181
|
<div className="pt-1 text-xs text-muted-foreground">
|
|
182
|
-
<div className="font-semibold text-foreground">{formatDayLabel(timestamp, t)}</div>
|
|
183
|
-
<div>{formatTimeLabel(timestamp)}</div>
|
|
182
|
+
<div className="font-semibold text-foreground">{timestamp ? formatDayLabel(timestamp, t) : t('customers.timeline.date.unknown', 'Unknown date')}</div>
|
|
183
|
+
<div>{timestamp ? formatTimeLabel(timestamp) : null}</div>
|
|
184
184
|
</div>
|
|
185
185
|
|
|
186
186
|
<div className="flex size-9 items-center justify-center rounded-lg bg-muted/80">
|