@open-mercato/core 0.10.1-develop.7727.1.0b89c4f800 → 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.
@@ -92,6 +92,7 @@ import {
92
92
  reconcileLinePersistedTotals,
93
93
  deriveLineNetFromGross,
94
94
  enforceSalesDocumentOptimisticLock,
95
+ lockSalesDocumentRow,
95
96
  SALES_RESOURCE_KIND_ORDER,
96
97
  SALES_RESOURCE_KIND_QUOTE
97
98
  } from "./shared.js";
@@ -525,19 +526,26 @@ async function applyDocumentUpdate({
525
526
  entity.quoteNumber = input.quoteNumber;
526
527
  }
527
528
  if (input.customerEntityId !== void 0) {
528
- entity.customerEntityId = input.customerEntityId ?? null;
529
+ const nextCustomerEntityId = input.customerEntityId ?? null;
530
+ const customerChanged = (nextCustomerEntityId?.toLowerCase() ?? null) !== (entity.customerEntityId?.toLowerCase() ?? null);
531
+ const snapshotContactId = input.customerContactId !== void 0 ? input.customerContactId : customerChanged ? null : entity.customerContactId ?? null;
532
+ if (customerChanged) {
533
+ entity.customerEntityId = nextCustomerEntityId;
534
+ }
529
535
  entity.customerSnapshot = await resolveCustomerSnapshot(
530
536
  em,
531
537
  organizationId,
532
538
  tenantId,
533
- input.customerEntityId,
534
- input.customerContactId ?? entity.customerContactId ?? null
539
+ entity.customerEntityId ?? null,
540
+ snapshotContactId
535
541
  );
536
- entity.customerContactId = input.customerContactId ?? null;
537
- entity.billingAddressId = null;
538
- entity.shippingAddressId = null;
539
- entity.billingAddressSnapshot = null;
540
- entity.shippingAddressSnapshot = null;
542
+ if (customerChanged) {
543
+ entity.customerContactId = input.customerContactId ?? null;
544
+ entity.billingAddressId = null;
545
+ entity.shippingAddressId = null;
546
+ entity.billingAddressSnapshot = null;
547
+ entity.shippingAddressSnapshot = null;
548
+ }
541
549
  }
542
550
  if (input.customerContactId !== void 0) {
543
551
  entity.customerContactId = input.customerContactId ?? null;
@@ -3924,6 +3932,165 @@ const deleteQuoteCommand = {
3924
3932
  await undoQuoteGraph(ctx, before, payload?.after);
3925
3933
  }
3926
3934
  };
3935
+ const lockedGraphWriteInputs = /* @__PURE__ */ new WeakSet();
3936
+ const lockedOrderGraphSnapshots = /* @__PURE__ */ new WeakMap();
3937
+ const lockedQuoteGraphSnapshots = /* @__PURE__ */ new WeakMap();
3938
+ function resolveRequestDecryptionScope(ctx) {
3939
+ return {
3940
+ tenantId: ctx.auth?.tenantId ?? void 0,
3941
+ organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? void 0
3942
+ };
3943
+ }
3944
+ async function lockOrderForTotalsWrite(em, ctx, orderId, missingOrderMessage) {
3945
+ const existingOrder = await findOneWithDecryption(
3946
+ em,
3947
+ SalesOrder,
3948
+ { id: orderId, deletedAt: null },
3949
+ {},
3950
+ resolveRequestDecryptionScope(ctx)
3951
+ );
3952
+ if (!existingOrder) throw notFound(missingOrderMessage);
3953
+ ensureOrderScope(ctx, existingOrder.organizationId, existingOrder.tenantId);
3954
+ const scope = {
3955
+ tenantId: existingOrder.tenantId,
3956
+ organizationId: existingOrder.organizationId
3957
+ };
3958
+ if (!await lockSalesDocumentRow(em, "sales_orders", orderId, scope)) {
3959
+ throw notFound(missingOrderMessage);
3960
+ }
3961
+ const order = await findOneWithDecryption(
3962
+ em,
3963
+ SalesOrder,
3964
+ { id: orderId, deletedAt: null, ...scope },
3965
+ { refresh: true },
3966
+ scope
3967
+ );
3968
+ if (!order) throw notFound(missingOrderMessage);
3969
+ await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
3970
+ return order;
3971
+ }
3972
+ async function lockQuoteForTotalsWrite(em, ctx, quoteId, missingQuoteMessage) {
3973
+ const existingQuote = await findOneWithDecryption(
3974
+ em,
3975
+ SalesQuote,
3976
+ { id: quoteId, deletedAt: null },
3977
+ {},
3978
+ resolveRequestDecryptionScope(ctx)
3979
+ );
3980
+ if (!existingQuote) throw notFound(missingQuoteMessage);
3981
+ ensureQuoteScope(ctx, existingQuote.organizationId, existingQuote.tenantId);
3982
+ const scope = {
3983
+ tenantId: existingQuote.tenantId,
3984
+ organizationId: existingQuote.organizationId
3985
+ };
3986
+ if (!await lockSalesDocumentRow(em, "sales_quotes", quoteId, scope)) {
3987
+ throw notFound(missingQuoteMessage);
3988
+ }
3989
+ const quote = await findOneWithDecryption(
3990
+ em,
3991
+ SalesQuote,
3992
+ { id: quoteId, deletedAt: null, ...scope },
3993
+ { refresh: true },
3994
+ scope
3995
+ );
3996
+ if (!quote) throw notFound(missingQuoteMessage);
3997
+ await enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
3998
+ return quote;
3999
+ }
4000
+ function buildTotalsCalculatedPayload(documentKind, document, calculation) {
4001
+ return {
4002
+ documentKind,
4003
+ documentId: document.id,
4004
+ organizationId: document.organizationId,
4005
+ tenantId: document.tenantId,
4006
+ customerId: document.customerEntityId ?? null,
4007
+ totals: calculation.totals,
4008
+ lineCount: calculation.lines.length
4009
+ };
4010
+ }
4011
+ async function emitCommittedTotalsCalculated(ctx, payload) {
4012
+ if (!payload) return;
4013
+ let eventBus = null;
4014
+ try {
4015
+ eventBus = ctx.container.resolve("eventBus");
4016
+ } catch {
4017
+ eventBus = null;
4018
+ }
4019
+ await emitTotalsCalculated(eventBus, payload);
4020
+ }
4021
+ const lockedOrderGraphKind = {
4022
+ documentKind: "order",
4023
+ lock: lockOrderForTotalsWrite,
4024
+ loadSnapshot: loadOrderSnapshot,
4025
+ snapshots: lockedOrderGraphSnapshots
4026
+ };
4027
+ const lockedQuoteGraphKind = {
4028
+ documentKind: "quote",
4029
+ lock: lockQuoteForTotalsWrite,
4030
+ loadSnapshot: loadQuoteSnapshot,
4031
+ snapshots: lockedQuoteGraphSnapshots
4032
+ };
4033
+ function markLockedGraphWrite(input) {
4034
+ lockedGraphWriteInputs.add(input);
4035
+ return {};
4036
+ }
4037
+ async function writeLockedDocumentGraph(kind, input, ctx, documentId, missingMessage, write) {
4038
+ const em = ctx.container.resolve("em").fork();
4039
+ const captureSnapshots = lockedGraphWriteInputs.has(input);
4040
+ let written;
4041
+ let totalsEvent;
4042
+ let before = null;
4043
+ let after = null;
4044
+ await withAtomicFlush(
4045
+ em,
4046
+ [
4047
+ async () => {
4048
+ const document = await kind.lock(em, ctx, documentId, missingMessage);
4049
+ if (captureSnapshots) before = await kind.loadSnapshot(em, document.id);
4050
+ written = await write(em, document);
4051
+ totalsEvent = buildTotalsCalculatedPayload(
4052
+ kind.documentKind,
4053
+ document,
4054
+ written.calculation
4055
+ );
4056
+ },
4057
+ ...captureSnapshots ? [
4058
+ async () => {
4059
+ em.clear();
4060
+ after = await kind.loadSnapshot(em, documentId);
4061
+ }
4062
+ ] : []
4063
+ ],
4064
+ { transaction: true }
4065
+ );
4066
+ if (captureSnapshots) kind.snapshots.set(written.result, { before, after });
4067
+ await emitCommittedTotalsCalculated(ctx, totalsEvent);
4068
+ return written.result;
4069
+ }
4070
+ function writeLockedOrderGraph(input, ctx, orderId, missingOrderMessage, write) {
4071
+ return writeLockedDocumentGraph(lockedOrderGraphKind, input, ctx, orderId, missingOrderMessage, write);
4072
+ }
4073
+ function writeLockedQuoteGraph(input, ctx, quoteId, missingQuoteMessage, write) {
4074
+ return writeLockedDocumentGraph(lockedQuoteGraphKind, input, ctx, quoteId, missingQuoteMessage, write);
4075
+ }
4076
+ function resolveLockedOrderGraphBefore(result, preparedBefore) {
4077
+ return lockedOrderGraphSnapshots.get(result)?.before ?? preparedBefore;
4078
+ }
4079
+ function resolveLockedQuoteGraphBefore(result, preparedBefore) {
4080
+ return lockedQuoteGraphSnapshots.get(result)?.before ?? preparedBefore;
4081
+ }
4082
+ async function captureLockedOrderGraphAfter(ctx, result, orderId) {
4083
+ const locked = lockedOrderGraphSnapshots.get(result);
4084
+ if (locked) return locked.after;
4085
+ const em = ctx.container.resolve("em").fork();
4086
+ return loadOrderSnapshot(em, orderId);
4087
+ }
4088
+ async function captureLockedQuoteGraphAfter(ctx, result, quoteId) {
4089
+ const locked = lockedQuoteGraphSnapshots.get(result);
4090
+ if (locked) return locked.after;
4091
+ const em = ctx.container.resolve("em").fork();
4092
+ return loadQuoteSnapshot(em, quoteId);
4093
+ }
3927
4094
  const updateQuoteCommand = {
3928
4095
  id: "sales.quotes.update",
3929
4096
  async prepare(input, ctx) {
@@ -3944,20 +4111,15 @@ const updateQuoteCommand = {
3944
4111
  await assertAmountsModeUnsupportedOnQuote([parsed]);
3945
4112
  documentEditSchema.parse(rawInput ?? {});
3946
4113
  const em = ctx.container.resolve("em").fork();
3947
- const quote = await findOneWithDecryption(em, SalesQuote, {
3948
- id: parsed.id,
3949
- deletedAt: null
3950
- });
3951
- if (!quote)
3952
- throw notFound("Sales quote not found");
3953
- ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
3954
- await enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
3955
- const shouldInvalidateSentToken = (quote.status ?? null) === "sent";
4114
+ let quote;
4115
+ let totalsEvent = null;
3956
4116
  const shouldRecalculateTotals = parsed.shippingMethodId !== void 0 || parsed.shippingMethodSnapshot !== void 0 || parsed.shippingMethodCode !== void 0 || parsed.paymentMethodId !== void 0 || parsed.paymentMethodSnapshot !== void 0 || parsed.paymentMethodCode !== void 0 || parsed.currencyCode !== void 0;
3957
4117
  await withAtomicFlush(
3958
4118
  em,
3959
4119
  [
3960
4120
  async () => {
4121
+ quote = await lockQuoteForTotalsWrite(em, ctx, parsed.id, "Sales quote not found");
4122
+ const shouldInvalidateSentToken = (quote.status ?? null) === "sent";
3961
4123
  await applyDocumentUpdate({
3962
4124
  kind: "quote",
3963
4125
  entity: quote,
@@ -4055,27 +4217,14 @@ const updateQuoteCommand = {
4055
4217
  adjustmentInputs
4056
4218
  );
4057
4219
  applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
4058
- let eventBus = null;
4059
- try {
4060
- eventBus = ctx.container.resolve("eventBus");
4061
- } catch {
4062
- eventBus = null;
4063
- }
4064
- await emitTotalsCalculated(eventBus, {
4065
- documentKind: "quote",
4066
- documentId: quote.id,
4067
- organizationId: quote.organizationId,
4068
- tenantId: quote.tenantId,
4069
- customerId: quote.customerEntityId ?? null,
4070
- totals: calculation.totals,
4071
- lineCount: calculation.lines.length
4072
- });
4220
+ totalsEvent = buildTotalsCalculatedPayload("quote", quote, calculation);
4073
4221
  }
4074
4222
  quote.updatedAt = /* @__PURE__ */ new Date();
4075
4223
  }
4076
4224
  ],
4077
4225
  { transaction: true }
4078
4226
  );
4227
+ await emitCommittedTotalsCalculated(ctx, totalsEvent);
4079
4228
  const dataEngine = ctx.container.resolve("dataEngine");
4080
4229
  await emitCrudSideEffects({
4081
4230
  dataEngine,
@@ -4193,44 +4342,42 @@ const updateOrderCommand = {
4193
4342
  async execute(rawInput, ctx) {
4194
4343
  const parsed = documentUpdateSchema.parse(rawInput ?? {});
4195
4344
  const em = ctx.container.resolve("em").fork();
4196
- const order = await findOneWithDecryption(em, SalesOrder, {
4197
- id: parsed.id,
4198
- deletedAt: null
4199
- });
4200
- if (!order)
4201
- throw notFound("Sales order not found");
4202
- ensureOrderScope(ctx, order.organizationId, order.tenantId);
4203
- const currentTotalsMode = order.totalsMode ?? "computed";
4204
- const nextTotalsMode = parsed.totalsMode ?? currentTotalsMode;
4205
- const totalsModeChanged = nextTotalsMode !== currentTotalsMode;
4206
- const externalHeaderSupplied = isExternalAmountsMode(nextTotalsMode) && hasAnySuppliedHeaderTotal(parsed);
4207
- if (!totalsModeChanged && !externalHeaderSupplied) {
4208
- documentEditSchema.parse(rawInput ?? {});
4209
- }
4210
- if (isExternalAmountsMode(nextTotalsMode) && (totalsModeChanged || externalHeaderSupplied)) {
4211
- await requireOrderTotalsForExternalWrite(parsed);
4212
- }
4213
- await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
4214
- const previousStatus = normalizeStatusValue(order.status);
4215
- if (parsed.statusEntryId && !isCancelledOrderStatus(previousStatus)) {
4216
- const requestedStatus = normalizeStatusValue(
4217
- await resolveDictionaryEntryValue(em, parsed.statusEntryId, {
4218
- tenantId: order.tenantId
4219
- })
4220
- );
4221
- if (isCancelledOrderStatus(requestedStatus)) {
4222
- await assertOrderCanBeCancelled(em, order);
4223
- }
4224
- }
4345
+ let order;
4346
+ let previousStatus = null;
4347
+ let nextTotalsMode = "computed";
4348
+ let totalsModeChanged = false;
4349
+ let externalHeaderSupplied = false;
4350
+ let suppliedTotals = null;
4225
4351
  let statusChangeNote = null;
4226
- const suppliedTotals = isExternalAmountsMode(
4227
- nextTotalsMode
4228
- ) ? externalHeaderSupplied ? buildExternalHeaderTotals(parsed) : readPersistedHeaderTotals(order) : null;
4229
- const shouldRecalculateTotals = parsed.shippingMethodId !== void 0 || parsed.shippingMethodSnapshot !== void 0 || parsed.shippingMethodCode !== void 0 || parsed.paymentMethodId !== void 0 || parsed.paymentMethodSnapshot !== void 0 || parsed.paymentMethodCode !== void 0 || parsed.currencyCode !== void 0 || totalsModeChanged || externalHeaderSupplied;
4352
+ let totalsEvent = null;
4353
+ const shouldRecalculateTotals = parsed.shippingMethodId !== void 0 || parsed.shippingMethodSnapshot !== void 0 || parsed.shippingMethodCode !== void 0 || parsed.paymentMethodId !== void 0 || parsed.paymentMethodSnapshot !== void 0 || parsed.paymentMethodCode !== void 0 || parsed.currencyCode !== void 0;
4230
4354
  await withAtomicFlush(
4231
4355
  em,
4232
4356
  [
4233
4357
  async () => {
4358
+ order = await lockOrderForTotalsWrite(em, ctx, parsed.id, "Sales order not found");
4359
+ const currentTotalsMode = order.totalsMode ?? "computed";
4360
+ nextTotalsMode = parsed.totalsMode ?? currentTotalsMode;
4361
+ totalsModeChanged = nextTotalsMode !== currentTotalsMode;
4362
+ externalHeaderSupplied = isExternalAmountsMode(nextTotalsMode) && hasAnySuppliedHeaderTotal(parsed);
4363
+ if (!totalsModeChanged && !externalHeaderSupplied) {
4364
+ documentEditSchema.parse(rawInput ?? {});
4365
+ }
4366
+ if (isExternalAmountsMode(nextTotalsMode) && (totalsModeChanged || externalHeaderSupplied)) {
4367
+ await requireOrderTotalsForExternalWrite(parsed);
4368
+ }
4369
+ suppliedTotals = isExternalAmountsMode(nextTotalsMode) ? externalHeaderSupplied ? buildExternalHeaderTotals(parsed) : readPersistedHeaderTotals(order) : null;
4370
+ previousStatus = normalizeStatusValue(order.status);
4371
+ if (parsed.statusEntryId && !isCancelledOrderStatus(previousStatus)) {
4372
+ const requestedStatus = normalizeStatusValue(
4373
+ await resolveDictionaryEntryValue(em, parsed.statusEntryId, {
4374
+ tenantId: order.tenantId
4375
+ })
4376
+ );
4377
+ if (isCancelledOrderStatus(requestedStatus)) {
4378
+ await assertOrderCanBeCancelled(em, order);
4379
+ }
4380
+ }
4234
4381
  await applyDocumentUpdate({
4235
4382
  kind: "order",
4236
4383
  entity: order,
@@ -4245,7 +4392,7 @@ const updateOrderCommand = {
4245
4392
  // `em.find` lines/adjustments or appendOrderStatusChangeNote queries
4246
4393
  // reset the changeset (see SPEC-018).
4247
4394
  async () => {
4248
- if (shouldRecalculateTotals) {
4395
+ if (shouldRecalculateTotals || totalsModeChanged || externalHeaderSupplied) {
4249
4396
  const [existingLines, adjustments] = await Promise.all([
4250
4397
  em.find(
4251
4398
  SalesOrderLine,
@@ -4334,21 +4481,7 @@ const updateOrderCommand = {
4334
4481
  order.totalsMode = nextTotalsMode;
4335
4482
  }
4336
4483
  applyOrderTotals(order, calculation.totals, calculation.lines.length);
4337
- let eventBus = null;
4338
- try {
4339
- eventBus = ctx.container.resolve("eventBus");
4340
- } catch {
4341
- eventBus = null;
4342
- }
4343
- await emitTotalsCalculated(eventBus, {
4344
- documentKind: "order",
4345
- documentId: order.id,
4346
- organizationId: order.organizationId,
4347
- tenantId: order.tenantId,
4348
- customerId: order.customerEntityId ?? null,
4349
- totals: calculation.totals,
4350
- lineCount: calculation.lines.length
4351
- });
4484
+ totalsEvent = buildTotalsCalculatedPayload("order", order, calculation);
4352
4485
  }
4353
4486
  statusChangeNote = await appendOrderStatusChangeNote({
4354
4487
  em,
@@ -4361,6 +4494,7 @@ const updateOrderCommand = {
4361
4494
  ],
4362
4495
  { transaction: true }
4363
4496
  );
4497
+ await emitCommittedTotalsCalculated(ctx, totalsEvent);
4364
4498
  emitOrderLifecycleEventsForTransition({ order, previousStatus });
4365
4499
  const dataEngine = ctx.container.resolve("dataEngine");
4366
4500
  await emitCrudSideEffects({
@@ -5575,115 +5709,75 @@ async function assertOrderAcceptsNewLine(order, existingSnapshot) {
5575
5709
  }
5576
5710
  const orderLineUpsertCommand = {
5577
5711
  id: "sales.orders.lines.upsert",
5578
- async prepare(input, ctx) {
5579
- const raw = input?.body ?? {};
5580
- const orderId = typeof raw.orderId === "string" ? raw.orderId : null;
5581
- if (!orderId) return {};
5582
- const em = ctx.container.resolve("em");
5583
- const snapshot = await loadOrderSnapshot(em, orderId);
5584
- if (snapshot)
5585
- ensureOrderScope(
5586
- ctx,
5587
- snapshot.order.organizationId,
5588
- snapshot.order.tenantId
5589
- );
5590
- return snapshot ? { before: snapshot } : {};
5712
+ async prepare(input) {
5713
+ return markLockedGraphWrite(input);
5591
5714
  },
5592
5715
  async execute(input, ctx) {
5593
5716
  const rawBody = input?.body ?? {};
5594
5717
  const parsed = orderLineUpsertSchema.parse(rawBody);
5595
- const em = ctx.container.resolve("em").fork();
5596
- const order = await findOneWithDecryption(em, SalesOrder, {
5597
- id: parsed.orderId,
5598
- deletedAt: null
5599
- });
5600
- if (!order)
5601
- throw notFound("Sales order not found");
5602
- ensureOrderScope(ctx, order.organizationId, order.tenantId);
5603
- await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
5604
- const [existingLines, adjustments] = await Promise.all([
5605
- em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
5606
- em.find(
5607
- SalesOrderAdjustment,
5608
- { order },
5609
- { orderBy: { position: "asc" } }
5610
- )
5611
- ]);
5612
- const lineSnapshots = existingLines.map(mapOrderLineEntityToSnapshot);
5613
- const existingSnapshot = parsed.id ? lineSnapshots.find((line) => line.id === parsed.id) ?? null : null;
5614
- const totalsMode = order.totalsMode ?? "computed";
5615
- await assertUniformAmountsMode(totalsMode, [parsed]);
5616
- if (isExternalAmountsMode(totalsMode)) {
5617
- await requireOrderTotalsForExternalWrite(parsed.orderTotals);
5618
- }
5619
- const suppliedTotals = isExternalAmountsMode(totalsMode) ? buildExternalHeaderTotals(parsed.orderTotals) : null;
5620
- await assertOrderAcceptsNewLine(order, existingSnapshot);
5621
- await assertShippedOrderLineEditable(em, order, existingSnapshot, parsed);
5622
- const priceMode = parsed.priceMode === "gross" ? "gross" : parsed.priceMode === "net" ? "net" : null;
5623
- let unitPriceNet = parsed.unitPriceNet ?? existingSnapshot?.unitPriceNet ?? null;
5624
- let unitPriceGross = parsed.unitPriceGross ?? existingSnapshot?.unitPriceGross ?? null;
5625
- let taxRate = parsed.taxRate ?? existingSnapshot?.taxRate ?? null;
5626
- const unitPriceSupplied = unitPriceNet !== null || unitPriceGross !== null;
5627
- if (priceMode && (unitPriceNet === null || unitPriceGross === null)) {
5628
- let taxService = null;
5629
- try {
5630
- taxService = ctx.container.resolve(
5631
- "taxCalculationService"
5632
- );
5633
- } catch {
5634
- taxService = null;
5635
- }
5636
- if (taxService) {
5637
- const taxResult = await taxService.calculateUnitAmounts({
5638
- amount: priceMode === "gross" ? unitPriceGross ?? unitPriceNet ?? 0 : unitPriceNet ?? unitPriceGross ?? 0,
5639
- mode: priceMode,
5640
- organizationId: parsed.organizationId,
5641
- tenantId: parsed.tenantId,
5642
- taxRateId: parsed.taxRateId ?? void 0,
5643
- taxRate: taxRate ?? void 0
5644
- });
5645
- unitPriceNet = unitPriceNet ?? taxResult.netAmount;
5646
- unitPriceGross = unitPriceGross ?? taxResult.grossAmount;
5647
- taxRate = taxResult.taxRate ?? taxRate;
5718
+ return writeLockedOrderGraph(input, ctx, parsed.orderId, "Sales order not found", async (em, order) => {
5719
+ const [existingLines, adjustments] = await Promise.all([
5720
+ em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
5721
+ em.find(
5722
+ SalesOrderAdjustment,
5723
+ { order },
5724
+ { orderBy: { position: "asc" } }
5725
+ )
5726
+ ]);
5727
+ const lineSnapshots = existingLines.map(mapOrderLineEntityToSnapshot);
5728
+ const existingSnapshot = parsed.id ? lineSnapshots.find((line) => line.id === parsed.id) ?? null : null;
5729
+ const totalsMode = order.totalsMode ?? "computed";
5730
+ await assertUniformAmountsMode(totalsMode, [parsed]);
5731
+ if (isExternalAmountsMode(totalsMode)) {
5732
+ await requireOrderTotalsForExternalWrite(parsed.orderTotals);
5648
5733
  }
5649
- }
5650
- const metadata = typeof parsed.metadata === "object" && parsed.metadata ? { ...parsed.metadata } : existingSnapshot?.metadata ? cloneJson(existingSnapshot.metadata) : {};
5651
- if (parsed.priceId) metadata.priceId = parsed.priceId;
5652
- if (priceMode) metadata.priceMode = priceMode;
5653
- const statusEntryId = parsed.statusEntryId ?? existingSnapshot?.statusEntryId ?? null;
5654
- const lineId = parsed.id ?? existingSnapshot?.id ?? randomUUID();
5655
- const lineUomInput = {
5656
- productId: parsed.productId ?? existingSnapshot?.productId ?? null,
5657
- productVariantId: parsed.productVariantId ?? existingSnapshot?.productVariantId ?? null,
5658
- quantity: parsed.quantity ?? existingSnapshot?.quantity ?? 0,
5659
- quantityUnit: parsed.quantityUnit ?? existingSnapshot?.quantityUnit ?? null,
5660
- normalizedQuantity: existingSnapshot?.normalizedQuantity ?? null,
5661
- normalizedUnit: existingSnapshot?.normalizedUnit ?? null,
5662
- uomSnapshot: existingSnapshot?.uomSnapshot ?? null
5663
- };
5664
- const uomResolver = createUomResolver();
5665
- let normalizedUom = await normalizeLineUom({
5666
- em,
5667
- resolver: uomResolver,
5668
- organizationId: order.organizationId,
5669
- tenantId: order.tenantId,
5670
- line: {
5671
- ...lineUomInput,
5672
- unitPriceNet: unitPriceNet ?? 0,
5673
- unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0
5734
+ const suppliedTotals = isExternalAmountsMode(totalsMode) ? buildExternalHeaderTotals(parsed.orderTotals) : null;
5735
+ await assertOrderAcceptsNewLine(order, existingSnapshot);
5736
+ await assertShippedOrderLineEditable(em, order, existingSnapshot, parsed);
5737
+ const priceMode = parsed.priceMode === "gross" ? "gross" : parsed.priceMode === "net" ? "net" : null;
5738
+ let unitPriceNet = parsed.unitPriceNet ?? existingSnapshot?.unitPriceNet ?? null;
5739
+ let unitPriceGross = parsed.unitPriceGross ?? existingSnapshot?.unitPriceGross ?? null;
5740
+ let taxRate = parsed.taxRate ?? existingSnapshot?.taxRate ?? null;
5741
+ const unitPriceSupplied = unitPriceNet !== null || unitPriceGross !== null;
5742
+ if (priceMode && (unitPriceNet === null || unitPriceGross === null)) {
5743
+ let taxService = null;
5744
+ try {
5745
+ taxService = ctx.container.resolve(
5746
+ "taxCalculationService"
5747
+ );
5748
+ } catch {
5749
+ taxService = null;
5750
+ }
5751
+ if (taxService) {
5752
+ const taxResult = await taxService.calculateUnitAmounts({
5753
+ amount: priceMode === "gross" ? unitPriceGross ?? unitPriceNet ?? 0 : unitPriceNet ?? unitPriceGross ?? 0,
5754
+ mode: priceMode,
5755
+ organizationId: parsed.organizationId,
5756
+ tenantId: parsed.tenantId,
5757
+ taxRateId: parsed.taxRateId ?? void 0,
5758
+ taxRate: taxRate ?? void 0
5759
+ });
5760
+ unitPriceNet = unitPriceNet ?? taxResult.netAmount;
5761
+ unitPriceGross = unitPriceGross ?? taxResult.grossAmount;
5762
+ taxRate = taxResult.taxRate ?? taxRate;
5763
+ }
5674
5764
  }
5675
- });
5676
- const convertedPrices = convertLineUnitPricesOnUnitChange({
5677
- existingSnapshot,
5678
- nextQuantityUnit: normalizedUom.quantityUnit,
5679
- nextUomSnapshot: normalizedUom.uomSnapshot,
5680
- unitPriceNet,
5681
- unitPriceGross
5682
- });
5683
- if (convertedPrices.didConvert) {
5684
- unitPriceNet = convertedPrices.unitPriceNet;
5685
- unitPriceGross = convertedPrices.unitPriceGross;
5686
- normalizedUom = await normalizeLineUom({
5765
+ const metadata = typeof parsed.metadata === "object" && parsed.metadata ? { ...parsed.metadata } : existingSnapshot?.metadata ? cloneJson(existingSnapshot.metadata) : {};
5766
+ if (parsed.priceId) metadata.priceId = parsed.priceId;
5767
+ if (priceMode) metadata.priceMode = priceMode;
5768
+ const statusEntryId = parsed.statusEntryId ?? existingSnapshot?.statusEntryId ?? null;
5769
+ const lineId = parsed.id ?? existingSnapshot?.id ?? randomUUID();
5770
+ const lineUomInput = {
5771
+ productId: parsed.productId ?? existingSnapshot?.productId ?? null,
5772
+ productVariantId: parsed.productVariantId ?? existingSnapshot?.productVariantId ?? null,
5773
+ quantity: parsed.quantity ?? existingSnapshot?.quantity ?? 0,
5774
+ quantityUnit: parsed.quantityUnit ?? existingSnapshot?.quantityUnit ?? null,
5775
+ normalizedQuantity: existingSnapshot?.normalizedQuantity ?? null,
5776
+ normalizedUnit: existingSnapshot?.normalizedUnit ?? null,
5777
+ uomSnapshot: existingSnapshot?.uomSnapshot ?? null
5778
+ };
5779
+ const uomResolver = createUomResolver();
5780
+ let normalizedUom = await normalizeLineUom({
5687
5781
  em,
5688
5782
  resolver: uomResolver,
5689
5783
  organizationId: order.organizationId,
@@ -5694,134 +5788,130 @@ const orderLineUpsertCommand = {
5694
5788
  unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0
5695
5789
  }
5696
5790
  });
5697
- }
5698
- const updatedSnapshot = {
5699
- id: lineId,
5700
- lineNumber: parsed.lineNumber ?? existingSnapshot?.lineNumber ?? lineSnapshots.length + 1,
5701
- kind: parsed.kind ?? existingSnapshot?.kind ?? "product",
5702
- productId: parsed.productId ?? existingSnapshot?.productId ?? null,
5703
- productVariantId: parsed.productVariantId ?? existingSnapshot?.productVariantId ?? null,
5704
- name: parsed.name ?? existingSnapshot?.name ?? null,
5705
- description: parsed.description ?? existingSnapshot?.description ?? null,
5706
- comment: parsed.comment ?? existingSnapshot?.comment ?? null,
5707
- quantity: normalizedUom.quantity,
5708
- quantityUnit: normalizedUom.quantityUnit,
5709
- normalizedQuantity: normalizedUom.normalizedQuantity,
5710
- normalizedUnit: normalizedUom.normalizedUnit,
5711
- uomSnapshot: normalizedUom.uomSnapshot ? cloneJson(normalizedUom.uomSnapshot) : null,
5712
- currencyCode: parsed.currencyCode ?? existingSnapshot?.currencyCode ?? order.currencyCode,
5713
- unitPriceNet: unitPriceNet ?? 0,
5714
- unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0,
5715
- ...resolveUpsertDiscountFields(
5716
- parsed.discountAmount,
5717
- parsed.discountAmountBasis,
5718
- existingSnapshot
5719
- ),
5720
- discountPercent: parsed.discountPercent ?? existingSnapshot?.discountPercent ?? 0,
5721
- taxRate: taxRate ?? 0,
5722
- totalNetAmount: parsed.totalNetAmount ?? existingSnapshot?.totalNetAmount ?? null,
5723
- ...resolveUpsertTotalsOrigin(parsed.totalNetAmount, existingSnapshot),
5724
- configuration: parsed.configuration ?? existingSnapshot?.configuration ?? null,
5725
- promotionCode: parsed.promotionCode ?? existingSnapshot?.promotionCode ?? null,
5726
- metadata,
5727
- customFieldSetId: parsed.customFieldSetId ?? existingSnapshot?.customFieldSetId ?? null,
5728
- customFields: parsed.customFields && typeof parsed.customFields === "object" ? cloneJson(parsed.customFields) : existingSnapshot?.customFields ?? null,
5729
- amountsMode: totalsMode
5730
- };
5731
- Object.assign(
5732
- updatedSnapshot,
5733
- resolveUpsertCalculatedAmounts(parsed, updatedSnapshot, existingSnapshot)
5734
- );
5735
- if (isExternalAmountsMode(totalsMode)) {
5736
- await assertExternalLineComplete(
5737
- {
5738
- ...updatedSnapshot,
5739
- unitPriceNet: unitPriceSupplied ? unitPriceNet : null
5740
- },
5741
- updatedSnapshot.lineNumber ?? 1
5791
+ const convertedPrices = convertLineUnitPricesOnUnitChange({
5792
+ existingSnapshot,
5793
+ nextQuantityUnit: normalizedUom.quantityUnit,
5794
+ nextUomSnapshot: normalizedUom.uomSnapshot,
5795
+ unitPriceNet,
5796
+ unitPriceGross
5797
+ });
5798
+ if (convertedPrices.didConvert) {
5799
+ unitPriceNet = convertedPrices.unitPriceNet;
5800
+ unitPriceGross = convertedPrices.unitPriceGross;
5801
+ normalizedUom = await normalizeLineUom({
5802
+ em,
5803
+ resolver: uomResolver,
5804
+ organizationId: order.organizationId,
5805
+ tenantId: order.tenantId,
5806
+ line: {
5807
+ ...lineUomInput,
5808
+ unitPriceNet: unitPriceNet ?? 0,
5809
+ unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0
5810
+ }
5811
+ });
5812
+ }
5813
+ const updatedSnapshot = {
5814
+ id: lineId,
5815
+ lineNumber: parsed.lineNumber ?? existingSnapshot?.lineNumber ?? lineSnapshots.length + 1,
5816
+ kind: parsed.kind ?? existingSnapshot?.kind ?? "product",
5817
+ productId: parsed.productId ?? existingSnapshot?.productId ?? null,
5818
+ productVariantId: parsed.productVariantId ?? existingSnapshot?.productVariantId ?? null,
5819
+ name: parsed.name ?? existingSnapshot?.name ?? null,
5820
+ description: parsed.description ?? existingSnapshot?.description ?? null,
5821
+ comment: parsed.comment ?? existingSnapshot?.comment ?? null,
5822
+ quantity: normalizedUom.quantity,
5823
+ quantityUnit: normalizedUom.quantityUnit,
5824
+ normalizedQuantity: normalizedUom.normalizedQuantity,
5825
+ normalizedUnit: normalizedUom.normalizedUnit,
5826
+ uomSnapshot: normalizedUom.uomSnapshot ? cloneJson(normalizedUom.uomSnapshot) : null,
5827
+ currencyCode: parsed.currencyCode ?? existingSnapshot?.currencyCode ?? order.currencyCode,
5828
+ unitPriceNet: unitPriceNet ?? 0,
5829
+ unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0,
5830
+ ...resolveUpsertDiscountFields(
5831
+ parsed.discountAmount,
5832
+ parsed.discountAmountBasis,
5833
+ existingSnapshot
5834
+ ),
5835
+ discountPercent: parsed.discountPercent ?? existingSnapshot?.discountPercent ?? 0,
5836
+ taxRate: taxRate ?? 0,
5837
+ totalNetAmount: parsed.totalNetAmount ?? existingSnapshot?.totalNetAmount ?? null,
5838
+ ...resolveUpsertTotalsOrigin(parsed.totalNetAmount, existingSnapshot),
5839
+ configuration: parsed.configuration ?? existingSnapshot?.configuration ?? null,
5840
+ promotionCode: parsed.promotionCode ?? existingSnapshot?.promotionCode ?? null,
5841
+ metadata,
5842
+ customFieldSetId: parsed.customFieldSetId ?? existingSnapshot?.customFieldSetId ?? null,
5843
+ customFields: parsed.customFields && typeof parsed.customFields === "object" ? cloneJson(parsed.customFields) : existingSnapshot?.customFields ?? null,
5844
+ amountsMode: totalsMode
5845
+ };
5846
+ Object.assign(
5847
+ updatedSnapshot,
5848
+ resolveUpsertCalculatedAmounts(parsed, updatedSnapshot, existingSnapshot)
5742
5849
  );
5743
- }
5744
- updatedSnapshot.statusEntryId = statusEntryId;
5745
- updatedSnapshot.catalogSnapshot = parsed.catalogSnapshot ?? existingSnapshot?.catalogSnapshot ?? null;
5746
- updatedSnapshot.promotionSnapshot = parsed.promotionSnapshot ?? existingSnapshot?.promotionSnapshot ?? null;
5747
- let nextLines = parsed.id ? lineSnapshots.map(
5748
- (line) => line.id === parsed.id ? updatedSnapshot : line
5749
- ) : [...lineSnapshots, updatedSnapshot];
5750
- nextLines = nextLines.sort((a, b) => (a.lineNumber ?? 0) - (b.lineNumber ?? 0)).map((line, index) => ({ ...line, lineNumber: index + 1 }));
5751
- const sourceInputs = nextLines.map((line, index) => ({
5752
- ...line,
5753
- statusEntryId: line.statusEntryId ?? null,
5754
- catalogSnapshot: line.catalogSnapshot ?? null,
5755
- promotionSnapshot: line.promotionSnapshot ?? null,
5756
- organizationId: order.organizationId,
5757
- tenantId: order.tenantId,
5758
- orderId: order.id,
5759
- lineNumber: line.lineNumber ?? index + 1
5760
- }));
5761
- const calcLines = sourceInputs.map(
5762
- (line, index) => createLineSnapshotFromInput(line, line.lineNumber ?? index + 1)
5763
- );
5764
- const adjustmentDrafts = adjustments.map(mapOrderAdjustmentToDraft);
5765
- const salesCalculationService = ctx.container.resolve("salesCalculationService");
5766
- const calculationContext = buildCalculationContext({
5767
- tenantId: order.tenantId,
5768
- organizationId: order.organizationId,
5769
- currencyCode: order.currencyCode,
5770
- shippingSnapshot: order.shippingMethodSnapshot,
5771
- paymentSnapshot: order.paymentMethodSnapshot,
5772
- shippingMethodId: order.shippingMethodId ?? null,
5773
- paymentMethodId: order.paymentMethodId ?? null,
5774
- shippingMethodCode: order.shippingMethodCode ?? null,
5775
- paymentMethodCode: order.paymentMethodCode ?? null
5776
- });
5777
- const calculation = await salesCalculationService.calculateDocumentTotals({
5778
- documentKind: "order",
5779
- lines: calcLines,
5780
- adjustments: adjustmentDrafts,
5781
- context: calculationContext,
5782
- existingTotals: resolveExistingPaymentTotals(order),
5783
- totalsMode,
5784
- suppliedTotals
5850
+ if (isExternalAmountsMode(totalsMode)) {
5851
+ await assertExternalLineComplete(
5852
+ {
5853
+ ...updatedSnapshot,
5854
+ unitPriceNet: unitPriceSupplied ? unitPriceNet : null
5855
+ },
5856
+ updatedSnapshot.lineNumber ?? 1
5857
+ );
5858
+ }
5859
+ updatedSnapshot.statusEntryId = statusEntryId;
5860
+ updatedSnapshot.catalogSnapshot = parsed.catalogSnapshot ?? existingSnapshot?.catalogSnapshot ?? null;
5861
+ updatedSnapshot.promotionSnapshot = parsed.promotionSnapshot ?? existingSnapshot?.promotionSnapshot ?? null;
5862
+ let nextLines = parsed.id ? lineSnapshots.map(
5863
+ (line) => line.id === parsed.id ? updatedSnapshot : line
5864
+ ) : [...lineSnapshots, updatedSnapshot];
5865
+ nextLines = nextLines.sort((a, b) => (a.lineNumber ?? 0) - (b.lineNumber ?? 0)).map((line, index) => ({ ...line, lineNumber: index + 1 }));
5866
+ const sourceInputs = nextLines.map((line, index) => ({
5867
+ ...line,
5868
+ statusEntryId: line.statusEntryId ?? null,
5869
+ catalogSnapshot: line.catalogSnapshot ?? null,
5870
+ promotionSnapshot: line.promotionSnapshot ?? null,
5871
+ organizationId: order.organizationId,
5872
+ tenantId: order.tenantId,
5873
+ orderId: order.id,
5874
+ lineNumber: line.lineNumber ?? index + 1
5875
+ }));
5876
+ const calcLines = sourceInputs.map(
5877
+ (line, index) => createLineSnapshotFromInput(line, line.lineNumber ?? index + 1)
5878
+ );
5879
+ const adjustmentDrafts = adjustments.map(mapOrderAdjustmentToDraft);
5880
+ const salesCalculationService = ctx.container.resolve("salesCalculationService");
5881
+ const calculationContext = buildCalculationContext({
5882
+ tenantId: order.tenantId,
5883
+ organizationId: order.organizationId,
5884
+ currencyCode: order.currencyCode,
5885
+ shippingSnapshot: order.shippingMethodSnapshot,
5886
+ paymentSnapshot: order.paymentMethodSnapshot,
5887
+ shippingMethodId: order.shippingMethodId ?? null,
5888
+ paymentMethodId: order.paymentMethodId ?? null,
5889
+ shippingMethodCode: order.shippingMethodCode ?? null,
5890
+ paymentMethodCode: order.paymentMethodCode ?? null
5891
+ });
5892
+ const calculation = await salesCalculationService.calculateDocumentTotals({
5893
+ documentKind: "order",
5894
+ lines: calcLines,
5895
+ adjustments: adjustmentDrafts,
5896
+ context: calculationContext,
5897
+ existingTotals: resolveExistingPaymentTotals(order),
5898
+ totalsMode,
5899
+ suppliedTotals
5900
+ });
5901
+ await applyOrderLineResults({
5902
+ em,
5903
+ order,
5904
+ calculation,
5905
+ sourceLines: sourceInputs,
5906
+ existingLines
5907
+ });
5908
+ applyOrderTotals(order, calculation.totals, calculation.lines.length);
5909
+ return { result: { orderId: order.id, lineId }, calculation };
5785
5910
  });
5786
- let eventBus = null;
5787
- try {
5788
- eventBus = ctx.container.resolve("eventBus");
5789
- } catch {
5790
- eventBus = null;
5791
- }
5792
- await withAtomicFlush(
5793
- em,
5794
- [
5795
- async () => {
5796
- await applyOrderLineResults({
5797
- em,
5798
- order,
5799
- calculation,
5800
- sourceLines: sourceInputs,
5801
- existingLines
5802
- });
5803
- applyOrderTotals(order, calculation.totals, calculation.lines.length);
5804
- await emitTotalsCalculated(eventBus, {
5805
- documentKind: "order",
5806
- documentId: order.id,
5807
- organizationId: order.organizationId,
5808
- tenantId: order.tenantId,
5809
- customerId: order.customerEntityId ?? null,
5810
- totals: calculation.totals,
5811
- lineCount: calculation.lines.length
5812
- });
5813
- }
5814
- ],
5815
- { transaction: true }
5816
- );
5817
- return { orderId: order.id, lineId };
5818
- },
5819
- captureAfter: async (_input, result, ctx) => {
5820
- const em = ctx.container.resolve("em").fork();
5821
- return loadOrderSnapshot(em, result.orderId);
5822
5911
  },
5912
+ captureAfter: async (_input, result, ctx) => captureLockedOrderGraphAfter(ctx, result, result.orderId),
5823
5913
  buildLog: async ({ result, snapshots }) => {
5824
- const before = snapshots.before;
5914
+ const before = resolveLockedOrderGraphBefore(result, snapshots.before);
5825
5915
  const after = snapshots.after;
5826
5916
  if (!after) return null;
5827
5917
  const { translate } = await resolveTranslations();
@@ -5850,153 +5940,109 @@ const orderLineUpsertCommand = {
5850
5940
  };
5851
5941
  const orderLineDeleteCommand = {
5852
5942
  id: "sales.orders.lines.delete",
5853
- async prepare(input, ctx) {
5854
- const raw = input?.body ?? {};
5855
- const orderId = typeof raw.orderId === "string" ? raw.orderId : null;
5856
- if (!orderId) return {};
5857
- const em = ctx.container.resolve("em");
5858
- const snapshot = await loadOrderSnapshot(em, orderId);
5859
- if (snapshot)
5860
- ensureOrderScope(
5861
- ctx,
5862
- snapshot.order.organizationId,
5863
- snapshot.order.tenantId
5864
- );
5865
- return snapshot ? { before: snapshot } : {};
5943
+ async prepare(input) {
5944
+ return markLockedGraphWrite(input);
5866
5945
  },
5867
5946
  async execute(input, ctx) {
5868
5947
  const { translate } = await resolveTranslations();
5869
5948
  const parsed = orderLineDeleteSchema.parse(
5870
5949
  input?.body ?? {}
5871
5950
  );
5872
- const em = ctx.container.resolve("em").fork();
5873
- const order = await findOneWithDecryption(em, SalesOrder, {
5874
- id: parsed.orderId,
5875
- deletedAt: null
5876
- });
5877
- if (!order)
5878
- throw notFound(translate(
5879
- "sales.documents.detail.error",
5880
- "Document not found or inaccessible."
5881
- ));
5882
- ensureOrderScope(ctx, order.organizationId, order.tenantId);
5883
- await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
5884
- const shipmentCount = await em.count(SalesShipmentItem, {
5885
- orderLine: parsed.id,
5886
- shipment: { deletedAt: null }
5887
- });
5888
- if (shipmentCount > 0) {
5889
- throw new CrudHttpError(409, {
5890
- error: translate(
5891
- "sales.documents.items.errorDeleteShipped",
5892
- "Cannot delete a line that has shipped items."
5893
- )
5951
+ return writeLockedOrderGraph(input, ctx, parsed.orderId, translate(
5952
+ "sales.documents.detail.error",
5953
+ "Document not found or inaccessible."
5954
+ ), async (em, order) => {
5955
+ const shipmentCount = await em.count(SalesShipmentItem, {
5956
+ orderLine: parsed.id,
5957
+ shipment: { deletedAt: null }
5894
5958
  });
5895
- }
5896
- const existingLines = await em.find(
5897
- SalesOrderLine,
5898
- { order },
5899
- { orderBy: { lineNumber: "asc" } }
5900
- );
5901
- const adjustments = await em.find(
5902
- SalesOrderAdjustment,
5903
- { order },
5904
- { orderBy: { position: "asc" } }
5905
- );
5906
- const filtered = existingLines.filter((line) => line.id !== parsed.id);
5907
- if (filtered.length === existingLines.length) {
5908
- throw notFound(translate(
5909
- "sales.documents.detail.error",
5910
- "Document not found or inaccessible."
5911
- ));
5912
- }
5913
- if (filtered.length === 0) {
5914
- throw new CrudHttpError(409, {
5915
- error: translate(
5916
- "sales.documents.items.errorDeleteLast",
5917
- "An order must contain at least one line item."
5918
- )
5959
+ if (shipmentCount > 0) {
5960
+ throw new CrudHttpError(409, {
5961
+ error: translate(
5962
+ "sales.documents.items.errorDeleteShipped",
5963
+ "Cannot delete a line that has shipped items."
5964
+ )
5965
+ });
5966
+ }
5967
+ const existingLines = await em.find(
5968
+ SalesOrderLine,
5969
+ { order },
5970
+ { orderBy: { lineNumber: "asc" } }
5971
+ );
5972
+ const adjustments = await em.find(
5973
+ SalesOrderAdjustment,
5974
+ { order },
5975
+ { orderBy: { position: "asc" } }
5976
+ );
5977
+ const filtered = existingLines.filter((line) => line.id !== parsed.id);
5978
+ if (filtered.length === existingLines.length) {
5979
+ throw notFound(translate(
5980
+ "sales.documents.detail.error",
5981
+ "Document not found or inaccessible."
5982
+ ));
5983
+ }
5984
+ if (filtered.length === 0) {
5985
+ throw new CrudHttpError(409, {
5986
+ error: translate(
5987
+ "sales.documents.items.errorDeleteLast",
5988
+ "An order must contain at least one line item."
5989
+ )
5990
+ });
5991
+ }
5992
+ const totalsMode = order.totalsMode ?? "computed";
5993
+ if (isExternalAmountsMode(totalsMode)) {
5994
+ await requireOrderTotalsForExternalWrite(parsed.orderTotals);
5995
+ }
5996
+ const suppliedTotals = isExternalAmountsMode(totalsMode) ? buildExternalHeaderTotals(parsed.orderTotals) : null;
5997
+ const sourceInputs = filtered.map((line, index) => ({
5998
+ ...mapOrderLineEntityToSnapshot(line),
5999
+ statusEntryId: line.statusEntryId ?? null,
6000
+ catalogSnapshot: line.catalogSnapshot ? cloneJson(line.catalogSnapshot) : null,
6001
+ promotionSnapshot: line.promotionSnapshot ? cloneJson(line.promotionSnapshot) : null,
6002
+ organizationId: order.organizationId,
6003
+ tenantId: order.tenantId,
6004
+ orderId: order.id,
6005
+ lineNumber: index + 1
6006
+ }));
6007
+ const calcLines = sourceInputs.map(
6008
+ (line, index) => createLineSnapshotFromInput(line, line.lineNumber ?? index + 1)
6009
+ );
6010
+ const adjustmentDrafts = adjustments.map(mapOrderAdjustmentToDraft);
6011
+ const salesCalculationService = ctx.container.resolve("salesCalculationService");
6012
+ const calculationContext = buildCalculationContext({
6013
+ tenantId: order.tenantId,
6014
+ organizationId: order.organizationId,
6015
+ currencyCode: order.currencyCode,
6016
+ shippingSnapshot: order.shippingMethodSnapshot,
6017
+ paymentSnapshot: order.paymentMethodSnapshot,
6018
+ shippingMethodId: order.shippingMethodId ?? null,
6019
+ paymentMethodId: order.paymentMethodId ?? null,
6020
+ shippingMethodCode: order.shippingMethodCode ?? null,
6021
+ paymentMethodCode: order.paymentMethodCode ?? null
5919
6022
  });
5920
- }
5921
- const totalsMode = order.totalsMode ?? "computed";
5922
- if (isExternalAmountsMode(totalsMode)) {
5923
- await requireOrderTotalsForExternalWrite(parsed.orderTotals);
5924
- }
5925
- const suppliedTotals = isExternalAmountsMode(totalsMode) ? buildExternalHeaderTotals(parsed.orderTotals) : null;
5926
- const sourceInputs = filtered.map((line, index) => ({
5927
- ...mapOrderLineEntityToSnapshot(line),
5928
- statusEntryId: line.statusEntryId ?? null,
5929
- catalogSnapshot: line.catalogSnapshot ? cloneJson(line.catalogSnapshot) : null,
5930
- promotionSnapshot: line.promotionSnapshot ? cloneJson(line.promotionSnapshot) : null,
5931
- organizationId: order.organizationId,
5932
- tenantId: order.tenantId,
5933
- orderId: order.id,
5934
- lineNumber: index + 1
5935
- }));
5936
- const calcLines = sourceInputs.map(
5937
- (line, index) => createLineSnapshotFromInput(line, line.lineNumber ?? index + 1)
5938
- );
5939
- const adjustmentDrafts = adjustments.map(mapOrderAdjustmentToDraft);
5940
- const salesCalculationService = ctx.container.resolve("salesCalculationService");
5941
- const calculationContext = buildCalculationContext({
5942
- tenantId: order.tenantId,
5943
- organizationId: order.organizationId,
5944
- currencyCode: order.currencyCode,
5945
- shippingSnapshot: order.shippingMethodSnapshot,
5946
- paymentSnapshot: order.paymentMethodSnapshot,
5947
- shippingMethodId: order.shippingMethodId ?? null,
5948
- paymentMethodId: order.paymentMethodId ?? null,
5949
- shippingMethodCode: order.shippingMethodCode ?? null,
5950
- paymentMethodCode: order.paymentMethodCode ?? null
5951
- });
5952
- const calculation = await salesCalculationService.calculateDocumentTotals({
5953
- documentKind: "order",
5954
- lines: calcLines,
5955
- adjustments: adjustmentDrafts,
5956
- context: calculationContext,
5957
- existingTotals: resolveExistingPaymentTotals(order),
5958
- totalsMode,
5959
- suppliedTotals
6023
+ const calculation = await salesCalculationService.calculateDocumentTotals({
6024
+ documentKind: "order",
6025
+ lines: calcLines,
6026
+ adjustments: adjustmentDrafts,
6027
+ context: calculationContext,
6028
+ existingTotals: resolveExistingPaymentTotals(order),
6029
+ totalsMode,
6030
+ suppliedTotals
6031
+ });
6032
+ await applyOrderLineResults({
6033
+ em,
6034
+ order,
6035
+ calculation,
6036
+ sourceLines: sourceInputs,
6037
+ existingLines
6038
+ });
6039
+ applyOrderTotals(order, calculation.totals, calculation.lines.length);
6040
+ return { result: { orderId: order.id, lineId: parsed.id }, calculation };
5960
6041
  });
5961
- let eventBus = null;
5962
- try {
5963
- eventBus = ctx.container.resolve("eventBus");
5964
- } catch {
5965
- eventBus = null;
5966
- }
5967
- await withAtomicFlush(
5968
- em,
5969
- [
5970
- async () => {
5971
- await applyOrderLineResults({
5972
- em,
5973
- order,
5974
- calculation,
5975
- sourceLines: sourceInputs,
5976
- existingLines
5977
- });
5978
- applyOrderTotals(order, calculation.totals, calculation.lines.length);
5979
- await emitTotalsCalculated(eventBus, {
5980
- documentKind: "order",
5981
- documentId: order.id,
5982
- organizationId: order.organizationId,
5983
- tenantId: order.tenantId,
5984
- customerId: order.customerEntityId ?? null,
5985
- totals: calculation.totals,
5986
- lineCount: calculation.lines.length
5987
- });
5988
- }
5989
- ],
5990
- { transaction: true }
5991
- );
5992
- return { orderId: order.id, lineId: parsed.id };
5993
- },
5994
- captureAfter: async (_input, result, ctx) => {
5995
- const em = ctx.container.resolve("em").fork();
5996
- return loadOrderSnapshot(em, result.orderId);
5997
6042
  },
6043
+ captureAfter: async (_input, result, ctx) => captureLockedOrderGraphAfter(ctx, result, result.orderId),
5998
6044
  buildLog: async ({ result, snapshots }) => {
5999
- const before = snapshots.before;
6045
+ const before = resolveLockedOrderGraphBefore(result, snapshots.before);
6000
6046
  const after = snapshots.after;
6001
6047
  if (!after) return null;
6002
6048
  const { translate } = await resolveTranslations();
@@ -6025,107 +6071,67 @@ const orderLineDeleteCommand = {
6025
6071
  };
6026
6072
  const quoteLineUpsertCommand = {
6027
6073
  id: "sales.quotes.lines.upsert",
6028
- async prepare(input, ctx) {
6029
- const raw = input?.body ?? {};
6030
- const quoteId = typeof raw.quoteId === "string" ? raw.quoteId : null;
6031
- if (!quoteId) return {};
6032
- const em = ctx.container.resolve("em");
6033
- const snapshot = await loadQuoteSnapshot(em, quoteId);
6034
- if (snapshot)
6035
- ensureQuoteScope(
6036
- ctx,
6037
- snapshot.quote.organizationId,
6038
- snapshot.quote.tenantId
6039
- );
6040
- return snapshot ? { before: snapshot } : {};
6074
+ async prepare(input) {
6075
+ return markLockedGraphWrite(input);
6041
6076
  },
6042
6077
  async execute(input, ctx) {
6043
6078
  const rawBody = input?.body ?? {};
6044
6079
  const parsed = quoteLineUpsertSchema.parse(rawBody);
6045
6080
  await assertAmountsModeUnsupportedOnQuote([parsed]);
6046
- const em = ctx.container.resolve("em").fork();
6047
- const quote = await findOneWithDecryption(em, SalesQuote, {
6048
- id: parsed.quoteId,
6049
- deletedAt: null
6050
- });
6051
- if (!quote)
6052
- throw notFound("Sales quote not found");
6053
- ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
6054
- await enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
6055
- const [existingLines, adjustments] = await Promise.all([
6056
- em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
6057
- em.find(
6058
- SalesQuoteAdjustment,
6059
- { quote },
6060
- { orderBy: { position: "asc" } }
6061
- )
6062
- ]);
6063
- const lineSnapshots = existingLines.map(mapQuoteLineEntityToSnapshot);
6064
- const existingSnapshot = parsed.id ? lineSnapshots.find((line) => line.id === parsed.id) ?? null : null;
6065
- const priceMode = parsed.priceMode === "gross" ? "gross" : parsed.priceMode === "net" ? "net" : null;
6066
- let unitPriceNet = parsed.unitPriceNet ?? existingSnapshot?.unitPriceNet ?? null;
6067
- let unitPriceGross = parsed.unitPriceGross ?? existingSnapshot?.unitPriceGross ?? null;
6068
- let taxRate = parsed.taxRate ?? existingSnapshot?.taxRate ?? null;
6069
- if (priceMode && (unitPriceNet === null || unitPriceGross === null)) {
6070
- let taxService = null;
6071
- try {
6072
- taxService = ctx.container.resolve(
6073
- "taxCalculationService"
6074
- );
6075
- } catch {
6076
- taxService = null;
6077
- }
6078
- if (taxService) {
6079
- const taxResult = await taxService.calculateUnitAmounts({
6080
- amount: priceMode === "gross" ? unitPriceGross ?? unitPriceNet ?? 0 : unitPriceNet ?? unitPriceGross ?? 0,
6081
- mode: priceMode,
6082
- organizationId: parsed.organizationId,
6083
- tenantId: parsed.tenantId,
6084
- taxRateId: parsed.taxRateId ?? void 0,
6085
- taxRate: taxRate ?? void 0
6086
- });
6087
- unitPriceNet = unitPriceNet ?? taxResult.netAmount;
6088
- unitPriceGross = unitPriceGross ?? taxResult.grossAmount;
6089
- taxRate = taxResult.taxRate ?? taxRate;
6090
- }
6091
- }
6092
- const metadata = typeof parsed.metadata === "object" && parsed.metadata ? { ...parsed.metadata } : existingSnapshot?.metadata ? cloneJson(existingSnapshot.metadata) : {};
6093
- if (parsed.priceId) metadata.priceId = parsed.priceId;
6094
- if (priceMode) metadata.priceMode = priceMode;
6095
- const statusEntryId = parsed.statusEntryId ?? existingSnapshot?.statusEntryId ?? null;
6096
- const lineId = parsed.id ?? existingSnapshot?.id ?? randomUUID();
6097
- const lineUomInput = {
6098
- productId: parsed.productId ?? existingSnapshot?.productId ?? null,
6099
- productVariantId: parsed.productVariantId ?? existingSnapshot?.productVariantId ?? null,
6100
- quantity: parsed.quantity ?? existingSnapshot?.quantity ?? 0,
6101
- quantityUnit: parsed.quantityUnit ?? existingSnapshot?.quantityUnit ?? null,
6102
- normalizedQuantity: existingSnapshot?.normalizedQuantity ?? null,
6103
- normalizedUnit: existingSnapshot?.normalizedUnit ?? null,
6104
- uomSnapshot: existingSnapshot?.uomSnapshot ?? null
6105
- };
6106
- const uomResolver = createUomResolver();
6107
- let normalizedUom = await normalizeLineUom({
6108
- em,
6109
- resolver: uomResolver,
6110
- organizationId: quote.organizationId,
6111
- tenantId: quote.tenantId,
6112
- line: {
6113
- ...lineUomInput,
6114
- unitPriceNet: unitPriceNet ?? 0,
6115
- unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0
6081
+ return writeLockedQuoteGraph(input, ctx, parsed.quoteId, "Sales quote not found", async (em, quote) => {
6082
+ const [existingLines, adjustments] = await Promise.all([
6083
+ em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
6084
+ em.find(
6085
+ SalesQuoteAdjustment,
6086
+ { quote },
6087
+ { orderBy: { position: "asc" } }
6088
+ )
6089
+ ]);
6090
+ const lineSnapshots = existingLines.map(mapQuoteLineEntityToSnapshot);
6091
+ const existingSnapshot = parsed.id ? lineSnapshots.find((line) => line.id === parsed.id) ?? null : null;
6092
+ const priceMode = parsed.priceMode === "gross" ? "gross" : parsed.priceMode === "net" ? "net" : null;
6093
+ let unitPriceNet = parsed.unitPriceNet ?? existingSnapshot?.unitPriceNet ?? null;
6094
+ let unitPriceGross = parsed.unitPriceGross ?? existingSnapshot?.unitPriceGross ?? null;
6095
+ let taxRate = parsed.taxRate ?? existingSnapshot?.taxRate ?? null;
6096
+ if (priceMode && (unitPriceNet === null || unitPriceGross === null)) {
6097
+ let taxService = null;
6098
+ try {
6099
+ taxService = ctx.container.resolve(
6100
+ "taxCalculationService"
6101
+ );
6102
+ } catch {
6103
+ taxService = null;
6104
+ }
6105
+ if (taxService) {
6106
+ const taxResult = await taxService.calculateUnitAmounts({
6107
+ amount: priceMode === "gross" ? unitPriceGross ?? unitPriceNet ?? 0 : unitPriceNet ?? unitPriceGross ?? 0,
6108
+ mode: priceMode,
6109
+ organizationId: parsed.organizationId,
6110
+ tenantId: parsed.tenantId,
6111
+ taxRateId: parsed.taxRateId ?? void 0,
6112
+ taxRate: taxRate ?? void 0
6113
+ });
6114
+ unitPriceNet = unitPriceNet ?? taxResult.netAmount;
6115
+ unitPriceGross = unitPriceGross ?? taxResult.grossAmount;
6116
+ taxRate = taxResult.taxRate ?? taxRate;
6117
+ }
6116
6118
  }
6117
- });
6118
- const convertedPrices = convertLineUnitPricesOnUnitChange({
6119
- existingSnapshot,
6120
- nextQuantityUnit: normalizedUom.quantityUnit,
6121
- nextUomSnapshot: normalizedUom.uomSnapshot,
6122
- unitPriceNet,
6123
- unitPriceGross
6124
- });
6125
- if (convertedPrices.didConvert) {
6126
- unitPriceNet = convertedPrices.unitPriceNet;
6127
- unitPriceGross = convertedPrices.unitPriceGross;
6128
- normalizedUom = await normalizeLineUom({
6119
+ const metadata = typeof parsed.metadata === "object" && parsed.metadata ? { ...parsed.metadata } : existingSnapshot?.metadata ? cloneJson(existingSnapshot.metadata) : {};
6120
+ if (parsed.priceId) metadata.priceId = parsed.priceId;
6121
+ if (priceMode) metadata.priceMode = priceMode;
6122
+ const statusEntryId = parsed.statusEntryId ?? existingSnapshot?.statusEntryId ?? null;
6123
+ const lineId = parsed.id ?? existingSnapshot?.id ?? randomUUID();
6124
+ const lineUomInput = {
6125
+ productId: parsed.productId ?? existingSnapshot?.productId ?? null,
6126
+ productVariantId: parsed.productVariantId ?? existingSnapshot?.productVariantId ?? null,
6127
+ quantity: parsed.quantity ?? existingSnapshot?.quantity ?? 0,
6128
+ quantityUnit: parsed.quantityUnit ?? existingSnapshot?.quantityUnit ?? null,
6129
+ normalizedQuantity: existingSnapshot?.normalizedQuantity ?? null,
6130
+ normalizedUnit: existingSnapshot?.normalizedUnit ?? null,
6131
+ uomSnapshot: existingSnapshot?.uomSnapshot ?? null
6132
+ };
6133
+ const uomResolver = createUomResolver();
6134
+ let normalizedUom = await normalizeLineUom({
6129
6135
  em,
6130
6136
  resolver: uomResolver,
6131
6137
  organizationId: quote.organizationId,
@@ -6136,121 +6142,117 @@ const quoteLineUpsertCommand = {
6136
6142
  unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0
6137
6143
  }
6138
6144
  });
6139
- }
6140
- const updatedSnapshot = {
6141
- id: lineId,
6142
- lineNumber: parsed.lineNumber ?? existingSnapshot?.lineNumber ?? lineSnapshots.length + 1,
6143
- kind: parsed.kind ?? existingSnapshot?.kind ?? "product",
6144
- productId: parsed.productId ?? existingSnapshot?.productId ?? null,
6145
- productVariantId: parsed.productVariantId ?? existingSnapshot?.productVariantId ?? null,
6146
- name: parsed.name ?? existingSnapshot?.name ?? null,
6147
- description: parsed.description ?? existingSnapshot?.description ?? null,
6148
- comment: parsed.comment ?? existingSnapshot?.comment ?? null,
6149
- quantity: normalizedUom.quantity,
6150
- quantityUnit: normalizedUom.quantityUnit,
6151
- normalizedQuantity: normalizedUom.normalizedQuantity,
6152
- normalizedUnit: normalizedUom.normalizedUnit,
6153
- uomSnapshot: normalizedUom.uomSnapshot ? cloneJson(normalizedUom.uomSnapshot) : null,
6154
- currencyCode: parsed.currencyCode ?? existingSnapshot?.currencyCode ?? quote.currencyCode,
6155
- unitPriceNet: unitPriceNet ?? 0,
6156
- unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0,
6157
- ...resolveUpsertDiscountFields(
6158
- parsed.discountAmount,
6159
- parsed.discountAmountBasis,
6160
- existingSnapshot
6161
- ),
6162
- discountPercent: parsed.discountPercent ?? existingSnapshot?.discountPercent ?? 0,
6163
- taxRate: taxRate ?? 0,
6164
- totalNetAmount: parsed.totalNetAmount ?? existingSnapshot?.totalNetAmount ?? null,
6165
- ...resolveUpsertTotalsOrigin(parsed.totalNetAmount, existingSnapshot),
6166
- configuration: parsed.configuration ?? existingSnapshot?.configuration ?? null,
6167
- promotionCode: parsed.promotionCode ?? existingSnapshot?.promotionCode ?? null,
6168
- metadata,
6169
- customFieldSetId: parsed.customFieldSetId ?? existingSnapshot?.customFieldSetId ?? null,
6170
- customFields: parsed.customFields && typeof parsed.customFields === "object" ? cloneJson(parsed.customFields) : existingSnapshot?.customFields ?? null
6171
- };
6172
- Object.assign(
6173
- updatedSnapshot,
6174
- resolveUpsertCalculatedAmounts(parsed, updatedSnapshot, existingSnapshot)
6175
- );
6176
- updatedSnapshot.statusEntryId = statusEntryId;
6177
- updatedSnapshot.catalogSnapshot = parsed.catalogSnapshot ?? existingSnapshot?.catalogSnapshot ?? null;
6178
- updatedSnapshot.promotionSnapshot = parsed.promotionSnapshot ?? existingSnapshot?.promotionSnapshot ?? null;
6179
- let nextLines = parsed.id ? lineSnapshots.map(
6180
- (line) => line.id === parsed.id ? updatedSnapshot : line
6181
- ) : [...lineSnapshots, updatedSnapshot];
6182
- nextLines = nextLines.sort((a, b) => (a.lineNumber ?? 0) - (b.lineNumber ?? 0)).map((line, index) => ({ ...line, lineNumber: index + 1 }));
6183
- const sourceInputs = nextLines.map((line, index) => ({
6184
- ...line,
6185
- statusEntryId: line.statusEntryId ?? null,
6186
- catalogSnapshot: line.catalogSnapshot ?? null,
6187
- promotionSnapshot: line.promotionSnapshot ?? null,
6188
- organizationId: quote.organizationId,
6189
- tenantId: quote.tenantId,
6190
- quoteId: quote.id,
6191
- lineNumber: line.lineNumber ?? index + 1
6192
- }));
6193
- const calcLines = sourceInputs.map(
6194
- (line, index) => createLineSnapshotFromInput(line, line.lineNumber ?? index + 1)
6195
- );
6196
- const adjustmentDrafts = adjustments.map(mapQuoteAdjustmentToDraft);
6197
- const salesCalculationService = ctx.container.resolve("salesCalculationService");
6198
- const calculationContext = buildCalculationContext({
6199
- tenantId: quote.tenantId,
6200
- organizationId: quote.organizationId,
6201
- currencyCode: quote.currencyCode,
6202
- shippingSnapshot: quote.shippingMethodSnapshot,
6203
- paymentSnapshot: quote.paymentMethodSnapshot,
6204
- shippingMethodId: quote.shippingMethodId ?? null,
6205
- paymentMethodId: quote.paymentMethodId ?? null,
6206
- shippingMethodCode: quote.shippingMethodCode ?? null,
6207
- paymentMethodCode: quote.paymentMethodCode ?? null
6208
- });
6209
- const calculation = await salesCalculationService.calculateDocumentTotals({
6210
- documentKind: "quote",
6211
- lines: calcLines,
6212
- adjustments: adjustmentDrafts,
6213
- context: calculationContext
6145
+ const convertedPrices = convertLineUnitPricesOnUnitChange({
6146
+ existingSnapshot,
6147
+ nextQuantityUnit: normalizedUom.quantityUnit,
6148
+ nextUomSnapshot: normalizedUom.uomSnapshot,
6149
+ unitPriceNet,
6150
+ unitPriceGross
6151
+ });
6152
+ if (convertedPrices.didConvert) {
6153
+ unitPriceNet = convertedPrices.unitPriceNet;
6154
+ unitPriceGross = convertedPrices.unitPriceGross;
6155
+ normalizedUom = await normalizeLineUom({
6156
+ em,
6157
+ resolver: uomResolver,
6158
+ organizationId: quote.organizationId,
6159
+ tenantId: quote.tenantId,
6160
+ line: {
6161
+ ...lineUomInput,
6162
+ unitPriceNet: unitPriceNet ?? 0,
6163
+ unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0
6164
+ }
6165
+ });
6166
+ }
6167
+ const updatedSnapshot = {
6168
+ id: lineId,
6169
+ lineNumber: parsed.lineNumber ?? existingSnapshot?.lineNumber ?? lineSnapshots.length + 1,
6170
+ kind: parsed.kind ?? existingSnapshot?.kind ?? "product",
6171
+ productId: parsed.productId ?? existingSnapshot?.productId ?? null,
6172
+ productVariantId: parsed.productVariantId ?? existingSnapshot?.productVariantId ?? null,
6173
+ name: parsed.name ?? existingSnapshot?.name ?? null,
6174
+ description: parsed.description ?? existingSnapshot?.description ?? null,
6175
+ comment: parsed.comment ?? existingSnapshot?.comment ?? null,
6176
+ quantity: normalizedUom.quantity,
6177
+ quantityUnit: normalizedUom.quantityUnit,
6178
+ normalizedQuantity: normalizedUom.normalizedQuantity,
6179
+ normalizedUnit: normalizedUom.normalizedUnit,
6180
+ uomSnapshot: normalizedUom.uomSnapshot ? cloneJson(normalizedUom.uomSnapshot) : null,
6181
+ currencyCode: parsed.currencyCode ?? existingSnapshot?.currencyCode ?? quote.currencyCode,
6182
+ unitPriceNet: unitPriceNet ?? 0,
6183
+ unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0,
6184
+ ...resolveUpsertDiscountFields(
6185
+ parsed.discountAmount,
6186
+ parsed.discountAmountBasis,
6187
+ existingSnapshot
6188
+ ),
6189
+ discountPercent: parsed.discountPercent ?? existingSnapshot?.discountPercent ?? 0,
6190
+ taxRate: taxRate ?? 0,
6191
+ totalNetAmount: parsed.totalNetAmount ?? existingSnapshot?.totalNetAmount ?? null,
6192
+ ...resolveUpsertTotalsOrigin(parsed.totalNetAmount, existingSnapshot),
6193
+ configuration: parsed.configuration ?? existingSnapshot?.configuration ?? null,
6194
+ promotionCode: parsed.promotionCode ?? existingSnapshot?.promotionCode ?? null,
6195
+ metadata,
6196
+ customFieldSetId: parsed.customFieldSetId ?? existingSnapshot?.customFieldSetId ?? null,
6197
+ customFields: parsed.customFields && typeof parsed.customFields === "object" ? cloneJson(parsed.customFields) : existingSnapshot?.customFields ?? null
6198
+ };
6199
+ Object.assign(
6200
+ updatedSnapshot,
6201
+ resolveUpsertCalculatedAmounts(parsed, updatedSnapshot, existingSnapshot)
6202
+ );
6203
+ updatedSnapshot.statusEntryId = statusEntryId;
6204
+ updatedSnapshot.catalogSnapshot = parsed.catalogSnapshot ?? existingSnapshot?.catalogSnapshot ?? null;
6205
+ updatedSnapshot.promotionSnapshot = parsed.promotionSnapshot ?? existingSnapshot?.promotionSnapshot ?? null;
6206
+ let nextLines = parsed.id ? lineSnapshots.map(
6207
+ (line) => line.id === parsed.id ? updatedSnapshot : line
6208
+ ) : [...lineSnapshots, updatedSnapshot];
6209
+ nextLines = nextLines.sort((a, b) => (a.lineNumber ?? 0) - (b.lineNumber ?? 0)).map((line, index) => ({ ...line, lineNumber: index + 1 }));
6210
+ const sourceInputs = nextLines.map((line, index) => ({
6211
+ ...line,
6212
+ statusEntryId: line.statusEntryId ?? null,
6213
+ catalogSnapshot: line.catalogSnapshot ?? null,
6214
+ promotionSnapshot: line.promotionSnapshot ?? null,
6215
+ organizationId: quote.organizationId,
6216
+ tenantId: quote.tenantId,
6217
+ quoteId: quote.id,
6218
+ lineNumber: line.lineNumber ?? index + 1
6219
+ }));
6220
+ const calcLines = sourceInputs.map(
6221
+ (line, index) => createLineSnapshotFromInput(line, line.lineNumber ?? index + 1)
6222
+ );
6223
+ const adjustmentDrafts = adjustments.map(mapQuoteAdjustmentToDraft);
6224
+ const salesCalculationService = ctx.container.resolve("salesCalculationService");
6225
+ const calculationContext = buildCalculationContext({
6226
+ tenantId: quote.tenantId,
6227
+ organizationId: quote.organizationId,
6228
+ currencyCode: quote.currencyCode,
6229
+ shippingSnapshot: quote.shippingMethodSnapshot,
6230
+ paymentSnapshot: quote.paymentMethodSnapshot,
6231
+ shippingMethodId: quote.shippingMethodId ?? null,
6232
+ paymentMethodId: quote.paymentMethodId ?? null,
6233
+ shippingMethodCode: quote.shippingMethodCode ?? null,
6234
+ paymentMethodCode: quote.paymentMethodCode ?? null
6235
+ });
6236
+ const calculation = await salesCalculationService.calculateDocumentTotals({
6237
+ documentKind: "quote",
6238
+ lines: calcLines,
6239
+ adjustments: adjustmentDrafts,
6240
+ context: calculationContext
6241
+ });
6242
+ await applyQuoteLineResults({
6243
+ em,
6244
+ quote,
6245
+ calculation,
6246
+ sourceLines: sourceInputs,
6247
+ existingLines
6248
+ });
6249
+ applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
6250
+ return { result: { quoteId: quote.id, lineId }, calculation };
6214
6251
  });
6215
- let eventBus = null;
6216
- try {
6217
- eventBus = ctx.container.resolve("eventBus");
6218
- } catch {
6219
- eventBus = null;
6220
- }
6221
- await withAtomicFlush(
6222
- em,
6223
- [
6224
- async () => {
6225
- await applyQuoteLineResults({
6226
- em,
6227
- quote,
6228
- calculation,
6229
- sourceLines: sourceInputs,
6230
- existingLines
6231
- });
6232
- applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
6233
- await emitTotalsCalculated(eventBus, {
6234
- documentKind: "quote",
6235
- documentId: quote.id,
6236
- organizationId: quote.organizationId,
6237
- tenantId: quote.tenantId,
6238
- customerId: quote.customerEntityId ?? null,
6239
- totals: calculation.totals,
6240
- lineCount: calculation.lines.length
6241
- });
6242
- }
6243
- ],
6244
- { transaction: true }
6245
- );
6246
- return { quoteId: quote.id, lineId };
6247
- },
6248
- captureAfter: async (_input, result, ctx) => {
6249
- const em = ctx.container.resolve("em").fork();
6250
- return loadQuoteSnapshot(em, result.quoteId);
6251
6252
  },
6253
+ captureAfter: async (_input, result, ctx) => captureLockedQuoteGraphAfter(ctx, result, result.quoteId),
6252
6254
  buildLog: async ({ result, snapshots }) => {
6253
- const before = snapshots.before;
6255
+ const before = resolveLockedQuoteGraphBefore(result, snapshots.before);
6254
6256
  const after = snapshots.after;
6255
6257
  if (!after) return null;
6256
6258
  const { translate } = await resolveTranslations();
@@ -6279,118 +6281,74 @@ const quoteLineUpsertCommand = {
6279
6281
  };
6280
6282
  const quoteLineDeleteCommand = {
6281
6283
  id: "sales.quotes.lines.delete",
6282
- async prepare(input, ctx) {
6283
- const raw = input?.body ?? {};
6284
- const quoteId = typeof raw.quoteId === "string" ? raw.quoteId : null;
6285
- if (!quoteId) return {};
6286
- const em = ctx.container.resolve("em");
6287
- const snapshot = await loadQuoteSnapshot(em, quoteId);
6288
- if (snapshot)
6289
- ensureQuoteScope(
6290
- ctx,
6291
- snapshot.quote.organizationId,
6292
- snapshot.quote.tenantId
6293
- );
6294
- return snapshot ? { before: snapshot } : {};
6284
+ async prepare(input) {
6285
+ return markLockedGraphWrite(input);
6295
6286
  },
6296
6287
  async execute(input, ctx) {
6297
6288
  const parsed = quoteLineDeleteSchema.parse(
6298
6289
  input?.body ?? {}
6299
6290
  );
6300
- const em = ctx.container.resolve("em").fork();
6301
- const quote = await findOneWithDecryption(em, SalesQuote, {
6302
- id: parsed.quoteId,
6303
- deletedAt: null
6304
- });
6305
- if (!quote)
6306
- throw notFound("Sales quote not found");
6307
- ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
6308
- await enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
6309
- const existingLines = await em.find(
6310
- SalesQuoteLine,
6311
- { quote },
6312
- { orderBy: { lineNumber: "asc" } }
6313
- );
6314
- const adjustments = await em.find(
6315
- SalesQuoteAdjustment,
6316
- { quote },
6317
- { orderBy: { position: "asc" } }
6318
- );
6319
- const filtered = existingLines.filter((line) => line.id !== parsed.id);
6320
- if (filtered.length === existingLines.length) {
6321
- throw notFound("Quote line not found");
6322
- }
6323
- const sourceInputs = filtered.map((line, index) => ({
6324
- ...mapQuoteLineEntityToSnapshot(line),
6325
- statusEntryId: line.statusEntryId ?? null,
6326
- catalogSnapshot: line.catalogSnapshot ? cloneJson(line.catalogSnapshot) : null,
6327
- promotionSnapshot: line.promotionSnapshot ? cloneJson(line.promotionSnapshot) : null,
6328
- organizationId: quote.organizationId,
6329
- tenantId: quote.tenantId,
6330
- quoteId: quote.id,
6331
- lineNumber: index + 1
6332
- }));
6333
- const calcLines = sourceInputs.map(
6334
- (line, index) => createLineSnapshotFromInput(line, line.lineNumber ?? index + 1)
6335
- );
6336
- const adjustmentDrafts = adjustments.map(mapQuoteAdjustmentToDraft);
6337
- const salesCalculationService = ctx.container.resolve("salesCalculationService");
6338
- const calculationContext = buildCalculationContext({
6339
- tenantId: quote.tenantId,
6340
- organizationId: quote.organizationId,
6341
- currencyCode: quote.currencyCode,
6342
- shippingSnapshot: quote.shippingMethodSnapshot,
6343
- paymentSnapshot: quote.paymentMethodSnapshot,
6344
- shippingMethodId: quote.shippingMethodId ?? null,
6345
- paymentMethodId: quote.paymentMethodId ?? null,
6346
- shippingMethodCode: quote.shippingMethodCode ?? null,
6347
- paymentMethodCode: quote.paymentMethodCode ?? null
6348
- });
6349
- const calculation = await salesCalculationService.calculateDocumentTotals({
6350
- documentKind: "quote",
6351
- lines: calcLines,
6352
- adjustments: adjustmentDrafts,
6353
- context: calculationContext
6291
+ return writeLockedQuoteGraph(input, ctx, parsed.quoteId, "Sales quote not found", async (em, quote) => {
6292
+ const existingLines = await em.find(
6293
+ SalesQuoteLine,
6294
+ { quote },
6295
+ { orderBy: { lineNumber: "asc" } }
6296
+ );
6297
+ const adjustments = await em.find(
6298
+ SalesQuoteAdjustment,
6299
+ { quote },
6300
+ { orderBy: { position: "asc" } }
6301
+ );
6302
+ const filtered = existingLines.filter((line) => line.id !== parsed.id);
6303
+ if (filtered.length === existingLines.length) {
6304
+ throw notFound("Quote line not found");
6305
+ }
6306
+ const sourceInputs = filtered.map((line, index) => ({
6307
+ ...mapQuoteLineEntityToSnapshot(line),
6308
+ statusEntryId: line.statusEntryId ?? null,
6309
+ catalogSnapshot: line.catalogSnapshot ? cloneJson(line.catalogSnapshot) : null,
6310
+ promotionSnapshot: line.promotionSnapshot ? cloneJson(line.promotionSnapshot) : null,
6311
+ organizationId: quote.organizationId,
6312
+ tenantId: quote.tenantId,
6313
+ quoteId: quote.id,
6314
+ lineNumber: index + 1
6315
+ }));
6316
+ const calcLines = sourceInputs.map(
6317
+ (line, index) => createLineSnapshotFromInput(line, line.lineNumber ?? index + 1)
6318
+ );
6319
+ const adjustmentDrafts = adjustments.map(mapQuoteAdjustmentToDraft);
6320
+ const salesCalculationService = ctx.container.resolve("salesCalculationService");
6321
+ const calculationContext = buildCalculationContext({
6322
+ tenantId: quote.tenantId,
6323
+ organizationId: quote.organizationId,
6324
+ currencyCode: quote.currencyCode,
6325
+ shippingSnapshot: quote.shippingMethodSnapshot,
6326
+ paymentSnapshot: quote.paymentMethodSnapshot,
6327
+ shippingMethodId: quote.shippingMethodId ?? null,
6328
+ paymentMethodId: quote.paymentMethodId ?? null,
6329
+ shippingMethodCode: quote.shippingMethodCode ?? null,
6330
+ paymentMethodCode: quote.paymentMethodCode ?? null
6331
+ });
6332
+ const calculation = await salesCalculationService.calculateDocumentTotals({
6333
+ documentKind: "quote",
6334
+ lines: calcLines,
6335
+ adjustments: adjustmentDrafts,
6336
+ context: calculationContext
6337
+ });
6338
+ await applyQuoteLineResults({
6339
+ em,
6340
+ quote,
6341
+ calculation,
6342
+ sourceLines: sourceInputs,
6343
+ existingLines
6344
+ });
6345
+ applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
6346
+ return { result: { quoteId: quote.id, lineId: parsed.id }, calculation };
6354
6347
  });
6355
- let eventBus = null;
6356
- try {
6357
- eventBus = ctx.container.resolve("eventBus");
6358
- } catch {
6359
- eventBus = null;
6360
- }
6361
- await withAtomicFlush(
6362
- em,
6363
- [
6364
- async () => {
6365
- await applyQuoteLineResults({
6366
- em,
6367
- quote,
6368
- calculation,
6369
- sourceLines: sourceInputs,
6370
- existingLines
6371
- });
6372
- applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
6373
- await emitTotalsCalculated(eventBus, {
6374
- documentKind: "quote",
6375
- documentId: quote.id,
6376
- organizationId: quote.organizationId,
6377
- tenantId: quote.tenantId,
6378
- customerId: quote.customerEntityId ?? null,
6379
- totals: calculation.totals,
6380
- lineCount: calculation.lines.length
6381
- });
6382
- }
6383
- ],
6384
- { transaction: true }
6385
- );
6386
- return { quoteId: quote.id, lineId: parsed.id };
6387
- },
6388
- captureAfter: async (_input, result, ctx) => {
6389
- const em = ctx.container.resolve("em").fork();
6390
- return loadQuoteSnapshot(em, result.quoteId);
6391
6348
  },
6349
+ captureAfter: async (_input, result, ctx) => captureLockedQuoteGraphAfter(ctx, result, result.quoteId),
6392
6350
  buildLog: async ({ result, snapshots }) => {
6393
- const before = snapshots.before;
6351
+ const before = resolveLockedQuoteGraphBefore(result, snapshots.before);
6394
6352
  const after = snapshots.after;
6395
6353
  if (!after) return null;
6396
6354
  const { translate } = await resolveTranslations();
@@ -6419,229 +6377,185 @@ const quoteLineDeleteCommand = {
6419
6377
  };
6420
6378
  const orderAdjustmentUpsertCommand = {
6421
6379
  id: "sales.orders.adjustments.upsert",
6422
- async prepare(input, ctx) {
6423
- const raw = input?.body ?? {};
6424
- const orderId = typeof raw.orderId === "string" ? raw.orderId : null;
6425
- if (!orderId) return {};
6426
- const em = ctx.container.resolve("em");
6427
- const snapshot = await loadOrderSnapshot(em, orderId);
6428
- if (snapshot)
6429
- ensureOrderScope(
6430
- ctx,
6431
- snapshot.order.organizationId,
6432
- snapshot.order.tenantId
6433
- );
6434
- return snapshot ? { before: snapshot } : {};
6380
+ async prepare(input) {
6381
+ return markLockedGraphWrite(input);
6435
6382
  },
6436
6383
  async execute(input, ctx) {
6437
6384
  const parsed = orderAdjustmentUpsertSchema.parse(
6438
6385
  input?.body ?? {}
6439
6386
  );
6440
- const em = ctx.container.resolve("em").fork();
6441
- const order = await findOneWithDecryption(em, SalesOrder, {
6442
- id: parsed.orderId,
6443
- deletedAt: null
6444
- });
6445
- if (!order)
6446
- throw notFound("Sales order not found");
6447
- ensureOrderScope(ctx, order.organizationId, order.tenantId);
6448
- await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
6449
- if (isExternalAmountsMode(order.totalsMode ?? "computed")) await refuseOnExternalOrder();
6450
- if (parsed.scope === "line") {
6451
- throw new CrudHttpError(400, {
6452
- error: "Line-scoped adjustments are not supported yet."
6453
- });
6454
- }
6455
- const [existingLines, existingAdjustments] = await Promise.all([
6456
- em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
6457
- em.find(
6458
- SalesOrderAdjustment,
6459
- { order },
6460
- { orderBy: { position: "asc" } }
6461
- )
6462
- ]);
6463
- const lineSnapshots = existingLines.map(mapOrderLineEntityToSnapshot);
6464
- const adjustmentDrafts = existingAdjustments.map(mapOrderAdjustmentToDraft);
6465
- const existingSnapshot = parsed.id ? adjustmentDrafts.find((adj) => adj.id === parsed.id) ?? null : null;
6466
- const adjustmentId = parsed.id ?? existingSnapshot?.id ?? randomUUID();
6467
- let metadata = typeof parsed.metadata === "object" && parsed.metadata ? cloneJson(parsed.metadata) : existingSnapshot?.metadata ? cloneJson(existingSnapshot.metadata) : null;
6468
- const calculatorKey = parsed.calculatorKey ?? existingSnapshot?.calculatorKey ?? null;
6469
- if (parsed.id && calculatorKey && (calculatorKey.startsWith("shipping-provider:") || calculatorKey.startsWith("payment-provider:"))) {
6470
- metadata = { ...metadata ?? {}, manualOverride: true };
6471
- }
6472
- let nextAdjustments = parsed.id ? adjustmentDrafts.map(
6473
- (adj) => adj.id === parsed.id ? {
6474
- ...adj,
6475
- id: adjustmentId,
6476
- scope: parsed.scope ?? adj.scope ?? "order",
6477
- kind: parsed.kind ?? adj.kind ?? "custom",
6478
- code: parsed.code ?? adj.code ?? null,
6479
- label: parsed.label ?? adj.label ?? null,
6480
- calculatorKey: parsed.calculatorKey ?? adj.calculatorKey ?? null,
6481
- promotionId: parsed.promotionId ?? adj.promotionId ?? null,
6482
- rate: parsed.rate ?? adj.rate ?? null,
6483
- amountNet: parsed.amountNet ?? adj.amountNet ?? null,
6484
- amountGross: parsed.amountGross ?? adj.amountGross ?? null,
6485
- currencyCode: parsed.currencyCode ?? adj.currencyCode ?? order.currencyCode,
6486
- metadata,
6487
- customFields: parsed.customFields !== void 0 ? parsed.customFields : adj.customFields ?? null,
6488
- position: parsed.position ?? adj.position ?? adjustmentDrafts.length
6489
- } : adj
6490
- ) : [
6491
- ...adjustmentDrafts,
6492
- {
6493
- id: adjustmentId,
6494
- scope: parsed.scope ?? "order",
6495
- kind: parsed.kind ?? "custom",
6496
- code: parsed.code ?? null,
6497
- label: parsed.label ?? null,
6498
- calculatorKey: parsed.calculatorKey ?? null,
6499
- promotionId: parsed.promotionId ?? null,
6500
- rate: parsed.rate ?? null,
6501
- amountNet: parsed.amountNet ?? null,
6502
- amountGross: parsed.amountGross ?? null,
6503
- currencyCode: parsed.currencyCode ?? order.currencyCode,
6504
- metadata,
6505
- customFields: parsed.customFields ?? null,
6506
- position: parsed.position ?? adjustmentDrafts.length
6387
+ return writeLockedOrderGraph(input, ctx, parsed.orderId, "Sales order not found", async (em, order) => {
6388
+ if (isExternalAmountsMode(order.totalsMode ?? "computed")) await refuseOnExternalOrder();
6389
+ if (parsed.scope === "line") {
6390
+ throw new CrudHttpError(400, {
6391
+ error: "Line-scoped adjustments are not supported yet."
6392
+ });
6507
6393
  }
6508
- ];
6509
- nextAdjustments = nextAdjustments.sort((a, b) => (a.position ?? 0) - (b.position ?? 0)).map((adj, index) => ({ ...adj, position: adj.position ?? index }));
6510
- const sourceLines = lineSnapshots.map((line, index) => ({
6511
- ...line,
6512
- statusEntryId: line.statusEntryId ?? null,
6513
- catalogSnapshot: line.catalogSnapshot ?? null,
6514
- promotionSnapshot: line.promotionSnapshot ?? null,
6515
- organizationId: order.organizationId,
6516
- tenantId: order.tenantId,
6517
- orderId: order.id,
6518
- lineNumber: line.lineNumber ?? index + 1
6519
- }));
6520
- const calcLines = sourceLines.map(
6521
- (line, index) => createLineSnapshotFromInput(line, line.lineNumber ?? index + 1)
6522
- );
6523
- const salesCalculationService = ctx.container.resolve("salesCalculationService");
6524
- const calculationContext = buildCalculationContext({
6525
- tenantId: order.tenantId,
6526
- organizationId: order.organizationId,
6527
- currencyCode: order.currencyCode,
6528
- shippingSnapshot: order.shippingMethodSnapshot,
6529
- paymentSnapshot: order.paymentMethodSnapshot,
6530
- shippingMethodId: order.shippingMethodId ?? null,
6531
- paymentMethodId: order.paymentMethodId ?? null,
6532
- shippingMethodCode: order.shippingMethodCode ?? null,
6533
- paymentMethodCode: order.paymentMethodCode ?? null
6534
- });
6535
- const effectiveAdjustment = nextAdjustments.find(
6536
- (adj) => adj.id === adjustmentId
6537
- );
6538
- if (effectiveAdjustment?.kind === "return") {
6539
- const baselineAdjustments = nextAdjustments.filter(
6540
- (adj) => adj.id !== adjustmentId
6394
+ const [existingLines, existingAdjustments] = await Promise.all([
6395
+ em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
6396
+ em.find(
6397
+ SalesOrderAdjustment,
6398
+ { order },
6399
+ { orderBy: { position: "asc" } }
6400
+ )
6401
+ ]);
6402
+ const lineSnapshots = existingLines.map(mapOrderLineEntityToSnapshot);
6403
+ const adjustmentDrafts = existingAdjustments.map(mapOrderAdjustmentToDraft);
6404
+ const existingSnapshot = parsed.id ? adjustmentDrafts.find((adj) => adj.id === parsed.id) ?? null : null;
6405
+ const adjustmentId = parsed.id ?? existingSnapshot?.id ?? randomUUID();
6406
+ let metadata = typeof parsed.metadata === "object" && parsed.metadata ? cloneJson(parsed.metadata) : existingSnapshot?.metadata ? cloneJson(existingSnapshot.metadata) : null;
6407
+ const calculatorKey = parsed.calculatorKey ?? existingSnapshot?.calculatorKey ?? null;
6408
+ if (parsed.id && calculatorKey && (calculatorKey.startsWith("shipping-provider:") || calculatorKey.startsWith("payment-provider:"))) {
6409
+ metadata = { ...metadata ?? {}, manualOverride: true };
6410
+ }
6411
+ let nextAdjustments = parsed.id ? adjustmentDrafts.map(
6412
+ (adj) => adj.id === parsed.id ? {
6413
+ ...adj,
6414
+ id: adjustmentId,
6415
+ scope: parsed.scope ?? adj.scope ?? "order",
6416
+ kind: parsed.kind ?? adj.kind ?? "custom",
6417
+ code: parsed.code ?? adj.code ?? null,
6418
+ label: parsed.label ?? adj.label ?? null,
6419
+ calculatorKey: parsed.calculatorKey ?? adj.calculatorKey ?? null,
6420
+ promotionId: parsed.promotionId ?? adj.promotionId ?? null,
6421
+ rate: parsed.rate ?? adj.rate ?? null,
6422
+ amountNet: parsed.amountNet ?? adj.amountNet ?? null,
6423
+ amountGross: parsed.amountGross ?? adj.amountGross ?? null,
6424
+ currencyCode: parsed.currencyCode ?? adj.currencyCode ?? order.currencyCode,
6425
+ metadata,
6426
+ customFields: parsed.customFields !== void 0 ? parsed.customFields : adj.customFields ?? null,
6427
+ position: parsed.position ?? adj.position ?? adjustmentDrafts.length
6428
+ } : adj
6429
+ ) : [
6430
+ ...adjustmentDrafts,
6431
+ {
6432
+ id: adjustmentId,
6433
+ scope: parsed.scope ?? "order",
6434
+ kind: parsed.kind ?? "custom",
6435
+ code: parsed.code ?? null,
6436
+ label: parsed.label ?? null,
6437
+ calculatorKey: parsed.calculatorKey ?? null,
6438
+ promotionId: parsed.promotionId ?? null,
6439
+ rate: parsed.rate ?? null,
6440
+ amountNet: parsed.amountNet ?? null,
6441
+ amountGross: parsed.amountGross ?? null,
6442
+ currencyCode: parsed.currencyCode ?? order.currencyCode,
6443
+ metadata,
6444
+ customFields: parsed.customFields ?? null,
6445
+ position: parsed.position ?? adjustmentDrafts.length
6446
+ }
6447
+ ];
6448
+ nextAdjustments = nextAdjustments.sort((a, b) => (a.position ?? 0) - (b.position ?? 0)).map((adj, index) => ({ ...adj, position: adj.position ?? index }));
6449
+ const sourceLines = lineSnapshots.map((line, index) => ({
6450
+ ...line,
6451
+ statusEntryId: line.statusEntryId ?? null,
6452
+ catalogSnapshot: line.catalogSnapshot ?? null,
6453
+ promotionSnapshot: line.promotionSnapshot ?? null,
6454
+ organizationId: order.organizationId,
6455
+ tenantId: order.tenantId,
6456
+ orderId: order.id,
6457
+ lineNumber: line.lineNumber ?? index + 1
6458
+ }));
6459
+ const calcLines = sourceLines.map(
6460
+ (line, index) => createLineSnapshotFromInput(line, line.lineNumber ?? index + 1)
6461
+ );
6462
+ const salesCalculationService = ctx.container.resolve("salesCalculationService");
6463
+ const calculationContext = buildCalculationContext({
6464
+ tenantId: order.tenantId,
6465
+ organizationId: order.organizationId,
6466
+ currencyCode: order.currencyCode,
6467
+ shippingSnapshot: order.shippingMethodSnapshot,
6468
+ paymentSnapshot: order.paymentMethodSnapshot,
6469
+ shippingMethodId: order.shippingMethodId ?? null,
6470
+ paymentMethodId: order.paymentMethodId ?? null,
6471
+ shippingMethodCode: order.shippingMethodCode ?? null,
6472
+ paymentMethodCode: order.paymentMethodCode ?? null
6473
+ });
6474
+ const effectiveAdjustment = nextAdjustments.find(
6475
+ (adj) => adj.id === adjustmentId
6541
6476
  );
6542
- const baselineCalculation = await salesCalculationService.calculateDocumentTotals({
6477
+ if (effectiveAdjustment?.kind === "return") {
6478
+ const baselineAdjustments = nextAdjustments.filter(
6479
+ (adj) => adj.id !== adjustmentId
6480
+ );
6481
+ const baselineCalculation = await salesCalculationService.calculateDocumentTotals({
6482
+ documentKind: "order",
6483
+ lines: calcLines,
6484
+ adjustments: baselineAdjustments,
6485
+ context: calculationContext,
6486
+ existingTotals: resolveExistingPaymentTotals(order)
6487
+ });
6488
+ const issues = validateReturnAdjustmentWithinRemaining({
6489
+ kind: "return",
6490
+ amountNet: effectiveAdjustment.amountNet ?? effectiveAdjustment.amountGross ?? 0,
6491
+ amountGross: effectiveAdjustment.amountGross ?? effectiveAdjustment.amountNet ?? 0,
6492
+ remainingNet: Number(
6493
+ baselineCalculation.totals?.grandTotalNetAmount ?? 0
6494
+ ),
6495
+ remainingGross: Number(
6496
+ baselineCalculation.totals?.grandTotalGrossAmount ?? 0
6497
+ )
6498
+ });
6499
+ if (issues.length > 0) {
6500
+ const { translate } = await resolveTranslations();
6501
+ const grossIssue = issues.find((issue) => issue.path === "amountGross");
6502
+ const netIssue = issues.find((issue) => issue.path === "amountNet");
6503
+ const fieldErrors = {};
6504
+ if (grossIssue) {
6505
+ fieldErrors.amountGross = translate(
6506
+ "sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
6507
+ RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE
6508
+ );
6509
+ }
6510
+ if (netIssue) {
6511
+ fieldErrors.amountNet = translate(
6512
+ "sales.adjustments.error.returnExceedsRemainingGrandTotalNet",
6513
+ RETURN_ADJUSTMENT_EXCEEDS_REMAINING_NET_MESSAGE
6514
+ );
6515
+ }
6516
+ throw new CrudHttpError(400, {
6517
+ error: fieldErrors.amountGross ?? fieldErrors.amountNet ?? translate(
6518
+ "sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
6519
+ RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE
6520
+ ),
6521
+ fieldErrors
6522
+ });
6523
+ }
6524
+ }
6525
+ const calculation = await salesCalculationService.calculateDocumentTotals({
6543
6526
  documentKind: "order",
6544
6527
  lines: calcLines,
6545
- adjustments: baselineAdjustments,
6528
+ adjustments: nextAdjustments,
6546
6529
  context: calculationContext,
6547
6530
  existingTotals: resolveExistingPaymentTotals(order)
6548
6531
  });
6549
- const issues = validateReturnAdjustmentWithinRemaining({
6550
- kind: "return",
6551
- amountNet: effectiveAdjustment.amountNet ?? effectiveAdjustment.amountGross ?? 0,
6552
- amountGross: effectiveAdjustment.amountGross ?? effectiveAdjustment.amountNet ?? 0,
6553
- remainingNet: Number(
6554
- baselineCalculation.totals?.grandTotalNetAmount ?? 0
6555
- ),
6556
- remainingGross: Number(
6557
- baselineCalculation.totals?.grandTotalGrossAmount ?? 0
6558
- )
6559
- });
6560
- if (issues.length > 0) {
6561
- const { translate } = await resolveTranslations();
6562
- const grossIssue = issues.find((issue) => issue.path === "amountGross");
6563
- const netIssue = issues.find((issue) => issue.path === "amountNet");
6564
- const fieldErrors = {};
6565
- if (grossIssue) {
6566
- fieldErrors.amountGross = translate(
6567
- "sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
6568
- RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE
6569
- );
6570
- }
6571
- if (netIssue) {
6572
- fieldErrors.amountNet = translate(
6573
- "sales.adjustments.error.returnExceedsRemainingGrandTotalNet",
6574
- RETURN_ADJUSTMENT_EXCEEDS_REMAINING_NET_MESSAGE
6575
- );
6576
- }
6577
- throw new CrudHttpError(400, {
6578
- error: fieldErrors.amountGross ?? fieldErrors.amountNet ?? translate(
6579
- "sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
6580
- RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE
6581
- ),
6582
- fieldErrors
6583
- });
6584
- }
6585
- }
6586
- const calculation = await salesCalculationService.calculateDocumentTotals({
6587
- documentKind: "order",
6588
- lines: calcLines,
6589
- adjustments: nextAdjustments,
6590
- context: calculationContext,
6591
- existingTotals: resolveExistingPaymentTotals(order)
6532
+ const adjustmentInputs = nextAdjustments.map((adj, index) => ({
6533
+ organizationId: order.organizationId,
6534
+ tenantId: order.tenantId,
6535
+ orderId: order.id,
6536
+ scope: adj.scope ?? "order",
6537
+ kind: adj.kind ?? "custom",
6538
+ code: adj.code ?? void 0,
6539
+ label: adj.label ?? void 0,
6540
+ calculatorKey: adj.calculatorKey ?? void 0,
6541
+ promotionId: adj.promotionId ?? void 0,
6542
+ rate: adj.rate ?? void 0,
6543
+ amountNet: adj.amountNet ?? void 0,
6544
+ amountGross: adj.amountGross ?? void 0,
6545
+ currencyCode: adj.currencyCode ?? order.currencyCode,
6546
+ metadata: adj.metadata ?? void 0,
6547
+ customFields: adj.customFields ?? void 0,
6548
+ position: adj.position ?? index
6549
+ }));
6550
+ await replaceOrderAdjustments(em, order, calculation, adjustmentInputs);
6551
+ applyOrderTotals(order, calculation.totals, calculation.lines.length);
6552
+ order.updatedAt = /* @__PURE__ */ new Date();
6553
+ return { result: { orderId: order.id, adjustmentId }, calculation };
6592
6554
  });
6593
- const adjustmentInputs = nextAdjustments.map((adj, index) => ({
6594
- organizationId: order.organizationId,
6595
- tenantId: order.tenantId,
6596
- orderId: order.id,
6597
- scope: adj.scope ?? "order",
6598
- kind: adj.kind ?? "custom",
6599
- code: adj.code ?? void 0,
6600
- label: adj.label ?? void 0,
6601
- calculatorKey: adj.calculatorKey ?? void 0,
6602
- promotionId: adj.promotionId ?? void 0,
6603
- rate: adj.rate ?? void 0,
6604
- amountNet: adj.amountNet ?? void 0,
6605
- amountGross: adj.amountGross ?? void 0,
6606
- currencyCode: adj.currencyCode ?? order.currencyCode,
6607
- metadata: adj.metadata ?? void 0,
6608
- customFields: adj.customFields ?? void 0,
6609
- position: adj.position ?? index
6610
- }));
6611
- let eventBus = null;
6612
- try {
6613
- eventBus = ctx.container.resolve("eventBus");
6614
- } catch {
6615
- eventBus = null;
6616
- }
6617
- await withAtomicFlush(
6618
- em,
6619
- [
6620
- async () => {
6621
- await replaceOrderAdjustments(em, order, calculation, adjustmentInputs);
6622
- applyOrderTotals(order, calculation.totals, calculation.lines.length);
6623
- order.updatedAt = /* @__PURE__ */ new Date();
6624
- await emitTotalsCalculated(eventBus, {
6625
- documentKind: "order",
6626
- documentId: order.id,
6627
- organizationId: order.organizationId,
6628
- tenantId: order.tenantId,
6629
- customerId: order.customerEntityId ?? null,
6630
- totals: calculation.totals,
6631
- lineCount: calculation.lines.length
6632
- });
6633
- }
6634
- ],
6635
- { transaction: true }
6636
- );
6637
- return { orderId: order.id, adjustmentId };
6638
- },
6639
- captureAfter: async (_input, result, ctx) => {
6640
- const em = ctx.container.resolve("em").fork();
6641
- return loadOrderSnapshot(em, result.orderId);
6642
6555
  },
6556
+ captureAfter: async (_input, result, ctx) => captureLockedOrderGraphAfter(ctx, result, result.orderId),
6643
6557
  buildLog: async ({ snapshots, result }) => {
6644
- const before = snapshots.before;
6558
+ const before = resolveLockedOrderGraphBefore(result, snapshots.before);
6645
6559
  const after = snapshots.after;
6646
6560
  if (!after) return null;
6647
6561
  const { translate } = await resolveTranslations();
@@ -6670,133 +6584,89 @@ const orderAdjustmentUpsertCommand = {
6670
6584
  };
6671
6585
  const orderAdjustmentDeleteCommand = {
6672
6586
  id: "sales.orders.adjustments.delete",
6673
- async prepare(input, ctx) {
6674
- const raw = input?.body ?? {};
6675
- const orderId = typeof raw.orderId === "string" ? raw.orderId : null;
6676
- if (!orderId) return {};
6677
- const em = ctx.container.resolve("em");
6678
- const snapshot = await loadOrderSnapshot(em, orderId);
6679
- if (snapshot)
6680
- ensureOrderScope(
6681
- ctx,
6682
- snapshot.order.organizationId,
6683
- snapshot.order.tenantId
6684
- );
6685
- return snapshot ? { before: snapshot } : {};
6587
+ async prepare(input) {
6588
+ return markLockedGraphWrite(input);
6686
6589
  },
6687
6590
  async execute(input, ctx) {
6688
- const parsed = orderAdjustmentDeleteSchema.parse(
6689
- input?.body ?? {}
6690
- );
6691
- const em = ctx.container.resolve("em").fork();
6692
- const order = await findOneWithDecryption(em, SalesOrder, {
6693
- id: parsed.orderId,
6694
- deletedAt: null
6695
- });
6696
- if (!order)
6697
- throw notFound("Sales order not found");
6698
- ensureOrderScope(ctx, order.organizationId, order.tenantId);
6699
- await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
6700
- if (isExternalAmountsMode(order.totalsMode ?? "computed")) await refuseOnExternalOrder();
6701
- const [existingLines, adjustments] = await Promise.all([
6702
- em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
6703
- em.find(
6704
- SalesOrderAdjustment,
6705
- { order },
6706
- { orderBy: { position: "asc" } }
6707
- )
6708
- ]);
6709
- const filtered = adjustments.filter((adj) => adj.id !== parsed.id);
6710
- if (filtered.length === adjustments.length) {
6711
- throw notFound("Adjustment not found");
6712
- }
6713
- const lineSnapshots = existingLines.map(mapOrderLineEntityToSnapshot);
6714
- const calcLines = lineSnapshots.map(
6715
- (line, index) => createLineSnapshotFromInput(
6716
- {
6717
- ...line,
6718
- organizationId: order.organizationId,
6719
- tenantId: order.tenantId,
6720
- orderId: order.id,
6721
- lineNumber: line.lineNumber ?? index + 1,
6722
- statusEntryId: line.statusEntryId ?? null,
6723
- catalogSnapshot: line.catalogSnapshot ?? null,
6724
- promotionSnapshot: line.promotionSnapshot ?? null
6725
- },
6726
- line.lineNumber ?? index + 1
6727
- )
6728
- );
6729
- const adjustmentDrafts = filtered.map(mapOrderAdjustmentToDraft);
6730
- const salesCalculationService = ctx.container.resolve("salesCalculationService");
6731
- const calculationContext = buildCalculationContext({
6732
- tenantId: order.tenantId,
6733
- organizationId: order.organizationId,
6734
- currencyCode: order.currencyCode,
6735
- shippingSnapshot: order.shippingMethodSnapshot,
6736
- paymentSnapshot: order.paymentMethodSnapshot,
6737
- shippingMethodId: order.shippingMethodId ?? null,
6738
- paymentMethodId: order.paymentMethodId ?? null,
6739
- shippingMethodCode: order.shippingMethodCode ?? null,
6740
- paymentMethodCode: order.paymentMethodCode ?? null
6741
- });
6742
- const calculation = await salesCalculationService.calculateDocumentTotals({
6743
- documentKind: "order",
6744
- lines: calcLines,
6745
- adjustments: adjustmentDrafts,
6746
- context: calculationContext,
6747
- existingTotals: resolveExistingPaymentTotals(order)
6748
- });
6749
- const adjustmentInputs = adjustmentDrafts.map((adj, index) => ({
6750
- organizationId: order.organizationId,
6751
- tenantId: order.tenantId,
6752
- orderId: order.id,
6753
- scope: adj.scope ?? "order",
6754
- kind: adj.kind ?? "custom",
6755
- code: adj.code ?? void 0,
6756
- label: adj.label ?? void 0,
6757
- calculatorKey: adj.calculatorKey ?? void 0,
6758
- promotionId: adj.promotionId ?? void 0,
6759
- rate: adj.rate ?? void 0,
6760
- amountNet: adj.amountNet ?? void 0,
6761
- amountGross: adj.amountGross ?? void 0,
6762
- currencyCode: adj.currencyCode ?? order.currencyCode,
6763
- metadata: adj.metadata ?? void 0,
6764
- position: adj.position ?? index
6765
- }));
6766
- let eventBus = null;
6767
- try {
6768
- eventBus = ctx.container.resolve("eventBus");
6769
- } catch {
6770
- eventBus = null;
6771
- }
6772
- await withAtomicFlush(
6773
- em,
6774
- [
6775
- async () => {
6776
- await replaceOrderAdjustments(em, order, calculation, adjustmentInputs);
6777
- applyOrderTotals(order, calculation.totals, calculation.lines.length);
6778
- order.updatedAt = /* @__PURE__ */ new Date();
6779
- await emitTotalsCalculated(eventBus, {
6780
- documentKind: "order",
6781
- documentId: order.id,
6782
- organizationId: order.organizationId,
6783
- tenantId: order.tenantId,
6784
- customerId: order.customerEntityId ?? null,
6785
- totals: calculation.totals,
6786
- lineCount: calculation.lines.length
6787
- });
6788
- }
6789
- ],
6790
- { transaction: true }
6791
- );
6792
- return { orderId: order.id, adjustmentId: parsed.id };
6793
- },
6794
- captureAfter: async (_input, result, ctx) => {
6795
- const em = ctx.container.resolve("em").fork();
6796
- return loadOrderSnapshot(em, result.orderId);
6591
+ const parsed = orderAdjustmentDeleteSchema.parse(
6592
+ input?.body ?? {}
6593
+ );
6594
+ return writeLockedOrderGraph(input, ctx, parsed.orderId, "Sales order not found", async (em, order) => {
6595
+ if (isExternalAmountsMode(order.totalsMode ?? "computed")) await refuseOnExternalOrder();
6596
+ const [existingLines, adjustments] = await Promise.all([
6597
+ em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
6598
+ em.find(
6599
+ SalesOrderAdjustment,
6600
+ { order },
6601
+ { orderBy: { position: "asc" } }
6602
+ )
6603
+ ]);
6604
+ const filtered = adjustments.filter((adj) => adj.id !== parsed.id);
6605
+ if (filtered.length === adjustments.length) {
6606
+ throw notFound("Adjustment not found");
6607
+ }
6608
+ const lineSnapshots = existingLines.map(mapOrderLineEntityToSnapshot);
6609
+ const calcLines = lineSnapshots.map(
6610
+ (line, index) => createLineSnapshotFromInput(
6611
+ {
6612
+ ...line,
6613
+ organizationId: order.organizationId,
6614
+ tenantId: order.tenantId,
6615
+ orderId: order.id,
6616
+ lineNumber: line.lineNumber ?? index + 1,
6617
+ statusEntryId: line.statusEntryId ?? null,
6618
+ catalogSnapshot: line.catalogSnapshot ?? null,
6619
+ promotionSnapshot: line.promotionSnapshot ?? null
6620
+ },
6621
+ line.lineNumber ?? index + 1
6622
+ )
6623
+ );
6624
+ const adjustmentDrafts = filtered.map(mapOrderAdjustmentToDraft);
6625
+ const salesCalculationService = ctx.container.resolve("salesCalculationService");
6626
+ const calculationContext = buildCalculationContext({
6627
+ tenantId: order.tenantId,
6628
+ organizationId: order.organizationId,
6629
+ currencyCode: order.currencyCode,
6630
+ shippingSnapshot: order.shippingMethodSnapshot,
6631
+ paymentSnapshot: order.paymentMethodSnapshot,
6632
+ shippingMethodId: order.shippingMethodId ?? null,
6633
+ paymentMethodId: order.paymentMethodId ?? null,
6634
+ shippingMethodCode: order.shippingMethodCode ?? null,
6635
+ paymentMethodCode: order.paymentMethodCode ?? null
6636
+ });
6637
+ const calculation = await salesCalculationService.calculateDocumentTotals({
6638
+ documentKind: "order",
6639
+ lines: calcLines,
6640
+ adjustments: adjustmentDrafts,
6641
+ context: calculationContext,
6642
+ existingTotals: resolveExistingPaymentTotals(order)
6643
+ });
6644
+ const adjustmentInputs = adjustmentDrafts.map((adj, index) => ({
6645
+ organizationId: order.organizationId,
6646
+ tenantId: order.tenantId,
6647
+ orderId: order.id,
6648
+ scope: adj.scope ?? "order",
6649
+ kind: adj.kind ?? "custom",
6650
+ code: adj.code ?? void 0,
6651
+ label: adj.label ?? void 0,
6652
+ calculatorKey: adj.calculatorKey ?? void 0,
6653
+ promotionId: adj.promotionId ?? void 0,
6654
+ rate: adj.rate ?? void 0,
6655
+ amountNet: adj.amountNet ?? void 0,
6656
+ amountGross: adj.amountGross ?? void 0,
6657
+ currencyCode: adj.currencyCode ?? order.currencyCode,
6658
+ metadata: adj.metadata ?? void 0,
6659
+ position: adj.position ?? index
6660
+ }));
6661
+ await replaceOrderAdjustments(em, order, calculation, adjustmentInputs);
6662
+ applyOrderTotals(order, calculation.totals, calculation.lines.length);
6663
+ order.updatedAt = /* @__PURE__ */ new Date();
6664
+ return { result: { orderId: order.id, adjustmentId: parsed.id }, calculation };
6665
+ });
6797
6666
  },
6667
+ captureAfter: async (_input, result, ctx) => captureLockedOrderGraphAfter(ctx, result, result.orderId),
6798
6668
  buildLog: async ({ snapshots, result }) => {
6799
- const before = snapshots.before;
6669
+ const before = resolveLockedOrderGraphBefore(result, snapshots.before);
6800
6670
  const after = snapshots.after;
6801
6671
  if (!after) return null;
6802
6672
  const { translate } = await resolveTranslations();
@@ -6825,226 +6695,182 @@ const orderAdjustmentDeleteCommand = {
6825
6695
  };
6826
6696
  const quoteAdjustmentUpsertCommand = {
6827
6697
  id: "sales.quotes.adjustments.upsert",
6828
- async prepare(input, ctx) {
6829
- const raw = input?.body ?? {};
6830
- const quoteId = typeof raw.quoteId === "string" ? raw.quoteId : null;
6831
- if (!quoteId) return {};
6832
- const em = ctx.container.resolve("em");
6833
- const snapshot = await loadQuoteSnapshot(em, quoteId);
6834
- if (snapshot)
6835
- ensureQuoteScope(
6836
- ctx,
6837
- snapshot.quote.organizationId,
6838
- snapshot.quote.tenantId
6839
- );
6840
- return snapshot ? { before: snapshot } : {};
6698
+ async prepare(input) {
6699
+ return markLockedGraphWrite(input);
6841
6700
  },
6842
6701
  async execute(input, ctx) {
6843
6702
  const parsed = quoteAdjustmentUpsertSchema.parse(
6844
6703
  input?.body ?? {}
6845
6704
  );
6846
- const em = ctx.container.resolve("em").fork();
6847
- const quote = await findOneWithDecryption(em, SalesQuote, {
6848
- id: parsed.quoteId,
6849
- deletedAt: null
6850
- });
6851
- if (!quote)
6852
- throw notFound("Sales quote not found");
6853
- ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
6854
- await enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
6855
- if (parsed.scope === "line") {
6856
- throw new CrudHttpError(400, {
6857
- error: "Line-scoped adjustments are not supported yet."
6858
- });
6859
- }
6860
- const [existingLines, existingAdjustments] = await Promise.all([
6861
- em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
6862
- em.find(
6863
- SalesQuoteAdjustment,
6864
- { quote },
6865
- { orderBy: { position: "asc" } }
6866
- )
6867
- ]);
6868
- const lineSnapshots = existingLines.map(mapQuoteLineEntityToSnapshot);
6869
- const adjustmentDrafts = existingAdjustments.map(mapQuoteAdjustmentToDraft);
6870
- const existingSnapshot = parsed.id ? adjustmentDrafts.find((adj) => adj.id === parsed.id) ?? null : null;
6871
- const adjustmentId = parsed.id ?? existingSnapshot?.id ?? randomUUID();
6872
- let metadata = typeof parsed.metadata === "object" && parsed.metadata ? cloneJson(parsed.metadata) : existingSnapshot?.metadata ? cloneJson(existingSnapshot.metadata) : null;
6873
- const calculatorKey = parsed.calculatorKey ?? existingSnapshot?.calculatorKey ?? null;
6874
- if (parsed.id && calculatorKey && (calculatorKey.startsWith("shipping-provider:") || calculatorKey.startsWith("payment-provider:"))) {
6875
- metadata = { ...metadata ?? {}, manualOverride: true };
6876
- }
6877
- let nextAdjustments = parsed.id ? adjustmentDrafts.map(
6878
- (adj) => adj.id === parsed.id ? {
6879
- ...adj,
6880
- id: adjustmentId,
6881
- scope: parsed.scope ?? adj.scope ?? "order",
6882
- kind: parsed.kind ?? adj.kind ?? "custom",
6883
- code: parsed.code ?? adj.code ?? null,
6884
- label: parsed.label ?? adj.label ?? null,
6885
- calculatorKey: parsed.calculatorKey ?? adj.calculatorKey ?? null,
6886
- promotionId: parsed.promotionId ?? adj.promotionId ?? null,
6887
- rate: parsed.rate ?? adj.rate ?? null,
6888
- amountNet: parsed.amountNet ?? adj.amountNet ?? null,
6889
- amountGross: parsed.amountGross ?? adj.amountGross ?? null,
6890
- currencyCode: parsed.currencyCode ?? adj.currencyCode ?? quote.currencyCode,
6891
- metadata,
6892
- customFields: parsed.customFields !== void 0 ? parsed.customFields : adj.customFields ?? null,
6893
- position: parsed.position ?? adj.position ?? adjustmentDrafts.length
6894
- } : adj
6895
- ) : [
6896
- ...adjustmentDrafts,
6897
- {
6898
- id: adjustmentId,
6899
- scope: parsed.scope ?? "order",
6900
- kind: parsed.kind ?? "custom",
6901
- code: parsed.code ?? null,
6902
- label: parsed.label ?? null,
6903
- calculatorKey: parsed.calculatorKey ?? null,
6904
- promotionId: parsed.promotionId ?? null,
6905
- rate: parsed.rate ?? null,
6906
- amountNet: parsed.amountNet ?? null,
6907
- amountGross: parsed.amountGross ?? null,
6908
- currencyCode: parsed.currencyCode ?? quote.currencyCode,
6909
- metadata,
6910
- customFields: parsed.customFields ?? null,
6911
- position: parsed.position ?? adjustmentDrafts.length
6705
+ return writeLockedQuoteGraph(input, ctx, parsed.quoteId, "Sales quote not found", async (em, quote) => {
6706
+ if (parsed.scope === "line") {
6707
+ throw new CrudHttpError(400, {
6708
+ error: "Line-scoped adjustments are not supported yet."
6709
+ });
6912
6710
  }
6913
- ];
6914
- nextAdjustments = nextAdjustments.sort((a, b) => (a.position ?? 0) - (b.position ?? 0)).map((adj, index) => ({ ...adj, position: adj.position ?? index }));
6915
- const sourceLines = lineSnapshots.map((line, index) => ({
6916
- ...line,
6917
- statusEntryId: line.statusEntryId ?? null,
6918
- catalogSnapshot: line.catalogSnapshot ?? null,
6919
- promotionSnapshot: line.promotionSnapshot ?? null,
6920
- organizationId: quote.organizationId,
6921
- tenantId: quote.tenantId,
6922
- quoteId: quote.id,
6923
- lineNumber: line.lineNumber ?? index + 1
6924
- }));
6925
- const calcLines = sourceLines.map(
6926
- (line, index) => createLineSnapshotFromInput(line, line.lineNumber ?? index + 1)
6927
- );
6928
- const salesCalculationService = ctx.container.resolve("salesCalculationService");
6929
- const calculationContext = buildCalculationContext({
6930
- tenantId: quote.tenantId,
6931
- organizationId: quote.organizationId,
6932
- currencyCode: quote.currencyCode,
6933
- shippingSnapshot: quote.shippingMethodSnapshot,
6934
- paymentSnapshot: quote.paymentMethodSnapshot,
6935
- shippingMethodId: quote.shippingMethodId ?? null,
6936
- paymentMethodId: quote.paymentMethodId ?? null,
6937
- shippingMethodCode: quote.shippingMethodCode ?? null,
6938
- paymentMethodCode: quote.paymentMethodCode ?? null
6939
- });
6940
- const effectiveAdjustment = nextAdjustments.find(
6941
- (adj) => adj.id === adjustmentId
6942
- );
6943
- if (effectiveAdjustment?.kind === "return") {
6944
- const baselineAdjustments = nextAdjustments.filter(
6945
- (adj) => adj.id !== adjustmentId
6946
- );
6947
- const baselineCalculation = await salesCalculationService.calculateDocumentTotals({
6948
- documentKind: "quote",
6949
- lines: calcLines,
6950
- adjustments: baselineAdjustments,
6951
- context: calculationContext
6952
- });
6953
- const issues = validateReturnAdjustmentWithinRemaining({
6954
- kind: "return",
6955
- amountNet: effectiveAdjustment.amountNet ?? effectiveAdjustment.amountGross ?? 0,
6956
- amountGross: effectiveAdjustment.amountGross ?? effectiveAdjustment.amountNet ?? 0,
6957
- remainingNet: Number(
6958
- baselineCalculation.totals?.grandTotalNetAmount ?? 0
6959
- ),
6960
- remainingGross: Number(
6961
- baselineCalculation.totals?.grandTotalGrossAmount ?? 0
6711
+ const [existingLines, existingAdjustments] = await Promise.all([
6712
+ em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
6713
+ em.find(
6714
+ SalesQuoteAdjustment,
6715
+ { quote },
6716
+ { orderBy: { position: "asc" } }
6962
6717
  )
6963
- });
6964
- if (issues.length > 0) {
6965
- const { translate } = await resolveTranslations();
6966
- const grossIssue = issues.find((issue) => issue.path === "amountGross");
6967
- const netIssue = issues.find((issue) => issue.path === "amountNet");
6968
- const fieldErrors = {};
6969
- if (grossIssue) {
6970
- fieldErrors.amountGross = translate(
6971
- "sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
6972
- RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE
6973
- );
6974
- }
6975
- if (netIssue) {
6976
- fieldErrors.amountNet = translate(
6977
- "sales.adjustments.error.returnExceedsRemainingGrandTotalNet",
6978
- RETURN_ADJUSTMENT_EXCEEDS_REMAINING_NET_MESSAGE
6979
- );
6718
+ ]);
6719
+ const lineSnapshots = existingLines.map(mapQuoteLineEntityToSnapshot);
6720
+ const adjustmentDrafts = existingAdjustments.map(mapQuoteAdjustmentToDraft);
6721
+ const existingSnapshot = parsed.id ? adjustmentDrafts.find((adj) => adj.id === parsed.id) ?? null : null;
6722
+ const adjustmentId = parsed.id ?? existingSnapshot?.id ?? randomUUID();
6723
+ let metadata = typeof parsed.metadata === "object" && parsed.metadata ? cloneJson(parsed.metadata) : existingSnapshot?.metadata ? cloneJson(existingSnapshot.metadata) : null;
6724
+ const calculatorKey = parsed.calculatorKey ?? existingSnapshot?.calculatorKey ?? null;
6725
+ if (parsed.id && calculatorKey && (calculatorKey.startsWith("shipping-provider:") || calculatorKey.startsWith("payment-provider:"))) {
6726
+ metadata = { ...metadata ?? {}, manualOverride: true };
6727
+ }
6728
+ let nextAdjustments = parsed.id ? adjustmentDrafts.map(
6729
+ (adj) => adj.id === parsed.id ? {
6730
+ ...adj,
6731
+ id: adjustmentId,
6732
+ scope: parsed.scope ?? adj.scope ?? "order",
6733
+ kind: parsed.kind ?? adj.kind ?? "custom",
6734
+ code: parsed.code ?? adj.code ?? null,
6735
+ label: parsed.label ?? adj.label ?? null,
6736
+ calculatorKey: parsed.calculatorKey ?? adj.calculatorKey ?? null,
6737
+ promotionId: parsed.promotionId ?? adj.promotionId ?? null,
6738
+ rate: parsed.rate ?? adj.rate ?? null,
6739
+ amountNet: parsed.amountNet ?? adj.amountNet ?? null,
6740
+ amountGross: parsed.amountGross ?? adj.amountGross ?? null,
6741
+ currencyCode: parsed.currencyCode ?? adj.currencyCode ?? quote.currencyCode,
6742
+ metadata,
6743
+ customFields: parsed.customFields !== void 0 ? parsed.customFields : adj.customFields ?? null,
6744
+ position: parsed.position ?? adj.position ?? adjustmentDrafts.length
6745
+ } : adj
6746
+ ) : [
6747
+ ...adjustmentDrafts,
6748
+ {
6749
+ id: adjustmentId,
6750
+ scope: parsed.scope ?? "order",
6751
+ kind: parsed.kind ?? "custom",
6752
+ code: parsed.code ?? null,
6753
+ label: parsed.label ?? null,
6754
+ calculatorKey: parsed.calculatorKey ?? null,
6755
+ promotionId: parsed.promotionId ?? null,
6756
+ rate: parsed.rate ?? null,
6757
+ amountNet: parsed.amountNet ?? null,
6758
+ amountGross: parsed.amountGross ?? null,
6759
+ currencyCode: parsed.currencyCode ?? quote.currencyCode,
6760
+ metadata,
6761
+ customFields: parsed.customFields ?? null,
6762
+ position: parsed.position ?? adjustmentDrafts.length
6980
6763
  }
6981
- throw new CrudHttpError(400, {
6982
- error: fieldErrors.amountGross ?? fieldErrors.amountNet ?? translate(
6983
- "sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
6984
- RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE
6764
+ ];
6765
+ nextAdjustments = nextAdjustments.sort((a, b) => (a.position ?? 0) - (b.position ?? 0)).map((adj, index) => ({ ...adj, position: adj.position ?? index }));
6766
+ const sourceLines = lineSnapshots.map((line, index) => ({
6767
+ ...line,
6768
+ statusEntryId: line.statusEntryId ?? null,
6769
+ catalogSnapshot: line.catalogSnapshot ?? null,
6770
+ promotionSnapshot: line.promotionSnapshot ?? null,
6771
+ organizationId: quote.organizationId,
6772
+ tenantId: quote.tenantId,
6773
+ quoteId: quote.id,
6774
+ lineNumber: line.lineNumber ?? index + 1
6775
+ }));
6776
+ const calcLines = sourceLines.map(
6777
+ (line, index) => createLineSnapshotFromInput(line, line.lineNumber ?? index + 1)
6778
+ );
6779
+ const salesCalculationService = ctx.container.resolve("salesCalculationService");
6780
+ const calculationContext = buildCalculationContext({
6781
+ tenantId: quote.tenantId,
6782
+ organizationId: quote.organizationId,
6783
+ currencyCode: quote.currencyCode,
6784
+ shippingSnapshot: quote.shippingMethodSnapshot,
6785
+ paymentSnapshot: quote.paymentMethodSnapshot,
6786
+ shippingMethodId: quote.shippingMethodId ?? null,
6787
+ paymentMethodId: quote.paymentMethodId ?? null,
6788
+ shippingMethodCode: quote.shippingMethodCode ?? null,
6789
+ paymentMethodCode: quote.paymentMethodCode ?? null
6790
+ });
6791
+ const effectiveAdjustment = nextAdjustments.find(
6792
+ (adj) => adj.id === adjustmentId
6793
+ );
6794
+ if (effectiveAdjustment?.kind === "return") {
6795
+ const baselineAdjustments = nextAdjustments.filter(
6796
+ (adj) => adj.id !== adjustmentId
6797
+ );
6798
+ const baselineCalculation = await salesCalculationService.calculateDocumentTotals({
6799
+ documentKind: "quote",
6800
+ lines: calcLines,
6801
+ adjustments: baselineAdjustments,
6802
+ context: calculationContext
6803
+ });
6804
+ const issues = validateReturnAdjustmentWithinRemaining({
6805
+ kind: "return",
6806
+ amountNet: effectiveAdjustment.amountNet ?? effectiveAdjustment.amountGross ?? 0,
6807
+ amountGross: effectiveAdjustment.amountGross ?? effectiveAdjustment.amountNet ?? 0,
6808
+ remainingNet: Number(
6809
+ baselineCalculation.totals?.grandTotalNetAmount ?? 0
6985
6810
  ),
6986
- fieldErrors
6811
+ remainingGross: Number(
6812
+ baselineCalculation.totals?.grandTotalGrossAmount ?? 0
6813
+ )
6987
6814
  });
6988
- }
6989
- }
6990
- const calculation = await salesCalculationService.calculateDocumentTotals({
6991
- documentKind: "quote",
6992
- lines: calcLines,
6993
- adjustments: nextAdjustments,
6994
- context: calculationContext
6995
- });
6996
- const adjustmentInputs = nextAdjustments.map((adj, index) => ({
6997
- organizationId: quote.organizationId,
6998
- tenantId: quote.tenantId,
6999
- quoteId: quote.id,
7000
- scope: adj.scope ?? "order",
7001
- kind: adj.kind ?? "custom",
7002
- code: adj.code ?? void 0,
7003
- label: adj.label ?? void 0,
7004
- calculatorKey: adj.calculatorKey ?? void 0,
7005
- promotionId: adj.promotionId ?? void 0,
7006
- rate: adj.rate ?? void 0,
7007
- amountNet: adj.amountNet ?? void 0,
7008
- amountGross: adj.amountGross ?? void 0,
7009
- currencyCode: adj.currencyCode ?? quote.currencyCode,
7010
- metadata: adj.metadata ?? void 0,
7011
- customFields: adj.customFields ?? void 0,
7012
- position: adj.position ?? index
7013
- }));
7014
- let eventBus = null;
7015
- try {
7016
- eventBus = ctx.container.resolve("eventBus");
7017
- } catch {
7018
- eventBus = null;
7019
- }
7020
- await withAtomicFlush(
7021
- em,
7022
- [
7023
- async () => {
7024
- await replaceQuoteAdjustments(em, quote, calculation, adjustmentInputs);
7025
- applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
7026
- quote.updatedAt = /* @__PURE__ */ new Date();
7027
- await emitTotalsCalculated(eventBus, {
7028
- documentKind: "quote",
7029
- documentId: quote.id,
7030
- organizationId: quote.organizationId,
7031
- tenantId: quote.tenantId,
7032
- customerId: quote.customerEntityId ?? null,
7033
- totals: calculation.totals,
7034
- lineCount: calculation.lines.length
6815
+ if (issues.length > 0) {
6816
+ const { translate } = await resolveTranslations();
6817
+ const grossIssue = issues.find((issue) => issue.path === "amountGross");
6818
+ const netIssue = issues.find((issue) => issue.path === "amountNet");
6819
+ const fieldErrors = {};
6820
+ if (grossIssue) {
6821
+ fieldErrors.amountGross = translate(
6822
+ "sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
6823
+ RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE
6824
+ );
6825
+ }
6826
+ if (netIssue) {
6827
+ fieldErrors.amountNet = translate(
6828
+ "sales.adjustments.error.returnExceedsRemainingGrandTotalNet",
6829
+ RETURN_ADJUSTMENT_EXCEEDS_REMAINING_NET_MESSAGE
6830
+ );
6831
+ }
6832
+ throw new CrudHttpError(400, {
6833
+ error: fieldErrors.amountGross ?? fieldErrors.amountNet ?? translate(
6834
+ "sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
6835
+ RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE
6836
+ ),
6837
+ fieldErrors
7035
6838
  });
7036
6839
  }
7037
- ],
7038
- { transaction: true }
7039
- );
7040
- return { quoteId: quote.id, adjustmentId };
7041
- },
7042
- captureAfter: async (_input, result, ctx) => {
7043
- const em = ctx.container.resolve("em").fork();
7044
- return loadQuoteSnapshot(em, result.quoteId);
6840
+ }
6841
+ const calculation = await salesCalculationService.calculateDocumentTotals({
6842
+ documentKind: "quote",
6843
+ lines: calcLines,
6844
+ adjustments: nextAdjustments,
6845
+ context: calculationContext
6846
+ });
6847
+ const adjustmentInputs = nextAdjustments.map((adj, index) => ({
6848
+ organizationId: quote.organizationId,
6849
+ tenantId: quote.tenantId,
6850
+ quoteId: quote.id,
6851
+ scope: adj.scope ?? "order",
6852
+ kind: adj.kind ?? "custom",
6853
+ code: adj.code ?? void 0,
6854
+ label: adj.label ?? void 0,
6855
+ calculatorKey: adj.calculatorKey ?? void 0,
6856
+ promotionId: adj.promotionId ?? void 0,
6857
+ rate: adj.rate ?? void 0,
6858
+ amountNet: adj.amountNet ?? void 0,
6859
+ amountGross: adj.amountGross ?? void 0,
6860
+ currencyCode: adj.currencyCode ?? quote.currencyCode,
6861
+ metadata: adj.metadata ?? void 0,
6862
+ customFields: adj.customFields ?? void 0,
6863
+ position: adj.position ?? index
6864
+ }));
6865
+ await replaceQuoteAdjustments(em, quote, calculation, adjustmentInputs);
6866
+ applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
6867
+ quote.updatedAt = /* @__PURE__ */ new Date();
6868
+ return { result: { quoteId: quote.id, adjustmentId }, calculation };
6869
+ });
7045
6870
  },
6871
+ captureAfter: async (_input, result, ctx) => captureLockedQuoteGraphAfter(ctx, result, result.quoteId),
7046
6872
  buildLog: async ({ snapshots, result }) => {
7047
- const before = snapshots.before;
6873
+ const before = resolveLockedQuoteGraphBefore(result, snapshots.before);
7048
6874
  const after = snapshots.after;
7049
6875
  if (!after) return null;
7050
6876
  const { translate } = await resolveTranslations();
@@ -7073,131 +6899,87 @@ const quoteAdjustmentUpsertCommand = {
7073
6899
  };
7074
6900
  const quoteAdjustmentDeleteCommand = {
7075
6901
  id: "sales.quotes.adjustments.delete",
7076
- async prepare(input, ctx) {
7077
- const raw = input?.body ?? {};
7078
- const quoteId = typeof raw.quoteId === "string" ? raw.quoteId : null;
7079
- if (!quoteId) return {};
7080
- const em = ctx.container.resolve("em");
7081
- const snapshot = await loadQuoteSnapshot(em, quoteId);
7082
- if (snapshot)
7083
- ensureQuoteScope(
7084
- ctx,
7085
- snapshot.quote.organizationId,
7086
- snapshot.quote.tenantId
7087
- );
7088
- return snapshot ? { before: snapshot } : {};
6902
+ async prepare(input) {
6903
+ return markLockedGraphWrite(input);
7089
6904
  },
7090
6905
  async execute(input, ctx) {
7091
6906
  const parsed = quoteAdjustmentDeleteSchema.parse(
7092
6907
  input?.body ?? {}
7093
6908
  );
7094
- const em = ctx.container.resolve("em").fork();
7095
- const quote = await findOneWithDecryption(em, SalesQuote, {
7096
- id: parsed.quoteId,
7097
- deletedAt: null
7098
- });
7099
- if (!quote)
7100
- throw notFound("Sales quote not found");
7101
- ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
7102
- await enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
7103
- const [existingLines, adjustments] = await Promise.all([
7104
- em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
7105
- em.find(
7106
- SalesQuoteAdjustment,
7107
- { quote },
7108
- { orderBy: { position: "asc" } }
7109
- )
7110
- ]);
7111
- const filtered = adjustments.filter((adj) => adj.id !== parsed.id);
7112
- if (filtered.length === adjustments.length) {
7113
- throw notFound("Adjustment not found");
7114
- }
7115
- const lineSnapshots = existingLines.map(mapQuoteLineEntityToSnapshot);
7116
- const calcLines = lineSnapshots.map(
7117
- (line, index) => createLineSnapshotFromInput(
7118
- {
7119
- ...line,
7120
- organizationId: quote.organizationId,
7121
- tenantId: quote.tenantId,
7122
- quoteId: quote.id,
7123
- lineNumber: line.lineNumber ?? index + 1,
7124
- statusEntryId: line.statusEntryId ?? null,
7125
- catalogSnapshot: line.catalogSnapshot ?? null,
7126
- promotionSnapshot: line.promotionSnapshot ?? null
7127
- },
7128
- line.lineNumber ?? index + 1
7129
- )
7130
- );
7131
- const adjustmentDrafts = filtered.map(mapQuoteAdjustmentToDraft);
7132
- const salesCalculationService = ctx.container.resolve("salesCalculationService");
7133
- const calculationContext = buildCalculationContext({
7134
- tenantId: quote.tenantId,
7135
- organizationId: quote.organizationId,
7136
- currencyCode: quote.currencyCode,
7137
- shippingSnapshot: quote.shippingMethodSnapshot,
7138
- paymentSnapshot: quote.paymentMethodSnapshot,
7139
- shippingMethodId: quote.shippingMethodId ?? null,
7140
- paymentMethodId: quote.paymentMethodId ?? null,
7141
- shippingMethodCode: quote.shippingMethodCode ?? null,
7142
- paymentMethodCode: quote.paymentMethodCode ?? null
7143
- });
7144
- const calculation = await salesCalculationService.calculateDocumentTotals({
7145
- documentKind: "quote",
7146
- lines: calcLines,
7147
- adjustments: adjustmentDrafts,
7148
- context: calculationContext
7149
- });
7150
- const adjustmentInputs = adjustmentDrafts.map((adj, index) => ({
7151
- organizationId: quote.organizationId,
7152
- tenantId: quote.tenantId,
7153
- quoteId: quote.id,
7154
- scope: adj.scope ?? "order",
7155
- kind: adj.kind ?? "custom",
7156
- code: adj.code ?? void 0,
7157
- label: adj.label ?? void 0,
7158
- calculatorKey: adj.calculatorKey ?? void 0,
7159
- promotionId: adj.promotionId ?? void 0,
7160
- rate: adj.rate ?? void 0,
7161
- amountNet: adj.amountNet ?? void 0,
7162
- amountGross: adj.amountGross ?? void 0,
7163
- currencyCode: adj.currencyCode ?? quote.currencyCode,
7164
- metadata: adj.metadata ?? void 0,
7165
- position: adj.position ?? index
7166
- }));
7167
- let eventBus = null;
7168
- try {
7169
- eventBus = ctx.container.resolve("eventBus");
7170
- } catch {
7171
- eventBus = null;
7172
- }
7173
- await withAtomicFlush(
7174
- em,
7175
- [
7176
- async () => {
7177
- await replaceQuoteAdjustments(em, quote, calculation, adjustmentInputs);
7178
- applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
7179
- quote.updatedAt = /* @__PURE__ */ new Date();
7180
- await emitTotalsCalculated(eventBus, {
7181
- documentKind: "quote",
7182
- documentId: quote.id,
6909
+ return writeLockedQuoteGraph(input, ctx, parsed.quoteId, "Sales quote not found", async (em, quote) => {
6910
+ const [existingLines, adjustments] = await Promise.all([
6911
+ em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
6912
+ em.find(
6913
+ SalesQuoteAdjustment,
6914
+ { quote },
6915
+ { orderBy: { position: "asc" } }
6916
+ )
6917
+ ]);
6918
+ const filtered = adjustments.filter((adj) => adj.id !== parsed.id);
6919
+ if (filtered.length === adjustments.length) {
6920
+ throw notFound("Adjustment not found");
6921
+ }
6922
+ const lineSnapshots = existingLines.map(mapQuoteLineEntityToSnapshot);
6923
+ const calcLines = lineSnapshots.map(
6924
+ (line, index) => createLineSnapshotFromInput(
6925
+ {
6926
+ ...line,
7183
6927
  organizationId: quote.organizationId,
7184
6928
  tenantId: quote.tenantId,
7185
- customerId: quote.customerEntityId ?? null,
7186
- totals: calculation.totals,
7187
- lineCount: calculation.lines.length
7188
- });
7189
- }
7190
- ],
7191
- { transaction: true }
7192
- );
7193
- return { quoteId: quote.id, adjustmentId: parsed.id };
7194
- },
7195
- captureAfter: async (_input, result, ctx) => {
7196
- const em = ctx.container.resolve("em").fork();
7197
- return loadQuoteSnapshot(em, result.quoteId);
6929
+ quoteId: quote.id,
6930
+ lineNumber: line.lineNumber ?? index + 1,
6931
+ statusEntryId: line.statusEntryId ?? null,
6932
+ catalogSnapshot: line.catalogSnapshot ?? null,
6933
+ promotionSnapshot: line.promotionSnapshot ?? null
6934
+ },
6935
+ line.lineNumber ?? index + 1
6936
+ )
6937
+ );
6938
+ const adjustmentDrafts = filtered.map(mapQuoteAdjustmentToDraft);
6939
+ const salesCalculationService = ctx.container.resolve("salesCalculationService");
6940
+ const calculationContext = buildCalculationContext({
6941
+ tenantId: quote.tenantId,
6942
+ organizationId: quote.organizationId,
6943
+ currencyCode: quote.currencyCode,
6944
+ shippingSnapshot: quote.shippingMethodSnapshot,
6945
+ paymentSnapshot: quote.paymentMethodSnapshot,
6946
+ shippingMethodId: quote.shippingMethodId ?? null,
6947
+ paymentMethodId: quote.paymentMethodId ?? null,
6948
+ shippingMethodCode: quote.shippingMethodCode ?? null,
6949
+ paymentMethodCode: quote.paymentMethodCode ?? null
6950
+ });
6951
+ const calculation = await salesCalculationService.calculateDocumentTotals({
6952
+ documentKind: "quote",
6953
+ lines: calcLines,
6954
+ adjustments: adjustmentDrafts,
6955
+ context: calculationContext
6956
+ });
6957
+ const adjustmentInputs = adjustmentDrafts.map((adj, index) => ({
6958
+ organizationId: quote.organizationId,
6959
+ tenantId: quote.tenantId,
6960
+ quoteId: quote.id,
6961
+ scope: adj.scope ?? "order",
6962
+ kind: adj.kind ?? "custom",
6963
+ code: adj.code ?? void 0,
6964
+ label: adj.label ?? void 0,
6965
+ calculatorKey: adj.calculatorKey ?? void 0,
6966
+ promotionId: adj.promotionId ?? void 0,
6967
+ rate: adj.rate ?? void 0,
6968
+ amountNet: adj.amountNet ?? void 0,
6969
+ amountGross: adj.amountGross ?? void 0,
6970
+ currencyCode: adj.currencyCode ?? quote.currencyCode,
6971
+ metadata: adj.metadata ?? void 0,
6972
+ position: adj.position ?? index
6973
+ }));
6974
+ await replaceQuoteAdjustments(em, quote, calculation, adjustmentInputs);
6975
+ applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
6976
+ quote.updatedAt = /* @__PURE__ */ new Date();
6977
+ return { result: { quoteId: quote.id, adjustmentId: parsed.id }, calculation };
6978
+ });
7198
6979
  },
6980
+ captureAfter: async (_input, result, ctx) => captureLockedQuoteGraphAfter(ctx, result, result.quoteId),
7199
6981
  buildLog: async ({ snapshots, result }) => {
7200
- const before = snapshots.before;
6982
+ const before = resolveLockedQuoteGraphBefore(result, snapshots.before);
7201
6983
  const after = snapshots.after;
7202
6984
  if (!after) return null;
7203
6985
  const { translate } = await resolveTranslations();