@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
|
@@ -12,10 +12,17 @@ import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/
|
|
|
12
12
|
import { SalesDocumentNumberGenerator } from '../services/salesDocumentNumberGenerator'
|
|
13
13
|
import type { SalesCalculationService } from '../services/salesCalculationService'
|
|
14
14
|
import type { SalesAdjustmentDraft, SalesLineSnapshot, SalesDocumentCalculationResult } from '../lib/types'
|
|
15
|
-
import { cloneJson, ensureOrganizationScope, ensureSameScope, ensureTenantScope, extractUndoPayload, toNumericString, enforceSalesDocumentOptimisticLock, SALES_RESOURCE_KIND_ORDER } from './shared'
|
|
15
|
+
import { cloneJson, ensureOrganizationScope, ensureSameScope, ensureTenantScope, extractUndoPayload, toNumericString, enforceSalesDocumentOptimisticLock, SALES_RESOURCE_KIND_ORDER, SALES_RESOURCE_KIND_RETURN } from './shared'
|
|
16
16
|
import { resolveRedoSnapshot } from '@open-mercato/shared/lib/commands/redo'
|
|
17
17
|
import { SalesOrder, SalesOrderAdjustment, SalesOrderLine, SalesReturn, SalesReturnLine } from '../data/entities'
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
returnCreateSchema,
|
|
20
|
+
returnUpdateSchema,
|
|
21
|
+
returnDeleteSchema,
|
|
22
|
+
type ReturnCreateInput,
|
|
23
|
+
type ReturnUpdateInput,
|
|
24
|
+
type ReturnDeleteInput,
|
|
25
|
+
} from '../data/validators'
|
|
19
26
|
import { E } from '#generated/entities.ids.generated'
|
|
20
27
|
|
|
21
28
|
type ReturnLineInput = { orderLineId: string; quantity: number }
|
|
@@ -245,6 +252,295 @@ export async function loadReturnSnapshot(em: EntityManager, id: string): Promise
|
|
|
245
252
|
}
|
|
246
253
|
}
|
|
247
254
|
|
|
255
|
+
type ReturnHeaderSnapshot = {
|
|
256
|
+
id: string
|
|
257
|
+
orderId: string
|
|
258
|
+
organizationId: string
|
|
259
|
+
tenantId: string
|
|
260
|
+
reason: string | null
|
|
261
|
+
notes: string | null
|
|
262
|
+
returnedAt: string | null
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
type ReturnHeaderUndoPayload = {
|
|
266
|
+
before?: ReturnHeaderSnapshot | null
|
|
267
|
+
after?: ReturnHeaderSnapshot | null
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
type ReturnDeleteUndoPayload = {
|
|
271
|
+
before?: ReturnSnapshot | null
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function loadReturnHeaderSnapshot(em: EntityManager, id: string): Promise<ReturnHeaderSnapshot | null> {
|
|
275
|
+
const header = await findOneWithDecryption(em, SalesReturn, { id, deletedAt: null }, { populate: ['order'] }, {})
|
|
276
|
+
if (!header || !header.order) return null
|
|
277
|
+
const orderId = typeof header.order === 'string' ? header.order : header.order.id
|
|
278
|
+
return {
|
|
279
|
+
id: header.id,
|
|
280
|
+
orderId,
|
|
281
|
+
organizationId: header.organizationId,
|
|
282
|
+
tenantId: header.tenantId,
|
|
283
|
+
reason: header.reason ?? null,
|
|
284
|
+
notes: header.notes ?? null,
|
|
285
|
+
returnedAt: header.returnedAt ? header.returnedAt.toISOString() : null,
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Reverse the order-level effects of a return: restore each order line's
|
|
291
|
+
* `returnedQuantity`, drop the return's line-scoped credit adjustments, remove
|
|
292
|
+
* the return header + lines, and recalculate the order totals. Shared by the
|
|
293
|
+
* create command's undo and the delete command's execute — both need the exact
|
|
294
|
+
* same teardown. No-op when the order is gone.
|
|
295
|
+
*
|
|
296
|
+
* The line reversals, adjustment/return removals, and the order-total recompute
|
|
297
|
+
* interleave queries on the same EntityManager with scalar mutations, so they
|
|
298
|
+
* run inside an atomic flush to avoid lost updates and partial commits
|
|
299
|
+
* (SPEC-018): the per-phase flush boundary persists the line `returnedQuantity`
|
|
300
|
+
* reversals before the adjustment/header/return-line lookups in the next phase
|
|
301
|
+
* run any query, which under MikroORM v7 would otherwise silently discard the
|
|
302
|
+
* pending scalar changes on the managed lines.
|
|
303
|
+
*/
|
|
304
|
+
async function reverseReturnEffects(
|
|
305
|
+
em: EntityManager,
|
|
306
|
+
salesCalculationService: SalesCalculationService,
|
|
307
|
+
snapshot: ReturnSnapshot,
|
|
308
|
+
): Promise<void> {
|
|
309
|
+
const order = await findOneWithDecryption(
|
|
310
|
+
em,
|
|
311
|
+
SalesOrder,
|
|
312
|
+
{ id: snapshot.orderId, deletedAt: null },
|
|
313
|
+
{},
|
|
314
|
+
{ tenantId: snapshot.tenantId, organizationId: snapshot.organizationId },
|
|
315
|
+
)
|
|
316
|
+
if (!order) return
|
|
317
|
+
|
|
318
|
+
let lines: SalesOrderLine[] = []
|
|
319
|
+
await withAtomicFlush(
|
|
320
|
+
em,
|
|
321
|
+
[
|
|
322
|
+
async () => {
|
|
323
|
+
lines = await findWithDecryption(
|
|
324
|
+
em,
|
|
325
|
+
SalesOrderLine,
|
|
326
|
+
{ order: order.id, deletedAt: null },
|
|
327
|
+
{},
|
|
328
|
+
{ tenantId: snapshot.tenantId, organizationId: snapshot.organizationId },
|
|
329
|
+
)
|
|
330
|
+
const lineMap = new Map(lines.map((line) => [line.id, line]))
|
|
331
|
+
snapshot.lines.forEach((entry) => {
|
|
332
|
+
const line = lineMap.get(entry.orderLineId)
|
|
333
|
+
if (!line) return
|
|
334
|
+
const next = Math.max(0, toNumeric(line.returnedQuantity) - entry.quantityReturned)
|
|
335
|
+
line.returnedQuantity = next.toString()
|
|
336
|
+
line.updatedAt = new Date()
|
|
337
|
+
em.persist(line)
|
|
338
|
+
})
|
|
339
|
+
},
|
|
340
|
+
async () => {
|
|
341
|
+
if (snapshot.adjustmentIds.length) {
|
|
342
|
+
const adjustments = await findWithDecryption(
|
|
343
|
+
em,
|
|
344
|
+
SalesOrderAdjustment,
|
|
345
|
+
{ id: { $in: snapshot.adjustmentIds }, deletedAt: null },
|
|
346
|
+
{},
|
|
347
|
+
{ tenantId: snapshot.tenantId, organizationId: snapshot.organizationId },
|
|
348
|
+
)
|
|
349
|
+
adjustments.forEach((adj) => em.remove(adj))
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const header = await findOneWithDecryption(
|
|
353
|
+
em,
|
|
354
|
+
SalesReturn,
|
|
355
|
+
{ id: snapshot.id, deletedAt: null },
|
|
356
|
+
{},
|
|
357
|
+
{ tenantId: snapshot.tenantId, organizationId: snapshot.organizationId },
|
|
358
|
+
)
|
|
359
|
+
const returnLines = await findWithDecryption(
|
|
360
|
+
em,
|
|
361
|
+
SalesReturnLine,
|
|
362
|
+
{ salesReturn: snapshot.id, deletedAt: null },
|
|
363
|
+
{},
|
|
364
|
+
{ tenantId: snapshot.tenantId, organizationId: snapshot.organizationId },
|
|
365
|
+
)
|
|
366
|
+
returnLines.forEach((line) => em.remove(line))
|
|
367
|
+
if (header) em.remove(header)
|
|
368
|
+
|
|
369
|
+
const existingAdjustments = await findWithDecryption(
|
|
370
|
+
em,
|
|
371
|
+
SalesOrderAdjustment,
|
|
372
|
+
{ order: order.id, deletedAt: null },
|
|
373
|
+
{ orderBy: { position: 'asc' } },
|
|
374
|
+
{ tenantId: snapshot.tenantId, organizationId: snapshot.organizationId },
|
|
375
|
+
)
|
|
376
|
+
const lineSnapshots: SalesLineSnapshot[] = lines.map(mapOrderLineEntityToSnapshot)
|
|
377
|
+
const adjustmentDrafts: SalesAdjustmentDraft[] = existingAdjustments.map(mapOrderAdjustmentToDraft)
|
|
378
|
+
const calculation = await salesCalculationService.calculateDocumentTotals({
|
|
379
|
+
documentKind: 'order',
|
|
380
|
+
lines: lineSnapshots,
|
|
381
|
+
adjustments: adjustmentDrafts,
|
|
382
|
+
context: buildCalculationContext(order),
|
|
383
|
+
})
|
|
384
|
+
applyOrderTotals(order, calculation.totals, calculation.lines.length)
|
|
385
|
+
order.updatedAt = new Date()
|
|
386
|
+
em.persist(order)
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
{ transaction: true },
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Re-apply a return from a snapshot: recreate the return header + lines and the
|
|
395
|
+
* line-scoped credit adjustments, bump each order line's `returnedQuantity`, and
|
|
396
|
+
* recalculate the order totals. Shared by the create command's redo and the
|
|
397
|
+
* delete command's undo. Returns the recreated return lines so callers can emit
|
|
398
|
+
* index side effects. Throws a 404 when the order is gone.
|
|
399
|
+
*/
|
|
400
|
+
async function restoreReturnEffects(
|
|
401
|
+
em: EntityManager,
|
|
402
|
+
salesCalculationService: SalesCalculationService,
|
|
403
|
+
snapshot: ReturnSnapshot,
|
|
404
|
+
): Promise<SalesReturnLine[]> {
|
|
405
|
+
const returnId = snapshot.id
|
|
406
|
+
const createdLines: SalesReturnLine[] = []
|
|
407
|
+
|
|
408
|
+
await withAtomicFlush(
|
|
409
|
+
em,
|
|
410
|
+
[
|
|
411
|
+
async () => {
|
|
412
|
+
const order = await findOneWithDecryption(
|
|
413
|
+
em,
|
|
414
|
+
SalesOrder,
|
|
415
|
+
{ id: snapshot.orderId, deletedAt: null },
|
|
416
|
+
{},
|
|
417
|
+
{ tenantId: snapshot.tenantId, organizationId: snapshot.organizationId },
|
|
418
|
+
)
|
|
419
|
+
if (!order) {
|
|
420
|
+
throw new CrudHttpError(404, { error: 'sales.returns.orderMissing' })
|
|
421
|
+
}
|
|
422
|
+
ensureSameScope(order, snapshot.organizationId, snapshot.tenantId)
|
|
423
|
+
|
|
424
|
+
const orderLines = await findWithDecryption(
|
|
425
|
+
em,
|
|
426
|
+
SalesOrderLine,
|
|
427
|
+
{ order: order.id, deletedAt: null },
|
|
428
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
429
|
+
{ tenantId: snapshot.tenantId, organizationId: snapshot.organizationId },
|
|
430
|
+
)
|
|
431
|
+
const lineMap = new Map(orderLines.map((line) => [line.id, line]))
|
|
432
|
+
|
|
433
|
+
const existingAdjustments = await findWithDecryption(
|
|
434
|
+
em,
|
|
435
|
+
SalesOrderAdjustment,
|
|
436
|
+
{ order: order.id, deletedAt: null },
|
|
437
|
+
{ orderBy: { position: 'asc' } },
|
|
438
|
+
{ tenantId: snapshot.tenantId, organizationId: snapshot.organizationId },
|
|
439
|
+
)
|
|
440
|
+
const positionStart = existingAdjustments.reduce((acc, adj) => Math.max(acc, adj.position ?? 0), 0) + 1
|
|
441
|
+
|
|
442
|
+
const restoredHeader =
|
|
443
|
+
(await findOneWithDecryption(
|
|
444
|
+
em,
|
|
445
|
+
SalesReturn,
|
|
446
|
+
{ id: snapshot.id },
|
|
447
|
+
{},
|
|
448
|
+
{ tenantId: snapshot.tenantId, organizationId: snapshot.organizationId },
|
|
449
|
+
)) ??
|
|
450
|
+
em.create(SalesReturn, {
|
|
451
|
+
id: snapshot.id,
|
|
452
|
+
order,
|
|
453
|
+
organizationId: snapshot.organizationId,
|
|
454
|
+
tenantId: snapshot.tenantId,
|
|
455
|
+
returnNumber: snapshot.returnNumber,
|
|
456
|
+
reason: snapshot.reason ?? null,
|
|
457
|
+
notes: snapshot.notes ?? null,
|
|
458
|
+
returnedAt: snapshot.returnedAt ? new Date(snapshot.returnedAt) : new Date(),
|
|
459
|
+
createdAt: new Date(),
|
|
460
|
+
updatedAt: new Date(),
|
|
461
|
+
})
|
|
462
|
+
restoredHeader.order = order
|
|
463
|
+
restoredHeader.deletedAt = null
|
|
464
|
+
restoredHeader.organizationId = snapshot.organizationId
|
|
465
|
+
restoredHeader.tenantId = snapshot.tenantId
|
|
466
|
+
restoredHeader.returnNumber = snapshot.returnNumber
|
|
467
|
+
restoredHeader.reason = snapshot.reason ?? null
|
|
468
|
+
restoredHeader.notes = snapshot.notes ?? null
|
|
469
|
+
restoredHeader.returnedAt = snapshot.returnedAt ? new Date(snapshot.returnedAt) : new Date()
|
|
470
|
+
restoredHeader.updatedAt = new Date()
|
|
471
|
+
em.persist(restoredHeader)
|
|
472
|
+
|
|
473
|
+
const createdAdjustments: SalesOrderAdjustment[] = []
|
|
474
|
+
snapshot.lines.forEach((lineSnapshot, index) => {
|
|
475
|
+
const line = lineMap.get(lineSnapshot.orderLineId)
|
|
476
|
+
if (!line) return
|
|
477
|
+
const totalNet = lineSnapshot.totalNetAmount
|
|
478
|
+
const totalGross = lineSnapshot.totalGrossAmount
|
|
479
|
+
const adjustmentId = snapshot.adjustmentIds[index] ?? randomUUID()
|
|
480
|
+
|
|
481
|
+
const returnLine = em.create(SalesReturnLine, {
|
|
482
|
+
id: lineSnapshot.id,
|
|
483
|
+
salesReturn: restoredHeader,
|
|
484
|
+
orderLine: em.getReference(SalesOrderLine, line.id),
|
|
485
|
+
organizationId: snapshot.organizationId,
|
|
486
|
+
tenantId: snapshot.tenantId,
|
|
487
|
+
quantityReturned: lineSnapshot.quantityReturned.toString(),
|
|
488
|
+
unitPriceNet: lineSnapshot.unitPriceNet.toString(),
|
|
489
|
+
unitPriceGross: lineSnapshot.unitPriceGross.toString(),
|
|
490
|
+
totalNetAmount: totalNet.toString(),
|
|
491
|
+
totalGrossAmount: totalGross.toString(),
|
|
492
|
+
createdAt: new Date(),
|
|
493
|
+
updatedAt: new Date(),
|
|
494
|
+
})
|
|
495
|
+
createdLines.push(returnLine)
|
|
496
|
+
em.persist(returnLine)
|
|
497
|
+
|
|
498
|
+
const adjustment = em.create(SalesOrderAdjustment, {
|
|
499
|
+
id: adjustmentId,
|
|
500
|
+
order,
|
|
501
|
+
orderLine: em.getReference(SalesOrderLine, line.id),
|
|
502
|
+
organizationId: snapshot.organizationId,
|
|
503
|
+
tenantId: snapshot.tenantId,
|
|
504
|
+
scope: 'line',
|
|
505
|
+
kind: 'return',
|
|
506
|
+
rate: '0',
|
|
507
|
+
amountNet: totalNet.toString(),
|
|
508
|
+
amountGross: totalGross.toString(),
|
|
509
|
+
currencyCode: order.currencyCode,
|
|
510
|
+
metadata: { returnId, returnLineId: lineSnapshot.id },
|
|
511
|
+
position: positionStart + index,
|
|
512
|
+
createdAt: new Date(),
|
|
513
|
+
updatedAt: new Date(),
|
|
514
|
+
})
|
|
515
|
+
createdAdjustments.push(adjustment)
|
|
516
|
+
em.persist(adjustment)
|
|
517
|
+
|
|
518
|
+
line.returnedQuantity = (toNumeric(line.returnedQuantity) + lineSnapshot.quantityReturned).toString()
|
|
519
|
+
line.updatedAt = new Date()
|
|
520
|
+
em.persist(line)
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
const lineSnapshots: SalesLineSnapshot[] = orderLines.map(mapOrderLineEntityToSnapshot)
|
|
524
|
+
const adjustmentDrafts: SalesAdjustmentDraft[] = [...existingAdjustments, ...createdAdjustments].map(
|
|
525
|
+
mapOrderAdjustmentToDraft,
|
|
526
|
+
)
|
|
527
|
+
const calculation = await salesCalculationService.calculateDocumentTotals({
|
|
528
|
+
documentKind: 'order',
|
|
529
|
+
lines: lineSnapshots,
|
|
530
|
+
adjustments: adjustmentDrafts,
|
|
531
|
+
context: buildCalculationContext(order),
|
|
532
|
+
})
|
|
533
|
+
applyOrderTotals(order, calculation.totals, calculation.lines.length)
|
|
534
|
+
order.updatedAt = new Date()
|
|
535
|
+
em.persist(order)
|
|
536
|
+
},
|
|
537
|
+
],
|
|
538
|
+
{ transaction: true },
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
return createdLines
|
|
542
|
+
}
|
|
543
|
+
|
|
248
544
|
function normalizeLinesInput(lines: ReturnCreateInput['lines']): ReturnLineInput[] {
|
|
249
545
|
const seen = new Set<string>()
|
|
250
546
|
const result: ReturnLineInput[] = []
|
|
@@ -464,254 +760,290 @@ const createReturnCommand: CommandHandler<ReturnCreateInput, { returnId: string
|
|
|
464
760
|
const after = payload?.after
|
|
465
761
|
if (!after) return
|
|
466
762
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
467
|
-
const
|
|
763
|
+
const salesCalculationService = ctx.container.resolve<SalesCalculationService>('salesCalculationService')
|
|
764
|
+
await reverseReturnEffects(em, salesCalculationService, after)
|
|
765
|
+
},
|
|
766
|
+
redo: async ({ ctx, logEntry }) => {
|
|
767
|
+
const after = resolveRedoSnapshot<ReturnSnapshot>(logEntry)
|
|
768
|
+
if (!after || !after.id) {
|
|
769
|
+
throw new CrudHttpError(400, { error: '[internal] redo snapshot unavailable for sales.returns.create' })
|
|
770
|
+
}
|
|
771
|
+
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
772
|
+
const salesCalculationService = ctx.container.resolve<SalesCalculationService>('salesCalculationService')
|
|
773
|
+
|
|
774
|
+
const createdLines = await restoreReturnEffects(em, salesCalculationService, after)
|
|
775
|
+
|
|
776
|
+
const header = await findOneWithDecryption(
|
|
468
777
|
em,
|
|
469
|
-
|
|
470
|
-
{ id: after.
|
|
778
|
+
SalesReturn,
|
|
779
|
+
{ id: after.id, deletedAt: null },
|
|
471
780
|
{},
|
|
472
781
|
{ tenantId: after.tenantId, organizationId: after.organizationId },
|
|
473
782
|
)
|
|
474
|
-
if (!
|
|
783
|
+
if (!header) {
|
|
784
|
+
throw new CrudHttpError(404, { error: 'sales.returns.orderMissing' })
|
|
785
|
+
}
|
|
475
786
|
|
|
476
|
-
const
|
|
787
|
+
const dataEngine = ctx.container.resolve('dataEngine') as DataEngine
|
|
788
|
+
await emitCrudSideEffects({
|
|
789
|
+
dataEngine,
|
|
790
|
+
action: 'created',
|
|
791
|
+
entity: header,
|
|
792
|
+
identifiers: { id: header.id, organizationId: header.organizationId, tenantId: header.tenantId },
|
|
793
|
+
indexer: { entityType: E.sales.sales_return },
|
|
794
|
+
events: returnCrudEvents,
|
|
795
|
+
})
|
|
477
796
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
{ tenantId: after.tenantId, organizationId: after.organizationId },
|
|
492
|
-
)
|
|
493
|
-
const lineMap = new Map(lines.map((line) => [line.id, line]))
|
|
494
|
-
after.lines.forEach((entry) => {
|
|
495
|
-
const line = lineMap.get(entry.orderLineId)
|
|
496
|
-
if (!line) return
|
|
497
|
-
const next = Math.max(0, toNumeric(line.returnedQuantity) - entry.quantityReturned)
|
|
498
|
-
line.returnedQuantity = next.toString()
|
|
499
|
-
line.updatedAt = new Date()
|
|
500
|
-
em.persist(line)
|
|
501
|
-
})
|
|
502
|
-
},
|
|
503
|
-
// The line returnedQuantity reversals above are persisted by
|
|
504
|
-
// withAtomicFlush's per-phase flush boundary before the adjustment /
|
|
505
|
-
// header / return-line lookups below run any query on this
|
|
506
|
-
// EntityManager. MikroORM v7 would otherwise silently discard the pending
|
|
507
|
-
// scalar changes on the managed `lines` when the next read resets the
|
|
508
|
-
// changeset (see SPEC-018).
|
|
509
|
-
async () => {
|
|
510
|
-
if (after.adjustmentIds.length) {
|
|
511
|
-
const adjustments = await findWithDecryption(
|
|
512
|
-
em,
|
|
513
|
-
SalesOrderAdjustment,
|
|
514
|
-
{ id: { $in: after.adjustmentIds }, deletedAt: null },
|
|
515
|
-
{},
|
|
516
|
-
{ tenantId: after.tenantId, organizationId: after.organizationId },
|
|
517
|
-
)
|
|
518
|
-
adjustments.forEach((adj) => em.remove(adj))
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const header = await findOneWithDecryption(
|
|
522
|
-
em,
|
|
523
|
-
SalesReturn,
|
|
524
|
-
{ id: after.id, deletedAt: null },
|
|
525
|
-
{},
|
|
526
|
-
{ tenantId: after.tenantId, organizationId: after.organizationId },
|
|
527
|
-
)
|
|
528
|
-
const returnLines = await findWithDecryption(
|
|
529
|
-
em,
|
|
530
|
-
SalesReturnLine,
|
|
531
|
-
{ salesReturn: after.id, deletedAt: null },
|
|
532
|
-
{},
|
|
533
|
-
{ tenantId: after.tenantId, organizationId: after.organizationId },
|
|
534
|
-
)
|
|
535
|
-
returnLines.forEach((line) => em.remove(line))
|
|
536
|
-
if (header) em.remove(header)
|
|
797
|
+
if (createdLines.length) {
|
|
798
|
+
await Promise.all(
|
|
799
|
+
createdLines.map((line) =>
|
|
800
|
+
emitCrudSideEffects({
|
|
801
|
+
dataEngine,
|
|
802
|
+
action: 'created',
|
|
803
|
+
entity: line,
|
|
804
|
+
identifiers: { id: line.id, organizationId: line.organizationId, tenantId: line.tenantId },
|
|
805
|
+
indexer: { entityType: E.sales.sales_return_line },
|
|
806
|
+
}),
|
|
807
|
+
),
|
|
808
|
+
)
|
|
809
|
+
}
|
|
537
810
|
|
|
538
|
-
|
|
539
|
-
em,
|
|
540
|
-
SalesOrderAdjustment,
|
|
541
|
-
{ order: order.id, deletedAt: null },
|
|
542
|
-
{ orderBy: { position: 'asc' } },
|
|
543
|
-
{ tenantId: after.tenantId, organizationId: after.organizationId },
|
|
544
|
-
)
|
|
545
|
-
const lineSnapshots: SalesLineSnapshot[] = lines.map(mapOrderLineEntityToSnapshot)
|
|
546
|
-
const adjustmentDrafts: SalesAdjustmentDraft[] = existingAdjustments.map(mapOrderAdjustmentToDraft)
|
|
547
|
-
const calculation = await salesCalculationService.calculateDocumentTotals({
|
|
548
|
-
documentKind: 'order',
|
|
549
|
-
lines: lineSnapshots,
|
|
550
|
-
adjustments: adjustmentDrafts,
|
|
551
|
-
context: buildCalculationContext(order),
|
|
552
|
-
})
|
|
553
|
-
applyOrderTotals(order, calculation.totals, calculation.lines.length)
|
|
554
|
-
order.updatedAt = new Date()
|
|
555
|
-
em.persist(order)
|
|
556
|
-
},
|
|
557
|
-
],
|
|
558
|
-
{ transaction: true },
|
|
559
|
-
)
|
|
811
|
+
return { returnId: header.id }
|
|
560
812
|
},
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const updateReturnCommand: CommandHandler<ReturnUpdateInput, { returnId: string }> = {
|
|
816
|
+
id: 'sales.returns.update',
|
|
817
|
+
async prepare(rawInput, ctx) {
|
|
818
|
+
const parsed = returnUpdateSchema.parse(rawInput ?? {})
|
|
819
|
+
const em = ctx.container.resolve('em') as EntityManager
|
|
820
|
+
const snapshot = await loadReturnHeaderSnapshot(em, parsed.id)
|
|
821
|
+
if (snapshot) {
|
|
822
|
+
ensureTenantScope(ctx, snapshot.tenantId)
|
|
823
|
+
ensureOrganizationScope(ctx, snapshot.organizationId)
|
|
566
824
|
}
|
|
825
|
+
return snapshot ? { before: snapshot } : {}
|
|
826
|
+
},
|
|
827
|
+
async execute(rawInput, ctx) {
|
|
828
|
+
const input = returnUpdateSchema.parse(rawInput ?? {})
|
|
829
|
+
ensureTenantScope(ctx, input.tenantId)
|
|
830
|
+
ensureOrganizationScope(ctx, input.organizationId)
|
|
831
|
+
const { translate } = await resolveTranslations()
|
|
567
832
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
568
|
-
const salesCalculationService = ctx.container.resolve<SalesCalculationService>('salesCalculationService')
|
|
569
833
|
|
|
570
|
-
const
|
|
834
|
+
const header = await em.transactional(async (tx) => {
|
|
835
|
+
const entity = await findOneWithDecryption(
|
|
836
|
+
tx,
|
|
837
|
+
SalesReturn,
|
|
838
|
+
{ id: input.id, deletedAt: null },
|
|
839
|
+
{ populate: ['order'] },
|
|
840
|
+
{ tenantId: input.tenantId, organizationId: input.organizationId },
|
|
841
|
+
)
|
|
842
|
+
if (!entity || !entity.order) {
|
|
843
|
+
throw new CrudHttpError(404, { error: translate('sales.returns.notFound', 'Return not found.') })
|
|
844
|
+
}
|
|
845
|
+
ensureSameScope(entity, input.organizationId, input.tenantId)
|
|
846
|
+
const orderId = typeof entity.order === 'string' ? entity.order : entity.order.id
|
|
847
|
+
if (input.orderId !== orderId) {
|
|
848
|
+
throw new CrudHttpError(400, { error: translate('sales.returns.orderMismatch', 'Return does not belong to this order.') })
|
|
849
|
+
}
|
|
850
|
+
// Lock on the return's own version — editing header fields (reason / notes /
|
|
851
|
+
// returnedAt) only touches the return, not the order totals.
|
|
852
|
+
enforceSalesDocumentOptimisticLock(ctx, entity, SALES_RESOURCE_KIND_RETURN)
|
|
571
853
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
{},
|
|
581
|
-
{ tenantId: after.tenantId, organizationId: after.organizationId },
|
|
582
|
-
)
|
|
583
|
-
if (!order) {
|
|
584
|
-
throw new CrudHttpError(404, { error: 'sales.returns.orderMissing' })
|
|
585
|
-
}
|
|
586
|
-
ensureSameScope(order, after.organizationId, after.tenantId)
|
|
854
|
+
if (input.reason !== undefined) entity.reason = input.reason.length ? input.reason : null
|
|
855
|
+
if (input.notes !== undefined) entity.notes = input.notes.length ? input.notes : null
|
|
856
|
+
if (input.returnedAt !== undefined) entity.returnedAt = input.returnedAt ?? null
|
|
857
|
+
entity.updatedAt = new Date()
|
|
858
|
+
tx.persist(entity)
|
|
859
|
+
await tx.flush()
|
|
860
|
+
return entity
|
|
861
|
+
})
|
|
587
862
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
863
|
+
const dataEngine = ctx.container.resolve('dataEngine') as DataEngine
|
|
864
|
+
await emitCrudSideEffects({
|
|
865
|
+
dataEngine,
|
|
866
|
+
action: 'updated',
|
|
867
|
+
entity: header,
|
|
868
|
+
identifiers: { id: header.id, organizationId: header.organizationId, tenantId: header.tenantId },
|
|
869
|
+
indexer: { entityType: E.sales.sales_return },
|
|
870
|
+
events: returnCrudEvents,
|
|
871
|
+
})
|
|
596
872
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
const adjustmentId = after.adjustmentIds[index] ?? randomUUID()
|
|
644
|
-
|
|
645
|
-
const returnLine = em.create(SalesReturnLine, {
|
|
646
|
-
id: lineSnapshot.id,
|
|
647
|
-
salesReturn: restoredHeader,
|
|
648
|
-
orderLine: em.getReference(SalesOrderLine, line.id),
|
|
649
|
-
organizationId: after.organizationId,
|
|
650
|
-
tenantId: after.tenantId,
|
|
651
|
-
quantityReturned: lineSnapshot.quantityReturned.toString(),
|
|
652
|
-
unitPriceNet: lineSnapshot.unitPriceNet.toString(),
|
|
653
|
-
unitPriceGross: lineSnapshot.unitPriceGross.toString(),
|
|
654
|
-
totalNetAmount: totalNet.toString(),
|
|
655
|
-
totalGrossAmount: totalGross.toString(),
|
|
656
|
-
createdAt: new Date(),
|
|
657
|
-
updatedAt: new Date(),
|
|
658
|
-
})
|
|
659
|
-
createdLines.push(returnLine)
|
|
660
|
-
em.persist(returnLine)
|
|
661
|
-
|
|
662
|
-
const adjustment = em.create(SalesOrderAdjustment, {
|
|
663
|
-
id: adjustmentId,
|
|
664
|
-
order,
|
|
665
|
-
orderLine: em.getReference(SalesOrderLine, line.id),
|
|
666
|
-
organizationId: after.organizationId,
|
|
667
|
-
tenantId: after.tenantId,
|
|
668
|
-
scope: 'line',
|
|
669
|
-
kind: 'return',
|
|
670
|
-
rate: '0',
|
|
671
|
-
amountNet: totalNet.toString(),
|
|
672
|
-
amountGross: totalGross.toString(),
|
|
673
|
-
currencyCode: order.currencyCode,
|
|
674
|
-
metadata: { returnId, returnLineId: lineSnapshot.id },
|
|
675
|
-
position: positionStart + index,
|
|
676
|
-
createdAt: new Date(),
|
|
677
|
-
updatedAt: new Date(),
|
|
678
|
-
})
|
|
679
|
-
createdAdjustments.push(adjustment)
|
|
680
|
-
em.persist(adjustment)
|
|
681
|
-
|
|
682
|
-
line.returnedQuantity = (toNumeric(line.returnedQuantity) + lineSnapshot.quantityReturned).toString()
|
|
683
|
-
line.updatedAt = new Date()
|
|
684
|
-
em.persist(line)
|
|
685
|
-
})
|
|
873
|
+
return { returnId: header.id }
|
|
874
|
+
},
|
|
875
|
+
captureAfter: async (_input, result, ctx) => {
|
|
876
|
+
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
877
|
+
return loadReturnHeaderSnapshot(em, result.returnId)
|
|
878
|
+
},
|
|
879
|
+
buildLog: async ({ snapshots, result }) => {
|
|
880
|
+
const { translate } = await resolveTranslations()
|
|
881
|
+
const before = snapshots.before as ReturnHeaderSnapshot | undefined
|
|
882
|
+
const after = snapshots.after as ReturnHeaderSnapshot | undefined
|
|
883
|
+
return {
|
|
884
|
+
actionLabel: translate('sales.audit.returns.update', 'Update return'),
|
|
885
|
+
resourceKind: 'sales.return',
|
|
886
|
+
resourceId: result.returnId,
|
|
887
|
+
parentResourceKind: 'sales.order',
|
|
888
|
+
parentResourceId: after?.orderId ?? before?.orderId ?? null,
|
|
889
|
+
tenantId: after?.tenantId ?? before?.tenantId ?? null,
|
|
890
|
+
organizationId: after?.organizationId ?? before?.organizationId ?? null,
|
|
891
|
+
snapshotBefore: before ?? null,
|
|
892
|
+
snapshotAfter: after ?? null,
|
|
893
|
+
payload: {
|
|
894
|
+
undo: { before, after } satisfies ReturnHeaderUndoPayload,
|
|
895
|
+
},
|
|
896
|
+
}
|
|
897
|
+
},
|
|
898
|
+
undo: async ({ logEntry, ctx }) => {
|
|
899
|
+
const payload = extractUndoPayload<ReturnHeaderUndoPayload>(logEntry)
|
|
900
|
+
const before = payload?.before
|
|
901
|
+
if (!before) return
|
|
902
|
+
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
903
|
+
await em.transactional(async (tx) => {
|
|
904
|
+
const entity = await findOneWithDecryption(
|
|
905
|
+
tx,
|
|
906
|
+
SalesReturn,
|
|
907
|
+
{ id: before.id, deletedAt: null },
|
|
908
|
+
{},
|
|
909
|
+
{ tenantId: before.tenantId, organizationId: before.organizationId },
|
|
910
|
+
)
|
|
911
|
+
if (!entity) return
|
|
912
|
+
entity.reason = before.reason
|
|
913
|
+
entity.notes = before.notes
|
|
914
|
+
entity.returnedAt = before.returnedAt ? new Date(before.returnedAt) : null
|
|
915
|
+
entity.updatedAt = new Date()
|
|
916
|
+
tx.persist(entity)
|
|
917
|
+
await tx.flush()
|
|
918
|
+
})
|
|
686
919
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
adjustments: adjustmentDrafts,
|
|
695
|
-
context: buildCalculationContext(order),
|
|
696
|
-
})
|
|
697
|
-
applyOrderTotals(order, calculation.totals, calculation.lines.length)
|
|
698
|
-
order.updatedAt = new Date()
|
|
699
|
-
em.persist(order)
|
|
700
|
-
},
|
|
701
|
-
],
|
|
702
|
-
{ transaction: true },
|
|
920
|
+
const dataEngine = ctx.container.resolve('dataEngine') as DataEngine
|
|
921
|
+
const restored = await findOneWithDecryption(
|
|
922
|
+
em,
|
|
923
|
+
SalesReturn,
|
|
924
|
+
{ id: before.id, deletedAt: null },
|
|
925
|
+
{},
|
|
926
|
+
{ tenantId: before.tenantId, organizationId: before.organizationId },
|
|
703
927
|
)
|
|
928
|
+
if (restored) {
|
|
929
|
+
await emitCrudSideEffects({
|
|
930
|
+
dataEngine,
|
|
931
|
+
action: 'updated',
|
|
932
|
+
entity: restored,
|
|
933
|
+
identifiers: { id: restored.id, organizationId: restored.organizationId, tenantId: restored.tenantId },
|
|
934
|
+
indexer: { entityType: E.sales.sales_return },
|
|
935
|
+
events: returnCrudEvents,
|
|
936
|
+
})
|
|
937
|
+
}
|
|
938
|
+
},
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const deleteReturnCommand: CommandHandler<ReturnDeleteInput, { returnId: string }> = {
|
|
942
|
+
id: 'sales.returns.delete',
|
|
943
|
+
async prepare(rawInput, ctx) {
|
|
944
|
+
const parsed = returnDeleteSchema.parse(rawInput ?? {})
|
|
945
|
+
const em = ctx.container.resolve('em') as EntityManager
|
|
946
|
+
const snapshot = await loadReturnSnapshot(em, parsed.id)
|
|
947
|
+
if (snapshot) {
|
|
948
|
+
ensureTenantScope(ctx, snapshot.tenantId)
|
|
949
|
+
ensureOrganizationScope(ctx, snapshot.organizationId)
|
|
950
|
+
}
|
|
951
|
+
return snapshot ? { before: snapshot } : {}
|
|
952
|
+
},
|
|
953
|
+
async execute(rawInput, ctx) {
|
|
954
|
+
const input = returnDeleteSchema.parse(rawInput ?? {})
|
|
955
|
+
ensureTenantScope(ctx, input.tenantId)
|
|
956
|
+
ensureOrganizationScope(ctx, input.organizationId)
|
|
957
|
+
const { translate } = await resolveTranslations()
|
|
958
|
+
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
959
|
+
const salesCalculationService = ctx.container.resolve<SalesCalculationService>('salesCalculationService')
|
|
960
|
+
|
|
961
|
+
const snapshot = await loadReturnSnapshot(em, input.id)
|
|
962
|
+
if (!snapshot) {
|
|
963
|
+
throw new CrudHttpError(404, { error: translate('sales.returns.notFound', 'Return not found.') })
|
|
964
|
+
}
|
|
965
|
+
ensureSameScope(snapshot, input.organizationId, input.tenantId)
|
|
966
|
+
if (input.orderId !== snapshot.orderId) {
|
|
967
|
+
throw new CrudHttpError(400, { error: translate('sales.returns.orderMismatch', 'Return does not belong to this order.') })
|
|
968
|
+
}
|
|
704
969
|
|
|
705
970
|
const header = await findOneWithDecryption(
|
|
706
971
|
em,
|
|
707
972
|
SalesReturn,
|
|
708
|
-
{ id:
|
|
973
|
+
{ id: input.id, deletedAt: null },
|
|
709
974
|
{},
|
|
710
|
-
{ tenantId:
|
|
975
|
+
{ tenantId: input.tenantId, organizationId: input.organizationId },
|
|
711
976
|
)
|
|
712
977
|
if (!header) {
|
|
713
|
-
throw new CrudHttpError(404, { error: 'sales.returns.
|
|
978
|
+
throw new CrudHttpError(404, { error: translate('sales.returns.notFound', 'Return not found.') })
|
|
979
|
+
}
|
|
980
|
+
ensureSameScope(header, input.organizationId, input.tenantId)
|
|
981
|
+
// Lock on the return's own version, captured before any mutation.
|
|
982
|
+
enforceSalesDocumentOptimisticLock(ctx, header, SALES_RESOURCE_KIND_RETURN)
|
|
983
|
+
|
|
984
|
+
await reverseReturnEffects(em, salesCalculationService, snapshot)
|
|
985
|
+
|
|
986
|
+
const dataEngine = ctx.container.resolve('dataEngine') as DataEngine
|
|
987
|
+
await emitCrudSideEffects({
|
|
988
|
+
dataEngine,
|
|
989
|
+
action: 'deleted',
|
|
990
|
+
entity: header,
|
|
991
|
+
identifiers: { id: snapshot.id, organizationId: snapshot.organizationId, tenantId: snapshot.tenantId },
|
|
992
|
+
indexer: { entityType: E.sales.sales_return },
|
|
993
|
+
events: returnCrudEvents,
|
|
994
|
+
})
|
|
995
|
+
|
|
996
|
+
if (snapshot.lines.length) {
|
|
997
|
+
await Promise.all(
|
|
998
|
+
snapshot.lines.map((line) =>
|
|
999
|
+
emitCrudSideEffects({
|
|
1000
|
+
dataEngine,
|
|
1001
|
+
action: 'deleted',
|
|
1002
|
+
entity: { id: line.id, organizationId: snapshot.organizationId, tenantId: snapshot.tenantId },
|
|
1003
|
+
identifiers: { id: line.id, organizationId: snapshot.organizationId, tenantId: snapshot.tenantId },
|
|
1004
|
+
indexer: { entityType: E.sales.sales_return_line },
|
|
1005
|
+
}),
|
|
1006
|
+
),
|
|
1007
|
+
)
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
return { returnId: snapshot.id }
|
|
1011
|
+
},
|
|
1012
|
+
buildLog: async ({ snapshots, result }) => {
|
|
1013
|
+
const before = snapshots.before as ReturnSnapshot | undefined
|
|
1014
|
+
if (!before) return null
|
|
1015
|
+
const { translate } = await resolveTranslations()
|
|
1016
|
+
return {
|
|
1017
|
+
actionLabel: translate('sales.audit.returns.delete', 'Delete return'),
|
|
1018
|
+
resourceKind: 'sales.return',
|
|
1019
|
+
resourceId: result.returnId,
|
|
1020
|
+
parentResourceKind: 'sales.order',
|
|
1021
|
+
parentResourceId: before.orderId ?? null,
|
|
1022
|
+
tenantId: before.tenantId,
|
|
1023
|
+
organizationId: before.organizationId,
|
|
1024
|
+
snapshotBefore: before,
|
|
1025
|
+
payload: {
|
|
1026
|
+
undo: { before } satisfies ReturnDeleteUndoPayload,
|
|
1027
|
+
},
|
|
714
1028
|
}
|
|
1029
|
+
},
|
|
1030
|
+
undo: async ({ logEntry, ctx }) => {
|
|
1031
|
+
const payload = extractUndoPayload<ReturnDeleteUndoPayload>(logEntry)
|
|
1032
|
+
const before = payload?.before
|
|
1033
|
+
if (!before) return
|
|
1034
|
+
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
1035
|
+
const salesCalculationService = ctx.container.resolve<SalesCalculationService>('salesCalculationService')
|
|
1036
|
+
|
|
1037
|
+
const createdLines = await restoreReturnEffects(em, salesCalculationService, before)
|
|
1038
|
+
|
|
1039
|
+
const header = await findOneWithDecryption(
|
|
1040
|
+
em,
|
|
1041
|
+
SalesReturn,
|
|
1042
|
+
{ id: before.id, deletedAt: null },
|
|
1043
|
+
{},
|
|
1044
|
+
{ tenantId: before.tenantId, organizationId: before.organizationId },
|
|
1045
|
+
)
|
|
1046
|
+
if (!header) return
|
|
715
1047
|
|
|
716
1048
|
const dataEngine = ctx.container.resolve('dataEngine') as DataEngine
|
|
717
1049
|
await emitCrudSideEffects({
|
|
@@ -736,11 +1068,11 @@ const createReturnCommand: CommandHandler<ReturnCreateInput, { returnId: string
|
|
|
736
1068
|
),
|
|
737
1069
|
)
|
|
738
1070
|
}
|
|
739
|
-
|
|
740
|
-
return { returnId: header.id }
|
|
741
1071
|
},
|
|
742
1072
|
}
|
|
743
1073
|
|
|
744
1074
|
registerCommand(createReturnCommand)
|
|
1075
|
+
registerCommand(updateReturnCommand)
|
|
1076
|
+
registerCommand(deleteReturnCommand)
|
|
745
1077
|
|
|
746
|
-
export const returnCommands = [createReturnCommand]
|
|
1078
|
+
export const returnCommands = [createReturnCommand, updateReturnCommand, deleteReturnCommand]
|