@open-mercato/core 0.6.6-develop.5617.1.62538c48ca → 0.6.6-develop.5637.1.7a68607cc6

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.
Files changed (30) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/sales/acl.js +6 -0
  3. package/dist/modules/sales/acl.js.map +2 -2
  4. package/dist/modules/sales/api/returns/route.js +43 -3
  5. package/dist/modules/sales/api/returns/route.js.map +2 -2
  6. package/dist/modules/sales/commands/returns.js +473 -213
  7. package/dist/modules/sales/commands/returns.js.map +2 -2
  8. package/dist/modules/sales/commands/shared.js +2 -0
  9. package/dist/modules/sales/commands/shared.js.map +2 -2
  10. package/dist/modules/sales/components/documents/ReturnEditDialog.js +126 -0
  11. package/dist/modules/sales/components/documents/ReturnEditDialog.js.map +7 -0
  12. package/dist/modules/sales/components/documents/ReturnsSection.js +102 -6
  13. package/dist/modules/sales/components/documents/ReturnsSection.js.map +2 -2
  14. package/dist/modules/sales/data/validators.js +19 -1
  15. package/dist/modules/sales/data/validators.js.map +2 -2
  16. package/dist/modules/sales/setup.js +1 -0
  17. package/dist/modules/sales/setup.js.map +2 -2
  18. package/package.json +7 -7
  19. package/src/modules/sales/acl.ts +6 -0
  20. package/src/modules/sales/api/returns/route.ts +41 -3
  21. package/src/modules/sales/commands/returns.ts +561 -229
  22. package/src/modules/sales/commands/shared.ts +1 -0
  23. package/src/modules/sales/components/documents/ReturnEditDialog.tsx +158 -0
  24. package/src/modules/sales/components/documents/ReturnsSection.tsx +105 -3
  25. package/src/modules/sales/data/validators.ts +28 -1
  26. package/src/modules/sales/i18n/de.json +11 -0
  27. package/src/modules/sales/i18n/en.json +11 -0
  28. package/src/modules/sales/i18n/es.json +11 -0
  29. package/src/modules/sales/i18n/pl.json +11 -0
  30. 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 { returnCreateSchema } from "../data/validators.js";
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
- let lines = [];
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
- const returnId = after?.id ?? logEntry.resourceId ?? null;
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
- const returnCommands = [createReturnCommand];
907
+ registerCommand(updateReturnCommand);
908
+ registerCommand(deleteReturnCommand);
909
+ const returnCommands = [createReturnCommand, updateReturnCommand, deleteReturnCommand];
650
910
  export {
651
911
  loadReturnSnapshot,
652
912
  recalculateOrderTotalsForDisplay,