@open-mercato/core 0.10.1-develop.7728.1.6b52eb2849 → 0.10.1-develop.7736.1.88e07906bf
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/attachments/lib/ocrService.js +8 -0
- package/dist/modules/attachments/lib/ocrService.js.map +2 -2
- package/dist/modules/auth/api/logout.js +3 -1
- package/dist/modules/auth/api/logout.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +1190 -1408
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/commands/returns.js +34 -11
- package/dist/modules/sales/commands/returns.js.map +2 -2
- package/dist/modules/sales/commands/shared.js +5 -0
- package/dist/modules/sales/commands/shared.js.map +2 -2
- package/dist/modules/sales/commands/shipments.js +9 -0
- package/dist/modules/sales/commands/shipments.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-projects/route.js +6 -2
- package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/attachments/lib/ocrService.ts +8 -0
- package/src/modules/auth/api/logout.ts +3 -1
- package/src/modules/sales/commands/documents.ts +1501 -1626
- package/src/modules/sales/commands/returns.ts +41 -14
- package/src/modules/sales/commands/shared.ts +40 -0
- package/src/modules/sales/commands/shipments.ts +10 -0
- package/src/modules/staff/api/timesheets/time-projects/route.ts +6 -2
|
@@ -15,7 +15,7 @@ import type { SalesCalculationService } from '../services/salesCalculationServic
|
|
|
15
15
|
import type { SalesAdjustmentDraft, SalesLineSnapshot, SalesDocumentCalculationResult } from '../lib/types'
|
|
16
16
|
import { isExternalAmountsMode } from '../lib/calculations'
|
|
17
17
|
import { readPersistedHeaderTotals } from '../lib/externalAmounts'
|
|
18
|
-
import { cloneJson, deriveLineNetFromGross, ensureOrganizationScope, ensureSameScope, ensureTenantScope, extractUndoPayload, toNumericString, enforceSalesDocumentOptimisticLock, SALES_RESOURCE_KIND_ORDER, SALES_RESOURCE_KIND_RETURN } from './shared'
|
|
18
|
+
import { cloneJson, deriveLineNetFromGross, ensureOrganizationScope, ensureSameScope, ensureTenantScope, extractUndoPayload, toNumericString, enforceSalesDocumentOptimisticLock, lockSalesDocumentRow, SALES_RESOURCE_KIND_ORDER, SALES_RESOURCE_KIND_RETURN } from './shared'
|
|
19
19
|
import { resolveRedoSnapshot } from '@open-mercato/shared/lib/commands/redo'
|
|
20
20
|
import { SalesOrder, SalesOrderAdjustment, SalesOrderLine, SalesReturn, SalesReturnLine } from '../data/entities'
|
|
21
21
|
import { mapOrderLineEntityToSnapshot } from '../lib/lineSnapshots'
|
|
@@ -317,6 +317,21 @@ async function loadReturnHeaderSnapshot(em: EntityManager, id: string): Promise<
|
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
async function lockReturnOrder(
|
|
321
|
+
em: EntityManager,
|
|
322
|
+
orderId: string,
|
|
323
|
+
scope: { tenantId: string; organizationId: string },
|
|
324
|
+
): Promise<SalesOrder | null> {
|
|
325
|
+
if (!(await lockSalesDocumentRow(em, 'sales_orders', orderId, scope))) return null
|
|
326
|
+
return findOneWithDecryption(
|
|
327
|
+
em,
|
|
328
|
+
SalesOrder,
|
|
329
|
+
{ id: orderId, deletedAt: null, ...scope },
|
|
330
|
+
{ refresh: true },
|
|
331
|
+
scope,
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
|
|
320
335
|
/**
|
|
321
336
|
* Reverse the order-level effects of a return: restore each order line's
|
|
322
337
|
* `returnedQuantity`, drop the return's line-scoped credit adjustments, remove
|
|
@@ -337,20 +352,17 @@ async function reverseReturnEffects(
|
|
|
337
352
|
salesCalculationService: SalesCalculationService,
|
|
338
353
|
snapshot: ReturnSnapshot,
|
|
339
354
|
): Promise<void> {
|
|
340
|
-
|
|
341
|
-
em,
|
|
342
|
-
SalesOrder,
|
|
343
|
-
{ id: snapshot.orderId, deletedAt: null },
|
|
344
|
-
{},
|
|
345
|
-
{ tenantId: snapshot.tenantId, organizationId: snapshot.organizationId },
|
|
346
|
-
)
|
|
347
|
-
if (!order) return
|
|
348
|
-
|
|
355
|
+
let order: SalesOrder | null = null
|
|
349
356
|
let lines: SalesOrderLine[] = []
|
|
350
357
|
await withAtomicFlush(
|
|
351
358
|
em,
|
|
352
359
|
[
|
|
353
360
|
async () => {
|
|
361
|
+
order = await lockReturnOrder(em, snapshot.orderId, {
|
|
362
|
+
tenantId: snapshot.tenantId,
|
|
363
|
+
organizationId: snapshot.organizationId,
|
|
364
|
+
})
|
|
365
|
+
if (!order) return
|
|
354
366
|
lines = await findWithDecryption(
|
|
355
367
|
em,
|
|
356
368
|
SalesOrderLine,
|
|
@@ -369,6 +381,7 @@ async function reverseReturnEffects(
|
|
|
369
381
|
})
|
|
370
382
|
},
|
|
371
383
|
async () => {
|
|
384
|
+
if (!order) return
|
|
372
385
|
if (snapshot.adjustmentIds.length) {
|
|
373
386
|
const adjustments = await findWithDecryption(
|
|
374
387
|
em,
|
|
@@ -450,17 +463,24 @@ async function restoreReturnEffects(
|
|
|
450
463
|
em,
|
|
451
464
|
[
|
|
452
465
|
async () => {
|
|
453
|
-
const
|
|
466
|
+
const existingOrder = await findOneWithDecryption(
|
|
454
467
|
em,
|
|
455
468
|
SalesOrder,
|
|
456
469
|
{ id: snapshot.orderId, deletedAt: null },
|
|
457
470
|
{},
|
|
458
471
|
{ tenantId: snapshot.tenantId, organizationId: snapshot.organizationId },
|
|
459
472
|
)
|
|
473
|
+
if (!existingOrder) {
|
|
474
|
+
throw notFound(translate('sales.returns.orderMissing', 'Order not found.'))
|
|
475
|
+
}
|
|
476
|
+
ensureSameScope(existingOrder, snapshot.organizationId, snapshot.tenantId)
|
|
477
|
+
const order = await lockReturnOrder(em, existingOrder.id, {
|
|
478
|
+
tenantId: snapshot.tenantId,
|
|
479
|
+
organizationId: snapshot.organizationId,
|
|
480
|
+
})
|
|
460
481
|
if (!order) {
|
|
461
482
|
throw notFound(translate('sales.returns.orderMissing', 'Order not found.'))
|
|
462
483
|
}
|
|
463
|
-
ensureSameScope(order, snapshot.organizationId, snapshot.tenantId)
|
|
464
484
|
|
|
465
485
|
const orderLines = await findWithDecryption(
|
|
466
486
|
em,
|
|
@@ -643,17 +663,24 @@ const createReturnCommand: CommandHandler<ReturnCreateInput, { returnId: string
|
|
|
643
663
|
|
|
644
664
|
const salesCalculationService = ctx.container.resolve<SalesCalculationService>('salesCalculationService')
|
|
645
665
|
const { header, createdLines, order } = await em.transactional(async (tx) => {
|
|
646
|
-
const
|
|
666
|
+
const existingOrder = await findOneWithDecryption(
|
|
647
667
|
tx,
|
|
648
668
|
SalesOrder,
|
|
649
669
|
{ id: input.orderId, deletedAt: null },
|
|
650
670
|
{},
|
|
651
671
|
{ tenantId: input.tenantId, organizationId: input.organizationId },
|
|
652
672
|
)
|
|
673
|
+
if (!existingOrder) {
|
|
674
|
+
throw notFound(translate('sales.returns.orderMissing', 'Order not found.'))
|
|
675
|
+
}
|
|
676
|
+
ensureSameScope(existingOrder, input.organizationId, input.tenantId)
|
|
677
|
+
const order = await lockReturnOrder(tx, existingOrder.id, {
|
|
678
|
+
tenantId: input.tenantId,
|
|
679
|
+
organizationId: input.organizationId,
|
|
680
|
+
})
|
|
653
681
|
if (!order) {
|
|
654
682
|
throw notFound(translate('sales.returns.orderMissing', 'Order not found.'))
|
|
655
683
|
}
|
|
656
|
-
ensureSameScope(order, input.organizationId, input.tenantId)
|
|
657
684
|
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER)
|
|
658
685
|
|
|
659
686
|
const orderLines = await findWithDecryption(
|
|
@@ -49,6 +49,46 @@ export async function enforceSalesDocumentOptimisticLock(
|
|
|
49
49
|
})
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
type SalesDocumentLockTable = 'sales_orders' | 'sales_quotes'
|
|
53
|
+
type SalesDocumentLockRow = {
|
|
54
|
+
id: string
|
|
55
|
+
tenant_id: string
|
|
56
|
+
organization_id: string
|
|
57
|
+
deleted_at: Date | null
|
|
58
|
+
}
|
|
59
|
+
type SalesDocumentLockDatabase = Record<SalesDocumentLockTable, SalesDocumentLockRow>
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Take the parent-document row lock that serializes every write deriving the
|
|
63
|
+
* order/quote header (totals, line count, paid amounts) or the document's child
|
|
64
|
+
* rows (lines, adjustments, returned/fulfilled quantities) inside the caller's
|
|
65
|
+
* transaction. Acquire it BEFORE reading the child rows the header is
|
|
66
|
+
* recalculated from, and before locking those child rows, so all writers use the
|
|
67
|
+
* same parent-then-children lock order (#6463).
|
|
68
|
+
*
|
|
69
|
+
* `FOR NO KEY UPDATE` conflicts with other writers of the same document, including
|
|
70
|
+
* the `FOR UPDATE` locks taken by payments and undo, while still allowing the
|
|
71
|
+
* `KEY SHARE` locks that foreign-key inserts (for example shipments) take on the
|
|
72
|
+
* parent row. Returns false when no live row matches the scoped id.
|
|
73
|
+
*/
|
|
74
|
+
export async function lockSalesDocumentRow(
|
|
75
|
+
em: EntityManager,
|
|
76
|
+
table: SalesDocumentLockTable,
|
|
77
|
+
id: string,
|
|
78
|
+
scope: { tenantId: string; organizationId: string },
|
|
79
|
+
): Promise<boolean> {
|
|
80
|
+
const locked = await em.getKysely<SalesDocumentLockDatabase>()
|
|
81
|
+
.selectFrom(table)
|
|
82
|
+
.select('id')
|
|
83
|
+
.where('id', '=', id)
|
|
84
|
+
.where('tenant_id', '=', scope.tenantId)
|
|
85
|
+
.where('organization_id', '=', scope.organizationId)
|
|
86
|
+
.where('deleted_at', 'is', null)
|
|
87
|
+
.forNoKeyUpdate()
|
|
88
|
+
.executeTakeFirst()
|
|
89
|
+
return Boolean(locked)
|
|
90
|
+
}
|
|
91
|
+
|
|
52
92
|
export { cloneJson } from '../lib/json'
|
|
53
93
|
|
|
54
94
|
export function toNumericString(value: number | null | undefined): string | null {
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
ensureTenantScope,
|
|
32
32
|
extractUndoPayload,
|
|
33
33
|
enforceSalesDocumentOptimisticLock,
|
|
34
|
+
lockSalesDocumentRow,
|
|
34
35
|
SALES_RESOURCE_KIND_ORDER,
|
|
35
36
|
} from './shared'
|
|
36
37
|
import { resolveDictionaryEntryValue } from '../lib/dictionaries'
|
|
@@ -312,8 +313,16 @@ async function deleteShipmentWithItems(em: EntityManager, shipment: SalesShipmen
|
|
|
312
313
|
await em.flush()
|
|
313
314
|
}
|
|
314
315
|
|
|
316
|
+
async function lockOrderBeforeLines(em: EntityManager, order: SalesOrder): Promise<void> {
|
|
317
|
+
await lockSalesDocumentRow(em, 'sales_orders', order.id, {
|
|
318
|
+
tenantId: order.tenantId,
|
|
319
|
+
organizationId: order.organizationId,
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
|
|
315
323
|
async function recomputeFulfilledQuantities(em: EntityManager, order: SalesOrder): Promise<void> {
|
|
316
324
|
const scope = { tenantId: order.tenantId, organizationId: order.organizationId }
|
|
325
|
+
await lockOrderBeforeLines(em, order)
|
|
317
326
|
const shipments = await findWithDecryption(em, SalesShipment, { order, deletedAt: null }, {}, scope)
|
|
318
327
|
const shipmentIds = shipments.map((entry) => entry.id)
|
|
319
328
|
const shipmentItems = shipmentIds.length
|
|
@@ -386,6 +395,7 @@ async function validateShipmentItems(params: {
|
|
|
386
395
|
throw new CrudHttpError(400, { error: translate('sales.shipments.items_required', 'Add at least one line to ship.') })
|
|
387
396
|
}
|
|
388
397
|
const scope = { tenantId: order.tenantId, organizationId: order.organizationId }
|
|
398
|
+
if (lockOrderLines) await lockOrderBeforeLines(em, order)
|
|
389
399
|
const orderLines = await findWithDecryption(
|
|
390
400
|
em,
|
|
391
401
|
SalesOrderLine,
|
|
@@ -379,9 +379,13 @@ const crud = makeCrudRoute({
|
|
|
379
379
|
.filter((value) => value.length > 0)
|
|
380
380
|
}
|
|
381
381
|
if (parseBooleanFlag(query.mine) && ctx.auth) {
|
|
382
|
+
const tenantId = ctx.auth.tenantId ?? null
|
|
383
|
+
const organizationId = ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null
|
|
384
|
+
if (!tenantId || !organizationId) {
|
|
385
|
+
filters[F.id] = { $in: [IMPOSSIBLE_PROJECT_ID] }
|
|
386
|
+
return filters
|
|
387
|
+
}
|
|
382
388
|
const em = ctx.container.resolve('em') as EntityManager
|
|
383
|
-
const tenantId = ctx.auth.tenantId ?? ''
|
|
384
|
-
const organizationId = ctx.selectedOrganizationId ?? ctx.auth.orgId ?? ''
|
|
385
389
|
const assigned = await resolveCallerAssignedProjectIds(
|
|
386
390
|
em,
|
|
387
391
|
ctx.auth.sub,
|