@open-mercato/core 0.10.1-develop.7787.1.96c7cfc66f → 0.10.1-develop.7792.1.b0ce6289cf
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/api/audit-logs/actions/redo/route.js +4 -2
- package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js.map +2 -2
- package/dist/modules/audit_logs/api/audit-logs/actions/undo/route.js +3 -2
- package/dist/modules/audit_logs/api/audit-logs/actions/undo/route.js.map +3 -3
- package/dist/modules/entities/cli.js +146 -90
- package/dist/modules/entities/cli.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +14 -1
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/warranty_claims/commands/claims.js +13 -4
- package/dist/modules/warranty_claims/commands/claims.js.map +2 -2
- package/dist/modules/warranty_claims/lib/replacementOrderUndo.js +50 -0
- package/dist/modules/warranty_claims/lib/replacementOrderUndo.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/audit_logs/api/audit-logs/actions/redo/route.ts +4 -4
- package/src/modules/audit_logs/api/audit-logs/actions/undo/route.ts +3 -6
- package/src/modules/entities/cli.ts +193 -96
- package/src/modules/sales/commands/documents.ts +15 -1
- package/src/modules/warranty_claims/commands/claims.ts +16 -3
- package/src/modules/warranty_claims/lib/replacementOrderUndo.ts +67 -0
|
@@ -356,6 +356,7 @@ type QuoteAdjustmentSnapshot = {
|
|
|
356
356
|
type OrderGraphSnapshot = {
|
|
357
357
|
order: {
|
|
358
358
|
id: string;
|
|
359
|
+
updatedAt?: string;
|
|
359
360
|
organizationId: string;
|
|
360
361
|
tenantId: string;
|
|
361
362
|
orderNumber: string;
|
|
@@ -2033,6 +2034,7 @@ async function loadOrderSnapshot(
|
|
|
2033
2034
|
return {
|
|
2034
2035
|
order: {
|
|
2035
2036
|
id: order.id,
|
|
2037
|
+
updatedAt: order.updatedAt?.toISOString(),
|
|
2036
2038
|
organizationId: order.organizationId,
|
|
2037
2039
|
tenantId: order.tenantId,
|
|
2038
2040
|
orderNumber: order.orderNumber,
|
|
@@ -6977,6 +6979,18 @@ const deleteOrderCommand: CommandHandler<
|
|
|
6977
6979
|
em,
|
|
6978
6980
|
[
|
|
6979
6981
|
async () => {
|
|
6982
|
+
const lockedOrder = await findOneWithDecryption(
|
|
6983
|
+
em,
|
|
6984
|
+
SalesOrder,
|
|
6985
|
+
{ id, tenantId: order.tenantId, organizationId: order.organizationId },
|
|
6986
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE, refresh: true },
|
|
6987
|
+
{ tenantId: order.tenantId, organizationId: order.organizationId },
|
|
6988
|
+
);
|
|
6989
|
+
if (!lockedOrder) {
|
|
6990
|
+
const { translate } = await resolveTranslations();
|
|
6991
|
+
throw notFound(translate("sales.documents.detail.notFound", "Document not found."));
|
|
6992
|
+
}
|
|
6993
|
+
await enforceSalesDocumentOptimisticLock(ctx, lockedOrder, SALES_RESOURCE_KIND_ORDER);
|
|
6980
6994
|
if (shipmentIds.length) {
|
|
6981
6995
|
await em.nativeDelete(SalesShipmentItem, {
|
|
6982
6996
|
shipment: { $in: shipmentIds },
|
|
@@ -6999,7 +7013,7 @@ const deleteOrderCommand: CommandHandler<
|
|
|
6999
7013
|
});
|
|
7000
7014
|
await em.nativeDelete(SalesOrderAdjustment, { order: order.id });
|
|
7001
7015
|
await em.nativeDelete(SalesOrderLine, { order: order.id });
|
|
7002
|
-
em.remove(
|
|
7016
|
+
em.remove(lockedOrder);
|
|
7003
7017
|
},
|
|
7004
7018
|
],
|
|
7005
7019
|
{ transaction: true },
|
|
@@ -11,6 +11,8 @@ import { invalidateCrudCache } from '@open-mercato/shared/lib/crud/cache'
|
|
|
11
11
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
12
12
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
13
13
|
import { readSafeDecryptedString } from '../lib/decryptionSafety'
|
|
14
|
+
import { isInitialReplacementOrderApproval } from '../lib/replacementOrderUndo'
|
|
15
|
+
import { OPTIMISTIC_LOCK_HEADER_NAME } from '@open-mercato/shared/lib/crud/optimistic-lock-headers'
|
|
14
16
|
import type { EntityId } from '@open-mercato/shared/modules/entities'
|
|
15
17
|
import type { CrudEventsConfig } from '@open-mercato/shared/lib/crud/types'
|
|
16
18
|
import { E } from '#generated/entities.ids.generated'
|
|
@@ -3062,13 +3064,20 @@ async function dispatchSalesOrderDelete(
|
|
|
3062
3064
|
ctx: CommandRuntimeContext,
|
|
3063
3065
|
scope: WarrantyClaimScope,
|
|
3064
3066
|
replacementOrderId: string,
|
|
3067
|
+
expectedUpdatedAt?: string,
|
|
3065
3068
|
): Promise<void> {
|
|
3066
3069
|
const commandBus = ctx.container.resolve('commandBus') as CommandBus
|
|
3070
|
+
const dispatchContext = scrubbedDispatchContext(ctx)
|
|
3071
|
+
if (expectedUpdatedAt) {
|
|
3072
|
+
dispatchContext.request = new Request(ctx.request?.url ?? 'http://localhost', {
|
|
3073
|
+
headers: { [OPTIMISTIC_LOCK_HEADER_NAME]: expectedUpdatedAt },
|
|
3074
|
+
})
|
|
3075
|
+
}
|
|
3067
3076
|
await commandBus.execute(
|
|
3068
3077
|
'sales.orders.delete',
|
|
3069
3078
|
{
|
|
3070
3079
|
input: { id: replacementOrderId, tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
3071
|
-
ctx:
|
|
3080
|
+
ctx: dispatchContext,
|
|
3072
3081
|
},
|
|
3073
3082
|
)
|
|
3074
3083
|
}
|
|
@@ -3076,6 +3085,7 @@ async function dispatchSalesOrderDelete(
|
|
|
3076
3085
|
const createReplacementOrderCommand: CommandHandler<ClaimCreateReplacementOrderInput, ClaimCreateReplacementOrderResult> = {
|
|
3077
3086
|
id: 'warranty_claims.claim.create_replacement_order',
|
|
3078
3087
|
isUndoable: true,
|
|
3088
|
+
redo: ({ input, ctx }) => createReplacementOrderCommand.execute({ ...input, updatedAt: undefined }, ctx),
|
|
3079
3089
|
prepare: async (rawInput, ctx) => {
|
|
3080
3090
|
const input = parseCommandInput(claimCreateReplacementOrderSchema, rawInput)
|
|
3081
3091
|
const scope = resolveScope(ctx, input)
|
|
@@ -3336,10 +3346,13 @@ const createReplacementOrderCommand: CommandHandler<ClaimCreateReplacementOrderI
|
|
|
3336
3346
|
const currentUpdatedAt = currentRow.updated_at
|
|
3337
3347
|
? new Date(currentRow.updated_at as string | Date).toISOString()
|
|
3338
3348
|
: null
|
|
3339
|
-
if (!replacementOrder.updatedAt || !currentUpdatedAt ||
|
|
3349
|
+
if (!replacementOrder.updatedAt || !currentUpdatedAt || (
|
|
3350
|
+
currentUpdatedAt !== replacementOrder.updatedAt
|
|
3351
|
+
&& !await isInitialReplacementOrderApproval(ctx, scope, replacementOrder.id, replacementOrder.updatedAt, currentUpdatedAt)
|
|
3352
|
+
)) {
|
|
3340
3353
|
throw new CrudHttpError(409, { error: 'warranty_claims.errors.replacementOrderChangedUndoAborted' })
|
|
3341
3354
|
}
|
|
3342
|
-
await dispatchSalesOrderDelete(ctx, scope, replacementOrder.id)
|
|
3355
|
+
await dispatchSalesOrderDelete(ctx, scope, replacementOrder.id, currentUpdatedAt)
|
|
3343
3356
|
} else {
|
|
3344
3357
|
replacementOrderBridgeLogger.info('warranty_claims.claim.create_replacement_order undo found replacement order already absent', {
|
|
3345
3358
|
claimId: before.id,
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util'
|
|
2
|
+
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
3
|
+
import type { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'
|
|
4
|
+
import type { WarrantyClaimScope } from '../commands/shared'
|
|
5
|
+
|
|
6
|
+
type OrderAuditEntry = NonNullable<Awaited<ReturnType<ActionLogService['findById']>>>
|
|
7
|
+
|
|
8
|
+
function record(value: unknown): Record<string, unknown> | null {
|
|
9
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
10
|
+
? value as Record<string, unknown>
|
|
11
|
+
: null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function isInitialReplacementOrderApproval(
|
|
15
|
+
ctx: CommandRuntimeContext,
|
|
16
|
+
scope: WarrantyClaimScope,
|
|
17
|
+
orderId: string,
|
|
18
|
+
savedVersion: string,
|
|
19
|
+
currentVersion: string,
|
|
20
|
+
): Promise<boolean> {
|
|
21
|
+
let logs: ActionLogService
|
|
22
|
+
try {
|
|
23
|
+
logs = ctx.container.resolve<ActionLogService>('actionLogService')
|
|
24
|
+
} catch {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
if (!logs) return false
|
|
28
|
+
const history = await logs.list({
|
|
29
|
+
...scope,
|
|
30
|
+
resourceKind: 'sales.order',
|
|
31
|
+
resourceId: orderId,
|
|
32
|
+
includeRelated: true,
|
|
33
|
+
after: new Date(savedVersion),
|
|
34
|
+
pageSize: 3,
|
|
35
|
+
})
|
|
36
|
+
const entries: OrderAuditEntry[] = history.items
|
|
37
|
+
if (history.total > 3 || entries.length !== history.total) return false
|
|
38
|
+
const edits = entries.filter((entry) => !(entry.commandId === 'sales.orders.create'
|
|
39
|
+
&& entry.resourceKind === 'sales.order' && entry.resourceId === orderId
|
|
40
|
+
&& entry.tenantId === scope.tenantId && entry.organizationId === scope.organizationId))
|
|
41
|
+
if (edits.length !== 1) return false
|
|
42
|
+
const entry = edits[0]
|
|
43
|
+
if (entry.commandId !== 'sales.orders.update' || entry.executionState !== 'done') return false
|
|
44
|
+
if (entry.tenantId !== scope.tenantId || entry.organizationId !== scope.organizationId
|
|
45
|
+
|| entry.resourceKind !== 'sales.order' || entry.resourceId !== orderId) return false
|
|
46
|
+
const before = record(entry.snapshotBefore)
|
|
47
|
+
const after = record(entry.snapshotAfter)
|
|
48
|
+
const beforeOrder = record(before?.order)
|
|
49
|
+
const afterOrder = record(after?.order)
|
|
50
|
+
if (!beforeOrder || !afterOrder || beforeOrder.id !== orderId || afterOrder.id !== orderId) return false
|
|
51
|
+
if (beforeOrder.updatedAt !== savedVersion || afterOrder.updatedAt !== currentVersion) return false
|
|
52
|
+
if (beforeOrder.status !== null || beforeOrder.statusEntryId !== null || afterOrder.status !== 'pending_approval') return false
|
|
53
|
+
const comparableOrder = (order: Record<string, unknown>) => Object.fromEntries(
|
|
54
|
+
Object.entries(order).filter(([key]) => !['status', 'statusEntryId', 'updatedAt'].includes(key)),
|
|
55
|
+
)
|
|
56
|
+
if (!isDeepStrictEqual(comparableOrder(beforeOrder), comparableOrder(afterOrder))) return false
|
|
57
|
+
const input = record(record(entry.commandPayload)?.__redoInput)
|
|
58
|
+
if (!input || input.id !== orderId || typeof input.statusEntryId !== 'string'
|
|
59
|
+
|| input.statusEntryId !== afterOrder.statusEntryId
|
|
60
|
+
|| Object.keys(input).some((key) => !['id', 'statusEntryId'].includes(key))) return false
|
|
61
|
+
const beforeNotes = before?.notes
|
|
62
|
+
const afterNotes = after?.notes
|
|
63
|
+
if (!Array.isArray(beforeNotes) || !Array.isArray(afterNotes) || afterNotes.length !== beforeNotes.length + 1) return false
|
|
64
|
+
if (!beforeNotes.every((note) => afterNotes.some((candidate) => isDeepStrictEqual(note, candidate)))) return false
|
|
65
|
+
return ['lines', 'adjustments', 'addresses', 'tags', 'shipments', 'payments']
|
|
66
|
+
.every((key) => Array.isArray(before?.[key]) && isDeepStrictEqual(before?.[key], after?.[key]))
|
|
67
|
+
}
|