@open-mercato/core 0.9.1-develop.7513.1.ae2715553f → 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/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/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/package.json +7 -7
- 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/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
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
|
-
import { assertFound } from "@open-mercato/shared/lib/crud/errors";
|
|
2
|
+
import { CrudHttpError, assertFound } from "@open-mercato/shared/lib/crud/errors";
|
|
3
3
|
import { invalidateCrudCache } from "@open-mercato/shared/lib/crud/cache";
|
|
4
4
|
import { enforceCommandOptimisticLockWithGuards } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
|
|
5
5
|
import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
|
|
6
6
|
import { emitCrudSideEffects, flushCrudSideEffects } from "@open-mercato/shared/lib/commands/helpers";
|
|
7
7
|
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
8
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
8
9
|
import {
|
|
9
10
|
WarrantyClaim,
|
|
10
11
|
WarrantyClaimEvent,
|
|
@@ -26,6 +27,10 @@ async function enforceWarrantyClaimOptimisticLock(ctx, record, resourceKind = WA
|
|
|
26
27
|
request: ctx.request ?? null
|
|
27
28
|
});
|
|
28
29
|
}
|
|
30
|
+
async function throwUndoRefusal(key, fallback) {
|
|
31
|
+
const { translate } = await resolveTranslations();
|
|
32
|
+
throw new CrudHttpError(409, { error: translate(key, fallback), code: key });
|
|
33
|
+
}
|
|
29
34
|
async function loadScopedClaim(em, id, scope, options = {}) {
|
|
30
35
|
return findOneWithDecryption(
|
|
31
36
|
em,
|
|
@@ -157,6 +162,7 @@ export {
|
|
|
157
162
|
extractUndoPayload,
|
|
158
163
|
loadScopedClaim,
|
|
159
164
|
reconcileVendorRecoverySourceClaim,
|
|
160
|
-
requireScopedClaim
|
|
165
|
+
requireScopedClaim,
|
|
166
|
+
throwUndoRefusal
|
|
161
167
|
};
|
|
162
168
|
//# sourceMappingURL=shared.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/warranty_claims/commands/shared.ts"],
|
|
4
|
-
"sourcesContent": ["import { randomUUID } from 'crypto'\nimport type { FindOneOptions } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { assertFound } from '@open-mercato/shared/lib/crud/errors'\nimport { invalidateCrudCache } from '@open-mercato/shared/lib/crud/cache'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { emitCrudSideEffects, flushCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n WarrantyClaim,\n WarrantyClaimEvent,\n WarrantyClaimLine,\n} from '../data/entities'\nimport type {\n WarrantyClaimEventKind,\n WarrantyClaimEventVisibility,\n} from '../data/validators'\nimport { computeHeaderRollups } from '../lib/stateMachine'\n\nexport { assertFound } from '@open-mercato/shared/lib/crud/errors'\nexport { ensureOrganizationScope, ensureSameScope, ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'\nexport { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'\n\nexport const WARRANTY_CLAIM_RESOURCE_KIND = 'warranty_claims.claim'\nexport const WARRANTY_CLAIM_LINE_RESOURCE_KIND = 'warranty_claims.claim_line'\n\nexport type WarrantyClaimScope = {\n organizationId: string\n tenantId: string\n}\n\nexport type VersionedRecord = {\n id: string\n updatedAt?: Date | string | null\n}\n\nexport type ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport type AppendClaimEventInput = {\n visibility?: WarrantyClaimEventVisibility\n body?: string | null\n payload?: Record<string, unknown> | null\n actorUserId?: string | null\n actorCustomerId?: string | null\n}\n\nexport async function enforceWarrantyClaimOptimisticLock(\n ctx: CommandRuntimeContext,\n record: VersionedRecord | null | undefined,\n resourceKind = WARRANTY_CLAIM_RESOURCE_KIND,\n expected?: string | Date | null,\n): Promise<void> {\n if (!record) return\n await enforceCommandOptimisticLockWithGuards(ctx.container, {\n resourceKind,\n resourceId: record.id,\n current: record.updatedAt ?? null,\n expected,\n request: ctx.request ?? null,\n })\n}\n\nexport async function loadScopedClaim(\n em: EntityManager,\n id: string,\n scope: WarrantyClaimScope,\n options: FindOneOptions<WarrantyClaim> = {},\n): Promise<WarrantyClaim | null> {\n return findOneWithDecryption(\n em,\n WarrantyClaim,\n { id, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },\n options,\n scope,\n )\n}\n\n/**\n * `notFoundMessage` is passed to `assertFound` verbatim, so request-scoped callers\n * must hand in an already-translated sentence; the key default only suits the\n * command surface, which runs without a request locale.\n */\nexport async function requireScopedClaim(\n em: EntityManager,\n id: string,\n scope: WarrantyClaimScope,\n options: FindOneOptions<WarrantyClaim> = {},\n notFoundMessage = 'warranty_claims.errors.notFound',\n): Promise<WarrantyClaim> {\n const claim = await loadScopedClaim(em, id, scope, options)\n return assertFound(claim, notFoundMessage)\n}\n\nexport function appendClaimEvent(\n em: EntityManager,\n claim: WarrantyClaim,\n kind: WarrantyClaimEventKind,\n input: AppendClaimEventInput = {},\n): WarrantyClaimEvent {\n const event = em.create(WarrantyClaimEvent, {\n id: randomUUID(),\n claim,\n organizationId: claim.organizationId,\n tenantId: claim.tenantId,\n kind,\n visibility: input.visibility ?? 'internal',\n body: input.body ?? null,\n payload: input.payload ?? null,\n actorUserId: input.actorUserId ?? null,\n actorCustomerId: input.actorCustomerId ?? null,\n createdAt: new Date(),\n })\n em.persist(event)\n return event\n}\n\nexport async function reconcileVendorRecoverySourceClaim(\n ctx: ResolverContext,\n input: { claimId: string; tenantId: string; organizationId: string },\n): Promise<void> {\n const em = (ctx.resolve('em') as EntityManager).fork()\n const scope = { tenantId: input.tenantId, organizationId: input.organizationId }\n const recoveryClaim = await findOneWithDecryption(\n em,\n WarrantyClaim,\n { id: input.claimId, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },\n {},\n scope,\n )\n if (!recoveryClaim || recoveryClaim.claimType !== 'vendor_recovery' || !recoveryClaim.sourceClaimId) return\n\n const sourceClaim = await findOneWithDecryption(\n em,\n WarrantyClaim,\n { id: recoveryClaim.sourceClaimId, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },\n {},\n scope,\n )\n if (!sourceClaim) return\n\n const resolvedChildren = await findWithDecryption(\n em,\n WarrantyClaim,\n {\n sourceClaimId: sourceClaim.id,\n claimType: 'vendor_recovery',\n status: { $in: ['resolved', 'closed'] },\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n {},\n scope,\n )\n const childLines = resolvedChildren.length\n ? await findWithDecryption(\n em,\n WarrantyClaimLine,\n {\n claim: { $in: resolvedChildren.map((child) => child.id) },\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n {},\n scope,\n )\n : []\n const recoveredTotal = computeHeaderRollups(childLines).totalApprovedAmount\n await withAtomicFlush(\n em,\n [\n () => {\n sourceClaim.totalRecoveredAmount = String(recoveredTotal)\n sourceClaim.updatedAt = new Date()\n },\n ],\n { transaction: true, label: 'warranty_claims.vendor_recovery.reconciliation' },\n )\n\n const dataEngine = ctx.resolve<DataEngine>('dataEngine')\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: sourceClaim,\n identifiers: {\n id: sourceClaim.id,\n organizationId: sourceClaim.organizationId,\n tenantId: sourceClaim.tenantId,\n },\n indexer: { entityType: 'warranty_claims:warranty_claim' },\n events: {\n module: 'warranty_claims',\n entity: 'claim',\n persistent: true,\n buildPayload: () => ({\n id: sourceClaim.id,\n organizationId: sourceClaim.organizationId,\n tenantId: sourceClaim.tenantId,\n claimType: sourceClaim.claimType,\n status: sourceClaim.status,\n }),\n },\n })\n await flushCrudSideEffects(dataEngine)\n await invalidateCrudCache(\n ctx as unknown as Parameters<typeof invalidateCrudCache>[0],\n 'warranty_claims.claim',\n { id: sourceClaim.id, organizationId: sourceClaim.organizationId, tenantId: sourceClaim.tenantId },\n input.tenantId,\n 'warranty_claims.vendor_recovery.reconciliation',\n )\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,kBAAkB;AAI3B,SAAS,mBAAmB;
|
|
4
|
+
"sourcesContent": ["import { randomUUID } from 'crypto'\nimport type { FindOneOptions } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError, assertFound } from '@open-mercato/shared/lib/crud/errors'\nimport { invalidateCrudCache } from '@open-mercato/shared/lib/crud/cache'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { emitCrudSideEffects, flushCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport {\n WarrantyClaim,\n WarrantyClaimEvent,\n WarrantyClaimLine,\n} from '../data/entities'\nimport type {\n WarrantyClaimEventKind,\n WarrantyClaimEventVisibility,\n} from '../data/validators'\nimport { computeHeaderRollups } from '../lib/stateMachine'\n\nexport { assertFound } from '@open-mercato/shared/lib/crud/errors'\nexport { ensureOrganizationScope, ensureSameScope, ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'\nexport { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'\n\nexport const WARRANTY_CLAIM_RESOURCE_KIND = 'warranty_claims.claim'\nexport const WARRANTY_CLAIM_LINE_RESOURCE_KIND = 'warranty_claims.claim_line'\n\nexport type WarrantyClaimScope = {\n organizationId: string\n tenantId: string\n}\n\nexport type VersionedRecord = {\n id: string\n updatedAt?: Date | string | null\n}\n\nexport type ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport type AppendClaimEventInput = {\n visibility?: WarrantyClaimEventVisibility\n body?: string | null\n payload?: Record<string, unknown> | null\n actorUserId?: string | null\n actorCustomerId?: string | null\n}\n\nexport async function enforceWarrantyClaimOptimisticLock(\n ctx: CommandRuntimeContext,\n record: VersionedRecord | null | undefined,\n resourceKind = WARRANTY_CLAIM_RESOURCE_KIND,\n expected?: string | Date | null,\n): Promise<void> {\n if (!record) return\n await enforceCommandOptimisticLockWithGuards(ctx.container, {\n resourceKind,\n resourceId: record.id,\n current: record.updatedAt ?? null,\n expected,\n request: ctx.request ?? null,\n })\n}\n\nexport async function throwUndoRefusal(key: string, fallback: string): Promise<never> {\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(409, { error: translate(key, fallback), code: key })\n}\n\nexport async function loadScopedClaim(\n em: EntityManager,\n id: string,\n scope: WarrantyClaimScope,\n options: FindOneOptions<WarrantyClaim> = {},\n): Promise<WarrantyClaim | null> {\n return findOneWithDecryption(\n em,\n WarrantyClaim,\n { id, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },\n options,\n scope,\n )\n}\n\n/**\n * `notFoundMessage` is passed to `assertFound` verbatim, so request-scoped callers\n * must hand in an already-translated sentence; the key default only suits the\n * command surface, which runs without a request locale.\n */\nexport async function requireScopedClaim(\n em: EntityManager,\n id: string,\n scope: WarrantyClaimScope,\n options: FindOneOptions<WarrantyClaim> = {},\n notFoundMessage = 'warranty_claims.errors.notFound',\n): Promise<WarrantyClaim> {\n const claim = await loadScopedClaim(em, id, scope, options)\n return assertFound(claim, notFoundMessage)\n}\n\nexport function appendClaimEvent(\n em: EntityManager,\n claim: WarrantyClaim,\n kind: WarrantyClaimEventKind,\n input: AppendClaimEventInput = {},\n): WarrantyClaimEvent {\n const event = em.create(WarrantyClaimEvent, {\n id: randomUUID(),\n claim,\n organizationId: claim.organizationId,\n tenantId: claim.tenantId,\n kind,\n visibility: input.visibility ?? 'internal',\n body: input.body ?? null,\n payload: input.payload ?? null,\n actorUserId: input.actorUserId ?? null,\n actorCustomerId: input.actorCustomerId ?? null,\n createdAt: new Date(),\n })\n em.persist(event)\n return event\n}\n\nexport async function reconcileVendorRecoverySourceClaim(\n ctx: ResolverContext,\n input: { claimId: string; tenantId: string; organizationId: string },\n): Promise<void> {\n const em = (ctx.resolve('em') as EntityManager).fork()\n const scope = { tenantId: input.tenantId, organizationId: input.organizationId }\n const recoveryClaim = await findOneWithDecryption(\n em,\n WarrantyClaim,\n { id: input.claimId, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },\n {},\n scope,\n )\n if (!recoveryClaim || recoveryClaim.claimType !== 'vendor_recovery' || !recoveryClaim.sourceClaimId) return\n\n const sourceClaim = await findOneWithDecryption(\n em,\n WarrantyClaim,\n { id: recoveryClaim.sourceClaimId, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },\n {},\n scope,\n )\n if (!sourceClaim) return\n\n const resolvedChildren = await findWithDecryption(\n em,\n WarrantyClaim,\n {\n sourceClaimId: sourceClaim.id,\n claimType: 'vendor_recovery',\n status: { $in: ['resolved', 'closed'] },\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n {},\n scope,\n )\n const childLines = resolvedChildren.length\n ? await findWithDecryption(\n em,\n WarrantyClaimLine,\n {\n claim: { $in: resolvedChildren.map((child) => child.id) },\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n {},\n scope,\n )\n : []\n const recoveredTotal = computeHeaderRollups(childLines).totalApprovedAmount\n await withAtomicFlush(\n em,\n [\n () => {\n sourceClaim.totalRecoveredAmount = String(recoveredTotal)\n sourceClaim.updatedAt = new Date()\n },\n ],\n { transaction: true, label: 'warranty_claims.vendor_recovery.reconciliation' },\n )\n\n const dataEngine = ctx.resolve<DataEngine>('dataEngine')\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: sourceClaim,\n identifiers: {\n id: sourceClaim.id,\n organizationId: sourceClaim.organizationId,\n tenantId: sourceClaim.tenantId,\n },\n indexer: { entityType: 'warranty_claims:warranty_claim' },\n events: {\n module: 'warranty_claims',\n entity: 'claim',\n persistent: true,\n buildPayload: () => ({\n id: sourceClaim.id,\n organizationId: sourceClaim.organizationId,\n tenantId: sourceClaim.tenantId,\n claimType: sourceClaim.claimType,\n status: sourceClaim.status,\n }),\n },\n })\n await flushCrudSideEffects(dataEngine)\n await invalidateCrudCache(\n ctx as unknown as Parameters<typeof invalidateCrudCache>[0],\n 'warranty_claims.claim',\n { id: sourceClaim.id, organizationId: sourceClaim.organizationId, tenantId: sourceClaim.tenantId },\n input.tenantId,\n 'warranty_claims.vendor_recovery.reconciliation',\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["assertFound"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.9.1-develop.
|
|
3
|
+
"version": "0.9.1-develop.7515.1.e2712c8b00",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -264,16 +264,16 @@
|
|
|
264
264
|
"zod": "^4.4.3"
|
|
265
265
|
},
|
|
266
266
|
"peerDependencies": {
|
|
267
|
-
"@open-mercato/ai-assistant": "0.9.1-develop.
|
|
268
|
-
"@open-mercato/shared": "0.9.1-develop.
|
|
269
|
-
"@open-mercato/ui": "0.9.1-develop.
|
|
267
|
+
"@open-mercato/ai-assistant": "0.9.1-develop.7515.1.e2712c8b00",
|
|
268
|
+
"@open-mercato/shared": "0.9.1-develop.7515.1.e2712c8b00",
|
|
269
|
+
"@open-mercato/ui": "0.9.1-develop.7515.1.e2712c8b00",
|
|
270
270
|
"react": "^19.0.0",
|
|
271
271
|
"react-dom": "^19.0.0"
|
|
272
272
|
},
|
|
273
273
|
"devDependencies": {
|
|
274
|
-
"@open-mercato/ai-assistant": "0.9.1-develop.
|
|
275
|
-
"@open-mercato/shared": "0.9.1-develop.
|
|
276
|
-
"@open-mercato/ui": "0.9.1-develop.
|
|
274
|
+
"@open-mercato/ai-assistant": "0.9.1-develop.7515.1.e2712c8b00",
|
|
275
|
+
"@open-mercato/shared": "0.9.1-develop.7515.1.e2712c8b00",
|
|
276
|
+
"@open-mercato/ui": "0.9.1-develop.7515.1.e2712c8b00",
|
|
277
277
|
"@testing-library/dom": "^10.4.1",
|
|
278
278
|
"@testing-library/jest-dom": "^7.0.1",
|
|
279
279
|
"@testing-library/react": "^16.3.3",
|
|
@@ -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
|
}
|
|
@@ -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
|
|