@open-mercato/core 0.6.7-develop.6851.1.c1680b00d8 → 0.6.7-develop.6860.1.f796b22e80
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/cli.js +193 -19
- package/dist/modules/entities/cli.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/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 +110 -22
- package/dist/modules/sales/commands/documents.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/ko.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/cli.ts +238 -21
- package/src/modules/messages/api/[id]/route.ts +21 -0
- package/src/modules/query_index/lib/subscriber-scope.ts +2 -7
- package/src/modules/sales/commands/documents.ts +188 -29
- 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
|
@@ -4221,6 +4221,20 @@ async function restoreOrderGraph(
|
|
|
4221
4221
|
organizationId: snapshot.order.organizationId,
|
|
4222
4222
|
},
|
|
4223
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
|
+
}
|
|
4224
4238
|
if (!order) {
|
|
4225
4239
|
order = em.create(SalesOrder, {
|
|
4226
4240
|
id: snapshot.order.id,
|
|
@@ -4305,11 +4319,6 @@ async function restoreOrderGraph(
|
|
|
4305
4319
|
}
|
|
4306
4320
|
applyOrderSnapshot(order, snapshot.order);
|
|
4307
4321
|
await em.flush();
|
|
4308
|
-
const existingLines = await em.find(
|
|
4309
|
-
SalesOrderLine,
|
|
4310
|
-
{ order: order.id },
|
|
4311
|
-
{ fields: ["id"] },
|
|
4312
|
-
);
|
|
4313
4322
|
const existingAdjustments = await em.find(
|
|
4314
4323
|
SalesOrderAdjustment,
|
|
4315
4324
|
{ order: order.id },
|
|
@@ -6674,6 +6683,34 @@ const quoteAdjustmentDeleteSchema = z.object({
|
|
|
6674
6683
|
|
|
6675
6684
|
const SHIPPED_QUANTITY_TOLERANCE = 1e-6;
|
|
6676
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
|
+
|
|
6677
6714
|
const hasNumericChange = (
|
|
6678
6715
|
next: number | null | undefined,
|
|
6679
6716
|
previous: number | null | undefined,
|
|
@@ -6683,6 +6720,17 @@ const hasNumericChange = (
|
|
|
6683
6720
|
return Math.abs(next - previous) > SHIPPED_QUANTITY_TOLERANCE;
|
|
6684
6721
|
};
|
|
6685
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
|
+
|
|
6686
6734
|
const hasUnitChange = (
|
|
6687
6735
|
next: string | null | undefined,
|
|
6688
6736
|
previous: string | null | undefined,
|
|
@@ -6693,6 +6741,111 @@ const hasUnitChange = (
|
|
|
6693
6741
|
return next.trim().toLowerCase() !== normalizedPrevious;
|
|
6694
6742
|
};
|
|
6695
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
|
+
|
|
6696
6849
|
async function assertShippedOrderLineEditable(
|
|
6697
6850
|
em: EntityManager,
|
|
6698
6851
|
order: SalesOrder,
|
|
@@ -6706,32 +6859,38 @@ async function assertShippedOrderLineEditable(
|
|
|
6706
6859
|
organizationId: order.organizationId,
|
|
6707
6860
|
});
|
|
6708
6861
|
const shippedQuantity = shippedByLine.get(lineId) ?? 0;
|
|
6709
|
-
|
|
6862
|
+
await assertShippedOrderLineChangeAllowed(
|
|
6863
|
+
existingSnapshot,
|
|
6864
|
+
parsed,
|
|
6865
|
+
shippedQuantity,
|
|
6866
|
+
false,
|
|
6867
|
+
);
|
|
6868
|
+
}
|
|
6710
6869
|
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
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;
|
|
6722
6881
|
|
|
6723
|
-
const
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
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
|
+
);
|
|
6735
6894
|
}
|
|
6736
6895
|
}
|
|
6737
6896
|
|
|
@@ -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,
|