@open-mercato/core 0.9.1-develop.7499.1.fcec456e4e → 0.9.1-develop.7515.1.e2712c8b00
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/audit_logs/components/AuditLogsActions.js +1 -1
- package/dist/modules/audit_logs/components/AuditLogsActions.js.map +2 -2
- package/dist/modules/auth/frontend/reset.js +7 -3
- package/dist/modules/auth/frontend/reset.js.map +2 -2
- package/dist/modules/business_rules/api/rules/route.js +12 -1
- package/dist/modules/business_rules/api/rules/route.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +17 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +3 -2
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/create/page.js +1 -0
- package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
- package/dist/modules/catalog/components/products/productFormUtils.js +9 -0
- package/dist/modules/catalog/components/products/productFormUtils.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/page.js +32 -32
- package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +22 -22
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/page.js +34 -34
- package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
- package/dist/modules/ecommerce/search.js +1 -2
- package/dist/modules/ecommerce/search.js.map +2 -2
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js +4 -7
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/shipping-service.js +29 -11
- package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/status-sync.js +2 -2
- package/dist/modules/shipping_carriers/lib/status-sync.js.map +2 -2
- package/dist/modules/shipping_carriers/workers/webhook-processor.js +60 -33
- package/dist/modules/shipping_carriers/workers/webhook-processor.js.map +3 -3
- package/dist/modules/staff/backend/staff/time-tracking/entries/page.js +10 -7
- package/dist/modules/staff/backend/staff/time-tracking/entries/page.js.map +2 -2
- package/dist/modules/warranty_claims/commands/claim-lines.js +89 -68
- package/dist/modules/warranty_claims/commands/claim-lines.js.map +2 -2
- package/dist/modules/warranty_claims/commands/claims.js +14 -13
- package/dist/modules/warranty_claims/commands/claims.js.map +2 -2
- package/dist/modules/warranty_claims/commands/shared.js +8 -2
- package/dist/modules/warranty_claims/commands/shared.js.map +2 -2
- package/dist/modules/wms/api/interceptors.js +3 -0
- package/dist/modules/wms/api/interceptors.js.map +2 -2
- package/dist/modules/wms/commands/configuration.js +16 -16
- package/dist/modules/wms/commands/configuration.js.map +2 -2
- package/dist/modules/wms/events.js +1 -0
- package/dist/modules/wms/events.js.map +2 -2
- package/dist/modules/wms/lib/catalogInventoryProfileIntent.js +9 -0
- package/dist/modules/wms/lib/catalogInventoryProfileIntent.js.map +2 -2
- package/dist/modules/wms/widgets/injection/catalog-inventory-profile/widget.js +11 -3
- package/dist/modules/wms/widgets/injection/catalog-inventory-profile/widget.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/audit_logs/components/AuditLogsActions.tsx +1 -1
- package/src/modules/auth/frontend/reset.tsx +8 -3
- package/src/modules/business_rules/api/rules/route.ts +11 -0
- package/src/modules/business_rules/i18n/de.json +1 -0
- package/src/modules/business_rules/i18n/en.json +1 -0
- package/src/modules/business_rules/i18n/es.json +1 -0
- package/src/modules/business_rules/i18n/ko.json +1 -0
- package/src/modules/business_rules/i18n/pl.json +1 -0
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +20 -1
- package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +2 -1
- package/src/modules/catalog/backend/catalog/products/create/page.tsx +1 -0
- package/src/modules/catalog/components/products/productFormUtils.ts +9 -0
- package/src/modules/customers/backend/customers/companies/page.tsx +32 -32
- package/src/modules/customers/backend/customers/deals/page.tsx +22 -22
- package/src/modules/customers/backend/customers/people/page.tsx +34 -34
- package/src/modules/customers/i18n/de.json +12 -0
- package/src/modules/customers/i18n/en.json +12 -0
- package/src/modules/customers/i18n/es.json +12 -0
- package/src/modules/customers/i18n/ko.json +12 -0
- package/src/modules/customers/i18n/pl.json +12 -0
- package/src/modules/ecommerce/i18n/de.json +2 -1
- package/src/modules/ecommerce/i18n/en.json +2 -1
- package/src/modules/ecommerce/i18n/es.json +2 -1
- package/src/modules/ecommerce/i18n/ko.json +2 -1
- package/src/modules/ecommerce/i18n/pl.json +2 -1
- package/src/modules/ecommerce/search.ts +0 -1
- package/src/modules/shipping_carriers/api/webhook/[provider]/route.ts +4 -7
- package/src/modules/shipping_carriers/lib/shipping-service.ts +29 -11
- package/src/modules/shipping_carriers/lib/status-sync.ts +2 -2
- package/src/modules/shipping_carriers/workers/webhook-processor.ts +71 -39
- package/src/modules/staff/backend/staff/time-tracking/entries/page.tsx +20 -14
- package/src/modules/warranty_claims/commands/claim-lines.ts +107 -66
- package/src/modules/warranty_claims/commands/claims.ts +14 -13
- package/src/modules/warranty_claims/commands/shared.ts +7 -1
- package/src/modules/wms/api/interceptors.ts +9 -0
- package/src/modules/wms/commands/configuration.ts +28 -16
- package/src/modules/wms/events.ts +1 -0
- package/src/modules/wms/lib/catalogInventoryProfileIntent.ts +9 -0
- package/src/modules/wms/widgets/injection/catalog-inventory-profile/widget.ts +13 -4
|
@@ -337,5 +337,6 @@
|
|
|
337
337
|
"ecommerce.validation.pathPrefixInvalid": "Path prefix may contain only lowercase letters, digits, dashes and slashes, for example /shop.",
|
|
338
338
|
"ecommerce.validation.slugInvalid": "Slug may contain only lowercase letters, digits and single dashes between them.",
|
|
339
339
|
"ecommerce.validation.urlInvalid": "Enter a valid http(s) URL or a path starting with /.",
|
|
340
|
-
"notifications.categories.ecommerce": "Ecommerce"
|
|
340
|
+
"notifications.categories.ecommerce": "Ecommerce",
|
|
341
|
+
"search.entityType.ecommerce.ecommerce_store": "Store"
|
|
341
342
|
}
|
|
@@ -337,5 +337,6 @@
|
|
|
337
337
|
"ecommerce.validation.pathPrefixInvalid": "El prefijo de ruta solo puede contener letras minúsculas, dígitos, guiones y barras, por ejemplo /shop.",
|
|
338
338
|
"ecommerce.validation.slugInvalid": "El slug solo puede contener letras minúsculas, dígitos y guiones simples entre ellos.",
|
|
339
339
|
"ecommerce.validation.urlInvalid": "Introduce una URL http(s) válida o una ruta que empiece por /.",
|
|
340
|
-
"notifications.categories.ecommerce": "Comercio electrónico"
|
|
340
|
+
"notifications.categories.ecommerce": "Comercio electrónico",
|
|
341
|
+
"search.entityType.ecommerce.ecommerce_store": "Tienda"
|
|
341
342
|
}
|
|
@@ -337,5 +337,6 @@
|
|
|
337
337
|
"ecommerce.validation.pathPrefixInvalid": "경로 접두사에는 영문 소문자, 숫자, 하이픈, 슬래시만 사용할 수 있습니다(예: /shop).",
|
|
338
338
|
"ecommerce.validation.slugInvalid": "슬러그에는 영문 소문자, 숫자와 그 사이의 단일 하이픈만 사용할 수 있습니다.",
|
|
339
339
|
"ecommerce.validation.urlInvalid": "유효한 http(s) URL 또는 /로 시작하는 경로를 입력하세요.",
|
|
340
|
-
"notifications.categories.ecommerce": "이커머스"
|
|
340
|
+
"notifications.categories.ecommerce": "이커머스",
|
|
341
|
+
"search.entityType.ecommerce.ecommerce_store": "스토어"
|
|
341
342
|
}
|
|
@@ -337,5 +337,6 @@
|
|
|
337
337
|
"ecommerce.validation.pathPrefixInvalid": "Prefiks ścieżki może zawierać tylko małe litery, cyfry, myślniki i ukośniki, np. /shop.",
|
|
338
338
|
"ecommerce.validation.slugInvalid": "Slug może zawierać tylko małe litery, cyfry i pojedyncze myślniki między nimi.",
|
|
339
339
|
"ecommerce.validation.urlInvalid": "Podaj prawidłowy adres URL http(s) lub ścieżkę zaczynającą się od /.",
|
|
340
|
-
"notifications.categories.ecommerce": "E-commerce"
|
|
340
|
+
"notifications.categories.ecommerce": "E-commerce",
|
|
341
|
+
"search.entityType.ecommerce.ecommerce_store": "Sklep"
|
|
341
342
|
}
|
|
@@ -115,13 +115,10 @@ export async function POST(req: Request, { params }: { params: Promise<{ provide
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
await queue.enqueue({
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
shipmentId: shipment.id,
|
|
123
|
-
scope: matchedScope,
|
|
124
|
-
},
|
|
118
|
+
providerKey,
|
|
119
|
+
event,
|
|
120
|
+
shipmentId: shipment.id,
|
|
121
|
+
scope: matchedScope,
|
|
125
122
|
})
|
|
126
123
|
|
|
127
124
|
return NextResponse.json({ received: true, queued: true }, { status: 202 })
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LockMode } from '@mikro-orm/core'
|
|
1
2
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
3
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
3
4
|
import type { CredentialsService } from '../../integrations/lib/credentials-service'
|
|
@@ -200,7 +201,7 @@ export function createShippingCarrierService(deps: {
|
|
|
200
201
|
trackingNumber: created.trackingNumber,
|
|
201
202
|
organizationId: input.organizationId,
|
|
202
203
|
tenantId: input.tenantId,
|
|
203
|
-
})
|
|
204
|
+
}, { tenantId: input.tenantId, organizationId: input.organizationId })
|
|
204
205
|
return shipment
|
|
205
206
|
} catch (error: unknown) {
|
|
206
207
|
// Release the claim only when the carrier was NOT successfully called, so a retry can
|
|
@@ -256,24 +257,41 @@ export function createShippingCarrierService(deps: {
|
|
|
256
257
|
trackingNumber: shipment.trackingNumber,
|
|
257
258
|
credentials,
|
|
258
259
|
})
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
260
|
+
const outcome = await em.transactional(async (tx) => {
|
|
261
|
+
const locked = await findOneWithDecryption(
|
|
262
|
+
tx,
|
|
263
|
+
CarrierShipment,
|
|
264
|
+
{
|
|
265
|
+
id: shipment.id,
|
|
266
|
+
organizationId: scope.organizationId,
|
|
267
|
+
tenantId: scope.tenantId,
|
|
268
|
+
deletedAt: null,
|
|
269
|
+
},
|
|
270
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE, refresh: true },
|
|
271
|
+
scope,
|
|
272
|
+
)
|
|
273
|
+
if (!locked) return null
|
|
274
|
+
const previousStatus = locked.unifiedStatus
|
|
275
|
+
locked.trackingEvents = tracking.events
|
|
276
|
+
locked.lastPolledAt = new Date()
|
|
277
|
+
const applied = syncShipmentStatus(locked, tracking.status)
|
|
278
|
+
await tx.flush()
|
|
279
|
+
return { applied, previousStatus }
|
|
280
|
+
}, { clear: true })
|
|
281
|
+
if (outcome?.applied) {
|
|
265
282
|
const eventPayload = {
|
|
266
283
|
shipmentId: shipment.id,
|
|
267
284
|
providerKey: input.providerKey,
|
|
268
|
-
previousStatus,
|
|
285
|
+
previousStatus: outcome.previousStatus,
|
|
269
286
|
newStatus: tracking.status,
|
|
270
287
|
organizationId: input.organizationId,
|
|
271
288
|
tenantId: input.tenantId,
|
|
272
289
|
}
|
|
273
|
-
|
|
290
|
+
const eventScope = { tenantId: input.tenantId, organizationId: input.organizationId }
|
|
291
|
+
await emitShippingEvent('shipping_carriers.shipment.status_changed', eventPayload, eventScope)
|
|
274
292
|
if (TERMINAL_SHIPPING_STATUSES.has(tracking.status)) {
|
|
275
293
|
const terminalEvent = getTerminalShippingEvent(tracking.status)
|
|
276
|
-
if (terminalEvent) await emitShippingEvent(terminalEvent, eventPayload)
|
|
294
|
+
if (terminalEvent) await emitShippingEvent(terminalEvent, eventPayload, eventScope)
|
|
277
295
|
}
|
|
278
296
|
}
|
|
279
297
|
return tracking
|
|
@@ -307,7 +325,7 @@ export function createShippingCarrierService(deps: {
|
|
|
307
325
|
providerKey: input.providerKey,
|
|
308
326
|
organizationId: input.organizationId,
|
|
309
327
|
tenantId: input.tenantId,
|
|
310
|
-
})
|
|
328
|
+
}, { tenantId: input.tenantId, organizationId: input.organizationId })
|
|
311
329
|
return result
|
|
312
330
|
},
|
|
313
331
|
|
|
@@ -29,8 +29,8 @@ export function isShipmentCancelNotAllowedError(error: unknown): error is Shipme
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const VALID_SHIPPING_TRANSITIONS: Record<string, UnifiedShipmentStatus[]> = {
|
|
32
|
-
label_created: ['picked_up', 'in_transit', 'cancelled'],
|
|
33
|
-
picked_up: ['in_transit', 'cancelled'],
|
|
32
|
+
label_created: ['picked_up', 'in_transit', 'out_for_delivery', 'delivered', 'failed_delivery', 'returned', 'cancelled'],
|
|
33
|
+
picked_up: ['in_transit', 'out_for_delivery', 'delivered', 'failed_delivery', 'returned', 'cancelled'],
|
|
34
34
|
in_transit: ['out_for_delivery', 'delivered', 'returned', 'failed_delivery'],
|
|
35
35
|
out_for_delivery: ['delivered', 'returned', 'failed_delivery'],
|
|
36
36
|
failed_delivery: ['in_transit', 'out_for_delivery', 'delivered', 'returned', 'cancelled'],
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { LockMode } from '@mikro-orm/core'
|
|
1
2
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
3
|
import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'
|
|
3
4
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
4
5
|
import { CarrierShipment } from '../data/entities'
|
|
5
6
|
import { emitShippingEvent } from '../events'
|
|
6
|
-
import type { ShippingWebhookEvent } from '../lib/adapter'
|
|
7
|
+
import type { ShippingWebhookEvent, UnifiedShipmentStatus } from '../lib/adapter'
|
|
7
8
|
import { getShippingAdapter } from '../lib/adapter-registry'
|
|
8
9
|
import { getTerminalShippingEvent, syncShipmentStatus, TERMINAL_SHIPPING_STATUSES } from '../lib/status-sync'
|
|
9
10
|
import { claimWebhookProcessing, releaseWebhookClaim } from '../lib/webhook-utils'
|
|
@@ -21,6 +22,16 @@ type WebhookJobPayload = {
|
|
|
21
22
|
} | null
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
type LegacyWebhookJobEnvelope = {
|
|
26
|
+
name?: string
|
|
27
|
+
payload: WebhookJobPayload
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readWebhookJobPayload(data: WebhookJobPayload | LegacyWebhookJobEnvelope): WebhookJobPayload {
|
|
31
|
+
if ('providerKey' in data) return data
|
|
32
|
+
return data.payload
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
type HandlerContext = JobContext & {
|
|
25
36
|
resolve: <T = unknown>(name: string) => T
|
|
26
37
|
}
|
|
@@ -31,24 +42,25 @@ export const metadata: WorkerMeta = {
|
|
|
31
42
|
concurrency: 5,
|
|
32
43
|
}
|
|
33
44
|
|
|
34
|
-
export default async function handle(job: QueuedJob<WebhookJobPayload>, ctx: HandlerContext): Promise<void> {
|
|
45
|
+
export default async function handle(job: QueuedJob<WebhookJobPayload | LegacyWebhookJobEnvelope>, ctx: HandlerContext): Promise<void> {
|
|
46
|
+
const payload = readWebhookJobPayload(job.payload)
|
|
35
47
|
try {
|
|
36
48
|
const em = ctx.resolve<EntityManager>('em')
|
|
37
|
-
const adapter = getShippingAdapter(
|
|
49
|
+
const adapter = getShippingAdapter(payload.providerKey)
|
|
38
50
|
if (!adapter) return
|
|
39
51
|
|
|
40
|
-
const shipment =
|
|
52
|
+
const shipment = payload.shipmentId && payload.scope
|
|
41
53
|
? await findOneWithDecryption(
|
|
42
54
|
em,
|
|
43
55
|
CarrierShipment,
|
|
44
56
|
{
|
|
45
|
-
id:
|
|
46
|
-
organizationId:
|
|
47
|
-
tenantId:
|
|
57
|
+
id: payload.shipmentId,
|
|
58
|
+
organizationId: payload.scope.organizationId,
|
|
59
|
+
tenantId: payload.scope.tenantId,
|
|
48
60
|
deletedAt: null,
|
|
49
61
|
},
|
|
50
62
|
undefined,
|
|
51
|
-
|
|
63
|
+
payload.scope,
|
|
52
64
|
)
|
|
53
65
|
: null
|
|
54
66
|
if (!shipment) return
|
|
@@ -56,53 +68,73 @@ export default async function handle(job: QueuedJob<WebhookJobPayload>, ctx: Han
|
|
|
56
68
|
const scope = { organizationId: shipment.organizationId, tenantId: shipment.tenantId }
|
|
57
69
|
const claimed = await claimWebhookProcessing(
|
|
58
70
|
em,
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
payload.event.idempotencyKey,
|
|
72
|
+
payload.providerKey,
|
|
61
73
|
scope,
|
|
62
|
-
|
|
74
|
+
payload.event.eventType,
|
|
63
75
|
)
|
|
64
76
|
if (!claimed) {
|
|
65
77
|
return
|
|
66
78
|
}
|
|
67
79
|
|
|
80
|
+
const carrierStatus = typeof payload.event.data.status === 'string'
|
|
81
|
+
? payload.event.data.status
|
|
82
|
+
: payload.event.eventType
|
|
83
|
+
|
|
84
|
+
let transition: { previousStatus: string; newStatus: UnifiedShipmentStatus } | null = null
|
|
68
85
|
try {
|
|
69
|
-
const carrierStatus = typeof job.payload.event.data.status === 'string'
|
|
70
|
-
? job.payload.event.data.status
|
|
71
|
-
: job.payload.event.eventType
|
|
72
86
|
const unifiedStatus = adapter.mapStatus(carrierStatus)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
87
|
+
transition = await em.transactional(async (tx) => {
|
|
88
|
+
const locked = await findOneWithDecryption(
|
|
89
|
+
tx,
|
|
90
|
+
CarrierShipment,
|
|
91
|
+
{
|
|
92
|
+
id: shipment.id,
|
|
93
|
+
organizationId: scope.organizationId,
|
|
94
|
+
tenantId: scope.tenantId,
|
|
95
|
+
deletedAt: null,
|
|
96
|
+
},
|
|
97
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE, refresh: true },
|
|
98
|
+
scope,
|
|
99
|
+
)
|
|
100
|
+
if (!locked) return null
|
|
101
|
+
const previousStatus = locked.unifiedStatus
|
|
102
|
+
if (!syncShipmentStatus(locked, unifiedStatus)) return null
|
|
103
|
+
locked.carrierStatus = carrierStatus
|
|
104
|
+
locked.lastWebhookAt = new Date()
|
|
105
|
+
await tx.flush()
|
|
106
|
+
return { previousStatus, newStatus: unifiedStatus }
|
|
107
|
+
}, { clear: true })
|
|
95
108
|
} catch (error) {
|
|
96
109
|
await releaseWebhookClaim(
|
|
97
110
|
em,
|
|
98
|
-
|
|
99
|
-
|
|
111
|
+
payload.event.idempotencyKey,
|
|
112
|
+
payload.providerKey,
|
|
100
113
|
scope,
|
|
101
114
|
)
|
|
102
115
|
throw error
|
|
103
116
|
}
|
|
117
|
+
if (!transition) return
|
|
118
|
+
|
|
119
|
+
const unifiedStatus = transition.newStatus
|
|
120
|
+
const eventPayload = {
|
|
121
|
+
shipmentId: shipment.id,
|
|
122
|
+
providerKey: payload.providerKey,
|
|
123
|
+
previousStatus: transition.previousStatus,
|
|
124
|
+
newStatus: unifiedStatus,
|
|
125
|
+
carrierStatus,
|
|
126
|
+
organizationId: shipment.organizationId,
|
|
127
|
+
tenantId: shipment.tenantId,
|
|
128
|
+
}
|
|
129
|
+
const eventScope = { tenantId: shipment.tenantId, organizationId: shipment.organizationId }
|
|
130
|
+
await emitShippingEvent('shipping_carriers.shipment.status_changed', eventPayload, eventScope)
|
|
131
|
+
if (TERMINAL_SHIPPING_STATUSES.has(unifiedStatus)) {
|
|
132
|
+
const terminalEvent = getTerminalShippingEvent(unifiedStatus)
|
|
133
|
+
if (!terminalEvent) return
|
|
134
|
+
await emitShippingEvent(terminalEvent, eventPayload, eventScope)
|
|
135
|
+
}
|
|
104
136
|
} catch (error) {
|
|
105
|
-
logger.error('Job processing failed', { providerKey:
|
|
137
|
+
logger.error('Job processing failed', { providerKey: payload.providerKey, shipmentId: payload.shipmentId, err: error })
|
|
106
138
|
throw error
|
|
107
139
|
}
|
|
108
140
|
}
|
|
@@ -353,7 +353,7 @@ export default function TimeTrackingEntriesPage() {
|
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
355
|
const [selfRes, projectsRes, tagsRes, peopleRes] = await Promise.all([
|
|
356
|
-
safeCall<{ member?: { id?: string } | null }>('/api/staff/team-members/self'),
|
|
356
|
+
safeCall<{ member?: { id?: string; displayName?: string } | null }>('/api/staff/team-members/self'),
|
|
357
357
|
safeCall<{ items?: Array<Record<string, unknown>> }>('/api/staff/timesheets/time-projects?page=1&pageSize=100'),
|
|
358
358
|
safeCall<{ items?: Array<Record<string, unknown>> }>('/api/staff/timesheets/tags?page=1&pageSize=100'),
|
|
359
359
|
safeCall<{ items?: Array<Record<string, unknown>> }>('/api/staff/team-members?page=1&pageSize=100'),
|
|
@@ -399,19 +399,25 @@ export default function TimeTrackingEntriesPage() {
|
|
|
399
399
|
)
|
|
400
400
|
|
|
401
401
|
const people = peopleRes?.ok && Array.isArray(peopleRes.result?.items) ? peopleRes.result.items : []
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
.
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
402
|
+
const personOptions = people
|
|
403
|
+
.map((row: Record<string, unknown>) => {
|
|
404
|
+
const id = typeof row.id === 'string' ? row.id : null
|
|
405
|
+
const label = typeof row.display_name === 'string'
|
|
406
|
+
? row.display_name
|
|
407
|
+
: typeof row.displayName === 'string'
|
|
408
|
+
? row.displayName
|
|
409
|
+
: null
|
|
410
|
+
return id && label ? { value: id, label } : null
|
|
411
|
+
})
|
|
412
|
+
.filter((option): option is { value: string; label: string } => option !== null)
|
|
413
|
+
// The team list is closed to a caller without team-member access, yet the
|
|
414
|
+
// self filter is seeded for everyone — name it from the self lookup so its
|
|
415
|
+
// chip never falls back to the raw member id.
|
|
416
|
+
const selfName = selfRes?.ok ? selfRes.result?.member?.displayName : undefined
|
|
417
|
+
if (selfId && selfName && !personOptions.some((option) => option.value === selfId)) {
|
|
418
|
+
personOptions.push({ value: selfId, label: selfName })
|
|
419
|
+
}
|
|
420
|
+
setPeopleOptions(personOptions)
|
|
415
421
|
})()
|
|
416
422
|
return () => { cancelled = true }
|
|
417
423
|
}, [scopeVersion])
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
ensureTenantScope,
|
|
44
44
|
extractUndoPayload,
|
|
45
45
|
requireScopedClaim,
|
|
46
|
+
throwUndoRefusal,
|
|
46
47
|
type WarrantyClaimScope,
|
|
47
48
|
} from './shared'
|
|
48
49
|
import { assertPendingClaimQuantitiesWithinSold, validateClaimReferences } from './claims'
|
|
@@ -135,6 +136,31 @@ const mutableParentStatuses = new Set([
|
|
|
135
136
|
'inspecting',
|
|
136
137
|
])
|
|
137
138
|
|
|
139
|
+
const receivingParentStatuses = new Set(['received', 'inspecting'])
|
|
140
|
+
|
|
141
|
+
type LineUndoParentRule = {
|
|
142
|
+
allowedParentStatuses: ReadonlySet<string>
|
|
143
|
+
lockedKey: string
|
|
144
|
+
lockedFallback: string
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
type LineUndoState = {
|
|
148
|
+
claim: WarrantyClaim
|
|
149
|
+
line: WarrantyClaimLine
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const mutableParentUndoRule: LineUndoParentRule = {
|
|
153
|
+
allowedParentStatuses: mutableParentStatuses,
|
|
154
|
+
lockedKey: 'warranty_claims.errors.lineLocked',
|
|
155
|
+
lockedFallback: 'This line is locked in the current claim status.',
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const receivingParentUndoRule: LineUndoParentRule = {
|
|
159
|
+
allowedParentStatuses: receivingParentStatuses,
|
|
160
|
+
lockedKey: 'warranty_claims.errors.receivingStatusInvalid',
|
|
161
|
+
lockedFallback: 'Receiving details can only be saved when the claim is received or under inspection.',
|
|
162
|
+
}
|
|
163
|
+
|
|
138
164
|
function parseCommandInput<T>(schema: z.ZodType<T>, rawInput: unknown): T {
|
|
139
165
|
const result = schema.safeParse(rawInput ?? {})
|
|
140
166
|
if (!result.success) {
|
|
@@ -482,6 +508,37 @@ async function recomputeClaimRollups(em: EntityManager, claim: WarrantyClaim): P
|
|
|
482
508
|
claim.updatedAt = new Date()
|
|
483
509
|
}
|
|
484
510
|
|
|
511
|
+
function refuseLineUndoConflict(): Promise<never> {
|
|
512
|
+
return throwUndoRefusal('warranty_claims.errors.conflict', 'This claim changed while you were editing it.')
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function requireCurrentLineUndoState(
|
|
516
|
+
em: EntityManager,
|
|
517
|
+
after: LineSnapshot | null | undefined,
|
|
518
|
+
rule: LineUndoParentRule,
|
|
519
|
+
): Promise<LineUndoState> {
|
|
520
|
+
if (!after) return refuseLineUndoConflict()
|
|
521
|
+
const scope: WarrantyClaimScope = { tenantId: after.tenantId, organizationId: after.organizationId }
|
|
522
|
+
const claim = await findOneWithDecryption(
|
|
523
|
+
em,
|
|
524
|
+
WarrantyClaim,
|
|
525
|
+
{ id: after.claimId, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },
|
|
526
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
527
|
+
scope,
|
|
528
|
+
)
|
|
529
|
+
if (!claim) return refuseLineUndoConflict()
|
|
530
|
+
if (!rule.allowedParentStatuses.has(claim.status)) return throwUndoRefusal(rule.lockedKey, rule.lockedFallback)
|
|
531
|
+
const line = await findOneWithDecryption(
|
|
532
|
+
em,
|
|
533
|
+
WarrantyClaimLine,
|
|
534
|
+
{ id: after.id, tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
535
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
536
|
+
scope,
|
|
537
|
+
)
|
|
538
|
+
if (!line || !after.updatedAt || toIso(line.updatedAt) !== after.updatedAt) return refuseLineUndoConflict()
|
|
539
|
+
return { claim, line }
|
|
540
|
+
}
|
|
541
|
+
|
|
485
542
|
async function requireScopedLine(
|
|
486
543
|
em: EntityManager,
|
|
487
544
|
lineId: string,
|
|
@@ -654,22 +711,20 @@ const createClaimLineCommand: CommandHandler<ClaimLineCreateInput, { lineId: str
|
|
|
654
711
|
buildLog: async ({ result, snapshots }) => buildLineLog('warranty_claims.audit.claim_line.create', result.lineId, snapshots),
|
|
655
712
|
undo: async ({ logEntry, ctx }) => {
|
|
656
713
|
const payload = extractUndoPayload<LineUndoPayload>(logEntry)
|
|
657
|
-
const after = payload?.after
|
|
658
|
-
if (!after) return
|
|
659
|
-
const scope = { tenantId: after.tenantId, organizationId: after.organizationId }
|
|
660
714
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
661
|
-
|
|
662
|
-
const claim = line ? claimOf(line) : null
|
|
663
|
-
if (!line || !claim) return
|
|
715
|
+
let state!: LineUndoState
|
|
664
716
|
await withAtomicFlush(em, [
|
|
717
|
+
async () => {
|
|
718
|
+
state = await requireCurrentLineUndoState(em, payload?.after, mutableParentUndoRule)
|
|
719
|
+
},
|
|
665
720
|
() => {
|
|
666
|
-
line.deletedAt = new Date()
|
|
667
|
-
line.updatedAt = new Date()
|
|
721
|
+
state.line.deletedAt = new Date()
|
|
722
|
+
state.line.updatedAt = new Date()
|
|
668
723
|
},
|
|
669
|
-
() => recomputeClaimRollups(em, claim),
|
|
724
|
+
() => recomputeClaimRollups(em, state.claim),
|
|
670
725
|
], { transaction: true, label: 'warranty_claims.claim_line.create.undo' })
|
|
671
|
-
await emitClaimUpdated(ctx, claim)
|
|
672
|
-
await emitLineUndoCrud(ctx, 'deleted', line)
|
|
726
|
+
await emitClaimUpdated(ctx, state.claim)
|
|
727
|
+
await emitLineUndoCrud(ctx, 'deleted', state.line)
|
|
673
728
|
},
|
|
674
729
|
}
|
|
675
730
|
|
|
@@ -744,17 +799,17 @@ const updateClaimLineCommand: CommandHandler<ClaimLineUpdateInput, { lineId: str
|
|
|
744
799
|
const payload = extractUndoPayload<LineUndoPayload>(logEntry)
|
|
745
800
|
const before = payload?.before
|
|
746
801
|
if (!before) return
|
|
747
|
-
const scope = { tenantId: before.tenantId, organizationId: before.organizationId }
|
|
748
802
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
749
|
-
|
|
750
|
-
if (!line) return
|
|
751
|
-
const claim = claimOf(line) ?? await requireScopedClaim(em, before.claimId, scope)
|
|
803
|
+
let state!: LineUndoState
|
|
752
804
|
await withAtomicFlush(em, [
|
|
753
|
-
() =>
|
|
754
|
-
|
|
805
|
+
async () => {
|
|
806
|
+
state = await requireCurrentLineUndoState(em, payload?.after, mutableParentUndoRule)
|
|
807
|
+
},
|
|
808
|
+
() => restoreLineFromSnapshot(state.line, before),
|
|
809
|
+
() => recomputeClaimRollups(em, state.claim),
|
|
755
810
|
], { transaction: true, label: 'warranty_claims.claim_line.update.undo' })
|
|
756
|
-
await emitClaimUpdated(ctx, claim)
|
|
757
|
-
await emitLineUndoCrud(ctx, 'updated', line)
|
|
811
|
+
await emitClaimUpdated(ctx, state.claim)
|
|
812
|
+
await emitLineUndoCrud(ctx, 'updated', state.line)
|
|
758
813
|
},
|
|
759
814
|
}
|
|
760
815
|
|
|
@@ -800,35 +855,20 @@ const deleteClaimLineCommand: CommandHandler<LineDeleteInput, { lineId: string;
|
|
|
800
855
|
const payload = extractUndoPayload<LineUndoPayload>(logEntry)
|
|
801
856
|
const before = payload?.before
|
|
802
857
|
if (!before) return
|
|
803
|
-
const scope = { tenantId: before.tenantId, organizationId: before.organizationId }
|
|
804
858
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
805
|
-
let
|
|
806
|
-
const claim = (line ? claimOf(line) : null) ?? await requireScopedClaim(em, before.claimId, scope)
|
|
859
|
+
let state!: LineUndoState
|
|
807
860
|
await withAtomicFlush(em, [
|
|
861
|
+
async () => {
|
|
862
|
+
state = await requireCurrentLineUndoState(em, payload?.after, mutableParentUndoRule)
|
|
863
|
+
},
|
|
808
864
|
() => {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
id: before.id,
|
|
812
|
-
claim,
|
|
813
|
-
organizationId: before.organizationId,
|
|
814
|
-
tenantId: before.tenantId,
|
|
815
|
-
lineNo: before.lineNo,
|
|
816
|
-
qtyClaimed: before.qtyClaimed,
|
|
817
|
-
lineStatus: before.lineStatus,
|
|
818
|
-
warrantyStatus: before.warrantyStatus,
|
|
819
|
-
createdAt: new Date(),
|
|
820
|
-
updatedAt: new Date(),
|
|
821
|
-
})
|
|
822
|
-
em.persist(line)
|
|
823
|
-
}
|
|
824
|
-
restoreLineFromSnapshot(line, before)
|
|
825
|
-
line.deletedAt = null
|
|
865
|
+
restoreLineFromSnapshot(state.line, before)
|
|
866
|
+
state.line.deletedAt = null
|
|
826
867
|
},
|
|
827
|
-
() => recomputeClaimRollups(em, claim),
|
|
868
|
+
() => recomputeClaimRollups(em, state.claim),
|
|
828
869
|
], { transaction: true, label: 'warranty_claims.claim_line.delete.undo' })
|
|
829
|
-
|
|
830
|
-
await
|
|
831
|
-
await emitLineUndoCrud(ctx, 'created', line)
|
|
870
|
+
await emitClaimUpdated(ctx, state.claim)
|
|
871
|
+
await emitLineUndoCrud(ctx, 'created', state.line)
|
|
832
872
|
},
|
|
833
873
|
}
|
|
834
874
|
|
|
@@ -916,17 +956,17 @@ const receiveClaimLineCommand: CommandHandler<ClaimLineReceiveInput, { lineId: s
|
|
|
916
956
|
const payload = extractUndoPayload<LineUndoPayload>(logEntry)
|
|
917
957
|
const before = payload?.before
|
|
918
958
|
if (!before) return
|
|
919
|
-
const scope = { tenantId: before.tenantId, organizationId: before.organizationId }
|
|
920
959
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
921
|
-
|
|
922
|
-
if (!line) return
|
|
923
|
-
const claim = await requireScopedClaim(em, before.claimId, scope)
|
|
960
|
+
let state!: LineUndoState
|
|
924
961
|
await withAtomicFlush(em, [
|
|
925
|
-
() =>
|
|
926
|
-
|
|
962
|
+
async () => {
|
|
963
|
+
state = await requireCurrentLineUndoState(em, payload?.after, receivingParentUndoRule)
|
|
964
|
+
},
|
|
965
|
+
() => restoreLineFromSnapshot(state.line, before),
|
|
966
|
+
() => recomputeClaimRollups(em, state.claim),
|
|
927
967
|
], { transaction: true, label: 'warranty_claims.claim_line.receive.undo' })
|
|
928
|
-
await emitClaimUpdated(ctx, claim)
|
|
929
|
-
await emitLineUndoCrud(ctx, 'updated', line)
|
|
968
|
+
await emitClaimUpdated(ctx, state.claim)
|
|
969
|
+
await emitLineUndoCrud(ctx, 'updated', state.line)
|
|
930
970
|
},
|
|
931
971
|
}
|
|
932
972
|
|
|
@@ -980,17 +1020,17 @@ const releaseClaimLineQuarantineCommand: CommandHandler<ClaimLineReleaseQuaranti
|
|
|
980
1020
|
const payload = extractUndoPayload<LineUndoPayload>(logEntry)
|
|
981
1021
|
const before = payload?.before
|
|
982
1022
|
if (!before) return
|
|
983
|
-
const scope = { tenantId: before.tenantId, organizationId: before.organizationId }
|
|
984
1023
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
985
|
-
|
|
986
|
-
if (!line) return
|
|
987
|
-
const claim = await requireScopedClaim(em, before.claimId, scope)
|
|
1024
|
+
let state!: LineUndoState
|
|
988
1025
|
await withAtomicFlush(em, [
|
|
989
|
-
() =>
|
|
990
|
-
|
|
1026
|
+
async () => {
|
|
1027
|
+
state = await requireCurrentLineUndoState(em, payload?.after, receivingParentUndoRule)
|
|
1028
|
+
},
|
|
1029
|
+
() => restoreLineFromSnapshot(state.line, before),
|
|
1030
|
+
() => recomputeClaimRollups(em, state.claim),
|
|
991
1031
|
], { transaction: true, label: 'warranty_claims.claim_line.release_quarantine.undo' })
|
|
992
|
-
await emitClaimUpdated(ctx, claim)
|
|
993
|
-
await emitLineUndoCrud(ctx, 'updated', line)
|
|
1032
|
+
await emitClaimUpdated(ctx, state.claim)
|
|
1033
|
+
await emitLineUndoCrud(ctx, 'updated', state.line)
|
|
994
1034
|
},
|
|
995
1035
|
}
|
|
996
1036
|
|
|
@@ -1043,16 +1083,17 @@ const setClaimLineAssessmentCommand: CommandHandler<LineSetAssessmentInput, { li
|
|
|
1043
1083
|
const payload = extractUndoPayload<LineUndoPayload>(logEntry)
|
|
1044
1084
|
const before = payload?.before
|
|
1045
1085
|
if (!before) return
|
|
1046
|
-
const scope = { tenantId: before.tenantId, organizationId: before.organizationId }
|
|
1047
1086
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
1048
|
-
|
|
1049
|
-
if (!line) return
|
|
1050
|
-
const claim = claimOf(line) ?? await requireScopedClaim(em, before.claimId, scope)
|
|
1087
|
+
let state!: LineUndoState
|
|
1051
1088
|
await withAtomicFlush(em, [
|
|
1052
|
-
() =>
|
|
1089
|
+
async () => {
|
|
1090
|
+
state = await requireCurrentLineUndoState(em, payload?.after, mutableParentUndoRule)
|
|
1091
|
+
},
|
|
1092
|
+
() => restoreLineFromSnapshot(state.line, before),
|
|
1093
|
+
() => recomputeClaimRollups(em, state.claim),
|
|
1053
1094
|
], { transaction: true, label: 'warranty_claims.claim_line.set_assessment.undo' })
|
|
1054
|
-
await emitClaimUpdated(ctx, claim)
|
|
1055
|
-
await emitLineUndoCrud(ctx, 'updated', line)
|
|
1095
|
+
await emitClaimUpdated(ctx, state.claim)
|
|
1096
|
+
await emitLineUndoCrud(ctx, 'updated', state.line)
|
|
1056
1097
|
},
|
|
1057
1098
|
}
|
|
1058
1099
|
|