@open-mercato/core 0.6.6-develop.5617.1.62538c48ca → 0.6.6-develop.5619.1.29f01e2c42
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/sales/acl.js +6 -0
- package/dist/modules/sales/acl.js.map +2 -2
- package/dist/modules/sales/api/returns/route.js +43 -3
- package/dist/modules/sales/api/returns/route.js.map +2 -2
- package/dist/modules/sales/commands/returns.js +473 -213
- package/dist/modules/sales/commands/returns.js.map +2 -2
- package/dist/modules/sales/commands/shared.js +2 -0
- package/dist/modules/sales/commands/shared.js.map +2 -2
- package/dist/modules/sales/components/documents/ReturnEditDialog.js +125 -0
- package/dist/modules/sales/components/documents/ReturnEditDialog.js.map +7 -0
- package/dist/modules/sales/components/documents/ReturnsSection.js +102 -6
- package/dist/modules/sales/components/documents/ReturnsSection.js.map +2 -2
- package/dist/modules/sales/data/validators.js +13 -0
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/sales/setup.js +1 -0
- package/dist/modules/sales/setup.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/sales/acl.ts +6 -0
- package/src/modules/sales/api/returns/route.ts +41 -3
- package/src/modules/sales/commands/returns.ts +561 -229
- package/src/modules/sales/commands/shared.ts +1 -0
- package/src/modules/sales/components/documents/ReturnEditDialog.tsx +157 -0
- package/src/modules/sales/components/documents/ReturnsSection.tsx +105 -3
- package/src/modules/sales/data/validators.ts +15 -0
- package/src/modules/sales/i18n/de.json +11 -0
- package/src/modules/sales/i18n/en.json +11 -0
- package/src/modules/sales/i18n/es.json +11 -0
- package/src/modules/sales/i18n/pl.json +11 -0
- package/src/modules/sales/setup.ts +1 -0
|
@@ -7,10 +7,14 @@ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
|
7
7
|
import { emitCrudSideEffects } from "@open-mercato/shared/lib/commands/helpers";
|
|
8
8
|
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
9
9
|
import { SalesDocumentNumberGenerator } from "../services/salesDocumentNumberGenerator.js";
|
|
10
|
-
import { cloneJson, ensureOrganizationScope, ensureSameScope, ensureTenantScope, extractUndoPayload, toNumericString, enforceSalesDocumentOptimisticLock, SALES_RESOURCE_KIND_ORDER } from "./shared.js";
|
|
10
|
+
import { cloneJson, ensureOrganizationScope, ensureSameScope, ensureTenantScope, extractUndoPayload, toNumericString, enforceSalesDocumentOptimisticLock, SALES_RESOURCE_KIND_ORDER, SALES_RESOURCE_KIND_RETURN } from "./shared.js";
|
|
11
11
|
import { resolveRedoSnapshot } from "@open-mercato/shared/lib/commands/redo";
|
|
12
12
|
import { SalesOrder, SalesOrderAdjustment, SalesOrderLine, SalesReturn, SalesReturnLine } from "../data/entities.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
returnCreateSchema,
|
|
15
|
+
returnUpdateSchema,
|
|
16
|
+
returnDeleteSchema
|
|
17
|
+
} from "../data/validators.js";
|
|
14
18
|
import { E } from "../../../generated/entities.ids.generated.js";
|
|
15
19
|
const returnCrudEvents = {
|
|
16
20
|
module: "sales",
|
|
@@ -191,6 +195,228 @@ async function loadReturnSnapshot(em, id) {
|
|
|
191
195
|
adjustmentIds
|
|
192
196
|
};
|
|
193
197
|
}
|
|
198
|
+
async function loadReturnHeaderSnapshot(em, id) {
|
|
199
|
+
const header = await findOneWithDecryption(em, SalesReturn, { id, deletedAt: null }, { populate: ["order"] }, {});
|
|
200
|
+
if (!header || !header.order) return null;
|
|
201
|
+
const orderId = typeof header.order === "string" ? header.order : header.order.id;
|
|
202
|
+
return {
|
|
203
|
+
id: header.id,
|
|
204
|
+
orderId,
|
|
205
|
+
organizationId: header.organizationId,
|
|
206
|
+
tenantId: header.tenantId,
|
|
207
|
+
reason: header.reason ?? null,
|
|
208
|
+
notes: header.notes ?? null,
|
|
209
|
+
returnedAt: header.returnedAt ? header.returnedAt.toISOString() : null
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
async function reverseReturnEffects(em, salesCalculationService, snapshot) {
|
|
213
|
+
const order = await findOneWithDecryption(
|
|
214
|
+
em,
|
|
215
|
+
SalesOrder,
|
|
216
|
+
{ id: snapshot.orderId, deletedAt: null },
|
|
217
|
+
{},
|
|
218
|
+
{ tenantId: snapshot.tenantId, organizationId: snapshot.organizationId }
|
|
219
|
+
);
|
|
220
|
+
if (!order) return;
|
|
221
|
+
let lines = [];
|
|
222
|
+
await withAtomicFlush(
|
|
223
|
+
em,
|
|
224
|
+
[
|
|
225
|
+
async () => {
|
|
226
|
+
lines = await findWithDecryption(
|
|
227
|
+
em,
|
|
228
|
+
SalesOrderLine,
|
|
229
|
+
{ order: order.id, deletedAt: null },
|
|
230
|
+
{},
|
|
231
|
+
{ tenantId: snapshot.tenantId, organizationId: snapshot.organizationId }
|
|
232
|
+
);
|
|
233
|
+
const lineMap = new Map(lines.map((line) => [line.id, line]));
|
|
234
|
+
snapshot.lines.forEach((entry) => {
|
|
235
|
+
const line = lineMap.get(entry.orderLineId);
|
|
236
|
+
if (!line) return;
|
|
237
|
+
const next = Math.max(0, toNumeric(line.returnedQuantity) - entry.quantityReturned);
|
|
238
|
+
line.returnedQuantity = next.toString();
|
|
239
|
+
line.updatedAt = /* @__PURE__ */ new Date();
|
|
240
|
+
em.persist(line);
|
|
241
|
+
});
|
|
242
|
+
},
|
|
243
|
+
async () => {
|
|
244
|
+
if (snapshot.adjustmentIds.length) {
|
|
245
|
+
const adjustments = await findWithDecryption(
|
|
246
|
+
em,
|
|
247
|
+
SalesOrderAdjustment,
|
|
248
|
+
{ id: { $in: snapshot.adjustmentIds }, deletedAt: null },
|
|
249
|
+
{},
|
|
250
|
+
{ tenantId: snapshot.tenantId, organizationId: snapshot.organizationId }
|
|
251
|
+
);
|
|
252
|
+
adjustments.forEach((adj) => em.remove(adj));
|
|
253
|
+
}
|
|
254
|
+
const header = await findOneWithDecryption(
|
|
255
|
+
em,
|
|
256
|
+
SalesReturn,
|
|
257
|
+
{ id: snapshot.id, deletedAt: null },
|
|
258
|
+
{},
|
|
259
|
+
{ tenantId: snapshot.tenantId, organizationId: snapshot.organizationId }
|
|
260
|
+
);
|
|
261
|
+
const returnLines = await findWithDecryption(
|
|
262
|
+
em,
|
|
263
|
+
SalesReturnLine,
|
|
264
|
+
{ salesReturn: snapshot.id, deletedAt: null },
|
|
265
|
+
{},
|
|
266
|
+
{ tenantId: snapshot.tenantId, organizationId: snapshot.organizationId }
|
|
267
|
+
);
|
|
268
|
+
returnLines.forEach((line) => em.remove(line));
|
|
269
|
+
if (header) em.remove(header);
|
|
270
|
+
const existingAdjustments = await findWithDecryption(
|
|
271
|
+
em,
|
|
272
|
+
SalesOrderAdjustment,
|
|
273
|
+
{ order: order.id, deletedAt: null },
|
|
274
|
+
{ orderBy: { position: "asc" } },
|
|
275
|
+
{ tenantId: snapshot.tenantId, organizationId: snapshot.organizationId }
|
|
276
|
+
);
|
|
277
|
+
const lineSnapshots = lines.map(mapOrderLineEntityToSnapshot);
|
|
278
|
+
const adjustmentDrafts = existingAdjustments.map(mapOrderAdjustmentToDraft);
|
|
279
|
+
const calculation = await salesCalculationService.calculateDocumentTotals({
|
|
280
|
+
documentKind: "order",
|
|
281
|
+
lines: lineSnapshots,
|
|
282
|
+
adjustments: adjustmentDrafts,
|
|
283
|
+
context: buildCalculationContext(order)
|
|
284
|
+
});
|
|
285
|
+
applyOrderTotals(order, calculation.totals, calculation.lines.length);
|
|
286
|
+
order.updatedAt = /* @__PURE__ */ new Date();
|
|
287
|
+
em.persist(order);
|
|
288
|
+
}
|
|
289
|
+
],
|
|
290
|
+
{ transaction: true }
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
async function restoreReturnEffects(em, salesCalculationService, snapshot) {
|
|
294
|
+
const returnId = snapshot.id;
|
|
295
|
+
const createdLines = [];
|
|
296
|
+
await withAtomicFlush(
|
|
297
|
+
em,
|
|
298
|
+
[
|
|
299
|
+
async () => {
|
|
300
|
+
const order = await findOneWithDecryption(
|
|
301
|
+
em,
|
|
302
|
+
SalesOrder,
|
|
303
|
+
{ id: snapshot.orderId, deletedAt: null },
|
|
304
|
+
{},
|
|
305
|
+
{ tenantId: snapshot.tenantId, organizationId: snapshot.organizationId }
|
|
306
|
+
);
|
|
307
|
+
if (!order) {
|
|
308
|
+
throw new CrudHttpError(404, { error: "sales.returns.orderMissing" });
|
|
309
|
+
}
|
|
310
|
+
ensureSameScope(order, snapshot.organizationId, snapshot.tenantId);
|
|
311
|
+
const orderLines = await findWithDecryption(
|
|
312
|
+
em,
|
|
313
|
+
SalesOrderLine,
|
|
314
|
+
{ order: order.id, deletedAt: null },
|
|
315
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
316
|
+
{ tenantId: snapshot.tenantId, organizationId: snapshot.organizationId }
|
|
317
|
+
);
|
|
318
|
+
const lineMap = new Map(orderLines.map((line) => [line.id, line]));
|
|
319
|
+
const existingAdjustments = await findWithDecryption(
|
|
320
|
+
em,
|
|
321
|
+
SalesOrderAdjustment,
|
|
322
|
+
{ order: order.id, deletedAt: null },
|
|
323
|
+
{ orderBy: { position: "asc" } },
|
|
324
|
+
{ tenantId: snapshot.tenantId, organizationId: snapshot.organizationId }
|
|
325
|
+
);
|
|
326
|
+
const positionStart = existingAdjustments.reduce((acc, adj) => Math.max(acc, adj.position ?? 0), 0) + 1;
|
|
327
|
+
const restoredHeader = await findOneWithDecryption(
|
|
328
|
+
em,
|
|
329
|
+
SalesReturn,
|
|
330
|
+
{ id: snapshot.id },
|
|
331
|
+
{},
|
|
332
|
+
{ tenantId: snapshot.tenantId, organizationId: snapshot.organizationId }
|
|
333
|
+
) ?? em.create(SalesReturn, {
|
|
334
|
+
id: snapshot.id,
|
|
335
|
+
order,
|
|
336
|
+
organizationId: snapshot.organizationId,
|
|
337
|
+
tenantId: snapshot.tenantId,
|
|
338
|
+
returnNumber: snapshot.returnNumber,
|
|
339
|
+
reason: snapshot.reason ?? null,
|
|
340
|
+
notes: snapshot.notes ?? null,
|
|
341
|
+
returnedAt: snapshot.returnedAt ? new Date(snapshot.returnedAt) : /* @__PURE__ */ new Date(),
|
|
342
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
343
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
344
|
+
});
|
|
345
|
+
restoredHeader.order = order;
|
|
346
|
+
restoredHeader.deletedAt = null;
|
|
347
|
+
restoredHeader.organizationId = snapshot.organizationId;
|
|
348
|
+
restoredHeader.tenantId = snapshot.tenantId;
|
|
349
|
+
restoredHeader.returnNumber = snapshot.returnNumber;
|
|
350
|
+
restoredHeader.reason = snapshot.reason ?? null;
|
|
351
|
+
restoredHeader.notes = snapshot.notes ?? null;
|
|
352
|
+
restoredHeader.returnedAt = snapshot.returnedAt ? new Date(snapshot.returnedAt) : /* @__PURE__ */ new Date();
|
|
353
|
+
restoredHeader.updatedAt = /* @__PURE__ */ new Date();
|
|
354
|
+
em.persist(restoredHeader);
|
|
355
|
+
const createdAdjustments = [];
|
|
356
|
+
snapshot.lines.forEach((lineSnapshot, index) => {
|
|
357
|
+
const line = lineMap.get(lineSnapshot.orderLineId);
|
|
358
|
+
if (!line) return;
|
|
359
|
+
const totalNet = lineSnapshot.totalNetAmount;
|
|
360
|
+
const totalGross = lineSnapshot.totalGrossAmount;
|
|
361
|
+
const adjustmentId = snapshot.adjustmentIds[index] ?? randomUUID();
|
|
362
|
+
const returnLine = em.create(SalesReturnLine, {
|
|
363
|
+
id: lineSnapshot.id,
|
|
364
|
+
salesReturn: restoredHeader,
|
|
365
|
+
orderLine: em.getReference(SalesOrderLine, line.id),
|
|
366
|
+
organizationId: snapshot.organizationId,
|
|
367
|
+
tenantId: snapshot.tenantId,
|
|
368
|
+
quantityReturned: lineSnapshot.quantityReturned.toString(),
|
|
369
|
+
unitPriceNet: lineSnapshot.unitPriceNet.toString(),
|
|
370
|
+
unitPriceGross: lineSnapshot.unitPriceGross.toString(),
|
|
371
|
+
totalNetAmount: totalNet.toString(),
|
|
372
|
+
totalGrossAmount: totalGross.toString(),
|
|
373
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
374
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
375
|
+
});
|
|
376
|
+
createdLines.push(returnLine);
|
|
377
|
+
em.persist(returnLine);
|
|
378
|
+
const adjustment = em.create(SalesOrderAdjustment, {
|
|
379
|
+
id: adjustmentId,
|
|
380
|
+
order,
|
|
381
|
+
orderLine: em.getReference(SalesOrderLine, line.id),
|
|
382
|
+
organizationId: snapshot.organizationId,
|
|
383
|
+
tenantId: snapshot.tenantId,
|
|
384
|
+
scope: "line",
|
|
385
|
+
kind: "return",
|
|
386
|
+
rate: "0",
|
|
387
|
+
amountNet: totalNet.toString(),
|
|
388
|
+
amountGross: totalGross.toString(),
|
|
389
|
+
currencyCode: order.currencyCode,
|
|
390
|
+
metadata: { returnId, returnLineId: lineSnapshot.id },
|
|
391
|
+
position: positionStart + index,
|
|
392
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
393
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
394
|
+
});
|
|
395
|
+
createdAdjustments.push(adjustment);
|
|
396
|
+
em.persist(adjustment);
|
|
397
|
+
line.returnedQuantity = (toNumeric(line.returnedQuantity) + lineSnapshot.quantityReturned).toString();
|
|
398
|
+
line.updatedAt = /* @__PURE__ */ new Date();
|
|
399
|
+
em.persist(line);
|
|
400
|
+
});
|
|
401
|
+
const lineSnapshots = orderLines.map(mapOrderLineEntityToSnapshot);
|
|
402
|
+
const adjustmentDrafts = [...existingAdjustments, ...createdAdjustments].map(
|
|
403
|
+
mapOrderAdjustmentToDraft
|
|
404
|
+
);
|
|
405
|
+
const calculation = await salesCalculationService.calculateDocumentTotals({
|
|
406
|
+
documentKind: "order",
|
|
407
|
+
lines: lineSnapshots,
|
|
408
|
+
adjustments: adjustmentDrafts,
|
|
409
|
+
context: buildCalculationContext(order)
|
|
410
|
+
});
|
|
411
|
+
applyOrderTotals(order, calculation.totals, calculation.lines.length);
|
|
412
|
+
order.updatedAt = /* @__PURE__ */ new Date();
|
|
413
|
+
em.persist(order);
|
|
414
|
+
}
|
|
415
|
+
],
|
|
416
|
+
{ transaction: true }
|
|
417
|
+
);
|
|
418
|
+
return createdLines;
|
|
419
|
+
}
|
|
194
420
|
function normalizeLinesInput(lines) {
|
|
195
421
|
const seen = /* @__PURE__ */ new Set();
|
|
196
422
|
const result = [];
|
|
@@ -392,224 +618,17 @@ const createReturnCommand = {
|
|
|
392
618
|
const after = payload?.after;
|
|
393
619
|
if (!after) return;
|
|
394
620
|
const em = ctx.container.resolve("em").fork();
|
|
395
|
-
const order = await findOneWithDecryption(
|
|
396
|
-
em,
|
|
397
|
-
SalesOrder,
|
|
398
|
-
{ id: after.orderId, deletedAt: null },
|
|
399
|
-
{},
|
|
400
|
-
{ tenantId: after.tenantId, organizationId: after.organizationId }
|
|
401
|
-
);
|
|
402
|
-
if (!order) return;
|
|
403
621
|
const salesCalculationService = ctx.container.resolve("salesCalculationService");
|
|
404
|
-
|
|
405
|
-
await withAtomicFlush(
|
|
406
|
-
em,
|
|
407
|
-
[
|
|
408
|
-
async () => {
|
|
409
|
-
lines = await findWithDecryption(
|
|
410
|
-
em,
|
|
411
|
-
SalesOrderLine,
|
|
412
|
-
{ order: order.id, deletedAt: null },
|
|
413
|
-
{},
|
|
414
|
-
{ tenantId: after.tenantId, organizationId: after.organizationId }
|
|
415
|
-
);
|
|
416
|
-
const lineMap = new Map(lines.map((line) => [line.id, line]));
|
|
417
|
-
after.lines.forEach((entry) => {
|
|
418
|
-
const line = lineMap.get(entry.orderLineId);
|
|
419
|
-
if (!line) return;
|
|
420
|
-
const next = Math.max(0, toNumeric(line.returnedQuantity) - entry.quantityReturned);
|
|
421
|
-
line.returnedQuantity = next.toString();
|
|
422
|
-
line.updatedAt = /* @__PURE__ */ new Date();
|
|
423
|
-
em.persist(line);
|
|
424
|
-
});
|
|
425
|
-
},
|
|
426
|
-
// The line returnedQuantity reversals above are persisted by
|
|
427
|
-
// withAtomicFlush's per-phase flush boundary before the adjustment /
|
|
428
|
-
// header / return-line lookups below run any query on this
|
|
429
|
-
// EntityManager. MikroORM v7 would otherwise silently discard the pending
|
|
430
|
-
// scalar changes on the managed `lines` when the next read resets the
|
|
431
|
-
// changeset (see SPEC-018).
|
|
432
|
-
async () => {
|
|
433
|
-
if (after.adjustmentIds.length) {
|
|
434
|
-
const adjustments = await findWithDecryption(
|
|
435
|
-
em,
|
|
436
|
-
SalesOrderAdjustment,
|
|
437
|
-
{ id: { $in: after.adjustmentIds }, deletedAt: null },
|
|
438
|
-
{},
|
|
439
|
-
{ tenantId: after.tenantId, organizationId: after.organizationId }
|
|
440
|
-
);
|
|
441
|
-
adjustments.forEach((adj) => em.remove(adj));
|
|
442
|
-
}
|
|
443
|
-
const header = await findOneWithDecryption(
|
|
444
|
-
em,
|
|
445
|
-
SalesReturn,
|
|
446
|
-
{ id: after.id, deletedAt: null },
|
|
447
|
-
{},
|
|
448
|
-
{ tenantId: after.tenantId, organizationId: after.organizationId }
|
|
449
|
-
);
|
|
450
|
-
const returnLines = await findWithDecryption(
|
|
451
|
-
em,
|
|
452
|
-
SalesReturnLine,
|
|
453
|
-
{ salesReturn: after.id, deletedAt: null },
|
|
454
|
-
{},
|
|
455
|
-
{ tenantId: after.tenantId, organizationId: after.organizationId }
|
|
456
|
-
);
|
|
457
|
-
returnLines.forEach((line) => em.remove(line));
|
|
458
|
-
if (header) em.remove(header);
|
|
459
|
-
const existingAdjustments = await findWithDecryption(
|
|
460
|
-
em,
|
|
461
|
-
SalesOrderAdjustment,
|
|
462
|
-
{ order: order.id, deletedAt: null },
|
|
463
|
-
{ orderBy: { position: "asc" } },
|
|
464
|
-
{ tenantId: after.tenantId, organizationId: after.organizationId }
|
|
465
|
-
);
|
|
466
|
-
const lineSnapshots = lines.map(mapOrderLineEntityToSnapshot);
|
|
467
|
-
const adjustmentDrafts = existingAdjustments.map(mapOrderAdjustmentToDraft);
|
|
468
|
-
const calculation = await salesCalculationService.calculateDocumentTotals({
|
|
469
|
-
documentKind: "order",
|
|
470
|
-
lines: lineSnapshots,
|
|
471
|
-
adjustments: adjustmentDrafts,
|
|
472
|
-
context: buildCalculationContext(order)
|
|
473
|
-
});
|
|
474
|
-
applyOrderTotals(order, calculation.totals, calculation.lines.length);
|
|
475
|
-
order.updatedAt = /* @__PURE__ */ new Date();
|
|
476
|
-
em.persist(order);
|
|
477
|
-
}
|
|
478
|
-
],
|
|
479
|
-
{ transaction: true }
|
|
480
|
-
);
|
|
622
|
+
await reverseReturnEffects(em, salesCalculationService, after);
|
|
481
623
|
},
|
|
482
624
|
redo: async ({ ctx, logEntry }) => {
|
|
483
625
|
const after = resolveRedoSnapshot(logEntry);
|
|
484
|
-
|
|
485
|
-
if (!after || !returnId) {
|
|
626
|
+
if (!after || !after.id) {
|
|
486
627
|
throw new CrudHttpError(400, { error: "[internal] redo snapshot unavailable for sales.returns.create" });
|
|
487
628
|
}
|
|
488
629
|
const em = ctx.container.resolve("em").fork();
|
|
489
630
|
const salesCalculationService = ctx.container.resolve("salesCalculationService");
|
|
490
|
-
const createdLines =
|
|
491
|
-
await withAtomicFlush(
|
|
492
|
-
em,
|
|
493
|
-
[
|
|
494
|
-
async () => {
|
|
495
|
-
const order = await findOneWithDecryption(
|
|
496
|
-
em,
|
|
497
|
-
SalesOrder,
|
|
498
|
-
{ id: after.orderId, deletedAt: null },
|
|
499
|
-
{},
|
|
500
|
-
{ tenantId: after.tenantId, organizationId: after.organizationId }
|
|
501
|
-
);
|
|
502
|
-
if (!order) {
|
|
503
|
-
throw new CrudHttpError(404, { error: "sales.returns.orderMissing" });
|
|
504
|
-
}
|
|
505
|
-
ensureSameScope(order, after.organizationId, after.tenantId);
|
|
506
|
-
const orderLines = await findWithDecryption(
|
|
507
|
-
em,
|
|
508
|
-
SalesOrderLine,
|
|
509
|
-
{ order: order.id, deletedAt: null },
|
|
510
|
-
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
511
|
-
{ tenantId: after.tenantId, organizationId: after.organizationId }
|
|
512
|
-
);
|
|
513
|
-
const lineMap = new Map(orderLines.map((line) => [line.id, line]));
|
|
514
|
-
const existingAdjustments = await findWithDecryption(
|
|
515
|
-
em,
|
|
516
|
-
SalesOrderAdjustment,
|
|
517
|
-
{ order: order.id, deletedAt: null },
|
|
518
|
-
{ orderBy: { position: "asc" } },
|
|
519
|
-
{ tenantId: after.tenantId, organizationId: after.organizationId }
|
|
520
|
-
);
|
|
521
|
-
const positionStart = existingAdjustments.reduce((acc, adj) => Math.max(acc, adj.position ?? 0), 0) + 1;
|
|
522
|
-
const restoredHeader = await findOneWithDecryption(
|
|
523
|
-
em,
|
|
524
|
-
SalesReturn,
|
|
525
|
-
{ id: after.id },
|
|
526
|
-
{},
|
|
527
|
-
{ tenantId: after.tenantId, organizationId: after.organizationId }
|
|
528
|
-
) ?? em.create(SalesReturn, {
|
|
529
|
-
id: after.id,
|
|
530
|
-
order,
|
|
531
|
-
organizationId: after.organizationId,
|
|
532
|
-
tenantId: after.tenantId,
|
|
533
|
-
returnNumber: after.returnNumber,
|
|
534
|
-
reason: after.reason ?? null,
|
|
535
|
-
notes: after.notes ?? null,
|
|
536
|
-
returnedAt: after.returnedAt ? new Date(after.returnedAt) : /* @__PURE__ */ new Date(),
|
|
537
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
538
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
539
|
-
});
|
|
540
|
-
restoredHeader.order = order;
|
|
541
|
-
restoredHeader.deletedAt = null;
|
|
542
|
-
restoredHeader.organizationId = after.organizationId;
|
|
543
|
-
restoredHeader.tenantId = after.tenantId;
|
|
544
|
-
restoredHeader.returnNumber = after.returnNumber;
|
|
545
|
-
restoredHeader.reason = after.reason ?? null;
|
|
546
|
-
restoredHeader.notes = after.notes ?? null;
|
|
547
|
-
restoredHeader.returnedAt = after.returnedAt ? new Date(after.returnedAt) : /* @__PURE__ */ new Date();
|
|
548
|
-
restoredHeader.updatedAt = /* @__PURE__ */ new Date();
|
|
549
|
-
em.persist(restoredHeader);
|
|
550
|
-
const createdAdjustments = [];
|
|
551
|
-
after.lines.forEach((lineSnapshot, index) => {
|
|
552
|
-
const line = lineMap.get(lineSnapshot.orderLineId);
|
|
553
|
-
if (!line) return;
|
|
554
|
-
const totalNet = lineSnapshot.totalNetAmount;
|
|
555
|
-
const totalGross = lineSnapshot.totalGrossAmount;
|
|
556
|
-
const adjustmentId = after.adjustmentIds[index] ?? randomUUID();
|
|
557
|
-
const returnLine = em.create(SalesReturnLine, {
|
|
558
|
-
id: lineSnapshot.id,
|
|
559
|
-
salesReturn: restoredHeader,
|
|
560
|
-
orderLine: em.getReference(SalesOrderLine, line.id),
|
|
561
|
-
organizationId: after.organizationId,
|
|
562
|
-
tenantId: after.tenantId,
|
|
563
|
-
quantityReturned: lineSnapshot.quantityReturned.toString(),
|
|
564
|
-
unitPriceNet: lineSnapshot.unitPriceNet.toString(),
|
|
565
|
-
unitPriceGross: lineSnapshot.unitPriceGross.toString(),
|
|
566
|
-
totalNetAmount: totalNet.toString(),
|
|
567
|
-
totalGrossAmount: totalGross.toString(),
|
|
568
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
569
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
570
|
-
});
|
|
571
|
-
createdLines.push(returnLine);
|
|
572
|
-
em.persist(returnLine);
|
|
573
|
-
const adjustment = em.create(SalesOrderAdjustment, {
|
|
574
|
-
id: adjustmentId,
|
|
575
|
-
order,
|
|
576
|
-
orderLine: em.getReference(SalesOrderLine, line.id),
|
|
577
|
-
organizationId: after.organizationId,
|
|
578
|
-
tenantId: after.tenantId,
|
|
579
|
-
scope: "line",
|
|
580
|
-
kind: "return",
|
|
581
|
-
rate: "0",
|
|
582
|
-
amountNet: totalNet.toString(),
|
|
583
|
-
amountGross: totalGross.toString(),
|
|
584
|
-
currencyCode: order.currencyCode,
|
|
585
|
-
metadata: { returnId, returnLineId: lineSnapshot.id },
|
|
586
|
-
position: positionStart + index,
|
|
587
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
588
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
589
|
-
});
|
|
590
|
-
createdAdjustments.push(adjustment);
|
|
591
|
-
em.persist(adjustment);
|
|
592
|
-
line.returnedQuantity = (toNumeric(line.returnedQuantity) + lineSnapshot.quantityReturned).toString();
|
|
593
|
-
line.updatedAt = /* @__PURE__ */ new Date();
|
|
594
|
-
em.persist(line);
|
|
595
|
-
});
|
|
596
|
-
const lineSnapshots = orderLines.map(mapOrderLineEntityToSnapshot);
|
|
597
|
-
const adjustmentDrafts = [...existingAdjustments, ...createdAdjustments].map(
|
|
598
|
-
mapOrderAdjustmentToDraft
|
|
599
|
-
);
|
|
600
|
-
const calculation = await salesCalculationService.calculateDocumentTotals({
|
|
601
|
-
documentKind: "order",
|
|
602
|
-
lines: lineSnapshots,
|
|
603
|
-
adjustments: adjustmentDrafts,
|
|
604
|
-
context: buildCalculationContext(order)
|
|
605
|
-
});
|
|
606
|
-
applyOrderTotals(order, calculation.totals, calculation.lines.length);
|
|
607
|
-
order.updatedAt = /* @__PURE__ */ new Date();
|
|
608
|
-
em.persist(order);
|
|
609
|
-
}
|
|
610
|
-
],
|
|
611
|
-
{ transaction: true }
|
|
612
|
-
);
|
|
631
|
+
const createdLines = await restoreReturnEffects(em, salesCalculationService, after);
|
|
613
632
|
const header = await findOneWithDecryption(
|
|
614
633
|
em,
|
|
615
634
|
SalesReturn,
|
|
@@ -645,8 +664,249 @@ const createReturnCommand = {
|
|
|
645
664
|
return { returnId: header.id };
|
|
646
665
|
}
|
|
647
666
|
};
|
|
667
|
+
const updateReturnCommand = {
|
|
668
|
+
id: "sales.returns.update",
|
|
669
|
+
async prepare(rawInput, ctx) {
|
|
670
|
+
const parsed = returnUpdateSchema.parse(rawInput ?? {});
|
|
671
|
+
const em = ctx.container.resolve("em");
|
|
672
|
+
const snapshot = await loadReturnHeaderSnapshot(em, parsed.id);
|
|
673
|
+
if (snapshot) {
|
|
674
|
+
ensureTenantScope(ctx, snapshot.tenantId);
|
|
675
|
+
ensureOrganizationScope(ctx, snapshot.organizationId);
|
|
676
|
+
}
|
|
677
|
+
return snapshot ? { before: snapshot } : {};
|
|
678
|
+
},
|
|
679
|
+
async execute(rawInput, ctx) {
|
|
680
|
+
const input = returnUpdateSchema.parse(rawInput ?? {});
|
|
681
|
+
ensureTenantScope(ctx, input.tenantId);
|
|
682
|
+
ensureOrganizationScope(ctx, input.organizationId);
|
|
683
|
+
const { translate } = await resolveTranslations();
|
|
684
|
+
const em = ctx.container.resolve("em").fork();
|
|
685
|
+
const header = await em.transactional(async (tx) => {
|
|
686
|
+
const entity = await findOneWithDecryption(
|
|
687
|
+
tx,
|
|
688
|
+
SalesReturn,
|
|
689
|
+
{ id: input.id, deletedAt: null },
|
|
690
|
+
{ populate: ["order"] },
|
|
691
|
+
{ tenantId: input.tenantId, organizationId: input.organizationId }
|
|
692
|
+
);
|
|
693
|
+
if (!entity || !entity.order) {
|
|
694
|
+
throw new CrudHttpError(404, { error: translate("sales.returns.notFound", "Return not found.") });
|
|
695
|
+
}
|
|
696
|
+
ensureSameScope(entity, input.organizationId, input.tenantId);
|
|
697
|
+
const orderId = typeof entity.order === "string" ? entity.order : entity.order.id;
|
|
698
|
+
if (input.orderId !== orderId) {
|
|
699
|
+
throw new CrudHttpError(400, { error: translate("sales.returns.orderMismatch", "Return does not belong to this order.") });
|
|
700
|
+
}
|
|
701
|
+
enforceSalesDocumentOptimisticLock(ctx, entity, SALES_RESOURCE_KIND_RETURN);
|
|
702
|
+
if (input.reason !== void 0) entity.reason = input.reason.length ? input.reason : null;
|
|
703
|
+
if (input.notes !== void 0) entity.notes = input.notes.length ? input.notes : null;
|
|
704
|
+
if (input.returnedAt !== void 0) entity.returnedAt = input.returnedAt ?? null;
|
|
705
|
+
entity.updatedAt = /* @__PURE__ */ new Date();
|
|
706
|
+
tx.persist(entity);
|
|
707
|
+
await tx.flush();
|
|
708
|
+
return entity;
|
|
709
|
+
});
|
|
710
|
+
const dataEngine = ctx.container.resolve("dataEngine");
|
|
711
|
+
await emitCrudSideEffects({
|
|
712
|
+
dataEngine,
|
|
713
|
+
action: "updated",
|
|
714
|
+
entity: header,
|
|
715
|
+
identifiers: { id: header.id, organizationId: header.organizationId, tenantId: header.tenantId },
|
|
716
|
+
indexer: { entityType: E.sales.sales_return },
|
|
717
|
+
events: returnCrudEvents
|
|
718
|
+
});
|
|
719
|
+
return { returnId: header.id };
|
|
720
|
+
},
|
|
721
|
+
captureAfter: async (_input, result, ctx) => {
|
|
722
|
+
const em = ctx.container.resolve("em").fork();
|
|
723
|
+
return loadReturnHeaderSnapshot(em, result.returnId);
|
|
724
|
+
},
|
|
725
|
+
buildLog: async ({ snapshots, result }) => {
|
|
726
|
+
const { translate } = await resolveTranslations();
|
|
727
|
+
const before = snapshots.before;
|
|
728
|
+
const after = snapshots.after;
|
|
729
|
+
return {
|
|
730
|
+
actionLabel: translate("sales.audit.returns.update", "Update return"),
|
|
731
|
+
resourceKind: "sales.return",
|
|
732
|
+
resourceId: result.returnId,
|
|
733
|
+
parentResourceKind: "sales.order",
|
|
734
|
+
parentResourceId: after?.orderId ?? before?.orderId ?? null,
|
|
735
|
+
tenantId: after?.tenantId ?? before?.tenantId ?? null,
|
|
736
|
+
organizationId: after?.organizationId ?? before?.organizationId ?? null,
|
|
737
|
+
snapshotBefore: before ?? null,
|
|
738
|
+
snapshotAfter: after ?? null,
|
|
739
|
+
payload: {
|
|
740
|
+
undo: { before, after }
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
},
|
|
744
|
+
undo: async ({ logEntry, ctx }) => {
|
|
745
|
+
const payload = extractUndoPayload(logEntry);
|
|
746
|
+
const before = payload?.before;
|
|
747
|
+
if (!before) return;
|
|
748
|
+
const em = ctx.container.resolve("em").fork();
|
|
749
|
+
await em.transactional(async (tx) => {
|
|
750
|
+
const entity = await findOneWithDecryption(
|
|
751
|
+
tx,
|
|
752
|
+
SalesReturn,
|
|
753
|
+
{ id: before.id, deletedAt: null },
|
|
754
|
+
{},
|
|
755
|
+
{ tenantId: before.tenantId, organizationId: before.organizationId }
|
|
756
|
+
);
|
|
757
|
+
if (!entity) return;
|
|
758
|
+
entity.reason = before.reason;
|
|
759
|
+
entity.notes = before.notes;
|
|
760
|
+
entity.returnedAt = before.returnedAt ? new Date(before.returnedAt) : null;
|
|
761
|
+
entity.updatedAt = /* @__PURE__ */ new Date();
|
|
762
|
+
tx.persist(entity);
|
|
763
|
+
await tx.flush();
|
|
764
|
+
});
|
|
765
|
+
const dataEngine = ctx.container.resolve("dataEngine");
|
|
766
|
+
const restored = await findOneWithDecryption(
|
|
767
|
+
em,
|
|
768
|
+
SalesReturn,
|
|
769
|
+
{ id: before.id, deletedAt: null },
|
|
770
|
+
{},
|
|
771
|
+
{ tenantId: before.tenantId, organizationId: before.organizationId }
|
|
772
|
+
);
|
|
773
|
+
if (restored) {
|
|
774
|
+
await emitCrudSideEffects({
|
|
775
|
+
dataEngine,
|
|
776
|
+
action: "updated",
|
|
777
|
+
entity: restored,
|
|
778
|
+
identifiers: { id: restored.id, organizationId: restored.organizationId, tenantId: restored.tenantId },
|
|
779
|
+
indexer: { entityType: E.sales.sales_return },
|
|
780
|
+
events: returnCrudEvents
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
const deleteReturnCommand = {
|
|
786
|
+
id: "sales.returns.delete",
|
|
787
|
+
async prepare(rawInput, ctx) {
|
|
788
|
+
const parsed = returnDeleteSchema.parse(rawInput ?? {});
|
|
789
|
+
const em = ctx.container.resolve("em");
|
|
790
|
+
const snapshot = await loadReturnSnapshot(em, parsed.id);
|
|
791
|
+
if (snapshot) {
|
|
792
|
+
ensureTenantScope(ctx, snapshot.tenantId);
|
|
793
|
+
ensureOrganizationScope(ctx, snapshot.organizationId);
|
|
794
|
+
}
|
|
795
|
+
return snapshot ? { before: snapshot } : {};
|
|
796
|
+
},
|
|
797
|
+
async execute(rawInput, ctx) {
|
|
798
|
+
const input = returnDeleteSchema.parse(rawInput ?? {});
|
|
799
|
+
ensureTenantScope(ctx, input.tenantId);
|
|
800
|
+
ensureOrganizationScope(ctx, input.organizationId);
|
|
801
|
+
const { translate } = await resolveTranslations();
|
|
802
|
+
const em = ctx.container.resolve("em").fork();
|
|
803
|
+
const salesCalculationService = ctx.container.resolve("salesCalculationService");
|
|
804
|
+
const snapshot = await loadReturnSnapshot(em, input.id);
|
|
805
|
+
if (!snapshot) {
|
|
806
|
+
throw new CrudHttpError(404, { error: translate("sales.returns.notFound", "Return not found.") });
|
|
807
|
+
}
|
|
808
|
+
ensureSameScope(snapshot, input.organizationId, input.tenantId);
|
|
809
|
+
if (input.orderId !== snapshot.orderId) {
|
|
810
|
+
throw new CrudHttpError(400, { error: translate("sales.returns.orderMismatch", "Return does not belong to this order.") });
|
|
811
|
+
}
|
|
812
|
+
const header = await findOneWithDecryption(
|
|
813
|
+
em,
|
|
814
|
+
SalesReturn,
|
|
815
|
+
{ id: input.id, deletedAt: null },
|
|
816
|
+
{},
|
|
817
|
+
{ tenantId: input.tenantId, organizationId: input.organizationId }
|
|
818
|
+
);
|
|
819
|
+
if (!header) {
|
|
820
|
+
throw new CrudHttpError(404, { error: translate("sales.returns.notFound", "Return not found.") });
|
|
821
|
+
}
|
|
822
|
+
ensureSameScope(header, input.organizationId, input.tenantId);
|
|
823
|
+
enforceSalesDocumentOptimisticLock(ctx, header, SALES_RESOURCE_KIND_RETURN);
|
|
824
|
+
await reverseReturnEffects(em, salesCalculationService, snapshot);
|
|
825
|
+
const dataEngine = ctx.container.resolve("dataEngine");
|
|
826
|
+
await emitCrudSideEffects({
|
|
827
|
+
dataEngine,
|
|
828
|
+
action: "deleted",
|
|
829
|
+
entity: header,
|
|
830
|
+
identifiers: { id: snapshot.id, organizationId: snapshot.organizationId, tenantId: snapshot.tenantId },
|
|
831
|
+
indexer: { entityType: E.sales.sales_return },
|
|
832
|
+
events: returnCrudEvents
|
|
833
|
+
});
|
|
834
|
+
if (snapshot.lines.length) {
|
|
835
|
+
await Promise.all(
|
|
836
|
+
snapshot.lines.map(
|
|
837
|
+
(line) => emitCrudSideEffects({
|
|
838
|
+
dataEngine,
|
|
839
|
+
action: "deleted",
|
|
840
|
+
entity: { id: line.id, organizationId: snapshot.organizationId, tenantId: snapshot.tenantId },
|
|
841
|
+
identifiers: { id: line.id, organizationId: snapshot.organizationId, tenantId: snapshot.tenantId },
|
|
842
|
+
indexer: { entityType: E.sales.sales_return_line }
|
|
843
|
+
})
|
|
844
|
+
)
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
return { returnId: snapshot.id };
|
|
848
|
+
},
|
|
849
|
+
buildLog: async ({ snapshots, result }) => {
|
|
850
|
+
const before = snapshots.before;
|
|
851
|
+
if (!before) return null;
|
|
852
|
+
const { translate } = await resolveTranslations();
|
|
853
|
+
return {
|
|
854
|
+
actionLabel: translate("sales.audit.returns.delete", "Delete return"),
|
|
855
|
+
resourceKind: "sales.return",
|
|
856
|
+
resourceId: result.returnId,
|
|
857
|
+
parentResourceKind: "sales.order",
|
|
858
|
+
parentResourceId: before.orderId ?? null,
|
|
859
|
+
tenantId: before.tenantId,
|
|
860
|
+
organizationId: before.organizationId,
|
|
861
|
+
snapshotBefore: before,
|
|
862
|
+
payload: {
|
|
863
|
+
undo: { before }
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
},
|
|
867
|
+
undo: async ({ logEntry, ctx }) => {
|
|
868
|
+
const payload = extractUndoPayload(logEntry);
|
|
869
|
+
const before = payload?.before;
|
|
870
|
+
if (!before) return;
|
|
871
|
+
const em = ctx.container.resolve("em").fork();
|
|
872
|
+
const salesCalculationService = ctx.container.resolve("salesCalculationService");
|
|
873
|
+
const createdLines = await restoreReturnEffects(em, salesCalculationService, before);
|
|
874
|
+
const header = await findOneWithDecryption(
|
|
875
|
+
em,
|
|
876
|
+
SalesReturn,
|
|
877
|
+
{ id: before.id, deletedAt: null },
|
|
878
|
+
{},
|
|
879
|
+
{ tenantId: before.tenantId, organizationId: before.organizationId }
|
|
880
|
+
);
|
|
881
|
+
if (!header) return;
|
|
882
|
+
const dataEngine = ctx.container.resolve("dataEngine");
|
|
883
|
+
await emitCrudSideEffects({
|
|
884
|
+
dataEngine,
|
|
885
|
+
action: "created",
|
|
886
|
+
entity: header,
|
|
887
|
+
identifiers: { id: header.id, organizationId: header.organizationId, tenantId: header.tenantId },
|
|
888
|
+
indexer: { entityType: E.sales.sales_return },
|
|
889
|
+
events: returnCrudEvents
|
|
890
|
+
});
|
|
891
|
+
if (createdLines.length) {
|
|
892
|
+
await Promise.all(
|
|
893
|
+
createdLines.map(
|
|
894
|
+
(line) => emitCrudSideEffects({
|
|
895
|
+
dataEngine,
|
|
896
|
+
action: "created",
|
|
897
|
+
entity: line,
|
|
898
|
+
identifiers: { id: line.id, organizationId: line.organizationId, tenantId: line.tenantId },
|
|
899
|
+
indexer: { entityType: E.sales.sales_return_line }
|
|
900
|
+
})
|
|
901
|
+
)
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
};
|
|
648
906
|
registerCommand(createReturnCommand);
|
|
649
|
-
|
|
907
|
+
registerCommand(updateReturnCommand);
|
|
908
|
+
registerCommand(deleteReturnCommand);
|
|
909
|
+
const returnCommands = [createReturnCommand, updateReturnCommand, deleteReturnCommand];
|
|
650
910
|
export {
|
|
651
911
|
loadReturnSnapshot,
|
|
652
912
|
recalculateOrderTotalsForDisplay,
|