@open-mercato/core 0.6.7-develop.6842.1.8cd8a8883c → 0.6.7-develop.6859.1.e4727b568a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/dist/bootstrap.js +16 -1
- package/dist/bootstrap.js.map +2 -2
- package/dist/generated/entities/attachment_quota_reservation/index.js +33 -0
- package/dist/generated/entities/attachment_quota_reservation/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +2 -1
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +17 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/attachments/api/route.js +104 -8
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/data/entities.js +67 -1
- package/dist/modules/attachments/data/entities.js.map +2 -2
- package/dist/modules/attachments/di.js +7 -1
- package/dist/modules/attachments/di.js.map +2 -2
- package/dist/modules/attachments/lib/drivers/index.js.map +1 -1
- package/dist/modules/attachments/lib/drivers/localDriver.js +19 -6
- package/dist/modules/attachments/lib/drivers/localDriver.js.map +2 -2
- package/dist/modules/attachments/lib/quota-recovery-queue.js +9 -0
- package/dist/modules/attachments/lib/quota-recovery-queue.js.map +7 -0
- package/dist/modules/attachments/lib/quota-service.js +224 -0
- package/dist/modules/attachments/lib/quota-service.js.map +7 -0
- package/dist/modules/attachments/migrations/Migration20260709234741_attachments.js +13 -0
- package/dist/modules/attachments/migrations/Migration20260709234741_attachments.js.map +7 -0
- package/dist/modules/attachments/workers/quota-recovery.js +45 -0
- package/dist/modules/attachments/workers/quota-recovery.js.map +7 -0
- package/dist/modules/auth/lib/setup-app.js +16 -2
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/dashboards/services/widgetDataService.js +3 -4
- package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
- package/dist/modules/entities/api/definitions.js +2 -0
- package/dist/modules/entities/api/definitions.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +18 -3
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
- package/dist/modules/entities/cli.js +193 -19
- package/dist/modules/entities/cli.js.map +2 -2
- package/dist/modules/entities/components/RecordListRelationCell.js +63 -0
- package/dist/modules/entities/components/RecordListRelationCell.js.map +7 -0
- package/dist/modules/entities/components/recordListRelationDisplays.js +80 -0
- package/dist/modules/entities/components/recordListRelationDisplays.js.map +7 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js +14 -1
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js.map +2 -2
- package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js +22 -17
- package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js.map +2 -2
- package/dist/modules/messages/api/[id]/route.js +15 -0
- package/dist/modules/messages/api/[id]/route.js.map +2 -2
- package/dist/modules/query_index/api/reindex.js +10 -0
- package/dist/modules/query_index/api/reindex.js.map +2 -2
- package/dist/modules/query_index/cli.js +125 -1
- package/dist/modules/query_index/cli.js.map +2 -2
- package/dist/modules/query_index/lib/coverage.js +19 -7
- package/dist/modules/query_index/lib/coverage.js.map +2 -2
- package/dist/modules/query_index/lib/reindexer.js +43 -37
- package/dist/modules/query_index/lib/reindexer.js.map +2 -2
- package/dist/modules/query_index/lib/subscriber-scope.js +2 -3
- package/dist/modules/query_index/lib/subscriber-scope.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +137 -22
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/data/validators.js +2 -0
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/sales/workflows.js +10 -7
- package/dist/modules/sales/workflows.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js +27 -0
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/widgets/injection/order-approval/context-keys.js +10 -0
- package/dist/modules/workflows/widgets/injection/order-approval/context-keys.js.map +7 -0
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +10 -6
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +2 -2
- package/generated/entities/attachment_quota_reservation/index.ts +15 -0
- package/generated/entities.ids.generated.ts +2 -1
- package/generated/entity-fields-registry.ts +17 -0
- package/package.json +7 -7
- package/src/bootstrap.ts +17 -2
- package/src/modules/attachments/api/route.ts +114 -7
- package/src/modules/attachments/data/entities.ts +59 -0
- package/src/modules/attachments/di.ts +9 -1
- package/src/modules/attachments/i18n/de.json +3 -0
- package/src/modules/attachments/i18n/en.json +3 -0
- package/src/modules/attachments/i18n/es.json +3 -0
- package/src/modules/attachments/i18n/pl.json +3 -0
- package/src/modules/attachments/lib/drivers/index.ts +1 -1
- package/src/modules/attachments/lib/drivers/localDriver.ts +23 -8
- package/src/modules/attachments/lib/drivers/types.ts +5 -0
- package/src/modules/attachments/lib/quota-recovery-queue.ts +16 -0
- package/src/modules/attachments/lib/quota-service.ts +442 -0
- package/src/modules/attachments/migrations/.snapshot-open-mercato.json +648 -120
- package/src/modules/attachments/migrations/Migration20260709234741_attachments.ts +12 -0
- package/src/modules/attachments/workers/quota-recovery.ts +68 -0
- package/src/modules/auth/lib/setup-app.ts +20 -2
- package/src/modules/dashboards/services/widgetDataService.ts +3 -6
- package/src/modules/entities/api/definitions.ts +4 -0
- package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +94 -77
- package/src/modules/entities/cli.ts +238 -21
- package/src/modules/entities/components/RecordListRelationCell.tsx +90 -0
- package/src/modules/entities/components/recordListRelationDisplays.ts +109 -0
- package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.ts +17 -1
- package/src/modules/inbox_ops/components/proposals/EditActionDialog.tsx +21 -16
- package/src/modules/inbox_ops/components/proposals/types.ts +1 -0
- package/src/modules/messages/api/[id]/route.ts +21 -0
- package/src/modules/query_index/api/reindex.ts +15 -0
- package/src/modules/query_index/cli.ts +180 -1
- package/src/modules/query_index/lib/coverage.ts +38 -8
- package/src/modules/query_index/lib/reindexer.ts +62 -40
- package/src/modules/query_index/lib/subscriber-scope.ts +2 -7
- package/src/modules/sales/commands/documents.ts +237 -29
- package/src/modules/sales/data/validators.ts +2 -0
- package/src/modules/sales/workflows.ts +10 -7
- package/src/modules/workflows/i18n/de.json +1 -0
- package/src/modules/workflows/i18n/en.json +1 -0
- package/src/modules/workflows/i18n/es.json +1 -0
- package/src/modules/workflows/i18n/ko.json +1 -0
- package/src/modules/workflows/i18n/pl.json +1 -0
- package/src/modules/workflows/lib/activity-executor.ts +40 -1
- package/src/modules/workflows/widgets/injection/order-approval/context-keys.ts +18 -0
- package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +15 -6
|
@@ -665,6 +665,41 @@ function cloneJson<T>(value: T): T {
|
|
|
665
665
|
return JSON.parse(JSON.stringify(value)) as T;
|
|
666
666
|
}
|
|
667
667
|
|
|
668
|
+
/**
|
|
669
|
+
* Persists document-level custom fields supplied on a create input.
|
|
670
|
+
*
|
|
671
|
+
* Order and quote creates used to accept `customFields` at the HTTP boundary
|
|
672
|
+
* (`splitCustomFieldPayload` folds `cf_*` keys into it) and then drop the values
|
|
673
|
+
* on the floor, because the create schemas never declared the key and the
|
|
674
|
+
* handlers never wrote it — only updates did. Callers therefore had to create,
|
|
675
|
+
* then immediately update, to get custom fields to stick.
|
|
676
|
+
*/
|
|
677
|
+
async function setDocumentCustomFieldsIfSupplied(
|
|
678
|
+
em: EntityManager,
|
|
679
|
+
params: {
|
|
680
|
+
entityId: string;
|
|
681
|
+
recordId: string;
|
|
682
|
+
organizationId: string;
|
|
683
|
+
tenantId: string;
|
|
684
|
+
customFields: unknown;
|
|
685
|
+
},
|
|
686
|
+
): Promise<void> {
|
|
687
|
+
const { customFields } = params;
|
|
688
|
+
if (customFields === undefined || customFields === null) return;
|
|
689
|
+
const values =
|
|
690
|
+
typeof customFields === "object" && !Array.isArray(customFields)
|
|
691
|
+
? (customFields as Record<string, unknown>)
|
|
692
|
+
: {};
|
|
693
|
+
if (!Object.keys(values).length) return;
|
|
694
|
+
await setRecordCustomFields(em, {
|
|
695
|
+
entityId: params.entityId,
|
|
696
|
+
recordId: params.recordId,
|
|
697
|
+
organizationId: params.organizationId,
|
|
698
|
+
tenantId: params.tenantId,
|
|
699
|
+
values: normalizeCustomFieldValues(values),
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
|
|
668
703
|
async function resolveCustomerSnapshot(
|
|
669
704
|
em: EntityManager,
|
|
670
705
|
organizationId: string,
|
|
@@ -4186,6 +4221,20 @@ async function restoreOrderGraph(
|
|
|
4186
4221
|
organizationId: snapshot.order.organizationId,
|
|
4187
4222
|
},
|
|
4188
4223
|
);
|
|
4224
|
+
let existingLines: SalesOrderLine[] = [];
|
|
4225
|
+
if (order) {
|
|
4226
|
+
existingLines = await em.find(
|
|
4227
|
+
SalesOrderLine,
|
|
4228
|
+
{ order },
|
|
4229
|
+
{ orderBy: { lineNumber: "asc" } },
|
|
4230
|
+
);
|
|
4231
|
+
await assertShippedOrderGraphRestorable(
|
|
4232
|
+
em,
|
|
4233
|
+
order,
|
|
4234
|
+
existingLines,
|
|
4235
|
+
snapshot,
|
|
4236
|
+
);
|
|
4237
|
+
}
|
|
4189
4238
|
if (!order) {
|
|
4190
4239
|
order = em.create(SalesOrder, {
|
|
4191
4240
|
id: snapshot.order.id,
|
|
@@ -4270,11 +4319,6 @@ async function restoreOrderGraph(
|
|
|
4270
4319
|
}
|
|
4271
4320
|
applyOrderSnapshot(order, snapshot.order);
|
|
4272
4321
|
await em.flush();
|
|
4273
|
-
const existingLines = await em.find(
|
|
4274
|
-
SalesOrderLine,
|
|
4275
|
-
{ order: order.id },
|
|
4276
|
-
{ fields: ["id"] },
|
|
4277
|
-
);
|
|
4278
4322
|
const existingAdjustments = await em.find(
|
|
4279
4323
|
SalesOrderAdjustment,
|
|
4280
4324
|
{ order: order.id },
|
|
@@ -4782,6 +4826,13 @@ const createQuoteCommand: CommandHandler<
|
|
|
4782
4826
|
tenantId: quote.tenantId,
|
|
4783
4827
|
tagIds: parsed.tags,
|
|
4784
4828
|
});
|
|
4829
|
+
await setDocumentCustomFieldsIfSupplied(em, {
|
|
4830
|
+
entityId: E.sales.sales_quote,
|
|
4831
|
+
recordId: quote.id,
|
|
4832
|
+
organizationId: quote.organizationId,
|
|
4833
|
+
tenantId: quote.tenantId,
|
|
4834
|
+
customFields: parsed.customFields,
|
|
4835
|
+
});
|
|
4785
4836
|
},
|
|
4786
4837
|
],
|
|
4787
4838
|
{ transaction: true },
|
|
@@ -5804,6 +5855,13 @@ const createOrderCommand: CommandHandler<
|
|
|
5804
5855
|
tenantId: order.tenantId,
|
|
5805
5856
|
tagIds: parsed.tags,
|
|
5806
5857
|
});
|
|
5858
|
+
await setDocumentCustomFieldsIfSupplied(em, {
|
|
5859
|
+
entityId: E.sales.sales_order,
|
|
5860
|
+
recordId: order.id,
|
|
5861
|
+
organizationId: order.organizationId,
|
|
5862
|
+
tenantId: order.tenantId,
|
|
5863
|
+
customFields: parsed.customFields,
|
|
5864
|
+
});
|
|
5807
5865
|
},
|
|
5808
5866
|
],
|
|
5809
5867
|
{ transaction: true },
|
|
@@ -6625,6 +6683,34 @@ const quoteAdjustmentDeleteSchema = z.object({
|
|
|
6625
6683
|
|
|
6626
6684
|
const SHIPPED_QUANTITY_TOLERANCE = 1e-6;
|
|
6627
6685
|
|
|
6686
|
+
// Locked outright on a shipped line: none of these may move at all.
|
|
6687
|
+
const SHIPPED_LINE_NUMERIC_PRICING_FIELDS = [
|
|
6688
|
+
"unitPriceNet",
|
|
6689
|
+
"unitPriceGross",
|
|
6690
|
+
"discountAmount",
|
|
6691
|
+
"discountPercent",
|
|
6692
|
+
"taxRate",
|
|
6693
|
+
] as const;
|
|
6694
|
+
|
|
6695
|
+
// Line totals are derived, not authored: clients recompute and echo them on every
|
|
6696
|
+
// submit, so raising the quantity of a shipped line legitimately changes them. They
|
|
6697
|
+
// are still writable straight through, so they cannot simply be ignored either.
|
|
6698
|
+
// The invariant that separates a quantity edit from moving money is the per-unit
|
|
6699
|
+
// economics — total per unit must stay put, whatever the quantity does. That also
|
|
6700
|
+
// keeps discounted lines working, since a proportional total scales with quantity.
|
|
6701
|
+
const SHIPPED_LINE_DERIVED_TOTAL_FIELDS = [
|
|
6702
|
+
"totalNetAmount",
|
|
6703
|
+
"totalGrossAmount",
|
|
6704
|
+
] as const;
|
|
6705
|
+
|
|
6706
|
+
type ShippedLineComparable = Pick<
|
|
6707
|
+
SalesLineSnapshot,
|
|
6708
|
+
| "quantity"
|
|
6709
|
+
| "quantityUnit"
|
|
6710
|
+
| (typeof SHIPPED_LINE_NUMERIC_PRICING_FIELDS)[number]
|
|
6711
|
+
| (typeof SHIPPED_LINE_DERIVED_TOTAL_FIELDS)[number]
|
|
6712
|
+
>;
|
|
6713
|
+
|
|
6628
6714
|
const hasNumericChange = (
|
|
6629
6715
|
next: number | null | undefined,
|
|
6630
6716
|
previous: number | null | undefined,
|
|
@@ -6634,6 +6720,17 @@ const hasNumericChange = (
|
|
|
6634
6720
|
return Math.abs(next - previous) > SHIPPED_QUANTITY_TOLERANCE;
|
|
6635
6721
|
};
|
|
6636
6722
|
|
|
6723
|
+
const hasExactNumericChange = (
|
|
6724
|
+
next: number | null | undefined,
|
|
6725
|
+
previous: number | null | undefined,
|
|
6726
|
+
): boolean => {
|
|
6727
|
+
if (next === undefined || next === null) {
|
|
6728
|
+
return previous !== undefined && previous !== null;
|
|
6729
|
+
}
|
|
6730
|
+
if (previous === undefined || previous === null) return true;
|
|
6731
|
+
return Math.abs(next - previous) > SHIPPED_QUANTITY_TOLERANCE;
|
|
6732
|
+
};
|
|
6733
|
+
|
|
6637
6734
|
const hasUnitChange = (
|
|
6638
6735
|
next: string | null | undefined,
|
|
6639
6736
|
previous: string | null | undefined,
|
|
@@ -6644,6 +6741,111 @@ const hasUnitChange = (
|
|
|
6644
6741
|
return next.trim().toLowerCase() !== normalizedPrevious;
|
|
6645
6742
|
};
|
|
6646
6743
|
|
|
6744
|
+
const hasExactUnitChange = (
|
|
6745
|
+
next: string | null | undefined,
|
|
6746
|
+
previous: string | null | undefined,
|
|
6747
|
+
): boolean =>
|
|
6748
|
+
(next ?? "").trim().toLowerCase() !==
|
|
6749
|
+
(previous ?? "").trim().toLowerCase();
|
|
6750
|
+
|
|
6751
|
+
function hasShippedLineTotalChange(
|
|
6752
|
+
next: Partial<ShippedLineComparable>,
|
|
6753
|
+
previous: ShippedLineComparable,
|
|
6754
|
+
nextQuantity: number | null | undefined,
|
|
6755
|
+
exact: boolean,
|
|
6756
|
+
): boolean {
|
|
6757
|
+
const numericChange = exact ? hasExactNumericChange : hasNumericChange;
|
|
6758
|
+
const previousQuantity = previous.quantity;
|
|
6759
|
+
const canScale =
|
|
6760
|
+
typeof previousQuantity === "number" &&
|
|
6761
|
+
Math.abs(previousQuantity) > SHIPPED_QUANTITY_TOLERANCE &&
|
|
6762
|
+
typeof nextQuantity === "number";
|
|
6763
|
+
|
|
6764
|
+
return SHIPPED_LINE_DERIVED_TOTAL_FIELDS.some((field) => {
|
|
6765
|
+
const nextTotal = next[field];
|
|
6766
|
+
const previousTotal = previous[field];
|
|
6767
|
+
// Without a scalable quantity pair there is nothing to derive the expected
|
|
6768
|
+
// total from, so fall back to locking the value outright.
|
|
6769
|
+
if (!canScale || typeof previousTotal !== "number") {
|
|
6770
|
+
return numericChange(nextTotal, previousTotal);
|
|
6771
|
+
}
|
|
6772
|
+
if (nextTotal === undefined || nextTotal === null) {
|
|
6773
|
+
return exact ? numericChange(nextTotal, previousTotal) : false;
|
|
6774
|
+
}
|
|
6775
|
+
const expectedTotal =
|
|
6776
|
+
(previousTotal / (previousQuantity as number)) * (nextQuantity as number);
|
|
6777
|
+
// Scale the tolerance with magnitude: a fixed 1e-6 is below the float noise
|
|
6778
|
+
// floor once totals reach six figures.
|
|
6779
|
+
const tolerance = Math.max(
|
|
6780
|
+
SHIPPED_QUANTITY_TOLERANCE,
|
|
6781
|
+
Math.abs(expectedTotal) * 1e-9,
|
|
6782
|
+
);
|
|
6783
|
+
return Math.abs(nextTotal - expectedTotal) > tolerance;
|
|
6784
|
+
});
|
|
6785
|
+
}
|
|
6786
|
+
|
|
6787
|
+
function hasShippedLinePricingChange(
|
|
6788
|
+
next: Partial<ShippedLineComparable>,
|
|
6789
|
+
previous: ShippedLineComparable,
|
|
6790
|
+
nextQuantity: number | null | undefined,
|
|
6791
|
+
exact: boolean,
|
|
6792
|
+
): boolean {
|
|
6793
|
+
const numericChange = exact ? hasExactNumericChange : hasNumericChange;
|
|
6794
|
+
return (
|
|
6795
|
+
SHIPPED_LINE_NUMERIC_PRICING_FIELDS.some((field) =>
|
|
6796
|
+
numericChange(next[field], previous[field]),
|
|
6797
|
+
) ||
|
|
6798
|
+
hasShippedLineTotalChange(next, previous, nextQuantity, exact) ||
|
|
6799
|
+
(exact
|
|
6800
|
+
? hasExactUnitChange(next.quantityUnit, previous.quantityUnit)
|
|
6801
|
+
: hasUnitChange(next.quantityUnit, previous.quantityUnit))
|
|
6802
|
+
);
|
|
6803
|
+
}
|
|
6804
|
+
|
|
6805
|
+
async function assertShippedOrderLineChangeAllowed(
|
|
6806
|
+
existingSnapshot: SalesLineSnapshot,
|
|
6807
|
+
nextSnapshot: Partial<ShippedLineComparable> | null,
|
|
6808
|
+
shippedQuantity: number,
|
|
6809
|
+
exact: boolean,
|
|
6810
|
+
): Promise<void> {
|
|
6811
|
+
if (shippedQuantity <= 0) return;
|
|
6812
|
+
|
|
6813
|
+
const nextQuantity = nextSnapshot
|
|
6814
|
+
? exact
|
|
6815
|
+
? nextSnapshot.quantity
|
|
6816
|
+
: (nextSnapshot.quantity ?? existingSnapshot.quantity)
|
|
6817
|
+
: 0;
|
|
6818
|
+
const quantityInvalid =
|
|
6819
|
+
nextQuantity === undefined ||
|
|
6820
|
+
nextQuantity < shippedQuantity - SHIPPED_QUANTITY_TOLERANCE;
|
|
6821
|
+
const pricingChanged =
|
|
6822
|
+
nextSnapshot !== null &&
|
|
6823
|
+
hasShippedLinePricingChange(
|
|
6824
|
+
nextSnapshot,
|
|
6825
|
+
existingSnapshot,
|
|
6826
|
+
nextQuantity,
|
|
6827
|
+
exact,
|
|
6828
|
+
);
|
|
6829
|
+
if (!quantityInvalid && !pricingChanged) return;
|
|
6830
|
+
|
|
6831
|
+
const { translate } = await resolveTranslations();
|
|
6832
|
+
if (quantityInvalid) {
|
|
6833
|
+
throw new CrudHttpError(409, {
|
|
6834
|
+
error: translate(
|
|
6835
|
+
"sales.documents.items.errorQuantityBelowShipped",
|
|
6836
|
+
"You cannot lower the quantity below the {{shipped}} already shipped.",
|
|
6837
|
+
{ shipped: shippedQuantity },
|
|
6838
|
+
),
|
|
6839
|
+
});
|
|
6840
|
+
}
|
|
6841
|
+
throw new CrudHttpError(409, {
|
|
6842
|
+
error: translate(
|
|
6843
|
+
"sales.documents.items.errorPriceShipped",
|
|
6844
|
+
"You cannot change the price or unit of a line that has shipped items.",
|
|
6845
|
+
),
|
|
6846
|
+
});
|
|
6847
|
+
}
|
|
6848
|
+
|
|
6647
6849
|
async function assertShippedOrderLineEditable(
|
|
6648
6850
|
em: EntityManager,
|
|
6649
6851
|
order: SalesOrder,
|
|
@@ -6657,32 +6859,38 @@ async function assertShippedOrderLineEditable(
|
|
|
6657
6859
|
organizationId: order.organizationId,
|
|
6658
6860
|
});
|
|
6659
6861
|
const shippedQuantity = shippedByLine.get(lineId) ?? 0;
|
|
6660
|
-
|
|
6862
|
+
await assertShippedOrderLineChangeAllowed(
|
|
6863
|
+
existingSnapshot,
|
|
6864
|
+
parsed,
|
|
6865
|
+
shippedQuantity,
|
|
6866
|
+
false,
|
|
6867
|
+
);
|
|
6868
|
+
}
|
|
6661
6869
|
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6870
|
+
async function assertShippedOrderGraphRestorable(
|
|
6871
|
+
em: EntityManager,
|
|
6872
|
+
order: SalesOrder,
|
|
6873
|
+
existingLines: SalesOrderLine[],
|
|
6874
|
+
snapshot: OrderGraphSnapshot,
|
|
6875
|
+
): Promise<void> {
|
|
6876
|
+
const shippedByLine = await loadShippedQuantityByLine(em, order.id, {
|
|
6877
|
+
tenantId: order.tenantId,
|
|
6878
|
+
organizationId: order.organizationId,
|
|
6879
|
+
});
|
|
6880
|
+
if (shippedByLine.size === 0) return;
|
|
6673
6881
|
|
|
6674
|
-
const
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6882
|
+
const snapshotLinesById = new Map(
|
|
6883
|
+
snapshot.lines.flatMap((line) => (line.id ? [[line.id, line] as const] : [])),
|
|
6884
|
+
);
|
|
6885
|
+
for (const line of existingLines) {
|
|
6886
|
+
const shippedQuantity = shippedByLine.get(line.id) ?? 0;
|
|
6887
|
+
if (shippedQuantity <= 0) continue;
|
|
6888
|
+
await assertShippedOrderLineChangeAllowed(
|
|
6889
|
+
mapOrderLineEntityToSnapshot(line),
|
|
6890
|
+
snapshotLinesById.get(line.id) ?? null,
|
|
6891
|
+
shippedQuantity,
|
|
6892
|
+
true,
|
|
6893
|
+
);
|
|
6686
6894
|
}
|
|
6687
6895
|
}
|
|
6688
6896
|
|
|
@@ -717,6 +717,7 @@ export const orderCreateSchema = scoped.extend({
|
|
|
717
717
|
paymentMethodSnapshot: jsonRecord.optional(),
|
|
718
718
|
metadata,
|
|
719
719
|
customFieldSetId: uuid().optional(),
|
|
720
|
+
customFields: z.record(z.string(), z.unknown()).optional(),
|
|
720
721
|
lines: z
|
|
721
722
|
.array(orderLineCreateSchema.omit({ organizationId: true, tenantId: true, orderId: true }), {
|
|
722
723
|
error: SALES_ORDER_LINES_REQUIRED_MESSAGE_KEY,
|
|
@@ -761,6 +762,7 @@ export const quoteCreateSchema = scoped.extend({
|
|
|
761
762
|
paymentMethodSnapshot: jsonRecord.optional(),
|
|
762
763
|
metadata,
|
|
763
764
|
customFieldSetId: uuid().optional(),
|
|
765
|
+
customFields: z.record(z.string(), z.unknown()).optional(),
|
|
764
766
|
lines: z
|
|
765
767
|
.array(quoteLineCreateSchema.omit({ organizationId: true, tenantId: true, quoteId: true }))
|
|
766
768
|
.optional(),
|
|
@@ -57,7 +57,7 @@ const orderApproval = defineWorkflow({
|
|
|
57
57
|
config: {
|
|
58
58
|
commandId: 'sales.orders.update',
|
|
59
59
|
statusDictionary: 'sales.order_status',
|
|
60
|
-
input: { id: '{{context.
|
|
60
|
+
input: { id: '{{context.orderId}}', statusValue: 'pending_approval' },
|
|
61
61
|
},
|
|
62
62
|
retryPolicy: { maxAttempts: 3, initialIntervalMs: 1000, backoffCoefficient: 2, maxIntervalMs: 10000 },
|
|
63
63
|
},
|
|
@@ -68,7 +68,7 @@ const orderApproval = defineWorkflow({
|
|
|
68
68
|
async: true,
|
|
69
69
|
config: {
|
|
70
70
|
eventName: 'sales.order.approval.requested',
|
|
71
|
-
payload: { orderId: '{{context.
|
|
71
|
+
payload: { orderId: '{{context.orderId}}', workflowInstanceId: '{{workflow.instanceId}}' },
|
|
72
72
|
},
|
|
73
73
|
},
|
|
74
74
|
],
|
|
@@ -91,7 +91,7 @@ const orderApproval = defineWorkflow({
|
|
|
91
91
|
config: {
|
|
92
92
|
commandId: 'sales.orders.update',
|
|
93
93
|
statusDictionary: 'sales.order_status',
|
|
94
|
-
input: { id: '{{context.
|
|
94
|
+
input: { id: '{{context.orderId}}', statusValue: 'approved' },
|
|
95
95
|
},
|
|
96
96
|
retryPolicy: { maxAttempts: 3, initialIntervalMs: 1000, backoffCoefficient: 2, maxIntervalMs: 10000 },
|
|
97
97
|
},
|
|
@@ -103,7 +103,7 @@ const orderApproval = defineWorkflow({
|
|
|
103
103
|
config: {
|
|
104
104
|
eventName: 'sales.order.approval.approved',
|
|
105
105
|
payload: {
|
|
106
|
-
orderId: '{{context.
|
|
106
|
+
orderId: '{{context.orderId}}',
|
|
107
107
|
workflowInstanceId: '{{workflow.instanceId}}',
|
|
108
108
|
approvedBy: '{{context.completedBy}}',
|
|
109
109
|
comments: '{{context.comments}}',
|
|
@@ -130,7 +130,7 @@ const orderApproval = defineWorkflow({
|
|
|
130
130
|
config: {
|
|
131
131
|
commandId: 'sales.orders.update',
|
|
132
132
|
statusDictionary: 'sales.order_status',
|
|
133
|
-
input: { id: '{{context.
|
|
133
|
+
input: { id: '{{context.orderId}}', statusValue: 'rejected' },
|
|
134
134
|
},
|
|
135
135
|
retryPolicy: { maxAttempts: 3, initialIntervalMs: 1000, backoffCoefficient: 2, maxIntervalMs: 10000 },
|
|
136
136
|
},
|
|
@@ -142,7 +142,7 @@ const orderApproval = defineWorkflow({
|
|
|
142
142
|
config: {
|
|
143
143
|
eventName: 'sales.order.approval.rejected',
|
|
144
144
|
payload: {
|
|
145
|
-
orderId: '{{context.
|
|
145
|
+
orderId: '{{context.orderId}}',
|
|
146
146
|
workflowInstanceId: '{{workflow.instanceId}}',
|
|
147
147
|
rejectedBy: '{{context.completedBy}}',
|
|
148
148
|
comments: '{{context.comments}}',
|
|
@@ -175,7 +175,10 @@ const orderApproval = defineWorkflow({
|
|
|
175
175
|
name: 'Order Approval Trigger',
|
|
176
176
|
description: 'Triggers when a new sales order is created',
|
|
177
177
|
eventPattern: 'sales.order.created',
|
|
178
|
-
config: {
|
|
178
|
+
config: {
|
|
179
|
+
entityType: 'SalesOrder',
|
|
180
|
+
contextMapping: [{ targetKey: 'orderId', sourceExpression: 'id' }],
|
|
181
|
+
},
|
|
179
182
|
enabled: true,
|
|
180
183
|
priority: 0,
|
|
181
184
|
}],
|
|
@@ -935,6 +935,7 @@
|
|
|
935
935
|
"workflows.orderApproval.decisionLabel": "Entscheidung",
|
|
936
936
|
"workflows.orderApproval.groupDescription": "Bestellung prüfen und genehmigen oder ablehnen.",
|
|
937
937
|
"workflows.orderApproval.groupLabel": "Bestellgenehmigung",
|
|
938
|
+
"workflows.orderApproval.missingOrderId": "Der Genehmigungsworkflow kann erst gestartet werden, wenn die Bestellung geladen ist.",
|
|
938
939
|
"workflows.orderApproval.missingStatuses": "Fehlende Bestellstatus. Stellen Sie sicher, dass pending_approval, approved und rejected vorhanden sind.",
|
|
939
940
|
"workflows.orderApproval.noWorkflowActive": "Kein Genehmigungsworkflow für diese Bestellung aktiv.",
|
|
940
941
|
"workflows.orderApproval.pendingDescription": "Diese Bestellung erfordert eine Genehmigung.",
|
|
@@ -935,6 +935,7 @@
|
|
|
935
935
|
"workflows.orderApproval.decisionLabel": "Decision",
|
|
936
936
|
"workflows.orderApproval.groupDescription": "Review and approve or reject this order.",
|
|
937
937
|
"workflows.orderApproval.groupLabel": "Order Approval",
|
|
938
|
+
"workflows.orderApproval.missingOrderId": "Cannot start the approval workflow before the order has loaded.",
|
|
938
939
|
"workflows.orderApproval.missingStatuses": "Missing order status entries. Please ensure pending_approval, approved, and rejected statuses exist.",
|
|
939
940
|
"workflows.orderApproval.noWorkflowActive": "No approval workflow is active for this order.",
|
|
940
941
|
"workflows.orderApproval.pendingDescription": "This order requires approval before processing.",
|
|
@@ -935,6 +935,7 @@
|
|
|
935
935
|
"workflows.orderApproval.decisionLabel": "Decisión",
|
|
936
936
|
"workflows.orderApproval.groupDescription": "Revise y apruebe o rechace este pedido.",
|
|
937
937
|
"workflows.orderApproval.groupLabel": "Aprobación de pedido",
|
|
938
|
+
"workflows.orderApproval.missingOrderId": "No se puede iniciar el flujo de aprobación antes de que el pedido se haya cargado.",
|
|
938
939
|
"workflows.orderApproval.missingStatuses": "Faltan entradas de estado del pedido. Asegúrese de que existan los estados pending_approval, approved y rejected.",
|
|
939
940
|
"workflows.orderApproval.noWorkflowActive": "No hay un flujo de aprobación activo para este pedido.",
|
|
940
941
|
"workflows.orderApproval.pendingDescription": "Este pedido requiere aprobación antes de procesarse.",
|
|
@@ -935,6 +935,7 @@
|
|
|
935
935
|
"workflows.orderApproval.decisionLabel": "결정",
|
|
936
936
|
"workflows.orderApproval.groupDescription": "이 주문을 검토하여 승인하거나 반려하세요.",
|
|
937
937
|
"workflows.orderApproval.groupLabel": "주문 승인",
|
|
938
|
+
"workflows.orderApproval.missingOrderId": "주문이 로드되기 전에는 승인 워크플로를 시작할 수 없습니다.",
|
|
938
939
|
"workflows.orderApproval.missingStatuses": "주문 상태 항목이 누락되었습니다. pending_approval, approved, rejected 상태가 존재하는지 확인하세요.",
|
|
939
940
|
"workflows.orderApproval.noWorkflowActive": "이 주문에 대해 활성화된 승인 워크플로우가 없습니다.",
|
|
940
941
|
"workflows.orderApproval.pendingDescription": "이 주문은 처리 전에 승인이 필요합니다.",
|
|
@@ -935,6 +935,7 @@
|
|
|
935
935
|
"workflows.orderApproval.decisionLabel": "Decyzja",
|
|
936
936
|
"workflows.orderApproval.groupDescription": "Przejrzyj i zatwierdź lub odrzuć to zamówienie.",
|
|
937
937
|
"workflows.orderApproval.groupLabel": "Zatwierdzanie zamówienia",
|
|
938
|
+
"workflows.orderApproval.missingOrderId": "Nie można rozpocząć procesu zatwierdzania, zanim zamówienie się nie wczyta.",
|
|
938
939
|
"workflows.orderApproval.missingStatuses": "Brakuje wpisów statusów zamówienia. Upewnij się, że istnieją statusy pending_approval, approved i rejected.",
|
|
939
940
|
"workflows.orderApproval.noWorkflowActive": "Brak aktywnego przepływu zatwierdzania dla tego zamówienia.",
|
|
940
941
|
"workflows.orderApproval.pendingDescription": "To zamówienie wymaga zatwierdzenia przed realizacją.",
|
|
@@ -603,6 +603,17 @@ export async function executeEmitEvent(
|
|
|
603
603
|
throw new Error('EMIT_EVENT requires "eventName" field')
|
|
604
604
|
}
|
|
605
605
|
|
|
606
|
+
// Emissions are fire-and-forget and no subscriber validates the payload shape,
|
|
607
|
+
// so an unresolved `{{context.x}}` here is even quieter than on the command
|
|
608
|
+
// path — it ships the literal template to every consumer. Fail loudly instead.
|
|
609
|
+
const unresolvedPayloadKeys = findUnresolvedTemplateKeys(payload)
|
|
610
|
+
if (unresolvedPayloadKeys.length > 0) {
|
|
611
|
+
throw new Error(
|
|
612
|
+
`EMIT_EVENT payload contains unresolved template variables for: ${unresolvedPayloadKeys.join(', ')}. ` +
|
|
613
|
+
`Check that the workflow context provides these keys.`
|
|
614
|
+
)
|
|
615
|
+
}
|
|
616
|
+
|
|
606
617
|
// Get event bus from container
|
|
607
618
|
const eventBus = container.resolve<{ emitEvent: (event: string, payload: unknown, options?: unknown) => Promise<unknown> | unknown }>('eventBus')
|
|
608
619
|
|
|
@@ -629,6 +640,23 @@ export async function executeEmitEvent(
|
|
|
629
640
|
return { emitted: true, eventName, payload: enrichedPayload }
|
|
630
641
|
}
|
|
631
642
|
|
|
643
|
+
const UNRESOLVED_TEMPLATE_PATTERN = /\{\{[^}]+\}\}/
|
|
644
|
+
|
|
645
|
+
function findUnresolvedTemplateKeys(value: unknown, path = ''): string[] {
|
|
646
|
+
if (typeof value === 'string') {
|
|
647
|
+
return UNRESOLVED_TEMPLATE_PATTERN.test(value) ? [path] : []
|
|
648
|
+
}
|
|
649
|
+
if (Array.isArray(value)) {
|
|
650
|
+
return value.flatMap((item, index) => findUnresolvedTemplateKeys(item, `${path}[${index}]`))
|
|
651
|
+
}
|
|
652
|
+
if (value && typeof value === 'object') {
|
|
653
|
+
return Object.entries(value).flatMap(([key, nested]) =>
|
|
654
|
+
findUnresolvedTemplateKeys(nested, path ? `${path}.${key}` : key)
|
|
655
|
+
)
|
|
656
|
+
}
|
|
657
|
+
return []
|
|
658
|
+
}
|
|
659
|
+
|
|
632
660
|
/**
|
|
633
661
|
* UPDATE_ENTITY activity handler
|
|
634
662
|
*
|
|
@@ -651,11 +679,14 @@ export async function executeEmitEvent(
|
|
|
651
679
|
* "commandId": "sales.orders.update",
|
|
652
680
|
* "statusDictionary": "sales.order_status",
|
|
653
681
|
* "input": {
|
|
654
|
-
* "id": "{{context.
|
|
682
|
+
* "id": "{{context.orderId}}",
|
|
655
683
|
* "statusValue": "pending_approval"
|
|
656
684
|
* }
|
|
657
685
|
* }
|
|
658
686
|
* ```
|
|
687
|
+
*
|
|
688
|
+
* Every `{{...}}` reference in `input` must resolve against the workflow context —
|
|
689
|
+
* an unresolved reference is rejected rather than forwarded to the command bus.
|
|
659
690
|
*/
|
|
660
691
|
export async function executeUpdateEntity(
|
|
661
692
|
em: EntityManager,
|
|
@@ -701,6 +732,14 @@ export async function executeUpdateEntity(
|
|
|
701
732
|
throw new Error('CommandBus not available in container')
|
|
702
733
|
}
|
|
703
734
|
|
|
735
|
+
const unresolvedKeys = findUnresolvedTemplateKeys(input)
|
|
736
|
+
if (unresolvedKeys.length > 0) {
|
|
737
|
+
throw new Error(
|
|
738
|
+
`UPDATE_ENTITY input contains unresolved template variables for: ${unresolvedKeys.join(', ')}. ` +
|
|
739
|
+
`Check that the workflow context provides these keys.`
|
|
740
|
+
)
|
|
741
|
+
}
|
|
742
|
+
|
|
704
743
|
// Prepare final input, resolving statusValue if provided
|
|
705
744
|
let finalInput = { ...input }
|
|
706
745
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The context keys the order-approval widget seeds when it starts the
|
|
3
|
+
* `sales.order-approval` workflow. The workflow definition interpolates
|
|
4
|
+
* `{{context.<key>}}` against exactly this set, and `interpolateVariables()`
|
|
5
|
+
* silently returns the original string when a key is missing — so the two must
|
|
6
|
+
* not drift. Both the widget and the contract test import this list, which is
|
|
7
|
+
* what makes the drift a type error rather than a runtime surprise (issue #4334).
|
|
8
|
+
*/
|
|
9
|
+
export const ORDER_APPROVAL_CONTEXT_KEYS = [
|
|
10
|
+
'orderId',
|
|
11
|
+
'pendingApprovalStatusId',
|
|
12
|
+
'approvedStatusId',
|
|
13
|
+
'rejectedStatusId',
|
|
14
|
+
] as const
|
|
15
|
+
|
|
16
|
+
export type OrderApprovalContextKey = (typeof ORDER_APPROVAL_CONTEXT_KEYS)[number]
|
|
17
|
+
|
|
18
|
+
export type OrderApprovalInitialContext = Record<OrderApprovalContextKey, string>
|
|
@@ -11,6 +11,7 @@ import { Textarea } from '@open-mercato/ui/primitives/textarea'
|
|
|
11
11
|
import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
12
12
|
import { emitSalesDocumentDataRefresh } from '@open-mercato/core/modules/sales/lib/frontend/documentDataEvents'
|
|
13
13
|
import { fetchAllDictionaryEntries } from '@open-mercato/core/modules/dictionaries/lib/fetchAllEntries'
|
|
14
|
+
import type { OrderApprovalInitialContext } from './context-keys'
|
|
14
15
|
|
|
15
16
|
type OrderRecord = {
|
|
16
17
|
id: string
|
|
@@ -199,20 +200,28 @@ export default function OrderApprovalWidget({ data }: InjectionWidgetComponentPr
|
|
|
199
200
|
const approvedStatusId = findStatusId('approved')
|
|
200
201
|
const rejectedStatusId = findStatusId('rejected')
|
|
201
202
|
|
|
203
|
+
if (!orderId) {
|
|
204
|
+
throw new Error(t('workflows.orderApproval.missingOrderId', 'Cannot start the approval workflow before the order has loaded.'))
|
|
205
|
+
}
|
|
206
|
+
|
|
202
207
|
if (!pendingApprovalStatusId || !approvedStatusId || !rejectedStatusId) {
|
|
203
208
|
throw new Error(t('workflows.orderApproval.missingStatuses', 'Missing order status entries. Please ensure pending_approval, approved, and rejected statuses exist in the sales.order_status dictionary.'))
|
|
204
209
|
}
|
|
205
210
|
|
|
211
|
+
// Typed against the shared key list so a definition that references a new
|
|
212
|
+
// {{context.*}} key cannot compile until this seed provides it.
|
|
213
|
+
const initialContext: OrderApprovalInitialContext = {
|
|
214
|
+
orderId,
|
|
215
|
+
pendingApprovalStatusId,
|
|
216
|
+
approvedStatusId,
|
|
217
|
+
rejectedStatusId,
|
|
218
|
+
}
|
|
219
|
+
|
|
206
220
|
const result = await apiCall<{ data: WorkflowInstance }>('/api/workflows/instances', {
|
|
207
221
|
method: 'POST',
|
|
208
222
|
body: JSON.stringify({
|
|
209
223
|
workflowId: WORKFLOW_ID,
|
|
210
|
-
initialContext
|
|
211
|
-
orderId,
|
|
212
|
-
pendingApprovalStatusId,
|
|
213
|
-
approvedStatusId,
|
|
214
|
-
rejectedStatusId,
|
|
215
|
-
},
|
|
224
|
+
initialContext,
|
|
216
225
|
metadata: {
|
|
217
226
|
entityType: 'SalesOrder',
|
|
218
227
|
entityId: orderId,
|