@open-mercato/core 0.6.6-develop.6305.1.4503070c9d → 0.6.6-develop.6313.1.618a49a6b4
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/dist/helpers/integration/salesFixtures.js +10 -0
- package/dist/helpers/integration/salesFixtures.js.map +2 -2
- package/dist/modules/customers/data/guards.js +10 -1
- package/dist/modules/customers/data/guards.js.map +2 -2
- package/dist/modules/sales/commands/returns.js +38 -3
- package/dist/modules/sales/commands/returns.js.map +2 -2
- package/dist/modules/sales/components/documents/ReturnDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/ReturnsSection.js +18 -5
- package/dist/modules/sales/components/documents/ReturnsSection.js.map +2 -2
- package/dist/modules/sales/lib/returnQuantity.js +29 -2
- package/dist/modules/sales/lib/returnQuantity.js.map +2 -2
- package/dist/modules/workflows/lib/event-trigger-service.js.map +2 -2
- package/package.json +7 -7
- package/src/helpers/integration/salesFixtures.ts +21 -0
- package/src/modules/customers/data/guards.ts +29 -0
- package/src/modules/sales/commands/returns.ts +44 -3
- package/src/modules/sales/components/documents/ReturnDialog.tsx +1 -0
- package/src/modules/sales/components/documents/ReturnsSection.tsx +18 -5
- package/src/modules/sales/i18n/de.json +1 -0
- package/src/modules/sales/i18n/en.json +1 -0
- package/src/modules/sales/i18n/es.json +1 -0
- package/src/modules/sales/i18n/pl.json +1 -0
- package/src/modules/sales/lib/returnQuantity.ts +48 -1
- package/src/modules/workflows/lib/event-trigger-service.ts +3 -0
|
@@ -14,7 +14,9 @@ import type { SalesCalculationService } from '../services/salesCalculationServic
|
|
|
14
14
|
import type { SalesAdjustmentDraft, SalesLineSnapshot, SalesDocumentCalculationResult } from '../lib/types'
|
|
15
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
|
-
import { SalesOrder, SalesOrderAdjustment, SalesOrderLine, SalesReturn, SalesReturnLine } from '../data/entities'
|
|
17
|
+
import { SalesOrder, SalesOrderAdjustment, SalesOrderLine, SalesReturn, SalesReturnLine, SalesShipment, SalesShipmentItem } from '../data/entities'
|
|
18
|
+
import { coerceShipmentQuantity } from '../lib/shipments/snapshots'
|
|
19
|
+
import { computeAvailableReturnQuantity } from '../lib/returnQuantity'
|
|
18
20
|
import {
|
|
19
21
|
returnCreateSchema,
|
|
20
22
|
returnUpdateSchema,
|
|
@@ -541,6 +543,36 @@ async function restoreReturnEffects(
|
|
|
541
543
|
return createdLines
|
|
542
544
|
}
|
|
543
545
|
|
|
546
|
+
async function loadShippedQuantityByLine(
|
|
547
|
+
em: EntityManager,
|
|
548
|
+
orderId: string,
|
|
549
|
+
scope: { tenantId: string; organizationId: string },
|
|
550
|
+
): Promise<Map<string, number>> {
|
|
551
|
+
const shipments = await findWithDecryption(
|
|
552
|
+
em,
|
|
553
|
+
SalesShipment,
|
|
554
|
+
{ order: orderId, deletedAt: null },
|
|
555
|
+
{},
|
|
556
|
+
scope,
|
|
557
|
+
)
|
|
558
|
+
const shippedByLine = new Map<string, number>()
|
|
559
|
+
if (!shipments.length) return shippedByLine
|
|
560
|
+
const items = await findWithDecryption(
|
|
561
|
+
em,
|
|
562
|
+
SalesShipmentItem,
|
|
563
|
+
{ shipment: { $in: shipments.map((shipment) => shipment.id) } },
|
|
564
|
+
{},
|
|
565
|
+
scope,
|
|
566
|
+
)
|
|
567
|
+
items.forEach((item) => {
|
|
568
|
+
const orderLineId = typeof item.orderLine === 'string' ? item.orderLine : item.orderLine?.id ?? null
|
|
569
|
+
if (!orderLineId) return
|
|
570
|
+
const next = (shippedByLine.get(orderLineId) ?? 0) + coerceShipmentQuantity(item.quantity)
|
|
571
|
+
shippedByLine.set(orderLineId, next)
|
|
572
|
+
})
|
|
573
|
+
return shippedByLine
|
|
574
|
+
}
|
|
575
|
+
|
|
544
576
|
function normalizeLinesInput(lines: ReturnCreateInput['lines']): ReturnLineInput[] {
|
|
545
577
|
const seen = new Set<string>()
|
|
546
578
|
const result: ReturnLineInput[] = []
|
|
@@ -594,14 +626,23 @@ const createReturnCommand: CommandHandler<ReturnCreateInput, { returnId: string
|
|
|
594
626
|
)
|
|
595
627
|
const lineMap = new Map(orderLines.map((line) => [line.id, line]))
|
|
596
628
|
|
|
629
|
+
const shippedByLine = await loadShippedQuantityByLine(tx, order.id, {
|
|
630
|
+
tenantId: input.tenantId,
|
|
631
|
+
organizationId: input.organizationId,
|
|
632
|
+
})
|
|
633
|
+
|
|
597
634
|
requested.forEach(({ orderLineId, quantity }) => {
|
|
598
635
|
const line = lineMap.get(orderLineId)
|
|
599
636
|
if (!line) {
|
|
600
637
|
throw new CrudHttpError(404, { error: translate('sales.returns.lineMissing', 'Order line not found.') })
|
|
601
638
|
}
|
|
602
|
-
const available =
|
|
639
|
+
const available = computeAvailableReturnQuantity({
|
|
640
|
+
quantity: toNumeric(line.quantity),
|
|
641
|
+
returnedQuantity: toNumeric(line.returnedQuantity),
|
|
642
|
+
shippedQuantity: shippedByLine.get(orderLineId) ?? 0,
|
|
643
|
+
})
|
|
603
644
|
if (quantity - 1e-6 > available) {
|
|
604
|
-
throw new CrudHttpError(400, { error: translate('sales.returns.
|
|
645
|
+
throw new CrudHttpError(400, { error: translate('sales.returns.quantityExceedsShipped', 'Cannot return more than the shipped quantity. Ship the items before recording a return.') })
|
|
605
646
|
}
|
|
606
647
|
})
|
|
607
648
|
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
emitSalesDocumentTotalsRefresh,
|
|
18
18
|
subscribeSalesDocumentTotalsRefresh,
|
|
19
19
|
} from '@open-mercato/core/modules/sales/lib/frontend/documentTotalsEvents'
|
|
20
|
+
import { sumShippedQuantityByLine } from '@open-mercato/core/modules/sales/lib/returnQuantity'
|
|
20
21
|
import { formatMoney, normalizeNumber } from './lineItemUtils'
|
|
21
22
|
import { ReturnDialog, type ReturnOrderLine } from './ReturnDialog'
|
|
22
23
|
import { ReturnEditDialog, type ReturnEditRecord } from './ReturnEditDialog'
|
|
@@ -60,14 +61,25 @@ export function SalesReturnsSection({ orderId, currencyCode, documentUpdatedAt }
|
|
|
60
61
|
|
|
61
62
|
const loadLines = React.useCallback(async () => {
|
|
62
63
|
const params = new URLSearchParams({ page: '1', pageSize: '100', orderId })
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
const shipmentParams = new URLSearchParams({ page: '1', pageSize: '200', orderId })
|
|
65
|
+
const [response, shipmentsResponse] = await Promise.all([
|
|
66
|
+
apiCall<{ items?: Array<Record<string, unknown>> }>(
|
|
67
|
+
`/api/sales/order-lines?${params.toString()}`,
|
|
68
|
+
undefined,
|
|
69
|
+
{ fallback: { items: [] } },
|
|
70
|
+
),
|
|
71
|
+
apiCall<{ items?: Array<Record<string, unknown>> }>(
|
|
72
|
+
`/api/sales/shipments?${shipmentParams.toString()}`,
|
|
73
|
+
undefined,
|
|
74
|
+
{ fallback: { items: [] } },
|
|
75
|
+
),
|
|
76
|
+
])
|
|
77
|
+
const shippedByLine = sumShippedQuantityByLine(
|
|
78
|
+
Array.isArray(shipmentsResponse.result?.items) ? shipmentsResponse.result?.items : [],
|
|
67
79
|
)
|
|
68
80
|
const items = Array.isArray(response.result?.items) ? response.result?.items ?? [] : []
|
|
69
81
|
const mapped: ReturnOrderLine[] = items
|
|
70
|
-
.map((item) => {
|
|
82
|
+
.map((item): ReturnOrderLine | null => {
|
|
71
83
|
const map = item as Record<string, unknown>
|
|
72
84
|
const id = typeof map.id === 'string' ? map.id : null
|
|
73
85
|
if (!id) return null
|
|
@@ -110,6 +122,7 @@ export function SalesReturnsSection({ orderId, currencyCode, documentUpdatedAt }
|
|
|
110
122
|
lineNumber,
|
|
111
123
|
quantity: Number.isFinite(quantity) ? quantity : 0,
|
|
112
124
|
returnedQuantity: Number.isFinite(returnedQuantity) ? returnedQuantity : 0,
|
|
125
|
+
shippedQuantity: shippedByLine.get(id) ?? 0,
|
|
113
126
|
}
|
|
114
127
|
})
|
|
115
128
|
.filter((entry): entry is ReturnOrderLine => Boolean(entry?.id))
|
|
@@ -1326,6 +1326,7 @@
|
|
|
1326
1326
|
"sales.returns.qty.increase": "Menge erhöhen",
|
|
1327
1327
|
"sales.returns.quantity": "Menge",
|
|
1328
1328
|
"sales.returns.quantityExceeded": "Es kann nicht mehr als die verbleibende Menge zurückgegeben werden.",
|
|
1329
|
+
"sales.returns.quantityExceedsShipped": "Es kann nicht mehr als die versandte Menge zurückgegeben werden. Versenden Sie die Artikel, bevor Sie eine Rücksendung erfassen.",
|
|
1329
1330
|
"sales.returns.reason": "Grund",
|
|
1330
1331
|
"sales.returns.reason.placeholder": "Optional",
|
|
1331
1332
|
"sales.returns.returnNumber": "Rückgabe",
|
|
@@ -1326,6 +1326,7 @@
|
|
|
1326
1326
|
"sales.returns.qty.increase": "Increase quantity",
|
|
1327
1327
|
"sales.returns.quantity": "Quantity",
|
|
1328
1328
|
"sales.returns.quantityExceeded": "Cannot return more than the remaining quantity.",
|
|
1329
|
+
"sales.returns.quantityExceedsShipped": "Cannot return more than the shipped quantity. Ship the items before recording a return.",
|
|
1329
1330
|
"sales.returns.reason": "Reason",
|
|
1330
1331
|
"sales.returns.reason.placeholder": "Optional",
|
|
1331
1332
|
"sales.returns.returnNumber": "Return",
|
|
@@ -1326,6 +1326,7 @@
|
|
|
1326
1326
|
"sales.returns.qty.increase": "Aumentar cantidad",
|
|
1327
1327
|
"sales.returns.quantity": "Cantidad",
|
|
1328
1328
|
"sales.returns.quantityExceeded": "No se puede devolver más que la cantidad restante.",
|
|
1329
|
+
"sales.returns.quantityExceedsShipped": "No se puede devolver más que la cantidad enviada. Envíe los artículos antes de registrar una devolución.",
|
|
1329
1330
|
"sales.returns.reason": "Motivo",
|
|
1330
1331
|
"sales.returns.reason.placeholder": "Opcional",
|
|
1331
1332
|
"sales.returns.returnNumber": "Devolución",
|
|
@@ -1326,6 +1326,7 @@
|
|
|
1326
1326
|
"sales.returns.qty.increase": "Zwiększ ilość",
|
|
1327
1327
|
"sales.returns.quantity": "Ilość",
|
|
1328
1328
|
"sales.returns.quantityExceeded": "Nie można zwrócić więcej niż pozostała ilość.",
|
|
1329
|
+
"sales.returns.quantityExceedsShipped": "Nie można zwrócić więcej niż wysłana ilość. Wyślij artykuły, zanim zarejestrujesz zwrot.",
|
|
1329
1330
|
"sales.returns.reason": "Powód",
|
|
1330
1331
|
"sales.returns.reason.placeholder": "Opcjonalny",
|
|
1331
1332
|
"sales.returns.returnNumber": "Zwrot",
|
|
@@ -1,10 +1,57 @@
|
|
|
1
1
|
export type ReturnAvailableInput = {
|
|
2
2
|
quantity: number
|
|
3
3
|
returnedQuantity: number
|
|
4
|
+
shippedQuantity?: number
|
|
4
5
|
}
|
|
5
6
|
|
|
6
7
|
export function computeAvailableReturnQuantity(line: ReturnAvailableInput): number {
|
|
7
|
-
const
|
|
8
|
+
const cap =
|
|
9
|
+
typeof line.shippedQuantity === 'number' && Number.isFinite(line.shippedQuantity)
|
|
10
|
+
? Math.min(line.quantity, line.shippedQuantity)
|
|
11
|
+
: line.quantity
|
|
12
|
+
const raw = cap - line.returnedQuantity
|
|
8
13
|
if (!Number.isFinite(raw) || raw <= 0) return 0
|
|
9
14
|
return Math.max(0, Math.floor(raw + 1e-6))
|
|
10
15
|
}
|
|
16
|
+
|
|
17
|
+
function coerceQuantity(value: unknown): number {
|
|
18
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
19
|
+
if (typeof value === 'string' && value.trim().length) {
|
|
20
|
+
const parsed = Number(value)
|
|
21
|
+
if (Number.isFinite(parsed)) return parsed
|
|
22
|
+
}
|
|
23
|
+
return 0
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Aggregate shipped quantity per order line from the `/api/sales/shipments`
|
|
28
|
+
* list payload. Mirrors the server-side `loadShippedQuantityByLine` cap so the
|
|
29
|
+
* Return dialog's "Available" figure matches what `sales.returns.create`
|
|
30
|
+
* enforces. Each shipment exposes its lines under `items` (either the
|
|
31
|
+
* persisted snapshot or DB-grouped rows), both carrying `orderLineId` and
|
|
32
|
+
* `quantity`.
|
|
33
|
+
*/
|
|
34
|
+
export function sumShippedQuantityByLine(
|
|
35
|
+
shipments: ReadonlyArray<Record<string, unknown>> | null | undefined,
|
|
36
|
+
): Map<string, number> {
|
|
37
|
+
const shippedByLine = new Map<string, number>()
|
|
38
|
+
if (!Array.isArray(shipments)) return shippedByLine
|
|
39
|
+
for (const shipment of shipments) {
|
|
40
|
+
if (!shipment || typeof shipment !== 'object') continue
|
|
41
|
+
const lineItems = (shipment as Record<string, unknown>).items
|
|
42
|
+
if (!Array.isArray(lineItems)) continue
|
|
43
|
+
for (const lineItem of lineItems) {
|
|
44
|
+
if (!lineItem || typeof lineItem !== 'object') continue
|
|
45
|
+
const record = lineItem as Record<string, unknown>
|
|
46
|
+
const orderLineId =
|
|
47
|
+
typeof record.orderLineId === 'string'
|
|
48
|
+
? record.orderLineId
|
|
49
|
+
: typeof record.order_line_id === 'string'
|
|
50
|
+
? record.order_line_id
|
|
51
|
+
: null
|
|
52
|
+
if (!orderLineId) continue
|
|
53
|
+
shippedByLine.set(orderLineId, (shippedByLine.get(orderLineId) ?? 0) + coerceQuantity(record.quantity))
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return shippedByLine
|
|
57
|
+
}
|
|
@@ -123,6 +123,9 @@ function isSafeWorkflowRegexPattern(pattern: string): boolean {
|
|
|
123
123
|
let lastClosedGroup: RegexGroupFrame | null = null
|
|
124
124
|
let lastAtomWasQuantified = false
|
|
125
125
|
|
|
126
|
+
// Hand-written single-pass lexer: `index` is intentionally advanced inside the
|
|
127
|
+
// loop body (escape pairs, `(?:` prefixes, multi-char quantifiers, lazy `?`) on
|
|
128
|
+
// top of the `index += 1` update clause. Do not "simplify" these in-body writes.
|
|
126
129
|
for (let index = 0; index < pattern.length; index += 1) {
|
|
127
130
|
const char = pattern[index]
|
|
128
131
|
|