@open-mercato/core 0.6.6-develop.6330.1.a261878aa8 → 0.6.6-develop.6332.1.6e73f0f55b
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/dist/modules/attachments/components/AttachmentPartitionSettings.js +3 -0
- package/dist/modules/attachments/components/AttachmentPartitionSettings.js.map +2 -2
- package/dist/modules/auth/api/logout.js +2 -3
- package/dist/modules/auth/api/logout.js.map +2 -2
- package/dist/modules/auth/api/roles/acl/route.js +2 -2
- package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
- package/dist/modules/auth/api/session/refresh.js +5 -6
- package/dist/modules/auth/api/session/refresh.js.map +2 -2
- package/dist/modules/auth/api/users/acl/route.js +2 -2
- package/dist/modules/auth/api/users/acl/route.js.map +2 -2
- package/dist/modules/auth/backend/roles/[id]/edit/page.js +12 -0
- package/dist/modules/auth/backend/roles/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/backend/users/[id]/edit/page.js +12 -0
- package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/data/entities.js +2 -2
- package/dist/modules/auth/data/entities.js.map +2 -2
- package/dist/modules/auth/lib/requestRedirect.js +31 -3
- package/dist/modules/auth/lib/requestRedirect.js.map +2 -2
- package/dist/modules/business_rules/api/rules/route.js +3 -3
- package/dist/modules/business_rules/api/rules/route.js.map +2 -2
- package/dist/modules/business_rules/api/sets/route.js +3 -3
- package/dist/modules/business_rules/api/sets/route.js.map +2 -2
- package/dist/modules/business_rules/backend/rules/[id]/page.js +12 -1
- package/dist/modules/business_rules/backend/rules/[id]/page.js.map +2 -2
- package/dist/modules/business_rules/backend/sets/[id]/page.js +12 -1
- package/dist/modules/business_rules/backend/sets/[id]/page.js.map +2 -2
- package/dist/modules/catalog/api/product-media/route.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js +12 -0
- package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +15 -0
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +12 -1
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
- package/dist/modules/catalog/components/PriceKindSettings.js +13 -3
- package/dist/modules/catalog/components/PriceKindSettings.js.map +2 -2
- package/dist/modules/catalog/lib/bulkDelete.js.map +2 -2
- package/dist/modules/currencies/backend/currencies/[id]/page.js +15 -3
- package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
- package/dist/modules/currencies/backend/currencies/page.js +2 -1
- package/dist/modules/currencies/backend/currencies/page.js.map +2 -2
- package/dist/modules/currencies/backend/exchange-rates/[id]/page.js +12 -1
- package/dist/modules/currencies/backend/exchange-rates/[id]/page.js.map +2 -2
- package/dist/modules/currencies/backend/exchange-rates/page.js +3 -2
- package/dist/modules/currencies/backend/exchange-rates/page.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/roles/[id].js +3 -3
- package/dist/modules/customer_accounts/api/admin/roles/[id].js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users/[id].js +3 -3
- package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
- package/dist/modules/customers/api/addresses/route.js +4 -2
- package/dist/modules/customers/api/addresses/route.js.map +2 -2
- package/dist/modules/customers/api/tags/route.js +8 -3
- package/dist/modules/customers/api/tags/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +13 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +24 -9
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js +53 -23
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.js +13 -3
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +19 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +13 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/commands/interactions.js +5 -5
- package/dist/modules/customers/commands/interactions.js.map +2 -2
- package/dist/modules/customers/commands/pipeline-stages.js +24 -2
- package/dist/modules/customers/commands/pipeline-stages.js.map +2 -2
- package/dist/modules/customers/commands/pipelines.js +24 -2
- package/dist/modules/customers/commands/pipelines.js.map +2 -2
- package/dist/modules/customers/components/detail/AddressesSection.js +46 -29
- package/dist/modules/customers/components/detail/AddressesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +5 -1
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/notesAdapter.js +25 -18
- package/dist/modules/customers/components/detail/notesAdapter.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/[id]/route.js +38 -19
- package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/route.js +1 -1
- package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.js +32 -15
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-schedule-service.js +19 -5
- package/dist/modules/data_sync/lib/sync-schedule-service.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js +3 -3
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/route.js +3 -3
- package/dist/modules/dictionaries/api/[dictionaryId]/route.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionariesManager.js +12 -1
- package/dist/modules/dictionaries/components/DictionariesManager.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js.map +2 -2
- package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js +12 -0
- package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js.map +2 -2
- package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.js +12 -0
- package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.js.map +2 -2
- package/dist/modules/entities/api/entities.js +2 -2
- package/dist/modules/entities/api/entities.js.map +2 -2
- package/dist/modules/entities/api/records.js +7 -5
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/feature_toggles/api/overrides/route.js +2 -2
- package/dist/modules/feature_toggles/api/overrides/route.js.map +2 -2
- package/dist/modules/feature_toggles/backend/feature-toggles/global/[id]/edit/page.js.map +2 -2
- package/dist/modules/feature_toggles/data/entities.js +1 -1
- package/dist/modules/feature_toggles/data/entities.js.map +2 -2
- package/dist/modules/feature_toggles/data/validators.js +2 -1
- package/dist/modules/feature_toggles/data/validators.js.map +2 -2
- package/dist/modules/feature_toggles/lib/queries.js +2 -1
- package/dist/modules/feature_toggles/lib/queries.js.map +2 -2
- package/dist/modules/inbox_ops/api/settings/route.js +2 -2
- package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
- package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +12 -1
- package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js +12 -1
- package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
- package/dist/modules/resources/components/detail/notesAdapter.js +25 -18
- package/dist/modules/resources/components/detail/notesAdapter.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +14 -1
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +11 -10
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/commands/payments.js +6 -1
- package/dist/modules/sales/commands/payments.js.map +2 -2
- package/dist/modules/sales/commands/returns.js +3 -3
- package/dist/modules/sales/commands/returns.js.map +2 -2
- package/dist/modules/sales/commands/shared.js +3 -3
- package/dist/modules/sales/commands/shared.js.map +2 -2
- package/dist/modules/sales/commands/shipments.js +6 -1
- package/dist/modules/sales/commands/shipments.js.map +2 -2
- package/dist/modules/sales/components/documents/PaymentDialog.js +6 -3
- package/dist/modules/sales/components/documents/PaymentDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/PaymentsSection.js +7 -3
- package/dist/modules/sales/components/documents/PaymentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentDialog.js +6 -3
- package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentsSection.js +7 -3
- package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
- package/dist/modules/sales/di.js +39 -3
- package/dist/modules/sales/di.js.map +2 -2
- package/dist/modules/sales/setup.js +2 -0
- package/dist/modules/sales/setup.js.map +2 -2
- package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js +12 -1
- package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js +12 -1
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js +12 -1
- package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +12 -1
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/commands/job-histories.js +3 -3
- package/dist/modules/staff/commands/job-histories.js.map +2 -2
- package/dist/modules/staff/components/detail/addressesAdapter.js +33 -24
- package/dist/modules/staff/components/detail/addressesAdapter.js.map +2 -2
- package/dist/modules/staff/components/detail/notesAdapter.js +25 -18
- package/dist/modules/staff/components/detail/notesAdapter.js.map +2 -2
- package/dist/modules/translations/api/[entityType]/[entityId]/route.js +37 -0
- package/dist/modules/translations/api/[entityType]/[entityId]/route.js.map +2 -2
- package/dist/modules/translations/components/TranslationManager.js +7 -1
- package/dist/modules/translations/components/TranslationManager.js.map +2 -2
- package/dist/modules/workflows/api/definitions/[id]/route.js +3 -3
- package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/[id]/page.js +12 -1
- package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js +11 -1
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/attachments/components/AttachmentPartitionSettings.tsx +3 -0
- package/src/modules/auth/api/logout.ts +2 -3
- package/src/modules/auth/api/roles/acl/route.ts +2 -2
- package/src/modules/auth/api/session/refresh.ts +5 -6
- package/src/modules/auth/api/users/acl/route.ts +2 -2
- package/src/modules/auth/backend/roles/[id]/edit/page.tsx +17 -0
- package/src/modules/auth/backend/users/[id]/edit/page.tsx +17 -0
- package/src/modules/auth/data/entities.ts +2 -2
- package/src/modules/auth/lib/requestRedirect.ts +44 -2
- package/src/modules/business_rules/api/rules/route.ts +3 -3
- package/src/modules/business_rules/api/sets/route.ts +3 -3
- package/src/modules/business_rules/backend/rules/[id]/page.tsx +17 -1
- package/src/modules/business_rules/backend/sets/[id]/page.tsx +18 -1
- package/src/modules/catalog/api/product-media/route.ts +4 -0
- package/src/modules/catalog/backend/catalog/categories/[id]/edit/page.tsx +17 -0
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +21 -0
- package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +16 -1
- package/src/modules/catalog/components/PriceKindSettings.tsx +15 -7
- package/src/modules/catalog/lib/bulkDelete.ts +6 -0
- package/src/modules/currencies/backend/currencies/[id]/page.tsx +20 -3
- package/src/modules/currencies/backend/currencies/page.tsx +2 -1
- package/src/modules/currencies/backend/exchange-rates/[id]/page.tsx +17 -1
- package/src/modules/currencies/backend/exchange-rates/page.tsx +3 -2
- package/src/modules/customer_accounts/api/admin/roles/[id].ts +3 -3
- package/src/modules/customer_accounts/api/admin/users/[id].ts +3 -3
- package/src/modules/customers/api/addresses/route.ts +2 -0
- package/src/modules/customers/api/tags/route.ts +6 -1
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +19 -2
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +29 -8
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.ts +47 -20
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.ts +13 -3
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +21 -1
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +18 -2
- package/src/modules/customers/commands/interactions.ts +5 -5
- package/src/modules/customers/commands/pipeline-stages.ts +26 -2
- package/src/modules/customers/commands/pipelines.ts +26 -2
- package/src/modules/customers/components/detail/AddressesSection.tsx +50 -28
- package/src/modules/customers/components/detail/DealForm.tsx +12 -0
- package/src/modules/customers/components/detail/notesAdapter.ts +29 -18
- package/src/modules/customers/i18n/de.json +2 -0
- package/src/modules/customers/i18n/en.json +2 -0
- package/src/modules/customers/i18n/es.json +2 -0
- package/src/modules/customers/i18n/pl.json +2 -0
- package/src/modules/data_sync/api/schedules/[id]/route.ts +38 -20
- package/src/modules/data_sync/api/schedules/route.ts +1 -1
- package/src/modules/data_sync/backend/data-sync/page.tsx +32 -15
- package/src/modules/data_sync/lib/sync-schedule-service.ts +30 -5
- package/src/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.ts +3 -3
- package/src/modules/dictionaries/api/[dictionaryId]/route.ts +3 -3
- package/src/modules/dictionaries/components/DictionariesManager.tsx +21 -1
- package/src/modules/dictionaries/components/DictionaryEntriesEditor.tsx +2 -0
- package/src/modules/directory/backend/directory/organizations/[id]/edit/page.tsx +17 -0
- package/src/modules/directory/backend/directory/tenants/[id]/edit/page.tsx +17 -0
- package/src/modules/entities/api/entities.ts +2 -2
- package/src/modules/entities/api/records.ts +7 -5
- package/src/modules/feature_toggles/api/overrides/route.ts +2 -2
- package/src/modules/feature_toggles/backend/feature-toggles/global/[id]/edit/page.tsx +12 -0
- package/src/modules/feature_toggles/data/entities.ts +1 -1
- package/src/modules/feature_toggles/data/validators.ts +1 -0
- package/src/modules/feature_toggles/lib/queries.ts +1 -0
- package/src/modules/inbox_ops/api/settings/route.ts +2 -2
- package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +17 -1
- package/src/modules/resources/backend/resources/resources/[id]/page.tsx +17 -1
- package/src/modules/resources/components/detail/notesAdapter.ts +29 -18
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +17 -1
- package/src/modules/sales/commands/documents.ts +11 -10
- package/src/modules/sales/commands/payments.ts +11 -0
- package/src/modules/sales/commands/returns.ts +3 -3
- package/src/modules/sales/commands/shared.ts +10 -4
- package/src/modules/sales/commands/shipments.ts +14 -0
- package/src/modules/sales/components/documents/PaymentDialog.tsx +7 -3
- package/src/modules/sales/components/documents/PaymentsSection.tsx +8 -3
- package/src/modules/sales/components/documents/ShipmentDialog.tsx +7 -3
- package/src/modules/sales/components/documents/ShipmentsSection.tsx +8 -3
- package/src/modules/sales/di.ts +68 -9
- package/src/modules/sales/setup.ts +2 -0
- package/src/modules/staff/backend/staff/leave-requests/[id]/page.tsx +17 -1
- package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +17 -1
- package/src/modules/staff/backend/staff/team-roles/[id]/edit/page.tsx +17 -1
- package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +17 -1
- package/src/modules/staff/commands/job-histories.ts +3 -3
- package/src/modules/staff/components/detail/addressesAdapter.ts +40 -23
- package/src/modules/staff/components/detail/notesAdapter.ts +28 -18
- package/src/modules/translations/api/[entityType]/[entityId]/route.ts +74 -0
- package/src/modules/translations/components/TranslationManager.tsx +14 -1
- package/src/modules/workflows/api/definitions/[id]/route.ts +3 -3
- package/src/modules/workflows/backend/definitions/[id]/page.tsx +18 -1
- package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +19 -1
|
@@ -4981,7 +4981,7 @@ const updateQuoteCommand: CommandHandler<
|
|
|
4981
4981
|
if (!quote)
|
|
4982
4982
|
throw new CrudHttpError(404, { error: "Sales quote not found" });
|
|
4983
4983
|
ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
|
|
4984
|
-
enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
4984
|
+
await enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
4985
4985
|
const shouldInvalidateSentToken = (quote.status ?? null) === "sent";
|
|
4986
4986
|
if (shouldInvalidateSentToken) {
|
|
4987
4987
|
quote.acceptanceToken = null;
|
|
@@ -5213,6 +5213,7 @@ const updateOrderCommand: CommandHandler<
|
|
|
5213
5213
|
if (!order)
|
|
5214
5214
|
throw new CrudHttpError(404, { error: "Sales order not found" });
|
|
5215
5215
|
ensureOrderScope(ctx, order.organizationId, order.tenantId);
|
|
5216
|
+
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
5216
5217
|
const previousStatus = normalizeStatusValue(order.status);
|
|
5217
5218
|
let statusChangeNote: SalesNote | null = null;
|
|
5218
5219
|
const shouldRecalculateTotals =
|
|
@@ -6055,7 +6056,7 @@ const convertQuoteToOrderCommand: CommandHandler<
|
|
|
6055
6056
|
),
|
|
6056
6057
|
});
|
|
6057
6058
|
ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
|
|
6058
|
-
enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
6059
|
+
await enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
6059
6060
|
const snapshot = await loadQuoteSnapshot(em, payload.quoteId);
|
|
6060
6061
|
if (!snapshot)
|
|
6061
6062
|
throw new CrudHttpError(404, {
|
|
@@ -6545,7 +6546,7 @@ const orderLineUpsertCommand: CommandHandler<
|
|
|
6545
6546
|
if (!order)
|
|
6546
6547
|
throw new CrudHttpError(404, { error: "Sales order not found" });
|
|
6547
6548
|
ensureOrderScope(ctx, order.organizationId, order.tenantId);
|
|
6548
|
-
enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
6549
|
+
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
6549
6550
|
|
|
6550
6551
|
const [existingLines, adjustments] = await Promise.all([
|
|
6551
6552
|
em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
|
|
@@ -6866,7 +6867,7 @@ const orderLineDeleteCommand: CommandHandler<
|
|
|
6866
6867
|
),
|
|
6867
6868
|
});
|
|
6868
6869
|
ensureOrderScope(ctx, order.organizationId, order.tenantId);
|
|
6869
|
-
enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
6870
|
+
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
6870
6871
|
const shipmentCount = await em.count(SalesShipmentItem, {
|
|
6871
6872
|
orderLine: parsed.id,
|
|
6872
6873
|
shipment: { deletedAt: null },
|
|
@@ -7037,7 +7038,7 @@ const quoteLineUpsertCommand: CommandHandler<
|
|
|
7037
7038
|
if (!quote)
|
|
7038
7039
|
throw new CrudHttpError(404, { error: "Sales quote not found" });
|
|
7039
7040
|
ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
|
|
7040
|
-
enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
7041
|
+
await enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
7041
7042
|
const [existingLines, adjustments] = await Promise.all([
|
|
7042
7043
|
em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
|
|
7043
7044
|
em.find(
|
|
@@ -7349,7 +7350,7 @@ const quoteLineDeleteCommand: CommandHandler<
|
|
|
7349
7350
|
if (!quote)
|
|
7350
7351
|
throw new CrudHttpError(404, { error: "Sales quote not found" });
|
|
7351
7352
|
ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
|
|
7352
|
-
enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
7353
|
+
await enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
7353
7354
|
const existingLines = await em.find(
|
|
7354
7355
|
SalesQuoteLine,
|
|
7355
7356
|
{ quote },
|
|
@@ -7503,7 +7504,7 @@ const orderAdjustmentUpsertCommand: CommandHandler<
|
|
|
7503
7504
|
if (!order)
|
|
7504
7505
|
throw new CrudHttpError(404, { error: "Sales order not found" });
|
|
7505
7506
|
ensureOrderScope(ctx, order.organizationId, order.tenantId);
|
|
7506
|
-
enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
7507
|
+
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
7507
7508
|
if (parsed.scope === "line") {
|
|
7508
7509
|
throw new CrudHttpError(400, {
|
|
7509
7510
|
error: "Line-scoped adjustments are not supported yet.",
|
|
@@ -7797,7 +7798,7 @@ const orderAdjustmentDeleteCommand: CommandHandler<
|
|
|
7797
7798
|
if (!order)
|
|
7798
7799
|
throw new CrudHttpError(404, { error: "Sales order not found" });
|
|
7799
7800
|
ensureOrderScope(ctx, order.organizationId, order.tenantId);
|
|
7800
|
-
enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
7801
|
+
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
7801
7802
|
|
|
7802
7803
|
const [existingLines, adjustments] = await Promise.all([
|
|
7803
7804
|
em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
|
|
@@ -7962,7 +7963,7 @@ const quoteAdjustmentUpsertCommand: CommandHandler<
|
|
|
7962
7963
|
if (!quote)
|
|
7963
7964
|
throw new CrudHttpError(404, { error: "Sales quote not found" });
|
|
7964
7965
|
ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
|
|
7965
|
-
enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
7966
|
+
await enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
7966
7967
|
if (parsed.scope === "line") {
|
|
7967
7968
|
throw new CrudHttpError(400, {
|
|
7968
7969
|
error: "Line-scoped adjustments are not supported yet.",
|
|
@@ -8254,7 +8255,7 @@ const quoteAdjustmentDeleteCommand: CommandHandler<
|
|
|
8254
8255
|
if (!quote)
|
|
8255
8256
|
throw new CrudHttpError(404, { error: "Sales quote not found" });
|
|
8256
8257
|
ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
|
|
8257
|
-
enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
8258
|
+
await enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
8258
8259
|
|
|
8259
8260
|
const [existingLines, adjustments] = await Promise.all([
|
|
8260
8261
|
em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
|
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
ensureTenantScope,
|
|
31
31
|
extractUndoPayload,
|
|
32
32
|
toNumericString,
|
|
33
|
+
enforceSalesDocumentOptimisticLock,
|
|
34
|
+
SALES_RESOURCE_KIND_ORDER,
|
|
33
35
|
} from './shared'
|
|
34
36
|
import { resolveDictionaryEntryValue } from '../lib/dictionaries'
|
|
35
37
|
import { resolveRedoSnapshot } from '@open-mercato/shared/lib/commands/redo'
|
|
@@ -343,6 +345,9 @@ const createPaymentCommand: CommandHandler<
|
|
|
343
345
|
if (order.deletedAt) {
|
|
344
346
|
throw new CrudHttpError(404, { error: 'sales.payments.order_not_found' })
|
|
345
347
|
}
|
|
348
|
+
// Guard the parent order's aggregate version (Gap A): a payment mutation
|
|
349
|
+
// recalculates the order totals, so a stale parent must 409 before we touch it.
|
|
350
|
+
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER)
|
|
346
351
|
if (
|
|
347
352
|
order.currencyCode &&
|
|
348
353
|
input.currencyCode &&
|
|
@@ -730,6 +735,9 @@ const updatePaymentCommand: CommandHandler<
|
|
|
730
735
|
)
|
|
731
736
|
ensureSameScope(payment, resolvedOrganizationId, resolvedTenantId)
|
|
732
737
|
const previousOrder = payment.order as SalesOrder | null
|
|
738
|
+
// Guard the parent order's aggregate version (Gap A): updating a payment
|
|
739
|
+
// recalculates the order totals, so a stale parent must 409 before mutating.
|
|
740
|
+
await enforceSalesDocumentOptimisticLock(ctx, previousOrder, SALES_RESOURCE_KIND_ORDER)
|
|
733
741
|
// Apply payment scalar fields, order/line status changes and the
|
|
734
742
|
// allocations rebuild in one transaction so a mid-write failure cannot
|
|
735
743
|
// leave the payment and its allocations partially committed (#2336).
|
|
@@ -1064,6 +1072,9 @@ const deletePaymentCommand: CommandHandler<
|
|
|
1064
1072
|
)
|
|
1065
1073
|
ensureSameScope(payment, input.organizationId, input.tenantId)
|
|
1066
1074
|
const order = payment.order as SalesOrder | null
|
|
1075
|
+
// Guard the parent order's aggregate version (Gap A): deleting a payment
|
|
1076
|
+
// recalculates the order totals, so a stale parent must 409 before mutating.
|
|
1077
|
+
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER)
|
|
1067
1078
|
const allocations = await findWithDecryption(em, SalesPaymentAllocation, { payment }, {}, { tenantId: payment.tenantId, organizationId: payment.organizationId })
|
|
1068
1079
|
const allocationOrders = allocations
|
|
1069
1080
|
.map((allocation) =>
|
|
@@ -615,7 +615,7 @@ const createReturnCommand: CommandHandler<ReturnCreateInput, { returnId: string
|
|
|
615
615
|
throw new CrudHttpError(404, { error: translate('sales.returns.orderMissing', 'Order not found.') })
|
|
616
616
|
}
|
|
617
617
|
ensureSameScope(order, input.organizationId, input.tenantId)
|
|
618
|
-
enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER)
|
|
618
|
+
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER)
|
|
619
619
|
|
|
620
620
|
const orderLines = await findWithDecryption(
|
|
621
621
|
tx,
|
|
@@ -890,7 +890,7 @@ const updateReturnCommand: CommandHandler<ReturnUpdateInput, { returnId: string
|
|
|
890
890
|
}
|
|
891
891
|
// Lock on the return's own version — editing header fields (reason / notes /
|
|
892
892
|
// returnedAt) only touches the return, not the order totals.
|
|
893
|
-
enforceSalesDocumentOptimisticLock(ctx, entity, SALES_RESOURCE_KIND_RETURN)
|
|
893
|
+
await enforceSalesDocumentOptimisticLock(ctx, entity, SALES_RESOURCE_KIND_RETURN)
|
|
894
894
|
|
|
895
895
|
if (input.reason !== undefined) entity.reason = input.reason.length ? input.reason : null
|
|
896
896
|
if (input.notes !== undefined) entity.notes = input.notes.length ? input.notes : null
|
|
@@ -1020,7 +1020,7 @@ const deleteReturnCommand: CommandHandler<ReturnDeleteInput, { returnId: string
|
|
|
1020
1020
|
}
|
|
1021
1021
|
ensureSameScope(header, input.organizationId, input.tenantId)
|
|
1022
1022
|
// Lock on the return's own version, captured before any mutation.
|
|
1023
|
-
enforceSalesDocumentOptimisticLock(ctx, header, SALES_RESOURCE_KIND_RETURN)
|
|
1023
|
+
await enforceSalesDocumentOptimisticLock(ctx, header, SALES_RESOURCE_KIND_RETURN)
|
|
1024
1024
|
|
|
1025
1025
|
await reverseReturnEffects(em, salesCalculationService, snapshot)
|
|
1026
1026
|
|
|
@@ -2,7 +2,7 @@ import type { EntityManager } from '@mikro-orm/postgresql'
|
|
|
2
2
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
3
3
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
4
4
|
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
5
|
-
import {
|
|
5
|
+
import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
6
6
|
export { assertFound } from '@open-mercato/shared/lib/crud/errors'
|
|
7
7
|
export { ensureOrganizationScope, ensureSameScope, ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'
|
|
8
8
|
export { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
|
|
@@ -28,14 +28,20 @@ export const SALES_RESOURCE_KIND_RETURN = 'sales.return'
|
|
|
28
28
|
*
|
|
29
29
|
* Strictly additive: when the client sends no header the check is a no-op, so
|
|
30
30
|
* existing API consumers are unaffected. Respects `OM_OPTIMISTIC_LOCK`.
|
|
31
|
+
*
|
|
32
|
+
* Routes through the async DI-aware seam `enforceCommandOptimisticLockWithGuards`
|
|
33
|
+
* (Phase 0 / S1): the OSS `updated_at` floor runs first (identical behavior when
|
|
34
|
+
* `record_locks` is disabled — floor only), then the optional enterprise
|
|
35
|
+
* `record_locks` enrichment is awaited so the aggregate check observes the
|
|
36
|
+
* action-log diff when the resource is enabled. Callers MUST `await` it.
|
|
31
37
|
*/
|
|
32
|
-
export function enforceSalesDocumentOptimisticLock(
|
|
38
|
+
export async function enforceSalesDocumentOptimisticLock(
|
|
33
39
|
ctx: CommandRuntimeContext,
|
|
34
40
|
document: { id: string; updatedAt?: Date | string | null } | null | undefined,
|
|
35
41
|
resourceKind: string,
|
|
36
|
-
): void {
|
|
42
|
+
): Promise<void> {
|
|
37
43
|
if (!document) return
|
|
38
|
-
|
|
44
|
+
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
39
45
|
resourceKind,
|
|
40
46
|
resourceId: document.id,
|
|
41
47
|
current: document.updatedAt ?? null,
|
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
ensureSameScope,
|
|
30
30
|
ensureTenantScope,
|
|
31
31
|
extractUndoPayload,
|
|
32
|
+
enforceSalesDocumentOptimisticLock,
|
|
33
|
+
SALES_RESOURCE_KIND_ORDER,
|
|
32
34
|
} from './shared'
|
|
33
35
|
import { resolveDictionaryEntryValue } from '../lib/dictionaries'
|
|
34
36
|
import { resolveRedoSnapshot } from '@open-mercato/shared/lib/commands/redo'
|
|
@@ -442,6 +444,10 @@ const createShipmentCommand: CommandHandler<ShipmentCreateInput, { shipmentId: s
|
|
|
442
444
|
const shipment = await em.transactional(async (tx) => {
|
|
443
445
|
const order = await loadOrder(tx, input.orderId, { tenantId: input.tenantId, organizationId: input.organizationId })
|
|
444
446
|
ensureSameScope(order, input.organizationId, input.tenantId)
|
|
447
|
+
// Guard the parent order's aggregate version (Gap B): a shipment mutation
|
|
448
|
+
// recalculates the order's fulfilled quantities, so a stale parent must 409
|
|
449
|
+
// before we touch it.
|
|
450
|
+
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER)
|
|
445
451
|
const { items: normalizedItems, lineMap } = await validateShipmentItems({
|
|
446
452
|
em: tx,
|
|
447
453
|
order,
|
|
@@ -674,6 +680,10 @@ const updateShipmentCommand: CommandHandler<ShipmentUpdateInput, { shipmentId: s
|
|
|
674
680
|
if (input.orderId && input.orderId !== order.id) {
|
|
675
681
|
throw new CrudHttpError(400, { error: 'sales.shipments.invalid_order' })
|
|
676
682
|
}
|
|
683
|
+
// Guard the parent order's aggregate version (Gap B): updating a shipment
|
|
684
|
+
// recalculates the order's fulfilled quantities, so a stale parent must 409
|
|
685
|
+
// before we touch it.
|
|
686
|
+
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER)
|
|
677
687
|
|
|
678
688
|
// Check if order is financially closed - prevent modifications to shipments
|
|
679
689
|
const paidAmount = parseFloat(order.paidTotalAmount || '0')
|
|
@@ -976,6 +986,10 @@ const deleteShipmentCommand: CommandHandler<
|
|
|
976
986
|
if (order.id !== payload.orderId) {
|
|
977
987
|
throw new CrudHttpError(400, { error: translate('sales.shipments.invalid_order', 'Shipment does not belong to this order') })
|
|
978
988
|
}
|
|
989
|
+
// Guard the parent order's aggregate version (Gap B): deleting a shipment
|
|
990
|
+
// recalculates the order's fulfilled quantities, so a stale parent must 409
|
|
991
|
+
// before we touch it.
|
|
992
|
+
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER)
|
|
979
993
|
const scope = { tenantId: shipmentEntity.tenantId, organizationId: shipmentEntity.organizationId }
|
|
980
994
|
const items = await findWithDecryption(tx, SalesShipmentItem, { shipment: shipmentEntity }, {}, scope)
|
|
981
995
|
items.forEach((item) => tx.remove(item))
|
|
@@ -9,7 +9,7 @@ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimi
|
|
|
9
9
|
import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
|
|
10
10
|
import { createCrud, updateCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
11
11
|
import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
12
|
-
import { handleSectionMutationError
|
|
12
|
+
import { handleSectionMutationError } from './optimisticLock'
|
|
13
13
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
|
|
14
14
|
import { useDialogKeyHandler } from '@open-mercato/ui/hooks/useDialogKeyHandler'
|
|
15
15
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
@@ -58,6 +58,7 @@ type PaymentDialogProps = {
|
|
|
58
58
|
orderId: string
|
|
59
59
|
organizationId: string | null
|
|
60
60
|
tenantId: string | null
|
|
61
|
+
documentUpdatedAt?: string | null
|
|
61
62
|
onOpenChange: (open: boolean) => void
|
|
62
63
|
onSaved?: (totals?: PaymentTotals | null) => void | Promise<void>
|
|
63
64
|
}
|
|
@@ -79,6 +80,7 @@ export function PaymentDialog({
|
|
|
79
80
|
orderId,
|
|
80
81
|
organizationId,
|
|
81
82
|
tenantId,
|
|
83
|
+
documentUpdatedAt,
|
|
82
84
|
onOpenChange,
|
|
83
85
|
onSaved,
|
|
84
86
|
}: PaymentDialogProps) {
|
|
@@ -539,7 +541,9 @@ export function PaymentDialog({
|
|
|
539
541
|
const action = payment?.id ? updateCrud : createCrud
|
|
540
542
|
try {
|
|
541
543
|
const result = await withScopedApiRequestHeaders(
|
|
542
|
-
|
|
544
|
+
// The server guards the PARENT order's aggregate version (Gap A) for
|
|
545
|
+
// both create and update, so send the order's `updated_at`.
|
|
546
|
+
buildOptimisticLockHeader(documentUpdatedAt ?? undefined),
|
|
543
547
|
() =>
|
|
544
548
|
action(
|
|
545
549
|
'sales/payments',
|
|
@@ -569,7 +573,7 @@ export function PaymentDialog({
|
|
|
569
573
|
throw err
|
|
570
574
|
}
|
|
571
575
|
},
|
|
572
|
-
[currencyCode, mode, onOpenChange, onSaved, orderId, organizationId, payment?.id,
|
|
576
|
+
[currencyCode, documentUpdatedAt, mode, onOpenChange, onSaved, orderId, organizationId, payment?.id, t, tenantId]
|
|
573
577
|
)
|
|
574
578
|
|
|
575
579
|
const handleSubmitForm = React.useCallback(
|
|
@@ -7,7 +7,7 @@ import { LoadingMessage, ErrorMessage, TabEmptyState } from '@open-mercato/ui/ba
|
|
|
7
7
|
import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
8
8
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
9
9
|
import { deleteCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
10
|
-
import { handleSectionMutationError, readRowUpdatedAt
|
|
10
|
+
import { handleSectionMutationError, readRowUpdatedAt } from './optimisticLock'
|
|
11
11
|
import type { SectionAction } from '@open-mercato/ui/backend/detail'
|
|
12
12
|
import { RowActions } from '@open-mercato/ui/backend/RowActions'
|
|
13
13
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
@@ -41,6 +41,7 @@ type SalesDocumentPaymentsSectionProps = {
|
|
|
41
41
|
currencyCode: string | null | undefined
|
|
42
42
|
organizationId?: string | null
|
|
43
43
|
tenantId?: string | null
|
|
44
|
+
documentUpdatedAt?: string | null
|
|
44
45
|
onActionChange?: (action: SectionAction | null) => void
|
|
45
46
|
onTotalsChange?: () => void
|
|
46
47
|
onPaymentsChange?: (payments: PaymentRow[]) => void
|
|
@@ -69,6 +70,7 @@ export function SalesDocumentPaymentsSection({
|
|
|
69
70
|
currencyCode,
|
|
70
71
|
organizationId: orgFromProps,
|
|
71
72
|
tenantId: tenantFromProps,
|
|
73
|
+
documentUpdatedAt,
|
|
72
74
|
onActionChange,
|
|
73
75
|
onTotalsChange,
|
|
74
76
|
onPaymentsChange,
|
|
@@ -216,7 +218,9 @@ export function SalesDocumentPaymentsSection({
|
|
|
216
218
|
async (row: PaymentRow) => {
|
|
217
219
|
try {
|
|
218
220
|
const result = await withScopedApiRequestHeaders(
|
|
219
|
-
|
|
221
|
+
// The server guards the PARENT order's aggregate version (Gap A), so
|
|
222
|
+
// send the order's `updated_at`, not the payment row's.
|
|
223
|
+
buildOptimisticLockHeader(documentUpdatedAt ?? undefined),
|
|
220
224
|
() =>
|
|
221
225
|
deleteCrud<{ orderTotals?: PaymentTotals | null }>('sales/payments', {
|
|
222
226
|
body: {
|
|
@@ -242,7 +246,7 @@ export function SalesDocumentPaymentsSection({
|
|
|
242
246
|
flash(t('sales.documents.payments.errorDelete', 'Failed to delete payment.'), 'error')
|
|
243
247
|
}
|
|
244
248
|
},
|
|
245
|
-
[loadPayments, onTotalsChange, orderId, resolvedOrganizationId, resolvedTenantId, t]
|
|
249
|
+
[documentUpdatedAt, loadPayments, onTotalsChange, orderId, resolvedOrganizationId, resolvedTenantId, t]
|
|
246
250
|
)
|
|
247
251
|
|
|
248
252
|
React.useEffect(() => {
|
|
@@ -369,6 +373,7 @@ export function SalesDocumentPaymentsSection({
|
|
|
369
373
|
orderId={orderId}
|
|
370
374
|
organizationId={resolvedOrganizationId}
|
|
371
375
|
tenantId={resolvedTenantId}
|
|
376
|
+
documentUpdatedAt={documentUpdatedAt ?? null}
|
|
372
377
|
onSaved={handlePaymentSaved}
|
|
373
378
|
/>
|
|
374
379
|
</div>
|
|
@@ -15,7 +15,7 @@ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimi
|
|
|
15
15
|
import { createCrud, updateCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
16
16
|
import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
|
|
17
17
|
import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
18
|
-
import { handleSectionMutationError
|
|
18
|
+
import { handleSectionMutationError } from './optimisticLock'
|
|
19
19
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
20
20
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
21
21
|
import { E } from '#generated/entities.ids.generated'
|
|
@@ -46,6 +46,7 @@ type ShipmentDialogProps = {
|
|
|
46
46
|
currencyCode?: string | null
|
|
47
47
|
organizationId: string | null
|
|
48
48
|
tenantId: string | null
|
|
49
|
+
documentUpdatedAt?: string | null
|
|
49
50
|
computeAvailable: (lineId: string, excludeShipmentId?: string | null) => number
|
|
50
51
|
shippingAddressSnapshot?: NormalizedAddressSnapshot | Record<string, unknown> | null
|
|
51
52
|
onClose: () => void
|
|
@@ -278,6 +279,7 @@ export function ShipmentDialog({
|
|
|
278
279
|
currencyCode,
|
|
279
280
|
organizationId,
|
|
280
281
|
tenantId,
|
|
282
|
+
documentUpdatedAt,
|
|
281
283
|
computeAvailable,
|
|
282
284
|
shippingAddressSnapshot,
|
|
283
285
|
onClose,
|
|
@@ -1089,7 +1091,9 @@ export function ShipmentDialog({
|
|
|
1089
1091
|
let result
|
|
1090
1092
|
try {
|
|
1091
1093
|
result = await withScopedApiRequestHeaders(
|
|
1092
|
-
|
|
1094
|
+
// The server guards the PARENT order's aggregate version (Gap B) for
|
|
1095
|
+
// both create and update, so send the order's `updated_at`.
|
|
1096
|
+
buildOptimisticLockHeader(documentUpdatedAt ?? undefined),
|
|
1093
1097
|
() =>
|
|
1094
1098
|
action(
|
|
1095
1099
|
'sales/shipments',
|
|
@@ -1219,6 +1223,7 @@ export function ShipmentDialog({
|
|
|
1219
1223
|
},
|
|
1220
1224
|
[
|
|
1221
1225
|
currencyCode,
|
|
1226
|
+
documentUpdatedAt,
|
|
1222
1227
|
lines,
|
|
1223
1228
|
mode,
|
|
1224
1229
|
onAddComment,
|
|
@@ -1226,7 +1231,6 @@ export function ShipmentDialog({
|
|
|
1226
1231
|
orderId,
|
|
1227
1232
|
organizationId,
|
|
1228
1233
|
shipment?.id,
|
|
1229
|
-
shipment?.updatedAt,
|
|
1230
1234
|
addressOptions,
|
|
1231
1235
|
addressOptionsMap,
|
|
1232
1236
|
shippingMethods,
|
|
@@ -20,7 +20,7 @@ import type { SectionAction } from '@open-mercato/core/modules/customers/compone
|
|
|
20
20
|
import { generateTempId } from '@open-mercato/core/modules/customers/lib/detailHelpers'
|
|
21
21
|
import { formatAddressString, type AddressValue } from '@open-mercato/core/modules/customers/utils/addressFormat'
|
|
22
22
|
import { ShipmentDialog } from './ShipmentDialog'
|
|
23
|
-
import { handleSectionMutationError, readRowUpdatedAt
|
|
23
|
+
import { handleSectionMutationError, readRowUpdatedAt } from './optimisticLock'
|
|
24
24
|
import { extractCustomFieldValues } from './customFieldHelpers'
|
|
25
25
|
import type { OrderLine, ShipmentRow, ShipmentItem } from './shipmentTypes'
|
|
26
26
|
|
|
@@ -33,6 +33,7 @@ type SalesShipmentsSectionProps = {
|
|
|
33
33
|
shippingAddressSnapshot?: Record<string, unknown> | null
|
|
34
34
|
organizationId?: string | null
|
|
35
35
|
tenantId?: string | null
|
|
36
|
+
documentUpdatedAt?: string | null
|
|
36
37
|
onActionChange?: (action: SectionAction | null) => void
|
|
37
38
|
onAddComment?: (body: string) => Promise<void>
|
|
38
39
|
}
|
|
@@ -114,6 +115,7 @@ export function SalesShipmentsSection({
|
|
|
114
115
|
shippingAddressSnapshot,
|
|
115
116
|
organizationId: organizationIdProp,
|
|
116
117
|
tenantId: tenantIdProp,
|
|
118
|
+
documentUpdatedAt,
|
|
117
119
|
onActionChange,
|
|
118
120
|
onAddComment,
|
|
119
121
|
}: SalesShipmentsSectionProps) {
|
|
@@ -401,7 +403,9 @@ export function SalesShipmentsSection({
|
|
|
401
403
|
if (!confirmed) return
|
|
402
404
|
try {
|
|
403
405
|
const result = await withScopedApiRequestHeaders(
|
|
404
|
-
|
|
406
|
+
// The server guards the PARENT order's aggregate version (Gap B), so
|
|
407
|
+
// send the order's `updated_at`, not the shipment row's.
|
|
408
|
+
buildOptimisticLockHeader(documentUpdatedAt ?? undefined),
|
|
405
409
|
() =>
|
|
406
410
|
deleteCrud('sales/shipments', {
|
|
407
411
|
body: {
|
|
@@ -425,7 +429,7 @@ export function SalesShipmentsSection({
|
|
|
425
429
|
flash(t('sales.documents.shipments.errorDelete', 'Failed to delete shipment.'), 'error')
|
|
426
430
|
}
|
|
427
431
|
},
|
|
428
|
-
[confirm, loadShipments, orderId, resolvedOrganizationId, resolvedTenantId, t]
|
|
432
|
+
[confirm, documentUpdatedAt, loadShipments, orderId, resolvedOrganizationId, resolvedTenantId, t]
|
|
429
433
|
)
|
|
430
434
|
|
|
431
435
|
const renderItemList = (items: ShipmentItem[]) => (
|
|
@@ -573,6 +577,7 @@ export function SalesShipmentsSection({
|
|
|
573
577
|
currencyCode={currencyCode}
|
|
574
578
|
organizationId={resolvedOrganizationId}
|
|
575
579
|
tenantId={resolvedTenantId}
|
|
580
|
+
documentUpdatedAt={documentUpdatedAt ?? null}
|
|
576
581
|
computeAvailable={computeAvailable}
|
|
577
582
|
shippingAddressSnapshot={shippingAddressSnapshot}
|
|
578
583
|
onClose={() => setDialogState(null)}
|
package/src/modules/sales/di.ts
CHANGED
|
@@ -42,15 +42,20 @@ type AppCradle = AppContainer['cradle'] & {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
const RESOURCE_KIND_ORDER = 'sales.order'
|
|
45
|
+
const RESOURCE_KIND_PAYMENT = 'sales.payment'
|
|
46
|
+
const RESOURCE_KIND_SHIPMENT = 'sales.shipment'
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
async function readOrderUpdatedAtById(
|
|
47
49
|
em: EntityManager,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
orderId: string | null | undefined,
|
|
51
|
+
tenantId: string,
|
|
52
|
+
organizationId: string | null | undefined,
|
|
53
|
+
): Promise<string | null> {
|
|
54
|
+
if (!orderId) return null
|
|
50
55
|
const row = await em.findOne(
|
|
51
56
|
SalesOrder,
|
|
52
57
|
{
|
|
53
|
-
id:
|
|
58
|
+
id: orderId,
|
|
54
59
|
tenantId,
|
|
55
60
|
...(organizationId ? { organizationId } : {}),
|
|
56
61
|
deletedAt: null,
|
|
@@ -60,13 +65,67 @@ const readSalesOrderUpdatedAt: OptimisticLockCurrentReader = async (
|
|
|
60
65
|
return row?.updatedAt instanceof Date ? row.updatedAt.toISOString() : null
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
const readSalesOrderUpdatedAt: OptimisticLockCurrentReader = async (
|
|
69
|
+
em: EntityManager,
|
|
70
|
+
{ resourceId, tenantId, organizationId },
|
|
71
|
+
) => readOrderUpdatedAtById(em, resourceId, tenantId, organizationId)
|
|
72
|
+
|
|
73
|
+
// Payments and shipments are guarded against the PARENT ORDER's aggregate
|
|
74
|
+
// version (their mutations recalc order totals / fulfilled quantities, so the
|
|
75
|
+
// order is the consistency boundary — same model as lines/adjustments/returns).
|
|
76
|
+
// The CRUD-layer guard therefore must compare against the order, not the
|
|
77
|
+
// payment/shipment row: resolve the parent order from the sub-resource id and
|
|
78
|
+
// return its `updated_at`. This keeps the single optimistic-lock header
|
|
79
|
+
// consistent across the makeCrudRoute guard and the command-level
|
|
80
|
+
// `enforceSalesDocumentOptimisticLock` guard (both read the order version).
|
|
81
|
+
const readPaymentParentOrderUpdatedAt: OptimisticLockCurrentReader = async (
|
|
82
|
+
em: EntityManager,
|
|
83
|
+
{ resourceId, tenantId, organizationId },
|
|
84
|
+
) => {
|
|
85
|
+
const payment = await em.findOne(
|
|
86
|
+
SalesPayment,
|
|
87
|
+
{
|
|
88
|
+
id: resourceId,
|
|
89
|
+
tenantId,
|
|
90
|
+
...(organizationId ? { organizationId } : {}),
|
|
91
|
+
deletedAt: null,
|
|
92
|
+
},
|
|
93
|
+
{ fields: ['order'] as const },
|
|
94
|
+
)
|
|
95
|
+
const orderId = (payment as { order?: { id?: string } | string | null } | null)?.order
|
|
96
|
+
const resolvedOrderId = typeof orderId === 'string' ? orderId : orderId?.id ?? null
|
|
97
|
+
return readOrderUpdatedAtById(em, resolvedOrderId, tenantId, organizationId)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const readShipmentParentOrderUpdatedAt: OptimisticLockCurrentReader = async (
|
|
101
|
+
em: EntityManager,
|
|
102
|
+
{ resourceId, tenantId, organizationId },
|
|
103
|
+
) => {
|
|
104
|
+
const shipment = await em.findOne(
|
|
105
|
+
SalesShipment,
|
|
106
|
+
{
|
|
107
|
+
id: resourceId,
|
|
108
|
+
tenantId,
|
|
109
|
+
...(organizationId ? { organizationId } : {}),
|
|
110
|
+
deletedAt: null,
|
|
111
|
+
},
|
|
112
|
+
{ fields: ['order'] as const },
|
|
113
|
+
)
|
|
114
|
+
const orderId = (shipment as { order?: { id?: string } | string | null } | null)?.order
|
|
115
|
+
const resolvedOrderId = typeof orderId === 'string' ? orderId : orderId?.id ?? null
|
|
116
|
+
return readOrderUpdatedAtById(em, resolvedOrderId, tenantId, organizationId)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Hand-wired sales readers registered at module-load time. `sales.order` mirrors
|
|
120
|
+
// the auto-registered generic reader (kept as the polymorphic-table override
|
|
121
|
+
// reference). `sales.payment`/`sales.shipment` redirect the CRUD-layer guard to
|
|
122
|
+
// the parent order so payments/shipments guard the aggregate (Phase 3 Gap A/B)
|
|
123
|
+
// instead of their own row. The guard's mode check short-circuits when
|
|
124
|
+
// `OM_OPTIMISTIC_LOCK=off`.
|
|
68
125
|
registerOptimisticLockReaders({
|
|
69
126
|
[RESOURCE_KIND_ORDER]: readSalesOrderUpdatedAt,
|
|
127
|
+
[RESOURCE_KIND_PAYMENT]: readPaymentParentOrderUpdatedAt,
|
|
128
|
+
[RESOURCE_KIND_SHIPMENT]: readShipmentParentOrderUpdatedAt,
|
|
70
129
|
})
|
|
71
130
|
|
|
72
131
|
export function register(container: AppContainer) {
|
|
@@ -51,7 +51,9 @@ export const setup: ModuleSetupConfig = {
|
|
|
51
51
|
admin: ['sales.*', 'sales.documents.number.edit'],
|
|
52
52
|
employee: [
|
|
53
53
|
'sales.channels.view',
|
|
54
|
+
'sales.channels.manage',
|
|
54
55
|
'sales.settings.view',
|
|
56
|
+
'sales.settings.manage',
|
|
55
57
|
'sales.orders.view',
|
|
56
58
|
'sales.orders.manage',
|
|
57
59
|
'sales.orders.approve',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import { useRouter } from 'next/navigation'
|
|
4
|
+
import { usePathname, useRouter } from 'next/navigation'
|
|
5
5
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
6
6
|
import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
7
7
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
@@ -14,12 +14,14 @@ import { updateCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
|
14
14
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
15
15
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
16
16
|
import { LeaveRequestForm, buildLeaveRequestPayload, type LeaveRequestFormValues } from '@open-mercato/core/modules/staff/components/LeaveRequestForm'
|
|
17
|
+
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
17
18
|
import { type LeaveRequestRecord, type LeaveRequestsResponse, type NormalizedLeaveRequest, normalizeLeaveRequest, resolveStatusVariant, formatDateLabel, formatDateRange } from '../../../../lib/leaveRequestHelpers'
|
|
18
19
|
|
|
19
20
|
export default function StaffLeaveRequestDetailPage({ params }: { params?: { id?: string } }) {
|
|
20
21
|
const id = params?.id
|
|
21
22
|
const t = useT()
|
|
22
23
|
const router = useRouter()
|
|
24
|
+
const pathname = usePathname()
|
|
23
25
|
const [isLoading, setIsLoading] = React.useState(true)
|
|
24
26
|
const [error, setError] = React.useState<string | null>(null)
|
|
25
27
|
const [isNotFound, setIsNotFound] = React.useState(false)
|
|
@@ -123,6 +125,20 @@ const handleSubmit = React.useCallback(async (values: LeaveRequestFormValues) =>
|
|
|
123
125
|
router.refresh()
|
|
124
126
|
}, [decisionComment, record?.id, record?.updatedAt, router, t])
|
|
125
127
|
|
|
128
|
+
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
129
|
+
// mount so the enterprise record_locks widget resolves `staff.leaveRequest` + id
|
|
130
|
+
// explicitly. The resourceKind follows the module's camelCase version-history
|
|
131
|
+
// convention so the held lock matches the leave request's save-time conflict surface.
|
|
132
|
+
useSetCurrentRecordInjectionContext(
|
|
133
|
+
buildRecordInjectionContext({
|
|
134
|
+
resourceKind: 'staff.leaveRequest',
|
|
135
|
+
resourceId: id || null,
|
|
136
|
+
updatedAt: record?.updatedAt ?? null,
|
|
137
|
+
data: record as Record<string, unknown> | null,
|
|
138
|
+
path: pathname,
|
|
139
|
+
}),
|
|
140
|
+
)
|
|
141
|
+
|
|
126
142
|
if (isLoading) {
|
|
127
143
|
return (
|
|
128
144
|
<Page>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import Link from 'next/link'
|
|
5
|
-
import { useRouter, useSearchParams } from 'next/navigation'
|
|
5
|
+
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
|
6
6
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
7
7
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
8
8
|
import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
@@ -15,6 +15,7 @@ import { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/tran
|
|
|
15
15
|
import { AvailabilityRulesEditor } from '@open-mercato/core/modules/planner/components/AvailabilityRulesEditor'
|
|
16
16
|
import { buildMemberScheduleItems } from '@open-mercato/core/modules/staff/lib/memberSchedule'
|
|
17
17
|
import { TeamMemberForm, buildTeamMemberPayload, type TeamMemberFormValues } from '@open-mercato/core/modules/staff/components/TeamMemberForm'
|
|
18
|
+
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
18
19
|
import { NotesSection } from '@open-mercato/ui/backend/detail'
|
|
19
20
|
import { ActivitiesSection, type SectionAction } from '@open-mercato/ui/backend/detail'
|
|
20
21
|
import { AddressesSection as SharedAddressesSection } from '@open-mercato/ui/backend/detail'
|
|
@@ -71,6 +72,7 @@ export default function StaffTeamMemberDetailPage({ params }: { params?: { id?:
|
|
|
71
72
|
const t = useT()
|
|
72
73
|
const detailTranslator = React.useMemo(() => createTranslatorWithFallback(t), [t])
|
|
73
74
|
const router = useRouter()
|
|
75
|
+
const pathname = usePathname()
|
|
74
76
|
const searchParams = useSearchParams()
|
|
75
77
|
const [initialValues, setInitialValues] = React.useState<TeamMemberFormValues | null>(null)
|
|
76
78
|
const [memberRecord, setMemberRecord] = React.useState<TeamMemberRecord | null>(null)
|
|
@@ -341,6 +343,20 @@ export default function StaffTeamMemberDetailPage({ params }: { params?: { id?:
|
|
|
341
343
|
: [t('staff.teamMembers.detail.roles.unassigned', 'No roles assigned')]
|
|
342
344
|
const userEmail = memberRecord?.user?.email ?? null
|
|
343
345
|
|
|
346
|
+
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
347
|
+
// mount so the enterprise record_locks widget resolves `staff.teamMember` + id
|
|
348
|
+
// explicitly. The resourceKind mirrors the TeamMemberForm `versionHistory` so the held
|
|
349
|
+
// lock matches the save-time conflict surface for the same team member.
|
|
350
|
+
useSetCurrentRecordInjectionContext(
|
|
351
|
+
buildRecordInjectionContext({
|
|
352
|
+
resourceKind: 'staff.teamMember',
|
|
353
|
+
resourceId: memberId || null,
|
|
354
|
+
updatedAt: initialValues?.updatedAt ?? null,
|
|
355
|
+
data: initialValues as Record<string, unknown> | null,
|
|
356
|
+
path: pathname,
|
|
357
|
+
}),
|
|
358
|
+
)
|
|
359
|
+
|
|
344
360
|
if (isNotFound) {
|
|
345
361
|
return (
|
|
346
362
|
<Page>
|