@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
|
@@ -73,8 +73,8 @@ function computeRangeStart(range: '7d' | '30d' | '90d'): Date {
|
|
|
73
73
|
return date
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
function toTimelineTimestamp(activity: InteractionSummary): string {
|
|
77
|
-
return activity.occurredAt ?? activity.scheduledAt ??
|
|
76
|
+
function toTimelineTimestamp(activity: InteractionSummary): string | null {
|
|
77
|
+
return activity.occurredAt ?? activity.scheduledAt ?? null
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
function normalizeLegacyActivity(activity: ActivitySummary): InteractionSummary {
|
|
@@ -116,9 +116,17 @@ function sortTimelineActivities(items: InteractionSummary[]): InteractionSummary
|
|
|
116
116
|
const rightIsUpcoming = rightIsPlanned && rightScheduled >= now
|
|
117
117
|
|
|
118
118
|
if (leftIsUpcoming !== rightIsUpcoming) return leftIsUpcoming ? -1 : 1
|
|
119
|
-
if (leftIsUpcoming && rightIsUpcoming)
|
|
119
|
+
if (leftIsUpcoming && rightIsUpcoming) {
|
|
120
|
+
return leftScheduled - rightScheduled || left.id.localeCompare(right.id)
|
|
121
|
+
}
|
|
120
122
|
|
|
121
|
-
const
|
|
123
|
+
const leftTime = toTimelineTimestamp(left)
|
|
124
|
+
const rightTime = toTimelineTimestamp(right)
|
|
125
|
+
if (!leftTime || !rightTime) {
|
|
126
|
+
if (leftTime !== rightTime) return leftTime ? -1 : 1
|
|
127
|
+
return right.id.localeCompare(left.id)
|
|
128
|
+
}
|
|
129
|
+
const compare = rightTime.localeCompare(leftTime)
|
|
122
130
|
if (compare !== 0) return compare
|
|
123
131
|
return right.id.localeCompare(left.id)
|
|
124
132
|
})
|
|
@@ -152,7 +160,9 @@ function matchesSearch(activity: InteractionSummary, query: string): boolean {
|
|
|
152
160
|
}
|
|
153
161
|
|
|
154
162
|
function isWithinRange(activity: InteractionSummary, start: Date): boolean {
|
|
155
|
-
const
|
|
163
|
+
const value = toTimelineTimestamp(activity)
|
|
164
|
+
if (!value) return false
|
|
165
|
+
const timestamp = new Date(value)
|
|
156
166
|
if (Number.isNaN(timestamp.getTime())) return false
|
|
157
167
|
return timestamp >= start
|
|
158
168
|
}
|
|
@@ -451,9 +461,11 @@ export function ActivityHistorySection({
|
|
|
451
461
|
) : (
|
|
452
462
|
<div className="space-y-4">
|
|
453
463
|
{activities.map((activity, index) => {
|
|
454
|
-
const
|
|
455
|
-
const
|
|
456
|
-
const
|
|
464
|
+
const currentDate = toTimelineTimestamp(activity)
|
|
465
|
+
const previousDate = index > 0 ? toTimelineTimestamp(activities[index - 1]) : null
|
|
466
|
+
const currentYear = currentDate ? new Date(currentDate).getFullYear() : null
|
|
467
|
+
const previousYear = previousDate ? new Date(previousDate).getFullYear() : null
|
|
468
|
+
const showYearSeparator = currentYear !== null && previousYear !== null && currentYear !== previousYear
|
|
457
469
|
return (
|
|
458
470
|
<React.Fragment key={activity.id}>
|
|
459
471
|
{showYearSeparator ? (
|
|
@@ -38,9 +38,9 @@ export function ActivityTimeline({ activities, onEdit, onMarkDone }: ActivityTim
|
|
|
38
38
|
return (
|
|
39
39
|
<div>
|
|
40
40
|
{activities.map((activity, index) => {
|
|
41
|
-
const dateStr = activity.scheduledAt ?? activity.occurredAt
|
|
41
|
+
const dateStr = activity.scheduledAt ?? activity.occurredAt
|
|
42
42
|
const activityYear = dateStr ? new Date(dateStr).getFullYear() : null
|
|
43
|
-
const prevDateStr = index > 0 ? (activities[index - 1].scheduledAt ?? activities[index - 1].occurredAt
|
|
43
|
+
const prevDateStr = index > 0 ? (activities[index - 1].scheduledAt ?? activities[index - 1].occurredAt) : null
|
|
44
44
|
const prevYear = prevDateStr ? new Date(prevDateStr).getFullYear() : null
|
|
45
45
|
const showYearSeparator = activityYear !== null && prevYear !== null && activityYear !== prevYear
|
|
46
46
|
|
|
@@ -82,7 +82,7 @@ function TimelineEntry({
|
|
|
82
82
|
onEdit?: (activity: InteractionSummary) => void
|
|
83
83
|
onMarkDone?: (activityId: string, updatedAt?: string | null) => void | Promise<void>
|
|
84
84
|
}) {
|
|
85
|
-
const dateStr = activity.scheduledAt ?? activity.occurredAt
|
|
85
|
+
const dateStr = activity.scheduledAt ?? activity.occurredAt
|
|
86
86
|
const TypeIcon = TYPE_ICONS[activity.interactionType]
|
|
87
87
|
const title = activity.title ?? activity.body ?? activity.interactionType
|
|
88
88
|
const duration = activity.duration ? ` (${activity.duration} min)` : ''
|
|
@@ -112,11 +112,11 @@ function TimelineEntry({
|
|
|
112
112
|
<div className="grid items-start gap-3" style={{ gridTemplateColumns: '75px 32px 1fr' }}>
|
|
113
113
|
{/* Column 1: Date */}
|
|
114
114
|
<div className="shrink-0">
|
|
115
|
-
<span className="block text-
|
|
116
|
-
{formatRelativeDate(dateStr, t)}
|
|
115
|
+
<span className="block text-xs font-semibold leading-tight text-foreground">
|
|
116
|
+
{dateStr ? formatRelativeDate(dateStr, t) : t('customers.timeline.date.unknown', 'Unknown date')}
|
|
117
117
|
</span>
|
|
118
|
-
<span className="block text-
|
|
119
|
-
{formatTime(dateStr)}
|
|
118
|
+
<span className="block text-xs leading-tight text-muted-foreground">
|
|
119
|
+
{dateStr ? formatTime(dateStr) : null}
|
|
120
120
|
</span>
|
|
121
121
|
</div>
|
|
122
122
|
|
|
@@ -3012,6 +3012,7 @@
|
|
|
3012
3012
|
"customers.timeline.author": "by {{name}}",
|
|
3013
3013
|
"customers.timeline.date.today": "today",
|
|
3014
3014
|
"customers.timeline.date.tomorrow": "tomorrow",
|
|
3015
|
+
"customers.timeline.date.unknown": "Unbekanntes Datum",
|
|
3015
3016
|
"customers.timeline.date.yesterday": "yesterday",
|
|
3016
3017
|
"customers.timeline.edit": "Edit",
|
|
3017
3018
|
"customers.timeline.empty": "No activities match the current filters.",
|
|
@@ -3012,6 +3012,7 @@
|
|
|
3012
3012
|
"customers.timeline.author": "by {{name}}",
|
|
3013
3013
|
"customers.timeline.date.today": "today",
|
|
3014
3014
|
"customers.timeline.date.tomorrow": "tomorrow",
|
|
3015
|
+
"customers.timeline.date.unknown": "Unknown date",
|
|
3015
3016
|
"customers.timeline.date.yesterday": "yesterday",
|
|
3016
3017
|
"customers.timeline.edit": "Edit",
|
|
3017
3018
|
"customers.timeline.empty": "No activities match the current filters.",
|
|
@@ -3012,6 +3012,7 @@
|
|
|
3012
3012
|
"customers.timeline.author": "by {{name}}",
|
|
3013
3013
|
"customers.timeline.date.today": "today",
|
|
3014
3014
|
"customers.timeline.date.tomorrow": "tomorrow",
|
|
3015
|
+
"customers.timeline.date.unknown": "Fecha desconocida",
|
|
3015
3016
|
"customers.timeline.date.yesterday": "yesterday",
|
|
3016
3017
|
"customers.timeline.edit": "Edit",
|
|
3017
3018
|
"customers.timeline.empty": "No activities match the current filters.",
|
|
@@ -3012,6 +3012,7 @@
|
|
|
3012
3012
|
"customers.timeline.author": "{{name}} 작성",
|
|
3013
3013
|
"customers.timeline.date.today": "오늘",
|
|
3014
3014
|
"customers.timeline.date.tomorrow": "내일",
|
|
3015
|
+
"customers.timeline.date.unknown": "날짜 알 수 없음",
|
|
3015
3016
|
"customers.timeline.date.yesterday": "어제",
|
|
3016
3017
|
"customers.timeline.edit": "편집",
|
|
3017
3018
|
"customers.timeline.empty": "현재 필터와 일치하는 활동이 없습니다.",
|
|
@@ -3012,6 +3012,7 @@
|
|
|
3012
3012
|
"customers.timeline.author": "przez {{name}}",
|
|
3013
3013
|
"customers.timeline.date.today": "dziś",
|
|
3014
3014
|
"customers.timeline.date.tomorrow": "jutro",
|
|
3015
|
+
"customers.timeline.date.unknown": "Nieznana data",
|
|
3015
3016
|
"customers.timeline.date.yesterday": "wczoraj",
|
|
3016
3017
|
"customers.timeline.edit": "Edytuj",
|
|
3017
3018
|
"customers.timeline.empty": "Brak aktywności pasujących do filtrów.",
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import type { AwilixContainer } from 'awilix'
|
|
3
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
4
|
+
import { getModules } from '@open-mercato/shared/lib/modules/registry'
|
|
5
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
6
|
+
|
|
7
|
+
const logger = createLogger('directory')
|
|
8
|
+
|
|
9
|
+
type OrganizationCreatedContext = {
|
|
10
|
+
resolve: <T = unknown>(name: string) => T
|
|
11
|
+
tenantId?: string | null
|
|
12
|
+
organizationId?: string | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createSeedContainer(
|
|
16
|
+
ctx: OrganizationCreatedContext,
|
|
17
|
+
em: EntityManager,
|
|
18
|
+
): AwilixContainer {
|
|
19
|
+
const resolve = <T = unknown>(name: string): T => {
|
|
20
|
+
if (name === 'em') return em as T
|
|
21
|
+
return ctx.resolve<T>(name)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
resolve,
|
|
26
|
+
hasRegistration: (name: string) => {
|
|
27
|
+
try {
|
|
28
|
+
resolve(name)
|
|
29
|
+
return true
|
|
30
|
+
} catch {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
} as unknown as AwilixContainer
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const metadata = {
|
|
38
|
+
event: 'directory.organization.created',
|
|
39
|
+
persistent: true,
|
|
40
|
+
id: 'directory:seed-organization-defaults',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default async function seedOrganizationDefaults(
|
|
44
|
+
_payload: unknown,
|
|
45
|
+
ctx: OrganizationCreatedContext,
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
const tenantId = typeof ctx.tenantId === 'string' ? ctx.tenantId : null
|
|
48
|
+
const organizationId = typeof ctx.organizationId === 'string' ? ctx.organizationId : null
|
|
49
|
+
if (!tenantId || !organizationId) return
|
|
50
|
+
|
|
51
|
+
const rootEm = ctx.resolve<EntityManager>('em')
|
|
52
|
+
const failures: unknown[] = []
|
|
53
|
+
for (const moduleEntry of getModules()) {
|
|
54
|
+
const onOrganizationCreated = moduleEntry.setup?.onOrganizationCreated
|
|
55
|
+
if (!onOrganizationCreated) continue
|
|
56
|
+
|
|
57
|
+
const em = rootEm.fork()
|
|
58
|
+
try {
|
|
59
|
+
await onOrganizationCreated({
|
|
60
|
+
em,
|
|
61
|
+
tenantId,
|
|
62
|
+
organizationId,
|
|
63
|
+
container: createSeedContainer(ctx, em),
|
|
64
|
+
})
|
|
65
|
+
} catch (error) {
|
|
66
|
+
failures.push(error)
|
|
67
|
+
logger.error('Organization defaults provisioning failed', {
|
|
68
|
+
moduleId: moduleEntry.id,
|
|
69
|
+
tenantId,
|
|
70
|
+
organizationId,
|
|
71
|
+
err: error,
|
|
72
|
+
})
|
|
73
|
+
getTelemetryRuntime()?.reportError(error, {
|
|
74
|
+
module: 'directory',
|
|
75
|
+
code: 'directory.organization_defaults_provisioning_failed',
|
|
76
|
+
attributes: { moduleId: moduleEntry.id, tenantId, organizationId },
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (failures.length > 0) {
|
|
82
|
+
throw new AggregateError(failures, 'One or more organization default hooks failed')
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -81,7 +81,7 @@ const storefrontCategoryGetDoc: OpenApiMethodDoc = {
|
|
|
81
81
|
{ status: 404, description: 'No store serves this host or path, or the category is not visible to this buyer', schema: storefrontErrorSchema },
|
|
82
82
|
{ status: 410, description: 'Store archived', schema: storefrontErrorSchema },
|
|
83
83
|
{ status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },
|
|
84
|
-
{ status: 503, description: 'Store misconfigured (no default channel binding)', schema: storefrontErrorSchema },
|
|
84
|
+
{ status: 503, description: 'Store misconfigured (no usable default channel binding)', schema: storefrontErrorSchema },
|
|
85
85
|
],
|
|
86
86
|
}
|
|
87
87
|
|
|
@@ -65,7 +65,7 @@ const storefrontCategoriesGetDoc: OpenApiMethodDoc = {
|
|
|
65
65
|
{ status: 404, description: 'No store serves this host or path', schema: storefrontErrorSchema },
|
|
66
66
|
{ status: 410, description: 'Store archived', schema: storefrontErrorSchema },
|
|
67
67
|
{ status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },
|
|
68
|
-
{ status: 503, description: 'Store misconfigured (no default channel binding)', schema: storefrontErrorSchema },
|
|
68
|
+
{ status: 503, description: 'Store misconfigured (no usable default channel binding)', schema: storefrontErrorSchema },
|
|
69
69
|
],
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -190,7 +190,7 @@ const storefrontContextGetDoc: OpenApiMethodDoc = {
|
|
|
190
190
|
{ status: 404, description: 'No store serves this host or path', schema: storefrontErrorSchema },
|
|
191
191
|
{ status: 410, description: 'Store archived', schema: storefrontErrorSchema },
|
|
192
192
|
{ status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },
|
|
193
|
-
{ status: 503, description: 'Store misconfigured (no default channel binding)', schema: storefrontErrorSchema },
|
|
193
|
+
{ status: 503, description: 'Store misconfigured (no usable default channel binding)', schema: storefrontErrorSchema },
|
|
194
194
|
],
|
|
195
195
|
}
|
|
196
196
|
|
|
@@ -81,7 +81,7 @@ const storefrontProductGetDoc: OpenApiMethodDoc = {
|
|
|
81
81
|
{ status: 404, description: 'No store serves this host or path, or the product is not visible to this buyer', schema: storefrontErrorSchema },
|
|
82
82
|
{ status: 410, description: 'Store archived', schema: storefrontErrorSchema },
|
|
83
83
|
{ status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },
|
|
84
|
-
{ status: 503, description: 'Store misconfigured (no default channel binding)', schema: storefrontErrorSchema },
|
|
84
|
+
{ status: 503, description: 'Store misconfigured (no usable default channel binding)', schema: storefrontErrorSchema },
|
|
85
85
|
],
|
|
86
86
|
}
|
|
87
87
|
|
|
@@ -68,7 +68,7 @@ const storefrontProductsGetDoc: OpenApiMethodDoc = {
|
|
|
68
68
|
{ status: 404, description: 'No store serves this host or path', schema: storefrontErrorSchema },
|
|
69
69
|
{ status: 410, description: 'Store archived', schema: storefrontErrorSchema },
|
|
70
70
|
{ status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },
|
|
71
|
-
{ status: 503, description: 'Store misconfigured (no default channel binding)', schema: storefrontErrorSchema },
|
|
71
|
+
{ status: 503, description: 'Store misconfigured (no usable default channel binding)', schema: storefrontErrorSchema },
|
|
72
72
|
],
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -65,7 +65,7 @@ const storefrontSearchSuggestGetDoc: OpenApiMethodDoc = {
|
|
|
65
65
|
{ status: 404, description: 'No store serves this host or path', schema: storefrontErrorSchema },
|
|
66
66
|
{ status: 410, description: 'Store archived', schema: storefrontErrorSchema },
|
|
67
67
|
{ status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },
|
|
68
|
-
{ status: 503, description: 'Store misconfigured (no default channel binding)', schema: storefrontErrorSchema },
|
|
68
|
+
{ status: 503, description: 'Store misconfigured (no usable default channel binding)', schema: storefrontErrorSchema },
|
|
69
69
|
],
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
ecommerceCustomerUserTag,
|
|
12
12
|
ecommerceDomainMappingTag,
|
|
13
13
|
ecommerceDomainTag,
|
|
14
|
+
ecommerceSalesChannelTag,
|
|
14
15
|
ecommerceStoreTag,
|
|
15
16
|
ecommerceUngroupedBuyerTag,
|
|
16
17
|
invalidateEcommerceCacheTags,
|
|
@@ -64,6 +65,9 @@ export const storeDomainBindingEventTags: TagDeriver = (payload) => [
|
|
|
64
65
|
export const storeChannelBindingEventTags: TagDeriver = (payload) =>
|
|
65
66
|
tagsFrom([readPayloadString(payload, 'storeId'), readPayloadString(payload, 'previousStoreId')], ecommerceStoreTag)
|
|
66
67
|
|
|
68
|
+
export const salesChannelEventTags: TagDeriver = (payload) =>
|
|
69
|
+
tagsFrom([readPayloadString(payload, 'id')], ecommerceSalesChannelTag)
|
|
70
|
+
|
|
67
71
|
export const domainMappingEventTags: TagDeriver = (payload) => [
|
|
68
72
|
...tagsFrom(
|
|
69
73
|
[readPayloadString(payload, 'id'), readPayloadString(payload, 'replacedDomainId')],
|
|
@@ -144,6 +144,10 @@ export function ecommerceDomainTag(hostname: string): string {
|
|
|
144
144
|
return `ecommerce-domain:${hostname}`
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
export function ecommerceSalesChannelTag(salesChannelId: string): string {
|
|
148
|
+
return `ecommerce-sales-channel:${salesChannelId}`
|
|
149
|
+
}
|
|
150
|
+
|
|
147
151
|
export function ecommerceCustomerTag(customerId: string): string {
|
|
148
152
|
return `customer:${customerId}`
|
|
149
153
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
ecommerceDomainMappingTag,
|
|
19
19
|
ecommerceDomainTag,
|
|
20
20
|
ecommerceResolutionCache,
|
|
21
|
+
ecommerceSalesChannelTag,
|
|
21
22
|
ecommerceStoreTag,
|
|
22
23
|
type CacheContainer,
|
|
23
24
|
} from './cacheKeys'
|
|
@@ -145,6 +146,7 @@ type StoreCandidate = {
|
|
|
145
146
|
store: CandidateStore
|
|
146
147
|
channel: ResolvedStoreChannel | null
|
|
147
148
|
binding: CandidateBinding | null
|
|
149
|
+
missingSalesChannelId?: string | null
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
type CachedStoreCandidates = { candidates: StoreCandidate[] }
|
|
@@ -187,6 +189,12 @@ type StoreResolutionDatabase = {
|
|
|
187
189
|
require_authentication: boolean
|
|
188
190
|
deleted_at: Date | null
|
|
189
191
|
}
|
|
192
|
+
sales_channels: {
|
|
193
|
+
id: string
|
|
194
|
+
tenant_id: string
|
|
195
|
+
organization_id: string
|
|
196
|
+
deleted_at: Date | null
|
|
197
|
+
}
|
|
190
198
|
}
|
|
191
199
|
|
|
192
200
|
type StoreResolutionRow = {
|
|
@@ -207,6 +215,7 @@ type StoreResolutionRow = {
|
|
|
207
215
|
channel_assortment_scope: unknown
|
|
208
216
|
channel_price_sort_fallback: string | null
|
|
209
217
|
channel_require_authentication?: boolean | null
|
|
218
|
+
channel_live_sales_channel_id?: string | null
|
|
210
219
|
binding_id?: string | null
|
|
211
220
|
binding_path_prefix?: string | null
|
|
212
221
|
binding_is_primary?: boolean | null
|
|
@@ -230,6 +239,7 @@ const STORE_COLUMNS = [
|
|
|
230
239
|
'c.assortment_scope as channel_assortment_scope',
|
|
231
240
|
'c.price_sort_fallback as channel_price_sort_fallback',
|
|
232
241
|
'c.require_authentication as channel_require_authentication',
|
|
242
|
+
'sc.id as channel_live_sales_channel_id',
|
|
233
243
|
] as const
|
|
234
244
|
|
|
235
245
|
const FORCE_HOST_HEADER = 'x-force-host'
|
|
@@ -401,8 +411,10 @@ function toCandidate(row: StoreResolutionRow): StoreCandidate {
|
|
|
401
411
|
defaultCurrencyCode: row.store_default_currency_code,
|
|
402
412
|
settings: parseSettings(row.store_settings, row.store_id),
|
|
403
413
|
}
|
|
414
|
+
const salesChannelMissing =
|
|
415
|
+
!!row.channel_binding_id && !!row.channel_sales_channel_id && !row.channel_live_sales_channel_id
|
|
404
416
|
const channel: ResolvedStoreChannel | null =
|
|
405
|
-
row.channel_binding_id && row.channel_sales_channel_id
|
|
417
|
+
row.channel_binding_id && row.channel_sales_channel_id && !salesChannelMissing
|
|
406
418
|
? {
|
|
407
419
|
channelBindingId: row.channel_binding_id,
|
|
408
420
|
salesChannelId: row.channel_sales_channel_id,
|
|
@@ -415,7 +427,7 @@ function toCandidate(row: StoreResolutionRow): StoreCandidate {
|
|
|
415
427
|
const binding: CandidateBinding | null = row.binding_id
|
|
416
428
|
? { id: row.binding_id, pathPrefix: row.binding_path_prefix ?? null, isPrimary: row.binding_is_primary === true }
|
|
417
429
|
: null
|
|
418
|
-
return { store, channel, binding }
|
|
430
|
+
return { store, channel, binding, missingSalesChannelId: salesChannelMissing ? row.channel_sales_channel_id : null }
|
|
419
431
|
}
|
|
420
432
|
|
|
421
433
|
async function queryDomainCandidates(
|
|
@@ -440,6 +452,13 @@ async function queryDomainCandidates(
|
|
|
440
452
|
.on('c.is_default', '=', true)
|
|
441
453
|
.on('c.deleted_at', 'is', null),
|
|
442
454
|
)
|
|
455
|
+
.leftJoin('sales_channels as sc', (join) =>
|
|
456
|
+
join
|
|
457
|
+
.onRef('sc.id', '=', 'c.sales_channel_id')
|
|
458
|
+
.onRef('sc.tenant_id', '=', 'c.tenant_id')
|
|
459
|
+
.onRef('sc.organization_id', '=', 'c.organization_id')
|
|
460
|
+
.on('sc.deleted_at', 'is', null),
|
|
461
|
+
)
|
|
443
462
|
.select([
|
|
444
463
|
...STORE_COLUMNS,
|
|
445
464
|
'b.id as binding_id',
|
|
@@ -469,6 +488,13 @@ async function querySlugCandidates(
|
|
|
469
488
|
.on('c.is_default', '=', true)
|
|
470
489
|
.on('c.deleted_at', 'is', null),
|
|
471
490
|
)
|
|
491
|
+
.leftJoin('sales_channels as sc', (join) =>
|
|
492
|
+
join
|
|
493
|
+
.onRef('sc.id', '=', 'c.sales_channel_id')
|
|
494
|
+
.onRef('sc.tenant_id', '=', 'c.tenant_id')
|
|
495
|
+
.onRef('sc.organization_id', '=', 'c.organization_id')
|
|
496
|
+
.on('sc.deleted_at', 'is', null),
|
|
497
|
+
)
|
|
472
498
|
.select([...STORE_COLUMNS])
|
|
473
499
|
.where('s.slug', '=', slug)
|
|
474
500
|
.where('s.deleted_at', 'is', null)
|
|
@@ -476,6 +502,10 @@ async function querySlugCandidates(
|
|
|
476
502
|
return query.limit(2).execute()
|
|
477
503
|
}
|
|
478
504
|
|
|
505
|
+
function salesChannelTags(rows: StoreResolutionRow[]): string[] {
|
|
506
|
+
return rows.flatMap((row) => (row.channel_sales_channel_id ? [ecommerceSalesChannelTag(row.channel_sales_channel_id)] : []))
|
|
507
|
+
}
|
|
508
|
+
|
|
479
509
|
async function loadDomainCandidates(
|
|
480
510
|
container: StoreContextContainer,
|
|
481
511
|
mapping: DomainMappingResolution,
|
|
@@ -490,6 +520,7 @@ async function loadDomainCandidates(
|
|
|
490
520
|
ecommerceDomainMappingTag(mapping.domainMappingId),
|
|
491
521
|
ecommerceDomainTag(mapping.hostname),
|
|
492
522
|
...candidates.map((candidate) => ecommerceStoreTag(candidate.store.id)),
|
|
523
|
+
...salesChannelTags(rows),
|
|
493
524
|
]
|
|
494
525
|
await cache.set<CachedStoreCandidates>(
|
|
495
526
|
key,
|
|
@@ -516,24 +547,39 @@ async function loadSlugCandidates(
|
|
|
516
547
|
{ candidates },
|
|
517
548
|
{
|
|
518
549
|
ttlMs: STORE_RESOLUTION_TTL_MS,
|
|
519
|
-
tags:
|
|
550
|
+
tags: Array.from(
|
|
551
|
+
new Set([...candidates.map((candidate) => ecommerceStoreTag(candidate.store.id)), ...salesChannelTags(rows)]),
|
|
552
|
+
),
|
|
520
553
|
},
|
|
521
554
|
)
|
|
522
555
|
}
|
|
523
556
|
return candidates
|
|
524
557
|
}
|
|
525
558
|
|
|
526
|
-
async function reportMisconfiguredStore(
|
|
559
|
+
async function reportMisconfiguredStore(
|
|
560
|
+
container: StoreContextContainer,
|
|
561
|
+
store: CandidateStore,
|
|
562
|
+
missingSalesChannelId: string | null,
|
|
563
|
+
): Promise<void> {
|
|
527
564
|
const cache = ecommerceResolutionCache(container)
|
|
528
565
|
const key = ['misconfigured', 'channel-binding-missing', store.id]
|
|
529
566
|
const alreadyReported = await cache.get<{ reportedAt: string }>(key)
|
|
530
567
|
if (alreadyReported) return
|
|
531
568
|
await cache.set(key, { reportedAt: new Date().toISOString() }, { ttlMs: STORE_MISCONFIGURED_THROTTLE_MS })
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
569
|
+
if (missingSalesChannelId) {
|
|
570
|
+
logger.warn('Store default channel binding references a sales channel that no longer exists; answering 503', {
|
|
571
|
+
storeId: store.id,
|
|
572
|
+
tenantId: store.tenantId,
|
|
573
|
+
organizationId: store.organizationId,
|
|
574
|
+
salesChannelId: missingSalesChannelId,
|
|
575
|
+
})
|
|
576
|
+
} else {
|
|
577
|
+
logger.warn('Store has no default channel binding; answering 503', {
|
|
578
|
+
storeId: store.id,
|
|
579
|
+
tenantId: store.tenantId,
|
|
580
|
+
organizationId: store.organizationId,
|
|
581
|
+
})
|
|
582
|
+
}
|
|
537
583
|
try {
|
|
538
584
|
await emitEcommerceEvent(
|
|
539
585
|
'ecommerce.store.misconfigured',
|
|
@@ -570,7 +616,7 @@ async function finalizeCandidate(
|
|
|
570
616
|
}
|
|
571
617
|
if (store.status === 'archived') throw new StorefrontResolutionError(410, 'store_archived')
|
|
572
618
|
if (!candidate.channel) {
|
|
573
|
-
await reportMisconfiguredStore(container, store)
|
|
619
|
+
await reportMisconfiguredStore(container, store, candidate.missingSalesChannelId ?? null)
|
|
574
620
|
throw new StorefrontResolutionError(503, 'store_misconfigured')
|
|
575
621
|
}
|
|
576
622
|
const locale = resolveStoreLocale(store, localeCandidates)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { invalidateForEvent, salesChannelEventTags } from '../lib/cacheInvalidation'
|
|
2
|
+
import type { EcommerceSubscriberContext } from '../lib/subscriberSupport'
|
|
3
|
+
|
|
4
|
+
export const metadata = {
|
|
5
|
+
event: 'sales.channel.*',
|
|
6
|
+
persistent: false,
|
|
7
|
+
id: 'ecommerce:sales-channel-cache-invalidation',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default async function handle(payload: unknown, ctx: EcommerceSubscriberContext): Promise<void> {
|
|
11
|
+
await invalidateForEvent(payload, ctx, salesChannelEventTags)
|
|
12
|
+
}
|
|
@@ -11,6 +11,7 @@ function stableUuidFromString(input: string): string {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
type SchedulerServiceLike = {
|
|
14
|
+
unregister?: (scheduleId: string) => Promise<void>
|
|
14
15
|
register: (registration: {
|
|
15
16
|
id: string
|
|
16
17
|
name: string
|
|
@@ -31,6 +32,39 @@ type SchedulerServiceLike = {
|
|
|
31
32
|
}) => Promise<void>
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
async function ensureHealthProbeSchedule(ctx: {
|
|
36
|
+
container: import('awilix').AwilixContainer
|
|
37
|
+
organizationId: string
|
|
38
|
+
tenantId: string
|
|
39
|
+
}, legacyScheduleId?: string): Promise<void> {
|
|
40
|
+
const cradle = ctx.container as { hasRegistration?: (name: string) => boolean }
|
|
41
|
+
if (typeof cradle.hasRegistration !== 'function' || !cradle.hasRegistration('schedulerService')) return
|
|
42
|
+
|
|
43
|
+
const schedulerService = ctx.container.resolve('schedulerService') as SchedulerServiceLike
|
|
44
|
+
if (legacyScheduleId && typeof schedulerService.unregister === 'function') {
|
|
45
|
+
await schedulerService.unregister(legacyScheduleId)
|
|
46
|
+
}
|
|
47
|
+
await schedulerService.register({
|
|
48
|
+
id: stableUuidFromString(`integrations:health-probe:${ctx.tenantId}:${ctx.organizationId}`),
|
|
49
|
+
name: 'Integration health probes',
|
|
50
|
+
description: 'Runs outbound health checks for enabled integrations every 15 minutes.',
|
|
51
|
+
scopeType: 'organization',
|
|
52
|
+
organizationId: ctx.organizationId,
|
|
53
|
+
tenantId: ctx.tenantId,
|
|
54
|
+
scheduleType: 'interval',
|
|
55
|
+
scheduleValue: '15m',
|
|
56
|
+
timezone: 'UTC',
|
|
57
|
+
targetType: 'queue',
|
|
58
|
+
targetQueue: 'integration-health-probe',
|
|
59
|
+
targetPayload: {
|
|
60
|
+
scope: { organizationId: ctx.organizationId, tenantId: ctx.tenantId },
|
|
61
|
+
},
|
|
62
|
+
sourceType: 'module',
|
|
63
|
+
sourceModule: 'integrations',
|
|
64
|
+
isEnabled: true,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
34
68
|
export const setup: ModuleSetupConfig = {
|
|
35
69
|
defaultRoleFeatures: {
|
|
36
70
|
superadmin: ['integrations.*', 'integrations.view', 'integrations.manage', 'integrations.credentials.manage'],
|
|
@@ -39,31 +73,14 @@ export const setup: ModuleSetupConfig = {
|
|
|
39
73
|
},
|
|
40
74
|
|
|
41
75
|
async seedDefaults({ container, organizationId, tenantId }) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
76
|
+
await ensureHealthProbeSchedule(
|
|
77
|
+
{ container, organizationId, tenantId },
|
|
78
|
+
stableUuidFromString(`integrations:health-probe:${tenantId}`),
|
|
79
|
+
)
|
|
80
|
+
},
|
|
46
81
|
|
|
47
|
-
|
|
48
|
-
await
|
|
49
|
-
id: stableUuidFromString(`integrations:health-probe:${tenantId}`),
|
|
50
|
-
name: 'Integration health probes',
|
|
51
|
-
description: 'Runs outbound health checks for enabled integrations every 15 minutes.',
|
|
52
|
-
scopeType: 'organization',
|
|
53
|
-
organizationId,
|
|
54
|
-
tenantId,
|
|
55
|
-
scheduleType: 'interval',
|
|
56
|
-
scheduleValue: '15m',
|
|
57
|
-
timezone: 'UTC',
|
|
58
|
-
targetType: 'queue',
|
|
59
|
-
targetQueue: 'integration-health-probe',
|
|
60
|
-
targetPayload: {
|
|
61
|
-
scope: { organizationId, tenantId },
|
|
62
|
-
},
|
|
63
|
-
sourceType: 'module',
|
|
64
|
-
sourceModule: 'integrations',
|
|
65
|
-
isEnabled: true,
|
|
66
|
-
})
|
|
82
|
+
async onOrganizationCreated({ container, organizationId, tenantId }) {
|
|
83
|
+
await ensureHealthProbeSchedule({ container, organizationId, tenantId })
|
|
67
84
|
},
|
|
68
85
|
}
|
|
69
86
|
|
|
@@ -360,7 +360,7 @@ export function MessagesInboxPageClient() {
|
|
|
360
360
|
const totalPages = listQuery.data?.totalPages ?? 0
|
|
361
361
|
|
|
362
362
|
return (
|
|
363
|
-
<div className="space-y-4">
|
|
363
|
+
<div className="min-w-0 max-w-full space-y-4 [&_.isolate]:overflow-x-auto">
|
|
364
364
|
<DataTable
|
|
365
365
|
title={t('messages.title', 'Messages')}
|
|
366
366
|
titleHeadingLevel={1}
|
|
@@ -98,9 +98,9 @@ export function MainMessageHeader(props: MainMessageHeaderProps) {
|
|
|
98
98
|
backHref="/backend/messages"
|
|
99
99
|
backLabel={t('messages.actions.backToList', 'Back to messages')}
|
|
100
100
|
title={(
|
|
101
|
-
<h1 className="
|
|
102
|
-
<span className="
|
|
103
|
-
<PriorityBadge priority={props.priority} />
|
|
101
|
+
<h1 className="flex min-w-0 max-w-full flex-wrap items-center gap-2">
|
|
102
|
+
<span className="min-w-0 break-words" title={props.subject}>{props.subject}</span>
|
|
103
|
+
<PriorityBadge priority={props.priority} className="shrink-0" />
|
|
104
104
|
</h1>
|
|
105
105
|
)}
|
|
106
106
|
utilityActions={props.canReply ? (
|
|
@@ -14,14 +14,14 @@ type BodyPanelProps = {
|
|
|
14
14
|
export function MessageDetailBodySection(props: BodyPanelProps) {
|
|
15
15
|
if (props.ContentComponent) {
|
|
16
16
|
return (
|
|
17
|
-
<div className="rounded border bg-muted/30 p-3">
|
|
17
|
+
<div className="min-w-0 max-w-full overflow-x-auto rounded border bg-muted/30 p-3 break-words">
|
|
18
18
|
<props.ContentComponent {...props.contentProps} />
|
|
19
19
|
</div>
|
|
20
20
|
)
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
return (
|
|
24
|
-
<div className="rounded border bg-muted/30 p-3 text-sm whitespace-pre-wrap">
|
|
24
|
+
<div className="min-w-0 max-w-full overflow-x-auto rounded border bg-muted/30 p-3 text-sm whitespace-pre-wrap break-words">
|
|
25
25
|
<MarkdownContent
|
|
26
26
|
body={props.detail.body}
|
|
27
27
|
format={props.detail.bodyFormat}
|
|
@@ -68,6 +68,10 @@ export const setup: ModuleSetupConfig = {
|
|
|
68
68
|
async seedDefaults({ container, tenantId, organizationId }) {
|
|
69
69
|
await ensureSessionInitializationPruneSchedule(container, { tenantId, organizationId })
|
|
70
70
|
},
|
|
71
|
+
|
|
72
|
+
async onOrganizationCreated({ container, tenantId, organizationId }) {
|
|
73
|
+
await ensureSessionInitializationPruneSchedule(container, { tenantId, organizationId })
|
|
74
|
+
},
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
export default setup
|