@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
|
@@ -111,6 +111,7 @@ import {
|
|
|
111
111
|
reconcileLinePersistedTotals,
|
|
112
112
|
deriveLineNetFromGross,
|
|
113
113
|
enforceSalesDocumentOptimisticLock,
|
|
114
|
+
lockSalesDocumentRow,
|
|
114
115
|
SALES_RESOURCE_KIND_ORDER,
|
|
115
116
|
SALES_RESOURCE_KIND_QUOTE,
|
|
116
117
|
} from "./shared";
|
|
@@ -1193,19 +1194,33 @@ async function applyDocumentUpdate({
|
|
|
1193
1194
|
}
|
|
1194
1195
|
|
|
1195
1196
|
if (input.customerEntityId !== undefined) {
|
|
1196
|
-
|
|
1197
|
+
const nextCustomerEntityId = input.customerEntityId ?? null;
|
|
1198
|
+
const customerChanged =
|
|
1199
|
+
(nextCustomerEntityId?.toLowerCase() ?? null) !==
|
|
1200
|
+
(entity.customerEntityId?.toLowerCase() ?? null);
|
|
1201
|
+
const snapshotContactId =
|
|
1202
|
+
input.customerContactId !== undefined
|
|
1203
|
+
? input.customerContactId
|
|
1204
|
+
: customerChanged
|
|
1205
|
+
? null
|
|
1206
|
+
: entity.customerContactId ?? null;
|
|
1207
|
+
if (customerChanged) {
|
|
1208
|
+
entity.customerEntityId = nextCustomerEntityId;
|
|
1209
|
+
}
|
|
1197
1210
|
entity.customerSnapshot = await resolveCustomerSnapshot(
|
|
1198
1211
|
em,
|
|
1199
1212
|
organizationId,
|
|
1200
1213
|
tenantId,
|
|
1201
|
-
|
|
1202
|
-
|
|
1214
|
+
entity.customerEntityId ?? null,
|
|
1215
|
+
snapshotContactId,
|
|
1203
1216
|
);
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1217
|
+
if (customerChanged) {
|
|
1218
|
+
entity.customerContactId = input.customerContactId ?? null;
|
|
1219
|
+
entity.billingAddressId = null;
|
|
1220
|
+
entity.shippingAddressId = null;
|
|
1221
|
+
entity.billingAddressSnapshot = null;
|
|
1222
|
+
entity.shippingAddressSnapshot = null;
|
|
1223
|
+
}
|
|
1209
1224
|
}
|
|
1210
1225
|
if (input.customerContactId !== undefined) {
|
|
1211
1226
|
entity.customerContactId = input.customerContactId ?? null;
|
|
@@ -5595,6 +5610,264 @@ const deleteQuoteCommand: CommandHandler<
|
|
|
5595
5610
|
},
|
|
5596
5611
|
};
|
|
5597
5612
|
|
|
5613
|
+
type TotalsCalculatedPayload = Parameters<typeof emitTotalsCalculated>[1];
|
|
5614
|
+
type LockedGraphWrite<TResult> = {
|
|
5615
|
+
result: TResult;
|
|
5616
|
+
calculation: SalesDocumentCalculationResult;
|
|
5617
|
+
};
|
|
5618
|
+
type LockedGraphSnapshots<TSnapshot> = {
|
|
5619
|
+
before: TSnapshot | null;
|
|
5620
|
+
after: TSnapshot | null;
|
|
5621
|
+
};
|
|
5622
|
+
type LockedGraphKind<TDocument, TSnapshot> = {
|
|
5623
|
+
documentKind: SalesDocumentKind;
|
|
5624
|
+
lock: (
|
|
5625
|
+
em: EntityManager,
|
|
5626
|
+
ctx: CommandRuntimeContext,
|
|
5627
|
+
documentId: string,
|
|
5628
|
+
missingMessage: string,
|
|
5629
|
+
) => Promise<TDocument>;
|
|
5630
|
+
loadSnapshot: (em: EntityManager, documentId: string) => Promise<TSnapshot | null>;
|
|
5631
|
+
snapshots: WeakMap<object, LockedGraphSnapshots<TSnapshot>>;
|
|
5632
|
+
};
|
|
5633
|
+
|
|
5634
|
+
const lockedGraphWriteInputs = new WeakSet<object>();
|
|
5635
|
+
const lockedOrderGraphSnapshots = new WeakMap<object, LockedGraphSnapshots<OrderGraphSnapshot>>();
|
|
5636
|
+
const lockedQuoteGraphSnapshots = new WeakMap<object, LockedGraphSnapshots<QuoteGraphSnapshot>>();
|
|
5637
|
+
|
|
5638
|
+
function resolveRequestDecryptionScope(ctx: CommandRuntimeContext) {
|
|
5639
|
+
return {
|
|
5640
|
+
tenantId: ctx.auth?.tenantId ?? undefined,
|
|
5641
|
+
organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? undefined,
|
|
5642
|
+
};
|
|
5643
|
+
}
|
|
5644
|
+
|
|
5645
|
+
async function lockOrderForTotalsWrite(
|
|
5646
|
+
em: EntityManager,
|
|
5647
|
+
ctx: CommandRuntimeContext,
|
|
5648
|
+
orderId: string,
|
|
5649
|
+
missingOrderMessage: string,
|
|
5650
|
+
): Promise<SalesOrder> {
|
|
5651
|
+
const existingOrder = await findOneWithDecryption(
|
|
5652
|
+
em,
|
|
5653
|
+
SalesOrder,
|
|
5654
|
+
{ id: orderId, deletedAt: null },
|
|
5655
|
+
{},
|
|
5656
|
+
resolveRequestDecryptionScope(ctx),
|
|
5657
|
+
);
|
|
5658
|
+
if (!existingOrder) throw notFound(missingOrderMessage);
|
|
5659
|
+
ensureOrderScope(ctx, existingOrder.organizationId, existingOrder.tenantId);
|
|
5660
|
+
const scope = {
|
|
5661
|
+
tenantId: existingOrder.tenantId,
|
|
5662
|
+
organizationId: existingOrder.organizationId,
|
|
5663
|
+
};
|
|
5664
|
+
if (!(await lockSalesDocumentRow(em, "sales_orders", orderId, scope))) {
|
|
5665
|
+
throw notFound(missingOrderMessage);
|
|
5666
|
+
}
|
|
5667
|
+
const order = await findOneWithDecryption(
|
|
5668
|
+
em,
|
|
5669
|
+
SalesOrder,
|
|
5670
|
+
{ id: orderId, deletedAt: null, ...scope },
|
|
5671
|
+
{ refresh: true },
|
|
5672
|
+
scope,
|
|
5673
|
+
);
|
|
5674
|
+
if (!order) throw notFound(missingOrderMessage);
|
|
5675
|
+
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
5676
|
+
return order;
|
|
5677
|
+
}
|
|
5678
|
+
|
|
5679
|
+
async function lockQuoteForTotalsWrite(
|
|
5680
|
+
em: EntityManager,
|
|
5681
|
+
ctx: CommandRuntimeContext,
|
|
5682
|
+
quoteId: string,
|
|
5683
|
+
missingQuoteMessage: string,
|
|
5684
|
+
): Promise<SalesQuote> {
|
|
5685
|
+
const existingQuote = await findOneWithDecryption(
|
|
5686
|
+
em,
|
|
5687
|
+
SalesQuote,
|
|
5688
|
+
{ id: quoteId, deletedAt: null },
|
|
5689
|
+
{},
|
|
5690
|
+
resolveRequestDecryptionScope(ctx),
|
|
5691
|
+
);
|
|
5692
|
+
if (!existingQuote) throw notFound(missingQuoteMessage);
|
|
5693
|
+
ensureQuoteScope(ctx, existingQuote.organizationId, existingQuote.tenantId);
|
|
5694
|
+
const scope = {
|
|
5695
|
+
tenantId: existingQuote.tenantId,
|
|
5696
|
+
organizationId: existingQuote.organizationId,
|
|
5697
|
+
};
|
|
5698
|
+
if (!(await lockSalesDocumentRow(em, "sales_quotes", quoteId, scope))) {
|
|
5699
|
+
throw notFound(missingQuoteMessage);
|
|
5700
|
+
}
|
|
5701
|
+
const quote = await findOneWithDecryption(
|
|
5702
|
+
em,
|
|
5703
|
+
SalesQuote,
|
|
5704
|
+
{ id: quoteId, deletedAt: null, ...scope },
|
|
5705
|
+
{ refresh: true },
|
|
5706
|
+
scope,
|
|
5707
|
+
);
|
|
5708
|
+
if (!quote) throw notFound(missingQuoteMessage);
|
|
5709
|
+
await enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
5710
|
+
return quote;
|
|
5711
|
+
}
|
|
5712
|
+
|
|
5713
|
+
function buildTotalsCalculatedPayload(
|
|
5714
|
+
documentKind: SalesDocumentKind,
|
|
5715
|
+
document: SalesOrder | SalesQuote,
|
|
5716
|
+
calculation: SalesDocumentCalculationResult,
|
|
5717
|
+
): TotalsCalculatedPayload {
|
|
5718
|
+
return {
|
|
5719
|
+
documentKind,
|
|
5720
|
+
documentId: document.id,
|
|
5721
|
+
organizationId: document.organizationId,
|
|
5722
|
+
tenantId: document.tenantId,
|
|
5723
|
+
customerId: document.customerEntityId ?? null,
|
|
5724
|
+
totals: calculation.totals,
|
|
5725
|
+
lineCount: calculation.lines.length,
|
|
5726
|
+
};
|
|
5727
|
+
}
|
|
5728
|
+
|
|
5729
|
+
async function emitCommittedTotalsCalculated(
|
|
5730
|
+
ctx: CommandRuntimeContext,
|
|
5731
|
+
payload: TotalsCalculatedPayload | null,
|
|
5732
|
+
): Promise<void> {
|
|
5733
|
+
if (!payload) return;
|
|
5734
|
+
let eventBus: EventBus | null = null;
|
|
5735
|
+
try {
|
|
5736
|
+
eventBus = ctx.container.resolve("eventBus") as EventBus;
|
|
5737
|
+
} catch {
|
|
5738
|
+
eventBus = null;
|
|
5739
|
+
}
|
|
5740
|
+
await emitTotalsCalculated(eventBus, payload);
|
|
5741
|
+
}
|
|
5742
|
+
|
|
5743
|
+
const lockedOrderGraphKind: LockedGraphKind<SalesOrder, OrderGraphSnapshot> = {
|
|
5744
|
+
documentKind: "order",
|
|
5745
|
+
lock: lockOrderForTotalsWrite,
|
|
5746
|
+
loadSnapshot: loadOrderSnapshot,
|
|
5747
|
+
snapshots: lockedOrderGraphSnapshots,
|
|
5748
|
+
};
|
|
5749
|
+
|
|
5750
|
+
const lockedQuoteGraphKind: LockedGraphKind<SalesQuote, QuoteGraphSnapshot> = {
|
|
5751
|
+
documentKind: "quote",
|
|
5752
|
+
lock: lockQuoteForTotalsWrite,
|
|
5753
|
+
loadSnapshot: loadQuoteSnapshot,
|
|
5754
|
+
snapshots: lockedQuoteGraphSnapshots,
|
|
5755
|
+
};
|
|
5756
|
+
|
|
5757
|
+
function markLockedGraphWrite(input: object): Record<string, never> {
|
|
5758
|
+
lockedGraphWriteInputs.add(input);
|
|
5759
|
+
return {};
|
|
5760
|
+
}
|
|
5761
|
+
|
|
5762
|
+
async function writeLockedDocumentGraph<
|
|
5763
|
+
TDocument extends SalesOrder | SalesQuote,
|
|
5764
|
+
TSnapshot,
|
|
5765
|
+
TResult extends object,
|
|
5766
|
+
>(
|
|
5767
|
+
kind: LockedGraphKind<TDocument, TSnapshot>,
|
|
5768
|
+
input: object,
|
|
5769
|
+
ctx: CommandRuntimeContext,
|
|
5770
|
+
documentId: string,
|
|
5771
|
+
missingMessage: string,
|
|
5772
|
+
write: (em: EntityManager, document: TDocument) => Promise<LockedGraphWrite<TResult>>,
|
|
5773
|
+
): Promise<TResult> {
|
|
5774
|
+
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
5775
|
+
const captureSnapshots = lockedGraphWriteInputs.has(input);
|
|
5776
|
+
let written!: LockedGraphWrite<TResult>;
|
|
5777
|
+
let totalsEvent!: TotalsCalculatedPayload;
|
|
5778
|
+
let before: TSnapshot | null = null;
|
|
5779
|
+
let after: TSnapshot | null = null;
|
|
5780
|
+
await withAtomicFlush(
|
|
5781
|
+
em,
|
|
5782
|
+
[
|
|
5783
|
+
async () => {
|
|
5784
|
+
const document = await kind.lock(em, ctx, documentId, missingMessage);
|
|
5785
|
+
if (captureSnapshots) before = await kind.loadSnapshot(em, document.id);
|
|
5786
|
+
written = await write(em, document);
|
|
5787
|
+
totalsEvent = buildTotalsCalculatedPayload(
|
|
5788
|
+
kind.documentKind,
|
|
5789
|
+
document,
|
|
5790
|
+
written.calculation,
|
|
5791
|
+
);
|
|
5792
|
+
},
|
|
5793
|
+
...(captureSnapshots
|
|
5794
|
+
? [
|
|
5795
|
+
async () => {
|
|
5796
|
+
em.clear();
|
|
5797
|
+
after = await kind.loadSnapshot(em, documentId);
|
|
5798
|
+
},
|
|
5799
|
+
]
|
|
5800
|
+
: []),
|
|
5801
|
+
],
|
|
5802
|
+
{ transaction: true },
|
|
5803
|
+
);
|
|
5804
|
+
if (captureSnapshots) kind.snapshots.set(written.result, { before, after });
|
|
5805
|
+
await emitCommittedTotalsCalculated(ctx, totalsEvent);
|
|
5806
|
+
return written.result;
|
|
5807
|
+
}
|
|
5808
|
+
|
|
5809
|
+
function writeLockedOrderGraph<TResult extends object>(
|
|
5810
|
+
input: object,
|
|
5811
|
+
ctx: CommandRuntimeContext,
|
|
5812
|
+
orderId: string,
|
|
5813
|
+
missingOrderMessage: string,
|
|
5814
|
+
write: (em: EntityManager, order: SalesOrder) => Promise<LockedGraphWrite<TResult>>,
|
|
5815
|
+
): Promise<TResult> {
|
|
5816
|
+
return writeLockedDocumentGraph(lockedOrderGraphKind, input, ctx, orderId, missingOrderMessage, write);
|
|
5817
|
+
}
|
|
5818
|
+
|
|
5819
|
+
function writeLockedQuoteGraph<TResult extends object>(
|
|
5820
|
+
input: object,
|
|
5821
|
+
ctx: CommandRuntimeContext,
|
|
5822
|
+
quoteId: string,
|
|
5823
|
+
missingQuoteMessage: string,
|
|
5824
|
+
write: (em: EntityManager, quote: SalesQuote) => Promise<LockedGraphWrite<TResult>>,
|
|
5825
|
+
): Promise<TResult> {
|
|
5826
|
+
return writeLockedDocumentGraph(lockedQuoteGraphKind, input, ctx, quoteId, missingQuoteMessage, write);
|
|
5827
|
+
}
|
|
5828
|
+
|
|
5829
|
+
function resolveLockedOrderGraphBefore(
|
|
5830
|
+
result: object,
|
|
5831
|
+
preparedBefore: unknown,
|
|
5832
|
+
): OrderGraphSnapshot | undefined {
|
|
5833
|
+
return (
|
|
5834
|
+
lockedOrderGraphSnapshots.get(result)?.before ??
|
|
5835
|
+
(preparedBefore as OrderGraphSnapshot | undefined)
|
|
5836
|
+
);
|
|
5837
|
+
}
|
|
5838
|
+
|
|
5839
|
+
function resolveLockedQuoteGraphBefore(
|
|
5840
|
+
result: object,
|
|
5841
|
+
preparedBefore: unknown,
|
|
5842
|
+
): QuoteGraphSnapshot | undefined {
|
|
5843
|
+
return (
|
|
5844
|
+
lockedQuoteGraphSnapshots.get(result)?.before ??
|
|
5845
|
+
(preparedBefore as QuoteGraphSnapshot | undefined)
|
|
5846
|
+
);
|
|
5847
|
+
}
|
|
5848
|
+
|
|
5849
|
+
async function captureLockedOrderGraphAfter(
|
|
5850
|
+
ctx: CommandRuntimeContext,
|
|
5851
|
+
result: object,
|
|
5852
|
+
orderId: string,
|
|
5853
|
+
): Promise<OrderGraphSnapshot | null> {
|
|
5854
|
+
const locked = lockedOrderGraphSnapshots.get(result);
|
|
5855
|
+
if (locked) return locked.after;
|
|
5856
|
+
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
5857
|
+
return loadOrderSnapshot(em, orderId);
|
|
5858
|
+
}
|
|
5859
|
+
|
|
5860
|
+
async function captureLockedQuoteGraphAfter(
|
|
5861
|
+
ctx: CommandRuntimeContext,
|
|
5862
|
+
result: object,
|
|
5863
|
+
quoteId: string,
|
|
5864
|
+
): Promise<QuoteGraphSnapshot | null> {
|
|
5865
|
+
const locked = lockedQuoteGraphSnapshots.get(result);
|
|
5866
|
+
if (locked) return locked.after;
|
|
5867
|
+
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
5868
|
+
return loadQuoteSnapshot(em, quoteId);
|
|
5869
|
+
}
|
|
5870
|
+
|
|
5598
5871
|
const updateQuoteCommand: CommandHandler<
|
|
5599
5872
|
DocumentUpdateInput,
|
|
5600
5873
|
{ quote: SalesQuote }
|
|
@@ -5618,15 +5891,8 @@ const updateQuoteCommand: CommandHandler<
|
|
|
5618
5891
|
await assertAmountsModeUnsupportedOnQuote([parsed]);
|
|
5619
5892
|
documentEditSchema.parse(rawInput ?? {});
|
|
5620
5893
|
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
deletedAt: null,
|
|
5624
|
-
});
|
|
5625
|
-
if (!quote)
|
|
5626
|
-
throw notFound("Sales quote not found");
|
|
5627
|
-
ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
|
|
5628
|
-
await enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
5629
|
-
const shouldInvalidateSentToken = (quote.status ?? null) === "sent";
|
|
5894
|
+
let quote!: SalesQuote;
|
|
5895
|
+
let totalsEvent: TotalsCalculatedPayload | null = null;
|
|
5630
5896
|
const shouldRecalculateTotals =
|
|
5631
5897
|
parsed.shippingMethodId !== undefined ||
|
|
5632
5898
|
parsed.shippingMethodSnapshot !== undefined ||
|
|
@@ -5643,6 +5909,8 @@ const updateQuoteCommand: CommandHandler<
|
|
|
5643
5909
|
em,
|
|
5644
5910
|
[
|
|
5645
5911
|
async () => {
|
|
5912
|
+
quote = await lockQuoteForTotalsWrite(em, ctx, parsed.id, "Sales quote not found");
|
|
5913
|
+
const shouldInvalidateSentToken = (quote.status ?? null) === "sent";
|
|
5646
5914
|
await applyDocumentUpdate({
|
|
5647
5915
|
kind: "quote",
|
|
5648
5916
|
entity: quote,
|
|
@@ -5745,27 +6013,14 @@ const updateQuoteCommand: CommandHandler<
|
|
|
5745
6013
|
adjustmentInputs,
|
|
5746
6014
|
);
|
|
5747
6015
|
applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
|
|
5748
|
-
|
|
5749
|
-
try {
|
|
5750
|
-
eventBus = ctx.container.resolve("eventBus") as EventBus;
|
|
5751
|
-
} catch {
|
|
5752
|
-
eventBus = null;
|
|
5753
|
-
}
|
|
5754
|
-
await emitTotalsCalculated(eventBus, {
|
|
5755
|
-
documentKind: "quote",
|
|
5756
|
-
documentId: quote.id,
|
|
5757
|
-
organizationId: quote.organizationId,
|
|
5758
|
-
tenantId: quote.tenantId,
|
|
5759
|
-
customerId: quote.customerEntityId ?? null,
|
|
5760
|
-
totals: calculation.totals,
|
|
5761
|
-
lineCount: calculation.lines.length,
|
|
5762
|
-
});
|
|
6016
|
+
totalsEvent = buildTotalsCalculatedPayload("quote", quote, calculation);
|
|
5763
6017
|
}
|
|
5764
6018
|
quote.updatedAt = new Date();
|
|
5765
6019
|
},
|
|
5766
6020
|
],
|
|
5767
6021
|
{ transaction: true },
|
|
5768
6022
|
);
|
|
6023
|
+
await emitCommittedTotalsCalculated(ctx, totalsEvent);
|
|
5769
6024
|
// Same as sales.orders.update (#6217): refresh the query-index projection so
|
|
5770
6025
|
// customFields written in applyDocumentUpdate are visible on the next list GET.
|
|
5771
6026
|
const dataEngine = ctx.container.resolve("dataEngine") as DataEngine;
|
|
@@ -5907,44 +6162,14 @@ const updateOrderCommand: CommandHandler<
|
|
|
5907
6162
|
async execute(rawInput, ctx) {
|
|
5908
6163
|
const parsed = documentUpdateSchema.parse(rawInput ?? {});
|
|
5909
6164
|
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
ensureOrderScope(ctx, order.organizationId, order.tenantId);
|
|
5917
|
-
const currentTotalsMode: SalesAmountsMode = order.totalsMode ?? "computed";
|
|
5918
|
-
const nextTotalsMode: SalesAmountsMode = parsed.totalsMode ?? currentTotalsMode;
|
|
5919
|
-
const totalsModeChanged = nextTotalsMode !== currentTotalsMode;
|
|
5920
|
-
const externalHeaderSupplied =
|
|
5921
|
-
isExternalAmountsMode(nextTotalsMode) && hasAnySuppliedHeaderTotal(parsed);
|
|
5922
|
-
if (!totalsModeChanged && !externalHeaderSupplied) {
|
|
5923
|
-
documentEditSchema.parse(rawInput ?? {});
|
|
5924
|
-
}
|
|
5925
|
-
if (isExternalAmountsMode(nextTotalsMode) && (totalsModeChanged || externalHeaderSupplied)) {
|
|
5926
|
-
await requireOrderTotalsForExternalWrite(parsed);
|
|
5927
|
-
}
|
|
5928
|
-
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
5929
|
-
const previousStatus = normalizeStatusValue(order.status);
|
|
5930
|
-
if (parsed.statusEntryId && !isCancelledOrderStatus(previousStatus)) {
|
|
5931
|
-
const requestedStatus = normalizeStatusValue(
|
|
5932
|
-
await resolveDictionaryEntryValue(em, parsed.statusEntryId, {
|
|
5933
|
-
tenantId: order.tenantId,
|
|
5934
|
-
}),
|
|
5935
|
-
);
|
|
5936
|
-
if (isCancelledOrderStatus(requestedStatus)) {
|
|
5937
|
-
await assertOrderCanBeCancelled(em, order);
|
|
5938
|
-
}
|
|
5939
|
-
}
|
|
6165
|
+
let order!: SalesOrder;
|
|
6166
|
+
let previousStatus: string | null = null;
|
|
6167
|
+
let nextTotalsMode: SalesAmountsMode = "computed";
|
|
6168
|
+
let totalsModeChanged = false;
|
|
6169
|
+
let externalHeaderSupplied = false;
|
|
6170
|
+
let suppliedTotals: Partial<SalesDocumentAmounts> | null = null;
|
|
5940
6171
|
let statusChangeNote: SalesNote | null = null;
|
|
5941
|
-
|
|
5942
|
-
nextTotalsMode,
|
|
5943
|
-
)
|
|
5944
|
-
? externalHeaderSupplied
|
|
5945
|
-
? buildExternalHeaderTotals(parsed)
|
|
5946
|
-
: readPersistedHeaderTotals(order)
|
|
5947
|
-
: null;
|
|
6172
|
+
let totalsEvent: TotalsCalculatedPayload | null = null;
|
|
5948
6173
|
const shouldRecalculateTotals =
|
|
5949
6174
|
parsed.shippingMethodId !== undefined ||
|
|
5950
6175
|
parsed.shippingMethodSnapshot !== undefined ||
|
|
@@ -5952,9 +6177,7 @@ const updateOrderCommand: CommandHandler<
|
|
|
5952
6177
|
parsed.paymentMethodId !== undefined ||
|
|
5953
6178
|
parsed.paymentMethodSnapshot !== undefined ||
|
|
5954
6179
|
parsed.paymentMethodCode !== undefined ||
|
|
5955
|
-
parsed.currencyCode !== undefined
|
|
5956
|
-
totalsModeChanged ||
|
|
5957
|
-
externalHeaderSupplied;
|
|
6180
|
+
parsed.currencyCode !== undefined;
|
|
5958
6181
|
// Apply the scalar update, totals recalc, and status-change note atomically.
|
|
5959
6182
|
// applyDocumentUpdate/replace*/setRecordCustomFields flush mid-build, so
|
|
5960
6183
|
// without a transaction a later failure would leave a half-updated order
|
|
@@ -5963,6 +6186,34 @@ const updateOrderCommand: CommandHandler<
|
|
|
5963
6186
|
em,
|
|
5964
6187
|
[
|
|
5965
6188
|
async () => {
|
|
6189
|
+
order = await lockOrderForTotalsWrite(em, ctx, parsed.id, "Sales order not found");
|
|
6190
|
+
const currentTotalsMode: SalesAmountsMode = order.totalsMode ?? "computed";
|
|
6191
|
+
nextTotalsMode = parsed.totalsMode ?? currentTotalsMode;
|
|
6192
|
+
totalsModeChanged = nextTotalsMode !== currentTotalsMode;
|
|
6193
|
+
externalHeaderSupplied =
|
|
6194
|
+
isExternalAmountsMode(nextTotalsMode) && hasAnySuppliedHeaderTotal(parsed);
|
|
6195
|
+
if (!totalsModeChanged && !externalHeaderSupplied) {
|
|
6196
|
+
documentEditSchema.parse(rawInput ?? {});
|
|
6197
|
+
}
|
|
6198
|
+
if (isExternalAmountsMode(nextTotalsMode) && (totalsModeChanged || externalHeaderSupplied)) {
|
|
6199
|
+
await requireOrderTotalsForExternalWrite(parsed);
|
|
6200
|
+
}
|
|
6201
|
+
suppliedTotals = isExternalAmountsMode(nextTotalsMode)
|
|
6202
|
+
? externalHeaderSupplied
|
|
6203
|
+
? buildExternalHeaderTotals(parsed)
|
|
6204
|
+
: readPersistedHeaderTotals(order)
|
|
6205
|
+
: null;
|
|
6206
|
+
previousStatus = normalizeStatusValue(order.status);
|
|
6207
|
+
if (parsed.statusEntryId && !isCancelledOrderStatus(previousStatus)) {
|
|
6208
|
+
const requestedStatus = normalizeStatusValue(
|
|
6209
|
+
await resolveDictionaryEntryValue(em, parsed.statusEntryId, {
|
|
6210
|
+
tenantId: order.tenantId,
|
|
6211
|
+
}),
|
|
6212
|
+
);
|
|
6213
|
+
if (isCancelledOrderStatus(requestedStatus)) {
|
|
6214
|
+
await assertOrderCanBeCancelled(em, order);
|
|
6215
|
+
}
|
|
6216
|
+
}
|
|
5966
6217
|
await applyDocumentUpdate({
|
|
5967
6218
|
kind: "order",
|
|
5968
6219
|
entity: order,
|
|
@@ -5977,7 +6228,7 @@ const updateOrderCommand: CommandHandler<
|
|
|
5977
6228
|
// `em.find` lines/adjustments or appendOrderStatusChangeNote queries
|
|
5978
6229
|
// reset the changeset (see SPEC-018).
|
|
5979
6230
|
async () => {
|
|
5980
|
-
if (shouldRecalculateTotals) {
|
|
6231
|
+
if (shouldRecalculateTotals || totalsModeChanged || externalHeaderSupplied) {
|
|
5981
6232
|
const [existingLines, adjustments] = await Promise.all([
|
|
5982
6233
|
em.find(
|
|
5983
6234
|
SalesOrderLine,
|
|
@@ -6072,21 +6323,7 @@ const updateOrderCommand: CommandHandler<
|
|
|
6072
6323
|
order.totalsMode = nextTotalsMode;
|
|
6073
6324
|
}
|
|
6074
6325
|
applyOrderTotals(order, calculation.totals, calculation.lines.length);
|
|
6075
|
-
|
|
6076
|
-
try {
|
|
6077
|
-
eventBus = ctx.container.resolve("eventBus") as EventBus;
|
|
6078
|
-
} catch {
|
|
6079
|
-
eventBus = null;
|
|
6080
|
-
}
|
|
6081
|
-
await emitTotalsCalculated(eventBus, {
|
|
6082
|
-
documentKind: "order",
|
|
6083
|
-
documentId: order.id,
|
|
6084
|
-
organizationId: order.organizationId,
|
|
6085
|
-
tenantId: order.tenantId,
|
|
6086
|
-
customerId: order.customerEntityId ?? null,
|
|
6087
|
-
totals: calculation.totals,
|
|
6088
|
-
lineCount: calculation.lines.length,
|
|
6089
|
-
});
|
|
6326
|
+
totalsEvent = buildTotalsCalculatedPayload("order", order, calculation);
|
|
6090
6327
|
}
|
|
6091
6328
|
statusChangeNote = await appendOrderStatusChangeNote({
|
|
6092
6329
|
em,
|
|
@@ -6099,6 +6336,7 @@ const updateOrderCommand: CommandHandler<
|
|
|
6099
6336
|
],
|
|
6100
6337
|
{ transaction: true },
|
|
6101
6338
|
);
|
|
6339
|
+
await emitCommittedTotalsCalculated(ctx, totalsEvent);
|
|
6102
6340
|
emitOrderLifecycleEventsForTransition({ order, previousStatus });
|
|
6103
6341
|
// Refresh the query-index projection (including customValues). Create already
|
|
6104
6342
|
// did this; update used to only invalidate the HTTP CRUD cache, so a PUT that
|
|
@@ -7610,139 +7848,98 @@ const orderLineUpsertCommand: CommandHandler<
|
|
|
7610
7848
|
{ orderId: string; lineId: string }
|
|
7611
7849
|
> = {
|
|
7612
7850
|
id: "sales.orders.lines.upsert",
|
|
7613
|
-
async prepare(input
|
|
7614
|
-
|
|
7615
|
-
const orderId = typeof raw.orderId === "string" ? raw.orderId : null;
|
|
7616
|
-
if (!orderId) return {};
|
|
7617
|
-
const em = ctx.container.resolve("em") as EntityManager;
|
|
7618
|
-
const snapshot = await loadOrderSnapshot(em, orderId);
|
|
7619
|
-
if (snapshot)
|
|
7620
|
-
ensureOrderScope(
|
|
7621
|
-
ctx,
|
|
7622
|
-
snapshot.order.organizationId,
|
|
7623
|
-
snapshot.order.tenantId,
|
|
7624
|
-
);
|
|
7625
|
-
return snapshot ? { before: snapshot } : {};
|
|
7851
|
+
async prepare(input) {
|
|
7852
|
+
return markLockedGraphWrite(input);
|
|
7626
7853
|
},
|
|
7627
7854
|
async execute(input, ctx) {
|
|
7628
7855
|
const rawBody = (input?.body as Record<string, unknown> | undefined) ?? {};
|
|
7629
7856
|
const parsed = orderLineUpsertSchema.parse(rawBody);
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
]);
|
|
7648
|
-
const lineSnapshots = existingLines.map(mapOrderLineEntityToSnapshot);
|
|
7649
|
-
const existingSnapshot = parsed.id
|
|
7650
|
-
? (lineSnapshots.find((line) => line.id === parsed.id) ?? null)
|
|
7651
|
-
: null;
|
|
7652
|
-
const totalsMode: SalesAmountsMode = order.totalsMode ?? "computed";
|
|
7653
|
-
await assertUniformAmountsMode(totalsMode, [parsed]);
|
|
7654
|
-
if (isExternalAmountsMode(totalsMode)) {
|
|
7655
|
-
await requireOrderTotalsForExternalWrite(parsed.orderTotals);
|
|
7656
|
-
}
|
|
7657
|
-
const suppliedTotals: Partial<SalesDocumentAmounts> | null = isExternalAmountsMode(totalsMode)
|
|
7658
|
-
? buildExternalHeaderTotals(parsed.orderTotals)
|
|
7659
|
-
: null;
|
|
7660
|
-
await assertOrderAcceptsNewLine(order, existingSnapshot);
|
|
7661
|
-
await assertShippedOrderLineEditable(em, order, existingSnapshot, parsed);
|
|
7662
|
-
const priceMode =
|
|
7663
|
-
parsed.priceMode === "gross"
|
|
7664
|
-
? "gross"
|
|
7665
|
-
: parsed.priceMode === "net"
|
|
7666
|
-
? "net"
|
|
7667
|
-
: null;
|
|
7668
|
-
let unitPriceNet =
|
|
7669
|
-
parsed.unitPriceNet ?? existingSnapshot?.unitPriceNet ?? null;
|
|
7670
|
-
let unitPriceGross =
|
|
7671
|
-
parsed.unitPriceGross ?? existingSnapshot?.unitPriceGross ?? null;
|
|
7672
|
-
let taxRate = parsed.taxRate ?? existingSnapshot?.taxRate ?? null;
|
|
7673
|
-
const unitPriceSupplied = unitPriceNet !== null || unitPriceGross !== null;
|
|
7674
|
-
if (priceMode && (unitPriceNet === null || unitPriceGross === null)) {
|
|
7675
|
-
let taxService: TaxCalculationService | null = null;
|
|
7676
|
-
try {
|
|
7677
|
-
taxService = ctx.container.resolve(
|
|
7678
|
-
"taxCalculationService",
|
|
7679
|
-
) as TaxCalculationService;
|
|
7680
|
-
} catch {
|
|
7681
|
-
taxService = null;
|
|
7857
|
+
return writeLockedOrderGraph(input, ctx, parsed.orderId, "Sales order not found", async (em, order) => {
|
|
7858
|
+
const [existingLines, adjustments] = await Promise.all([
|
|
7859
|
+
em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
|
|
7860
|
+
em.find(
|
|
7861
|
+
SalesOrderAdjustment,
|
|
7862
|
+
{ order },
|
|
7863
|
+
{ orderBy: { position: "asc" } },
|
|
7864
|
+
),
|
|
7865
|
+
]);
|
|
7866
|
+
const lineSnapshots = existingLines.map(mapOrderLineEntityToSnapshot);
|
|
7867
|
+
const existingSnapshot = parsed.id
|
|
7868
|
+
? (lineSnapshots.find((line) => line.id === parsed.id) ?? null)
|
|
7869
|
+
: null;
|
|
7870
|
+
const totalsMode: SalesAmountsMode = order.totalsMode ?? "computed";
|
|
7871
|
+
await assertUniformAmountsMode(totalsMode, [parsed]);
|
|
7872
|
+
if (isExternalAmountsMode(totalsMode)) {
|
|
7873
|
+
await requireOrderTotalsForExternalWrite(parsed.orderTotals);
|
|
7682
7874
|
}
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
unitPriceNet
|
|
7696
|
-
|
|
7697
|
-
|
|
7875
|
+
const suppliedTotals: Partial<SalesDocumentAmounts> | null = isExternalAmountsMode(totalsMode)
|
|
7876
|
+
? buildExternalHeaderTotals(parsed.orderTotals)
|
|
7877
|
+
: null;
|
|
7878
|
+
await assertOrderAcceptsNewLine(order, existingSnapshot);
|
|
7879
|
+
await assertShippedOrderLineEditable(em, order, existingSnapshot, parsed);
|
|
7880
|
+
const priceMode =
|
|
7881
|
+
parsed.priceMode === "gross"
|
|
7882
|
+
? "gross"
|
|
7883
|
+
: parsed.priceMode === "net"
|
|
7884
|
+
? "net"
|
|
7885
|
+
: null;
|
|
7886
|
+
let unitPriceNet =
|
|
7887
|
+
parsed.unitPriceNet ?? existingSnapshot?.unitPriceNet ?? null;
|
|
7888
|
+
let unitPriceGross =
|
|
7889
|
+
parsed.unitPriceGross ?? existingSnapshot?.unitPriceGross ?? null;
|
|
7890
|
+
let taxRate = parsed.taxRate ?? existingSnapshot?.taxRate ?? null;
|
|
7891
|
+
const unitPriceSupplied = unitPriceNet !== null || unitPriceGross !== null;
|
|
7892
|
+
if (priceMode && (unitPriceNet === null || unitPriceGross === null)) {
|
|
7893
|
+
let taxService: TaxCalculationService | null = null;
|
|
7894
|
+
try {
|
|
7895
|
+
taxService = ctx.container.resolve(
|
|
7896
|
+
"taxCalculationService",
|
|
7897
|
+
) as TaxCalculationService;
|
|
7898
|
+
} catch {
|
|
7899
|
+
taxService = null;
|
|
7900
|
+
}
|
|
7901
|
+
if (taxService) {
|
|
7902
|
+
const taxResult = await taxService.calculateUnitAmounts({
|
|
7903
|
+
amount:
|
|
7904
|
+
priceMode === "gross"
|
|
7905
|
+
? (unitPriceGross ?? unitPriceNet ?? 0)
|
|
7906
|
+
: (unitPriceNet ?? unitPriceGross ?? 0),
|
|
7907
|
+
mode: priceMode,
|
|
7908
|
+
organizationId: parsed.organizationId,
|
|
7909
|
+
tenantId: parsed.tenantId,
|
|
7910
|
+
taxRateId: parsed.taxRateId ?? undefined,
|
|
7911
|
+
taxRate: taxRate ?? undefined,
|
|
7912
|
+
});
|
|
7913
|
+
unitPriceNet = unitPriceNet ?? taxResult.netAmount;
|
|
7914
|
+
unitPriceGross = unitPriceGross ?? taxResult.grossAmount;
|
|
7915
|
+
taxRate = taxResult.taxRate ?? taxRate;
|
|
7916
|
+
}
|
|
7698
7917
|
}
|
|
7699
|
-
}
|
|
7700
7918
|
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
em,
|
|
7726
|
-
resolver: uomResolver,
|
|
7727
|
-
organizationId: order.organizationId,
|
|
7728
|
-
tenantId: order.tenantId,
|
|
7729
|
-
line: {
|
|
7730
|
-
...lineUomInput,
|
|
7731
|
-
unitPriceNet: unitPriceNet ?? 0,
|
|
7732
|
-
unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0,
|
|
7733
|
-
},
|
|
7734
|
-
});
|
|
7735
|
-
const convertedPrices = convertLineUnitPricesOnUnitChange({
|
|
7736
|
-
existingSnapshot,
|
|
7737
|
-
nextQuantityUnit: normalizedUom.quantityUnit,
|
|
7738
|
-
nextUomSnapshot: normalizedUom.uomSnapshot,
|
|
7739
|
-
unitPriceNet,
|
|
7740
|
-
unitPriceGross,
|
|
7741
|
-
});
|
|
7742
|
-
if (convertedPrices.didConvert) {
|
|
7743
|
-
unitPriceNet = convertedPrices.unitPriceNet;
|
|
7744
|
-
unitPriceGross = convertedPrices.unitPriceGross;
|
|
7745
|
-
normalizedUom = await normalizeLineUom({
|
|
7919
|
+
const metadata =
|
|
7920
|
+
typeof parsed.metadata === "object" && parsed.metadata
|
|
7921
|
+
? { ...parsed.metadata }
|
|
7922
|
+
: existingSnapshot?.metadata
|
|
7923
|
+
? cloneJson(existingSnapshot.metadata)
|
|
7924
|
+
: {};
|
|
7925
|
+
if (parsed.priceId) metadata.priceId = parsed.priceId;
|
|
7926
|
+
if (priceMode) metadata.priceMode = priceMode;
|
|
7927
|
+
|
|
7928
|
+
const statusEntryId =
|
|
7929
|
+
parsed.statusEntryId ?? (existingSnapshot as any)?.statusEntryId ?? null;
|
|
7930
|
+
const lineId = parsed.id ?? existingSnapshot?.id ?? randomUUID();
|
|
7931
|
+
const lineUomInput = {
|
|
7932
|
+
productId: parsed.productId ?? existingSnapshot?.productId ?? null,
|
|
7933
|
+
productVariantId:
|
|
7934
|
+
parsed.productVariantId ?? existingSnapshot?.productVariantId ?? null,
|
|
7935
|
+
quantity: parsed.quantity ?? existingSnapshot?.quantity ?? 0,
|
|
7936
|
+
quantityUnit: parsed.quantityUnit ?? existingSnapshot?.quantityUnit ?? null,
|
|
7937
|
+
normalizedQuantity: existingSnapshot?.normalizedQuantity ?? null,
|
|
7938
|
+
normalizedUnit: existingSnapshot?.normalizedUnit ?? null,
|
|
7939
|
+
uomSnapshot: existingSnapshot?.uomSnapshot ?? null,
|
|
7940
|
+
};
|
|
7941
|
+
const uomResolver = createUomResolver();
|
|
7942
|
+
let normalizedUom = await normalizeLineUom({
|
|
7746
7943
|
em,
|
|
7747
7944
|
resolver: uomResolver,
|
|
7748
7945
|
organizationId: order.organizationId,
|
|
@@ -7753,174 +7950,169 @@ const orderLineUpsertCommand: CommandHandler<
|
|
|
7753
7950
|
unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0,
|
|
7754
7951
|
},
|
|
7755
7952
|
});
|
|
7756
|
-
|
|
7757
|
-
const updatedSnapshot: SalesLineSnapshot & {
|
|
7758
|
-
statusEntryId?: string | null;
|
|
7759
|
-
catalogSnapshot?: Record<string, unknown> | null;
|
|
7760
|
-
promotionSnapshot?: Record<string, unknown> | null;
|
|
7761
|
-
} = {
|
|
7762
|
-
id: lineId,
|
|
7763
|
-
lineNumber:
|
|
7764
|
-
parsed.lineNumber ??
|
|
7765
|
-
existingSnapshot?.lineNumber ??
|
|
7766
|
-
lineSnapshots.length + 1,
|
|
7767
|
-
kind: parsed.kind ?? existingSnapshot?.kind ?? "product",
|
|
7768
|
-
productId: parsed.productId ?? existingSnapshot?.productId ?? null,
|
|
7769
|
-
productVariantId:
|
|
7770
|
-
parsed.productVariantId ?? existingSnapshot?.productVariantId ?? null,
|
|
7771
|
-
name: parsed.name ?? existingSnapshot?.name ?? null,
|
|
7772
|
-
description: parsed.description ?? existingSnapshot?.description ?? null,
|
|
7773
|
-
comment: parsed.comment ?? existingSnapshot?.comment ?? null,
|
|
7774
|
-
quantity: normalizedUom.quantity,
|
|
7775
|
-
quantityUnit: normalizedUom.quantityUnit,
|
|
7776
|
-
normalizedQuantity: normalizedUom.normalizedQuantity,
|
|
7777
|
-
normalizedUnit: normalizedUom.normalizedUnit,
|
|
7778
|
-
uomSnapshot: normalizedUom.uomSnapshot
|
|
7779
|
-
? cloneJson(normalizedUom.uomSnapshot)
|
|
7780
|
-
: null,
|
|
7781
|
-
currencyCode:
|
|
7782
|
-
parsed.currencyCode ??
|
|
7783
|
-
existingSnapshot?.currencyCode ??
|
|
7784
|
-
order.currencyCode,
|
|
7785
|
-
unitPriceNet: unitPriceNet ?? 0,
|
|
7786
|
-
unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0,
|
|
7787
|
-
...resolveUpsertDiscountFields(
|
|
7788
|
-
parsed.discountAmount,
|
|
7789
|
-
parsed.discountAmountBasis,
|
|
7953
|
+
const convertedPrices = convertLineUnitPricesOnUnitChange({
|
|
7790
7954
|
existingSnapshot,
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
|
|
7819
|
-
|
|
7820
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7955
|
+
nextQuantityUnit: normalizedUom.quantityUnit,
|
|
7956
|
+
nextUomSnapshot: normalizedUom.uomSnapshot,
|
|
7957
|
+
unitPriceNet,
|
|
7958
|
+
unitPriceGross,
|
|
7959
|
+
});
|
|
7960
|
+
if (convertedPrices.didConvert) {
|
|
7961
|
+
unitPriceNet = convertedPrices.unitPriceNet;
|
|
7962
|
+
unitPriceGross = convertedPrices.unitPriceGross;
|
|
7963
|
+
normalizedUom = await normalizeLineUom({
|
|
7964
|
+
em,
|
|
7965
|
+
resolver: uomResolver,
|
|
7966
|
+
organizationId: order.organizationId,
|
|
7967
|
+
tenantId: order.tenantId,
|
|
7968
|
+
line: {
|
|
7969
|
+
...lineUomInput,
|
|
7970
|
+
unitPriceNet: unitPriceNet ?? 0,
|
|
7971
|
+
unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0,
|
|
7972
|
+
},
|
|
7973
|
+
});
|
|
7974
|
+
}
|
|
7975
|
+
const updatedSnapshot: SalesLineSnapshot & {
|
|
7976
|
+
statusEntryId?: string | null;
|
|
7977
|
+
catalogSnapshot?: Record<string, unknown> | null;
|
|
7978
|
+
promotionSnapshot?: Record<string, unknown> | null;
|
|
7979
|
+
} = {
|
|
7980
|
+
id: lineId,
|
|
7981
|
+
lineNumber:
|
|
7982
|
+
parsed.lineNumber ??
|
|
7983
|
+
existingSnapshot?.lineNumber ??
|
|
7984
|
+
lineSnapshots.length + 1,
|
|
7985
|
+
kind: parsed.kind ?? existingSnapshot?.kind ?? "product",
|
|
7986
|
+
productId: parsed.productId ?? existingSnapshot?.productId ?? null,
|
|
7987
|
+
productVariantId:
|
|
7988
|
+
parsed.productVariantId ?? existingSnapshot?.productVariantId ?? null,
|
|
7989
|
+
name: parsed.name ?? existingSnapshot?.name ?? null,
|
|
7990
|
+
description: parsed.description ?? existingSnapshot?.description ?? null,
|
|
7991
|
+
comment: parsed.comment ?? existingSnapshot?.comment ?? null,
|
|
7992
|
+
quantity: normalizedUom.quantity,
|
|
7993
|
+
quantityUnit: normalizedUom.quantityUnit,
|
|
7994
|
+
normalizedQuantity: normalizedUom.normalizedQuantity,
|
|
7995
|
+
normalizedUnit: normalizedUom.normalizedUnit,
|
|
7996
|
+
uomSnapshot: normalizedUom.uomSnapshot
|
|
7997
|
+
? cloneJson(normalizedUom.uomSnapshot)
|
|
7998
|
+
: null,
|
|
7999
|
+
currencyCode:
|
|
8000
|
+
parsed.currencyCode ??
|
|
8001
|
+
existingSnapshot?.currencyCode ??
|
|
8002
|
+
order.currencyCode,
|
|
8003
|
+
unitPriceNet: unitPriceNet ?? 0,
|
|
8004
|
+
unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0,
|
|
8005
|
+
...resolveUpsertDiscountFields(
|
|
8006
|
+
parsed.discountAmount,
|
|
8007
|
+
parsed.discountAmountBasis,
|
|
8008
|
+
existingSnapshot,
|
|
8009
|
+
),
|
|
8010
|
+
discountPercent:
|
|
8011
|
+
parsed.discountPercent ?? existingSnapshot?.discountPercent ?? 0,
|
|
8012
|
+
taxRate: taxRate ?? 0,
|
|
8013
|
+
totalNetAmount:
|
|
8014
|
+
parsed.totalNetAmount ?? existingSnapshot?.totalNetAmount ?? null,
|
|
8015
|
+
...resolveUpsertTotalsOrigin(parsed.totalNetAmount, existingSnapshot),
|
|
8016
|
+
configuration:
|
|
8017
|
+
parsed.configuration ?? existingSnapshot?.configuration ?? null,
|
|
8018
|
+
promotionCode:
|
|
8019
|
+
parsed.promotionCode ?? existingSnapshot?.promotionCode ?? null,
|
|
8020
|
+
metadata,
|
|
8021
|
+
customFieldSetId:
|
|
8022
|
+
parsed.customFieldSetId ?? existingSnapshot?.customFieldSetId ?? null,
|
|
8023
|
+
customFields:
|
|
8024
|
+
parsed.customFields && typeof parsed.customFields === "object"
|
|
8025
|
+
? cloneJson(parsed.customFields)
|
|
8026
|
+
: ((existingSnapshot as any)?.customFields ?? null),
|
|
8027
|
+
amountsMode: totalsMode,
|
|
8028
|
+
};
|
|
8029
|
+
Object.assign(
|
|
8030
|
+
updatedSnapshot,
|
|
8031
|
+
resolveUpsertCalculatedAmounts(parsed, updatedSnapshot, existingSnapshot),
|
|
7826
8032
|
);
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
(
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
8033
|
+
// After the line above, never before: tax and gross only exist once it runs,
|
|
8034
|
+
// and it nulls them when pricing changed, which an external line must restate.
|
|
8035
|
+
// The unit price is checked as resolved, not as defaulted to zero above,
|
|
8036
|
+
// and a price the tax service derived from no source price counts as absent.
|
|
8037
|
+
if (isExternalAmountsMode(totalsMode)) {
|
|
8038
|
+
await assertExternalLineComplete(
|
|
8039
|
+
{
|
|
8040
|
+
...(updatedSnapshot as Record<string, unknown>),
|
|
8041
|
+
unitPriceNet: unitPriceSupplied ? unitPriceNet : null,
|
|
8042
|
+
},
|
|
8043
|
+
updatedSnapshot.lineNumber ?? 1,
|
|
8044
|
+
);
|
|
8045
|
+
}
|
|
8046
|
+
(updatedSnapshot as any).statusEntryId = statusEntryId;
|
|
8047
|
+
(updatedSnapshot as any).catalogSnapshot =
|
|
8048
|
+
parsed.catalogSnapshot ??
|
|
8049
|
+
(existingSnapshot as any)?.catalogSnapshot ??
|
|
8050
|
+
null;
|
|
8051
|
+
(updatedSnapshot as any).promotionSnapshot =
|
|
8052
|
+
parsed.promotionSnapshot ??
|
|
8053
|
+
(existingSnapshot as any)?.promotionSnapshot ??
|
|
8054
|
+
null;
|
|
8055
|
+
|
|
8056
|
+
let nextLines = parsed.id
|
|
8057
|
+
? lineSnapshots.map((line) =>
|
|
8058
|
+
line.id === parsed.id ? updatedSnapshot : line,
|
|
8059
|
+
)
|
|
8060
|
+
: [...lineSnapshots, updatedSnapshot];
|
|
8061
|
+
nextLines = nextLines
|
|
8062
|
+
.sort((a, b) => (a.lineNumber ?? 0) - (b.lineNumber ?? 0))
|
|
8063
|
+
.map((line, index) => ({ ...line, lineNumber: index + 1 }));
|
|
7846
8064
|
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
8065
|
+
const sourceInputs = nextLines.map((line, index) => ({
|
|
8066
|
+
...line,
|
|
8067
|
+
statusEntryId: (line as any).statusEntryId ?? null,
|
|
8068
|
+
catalogSnapshot: (line as any).catalogSnapshot ?? null,
|
|
8069
|
+
promotionSnapshot: (line as any).promotionSnapshot ?? null,
|
|
8070
|
+
organizationId: order.organizationId,
|
|
8071
|
+
tenantId: order.tenantId,
|
|
8072
|
+
orderId: order.id,
|
|
8073
|
+
lineNumber: line.lineNumber ?? index + 1,
|
|
8074
|
+
}));
|
|
8075
|
+
const calcLines: SalesLineSnapshot[] = sourceInputs.map((line, index) =>
|
|
8076
|
+
createLineSnapshotFromInput(line, line.lineNumber ?? index + 1),
|
|
8077
|
+
);
|
|
8078
|
+
const adjustmentDrafts = adjustments.map(mapOrderAdjustmentToDraft);
|
|
8079
|
+
const salesCalculationService =
|
|
8080
|
+
ctx.container.resolve<SalesCalculationService>("salesCalculationService");
|
|
8081
|
+
const calculationContext = buildCalculationContext({
|
|
8082
|
+
tenantId: order.tenantId,
|
|
8083
|
+
organizationId: order.organizationId,
|
|
8084
|
+
currencyCode: order.currencyCode,
|
|
8085
|
+
shippingSnapshot: order.shippingMethodSnapshot,
|
|
8086
|
+
paymentSnapshot: order.paymentMethodSnapshot,
|
|
8087
|
+
shippingMethodId: order.shippingMethodId ?? null,
|
|
8088
|
+
paymentMethodId: order.paymentMethodId ?? null,
|
|
8089
|
+
shippingMethodCode: order.shippingMethodCode ?? null,
|
|
8090
|
+
paymentMethodCode: order.paymentMethodCode ?? null,
|
|
8091
|
+
});
|
|
8092
|
+
const calculation = await salesCalculationService.calculateDocumentTotals({
|
|
8093
|
+
documentKind: "order",
|
|
8094
|
+
lines: calcLines,
|
|
8095
|
+
adjustments: adjustmentDrafts,
|
|
8096
|
+
context: calculationContext,
|
|
8097
|
+
existingTotals: resolveExistingPaymentTotals(order),
|
|
8098
|
+
totalsMode,
|
|
8099
|
+
suppliedTotals,
|
|
8100
|
+
});
|
|
8101
|
+
await applyOrderLineResults({
|
|
8102
|
+
em,
|
|
8103
|
+
order,
|
|
8104
|
+
calculation,
|
|
8105
|
+
sourceLines: sourceInputs,
|
|
8106
|
+
existingLines,
|
|
8107
|
+
});
|
|
8108
|
+
applyOrderTotals(order, calculation.totals, calculation.lines.length);
|
|
8109
|
+
return { result: { orderId: order.id, lineId }, calculation };
|
|
7873
8110
|
});
|
|
7874
|
-
const calculation = await salesCalculationService.calculateDocumentTotals({
|
|
7875
|
-
documentKind: "order",
|
|
7876
|
-
lines: calcLines,
|
|
7877
|
-
adjustments: adjustmentDrafts,
|
|
7878
|
-
context: calculationContext,
|
|
7879
|
-
existingTotals: resolveExistingPaymentTotals(order),
|
|
7880
|
-
totalsMode,
|
|
7881
|
-
suppliedTotals,
|
|
7882
|
-
});
|
|
7883
|
-
let eventBus: EventBus | null = null;
|
|
7884
|
-
try {
|
|
7885
|
-
eventBus = ctx.container.resolve("eventBus") as EventBus;
|
|
7886
|
-
} catch {
|
|
7887
|
-
eventBus = null;
|
|
7888
|
-
}
|
|
7889
|
-
// Persist the line changes and recalculated totals atomically so a
|
|
7890
|
-
// mid-build failure cannot leave a half-updated order committed (#2336).
|
|
7891
|
-
await withAtomicFlush(
|
|
7892
|
-
em,
|
|
7893
|
-
[
|
|
7894
|
-
async () => {
|
|
7895
|
-
await applyOrderLineResults({
|
|
7896
|
-
em,
|
|
7897
|
-
order,
|
|
7898
|
-
calculation,
|
|
7899
|
-
sourceLines: sourceInputs,
|
|
7900
|
-
existingLines,
|
|
7901
|
-
});
|
|
7902
|
-
applyOrderTotals(order, calculation.totals, calculation.lines.length);
|
|
7903
|
-
await emitTotalsCalculated(eventBus, {
|
|
7904
|
-
documentKind: "order",
|
|
7905
|
-
documentId: order.id,
|
|
7906
|
-
organizationId: order.organizationId,
|
|
7907
|
-
tenantId: order.tenantId,
|
|
7908
|
-
customerId: order.customerEntityId ?? null,
|
|
7909
|
-
totals: calculation.totals,
|
|
7910
|
-
lineCount: calculation.lines.length,
|
|
7911
|
-
});
|
|
7912
|
-
},
|
|
7913
|
-
],
|
|
7914
|
-
{ transaction: true },
|
|
7915
|
-
);
|
|
7916
|
-
return { orderId: order.id, lineId };
|
|
7917
|
-
},
|
|
7918
|
-
captureAfter: async (_input, result, ctx) => {
|
|
7919
|
-
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
7920
|
-
return loadOrderSnapshot(em, result.orderId);
|
|
7921
8111
|
},
|
|
8112
|
+
captureAfter: async (_input, result, ctx) =>
|
|
8113
|
+
captureLockedOrderGraphAfter(ctx, result, result.orderId),
|
|
7922
8114
|
buildLog: async ({ result, snapshots }) => {
|
|
7923
|
-
const before = snapshots.before
|
|
8115
|
+
const before = resolveLockedOrderGraphBefore(result, snapshots.before);
|
|
7924
8116
|
const after = snapshots.after as OrderGraphSnapshot | undefined;
|
|
7925
8117
|
if (!after) return null;
|
|
7926
8118
|
const { translate } = await resolveTranslations();
|
|
@@ -7953,162 +8145,117 @@ const orderLineDeleteCommand: CommandHandler<
|
|
|
7953
8145
|
{ orderId: string; lineId: string }
|
|
7954
8146
|
> = {
|
|
7955
8147
|
id: "sales.orders.lines.delete",
|
|
7956
|
-
async prepare(input
|
|
7957
|
-
|
|
7958
|
-
const orderId = typeof raw.orderId === "string" ? raw.orderId : null;
|
|
7959
|
-
if (!orderId) return {};
|
|
7960
|
-
const em = ctx.container.resolve("em") as EntityManager;
|
|
7961
|
-
const snapshot = await loadOrderSnapshot(em, orderId);
|
|
7962
|
-
if (snapshot)
|
|
7963
|
-
ensureOrderScope(
|
|
7964
|
-
ctx,
|
|
7965
|
-
snapshot.order.organizationId,
|
|
7966
|
-
snapshot.order.tenantId,
|
|
7967
|
-
);
|
|
7968
|
-
return snapshot ? { before: snapshot } : {};
|
|
8148
|
+
async prepare(input) {
|
|
8149
|
+
return markLockedGraphWrite(input);
|
|
7969
8150
|
},
|
|
7970
8151
|
async execute(input, ctx) {
|
|
7971
8152
|
const { translate } = await resolveTranslations();
|
|
7972
8153
|
const parsed = orderLineDeleteSchema.parse(
|
|
7973
8154
|
(input?.body as Record<string, unknown> | undefined) ?? {},
|
|
7974
8155
|
);
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
"sales.documents.detail.error",
|
|
7983
|
-
"Document not found or inaccessible.",
|
|
7984
|
-
));
|
|
7985
|
-
ensureOrderScope(ctx, order.organizationId, order.tenantId);
|
|
7986
|
-
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
7987
|
-
const shipmentCount = await em.count(SalesShipmentItem, {
|
|
7988
|
-
orderLine: parsed.id,
|
|
7989
|
-
shipment: { deletedAt: null },
|
|
7990
|
-
});
|
|
7991
|
-
if (shipmentCount > 0) {
|
|
7992
|
-
throw new CrudHttpError(409, {
|
|
7993
|
-
error: translate(
|
|
7994
|
-
"sales.documents.items.errorDeleteShipped",
|
|
7995
|
-
"Cannot delete a line that has shipped items.",
|
|
7996
|
-
),
|
|
8156
|
+
return writeLockedOrderGraph(input, ctx, parsed.orderId, translate(
|
|
8157
|
+
"sales.documents.detail.error",
|
|
8158
|
+
"Document not found or inaccessible.",
|
|
8159
|
+
), async (em, order) => {
|
|
8160
|
+
const shipmentCount = await em.count(SalesShipmentItem, {
|
|
8161
|
+
orderLine: parsed.id,
|
|
8162
|
+
shipment: { deletedAt: null },
|
|
7997
8163
|
});
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8006
|
-
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8164
|
+
if (shipmentCount > 0) {
|
|
8165
|
+
throw new CrudHttpError(409, {
|
|
8166
|
+
error: translate(
|
|
8167
|
+
"sales.documents.items.errorDeleteShipped",
|
|
8168
|
+
"Cannot delete a line that has shipped items.",
|
|
8169
|
+
),
|
|
8170
|
+
});
|
|
8171
|
+
}
|
|
8172
|
+
const existingLines = await em.find(
|
|
8173
|
+
SalesOrderLine,
|
|
8174
|
+
{ order },
|
|
8175
|
+
{ orderBy: { lineNumber: "asc" } },
|
|
8176
|
+
);
|
|
8177
|
+
const adjustments = await em.find(
|
|
8178
|
+
SalesOrderAdjustment,
|
|
8179
|
+
{ order },
|
|
8180
|
+
{ orderBy: { position: "asc" } },
|
|
8181
|
+
);
|
|
8182
|
+
const filtered = existingLines.filter((line) => line.id !== parsed.id);
|
|
8183
|
+
if (filtered.length === existingLines.length) {
|
|
8184
|
+
throw notFound(translate(
|
|
8185
|
+
"sales.documents.detail.error",
|
|
8186
|
+
"Document not found or inaccessible.",
|
|
8187
|
+
));
|
|
8188
|
+
}
|
|
8189
|
+
if (filtered.length === 0) {
|
|
8190
|
+
throw new CrudHttpError(409, {
|
|
8191
|
+
error: translate(
|
|
8192
|
+
"sales.documents.items.errorDeleteLast",
|
|
8193
|
+
"An order must contain at least one line item.",
|
|
8194
|
+
),
|
|
8195
|
+
});
|
|
8196
|
+
}
|
|
8197
|
+
const totalsMode: SalesAmountsMode = order.totalsMode ?? "computed";
|
|
8198
|
+
if (isExternalAmountsMode(totalsMode)) {
|
|
8199
|
+
await requireOrderTotalsForExternalWrite(parsed.orderTotals);
|
|
8200
|
+
}
|
|
8201
|
+
const suppliedTotals: Partial<SalesDocumentAmounts> | null = isExternalAmountsMode(totalsMode)
|
|
8202
|
+
? buildExternalHeaderTotals(parsed.orderTotals)
|
|
8203
|
+
: null;
|
|
8204
|
+
const sourceInputs = filtered.map((line, index) => ({
|
|
8205
|
+
...mapOrderLineEntityToSnapshot(line),
|
|
8206
|
+
statusEntryId: line.statusEntryId ?? null,
|
|
8207
|
+
catalogSnapshot: line.catalogSnapshot
|
|
8208
|
+
? cloneJson(line.catalogSnapshot)
|
|
8209
|
+
: null,
|
|
8210
|
+
promotionSnapshot: line.promotionSnapshot
|
|
8211
|
+
? cloneJson(line.promotionSnapshot)
|
|
8212
|
+
: null,
|
|
8213
|
+
organizationId: order.organizationId,
|
|
8214
|
+
tenantId: order.tenantId,
|
|
8215
|
+
orderId: order.id,
|
|
8216
|
+
lineNumber: index + 1,
|
|
8217
|
+
}));
|
|
8218
|
+
const calcLines = sourceInputs.map((line, index) =>
|
|
8219
|
+
createLineSnapshotFromInput(line, line.lineNumber ?? index + 1),
|
|
8220
|
+
);
|
|
8221
|
+
const adjustmentDrafts = adjustments.map(mapOrderAdjustmentToDraft);
|
|
8222
|
+
const salesCalculationService =
|
|
8223
|
+
ctx.container.resolve<SalesCalculationService>("salesCalculationService");
|
|
8224
|
+
const calculationContext = buildCalculationContext({
|
|
8225
|
+
tenantId: order.tenantId,
|
|
8226
|
+
organizationId: order.organizationId,
|
|
8227
|
+
currencyCode: order.currencyCode,
|
|
8228
|
+
shippingSnapshot: order.shippingMethodSnapshot,
|
|
8229
|
+
paymentSnapshot: order.paymentMethodSnapshot,
|
|
8230
|
+
shippingMethodId: order.shippingMethodId ?? null,
|
|
8231
|
+
paymentMethodId: order.paymentMethodId ?? null,
|
|
8232
|
+
shippingMethodCode: order.shippingMethodCode ?? null,
|
|
8233
|
+
paymentMethodCode: order.paymentMethodCode ?? null,
|
|
8022
8234
|
});
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
:
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
tenantId: order.tenantId,
|
|
8042
|
-
orderId: order.id,
|
|
8043
|
-
lineNumber: index + 1,
|
|
8044
|
-
}));
|
|
8045
|
-
const calcLines = sourceInputs.map((line, index) =>
|
|
8046
|
-
createLineSnapshotFromInput(line, line.lineNumber ?? index + 1),
|
|
8047
|
-
);
|
|
8048
|
-
const adjustmentDrafts = adjustments.map(mapOrderAdjustmentToDraft);
|
|
8049
|
-
const salesCalculationService =
|
|
8050
|
-
ctx.container.resolve<SalesCalculationService>("salesCalculationService");
|
|
8051
|
-
const calculationContext = buildCalculationContext({
|
|
8052
|
-
tenantId: order.tenantId,
|
|
8053
|
-
organizationId: order.organizationId,
|
|
8054
|
-
currencyCode: order.currencyCode,
|
|
8055
|
-
shippingSnapshot: order.shippingMethodSnapshot,
|
|
8056
|
-
paymentSnapshot: order.paymentMethodSnapshot,
|
|
8057
|
-
shippingMethodId: order.shippingMethodId ?? null,
|
|
8058
|
-
paymentMethodId: order.paymentMethodId ?? null,
|
|
8059
|
-
shippingMethodCode: order.shippingMethodCode ?? null,
|
|
8060
|
-
paymentMethodCode: order.paymentMethodCode ?? null,
|
|
8061
|
-
});
|
|
8062
|
-
const calculation = await salesCalculationService.calculateDocumentTotals({
|
|
8063
|
-
documentKind: "order",
|
|
8064
|
-
lines: calcLines,
|
|
8065
|
-
adjustments: adjustmentDrafts,
|
|
8066
|
-
context: calculationContext,
|
|
8067
|
-
existingTotals: resolveExistingPaymentTotals(order),
|
|
8068
|
-
totalsMode,
|
|
8069
|
-
suppliedTotals,
|
|
8235
|
+
const calculation = await salesCalculationService.calculateDocumentTotals({
|
|
8236
|
+
documentKind: "order",
|
|
8237
|
+
lines: calcLines,
|
|
8238
|
+
adjustments: adjustmentDrafts,
|
|
8239
|
+
context: calculationContext,
|
|
8240
|
+
existingTotals: resolveExistingPaymentTotals(order),
|
|
8241
|
+
totalsMode,
|
|
8242
|
+
suppliedTotals,
|
|
8243
|
+
});
|
|
8244
|
+
await applyOrderLineResults({
|
|
8245
|
+
em,
|
|
8246
|
+
order,
|
|
8247
|
+
calculation,
|
|
8248
|
+
sourceLines: sourceInputs,
|
|
8249
|
+
existingLines,
|
|
8250
|
+
});
|
|
8251
|
+
applyOrderTotals(order, calculation.totals, calculation.lines.length);
|
|
8252
|
+
return { result: { orderId: order.id, lineId: parsed.id }, calculation };
|
|
8070
8253
|
});
|
|
8071
|
-
let eventBus: EventBus | null = null;
|
|
8072
|
-
try {
|
|
8073
|
-
eventBus = ctx.container.resolve("eventBus") as EventBus;
|
|
8074
|
-
} catch {
|
|
8075
|
-
eventBus = null;
|
|
8076
|
-
}
|
|
8077
|
-
// Persist the line removal and recalculated totals atomically so a
|
|
8078
|
-
// mid-build failure cannot leave a half-updated order committed (#2336).
|
|
8079
|
-
await withAtomicFlush(
|
|
8080
|
-
em,
|
|
8081
|
-
[
|
|
8082
|
-
async () => {
|
|
8083
|
-
await applyOrderLineResults({
|
|
8084
|
-
em,
|
|
8085
|
-
order,
|
|
8086
|
-
calculation,
|
|
8087
|
-
sourceLines: sourceInputs,
|
|
8088
|
-
existingLines,
|
|
8089
|
-
});
|
|
8090
|
-
applyOrderTotals(order, calculation.totals, calculation.lines.length);
|
|
8091
|
-
await emitTotalsCalculated(eventBus, {
|
|
8092
|
-
documentKind: "order",
|
|
8093
|
-
documentId: order.id,
|
|
8094
|
-
organizationId: order.organizationId,
|
|
8095
|
-
tenantId: order.tenantId,
|
|
8096
|
-
customerId: order.customerEntityId ?? null,
|
|
8097
|
-
totals: calculation.totals,
|
|
8098
|
-
lineCount: calculation.lines.length,
|
|
8099
|
-
});
|
|
8100
|
-
},
|
|
8101
|
-
],
|
|
8102
|
-
{ transaction: true },
|
|
8103
|
-
);
|
|
8104
|
-
return { orderId: order.id, lineId: parsed.id };
|
|
8105
|
-
},
|
|
8106
|
-
captureAfter: async (_input, result, ctx) => {
|
|
8107
|
-
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
8108
|
-
return loadOrderSnapshot(em, result.orderId);
|
|
8109
8254
|
},
|
|
8255
|
+
captureAfter: async (_input, result, ctx) =>
|
|
8256
|
+
captureLockedOrderGraphAfter(ctx, result, result.orderId),
|
|
8110
8257
|
buildLog: async ({ result, snapshots }) => {
|
|
8111
|
-
const before = snapshots.before
|
|
8258
|
+
const before = resolveLockedOrderGraphBefore(result, snapshots.before);
|
|
8112
8259
|
const after = snapshots.after as OrderGraphSnapshot | undefined;
|
|
8113
8260
|
if (!after) return null;
|
|
8114
8261
|
const { translate } = await resolveTranslations();
|
|
@@ -8141,127 +8288,87 @@ const quoteLineUpsertCommand: CommandHandler<
|
|
|
8141
8288
|
{ quoteId: string; lineId: string }
|
|
8142
8289
|
> = {
|
|
8143
8290
|
id: "sales.quotes.lines.upsert",
|
|
8144
|
-
async prepare(input
|
|
8145
|
-
|
|
8146
|
-
const quoteId = typeof raw.quoteId === "string" ? raw.quoteId : null;
|
|
8147
|
-
if (!quoteId) return {};
|
|
8148
|
-
const em = ctx.container.resolve("em") as EntityManager;
|
|
8149
|
-
const snapshot = await loadQuoteSnapshot(em, quoteId);
|
|
8150
|
-
if (snapshot)
|
|
8151
|
-
ensureQuoteScope(
|
|
8152
|
-
ctx,
|
|
8153
|
-
snapshot.quote.organizationId,
|
|
8154
|
-
snapshot.quote.tenantId,
|
|
8155
|
-
);
|
|
8156
|
-
return snapshot ? { before: snapshot } : {};
|
|
8291
|
+
async prepare(input) {
|
|
8292
|
+
return markLockedGraphWrite(input);
|
|
8157
8293
|
},
|
|
8158
8294
|
async execute(input, ctx) {
|
|
8159
8295
|
const rawBody = (input?.body as Record<string, unknown> | undefined) ?? {};
|
|
8160
8296
|
const parsed = quoteLineUpsertSchema.parse(rawBody);
|
|
8161
8297
|
await assertAmountsModeUnsupportedOnQuote([parsed]);
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
tenantId: parsed.tenantId,
|
|
8212
|
-
taxRateId: parsed.taxRateId ?? undefined,
|
|
8213
|
-
taxRate: taxRate ?? undefined,
|
|
8214
|
-
});
|
|
8215
|
-
unitPriceNet = unitPriceNet ?? taxResult.netAmount;
|
|
8216
|
-
unitPriceGross = unitPriceGross ?? taxResult.grossAmount;
|
|
8217
|
-
taxRate = taxResult.taxRate ?? taxRate;
|
|
8298
|
+
return writeLockedQuoteGraph(input, ctx, parsed.quoteId, "Sales quote not found", async (em, quote) => {
|
|
8299
|
+
const [existingLines, adjustments] = await Promise.all([
|
|
8300
|
+
em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
|
|
8301
|
+
em.find(
|
|
8302
|
+
SalesQuoteAdjustment,
|
|
8303
|
+
{ quote },
|
|
8304
|
+
{ orderBy: { position: "asc" } },
|
|
8305
|
+
),
|
|
8306
|
+
]);
|
|
8307
|
+
const lineSnapshots = existingLines.map(mapQuoteLineEntityToSnapshot);
|
|
8308
|
+
const existingSnapshot = parsed.id
|
|
8309
|
+
? (lineSnapshots.find((line) => line.id === parsed.id) ?? null)
|
|
8310
|
+
: null;
|
|
8311
|
+
const priceMode =
|
|
8312
|
+
parsed.priceMode === "gross"
|
|
8313
|
+
? "gross"
|
|
8314
|
+
: parsed.priceMode === "net"
|
|
8315
|
+
? "net"
|
|
8316
|
+
: null;
|
|
8317
|
+
let unitPriceNet =
|
|
8318
|
+
parsed.unitPriceNet ?? existingSnapshot?.unitPriceNet ?? null;
|
|
8319
|
+
let unitPriceGross =
|
|
8320
|
+
parsed.unitPriceGross ?? existingSnapshot?.unitPriceGross ?? null;
|
|
8321
|
+
let taxRate = parsed.taxRate ?? existingSnapshot?.taxRate ?? null;
|
|
8322
|
+
if (priceMode && (unitPriceNet === null || unitPriceGross === null)) {
|
|
8323
|
+
let taxService: TaxCalculationService | null = null;
|
|
8324
|
+
try {
|
|
8325
|
+
taxService = ctx.container.resolve(
|
|
8326
|
+
"taxCalculationService",
|
|
8327
|
+
) as TaxCalculationService;
|
|
8328
|
+
} catch {
|
|
8329
|
+
taxService = null;
|
|
8330
|
+
}
|
|
8331
|
+
if (taxService) {
|
|
8332
|
+
const taxResult = await taxService.calculateUnitAmounts({
|
|
8333
|
+
amount:
|
|
8334
|
+
priceMode === "gross"
|
|
8335
|
+
? (unitPriceGross ?? unitPriceNet ?? 0)
|
|
8336
|
+
: (unitPriceNet ?? unitPriceGross ?? 0),
|
|
8337
|
+
mode: priceMode,
|
|
8338
|
+
organizationId: parsed.organizationId,
|
|
8339
|
+
tenantId: parsed.tenantId,
|
|
8340
|
+
taxRateId: parsed.taxRateId ?? undefined,
|
|
8341
|
+
taxRate: taxRate ?? undefined,
|
|
8342
|
+
});
|
|
8343
|
+
unitPriceNet = unitPriceNet ?? taxResult.netAmount;
|
|
8344
|
+
unitPriceGross = unitPriceGross ?? taxResult.grossAmount;
|
|
8345
|
+
taxRate = taxResult.taxRate ?? taxRate;
|
|
8346
|
+
}
|
|
8218
8347
|
}
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
parsed.
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
parsed.
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
let normalizedUom = await normalizeLineUom({
|
|
8244
|
-
em,
|
|
8245
|
-
resolver: uomResolver,
|
|
8246
|
-
organizationId: quote.organizationId,
|
|
8247
|
-
tenantId: quote.tenantId,
|
|
8248
|
-
line: {
|
|
8249
|
-
...lineUomInput,
|
|
8250
|
-
unitPriceNet: unitPriceNet ?? 0,
|
|
8251
|
-
unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0,
|
|
8252
|
-
},
|
|
8253
|
-
});
|
|
8254
|
-
const convertedPrices = convertLineUnitPricesOnUnitChange({
|
|
8255
|
-
existingSnapshot,
|
|
8256
|
-
nextQuantityUnit: normalizedUom.quantityUnit,
|
|
8257
|
-
nextUomSnapshot: normalizedUom.uomSnapshot,
|
|
8258
|
-
unitPriceNet,
|
|
8259
|
-
unitPriceGross,
|
|
8260
|
-
});
|
|
8261
|
-
if (convertedPrices.didConvert) {
|
|
8262
|
-
unitPriceNet = convertedPrices.unitPriceNet;
|
|
8263
|
-
unitPriceGross = convertedPrices.unitPriceGross;
|
|
8264
|
-
normalizedUom = await normalizeLineUom({
|
|
8348
|
+
const metadata =
|
|
8349
|
+
typeof parsed.metadata === "object" && parsed.metadata
|
|
8350
|
+
? { ...parsed.metadata }
|
|
8351
|
+
: existingSnapshot?.metadata
|
|
8352
|
+
? cloneJson(existingSnapshot.metadata)
|
|
8353
|
+
: {};
|
|
8354
|
+
if (parsed.priceId) metadata.priceId = parsed.priceId;
|
|
8355
|
+
if (priceMode) metadata.priceMode = priceMode;
|
|
8356
|
+
|
|
8357
|
+
const statusEntryId =
|
|
8358
|
+
parsed.statusEntryId ?? (existingSnapshot as any)?.statusEntryId ?? null;
|
|
8359
|
+
const lineId = parsed.id ?? existingSnapshot?.id ?? randomUUID();
|
|
8360
|
+
const lineUomInput = {
|
|
8361
|
+
productId: parsed.productId ?? existingSnapshot?.productId ?? null,
|
|
8362
|
+
productVariantId:
|
|
8363
|
+
parsed.productVariantId ?? existingSnapshot?.productVariantId ?? null,
|
|
8364
|
+
quantity: parsed.quantity ?? existingSnapshot?.quantity ?? 0,
|
|
8365
|
+
quantityUnit: parsed.quantityUnit ?? existingSnapshot?.quantityUnit ?? null,
|
|
8366
|
+
normalizedQuantity: existingSnapshot?.normalizedQuantity ?? null,
|
|
8367
|
+
normalizedUnit: existingSnapshot?.normalizedUnit ?? null,
|
|
8368
|
+
uomSnapshot: existingSnapshot?.uomSnapshot ?? null,
|
|
8369
|
+
};
|
|
8370
|
+
const uomResolver = createUomResolver();
|
|
8371
|
+
let normalizedUom = await normalizeLineUom({
|
|
8265
8372
|
em,
|
|
8266
8373
|
resolver: uomResolver,
|
|
8267
8374
|
organizationId: quote.organizationId,
|
|
@@ -8272,157 +8379,152 @@ const quoteLineUpsertCommand: CommandHandler<
|
|
|
8272
8379
|
unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0,
|
|
8273
8380
|
},
|
|
8274
8381
|
});
|
|
8275
|
-
|
|
8276
|
-
const updatedSnapshot: SalesLineSnapshot & {
|
|
8277
|
-
statusEntryId?: string | null;
|
|
8278
|
-
catalogSnapshot?: Record<string, unknown> | null;
|
|
8279
|
-
promotionSnapshot?: Record<string, unknown> | null;
|
|
8280
|
-
} = {
|
|
8281
|
-
id: lineId,
|
|
8282
|
-
lineNumber:
|
|
8283
|
-
parsed.lineNumber ??
|
|
8284
|
-
existingSnapshot?.lineNumber ??
|
|
8285
|
-
lineSnapshots.length + 1,
|
|
8286
|
-
kind: parsed.kind ?? existingSnapshot?.kind ?? "product",
|
|
8287
|
-
productId: parsed.productId ?? existingSnapshot?.productId ?? null,
|
|
8288
|
-
productVariantId:
|
|
8289
|
-
parsed.productVariantId ?? existingSnapshot?.productVariantId ?? null,
|
|
8290
|
-
name: parsed.name ?? existingSnapshot?.name ?? null,
|
|
8291
|
-
description: parsed.description ?? existingSnapshot?.description ?? null,
|
|
8292
|
-
comment: parsed.comment ?? existingSnapshot?.comment ?? null,
|
|
8293
|
-
quantity: normalizedUom.quantity,
|
|
8294
|
-
quantityUnit: normalizedUom.quantityUnit,
|
|
8295
|
-
normalizedQuantity: normalizedUom.normalizedQuantity,
|
|
8296
|
-
normalizedUnit: normalizedUom.normalizedUnit,
|
|
8297
|
-
uomSnapshot: normalizedUom.uomSnapshot
|
|
8298
|
-
? cloneJson(normalizedUom.uomSnapshot)
|
|
8299
|
-
: null,
|
|
8300
|
-
currencyCode:
|
|
8301
|
-
parsed.currencyCode ??
|
|
8302
|
-
existingSnapshot?.currencyCode ??
|
|
8303
|
-
quote.currencyCode,
|
|
8304
|
-
unitPriceNet: unitPriceNet ?? 0,
|
|
8305
|
-
unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0,
|
|
8306
|
-
...resolveUpsertDiscountFields(
|
|
8307
|
-
parsed.discountAmount,
|
|
8308
|
-
parsed.discountAmountBasis,
|
|
8382
|
+
const convertedPrices = convertLineUnitPricesOnUnitChange({
|
|
8309
8383
|
existingSnapshot,
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
updatedSnapshot
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8384
|
+
nextQuantityUnit: normalizedUom.quantityUnit,
|
|
8385
|
+
nextUomSnapshot: normalizedUom.uomSnapshot,
|
|
8386
|
+
unitPriceNet,
|
|
8387
|
+
unitPriceGross,
|
|
8388
|
+
});
|
|
8389
|
+
if (convertedPrices.didConvert) {
|
|
8390
|
+
unitPriceNet = convertedPrices.unitPriceNet;
|
|
8391
|
+
unitPriceGross = convertedPrices.unitPriceGross;
|
|
8392
|
+
normalizedUom = await normalizeLineUom({
|
|
8393
|
+
em,
|
|
8394
|
+
resolver: uomResolver,
|
|
8395
|
+
organizationId: quote.organizationId,
|
|
8396
|
+
tenantId: quote.tenantId,
|
|
8397
|
+
line: {
|
|
8398
|
+
...lineUomInput,
|
|
8399
|
+
unitPriceNet: unitPriceNet ?? 0,
|
|
8400
|
+
unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0,
|
|
8401
|
+
},
|
|
8402
|
+
});
|
|
8403
|
+
}
|
|
8404
|
+
const updatedSnapshot: SalesLineSnapshot & {
|
|
8405
|
+
statusEntryId?: string | null;
|
|
8406
|
+
catalogSnapshot?: Record<string, unknown> | null;
|
|
8407
|
+
promotionSnapshot?: Record<string, unknown> | null;
|
|
8408
|
+
} = {
|
|
8409
|
+
id: lineId,
|
|
8410
|
+
lineNumber:
|
|
8411
|
+
parsed.lineNumber ??
|
|
8412
|
+
existingSnapshot?.lineNumber ??
|
|
8413
|
+
lineSnapshots.length + 1,
|
|
8414
|
+
kind: parsed.kind ?? existingSnapshot?.kind ?? "product",
|
|
8415
|
+
productId: parsed.productId ?? existingSnapshot?.productId ?? null,
|
|
8416
|
+
productVariantId:
|
|
8417
|
+
parsed.productVariantId ?? existingSnapshot?.productVariantId ?? null,
|
|
8418
|
+
name: parsed.name ?? existingSnapshot?.name ?? null,
|
|
8419
|
+
description: parsed.description ?? existingSnapshot?.description ?? null,
|
|
8420
|
+
comment: parsed.comment ?? existingSnapshot?.comment ?? null,
|
|
8421
|
+
quantity: normalizedUom.quantity,
|
|
8422
|
+
quantityUnit: normalizedUom.quantityUnit,
|
|
8423
|
+
normalizedQuantity: normalizedUom.normalizedQuantity,
|
|
8424
|
+
normalizedUnit: normalizedUom.normalizedUnit,
|
|
8425
|
+
uomSnapshot: normalizedUom.uomSnapshot
|
|
8426
|
+
? cloneJson(normalizedUom.uomSnapshot)
|
|
8427
|
+
: null,
|
|
8428
|
+
currencyCode:
|
|
8429
|
+
parsed.currencyCode ??
|
|
8430
|
+
existingSnapshot?.currencyCode ??
|
|
8431
|
+
quote.currencyCode,
|
|
8432
|
+
unitPriceNet: unitPriceNet ?? 0,
|
|
8433
|
+
unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0,
|
|
8434
|
+
...resolveUpsertDiscountFields(
|
|
8435
|
+
parsed.discountAmount,
|
|
8436
|
+
parsed.discountAmountBasis,
|
|
8437
|
+
existingSnapshot,
|
|
8438
|
+
),
|
|
8439
|
+
discountPercent:
|
|
8440
|
+
parsed.discountPercent ?? existingSnapshot?.discountPercent ?? 0,
|
|
8441
|
+
taxRate: taxRate ?? 0,
|
|
8442
|
+
totalNetAmount:
|
|
8443
|
+
parsed.totalNetAmount ?? existingSnapshot?.totalNetAmount ?? null,
|
|
8444
|
+
...resolveUpsertTotalsOrigin(parsed.totalNetAmount, existingSnapshot),
|
|
8445
|
+
configuration:
|
|
8446
|
+
parsed.configuration ?? existingSnapshot?.configuration ?? null,
|
|
8447
|
+
promotionCode:
|
|
8448
|
+
parsed.promotionCode ?? existingSnapshot?.promotionCode ?? null,
|
|
8449
|
+
metadata,
|
|
8450
|
+
customFieldSetId:
|
|
8451
|
+
parsed.customFieldSetId ?? existingSnapshot?.customFieldSetId ?? null,
|
|
8452
|
+
customFields:
|
|
8453
|
+
parsed.customFields && typeof parsed.customFields === "object"
|
|
8454
|
+
? cloneJson(parsed.customFields)
|
|
8455
|
+
: ((existingSnapshot as any)?.customFields ?? null),
|
|
8456
|
+
};
|
|
8457
|
+
Object.assign(
|
|
8458
|
+
updatedSnapshot,
|
|
8459
|
+
resolveUpsertCalculatedAmounts(parsed, updatedSnapshot, existingSnapshot),
|
|
8460
|
+
);
|
|
8461
|
+
(updatedSnapshot as any).statusEntryId = statusEntryId;
|
|
8462
|
+
(updatedSnapshot as any).catalogSnapshot =
|
|
8463
|
+
parsed.catalogSnapshot ??
|
|
8464
|
+
(existingSnapshot as any)?.catalogSnapshot ??
|
|
8465
|
+
null;
|
|
8466
|
+
(updatedSnapshot as any).promotionSnapshot =
|
|
8467
|
+
parsed.promotionSnapshot ??
|
|
8468
|
+
(existingSnapshot as any)?.promotionSnapshot ??
|
|
8469
|
+
null;
|
|
8470
|
+
|
|
8471
|
+
let nextLines = parsed.id
|
|
8472
|
+
? lineSnapshots.map((line) =>
|
|
8473
|
+
line.id === parsed.id ? updatedSnapshot : line,
|
|
8474
|
+
)
|
|
8475
|
+
: [...lineSnapshots, updatedSnapshot];
|
|
8476
|
+
nextLines = nextLines
|
|
8477
|
+
.sort((a, b) => (a.lineNumber ?? 0) - (b.lineNumber ?? 0))
|
|
8478
|
+
.map((line, index) => ({ ...line, lineNumber: index + 1 }));
|
|
8351
8479
|
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
|
|
8364
|
-
|
|
8365
|
-
|
|
8366
|
-
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
|
|
8372
|
-
|
|
8373
|
-
|
|
8374
|
-
|
|
8375
|
-
|
|
8376
|
-
|
|
8377
|
-
|
|
8378
|
-
|
|
8379
|
-
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8480
|
+
const sourceInputs = nextLines.map((line, index) => ({
|
|
8481
|
+
...line,
|
|
8482
|
+
statusEntryId: (line as any).statusEntryId ?? null,
|
|
8483
|
+
catalogSnapshot: (line as any).catalogSnapshot ?? null,
|
|
8484
|
+
promotionSnapshot: (line as any).promotionSnapshot ?? null,
|
|
8485
|
+
organizationId: quote.organizationId,
|
|
8486
|
+
tenantId: quote.tenantId,
|
|
8487
|
+
quoteId: quote.id,
|
|
8488
|
+
lineNumber: line.lineNumber ?? index + 1,
|
|
8489
|
+
}));
|
|
8490
|
+
const calcLines: SalesLineSnapshot[] = sourceInputs.map((line, index) =>
|
|
8491
|
+
createLineSnapshotFromInput(line, line.lineNumber ?? index + 1),
|
|
8492
|
+
);
|
|
8493
|
+
const adjustmentDrafts = adjustments.map(mapQuoteAdjustmentToDraft);
|
|
8494
|
+
const salesCalculationService =
|
|
8495
|
+
ctx.container.resolve<SalesCalculationService>("salesCalculationService");
|
|
8496
|
+
const calculationContext = buildCalculationContext({
|
|
8497
|
+
tenantId: quote.tenantId,
|
|
8498
|
+
organizationId: quote.organizationId,
|
|
8499
|
+
currencyCode: quote.currencyCode,
|
|
8500
|
+
shippingSnapshot: quote.shippingMethodSnapshot,
|
|
8501
|
+
paymentSnapshot: quote.paymentMethodSnapshot,
|
|
8502
|
+
shippingMethodId: quote.shippingMethodId ?? null,
|
|
8503
|
+
paymentMethodId: quote.paymentMethodId ?? null,
|
|
8504
|
+
shippingMethodCode: quote.shippingMethodCode ?? null,
|
|
8505
|
+
paymentMethodCode: quote.paymentMethodCode ?? null,
|
|
8506
|
+
});
|
|
8507
|
+
const calculation = await salesCalculationService.calculateDocumentTotals({
|
|
8508
|
+
documentKind: "quote",
|
|
8509
|
+
lines: calcLines,
|
|
8510
|
+
adjustments: adjustmentDrafts,
|
|
8511
|
+
context: calculationContext,
|
|
8512
|
+
});
|
|
8513
|
+
await applyQuoteLineResults({
|
|
8514
|
+
em,
|
|
8515
|
+
quote,
|
|
8516
|
+
calculation,
|
|
8517
|
+
sourceLines: sourceInputs,
|
|
8518
|
+
existingLines,
|
|
8519
|
+
});
|
|
8520
|
+
applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
|
|
8521
|
+
return { result: { quoteId: quote.id, lineId }, calculation };
|
|
8384
8522
|
});
|
|
8385
|
-
let eventBus: EventBus | null = null;
|
|
8386
|
-
try {
|
|
8387
|
-
eventBus = ctx.container.resolve("eventBus") as EventBus;
|
|
8388
|
-
} catch {
|
|
8389
|
-
eventBus = null;
|
|
8390
|
-
}
|
|
8391
|
-
// Persist the line changes and recalculated totals atomically so a
|
|
8392
|
-
// mid-build failure cannot leave a half-updated quote committed (#2336).
|
|
8393
|
-
await withAtomicFlush(
|
|
8394
|
-
em,
|
|
8395
|
-
[
|
|
8396
|
-
async () => {
|
|
8397
|
-
await applyQuoteLineResults({
|
|
8398
|
-
em,
|
|
8399
|
-
quote,
|
|
8400
|
-
calculation,
|
|
8401
|
-
sourceLines: sourceInputs,
|
|
8402
|
-
existingLines,
|
|
8403
|
-
});
|
|
8404
|
-
applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
|
|
8405
|
-
await emitTotalsCalculated(eventBus, {
|
|
8406
|
-
documentKind: "quote",
|
|
8407
|
-
documentId: quote.id,
|
|
8408
|
-
organizationId: quote.organizationId,
|
|
8409
|
-
tenantId: quote.tenantId,
|
|
8410
|
-
customerId: quote.customerEntityId ?? null,
|
|
8411
|
-
totals: calculation.totals,
|
|
8412
|
-
lineCount: calculation.lines.length,
|
|
8413
|
-
});
|
|
8414
|
-
},
|
|
8415
|
-
],
|
|
8416
|
-
{ transaction: true },
|
|
8417
|
-
);
|
|
8418
|
-
return { quoteId: quote.id, lineId };
|
|
8419
|
-
},
|
|
8420
|
-
captureAfter: async (_input, result, ctx) => {
|
|
8421
|
-
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
8422
|
-
return loadQuoteSnapshot(em, result.quoteId);
|
|
8423
8523
|
},
|
|
8524
|
+
captureAfter: async (_input, result, ctx) =>
|
|
8525
|
+
captureLockedQuoteGraphAfter(ctx, result, result.quoteId),
|
|
8424
8526
|
buildLog: async ({ result, snapshots }) => {
|
|
8425
|
-
const before = snapshots.before
|
|
8527
|
+
const before = resolveLockedQuoteGraphBefore(result, snapshots.before);
|
|
8426
8528
|
const after = snapshots.after as QuoteGraphSnapshot | undefined;
|
|
8427
8529
|
if (!after) return null;
|
|
8428
8530
|
const { translate } = await resolveTranslations();
|
|
@@ -8455,125 +8557,80 @@ const quoteLineDeleteCommand: CommandHandler<
|
|
|
8455
8557
|
{ quoteId: string; lineId: string }
|
|
8456
8558
|
> = {
|
|
8457
8559
|
id: "sales.quotes.lines.delete",
|
|
8458
|
-
async prepare(input
|
|
8459
|
-
|
|
8460
|
-
const quoteId = typeof raw.quoteId === "string" ? raw.quoteId : null;
|
|
8461
|
-
if (!quoteId) return {};
|
|
8462
|
-
const em = ctx.container.resolve("em") as EntityManager;
|
|
8463
|
-
const snapshot = await loadQuoteSnapshot(em, quoteId);
|
|
8464
|
-
if (snapshot)
|
|
8465
|
-
ensureQuoteScope(
|
|
8466
|
-
ctx,
|
|
8467
|
-
snapshot.quote.organizationId,
|
|
8468
|
-
snapshot.quote.tenantId,
|
|
8469
|
-
);
|
|
8470
|
-
return snapshot ? { before: snapshot } : {};
|
|
8560
|
+
async prepare(input) {
|
|
8561
|
+
return markLockedGraphWrite(input);
|
|
8471
8562
|
},
|
|
8472
8563
|
async execute(input, ctx) {
|
|
8473
8564
|
const parsed = quoteLineDeleteSchema.parse(
|
|
8474
8565
|
(input?.body as Record<string, unknown> | undefined) ?? {},
|
|
8475
8566
|
);
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
|
|
8567
|
+
return writeLockedQuoteGraph(input, ctx, parsed.quoteId, "Sales quote not found", async (em, quote) => {
|
|
8568
|
+
const existingLines = await em.find(
|
|
8569
|
+
SalesQuoteLine,
|
|
8570
|
+
{ quote },
|
|
8571
|
+
{ orderBy: { lineNumber: "asc" } },
|
|
8572
|
+
);
|
|
8573
|
+
const adjustments = await em.find(
|
|
8574
|
+
SalesQuoteAdjustment,
|
|
8575
|
+
{ quote },
|
|
8576
|
+
{ orderBy: { position: "asc" } },
|
|
8577
|
+
);
|
|
8578
|
+
const filtered = existingLines.filter((line) => line.id !== parsed.id);
|
|
8579
|
+
if (filtered.length === existingLines.length) {
|
|
8580
|
+
throw notFound("Quote line not found");
|
|
8581
|
+
}
|
|
8582
|
+
const sourceInputs = filtered.map((line, index) => ({
|
|
8583
|
+
...mapQuoteLineEntityToSnapshot(line),
|
|
8584
|
+
statusEntryId: line.statusEntryId ?? null,
|
|
8585
|
+
catalogSnapshot: line.catalogSnapshot
|
|
8586
|
+
? cloneJson(line.catalogSnapshot)
|
|
8587
|
+
: null,
|
|
8588
|
+
promotionSnapshot: line.promotionSnapshot
|
|
8589
|
+
? cloneJson(line.promotionSnapshot)
|
|
8590
|
+
: null,
|
|
8591
|
+
organizationId: quote.organizationId,
|
|
8592
|
+
tenantId: quote.tenantId,
|
|
8593
|
+
quoteId: quote.id,
|
|
8594
|
+
lineNumber: index + 1,
|
|
8595
|
+
}));
|
|
8596
|
+
const calcLines = sourceInputs.map((line, index) =>
|
|
8597
|
+
createLineSnapshotFromInput(line, line.lineNumber ?? index + 1),
|
|
8598
|
+
);
|
|
8599
|
+
const adjustmentDrafts = adjustments.map(mapQuoteAdjustmentToDraft);
|
|
8600
|
+
const salesCalculationService =
|
|
8601
|
+
ctx.container.resolve<SalesCalculationService>("salesCalculationService");
|
|
8602
|
+
const calculationContext = buildCalculationContext({
|
|
8603
|
+
tenantId: quote.tenantId,
|
|
8604
|
+
organizationId: quote.organizationId,
|
|
8605
|
+
currencyCode: quote.currencyCode,
|
|
8606
|
+
shippingSnapshot: quote.shippingMethodSnapshot,
|
|
8607
|
+
paymentSnapshot: quote.paymentMethodSnapshot,
|
|
8608
|
+
shippingMethodId: quote.shippingMethodId ?? null,
|
|
8609
|
+
paymentMethodId: quote.paymentMethodId ?? null,
|
|
8610
|
+
shippingMethodCode: quote.shippingMethodCode ?? null,
|
|
8611
|
+
paymentMethodCode: quote.paymentMethodCode ?? null,
|
|
8612
|
+
});
|
|
8613
|
+
const calculation = await salesCalculationService.calculateDocumentTotals({
|
|
8614
|
+
documentKind: "quote",
|
|
8615
|
+
lines: calcLines,
|
|
8616
|
+
adjustments: adjustmentDrafts,
|
|
8617
|
+
context: calculationContext,
|
|
8618
|
+
});
|
|
8619
|
+
await applyQuoteLineResults({
|
|
8620
|
+
em,
|
|
8621
|
+
quote,
|
|
8622
|
+
calculation,
|
|
8623
|
+
sourceLines: sourceInputs,
|
|
8624
|
+
existingLines,
|
|
8625
|
+
});
|
|
8626
|
+
applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
|
|
8627
|
+
return { result: { quoteId: quote.id, lineId: parsed.id }, calculation };
|
|
8535
8628
|
});
|
|
8536
|
-
let eventBus: EventBus | null = null;
|
|
8537
|
-
try {
|
|
8538
|
-
eventBus = ctx.container.resolve("eventBus") as EventBus;
|
|
8539
|
-
} catch {
|
|
8540
|
-
eventBus = null;
|
|
8541
|
-
}
|
|
8542
|
-
// Persist the line removal and recalculated totals atomically so a
|
|
8543
|
-
// mid-build failure cannot leave a half-updated quote committed (#2336).
|
|
8544
|
-
await withAtomicFlush(
|
|
8545
|
-
em,
|
|
8546
|
-
[
|
|
8547
|
-
async () => {
|
|
8548
|
-
await applyQuoteLineResults({
|
|
8549
|
-
em,
|
|
8550
|
-
quote,
|
|
8551
|
-
calculation,
|
|
8552
|
-
sourceLines: sourceInputs,
|
|
8553
|
-
existingLines,
|
|
8554
|
-
});
|
|
8555
|
-
applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
|
|
8556
|
-
await emitTotalsCalculated(eventBus, {
|
|
8557
|
-
documentKind: "quote",
|
|
8558
|
-
documentId: quote.id,
|
|
8559
|
-
organizationId: quote.organizationId,
|
|
8560
|
-
tenantId: quote.tenantId,
|
|
8561
|
-
customerId: quote.customerEntityId ?? null,
|
|
8562
|
-
totals: calculation.totals,
|
|
8563
|
-
lineCount: calculation.lines.length,
|
|
8564
|
-
});
|
|
8565
|
-
},
|
|
8566
|
-
],
|
|
8567
|
-
{ transaction: true },
|
|
8568
|
-
);
|
|
8569
|
-
return { quoteId: quote.id, lineId: parsed.id };
|
|
8570
|
-
},
|
|
8571
|
-
captureAfter: async (_input, result, ctx) => {
|
|
8572
|
-
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
8573
|
-
return loadQuoteSnapshot(em, result.quoteId);
|
|
8574
8629
|
},
|
|
8630
|
+
captureAfter: async (_input, result, ctx) =>
|
|
8631
|
+
captureLockedQuoteGraphAfter(ctx, result, result.quoteId),
|
|
8575
8632
|
buildLog: async ({ result, snapshots }) => {
|
|
8576
|
-
const before = snapshots.before
|
|
8633
|
+
const before = resolveLockedQuoteGraphBefore(result, snapshots.before);
|
|
8577
8634
|
const after = snapshots.after as QuoteGraphSnapshot | undefined;
|
|
8578
8635
|
if (!after) return null;
|
|
8579
8636
|
const { translate } = await resolveTranslations();
|
|
@@ -8606,266 +8663,221 @@ const orderAdjustmentUpsertCommand: CommandHandler<
|
|
|
8606
8663
|
{ orderId: string; adjustmentId: string }
|
|
8607
8664
|
> = {
|
|
8608
8665
|
id: "sales.orders.adjustments.upsert",
|
|
8609
|
-
async prepare(input
|
|
8610
|
-
|
|
8611
|
-
const orderId = typeof raw.orderId === "string" ? raw.orderId : null;
|
|
8612
|
-
if (!orderId) return {};
|
|
8613
|
-
const em = ctx.container.resolve("em") as EntityManager;
|
|
8614
|
-
const snapshot = await loadOrderSnapshot(em, orderId);
|
|
8615
|
-
if (snapshot)
|
|
8616
|
-
ensureOrderScope(
|
|
8617
|
-
ctx,
|
|
8618
|
-
snapshot.order.organizationId,
|
|
8619
|
-
snapshot.order.tenantId,
|
|
8620
|
-
);
|
|
8621
|
-
return snapshot ? { before: snapshot } : {};
|
|
8666
|
+
async prepare(input) {
|
|
8667
|
+
return markLockedGraphWrite(input);
|
|
8622
8668
|
},
|
|
8623
8669
|
async execute(input, ctx) {
|
|
8624
8670
|
const parsed = orderAdjustmentUpsertSchema.parse(
|
|
8625
8671
|
(input?.body as Record<string, unknown> | undefined) ?? {},
|
|
8626
8672
|
);
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
if (!order)
|
|
8633
|
-
throw notFound("Sales order not found");
|
|
8634
|
-
ensureOrderScope(ctx, order.organizationId, order.tenantId);
|
|
8635
|
-
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
8636
|
-
if (isExternalAmountsMode(order.totalsMode ?? "computed")) await refuseOnExternalOrder();
|
|
8637
|
-
if (parsed.scope === "line") {
|
|
8638
|
-
throw new CrudHttpError(400, {
|
|
8639
|
-
error: "Line-scoped adjustments are not supported yet.",
|
|
8640
|
-
});
|
|
8641
|
-
}
|
|
8642
|
-
|
|
8643
|
-
const [existingLines, existingAdjustments] = await Promise.all([
|
|
8644
|
-
em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
|
|
8645
|
-
em.find(
|
|
8646
|
-
SalesOrderAdjustment,
|
|
8647
|
-
{ order },
|
|
8648
|
-
{ orderBy: { position: "asc" } },
|
|
8649
|
-
),
|
|
8650
|
-
]);
|
|
8651
|
-
const lineSnapshots = existingLines.map(mapOrderLineEntityToSnapshot);
|
|
8652
|
-
const adjustmentDrafts = existingAdjustments.map(mapOrderAdjustmentToDraft);
|
|
8653
|
-
const existingSnapshot = parsed.id
|
|
8654
|
-
? (adjustmentDrafts.find((adj) => adj.id === parsed.id) ?? null)
|
|
8655
|
-
: null;
|
|
8656
|
-
const adjustmentId = parsed.id ?? existingSnapshot?.id ?? randomUUID();
|
|
8657
|
-
let metadata =
|
|
8658
|
-
typeof parsed.metadata === "object" && parsed.metadata
|
|
8659
|
-
? cloneJson(parsed.metadata)
|
|
8660
|
-
: existingSnapshot?.metadata
|
|
8661
|
-
? cloneJson(existingSnapshot.metadata)
|
|
8662
|
-
: null;
|
|
8663
|
-
const calculatorKey =
|
|
8664
|
-
parsed.calculatorKey ?? existingSnapshot?.calculatorKey ?? null;
|
|
8665
|
-
if (
|
|
8666
|
-
parsed.id &&
|
|
8667
|
-
calculatorKey &&
|
|
8668
|
-
(calculatorKey.startsWith("shipping-provider:") ||
|
|
8669
|
-
calculatorKey.startsWith("payment-provider:"))
|
|
8670
|
-
) {
|
|
8671
|
-
metadata = { ...(metadata ?? {}), manualOverride: true };
|
|
8672
|
-
}
|
|
8673
|
-
let nextAdjustments = parsed.id
|
|
8674
|
-
? adjustmentDrafts.map((adj) =>
|
|
8675
|
-
adj.id === parsed.id
|
|
8676
|
-
? {
|
|
8677
|
-
...adj,
|
|
8678
|
-
id: adjustmentId,
|
|
8679
|
-
scope: parsed.scope ?? adj.scope ?? "order",
|
|
8680
|
-
kind: parsed.kind ?? adj.kind ?? "custom",
|
|
8681
|
-
code: parsed.code ?? adj.code ?? null,
|
|
8682
|
-
label: parsed.label ?? adj.label ?? null,
|
|
8683
|
-
calculatorKey:
|
|
8684
|
-
parsed.calculatorKey ?? adj.calculatorKey ?? null,
|
|
8685
|
-
promotionId: parsed.promotionId ?? adj.promotionId ?? null,
|
|
8686
|
-
rate: parsed.rate ?? adj.rate ?? null,
|
|
8687
|
-
amountNet: parsed.amountNet ?? adj.amountNet ?? null,
|
|
8688
|
-
amountGross: parsed.amountGross ?? adj.amountGross ?? null,
|
|
8689
|
-
currencyCode:
|
|
8690
|
-
parsed.currencyCode ?? adj.currencyCode ?? order.currencyCode,
|
|
8691
|
-
metadata,
|
|
8692
|
-
customFields:
|
|
8693
|
-
parsed.customFields !== undefined
|
|
8694
|
-
? parsed.customFields
|
|
8695
|
-
: ((adj as any).customFields ?? null),
|
|
8696
|
-
position:
|
|
8697
|
-
parsed.position ?? adj.position ?? adjustmentDrafts.length,
|
|
8698
|
-
}
|
|
8699
|
-
: adj,
|
|
8700
|
-
)
|
|
8701
|
-
: [
|
|
8702
|
-
...adjustmentDrafts,
|
|
8703
|
-
{
|
|
8704
|
-
id: adjustmentId,
|
|
8705
|
-
scope: parsed.scope ?? "order",
|
|
8706
|
-
kind: parsed.kind ?? "custom",
|
|
8707
|
-
code: parsed.code ?? null,
|
|
8708
|
-
label: parsed.label ?? null,
|
|
8709
|
-
calculatorKey: parsed.calculatorKey ?? null,
|
|
8710
|
-
promotionId: parsed.promotionId ?? null,
|
|
8711
|
-
rate: parsed.rate ?? null,
|
|
8712
|
-
amountNet: parsed.amountNet ?? null,
|
|
8713
|
-
amountGross: parsed.amountGross ?? null,
|
|
8714
|
-
currencyCode: parsed.currencyCode ?? order.currencyCode,
|
|
8715
|
-
metadata,
|
|
8716
|
-
customFields: parsed.customFields ?? null,
|
|
8717
|
-
position: parsed.position ?? adjustmentDrafts.length,
|
|
8718
|
-
},
|
|
8719
|
-
];
|
|
8720
|
-
|
|
8721
|
-
nextAdjustments = nextAdjustments
|
|
8722
|
-
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
|
|
8723
|
-
.map((adj, index) => ({ ...adj, position: adj.position ?? index }));
|
|
8724
|
-
|
|
8725
|
-
const sourceLines = lineSnapshots.map((line, index) => ({
|
|
8726
|
-
...line,
|
|
8727
|
-
statusEntryId: (line as any).statusEntryId ?? null,
|
|
8728
|
-
catalogSnapshot: (line as any).catalogSnapshot ?? null,
|
|
8729
|
-
promotionSnapshot: (line as any).promotionSnapshot ?? null,
|
|
8730
|
-
organizationId: order.organizationId,
|
|
8731
|
-
tenantId: order.tenantId,
|
|
8732
|
-
orderId: order.id,
|
|
8733
|
-
lineNumber: line.lineNumber ?? index + 1,
|
|
8734
|
-
}));
|
|
8735
|
-
const calcLines = sourceLines.map((line, index) =>
|
|
8736
|
-
createLineSnapshotFromInput(line, line.lineNumber ?? index + 1),
|
|
8737
|
-
);
|
|
8738
|
-
const salesCalculationService =
|
|
8739
|
-
ctx.container.resolve<SalesCalculationService>("salesCalculationService");
|
|
8740
|
-
const calculationContext = buildCalculationContext({
|
|
8741
|
-
tenantId: order.tenantId,
|
|
8742
|
-
organizationId: order.organizationId,
|
|
8743
|
-
currencyCode: order.currencyCode,
|
|
8744
|
-
shippingSnapshot: order.shippingMethodSnapshot,
|
|
8745
|
-
paymentSnapshot: order.paymentMethodSnapshot,
|
|
8746
|
-
shippingMethodId: order.shippingMethodId ?? null,
|
|
8747
|
-
paymentMethodId: order.paymentMethodId ?? null,
|
|
8748
|
-
shippingMethodCode: order.shippingMethodCode ?? null,
|
|
8749
|
-
paymentMethodCode: order.paymentMethodCode ?? null,
|
|
8750
|
-
});
|
|
8751
|
-
const effectiveAdjustment = nextAdjustments.find(
|
|
8752
|
-
(adj) => adj.id === adjustmentId,
|
|
8753
|
-
);
|
|
8754
|
-
if (effectiveAdjustment?.kind === "return") {
|
|
8755
|
-
const baselineAdjustments = nextAdjustments.filter(
|
|
8756
|
-
(adj) => adj.id !== adjustmentId,
|
|
8757
|
-
);
|
|
8758
|
-
const baselineCalculation =
|
|
8759
|
-
await salesCalculationService.calculateDocumentTotals({
|
|
8760
|
-
documentKind: "order",
|
|
8761
|
-
lines: calcLines,
|
|
8762
|
-
adjustments: baselineAdjustments,
|
|
8763
|
-
context: calculationContext,
|
|
8764
|
-
existingTotals: resolveExistingPaymentTotals(order),
|
|
8673
|
+
return writeLockedOrderGraph(input, ctx, parsed.orderId, "Sales order not found", async (em, order) => {
|
|
8674
|
+
if (isExternalAmountsMode(order.totalsMode ?? "computed")) await refuseOnExternalOrder();
|
|
8675
|
+
if (parsed.scope === "line") {
|
|
8676
|
+
throw new CrudHttpError(400, {
|
|
8677
|
+
error: "Line-scoped adjustments are not supported yet.",
|
|
8765
8678
|
});
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
),
|
|
8775
|
-
remainingGross: Number(
|
|
8776
|
-
baselineCalculation.totals?.grandTotalGrossAmount ?? 0,
|
|
8679
|
+
}
|
|
8680
|
+
|
|
8681
|
+
const [existingLines, existingAdjustments] = await Promise.all([
|
|
8682
|
+
em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
|
|
8683
|
+
em.find(
|
|
8684
|
+
SalesOrderAdjustment,
|
|
8685
|
+
{ order },
|
|
8686
|
+
{ orderBy: { position: "asc" } },
|
|
8777
8687
|
),
|
|
8688
|
+
]);
|
|
8689
|
+
const lineSnapshots = existingLines.map(mapOrderLineEntityToSnapshot);
|
|
8690
|
+
const adjustmentDrafts = existingAdjustments.map(mapOrderAdjustmentToDraft);
|
|
8691
|
+
const existingSnapshot = parsed.id
|
|
8692
|
+
? (adjustmentDrafts.find((adj) => adj.id === parsed.id) ?? null)
|
|
8693
|
+
: null;
|
|
8694
|
+
const adjustmentId = parsed.id ?? existingSnapshot?.id ?? randomUUID();
|
|
8695
|
+
let metadata =
|
|
8696
|
+
typeof parsed.metadata === "object" && parsed.metadata
|
|
8697
|
+
? cloneJson(parsed.metadata)
|
|
8698
|
+
: existingSnapshot?.metadata
|
|
8699
|
+
? cloneJson(existingSnapshot.metadata)
|
|
8700
|
+
: null;
|
|
8701
|
+
const calculatorKey =
|
|
8702
|
+
parsed.calculatorKey ?? existingSnapshot?.calculatorKey ?? null;
|
|
8703
|
+
if (
|
|
8704
|
+
parsed.id &&
|
|
8705
|
+
calculatorKey &&
|
|
8706
|
+
(calculatorKey.startsWith("shipping-provider:") ||
|
|
8707
|
+
calculatorKey.startsWith("payment-provider:"))
|
|
8708
|
+
) {
|
|
8709
|
+
metadata = { ...(metadata ?? {}), manualOverride: true };
|
|
8710
|
+
}
|
|
8711
|
+
let nextAdjustments = parsed.id
|
|
8712
|
+
? adjustmentDrafts.map((adj) =>
|
|
8713
|
+
adj.id === parsed.id
|
|
8714
|
+
? {
|
|
8715
|
+
...adj,
|
|
8716
|
+
id: adjustmentId,
|
|
8717
|
+
scope: parsed.scope ?? adj.scope ?? "order",
|
|
8718
|
+
kind: parsed.kind ?? adj.kind ?? "custom",
|
|
8719
|
+
code: parsed.code ?? adj.code ?? null,
|
|
8720
|
+
label: parsed.label ?? adj.label ?? null,
|
|
8721
|
+
calculatorKey:
|
|
8722
|
+
parsed.calculatorKey ?? adj.calculatorKey ?? null,
|
|
8723
|
+
promotionId: parsed.promotionId ?? adj.promotionId ?? null,
|
|
8724
|
+
rate: parsed.rate ?? adj.rate ?? null,
|
|
8725
|
+
amountNet: parsed.amountNet ?? adj.amountNet ?? null,
|
|
8726
|
+
amountGross: parsed.amountGross ?? adj.amountGross ?? null,
|
|
8727
|
+
currencyCode:
|
|
8728
|
+
parsed.currencyCode ?? adj.currencyCode ?? order.currencyCode,
|
|
8729
|
+
metadata,
|
|
8730
|
+
customFields:
|
|
8731
|
+
parsed.customFields !== undefined
|
|
8732
|
+
? parsed.customFields
|
|
8733
|
+
: ((adj as any).customFields ?? null),
|
|
8734
|
+
position:
|
|
8735
|
+
parsed.position ?? adj.position ?? adjustmentDrafts.length,
|
|
8736
|
+
}
|
|
8737
|
+
: adj,
|
|
8738
|
+
)
|
|
8739
|
+
: [
|
|
8740
|
+
...adjustmentDrafts,
|
|
8741
|
+
{
|
|
8742
|
+
id: adjustmentId,
|
|
8743
|
+
scope: parsed.scope ?? "order",
|
|
8744
|
+
kind: parsed.kind ?? "custom",
|
|
8745
|
+
code: parsed.code ?? null,
|
|
8746
|
+
label: parsed.label ?? null,
|
|
8747
|
+
calculatorKey: parsed.calculatorKey ?? null,
|
|
8748
|
+
promotionId: parsed.promotionId ?? null,
|
|
8749
|
+
rate: parsed.rate ?? null,
|
|
8750
|
+
amountNet: parsed.amountNet ?? null,
|
|
8751
|
+
amountGross: parsed.amountGross ?? null,
|
|
8752
|
+
currencyCode: parsed.currencyCode ?? order.currencyCode,
|
|
8753
|
+
metadata,
|
|
8754
|
+
customFields: parsed.customFields ?? null,
|
|
8755
|
+
position: parsed.position ?? adjustmentDrafts.length,
|
|
8756
|
+
},
|
|
8757
|
+
];
|
|
8758
|
+
|
|
8759
|
+
nextAdjustments = nextAdjustments
|
|
8760
|
+
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
|
|
8761
|
+
.map((adj, index) => ({ ...adj, position: adj.position ?? index }));
|
|
8762
|
+
|
|
8763
|
+
const sourceLines = lineSnapshots.map((line, index) => ({
|
|
8764
|
+
...line,
|
|
8765
|
+
statusEntryId: (line as any).statusEntryId ?? null,
|
|
8766
|
+
catalogSnapshot: (line as any).catalogSnapshot ?? null,
|
|
8767
|
+
promotionSnapshot: (line as any).promotionSnapshot ?? null,
|
|
8768
|
+
organizationId: order.organizationId,
|
|
8769
|
+
tenantId: order.tenantId,
|
|
8770
|
+
orderId: order.id,
|
|
8771
|
+
lineNumber: line.lineNumber ?? index + 1,
|
|
8772
|
+
}));
|
|
8773
|
+
const calcLines = sourceLines.map((line, index) =>
|
|
8774
|
+
createLineSnapshotFromInput(line, line.lineNumber ?? index + 1),
|
|
8775
|
+
);
|
|
8776
|
+
const salesCalculationService =
|
|
8777
|
+
ctx.container.resolve<SalesCalculationService>("salesCalculationService");
|
|
8778
|
+
const calculationContext = buildCalculationContext({
|
|
8779
|
+
tenantId: order.tenantId,
|
|
8780
|
+
organizationId: order.organizationId,
|
|
8781
|
+
currencyCode: order.currencyCode,
|
|
8782
|
+
shippingSnapshot: order.shippingMethodSnapshot,
|
|
8783
|
+
paymentSnapshot: order.paymentMethodSnapshot,
|
|
8784
|
+
shippingMethodId: order.shippingMethodId ?? null,
|
|
8785
|
+
paymentMethodId: order.paymentMethodId ?? null,
|
|
8786
|
+
shippingMethodCode: order.shippingMethodCode ?? null,
|
|
8787
|
+
paymentMethodCode: order.paymentMethodCode ?? null,
|
|
8778
8788
|
});
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
const
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
|
|
8789
|
+
const effectiveAdjustment = nextAdjustments.find(
|
|
8790
|
+
(adj) => adj.id === adjustmentId,
|
|
8791
|
+
);
|
|
8792
|
+
if (effectiveAdjustment?.kind === "return") {
|
|
8793
|
+
const baselineAdjustments = nextAdjustments.filter(
|
|
8794
|
+
(adj) => adj.id !== adjustmentId,
|
|
8795
|
+
);
|
|
8796
|
+
const baselineCalculation =
|
|
8797
|
+
await salesCalculationService.calculateDocumentTotals({
|
|
8798
|
+
documentKind: "order",
|
|
8799
|
+
lines: calcLines,
|
|
8800
|
+
adjustments: baselineAdjustments,
|
|
8801
|
+
context: calculationContext,
|
|
8802
|
+
existingTotals: resolveExistingPaymentTotals(order),
|
|
8803
|
+
});
|
|
8804
|
+
const issues = validateReturnAdjustmentWithinRemaining({
|
|
8805
|
+
kind: "return",
|
|
8806
|
+
amountNet:
|
|
8807
|
+
effectiveAdjustment.amountNet ?? effectiveAdjustment.amountGross ?? 0,
|
|
8808
|
+
amountGross:
|
|
8809
|
+
effectiveAdjustment.amountGross ?? effectiveAdjustment.amountNet ?? 0,
|
|
8810
|
+
remainingNet: Number(
|
|
8811
|
+
baselineCalculation.totals?.grandTotalNetAmount ?? 0,
|
|
8812
|
+
),
|
|
8813
|
+
remainingGross: Number(
|
|
8814
|
+
baselineCalculation.totals?.grandTotalGrossAmount ?? 0,
|
|
8815
|
+
),
|
|
8816
|
+
});
|
|
8817
|
+
if (issues.length > 0) {
|
|
8818
|
+
const { translate } = await resolveTranslations();
|
|
8819
|
+
const grossIssue = issues.find((issue) => issue.path === "amountGross");
|
|
8820
|
+
const netIssue = issues.find((issue) => issue.path === "amountNet");
|
|
8821
|
+
const fieldErrors: Record<string, string> = {};
|
|
8822
|
+
if (grossIssue) {
|
|
8823
|
+
fieldErrors.amountGross = translate(
|
|
8801
8824
|
"sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
|
|
8802
8825
|
RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE,
|
|
8803
|
-
)
|
|
8804
|
-
|
|
8805
|
-
|
|
8826
|
+
);
|
|
8827
|
+
}
|
|
8828
|
+
if (netIssue) {
|
|
8829
|
+
fieldErrors.amountNet = translate(
|
|
8830
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalNet",
|
|
8831
|
+
RETURN_ADJUSTMENT_EXCEEDS_REMAINING_NET_MESSAGE,
|
|
8832
|
+
);
|
|
8833
|
+
}
|
|
8834
|
+
throw new CrudHttpError(400, {
|
|
8835
|
+
error:
|
|
8836
|
+
fieldErrors.amountGross ??
|
|
8837
|
+
fieldErrors.amountNet ??
|
|
8838
|
+
translate(
|
|
8839
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
|
|
8840
|
+
RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE,
|
|
8841
|
+
),
|
|
8842
|
+
fieldErrors,
|
|
8843
|
+
});
|
|
8844
|
+
}
|
|
8806
8845
|
}
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8846
|
+
const calculation = await salesCalculationService.calculateDocumentTotals({
|
|
8847
|
+
documentKind: "order",
|
|
8848
|
+
lines: calcLines,
|
|
8849
|
+
adjustments: nextAdjustments,
|
|
8850
|
+
context: calculationContext,
|
|
8851
|
+
existingTotals: resolveExistingPaymentTotals(order),
|
|
8852
|
+
});
|
|
8853
|
+
const adjustmentInputs = nextAdjustments.map((adj, index) => ({
|
|
8854
|
+
organizationId: order.organizationId,
|
|
8855
|
+
tenantId: order.tenantId,
|
|
8856
|
+
orderId: order.id,
|
|
8857
|
+
scope: adj.scope ?? "order",
|
|
8858
|
+
kind: adj.kind ?? "custom",
|
|
8859
|
+
code: adj.code ?? undefined,
|
|
8860
|
+
label: adj.label ?? undefined,
|
|
8861
|
+
calculatorKey: adj.calculatorKey ?? undefined,
|
|
8862
|
+
promotionId: adj.promotionId ?? undefined,
|
|
8863
|
+
rate: adj.rate ?? undefined,
|
|
8864
|
+
amountNet: adj.amountNet ?? undefined,
|
|
8865
|
+
amountGross: adj.amountGross ?? undefined,
|
|
8866
|
+
currencyCode: adj.currencyCode ?? order.currencyCode,
|
|
8867
|
+
metadata: adj.metadata ?? undefined,
|
|
8868
|
+
customFields: (adj as any).customFields ?? undefined,
|
|
8869
|
+
position: adj.position ?? index,
|
|
8870
|
+
}));
|
|
8871
|
+
await replaceOrderAdjustments(em, order, calculation, adjustmentInputs);
|
|
8872
|
+
applyOrderTotals(order, calculation.totals, calculation.lines.length);
|
|
8873
|
+
order.updatedAt = new Date();
|
|
8874
|
+
return { result: { orderId: order.id, adjustmentId }, calculation };
|
|
8814
8875
|
});
|
|
8815
|
-
const adjustmentInputs = nextAdjustments.map((adj, index) => ({
|
|
8816
|
-
organizationId: order.organizationId,
|
|
8817
|
-
tenantId: order.tenantId,
|
|
8818
|
-
orderId: order.id,
|
|
8819
|
-
scope: adj.scope ?? "order",
|
|
8820
|
-
kind: adj.kind ?? "custom",
|
|
8821
|
-
code: adj.code ?? undefined,
|
|
8822
|
-
label: adj.label ?? undefined,
|
|
8823
|
-
calculatorKey: adj.calculatorKey ?? undefined,
|
|
8824
|
-
promotionId: adj.promotionId ?? undefined,
|
|
8825
|
-
rate: adj.rate ?? undefined,
|
|
8826
|
-
amountNet: adj.amountNet ?? undefined,
|
|
8827
|
-
amountGross: adj.amountGross ?? undefined,
|
|
8828
|
-
currencyCode: adj.currencyCode ?? order.currencyCode,
|
|
8829
|
-
metadata: adj.metadata ?? undefined,
|
|
8830
|
-
customFields: (adj as any).customFields ?? undefined,
|
|
8831
|
-
position: adj.position ?? index,
|
|
8832
|
-
}));
|
|
8833
|
-
let eventBus: EventBus | null = null;
|
|
8834
|
-
try {
|
|
8835
|
-
eventBus = ctx.container.resolve("eventBus") as EventBus;
|
|
8836
|
-
} catch {
|
|
8837
|
-
eventBus = null;
|
|
8838
|
-
}
|
|
8839
|
-
// Persist the adjustment change and recalculated totals atomically so a
|
|
8840
|
-
// mid-build failure cannot leave a half-updated order committed (#2336).
|
|
8841
|
-
await withAtomicFlush(
|
|
8842
|
-
em,
|
|
8843
|
-
[
|
|
8844
|
-
async () => {
|
|
8845
|
-
await replaceOrderAdjustments(em, order, calculation, adjustmentInputs);
|
|
8846
|
-
applyOrderTotals(order, calculation.totals, calculation.lines.length);
|
|
8847
|
-
order.updatedAt = new Date();
|
|
8848
|
-
await emitTotalsCalculated(eventBus, {
|
|
8849
|
-
documentKind: "order",
|
|
8850
|
-
documentId: order.id,
|
|
8851
|
-
organizationId: order.organizationId,
|
|
8852
|
-
tenantId: order.tenantId,
|
|
8853
|
-
customerId: order.customerEntityId ?? null,
|
|
8854
|
-
totals: calculation.totals,
|
|
8855
|
-
lineCount: calculation.lines.length,
|
|
8856
|
-
});
|
|
8857
|
-
},
|
|
8858
|
-
],
|
|
8859
|
-
{ transaction: true },
|
|
8860
|
-
);
|
|
8861
|
-
return { orderId: order.id, adjustmentId };
|
|
8862
|
-
},
|
|
8863
|
-
captureAfter: async (_input, result, ctx) => {
|
|
8864
|
-
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
8865
|
-
return loadOrderSnapshot(em, result.orderId);
|
|
8866
8876
|
},
|
|
8877
|
+
captureAfter: async (_input, result, ctx) =>
|
|
8878
|
+
captureLockedOrderGraphAfter(ctx, result, result.orderId),
|
|
8867
8879
|
buildLog: async ({ snapshots, result }) => {
|
|
8868
|
-
const before = snapshots.before
|
|
8880
|
+
const before = resolveLockedOrderGraphBefore(result, snapshots.before);
|
|
8869
8881
|
const after = snapshots.after as OrderGraphSnapshot | undefined;
|
|
8870
8882
|
if (!after) return null;
|
|
8871
8883
|
const { translate } = await resolveTranslations();
|
|
@@ -8898,137 +8910,91 @@ const orderAdjustmentDeleteCommand: CommandHandler<
|
|
|
8898
8910
|
{ orderId: string; adjustmentId: string }
|
|
8899
8911
|
> = {
|
|
8900
8912
|
id: "sales.orders.adjustments.delete",
|
|
8901
|
-
async prepare(input
|
|
8902
|
-
|
|
8903
|
-
const orderId = typeof raw.orderId === "string" ? raw.orderId : null;
|
|
8904
|
-
if (!orderId) return {};
|
|
8905
|
-
const em = ctx.container.resolve("em") as EntityManager;
|
|
8906
|
-
const snapshot = await loadOrderSnapshot(em, orderId);
|
|
8907
|
-
if (snapshot)
|
|
8908
|
-
ensureOrderScope(
|
|
8909
|
-
ctx,
|
|
8910
|
-
snapshot.order.organizationId,
|
|
8911
|
-
snapshot.order.tenantId,
|
|
8912
|
-
);
|
|
8913
|
-
return snapshot ? { before: snapshot } : {};
|
|
8913
|
+
async prepare(input) {
|
|
8914
|
+
return markLockedGraphWrite(input);
|
|
8914
8915
|
},
|
|
8915
8916
|
async execute(input, ctx) {
|
|
8916
8917
|
const parsed = orderAdjustmentDeleteSchema.parse(
|
|
8917
8918
|
(input?.body as Record<string, unknown> | undefined) ?? {},
|
|
8918
8919
|
);
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
currencyCode: adj.currencyCode ?? order.currencyCode,
|
|
8993
|
-
metadata: adj.metadata ?? undefined,
|
|
8994
|
-
position: adj.position ?? index,
|
|
8995
|
-
}));
|
|
8996
|
-
let eventBus: EventBus | null = null;
|
|
8997
|
-
try {
|
|
8998
|
-
eventBus = ctx.container.resolve("eventBus") as EventBus;
|
|
8999
|
-
} catch {
|
|
9000
|
-
eventBus = null;
|
|
9001
|
-
}
|
|
9002
|
-
// Persist the adjustment removal and recalculated totals atomically so a
|
|
9003
|
-
// mid-build failure cannot leave a half-updated order committed (#2336).
|
|
9004
|
-
await withAtomicFlush(
|
|
9005
|
-
em,
|
|
9006
|
-
[
|
|
9007
|
-
async () => {
|
|
9008
|
-
await replaceOrderAdjustments(em, order, calculation, adjustmentInputs);
|
|
9009
|
-
applyOrderTotals(order, calculation.totals, calculation.lines.length);
|
|
9010
|
-
order.updatedAt = new Date();
|
|
9011
|
-
await emitTotalsCalculated(eventBus, {
|
|
9012
|
-
documentKind: "order",
|
|
9013
|
-
documentId: order.id,
|
|
9014
|
-
organizationId: order.organizationId,
|
|
9015
|
-
tenantId: order.tenantId,
|
|
9016
|
-
customerId: order.customerEntityId ?? null,
|
|
9017
|
-
totals: calculation.totals,
|
|
9018
|
-
lineCount: calculation.lines.length,
|
|
9019
|
-
});
|
|
9020
|
-
},
|
|
9021
|
-
],
|
|
9022
|
-
{ transaction: true },
|
|
9023
|
-
);
|
|
9024
|
-
return { orderId: order.id, adjustmentId: parsed.id };
|
|
9025
|
-
},
|
|
9026
|
-
captureAfter: async (_input, result, ctx) => {
|
|
9027
|
-
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
9028
|
-
return loadOrderSnapshot(em, result.orderId);
|
|
8920
|
+
return writeLockedOrderGraph(input, ctx, parsed.orderId, "Sales order not found", async (em, order) => {
|
|
8921
|
+
if (isExternalAmountsMode(order.totalsMode ?? "computed")) await refuseOnExternalOrder();
|
|
8922
|
+
const [existingLines, adjustments] = await Promise.all([
|
|
8923
|
+
em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
|
|
8924
|
+
em.find(
|
|
8925
|
+
SalesOrderAdjustment,
|
|
8926
|
+
{ order },
|
|
8927
|
+
{ orderBy: { position: "asc" } },
|
|
8928
|
+
),
|
|
8929
|
+
]);
|
|
8930
|
+
const filtered = adjustments.filter((adj) => adj.id !== parsed.id);
|
|
8931
|
+
if (filtered.length === adjustments.length) {
|
|
8932
|
+
throw notFound("Adjustment not found");
|
|
8933
|
+
}
|
|
8934
|
+
const lineSnapshots = existingLines.map(mapOrderLineEntityToSnapshot);
|
|
8935
|
+
const calcLines = lineSnapshots.map((line, index) =>
|
|
8936
|
+
createLineSnapshotFromInput(
|
|
8937
|
+
{
|
|
8938
|
+
...line,
|
|
8939
|
+
organizationId: order.organizationId,
|
|
8940
|
+
tenantId: order.tenantId,
|
|
8941
|
+
orderId: order.id,
|
|
8942
|
+
lineNumber: line.lineNumber ?? index + 1,
|
|
8943
|
+
statusEntryId: (line as any).statusEntryId ?? null,
|
|
8944
|
+
catalogSnapshot: (line as any).catalogSnapshot ?? null,
|
|
8945
|
+
promotionSnapshot: (line as any).promotionSnapshot ?? null,
|
|
8946
|
+
},
|
|
8947
|
+
line.lineNumber ?? index + 1,
|
|
8948
|
+
),
|
|
8949
|
+
);
|
|
8950
|
+
const adjustmentDrafts = filtered.map(mapOrderAdjustmentToDraft);
|
|
8951
|
+
const salesCalculationService =
|
|
8952
|
+
ctx.container.resolve<SalesCalculationService>("salesCalculationService");
|
|
8953
|
+
const calculationContext = buildCalculationContext({
|
|
8954
|
+
tenantId: order.tenantId,
|
|
8955
|
+
organizationId: order.organizationId,
|
|
8956
|
+
currencyCode: order.currencyCode,
|
|
8957
|
+
shippingSnapshot: order.shippingMethodSnapshot,
|
|
8958
|
+
paymentSnapshot: order.paymentMethodSnapshot,
|
|
8959
|
+
shippingMethodId: order.shippingMethodId ?? null,
|
|
8960
|
+
paymentMethodId: order.paymentMethodId ?? null,
|
|
8961
|
+
shippingMethodCode: order.shippingMethodCode ?? null,
|
|
8962
|
+
paymentMethodCode: order.paymentMethodCode ?? null,
|
|
8963
|
+
});
|
|
8964
|
+
const calculation = await salesCalculationService.calculateDocumentTotals({
|
|
8965
|
+
documentKind: "order",
|
|
8966
|
+
lines: calcLines,
|
|
8967
|
+
adjustments: adjustmentDrafts,
|
|
8968
|
+
context: calculationContext,
|
|
8969
|
+
existingTotals: resolveExistingPaymentTotals(order),
|
|
8970
|
+
});
|
|
8971
|
+
const adjustmentInputs = adjustmentDrafts.map((adj, index) => ({
|
|
8972
|
+
organizationId: order.organizationId,
|
|
8973
|
+
tenantId: order.tenantId,
|
|
8974
|
+
orderId: order.id,
|
|
8975
|
+
scope: adj.scope ?? "order",
|
|
8976
|
+
kind: adj.kind ?? "custom",
|
|
8977
|
+
code: adj.code ?? undefined,
|
|
8978
|
+
label: adj.label ?? undefined,
|
|
8979
|
+
calculatorKey: adj.calculatorKey ?? undefined,
|
|
8980
|
+
promotionId: adj.promotionId ?? undefined,
|
|
8981
|
+
rate: adj.rate ?? undefined,
|
|
8982
|
+
amountNet: adj.amountNet ?? undefined,
|
|
8983
|
+
amountGross: adj.amountGross ?? undefined,
|
|
8984
|
+
currencyCode: adj.currencyCode ?? order.currencyCode,
|
|
8985
|
+
metadata: adj.metadata ?? undefined,
|
|
8986
|
+
position: adj.position ?? index,
|
|
8987
|
+
}));
|
|
8988
|
+
await replaceOrderAdjustments(em, order, calculation, adjustmentInputs);
|
|
8989
|
+
applyOrderTotals(order, calculation.totals, calculation.lines.length);
|
|
8990
|
+
order.updatedAt = new Date();
|
|
8991
|
+
return { result: { orderId: order.id, adjustmentId: parsed.id }, calculation };
|
|
8992
|
+
});
|
|
9029
8993
|
},
|
|
8994
|
+
captureAfter: async (_input, result, ctx) =>
|
|
8995
|
+
captureLockedOrderGraphAfter(ctx, result, result.orderId),
|
|
9030
8996
|
buildLog: async ({ snapshots, result }) => {
|
|
9031
|
-
const before = snapshots.before
|
|
8997
|
+
const before = resolveLockedOrderGraphBefore(result, snapshots.before);
|
|
9032
8998
|
const after = snapshots.after as OrderGraphSnapshot | undefined;
|
|
9033
8999
|
if (!after) return null;
|
|
9034
9000
|
const { translate } = await resolveTranslations();
|
|
@@ -9061,263 +9027,218 @@ const quoteAdjustmentUpsertCommand: CommandHandler<
|
|
|
9061
9027
|
{ quoteId: string; adjustmentId: string }
|
|
9062
9028
|
> = {
|
|
9063
9029
|
id: "sales.quotes.adjustments.upsert",
|
|
9064
|
-
async prepare(input
|
|
9065
|
-
|
|
9066
|
-
const quoteId = typeof raw.quoteId === "string" ? raw.quoteId : null;
|
|
9067
|
-
if (!quoteId) return {};
|
|
9068
|
-
const em = ctx.container.resolve("em") as EntityManager;
|
|
9069
|
-
const snapshot = await loadQuoteSnapshot(em, quoteId);
|
|
9070
|
-
if (snapshot)
|
|
9071
|
-
ensureQuoteScope(
|
|
9072
|
-
ctx,
|
|
9073
|
-
snapshot.quote.organizationId,
|
|
9074
|
-
snapshot.quote.tenantId,
|
|
9075
|
-
);
|
|
9076
|
-
return snapshot ? { before: snapshot } : {};
|
|
9030
|
+
async prepare(input) {
|
|
9031
|
+
return markLockedGraphWrite(input);
|
|
9077
9032
|
},
|
|
9078
9033
|
async execute(input, ctx) {
|
|
9079
9034
|
const parsed = quoteAdjustmentUpsertSchema.parse(
|
|
9080
9035
|
(input?.body as Record<string, unknown> | undefined) ?? {},
|
|
9081
9036
|
);
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
});
|
|
9087
|
-
if (!quote)
|
|
9088
|
-
throw notFound("Sales quote not found");
|
|
9089
|
-
ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
|
|
9090
|
-
await enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
9091
|
-
if (parsed.scope === "line") {
|
|
9092
|
-
throw new CrudHttpError(400, {
|
|
9093
|
-
error: "Line-scoped adjustments are not supported yet.",
|
|
9094
|
-
});
|
|
9095
|
-
}
|
|
9096
|
-
|
|
9097
|
-
const [existingLines, existingAdjustments] = await Promise.all([
|
|
9098
|
-
em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
|
|
9099
|
-
em.find(
|
|
9100
|
-
SalesQuoteAdjustment,
|
|
9101
|
-
{ quote },
|
|
9102
|
-
{ orderBy: { position: "asc" } },
|
|
9103
|
-
),
|
|
9104
|
-
]);
|
|
9105
|
-
const lineSnapshots = existingLines.map(mapQuoteLineEntityToSnapshot);
|
|
9106
|
-
const adjustmentDrafts = existingAdjustments.map(mapQuoteAdjustmentToDraft);
|
|
9107
|
-
const existingSnapshot = parsed.id
|
|
9108
|
-
? (adjustmentDrafts.find((adj) => adj.id === parsed.id) ?? null)
|
|
9109
|
-
: null;
|
|
9110
|
-
const adjustmentId = parsed.id ?? existingSnapshot?.id ?? randomUUID();
|
|
9111
|
-
let metadata =
|
|
9112
|
-
typeof parsed.metadata === "object" && parsed.metadata
|
|
9113
|
-
? cloneJson(parsed.metadata)
|
|
9114
|
-
: existingSnapshot?.metadata
|
|
9115
|
-
? cloneJson(existingSnapshot.metadata)
|
|
9116
|
-
: null;
|
|
9117
|
-
const calculatorKey =
|
|
9118
|
-
parsed.calculatorKey ?? existingSnapshot?.calculatorKey ?? null;
|
|
9119
|
-
if (
|
|
9120
|
-
parsed.id &&
|
|
9121
|
-
calculatorKey &&
|
|
9122
|
-
(calculatorKey.startsWith("shipping-provider:") ||
|
|
9123
|
-
calculatorKey.startsWith("payment-provider:"))
|
|
9124
|
-
) {
|
|
9125
|
-
metadata = { ...(metadata ?? {}), manualOverride: true };
|
|
9126
|
-
}
|
|
9127
|
-
let nextAdjustments = parsed.id
|
|
9128
|
-
? adjustmentDrafts.map((adj) =>
|
|
9129
|
-
adj.id === parsed.id
|
|
9130
|
-
? {
|
|
9131
|
-
...adj,
|
|
9132
|
-
id: adjustmentId,
|
|
9133
|
-
scope: parsed.scope ?? adj.scope ?? "order",
|
|
9134
|
-
kind: parsed.kind ?? adj.kind ?? "custom",
|
|
9135
|
-
code: parsed.code ?? adj.code ?? null,
|
|
9136
|
-
label: parsed.label ?? adj.label ?? null,
|
|
9137
|
-
calculatorKey:
|
|
9138
|
-
parsed.calculatorKey ?? adj.calculatorKey ?? null,
|
|
9139
|
-
promotionId: parsed.promotionId ?? adj.promotionId ?? null,
|
|
9140
|
-
rate: parsed.rate ?? adj.rate ?? null,
|
|
9141
|
-
amountNet: parsed.amountNet ?? adj.amountNet ?? null,
|
|
9142
|
-
amountGross: parsed.amountGross ?? adj.amountGross ?? null,
|
|
9143
|
-
currencyCode:
|
|
9144
|
-
parsed.currencyCode ?? adj.currencyCode ?? quote.currencyCode,
|
|
9145
|
-
metadata,
|
|
9146
|
-
customFields:
|
|
9147
|
-
parsed.customFields !== undefined
|
|
9148
|
-
? parsed.customFields
|
|
9149
|
-
: ((adj as any).customFields ?? null),
|
|
9150
|
-
position:
|
|
9151
|
-
parsed.position ?? adj.position ?? adjustmentDrafts.length,
|
|
9152
|
-
}
|
|
9153
|
-
: adj,
|
|
9154
|
-
)
|
|
9155
|
-
: [
|
|
9156
|
-
...adjustmentDrafts,
|
|
9157
|
-
{
|
|
9158
|
-
id: adjustmentId,
|
|
9159
|
-
scope: parsed.scope ?? "order",
|
|
9160
|
-
kind: parsed.kind ?? "custom",
|
|
9161
|
-
code: parsed.code ?? null,
|
|
9162
|
-
label: parsed.label ?? null,
|
|
9163
|
-
calculatorKey: parsed.calculatorKey ?? null,
|
|
9164
|
-
promotionId: parsed.promotionId ?? null,
|
|
9165
|
-
rate: parsed.rate ?? null,
|
|
9166
|
-
amountNet: parsed.amountNet ?? null,
|
|
9167
|
-
amountGross: parsed.amountGross ?? null,
|
|
9168
|
-
currencyCode: parsed.currencyCode ?? quote.currencyCode,
|
|
9169
|
-
metadata,
|
|
9170
|
-
customFields: parsed.customFields ?? null,
|
|
9171
|
-
position: parsed.position ?? adjustmentDrafts.length,
|
|
9172
|
-
},
|
|
9173
|
-
];
|
|
9174
|
-
|
|
9175
|
-
nextAdjustments = nextAdjustments
|
|
9176
|
-
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
|
|
9177
|
-
.map((adj, index) => ({ ...adj, position: adj.position ?? index }));
|
|
9178
|
-
|
|
9179
|
-
const sourceLines = lineSnapshots.map((line, index) => ({
|
|
9180
|
-
...line,
|
|
9181
|
-
statusEntryId: (line as any).statusEntryId ?? null,
|
|
9182
|
-
catalogSnapshot: (line as any).catalogSnapshot ?? null,
|
|
9183
|
-
promotionSnapshot: (line as any).promotionSnapshot ?? null,
|
|
9184
|
-
organizationId: quote.organizationId,
|
|
9185
|
-
tenantId: quote.tenantId,
|
|
9186
|
-
quoteId: quote.id,
|
|
9187
|
-
lineNumber: line.lineNumber ?? index + 1,
|
|
9188
|
-
}));
|
|
9189
|
-
const calcLines = sourceLines.map((line, index) =>
|
|
9190
|
-
createLineSnapshotFromInput(line, line.lineNumber ?? index + 1),
|
|
9191
|
-
);
|
|
9192
|
-
const salesCalculationService =
|
|
9193
|
-
ctx.container.resolve<SalesCalculationService>("salesCalculationService");
|
|
9194
|
-
const calculationContext = buildCalculationContext({
|
|
9195
|
-
tenantId: quote.tenantId,
|
|
9196
|
-
organizationId: quote.organizationId,
|
|
9197
|
-
currencyCode: quote.currencyCode,
|
|
9198
|
-
shippingSnapshot: quote.shippingMethodSnapshot,
|
|
9199
|
-
paymentSnapshot: quote.paymentMethodSnapshot,
|
|
9200
|
-
shippingMethodId: quote.shippingMethodId ?? null,
|
|
9201
|
-
paymentMethodId: quote.paymentMethodId ?? null,
|
|
9202
|
-
shippingMethodCode: quote.shippingMethodCode ?? null,
|
|
9203
|
-
paymentMethodCode: quote.paymentMethodCode ?? null,
|
|
9204
|
-
});
|
|
9205
|
-
const effectiveAdjustment = nextAdjustments.find(
|
|
9206
|
-
(adj) => adj.id === adjustmentId,
|
|
9207
|
-
);
|
|
9208
|
-
if (effectiveAdjustment?.kind === "return") {
|
|
9209
|
-
const baselineAdjustments = nextAdjustments.filter(
|
|
9210
|
-
(adj) => adj.id !== adjustmentId,
|
|
9211
|
-
);
|
|
9212
|
-
const baselineCalculation =
|
|
9213
|
-
await salesCalculationService.calculateDocumentTotals({
|
|
9214
|
-
documentKind: "quote",
|
|
9215
|
-
lines: calcLines,
|
|
9216
|
-
adjustments: baselineAdjustments,
|
|
9217
|
-
context: calculationContext,
|
|
9037
|
+
return writeLockedQuoteGraph(input, ctx, parsed.quoteId, "Sales quote not found", async (em, quote) => {
|
|
9038
|
+
if (parsed.scope === "line") {
|
|
9039
|
+
throw new CrudHttpError(400, {
|
|
9040
|
+
error: "Line-scoped adjustments are not supported yet.",
|
|
9218
9041
|
});
|
|
9219
|
-
|
|
9220
|
-
|
|
9221
|
-
|
|
9222
|
-
|
|
9223
|
-
|
|
9224
|
-
|
|
9225
|
-
|
|
9226
|
-
|
|
9227
|
-
),
|
|
9228
|
-
remainingGross: Number(
|
|
9229
|
-
baselineCalculation.totals?.grandTotalGrossAmount ?? 0,
|
|
9042
|
+
}
|
|
9043
|
+
|
|
9044
|
+
const [existingLines, existingAdjustments] = await Promise.all([
|
|
9045
|
+
em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
|
|
9046
|
+
em.find(
|
|
9047
|
+
SalesQuoteAdjustment,
|
|
9048
|
+
{ quote },
|
|
9049
|
+
{ orderBy: { position: "asc" } },
|
|
9230
9050
|
),
|
|
9051
|
+
]);
|
|
9052
|
+
const lineSnapshots = existingLines.map(mapQuoteLineEntityToSnapshot);
|
|
9053
|
+
const adjustmentDrafts = existingAdjustments.map(mapQuoteAdjustmentToDraft);
|
|
9054
|
+
const existingSnapshot = parsed.id
|
|
9055
|
+
? (adjustmentDrafts.find((adj) => adj.id === parsed.id) ?? null)
|
|
9056
|
+
: null;
|
|
9057
|
+
const adjustmentId = parsed.id ?? existingSnapshot?.id ?? randomUUID();
|
|
9058
|
+
let metadata =
|
|
9059
|
+
typeof parsed.metadata === "object" && parsed.metadata
|
|
9060
|
+
? cloneJson(parsed.metadata)
|
|
9061
|
+
: existingSnapshot?.metadata
|
|
9062
|
+
? cloneJson(existingSnapshot.metadata)
|
|
9063
|
+
: null;
|
|
9064
|
+
const calculatorKey =
|
|
9065
|
+
parsed.calculatorKey ?? existingSnapshot?.calculatorKey ?? null;
|
|
9066
|
+
if (
|
|
9067
|
+
parsed.id &&
|
|
9068
|
+
calculatorKey &&
|
|
9069
|
+
(calculatorKey.startsWith("shipping-provider:") ||
|
|
9070
|
+
calculatorKey.startsWith("payment-provider:"))
|
|
9071
|
+
) {
|
|
9072
|
+
metadata = { ...(metadata ?? {}), manualOverride: true };
|
|
9073
|
+
}
|
|
9074
|
+
let nextAdjustments = parsed.id
|
|
9075
|
+
? adjustmentDrafts.map((adj) =>
|
|
9076
|
+
adj.id === parsed.id
|
|
9077
|
+
? {
|
|
9078
|
+
...adj,
|
|
9079
|
+
id: adjustmentId,
|
|
9080
|
+
scope: parsed.scope ?? adj.scope ?? "order",
|
|
9081
|
+
kind: parsed.kind ?? adj.kind ?? "custom",
|
|
9082
|
+
code: parsed.code ?? adj.code ?? null,
|
|
9083
|
+
label: parsed.label ?? adj.label ?? null,
|
|
9084
|
+
calculatorKey:
|
|
9085
|
+
parsed.calculatorKey ?? adj.calculatorKey ?? null,
|
|
9086
|
+
promotionId: parsed.promotionId ?? adj.promotionId ?? null,
|
|
9087
|
+
rate: parsed.rate ?? adj.rate ?? null,
|
|
9088
|
+
amountNet: parsed.amountNet ?? adj.amountNet ?? null,
|
|
9089
|
+
amountGross: parsed.amountGross ?? adj.amountGross ?? null,
|
|
9090
|
+
currencyCode:
|
|
9091
|
+
parsed.currencyCode ?? adj.currencyCode ?? quote.currencyCode,
|
|
9092
|
+
metadata,
|
|
9093
|
+
customFields:
|
|
9094
|
+
parsed.customFields !== undefined
|
|
9095
|
+
? parsed.customFields
|
|
9096
|
+
: ((adj as any).customFields ?? null),
|
|
9097
|
+
position:
|
|
9098
|
+
parsed.position ?? adj.position ?? adjustmentDrafts.length,
|
|
9099
|
+
}
|
|
9100
|
+
: adj,
|
|
9101
|
+
)
|
|
9102
|
+
: [
|
|
9103
|
+
...adjustmentDrafts,
|
|
9104
|
+
{
|
|
9105
|
+
id: adjustmentId,
|
|
9106
|
+
scope: parsed.scope ?? "order",
|
|
9107
|
+
kind: parsed.kind ?? "custom",
|
|
9108
|
+
code: parsed.code ?? null,
|
|
9109
|
+
label: parsed.label ?? null,
|
|
9110
|
+
calculatorKey: parsed.calculatorKey ?? null,
|
|
9111
|
+
promotionId: parsed.promotionId ?? null,
|
|
9112
|
+
rate: parsed.rate ?? null,
|
|
9113
|
+
amountNet: parsed.amountNet ?? null,
|
|
9114
|
+
amountGross: parsed.amountGross ?? null,
|
|
9115
|
+
currencyCode: parsed.currencyCode ?? quote.currencyCode,
|
|
9116
|
+
metadata,
|
|
9117
|
+
customFields: parsed.customFields ?? null,
|
|
9118
|
+
position: parsed.position ?? adjustmentDrafts.length,
|
|
9119
|
+
},
|
|
9120
|
+
];
|
|
9121
|
+
|
|
9122
|
+
nextAdjustments = nextAdjustments
|
|
9123
|
+
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
|
|
9124
|
+
.map((adj, index) => ({ ...adj, position: adj.position ?? index }));
|
|
9125
|
+
|
|
9126
|
+
const sourceLines = lineSnapshots.map((line, index) => ({
|
|
9127
|
+
...line,
|
|
9128
|
+
statusEntryId: (line as any).statusEntryId ?? null,
|
|
9129
|
+
catalogSnapshot: (line as any).catalogSnapshot ?? null,
|
|
9130
|
+
promotionSnapshot: (line as any).promotionSnapshot ?? null,
|
|
9131
|
+
organizationId: quote.organizationId,
|
|
9132
|
+
tenantId: quote.tenantId,
|
|
9133
|
+
quoteId: quote.id,
|
|
9134
|
+
lineNumber: line.lineNumber ?? index + 1,
|
|
9135
|
+
}));
|
|
9136
|
+
const calcLines = sourceLines.map((line, index) =>
|
|
9137
|
+
createLineSnapshotFromInput(line, line.lineNumber ?? index + 1),
|
|
9138
|
+
);
|
|
9139
|
+
const salesCalculationService =
|
|
9140
|
+
ctx.container.resolve<SalesCalculationService>("salesCalculationService");
|
|
9141
|
+
const calculationContext = buildCalculationContext({
|
|
9142
|
+
tenantId: quote.tenantId,
|
|
9143
|
+
organizationId: quote.organizationId,
|
|
9144
|
+
currencyCode: quote.currencyCode,
|
|
9145
|
+
shippingSnapshot: quote.shippingMethodSnapshot,
|
|
9146
|
+
paymentSnapshot: quote.paymentMethodSnapshot,
|
|
9147
|
+
shippingMethodId: quote.shippingMethodId ?? null,
|
|
9148
|
+
paymentMethodId: quote.paymentMethodId ?? null,
|
|
9149
|
+
shippingMethodCode: quote.shippingMethodCode ?? null,
|
|
9150
|
+
paymentMethodCode: quote.paymentMethodCode ?? null,
|
|
9231
9151
|
});
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
|
|
9236
|
-
const
|
|
9237
|
-
|
|
9238
|
-
|
|
9239
|
-
|
|
9240
|
-
|
|
9241
|
-
|
|
9242
|
-
|
|
9243
|
-
|
|
9244
|
-
|
|
9245
|
-
|
|
9246
|
-
|
|
9247
|
-
|
|
9248
|
-
|
|
9249
|
-
|
|
9250
|
-
|
|
9251
|
-
|
|
9252
|
-
|
|
9253
|
-
|
|
9152
|
+
const effectiveAdjustment = nextAdjustments.find(
|
|
9153
|
+
(adj) => adj.id === adjustmentId,
|
|
9154
|
+
);
|
|
9155
|
+
if (effectiveAdjustment?.kind === "return") {
|
|
9156
|
+
const baselineAdjustments = nextAdjustments.filter(
|
|
9157
|
+
(adj) => adj.id !== adjustmentId,
|
|
9158
|
+
);
|
|
9159
|
+
const baselineCalculation =
|
|
9160
|
+
await salesCalculationService.calculateDocumentTotals({
|
|
9161
|
+
documentKind: "quote",
|
|
9162
|
+
lines: calcLines,
|
|
9163
|
+
adjustments: baselineAdjustments,
|
|
9164
|
+
context: calculationContext,
|
|
9165
|
+
});
|
|
9166
|
+
const issues = validateReturnAdjustmentWithinRemaining({
|
|
9167
|
+
kind: "return",
|
|
9168
|
+
amountNet:
|
|
9169
|
+
effectiveAdjustment.amountNet ?? effectiveAdjustment.amountGross ?? 0,
|
|
9170
|
+
amountGross:
|
|
9171
|
+
effectiveAdjustment.amountGross ?? effectiveAdjustment.amountNet ?? 0,
|
|
9172
|
+
remainingNet: Number(
|
|
9173
|
+
baselineCalculation.totals?.grandTotalNetAmount ?? 0,
|
|
9174
|
+
),
|
|
9175
|
+
remainingGross: Number(
|
|
9176
|
+
baselineCalculation.totals?.grandTotalGrossAmount ?? 0,
|
|
9177
|
+
),
|
|
9178
|
+
});
|
|
9179
|
+
if (issues.length > 0) {
|
|
9180
|
+
const { translate } = await resolveTranslations();
|
|
9181
|
+
const grossIssue = issues.find((issue) => issue.path === "amountGross");
|
|
9182
|
+
const netIssue = issues.find((issue) => issue.path === "amountNet");
|
|
9183
|
+
const fieldErrors: Record<string, string> = {};
|
|
9184
|
+
if (grossIssue) {
|
|
9185
|
+
fieldErrors.amountGross = translate(
|
|
9254
9186
|
"sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
|
|
9255
9187
|
RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE,
|
|
9256
|
-
)
|
|
9257
|
-
|
|
9258
|
-
|
|
9188
|
+
);
|
|
9189
|
+
}
|
|
9190
|
+
if (netIssue) {
|
|
9191
|
+
fieldErrors.amountNet = translate(
|
|
9192
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalNet",
|
|
9193
|
+
RETURN_ADJUSTMENT_EXCEEDS_REMAINING_NET_MESSAGE,
|
|
9194
|
+
);
|
|
9195
|
+
}
|
|
9196
|
+
throw new CrudHttpError(400, {
|
|
9197
|
+
error:
|
|
9198
|
+
fieldErrors.amountGross ??
|
|
9199
|
+
fieldErrors.amountNet ??
|
|
9200
|
+
translate(
|
|
9201
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
|
|
9202
|
+
RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE,
|
|
9203
|
+
),
|
|
9204
|
+
fieldErrors,
|
|
9205
|
+
});
|
|
9206
|
+
}
|
|
9259
9207
|
}
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
|
|
9264
|
-
|
|
9265
|
-
|
|
9208
|
+
const calculation = await salesCalculationService.calculateDocumentTotals({
|
|
9209
|
+
documentKind: "quote",
|
|
9210
|
+
lines: calcLines,
|
|
9211
|
+
adjustments: nextAdjustments,
|
|
9212
|
+
context: calculationContext,
|
|
9213
|
+
});
|
|
9214
|
+
const adjustmentInputs = nextAdjustments.map((adj, index) => ({
|
|
9215
|
+
organizationId: quote.organizationId,
|
|
9216
|
+
tenantId: quote.tenantId,
|
|
9217
|
+
quoteId: quote.id,
|
|
9218
|
+
scope: adj.scope ?? "order",
|
|
9219
|
+
kind: adj.kind ?? "custom",
|
|
9220
|
+
code: adj.code ?? undefined,
|
|
9221
|
+
label: adj.label ?? undefined,
|
|
9222
|
+
calculatorKey: adj.calculatorKey ?? undefined,
|
|
9223
|
+
promotionId: adj.promotionId ?? undefined,
|
|
9224
|
+
rate: adj.rate ?? undefined,
|
|
9225
|
+
amountNet: adj.amountNet ?? undefined,
|
|
9226
|
+
amountGross: adj.amountGross ?? undefined,
|
|
9227
|
+
currencyCode: adj.currencyCode ?? quote.currencyCode,
|
|
9228
|
+
metadata: adj.metadata ?? undefined,
|
|
9229
|
+
customFields: (adj as any).customFields ?? undefined,
|
|
9230
|
+
position: adj.position ?? index,
|
|
9231
|
+
}));
|
|
9232
|
+
await replaceQuoteAdjustments(em, quote, calculation, adjustmentInputs);
|
|
9233
|
+
applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
|
|
9234
|
+
quote.updatedAt = new Date();
|
|
9235
|
+
return { result: { quoteId: quote.id, adjustmentId }, calculation };
|
|
9266
9236
|
});
|
|
9267
|
-
const adjustmentInputs = nextAdjustments.map((adj, index) => ({
|
|
9268
|
-
organizationId: quote.organizationId,
|
|
9269
|
-
tenantId: quote.tenantId,
|
|
9270
|
-
quoteId: quote.id,
|
|
9271
|
-
scope: adj.scope ?? "order",
|
|
9272
|
-
kind: adj.kind ?? "custom",
|
|
9273
|
-
code: adj.code ?? undefined,
|
|
9274
|
-
label: adj.label ?? undefined,
|
|
9275
|
-
calculatorKey: adj.calculatorKey ?? undefined,
|
|
9276
|
-
promotionId: adj.promotionId ?? undefined,
|
|
9277
|
-
rate: adj.rate ?? undefined,
|
|
9278
|
-
amountNet: adj.amountNet ?? undefined,
|
|
9279
|
-
amountGross: adj.amountGross ?? undefined,
|
|
9280
|
-
currencyCode: adj.currencyCode ?? quote.currencyCode,
|
|
9281
|
-
metadata: adj.metadata ?? undefined,
|
|
9282
|
-
customFields: (adj as any).customFields ?? undefined,
|
|
9283
|
-
position: adj.position ?? index,
|
|
9284
|
-
}));
|
|
9285
|
-
let eventBus: EventBus | null = null;
|
|
9286
|
-
try {
|
|
9287
|
-
eventBus = ctx.container.resolve("eventBus") as EventBus;
|
|
9288
|
-
} catch {
|
|
9289
|
-
eventBus = null;
|
|
9290
|
-
}
|
|
9291
|
-
// Persist the adjustment change and recalculated totals atomically so a
|
|
9292
|
-
// mid-build failure cannot leave a half-updated quote committed (#2336).
|
|
9293
|
-
await withAtomicFlush(
|
|
9294
|
-
em,
|
|
9295
|
-
[
|
|
9296
|
-
async () => {
|
|
9297
|
-
await replaceQuoteAdjustments(em, quote, calculation, adjustmentInputs);
|
|
9298
|
-
applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
|
|
9299
|
-
quote.updatedAt = new Date();
|
|
9300
|
-
await emitTotalsCalculated(eventBus, {
|
|
9301
|
-
documentKind: "quote",
|
|
9302
|
-
documentId: quote.id,
|
|
9303
|
-
organizationId: quote.organizationId,
|
|
9304
|
-
tenantId: quote.tenantId,
|
|
9305
|
-
customerId: quote.customerEntityId ?? null,
|
|
9306
|
-
totals: calculation.totals,
|
|
9307
|
-
lineCount: calculation.lines.length,
|
|
9308
|
-
});
|
|
9309
|
-
},
|
|
9310
|
-
],
|
|
9311
|
-
{ transaction: true },
|
|
9312
|
-
);
|
|
9313
|
-
return { quoteId: quote.id, adjustmentId };
|
|
9314
|
-
},
|
|
9315
|
-
captureAfter: async (_input, result, ctx) => {
|
|
9316
|
-
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
9317
|
-
return loadQuoteSnapshot(em, result.quoteId);
|
|
9318
9237
|
},
|
|
9238
|
+
captureAfter: async (_input, result, ctx) =>
|
|
9239
|
+
captureLockedQuoteGraphAfter(ctx, result, result.quoteId),
|
|
9319
9240
|
buildLog: async ({ snapshots, result }) => {
|
|
9320
|
-
const before = snapshots.before
|
|
9241
|
+
const before = resolveLockedQuoteGraphBefore(result, snapshots.before);
|
|
9321
9242
|
const after = snapshots.after as QuoteGraphSnapshot | undefined;
|
|
9322
9243
|
if (!after) return null;
|
|
9323
9244
|
const { translate } = await resolveTranslations();
|
|
@@ -9350,135 +9271,89 @@ const quoteAdjustmentDeleteCommand: CommandHandler<
|
|
|
9350
9271
|
{ quoteId: string; adjustmentId: string }
|
|
9351
9272
|
> = {
|
|
9352
9273
|
id: "sales.quotes.adjustments.delete",
|
|
9353
|
-
async prepare(input
|
|
9354
|
-
|
|
9355
|
-
const quoteId = typeof raw.quoteId === "string" ? raw.quoteId : null;
|
|
9356
|
-
if (!quoteId) return {};
|
|
9357
|
-
const em = ctx.container.resolve("em") as EntityManager;
|
|
9358
|
-
const snapshot = await loadQuoteSnapshot(em, quoteId);
|
|
9359
|
-
if (snapshot)
|
|
9360
|
-
ensureQuoteScope(
|
|
9361
|
-
ctx,
|
|
9362
|
-
snapshot.quote.organizationId,
|
|
9363
|
-
snapshot.quote.tenantId,
|
|
9364
|
-
);
|
|
9365
|
-
return snapshot ? { before: snapshot } : {};
|
|
9274
|
+
async prepare(input) {
|
|
9275
|
+
return markLockedGraphWrite(input);
|
|
9366
9276
|
},
|
|
9367
9277
|
async execute(input, ctx) {
|
|
9368
9278
|
const parsed = quoteAdjustmentDeleteSchema.parse(
|
|
9369
9279
|
(input?.body as Record<string, unknown> | undefined) ?? {},
|
|
9370
9280
|
);
|
|
9371
|
-
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
const filtered = adjustments.filter((adj) => adj.id !== parsed.id);
|
|
9390
|
-
if (filtered.length === adjustments.length) {
|
|
9391
|
-
throw notFound("Adjustment not found");
|
|
9392
|
-
}
|
|
9393
|
-
const lineSnapshots = existingLines.map(mapQuoteLineEntityToSnapshot);
|
|
9394
|
-
const calcLines = lineSnapshots.map((line, index) =>
|
|
9395
|
-
createLineSnapshotFromInput(
|
|
9396
|
-
{
|
|
9397
|
-
...line,
|
|
9398
|
-
organizationId: quote.organizationId,
|
|
9399
|
-
tenantId: quote.tenantId,
|
|
9400
|
-
quoteId: quote.id,
|
|
9401
|
-
lineNumber: line.lineNumber ?? index + 1,
|
|
9402
|
-
statusEntryId: (line as any).statusEntryId ?? null,
|
|
9403
|
-
catalogSnapshot: (line as any).catalogSnapshot ?? null,
|
|
9404
|
-
promotionSnapshot: (line as any).promotionSnapshot ?? null,
|
|
9405
|
-
},
|
|
9406
|
-
line.lineNumber ?? index + 1,
|
|
9407
|
-
),
|
|
9408
|
-
);
|
|
9409
|
-
const adjustmentDrafts = filtered.map(mapQuoteAdjustmentToDraft);
|
|
9410
|
-
const salesCalculationService =
|
|
9411
|
-
ctx.container.resolve<SalesCalculationService>("salesCalculationService");
|
|
9412
|
-
const calculationContext = buildCalculationContext({
|
|
9413
|
-
tenantId: quote.tenantId,
|
|
9414
|
-
organizationId: quote.organizationId,
|
|
9415
|
-
currencyCode: quote.currencyCode,
|
|
9416
|
-
shippingSnapshot: quote.shippingMethodSnapshot,
|
|
9417
|
-
paymentSnapshot: quote.paymentMethodSnapshot,
|
|
9418
|
-
shippingMethodId: quote.shippingMethodId ?? null,
|
|
9419
|
-
paymentMethodId: quote.paymentMethodId ?? null,
|
|
9420
|
-
shippingMethodCode: quote.shippingMethodCode ?? null,
|
|
9421
|
-
paymentMethodCode: quote.paymentMethodCode ?? null,
|
|
9422
|
-
});
|
|
9423
|
-
const calculation = await salesCalculationService.calculateDocumentTotals({
|
|
9424
|
-
documentKind: "quote",
|
|
9425
|
-
lines: calcLines,
|
|
9426
|
-
adjustments: adjustmentDrafts,
|
|
9427
|
-
context: calculationContext,
|
|
9428
|
-
});
|
|
9429
|
-
const adjustmentInputs = adjustmentDrafts.map((adj, index) => ({
|
|
9430
|
-
organizationId: quote.organizationId,
|
|
9431
|
-
tenantId: quote.tenantId,
|
|
9432
|
-
quoteId: quote.id,
|
|
9433
|
-
scope: adj.scope ?? "order",
|
|
9434
|
-
kind: adj.kind ?? "custom",
|
|
9435
|
-
code: adj.code ?? undefined,
|
|
9436
|
-
label: adj.label ?? undefined,
|
|
9437
|
-
calculatorKey: adj.calculatorKey ?? undefined,
|
|
9438
|
-
promotionId: adj.promotionId ?? undefined,
|
|
9439
|
-
rate: adj.rate ?? undefined,
|
|
9440
|
-
amountNet: adj.amountNet ?? undefined,
|
|
9441
|
-
amountGross: adj.amountGross ?? undefined,
|
|
9442
|
-
currencyCode: adj.currencyCode ?? quote.currencyCode,
|
|
9443
|
-
metadata: adj.metadata ?? undefined,
|
|
9444
|
-
position: adj.position ?? index,
|
|
9445
|
-
}));
|
|
9446
|
-
let eventBus: EventBus | null = null;
|
|
9447
|
-
try {
|
|
9448
|
-
eventBus = ctx.container.resolve("eventBus") as EventBus;
|
|
9449
|
-
} catch {
|
|
9450
|
-
eventBus = null;
|
|
9451
|
-
}
|
|
9452
|
-
// Persist the adjustment removal and recalculated totals atomically so a
|
|
9453
|
-
// mid-build failure cannot leave a half-updated quote committed (#2336).
|
|
9454
|
-
await withAtomicFlush(
|
|
9455
|
-
em,
|
|
9456
|
-
[
|
|
9457
|
-
async () => {
|
|
9458
|
-
await replaceQuoteAdjustments(em, quote, calculation, adjustmentInputs);
|
|
9459
|
-
applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
|
|
9460
|
-
quote.updatedAt = new Date();
|
|
9461
|
-
await emitTotalsCalculated(eventBus, {
|
|
9462
|
-
documentKind: "quote",
|
|
9463
|
-
documentId: quote.id,
|
|
9281
|
+
return writeLockedQuoteGraph(input, ctx, parsed.quoteId, "Sales quote not found", async (em, quote) => {
|
|
9282
|
+
const [existingLines, adjustments] = await Promise.all([
|
|
9283
|
+
em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
|
|
9284
|
+
em.find(
|
|
9285
|
+
SalesQuoteAdjustment,
|
|
9286
|
+
{ quote },
|
|
9287
|
+
{ orderBy: { position: "asc" } },
|
|
9288
|
+
),
|
|
9289
|
+
]);
|
|
9290
|
+
const filtered = adjustments.filter((adj) => adj.id !== parsed.id);
|
|
9291
|
+
if (filtered.length === adjustments.length) {
|
|
9292
|
+
throw notFound("Adjustment not found");
|
|
9293
|
+
}
|
|
9294
|
+
const lineSnapshots = existingLines.map(mapQuoteLineEntityToSnapshot);
|
|
9295
|
+
const calcLines = lineSnapshots.map((line, index) =>
|
|
9296
|
+
createLineSnapshotFromInput(
|
|
9297
|
+
{
|
|
9298
|
+
...line,
|
|
9464
9299
|
organizationId: quote.organizationId,
|
|
9465
9300
|
tenantId: quote.tenantId,
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
|
|
9301
|
+
quoteId: quote.id,
|
|
9302
|
+
lineNumber: line.lineNumber ?? index + 1,
|
|
9303
|
+
statusEntryId: (line as any).statusEntryId ?? null,
|
|
9304
|
+
catalogSnapshot: (line as any).catalogSnapshot ?? null,
|
|
9305
|
+
promotionSnapshot: (line as any).promotionSnapshot ?? null,
|
|
9306
|
+
},
|
|
9307
|
+
line.lineNumber ?? index + 1,
|
|
9308
|
+
),
|
|
9309
|
+
);
|
|
9310
|
+
const adjustmentDrafts = filtered.map(mapQuoteAdjustmentToDraft);
|
|
9311
|
+
const salesCalculationService =
|
|
9312
|
+
ctx.container.resolve<SalesCalculationService>("salesCalculationService");
|
|
9313
|
+
const calculationContext = buildCalculationContext({
|
|
9314
|
+
tenantId: quote.tenantId,
|
|
9315
|
+
organizationId: quote.organizationId,
|
|
9316
|
+
currencyCode: quote.currencyCode,
|
|
9317
|
+
shippingSnapshot: quote.shippingMethodSnapshot,
|
|
9318
|
+
paymentSnapshot: quote.paymentMethodSnapshot,
|
|
9319
|
+
shippingMethodId: quote.shippingMethodId ?? null,
|
|
9320
|
+
paymentMethodId: quote.paymentMethodId ?? null,
|
|
9321
|
+
shippingMethodCode: quote.shippingMethodCode ?? null,
|
|
9322
|
+
paymentMethodCode: quote.paymentMethodCode ?? null,
|
|
9323
|
+
});
|
|
9324
|
+
const calculation = await salesCalculationService.calculateDocumentTotals({
|
|
9325
|
+
documentKind: "quote",
|
|
9326
|
+
lines: calcLines,
|
|
9327
|
+
adjustments: adjustmentDrafts,
|
|
9328
|
+
context: calculationContext,
|
|
9329
|
+
});
|
|
9330
|
+
const adjustmentInputs = adjustmentDrafts.map((adj, index) => ({
|
|
9331
|
+
organizationId: quote.organizationId,
|
|
9332
|
+
tenantId: quote.tenantId,
|
|
9333
|
+
quoteId: quote.id,
|
|
9334
|
+
scope: adj.scope ?? "order",
|
|
9335
|
+
kind: adj.kind ?? "custom",
|
|
9336
|
+
code: adj.code ?? undefined,
|
|
9337
|
+
label: adj.label ?? undefined,
|
|
9338
|
+
calculatorKey: adj.calculatorKey ?? undefined,
|
|
9339
|
+
promotionId: adj.promotionId ?? undefined,
|
|
9340
|
+
rate: adj.rate ?? undefined,
|
|
9341
|
+
amountNet: adj.amountNet ?? undefined,
|
|
9342
|
+
amountGross: adj.amountGross ?? undefined,
|
|
9343
|
+
currencyCode: adj.currencyCode ?? quote.currencyCode,
|
|
9344
|
+
metadata: adj.metadata ?? undefined,
|
|
9345
|
+
position: adj.position ?? index,
|
|
9346
|
+
}));
|
|
9347
|
+
await replaceQuoteAdjustments(em, quote, calculation, adjustmentInputs);
|
|
9348
|
+
applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
|
|
9349
|
+
quote.updatedAt = new Date();
|
|
9350
|
+
return { result: { quoteId: quote.id, adjustmentId: parsed.id }, calculation };
|
|
9351
|
+
});
|
|
9479
9352
|
},
|
|
9353
|
+
captureAfter: async (_input, result, ctx) =>
|
|
9354
|
+
captureLockedQuoteGraphAfter(ctx, result, result.quoteId),
|
|
9480
9355
|
buildLog: async ({ snapshots, result }) => {
|
|
9481
|
-
const before = snapshots.before
|
|
9356
|
+
const before = resolveLockedQuoteGraphBefore(result, snapshots.before);
|
|
9482
9357
|
const after = snapshots.after as QuoteGraphSnapshot | undefined;
|
|
9483
9358
|
if (!after) return null;
|
|
9484
9359
|
const { translate } = await resolveTranslations();
|