@open-mercato/core 0.8.1-develop.7228.1.b7ea09d2d3 → 0.8.1-develop.7237.1.635faf2c7a
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/generated/entities/availability_policy/index.js +41 -0
- package/dist/generated/entities/availability_policy/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +4 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +21 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/attachments/data/entities.js +3 -3
- package/dist/modules/attachments/data/entities.js.map +2 -2
- package/dist/modules/availability/acl.js +16 -0
- package/dist/modules/availability/acl.js.map +7 -0
- package/dist/modules/availability/api/check/route.js +95 -0
- package/dist/modules/availability/api/check/route.js.map +7 -0
- package/dist/modules/availability/api/openapi.js +27 -0
- package/dist/modules/availability/api/openapi.js.map +7 -0
- package/dist/modules/availability/api/policies/resolve-preview/route.js +58 -0
- package/dist/modules/availability/api/policies/resolve-preview/route.js.map +7 -0
- package/dist/modules/availability/api/policies/route.js +188 -0
- package/dist/modules/availability/api/policies/route.js.map +7 -0
- package/dist/modules/availability/backend/availability/check/page.js +185 -0
- package/dist/modules/availability/backend/availability/check/page.js.map +7 -0
- package/dist/modules/availability/backend/availability/check/page.meta.js +16 -0
- package/dist/modules/availability/backend/availability/check/page.meta.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/ResolutionPreviewPanel.js +79 -0
- package/dist/modules/availability/backend/availability/policies/ResolutionPreviewPanel.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/ScopeFields.js +53 -0
- package/dist/modules/availability/backend/availability/policies/ScopeFields.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/[id]/page.js +152 -0
- package/dist/modules/availability/backend/availability/policies/[id]/page.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/[id]/page.meta.js +12 -0
- package/dist/modules/availability/backend/availability/policies/[id]/page.meta.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/create/page.js +94 -0
- package/dist/modules/availability/backend/availability/policies/create/page.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/create/page.meta.js +12 -0
- package/dist/modules/availability/backend/availability/policies/create/page.meta.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/formGroups.js +75 -0
- package/dist/modules/availability/backend/availability/policies/formGroups.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/page.js +176 -0
- package/dist/modules/availability/backend/availability/policies/page.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/page.meta.js +16 -0
- package/dist/modules/availability/backend/availability/policies/page.meta.js.map +7 -0
- package/dist/modules/availability/commands/policies.js +312 -0
- package/dist/modules/availability/commands/policies.js.map +7 -0
- package/dist/modules/availability/commands/scope.js +19 -0
- package/dist/modules/availability/commands/scope.js.map +7 -0
- package/dist/modules/availability/data/entities.js +95 -0
- package/dist/modules/availability/data/entities.js.map +7 -0
- package/dist/modules/availability/data/validators.js +53 -0
- package/dist/modules/availability/data/validators.js.map +7 -0
- package/dist/modules/availability/di.js +41 -0
- package/dist/modules/availability/di.js.map +7 -0
- package/dist/modules/availability/events.js +18 -0
- package/dist/modules/availability/events.js.map +7 -0
- package/dist/modules/availability/index.js +15 -0
- package/dist/modules/availability/index.js.map +7 -0
- package/dist/modules/availability/lib/policyResolution.js +155 -0
- package/dist/modules/availability/lib/policyResolution.js.map +7 -0
- package/dist/modules/availability/lib/tryResolve.js +11 -0
- package/dist/modules/availability/lib/tryResolve.js.map +7 -0
- package/dist/modules/availability/migrations/Migration20260922095844_availability.js +16 -0
- package/dist/modules/availability/migrations/Migration20260922095844_availability.js.map +7 -0
- package/dist/modules/availability/setup.js +12 -0
- package/dist/modules/availability/setup.js.map +7 -0
- package/dist/modules/entities/cli.js +33 -12
- package/dist/modules/entities/cli.js.map +2 -2
- package/dist/modules/inbox_ops/api/settings/route.js +7 -32
- package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js.map +2 -2
- package/dist/modules/inbox_ops/components/proposals/ActionCard.js +1 -1
- package/dist/modules/inbox_ops/components/proposals/ActionCard.js.map +2 -2
- package/dist/modules/inbox_ops/lib/ensure-settings.js +47 -0
- package/dist/modules/inbox_ops/lib/ensure-settings.js.map +7 -0
- package/dist/modules/inbox_ops/lib/executionEngine.js.map +1 -1
- package/dist/modules/inbox_ops/lib/executionHelpers.js +0 -23
- package/dist/modules/inbox_ops/lib/executionHelpers.js.map +2 -2
- package/dist/modules/inbox_ops/lib/payloadEnrichment.js +0 -3
- package/dist/modules/inbox_ops/lib/payloadEnrichment.js.map +2 -2
- package/dist/modules/inbox_ops/setup.js +2 -21
- package/dist/modules/inbox_ops/setup.js.map +2 -2
- package/dist/modules/inbox_ops/subscribers/extractionWorker.js.map +1 -1
- package/dist/modules/sales/data/entities.js +3 -1
- package/dist/modules/sales/data/entities.js.map +2 -2
- package/dist/modules/sales/inbox-actions.js +0 -12
- package/dist/modules/sales/inbox-actions.js.map +2 -2
- package/dist/modules/sales/migrations/Migration20260916120000_sales_payment_allocation_order_idx.js +20 -0
- package/dist/modules/sales/migrations/Migration20260916120000_sales_payment_allocation_order_idx.js.map +7 -0
- package/dist/modules/wms/di.js +3 -0
- package/dist/modules/wms/di.js.map +2 -2
- package/dist/modules/wms/lib/availabilityCache.js +85 -0
- package/dist/modules/wms/lib/availabilityCache.js.map +7 -0
- package/dist/modules/wms/lib/availabilityCalculation.js +178 -0
- package/dist/modules/wms/lib/availabilityCalculation.js.map +7 -0
- package/dist/modules/wms/lib/availabilityProvider.js +14 -0
- package/dist/modules/wms/lib/availabilityProvider.js.map +7 -0
- package/dist/modules/wms/lib/sqlInClause.js +9 -0
- package/dist/modules/wms/lib/sqlInClause.js.map +7 -0
- package/dist/modules/wms/lib/tryResolve.js +11 -0
- package/dist/modules/wms/lib/tryResolve.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-availability-cache.js +16 -0
- package/dist/modules/wms/subscribers/invalidate-availability-cache.js.map +7 -0
- package/generated/entities/availability_policy/index.ts +19 -0
- package/generated/entities.ids.generated.ts +4 -0
- package/generated/entity-fields-registry.ts +21 -0
- package/package.json +7 -7
- package/src/modules/attachments/data/entities.ts +5 -3
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/availability/acl.ts +12 -0
- package/src/modules/availability/api/check/route.ts +114 -0
- package/src/modules/availability/api/openapi.ts +28 -0
- package/src/modules/availability/api/policies/resolve-preview/route.ts +67 -0
- package/src/modules/availability/api/policies/route.ts +224 -0
- package/src/modules/availability/backend/availability/check/page.meta.ts +12 -0
- package/src/modules/availability/backend/availability/check/page.tsx +223 -0
- package/src/modules/availability/backend/availability/policies/ResolutionPreviewPanel.tsx +106 -0
- package/src/modules/availability/backend/availability/policies/ScopeFields.tsx +55 -0
- package/src/modules/availability/backend/availability/policies/[id]/page.meta.ts +8 -0
- package/src/modules/availability/backend/availability/policies/[id]/page.tsx +182 -0
- package/src/modules/availability/backend/availability/policies/create/page.meta.ts +8 -0
- package/src/modules/availability/backend/availability/policies/create/page.tsx +105 -0
- package/src/modules/availability/backend/availability/policies/formGroups.ts +77 -0
- package/src/modules/availability/backend/availability/policies/page.meta.ts +12 -0
- package/src/modules/availability/backend/availability/policies/page.tsx +196 -0
- package/src/modules/availability/commands/policies.ts +358 -0
- package/src/modules/availability/commands/scope.ts +26 -0
- package/src/modules/availability/data/entities.ts +92 -0
- package/src/modules/availability/data/validators.ts +70 -0
- package/src/modules/availability/di.ts +54 -0
- package/src/modules/availability/events.ts +17 -0
- package/src/modules/availability/i18n/de.json +81 -0
- package/src/modules/availability/i18n/en.json +81 -0
- package/src/modules/availability/i18n/es.json +81 -0
- package/src/modules/availability/i18n/ko.json +81 -0
- package/src/modules/availability/i18n/pl.json +81 -0
- package/src/modules/availability/index.ts +13 -0
- package/src/modules/availability/lib/policyResolution.ts +264 -0
- package/src/modules/availability/lib/tryResolve.ts +8 -0
- package/src/modules/availability/migrations/.snapshot-open-mercato.json +375 -0
- package/src/modules/availability/migrations/Migration20260922095844_availability.ts +13 -0
- package/src/modules/availability/setup.ts +10 -0
- package/src/modules/entities/cli.ts +53 -13
- package/src/modules/inbox_ops/api/settings/route.ts +9 -32
- package/src/modules/inbox_ops/backend/inbox-ops/settings/page.tsx +2 -2
- package/src/modules/inbox_ops/components/proposals/ActionCard.tsx +1 -1
- package/src/modules/inbox_ops/i18n/de.json +1 -1
- package/src/modules/inbox_ops/i18n/en.json +1 -1
- package/src/modules/inbox_ops/i18n/es.json +1 -1
- package/src/modules/inbox_ops/i18n/ko.json +1 -1
- package/src/modules/inbox_ops/i18n/pl.json +1 -1
- package/src/modules/inbox_ops/lib/ensure-settings.ts +77 -0
- package/src/modules/inbox_ops/lib/executionEngine.ts +1 -1
- package/src/modules/inbox_ops/lib/executionHelpers.ts +0 -27
- package/src/modules/inbox_ops/lib/payloadEnrichment.ts +1 -5
- package/src/modules/inbox_ops/setup.ts +2 -21
- package/src/modules/inbox_ops/subscribers/extractionWorker.ts +1 -1
- package/src/modules/sales/data/entities.ts +2 -0
- package/src/modules/sales/inbox-actions.ts +0 -20
- package/src/modules/sales/migrations/.snapshot-open-mercato.json +20 -0
- package/src/modules/sales/migrations/Migration20260916120000_sales_payment_allocation_order_idx.ts +74 -0
- package/src/modules/wms/di.ts +6 -0
- package/src/modules/wms/lib/availabilityCache.ts +136 -0
- package/src/modules/wms/lib/availabilityCalculation.ts +289 -0
- package/src/modules/wms/lib/availabilityProvider.ts +29 -0
- package/src/modules/wms/lib/sqlInClause.ts +21 -0
- package/src/modules/wms/lib/tryResolve.ts +8 -0
- package/src/modules/wms/subscribers/invalidate-availability-cache.ts +30 -0
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
buildSourceMetadata,
|
|
7
7
|
resolveOrderByReference,
|
|
8
8
|
resolveFirstChannelId,
|
|
9
|
-
resolveChannelCurrency,
|
|
10
9
|
resolveEffectiveDocumentKind,
|
|
11
10
|
resolveShipmentStatusEntryId,
|
|
12
11
|
resolveCustomerEntityIdByEmail,
|
|
@@ -215,15 +214,6 @@ async function executeUpdateShipmentAction(action, ctx) {
|
|
|
215
214
|
);
|
|
216
215
|
return { createdEntityId: shipment.id, createdEntityType: "sales_shipment" };
|
|
217
216
|
}
|
|
218
|
-
async function normalizeOrderPayload(payload, ctx) {
|
|
219
|
-
if (!payload.currencyCode) {
|
|
220
|
-
const hCtx = asHelperContext(ctx);
|
|
221
|
-
const channelId = typeof payload.channelId === "string" ? payload.channelId : null;
|
|
222
|
-
const resolved = await resolveChannelCurrency(hCtx, channelId);
|
|
223
|
-
if (resolved) payload.currencyCode = resolved;
|
|
224
|
-
}
|
|
225
|
-
return payload;
|
|
226
|
-
}
|
|
227
217
|
const inboxActions = [
|
|
228
218
|
{
|
|
229
219
|
type: "create_order",
|
|
@@ -238,7 +228,6 @@ const inboxActions = [
|
|
|
238
228
|
'For create_order / create_quote: each line item MUST have "productName" (the product name goes here, NOT in "description"). Include currencyCode and customerName.',
|
|
239
229
|
"For create_order / create_quote: extract shippingAddress and billingAddress as structured objects when addresses are mentioned. Parse street, city, postal code, country from the text. Do NOT put address data in notes."
|
|
240
230
|
],
|
|
241
|
-
normalizePayload: normalizeOrderPayload,
|
|
242
231
|
execute: (action, ctx) => executeCreateDocumentAction(action, ctx, "order")
|
|
243
232
|
},
|
|
244
233
|
{
|
|
@@ -247,7 +236,6 @@ const inboxActions = [
|
|
|
247
236
|
payloadSchema: orderPayloadSchema,
|
|
248
237
|
label: "Create Quote",
|
|
249
238
|
promptSchema: "(shared with create_order)",
|
|
250
|
-
normalizePayload: normalizeOrderPayload,
|
|
251
239
|
execute: (action, ctx) => executeCreateDocumentAction(action, ctx, "quote")
|
|
252
240
|
},
|
|
253
241
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/sales/inbox-actions.ts"],
|
|
4
|
-
"sourcesContent": ["import type { InboxActionDefinition, InboxActionExecutionContext } from '@open-mercato/shared/modules/inbox-actions'\nimport { orderPayloadSchema, updateOrderPayloadSchema, updateShipmentPayloadSchema } from '../inbox_ops/data/validators'\nimport type { OrderPayload, UpdateOrderPayload, UpdateShipmentPayload } from '../inbox_ops/data/validators'\nimport {\n asHelperContext,\n ExecutionError,\n executeCommand,\n buildSourceMetadata,\n resolveOrderByReference,\n resolveFirstChannelId,\n resolveChannelCurrency,\n resolveEffectiveDocumentKind,\n resolveShipmentStatusEntryId,\n resolveCustomerEntityIdByEmail,\n resolveEntityClass,\n normalizeAddressSnapshot,\n parseDateToken,\n parseNumberToken,\n loadOrderLineItems,\n matchLineItemByName,\n} from '../inbox_ops/lib/executionHelpers'\n\n// ---------------------------------------------------------------------------\n// create_order\n// ---------------------------------------------------------------------------\n\nasync function executeCreateDocumentAction(\n action: { id: string; proposalId: string; payload: unknown },\n ctx: InboxActionExecutionContext,\n kind: 'order' | 'quote',\n): Promise<{ createdEntityId?: string | null; createdEntityType?: string | null }> {\n const hCtx = asHelperContext(ctx)\n const payload = action.payload as OrderPayload\n\n let resolvedChannelId: string | undefined = payload.channelId\n if (!resolvedChannelId) {\n resolvedChannelId = (await resolveFirstChannelId(hCtx)) ?? undefined\n if (!resolvedChannelId) {\n throw new ExecutionError('No sales channel available. Create a channel first or set channelId in the payload.', 400)\n }\n }\n\n const currencyCode = payload.currencyCode.trim().toUpperCase()\n const lines = payload.lineItems.map((line, index) => {\n const quantity = parseNumberToken(line.quantity, `lineItems[${index}].quantity`)\n const unitPrice = line.unitPrice\n ? parseNumberToken(line.unitPrice, `lineItems[${index}].unitPrice`)\n : undefined\n\n const mappedLine: Record<string, unknown> = {\n lineNumber: index + 1,\n kind: line.kind ?? (line.productId ? 'product' : 'service'),\n name: line.productName,\n description: line.description,\n quantity,\n currencyCode,\n }\n\n if (line.productId) mappedLine.productId = line.productId\n if (line.variantId) mappedLine.productVariantId = line.variantId\n if (unitPrice !== undefined) mappedLine.unitPriceNet = unitPrice\n if (line.sku || line.catalogPrice) {\n mappedLine.catalogSnapshot = {\n sku: line.sku ?? null,\n catalogPrice: line.catalogPrice ?? null,\n }\n }\n\n return mappedLine\n })\n\n const metadata = buildSourceMetadata(action.id, action.proposalId)\n\n let resolvedCustomerEntityId = payload.customerEntityId\n if (!resolvedCustomerEntityId && payload.customerEmail) {\n resolvedCustomerEntityId = (await resolveCustomerEntityIdByEmail(hCtx, payload.customerEmail)) ?? undefined\n }\n\n const createInput: Record<string, unknown> = {\n organizationId: hCtx.organizationId,\n tenantId: hCtx.tenantId,\n customerEntityId: resolvedCustomerEntityId,\n customerReference: payload.customerReference,\n channelId: resolvedChannelId,\n currencyCode,\n taxRateId: payload.taxRateId,\n comments: payload.notes,\n metadata,\n lines,\n }\n\n if (!resolvedCustomerEntityId) {\n createInput.customerSnapshot = {\n displayName: payload.customerName,\n ...(payload.customerEmail && { primaryEmail: payload.customerEmail }),\n }\n }\n\n const normalizedBilling = payload.billingAddress\n ? normalizeAddressSnapshot(payload.billingAddress)\n : undefined\n const normalizedShipping = payload.shippingAddress\n ? normalizeAddressSnapshot(payload.shippingAddress)\n : undefined\n\n if (normalizedShipping || normalizedBilling) {\n createInput.shippingAddressSnapshot = normalizedShipping ?? normalizedBilling\n createInput.billingAddressSnapshot = normalizedBilling ?? normalizedShipping\n } else if (payload.billingAddressId || payload.shippingAddressId) {\n createInput.billingAddressId = payload.billingAddressId ?? payload.shippingAddressId\n createInput.shippingAddressId = payload.shippingAddressId ?? payload.billingAddressId\n }\n\n const requestedDeliveryAt = parseDateToken(payload.requestedDeliveryDate ?? undefined)\n if (requestedDeliveryAt) {\n createInput.expectedDeliveryAt = requestedDeliveryAt\n }\n\n const effectiveKind = kind === 'order'\n ? await resolveEffectiveDocumentKind(hCtx, resolvedChannelId)\n : kind\n\n if (effectiveKind === 'order') {\n const result = await executeCommand<Record<string, unknown>, { orderId?: string }>(\n hCtx,\n 'sales.orders.create',\n createInput,\n )\n if (!result.orderId) {\n throw new ExecutionError('Order creation did not return an order ID', 500)\n }\n return { createdEntityId: result.orderId, createdEntityType: 'sales_order' }\n }\n\n const result = await executeCommand<Record<string, unknown>, { quoteId?: string }>(\n hCtx,\n 'sales.quotes.create',\n createInput,\n )\n if (!result.quoteId) {\n throw new ExecutionError('Quote creation did not return a quote ID', 500)\n }\n return { createdEntityId: result.quoteId, createdEntityType: 'sales_quote' }\n}\n\n// ---------------------------------------------------------------------------\n// update_order\n// ---------------------------------------------------------------------------\n\nasync function executeUpdateOrderAction(\n action: { id: string; proposalId: string; payload: unknown },\n ctx: InboxActionExecutionContext,\n): Promise<{ createdEntityId?: string | null; createdEntityType?: string | null }> {\n const hCtx = asHelperContext(ctx)\n const payload = action.payload as UpdateOrderPayload\n\n const order = await resolveOrderByReference(hCtx, payload.orderId, payload.orderNumber)\n\n const updateInput: Record<string, unknown> = {\n id: order.id,\n organizationId: hCtx.organizationId,\n tenantId: hCtx.tenantId,\n }\n\n const newDeliveryDate = parseDateToken(payload.deliveryDateChange?.newDate)\n if (newDeliveryDate) {\n updateInput.expectedDeliveryAt = newDeliveryDate\n }\n\n const noteLines = payload.noteAdditions?.map((note) => note.trim()).filter((note) => note.length > 0) ?? []\n if (noteLines.length > 0) {\n const mergedNotes = [order.comments ?? null, ...noteLines].filter(Boolean).join('\\n')\n updateInput.comments = mergedNotes\n }\n\n if (Object.keys(updateInput).length > 3) {\n await executeCommand<Record<string, unknown>, { orderId?: string }>(\n hCtx,\n 'sales.orders.update',\n updateInput,\n )\n }\n\n const quantityChanges = payload.quantityChanges ?? []\n const orderLines = quantityChanges.length > 0 && quantityChanges.some((qc) => !qc.lineItemId)\n ? await loadOrderLineItems(hCtx, order.id)\n : []\n\n for (const quantityChange of quantityChanges) {\n let lineItemId = quantityChange.lineItemId\n if (!lineItemId) {\n const matched = matchLineItemByName(orderLines, quantityChange.lineItemName)\n if (matched) {\n lineItemId = matched\n } else {\n const availableNames = orderLines.map((l) => l.name).filter(Boolean).join(', ')\n throw new ExecutionError(\n `Cannot resolve line item \"${quantityChange.lineItemName}\". Available line items: ${availableNames || 'none'}`,\n 400,\n )\n }\n }\n\n await executeCommand<{ body: Record<string, unknown> }, { orderId?: string; lineId?: string }>(\n hCtx,\n 'sales.orders.lines.upsert',\n {\n body: {\n id: lineItemId,\n orderId: order.id,\n organizationId: hCtx.organizationId,\n tenantId: hCtx.tenantId,\n quantity: parseNumberToken(quantityChange.newQuantity, 'quantityChanges.newQuantity'),\n currencyCode: order.currencyCode,\n },\n },\n )\n }\n\n return { createdEntityId: order.id, createdEntityType: 'sales_order' }\n}\n\n// ---------------------------------------------------------------------------\n// update_shipment\n// ---------------------------------------------------------------------------\n\nasync function executeUpdateShipmentAction(\n action: { id: string; proposalId: string; payload: unknown },\n ctx: InboxActionExecutionContext,\n): Promise<{ createdEntityId?: string | null; createdEntityType?: string | null }> {\n const hCtx = asHelperContext(ctx)\n const payload = action.payload as UpdateShipmentPayload\n\n const order = await resolveOrderByReference(hCtx, payload.orderId, payload.orderNumber)\n\n const SalesShipmentClass = resolveEntityClass(hCtx, 'SalesShipment')\n if (!SalesShipmentClass) {\n throw new ExecutionError('Sales module entities not available', 503)\n }\n\n const { findOneWithDecryption } = await import('@open-mercato/shared/lib/encryption/find')\n\n const shipment = await findOneWithDecryption(\n hCtx.em,\n SalesShipmentClass,\n {\n order: order.id,\n tenantId: hCtx.tenantId,\n organizationId: hCtx.organizationId,\n deletedAt: null,\n },\n { orderBy: { createdAt: 'DESC' } },\n { tenantId: hCtx.tenantId, organizationId: hCtx.organizationId },\n )\n\n if (!shipment) {\n throw new ExecutionError('No shipment found for the referenced order', 404)\n }\n\n const statusEntryId = await resolveShipmentStatusEntryId(hCtx, payload.statusLabel)\n if (!statusEntryId) {\n throw new ExecutionError(`Shipment status \"${payload.statusLabel}\" not found`, 400)\n }\n\n const updateInput: Record<string, unknown> = {\n id: shipment.id,\n orderId: order.id,\n organizationId: hCtx.organizationId,\n tenantId: hCtx.tenantId,\n statusEntryId,\n }\n\n if (payload.trackingNumbers) updateInput.trackingNumbers = payload.trackingNumbers\n if (payload.carrierName) updateInput.carrierName = payload.carrierName\n if (payload.notes) updateInput.notes = payload.notes\n\n const shippedAt = parseDateToken(payload.shippedAt)\n const deliveredAt = parseDateToken(payload.deliveredAt)\n if (shippedAt) updateInput.shippedAt = shippedAt\n if (deliveredAt) updateInput.deliveredAt = deliveredAt\n\n await executeCommand<Record<string, unknown>, { shipmentId?: string }>(\n hCtx,\n 'sales.shipments.update',\n updateInput,\n )\n\n return { createdEntityId: shipment.id, createdEntityType: 'sales_shipment' }\n}\n\n// ---------------------------------------------------------------------------\n// Normalization helper for order/quote payloads\n// ---------------------------------------------------------------------------\n\nasync function normalizeOrderPayload(\n payload: Record<string, unknown>,\n ctx: InboxActionExecutionContext,\n): Promise<Record<string, unknown>> {\n if (!payload.currencyCode) {\n const hCtx = asHelperContext(ctx)\n const channelId = typeof payload.channelId === 'string' ? payload.channelId : null\n const resolved = await resolveChannelCurrency(hCtx, channelId)\n if (resolved) payload.currencyCode = resolved\n }\n return payload\n}\n\n// ---------------------------------------------------------------------------\n// Exported action definitions\n// ---------------------------------------------------------------------------\n\nexport const inboxActions: InboxActionDefinition[] = [\n {\n type: 'create_order',\n requiredFeature: 'sales.orders.manage',\n payloadSchema: orderPayloadSchema,\n label: 'Create Sales Order',\n promptSchema: `create_order / create_quote payload:\n{ customerName: string, customerEmail?: string, customerEntityId?: uuid, channelId?: uuid, currencyCode: string (3-letter ISO), taxRateId?: uuid, lineItems: [{ productName: string (REQUIRED), productId?: uuid, variantId?: uuid, sku?: string, quantity: string, unitPrice?: string, kind?: \"product\"|\"service\", description?: string }], requestedDeliveryDate?: string, notes?: string, customerReference?: string (customer's own PO number or reference code \u2014 only set if explicitly stated in the email, do NOT use the email subject), shippingAddress?: { line1?: string, line2?: string, city?: string, state?: string, postalCode?: string, country?: string, company?: string, contactName?: string }, billingAddress?: { line1?: string, line2?: string, city?: string, state?: string, postalCode?: string, country?: string, company?: string, contactName?: string } }`,\n promptRules: [\n 'ALWAYS propose a create_order or create_quote action when the customer expresses interest in buying, even if some product names are uncertain or not in the catalog. Use the best product name available; the system will flag unmatched products as discrepancies. Do NOT replace an order with a draft_reply asking for clarification \u2014 propose both if needed.',\n 'Use create_order when the customer has clearly confirmed they want to proceed (e.g., \"let\\'s go ahead\", \"please process\", \"confirmed\"). Use create_quote when the customer is still inquiring, requesting pricing, asking for a proposal, or negotiating (e.g., \"could you send a quote\", \"what would it cost\", \"we\\'re interested in\", \"can you offer\"). When in doubt, prefer create_quote.',\n 'For create_order / create_quote: each line item MUST have \"productName\" (the product name goes here, NOT in \"description\"). Include currencyCode and customerName.',\n 'For create_order / create_quote: extract shippingAddress and billingAddress as structured objects when addresses are mentioned. Parse street, city, postal code, country from the text. Do NOT put address data in notes.',\n ],\n normalizePayload: normalizeOrderPayload,\n execute: (action, ctx) => executeCreateDocumentAction(action, ctx, 'order'),\n },\n {\n type: 'create_quote',\n requiredFeature: 'sales.quotes.manage',\n payloadSchema: orderPayloadSchema,\n label: 'Create Quote',\n promptSchema: '(shared with create_order)',\n normalizePayload: normalizeOrderPayload,\n execute: (action, ctx) => executeCreateDocumentAction(action, ctx, 'quote'),\n },\n {\n type: 'update_order',\n requiredFeature: 'sales.orders.manage',\n payloadSchema: updateOrderPayloadSchema,\n label: 'Update Order',\n promptSchema: `update_order payload:\n{ orderId?: uuid, orderNumber?: string, quantityChanges?: [{ lineItemName: string, lineItemId?: uuid, oldQuantity?: string, newQuantity: string }], deliveryDateChange?: { oldDate?: string, newDate: string }, noteAdditions?: string[] }`,\n execute: executeUpdateOrderAction,\n },\n {\n type: 'update_shipment',\n requiredFeature: 'sales.shipments.manage',\n payloadSchema: updateShipmentPayloadSchema,\n label: 'Update Shipment',\n promptSchema: `update_shipment payload:\n{ orderId?: uuid, orderNumber?: string, trackingNumbers?: string[], carrierName?: string, statusLabel: string, shippedAt?: string, deliveredAt?: string, estimatedDelivery?: string, notes?: string }`,\n promptRules: ['For update_shipment: use statusLabel text only.'],\n execute: executeUpdateShipmentAction,\n },\n]\n\nexport default inboxActions\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,oBAAoB,0BAA0B,mCAAmC;AAE1F;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,
|
|
4
|
+
"sourcesContent": ["import type { InboxActionDefinition, InboxActionExecutionContext } from '@open-mercato/shared/modules/inbox-actions'\nimport { orderPayloadSchema, updateOrderPayloadSchema, updateShipmentPayloadSchema } from '../inbox_ops/data/validators'\nimport type { OrderPayload, UpdateOrderPayload, UpdateShipmentPayload } from '../inbox_ops/data/validators'\nimport {\n asHelperContext,\n ExecutionError,\n executeCommand,\n buildSourceMetadata,\n resolveOrderByReference,\n resolveFirstChannelId,\n resolveEffectiveDocumentKind,\n resolveShipmentStatusEntryId,\n resolveCustomerEntityIdByEmail,\n resolveEntityClass,\n normalizeAddressSnapshot,\n parseDateToken,\n parseNumberToken,\n loadOrderLineItems,\n matchLineItemByName,\n} from '../inbox_ops/lib/executionHelpers'\n\n// ---------------------------------------------------------------------------\n// create_order\n// ---------------------------------------------------------------------------\n\nasync function executeCreateDocumentAction(\n action: { id: string; proposalId: string; payload: unknown },\n ctx: InboxActionExecutionContext,\n kind: 'order' | 'quote',\n): Promise<{ createdEntityId?: string | null; createdEntityType?: string | null }> {\n const hCtx = asHelperContext(ctx)\n const payload = action.payload as OrderPayload\n\n let resolvedChannelId: string | undefined = payload.channelId\n if (!resolvedChannelId) {\n resolvedChannelId = (await resolveFirstChannelId(hCtx)) ?? undefined\n if (!resolvedChannelId) {\n throw new ExecutionError('No sales channel available. Create a channel first or set channelId in the payload.', 400)\n }\n }\n\n const currencyCode = payload.currencyCode.trim().toUpperCase()\n const lines = payload.lineItems.map((line, index) => {\n const quantity = parseNumberToken(line.quantity, `lineItems[${index}].quantity`)\n const unitPrice = line.unitPrice\n ? parseNumberToken(line.unitPrice, `lineItems[${index}].unitPrice`)\n : undefined\n\n const mappedLine: Record<string, unknown> = {\n lineNumber: index + 1,\n kind: line.kind ?? (line.productId ? 'product' : 'service'),\n name: line.productName,\n description: line.description,\n quantity,\n currencyCode,\n }\n\n if (line.productId) mappedLine.productId = line.productId\n if (line.variantId) mappedLine.productVariantId = line.variantId\n if (unitPrice !== undefined) mappedLine.unitPriceNet = unitPrice\n if (line.sku || line.catalogPrice) {\n mappedLine.catalogSnapshot = {\n sku: line.sku ?? null,\n catalogPrice: line.catalogPrice ?? null,\n }\n }\n\n return mappedLine\n })\n\n const metadata = buildSourceMetadata(action.id, action.proposalId)\n\n let resolvedCustomerEntityId = payload.customerEntityId\n if (!resolvedCustomerEntityId && payload.customerEmail) {\n resolvedCustomerEntityId = (await resolveCustomerEntityIdByEmail(hCtx, payload.customerEmail)) ?? undefined\n }\n\n const createInput: Record<string, unknown> = {\n organizationId: hCtx.organizationId,\n tenantId: hCtx.tenantId,\n customerEntityId: resolvedCustomerEntityId,\n customerReference: payload.customerReference,\n channelId: resolvedChannelId,\n currencyCode,\n taxRateId: payload.taxRateId,\n comments: payload.notes,\n metadata,\n lines,\n }\n\n if (!resolvedCustomerEntityId) {\n createInput.customerSnapshot = {\n displayName: payload.customerName,\n ...(payload.customerEmail && { primaryEmail: payload.customerEmail }),\n }\n }\n\n const normalizedBilling = payload.billingAddress\n ? normalizeAddressSnapshot(payload.billingAddress)\n : undefined\n const normalizedShipping = payload.shippingAddress\n ? normalizeAddressSnapshot(payload.shippingAddress)\n : undefined\n\n if (normalizedShipping || normalizedBilling) {\n createInput.shippingAddressSnapshot = normalizedShipping ?? normalizedBilling\n createInput.billingAddressSnapshot = normalizedBilling ?? normalizedShipping\n } else if (payload.billingAddressId || payload.shippingAddressId) {\n createInput.billingAddressId = payload.billingAddressId ?? payload.shippingAddressId\n createInput.shippingAddressId = payload.shippingAddressId ?? payload.billingAddressId\n }\n\n const requestedDeliveryAt = parseDateToken(payload.requestedDeliveryDate ?? undefined)\n if (requestedDeliveryAt) {\n createInput.expectedDeliveryAt = requestedDeliveryAt\n }\n\n const effectiveKind = kind === 'order'\n ? await resolveEffectiveDocumentKind(hCtx, resolvedChannelId)\n : kind\n\n if (effectiveKind === 'order') {\n const result = await executeCommand<Record<string, unknown>, { orderId?: string }>(\n hCtx,\n 'sales.orders.create',\n createInput,\n )\n if (!result.orderId) {\n throw new ExecutionError('Order creation did not return an order ID', 500)\n }\n return { createdEntityId: result.orderId, createdEntityType: 'sales_order' }\n }\n\n const result = await executeCommand<Record<string, unknown>, { quoteId?: string }>(\n hCtx,\n 'sales.quotes.create',\n createInput,\n )\n if (!result.quoteId) {\n throw new ExecutionError('Quote creation did not return a quote ID', 500)\n }\n return { createdEntityId: result.quoteId, createdEntityType: 'sales_quote' }\n}\n\n// ---------------------------------------------------------------------------\n// update_order\n// ---------------------------------------------------------------------------\n\nasync function executeUpdateOrderAction(\n action: { id: string; proposalId: string; payload: unknown },\n ctx: InboxActionExecutionContext,\n): Promise<{ createdEntityId?: string | null; createdEntityType?: string | null }> {\n const hCtx = asHelperContext(ctx)\n const payload = action.payload as UpdateOrderPayload\n\n const order = await resolveOrderByReference(hCtx, payload.orderId, payload.orderNumber)\n\n const updateInput: Record<string, unknown> = {\n id: order.id,\n organizationId: hCtx.organizationId,\n tenantId: hCtx.tenantId,\n }\n\n const newDeliveryDate = parseDateToken(payload.deliveryDateChange?.newDate)\n if (newDeliveryDate) {\n updateInput.expectedDeliveryAt = newDeliveryDate\n }\n\n const noteLines = payload.noteAdditions?.map((note) => note.trim()).filter((note) => note.length > 0) ?? []\n if (noteLines.length > 0) {\n const mergedNotes = [order.comments ?? null, ...noteLines].filter(Boolean).join('\\n')\n updateInput.comments = mergedNotes\n }\n\n if (Object.keys(updateInput).length > 3) {\n await executeCommand<Record<string, unknown>, { orderId?: string }>(\n hCtx,\n 'sales.orders.update',\n updateInput,\n )\n }\n\n const quantityChanges = payload.quantityChanges ?? []\n const orderLines = quantityChanges.length > 0 && quantityChanges.some((qc) => !qc.lineItemId)\n ? await loadOrderLineItems(hCtx, order.id)\n : []\n\n for (const quantityChange of quantityChanges) {\n let lineItemId = quantityChange.lineItemId\n if (!lineItemId) {\n const matched = matchLineItemByName(orderLines, quantityChange.lineItemName)\n if (matched) {\n lineItemId = matched\n } else {\n const availableNames = orderLines.map((l) => l.name).filter(Boolean).join(', ')\n throw new ExecutionError(\n `Cannot resolve line item \"${quantityChange.lineItemName}\". Available line items: ${availableNames || 'none'}`,\n 400,\n )\n }\n }\n\n await executeCommand<{ body: Record<string, unknown> }, { orderId?: string; lineId?: string }>(\n hCtx,\n 'sales.orders.lines.upsert',\n {\n body: {\n id: lineItemId,\n orderId: order.id,\n organizationId: hCtx.organizationId,\n tenantId: hCtx.tenantId,\n quantity: parseNumberToken(quantityChange.newQuantity, 'quantityChanges.newQuantity'),\n currencyCode: order.currencyCode,\n },\n },\n )\n }\n\n return { createdEntityId: order.id, createdEntityType: 'sales_order' }\n}\n\n// ---------------------------------------------------------------------------\n// update_shipment\n// ---------------------------------------------------------------------------\n\nasync function executeUpdateShipmentAction(\n action: { id: string; proposalId: string; payload: unknown },\n ctx: InboxActionExecutionContext,\n): Promise<{ createdEntityId?: string | null; createdEntityType?: string | null }> {\n const hCtx = asHelperContext(ctx)\n const payload = action.payload as UpdateShipmentPayload\n\n const order = await resolveOrderByReference(hCtx, payload.orderId, payload.orderNumber)\n\n const SalesShipmentClass = resolveEntityClass(hCtx, 'SalesShipment')\n if (!SalesShipmentClass) {\n throw new ExecutionError('Sales module entities not available', 503)\n }\n\n const { findOneWithDecryption } = await import('@open-mercato/shared/lib/encryption/find')\n\n const shipment = await findOneWithDecryption(\n hCtx.em,\n SalesShipmentClass,\n {\n order: order.id,\n tenantId: hCtx.tenantId,\n organizationId: hCtx.organizationId,\n deletedAt: null,\n },\n { orderBy: { createdAt: 'DESC' } },\n { tenantId: hCtx.tenantId, organizationId: hCtx.organizationId },\n )\n\n if (!shipment) {\n throw new ExecutionError('No shipment found for the referenced order', 404)\n }\n\n const statusEntryId = await resolveShipmentStatusEntryId(hCtx, payload.statusLabel)\n if (!statusEntryId) {\n throw new ExecutionError(`Shipment status \"${payload.statusLabel}\" not found`, 400)\n }\n\n const updateInput: Record<string, unknown> = {\n id: shipment.id,\n orderId: order.id,\n organizationId: hCtx.organizationId,\n tenantId: hCtx.tenantId,\n statusEntryId,\n }\n\n if (payload.trackingNumbers) updateInput.trackingNumbers = payload.trackingNumbers\n if (payload.carrierName) updateInput.carrierName = payload.carrierName\n if (payload.notes) updateInput.notes = payload.notes\n\n const shippedAt = parseDateToken(payload.shippedAt)\n const deliveredAt = parseDateToken(payload.deliveredAt)\n if (shippedAt) updateInput.shippedAt = shippedAt\n if (deliveredAt) updateInput.deliveredAt = deliveredAt\n\n await executeCommand<Record<string, unknown>, { shipmentId?: string }>(\n hCtx,\n 'sales.shipments.update',\n updateInput,\n )\n\n return { createdEntityId: shipment.id, createdEntityType: 'sales_shipment' }\n}\n\n// ---------------------------------------------------------------------------\n// Exported action definitions\n// ---------------------------------------------------------------------------\n\nexport const inboxActions: InboxActionDefinition[] = [\n {\n type: 'create_order',\n requiredFeature: 'sales.orders.manage',\n payloadSchema: orderPayloadSchema,\n label: 'Create Sales Order',\n promptSchema: `create_order / create_quote payload:\n{ customerName: string, customerEmail?: string, customerEntityId?: uuid, channelId?: uuid, currencyCode: string (3-letter ISO), taxRateId?: uuid, lineItems: [{ productName: string (REQUIRED), productId?: uuid, variantId?: uuid, sku?: string, quantity: string, unitPrice?: string, kind?: \"product\"|\"service\", description?: string }], requestedDeliveryDate?: string, notes?: string, customerReference?: string (customer's own PO number or reference code \u2014 only set if explicitly stated in the email, do NOT use the email subject), shippingAddress?: { line1?: string, line2?: string, city?: string, state?: string, postalCode?: string, country?: string, company?: string, contactName?: string }, billingAddress?: { line1?: string, line2?: string, city?: string, state?: string, postalCode?: string, country?: string, company?: string, contactName?: string } }`,\n promptRules: [\n 'ALWAYS propose a create_order or create_quote action when the customer expresses interest in buying, even if some product names are uncertain or not in the catalog. Use the best product name available; the system will flag unmatched products as discrepancies. Do NOT replace an order with a draft_reply asking for clarification \u2014 propose both if needed.',\n 'Use create_order when the customer has clearly confirmed they want to proceed (e.g., \"let\\'s go ahead\", \"please process\", \"confirmed\"). Use create_quote when the customer is still inquiring, requesting pricing, asking for a proposal, or negotiating (e.g., \"could you send a quote\", \"what would it cost\", \"we\\'re interested in\", \"can you offer\"). When in doubt, prefer create_quote.',\n 'For create_order / create_quote: each line item MUST have \"productName\" (the product name goes here, NOT in \"description\"). Include currencyCode and customerName.',\n 'For create_order / create_quote: extract shippingAddress and billingAddress as structured objects when addresses are mentioned. Parse street, city, postal code, country from the text. Do NOT put address data in notes.',\n ],\n execute: (action, ctx) => executeCreateDocumentAction(action, ctx, 'order'),\n },\n {\n type: 'create_quote',\n requiredFeature: 'sales.quotes.manage',\n payloadSchema: orderPayloadSchema,\n label: 'Create Quote',\n promptSchema: '(shared with create_order)',\n execute: (action, ctx) => executeCreateDocumentAction(action, ctx, 'quote'),\n },\n {\n type: 'update_order',\n requiredFeature: 'sales.orders.manage',\n payloadSchema: updateOrderPayloadSchema,\n label: 'Update Order',\n promptSchema: `update_order payload:\n{ orderId?: uuid, orderNumber?: string, quantityChanges?: [{ lineItemName: string, lineItemId?: uuid, oldQuantity?: string, newQuantity: string }], deliveryDateChange?: { oldDate?: string, newDate: string }, noteAdditions?: string[] }`,\n execute: executeUpdateOrderAction,\n },\n {\n type: 'update_shipment',\n requiredFeature: 'sales.shipments.manage',\n payloadSchema: updateShipmentPayloadSchema,\n label: 'Update Shipment',\n promptSchema: `update_shipment payload:\n{ orderId?: uuid, orderNumber?: string, trackingNumbers?: string[], carrierName?: string, statusLabel: string, shippedAt?: string, deliveredAt?: string, estimatedDelivery?: string, notes?: string }`,\n promptRules: ['For update_shipment: use statusLabel text only.'],\n execute: executeUpdateShipmentAction,\n },\n]\n\nexport default inboxActions\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["result"]
|
|
7
7
|
}
|
package/dist/modules/sales/migrations/Migration20260916120000_sales_payment_allocation_order_idx.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Migration } from "@mikro-orm/migrations";
|
|
2
|
+
class Migration20260916120000_sales_payment_allocation_order_idx extends Migration {
|
|
3
|
+
isTransactional() {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
up() {
|
|
7
|
+
this.addSql(`drop index concurrently if exists "sales_payment_allocations_order_idx";`);
|
|
8
|
+
this.addSql(`create index concurrently "sales_payment_allocations_order_idx" on "sales_payment_allocations" ("order_id");`);
|
|
9
|
+
this.addSql(`drop index concurrently if exists "sales_payment_allocations_invoice_idx";`);
|
|
10
|
+
this.addSql(`create index concurrently "sales_payment_allocations_invoice_idx" on "sales_payment_allocations" ("invoice_id");`);
|
|
11
|
+
}
|
|
12
|
+
down() {
|
|
13
|
+
this.addSql(`drop index concurrently if exists "sales_payment_allocations_invoice_idx";`);
|
|
14
|
+
this.addSql(`drop index concurrently if exists "sales_payment_allocations_order_idx";`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
Migration20260916120000_sales_payment_allocation_order_idx
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=Migration20260916120000_sales_payment_allocation_order_idx.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/sales/migrations/Migration20260916120000_sales_payment_allocation_order_idx.ts"],
|
|
4
|
+
"sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\n// sales_payment_allocations carried a single non-PK index \u2014 (payment_id,\n// organization_id, tenant_id) \u2014 which serves the reads that start from a payment.\n// Every such read in commands/payments.ts is `{ payment }`, so that index has\n// always matched them, and its leading column also serves the payment_id foreign\n// key's referential check. Nothing here replaces it.\n//\n// The table is also read from the ORDER side, and that direction was unindexed.\n// Recomputing an order's payment totals does:\n//\n// findWithDecryption(em, SalesPaymentAllocation, { ...scope, order: orderId },\n// { populate: ['payment'] }, scope)\n//\n// which the ORM emits filtered on (organization_id, tenant_id, order_id), inner\n// joined to sales_payments \u2014 `payment` is a required to-one, and the default\n// BALANCED load strategy joins those. That recompute runs on every payment create,\n// update and delete. commands/documents.ts reads the same way in four more places:\n// one em.find in the sales.orders.delete snapshot, and three nativeDelete({ order })\n// calls that take row locks while they scan.\n//\n// Measured on a table of 1.4M rows: a parallel sequential scan removing 478,376 rows\n// per worker, 28.6 ms per read to return one row. The cost is CPU filtering across\n// 23,757 buffer accesses, so a warm cache does not reduce it \u2014 the plan that\n// measured this reported every one of those buffers as `shared hit`.\n//\n// order_id and invoice_id are indexed separately rather than folded into the index\n// above, and the scope columns are deliberately omitted, for the reason given in\n// Migration20260821120000: sales_orders.id is gen_random_uuid() from its own table,\n// so an order_id already determines its tenant and organization. Four of the five\n// order-side reads supply no scope at all, so a composite would serve none of them\n// better; the seek returns the rows of one order and any remaining terms are\n// rechecked over those.\n//\n// invoice_id gets the same treatment on the foreign key's account alone. Both\n// columns are the child side of `on delete set null` constraints, which PostgreSQL\n// does not index automatically, and the constraint check probes the referencing\n// column \u2014 so without these, every delete of a parent order or invoice scans this\n// table in full. Both parents really are hard-deleted (commands/documents.ts). This\n// is the same pairing Migration20260821120000 made for order_id and quote_id.\n//\n// Built CONCURRENTLY \u2014 the table takes a write per payment allocation and is large\n// on any install that has been running a while. CREATE INDEX CONCURRENTLY cannot run\n// inside a transaction, hence isTransactional() => false; the runner applies\n// migrations one-by-one, so the opt-out is safe. Drop first so retrying a failed\n// concurrent build removes PostgreSQL's INVALID index stub instead of letting IF NOT\n// EXISTS report success over a table that stays unindexed. Same shape as\n// Migration20260806120000 and Migration20260821120000.\n//\n// On a database that does not have these indexes yet \u2014 every fresh install \u2014 the\n// drops are no-ops and there is no window without them. An operator who already\n// created one out of band should expect it to be rebuilt here, and the reads that\n// depend on it to fall back to their pre-index cost until the concurrent build\n// finishes; on a large table, run this while the writers that depend on it are\n// paused.\nexport class Migration20260916120000_sales_payment_allocation_order_idx extends Migration {\n\n override isTransactional(): boolean {\n return false;\n }\n\n override up(): void | Promise<void> {\n this.addSql(`drop index concurrently if exists \"sales_payment_allocations_order_idx\";`);\n this.addSql(`create index concurrently \"sales_payment_allocations_order_idx\" on \"sales_payment_allocations\" (\"order_id\");`);\n this.addSql(`drop index concurrently if exists \"sales_payment_allocations_invoice_idx\";`);\n this.addSql(`create index concurrently \"sales_payment_allocations_invoice_idx\" on \"sales_payment_allocations\" (\"invoice_id\");`);\n }\n\n override down(): void | Promise<void> {\n this.addSql(`drop index concurrently if exists \"sales_payment_allocations_invoice_idx\";`);\n this.addSql(`drop index concurrently if exists \"sales_payment_allocations_order_idx\";`);\n }\n\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,iBAAiB;AAuDnB,MAAM,mEAAmE,UAAU;AAAA,EAE/E,kBAA2B;AAClC,WAAO;AAAA,EACT;AAAA,EAES,KAA2B;AAClC,SAAK,OAAO,0EAA0E;AACtF,SAAK,OAAO,8GAA8G;AAC1H,SAAK,OAAO,4EAA4E;AACxF,SAAK,OAAO,kHAAkH;AAAA,EAChI;AAAA,EAES,OAA6B;AACpC,SAAK,OAAO,4EAA4E;AACxF,SAAK,OAAO,0EAA0E;AAAA,EACxF;AAEF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/modules/wms/di.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { asValue } from "awilix";
|
|
2
|
+
import { availabilityProviderRegistry } from "@open-mercato/shared/lib/availability";
|
|
2
3
|
import {
|
|
3
4
|
InventoryBalance,
|
|
4
5
|
InventoryLot,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
WarehouseLocation,
|
|
10
11
|
WarehouseZone
|
|
11
12
|
} from "./data/entities.js";
|
|
13
|
+
import { createWmsAvailabilityProvider } from "./lib/availabilityProvider.js";
|
|
12
14
|
function register(container) {
|
|
13
15
|
container.register({
|
|
14
16
|
Warehouse: asValue(Warehouse),
|
|
@@ -20,6 +22,7 @@ function register(container) {
|
|
|
20
22
|
InventoryReservation: asValue(InventoryReservation),
|
|
21
23
|
InventoryMovement: asValue(InventoryMovement)
|
|
22
24
|
});
|
|
25
|
+
availabilityProviderRegistry.register(createWmsAvailabilityProvider(container));
|
|
23
26
|
}
|
|
24
27
|
export {
|
|
25
28
|
register
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/wms/di.ts"],
|
|
4
|
-
"sourcesContent": ["import { asValue } from 'awilix'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport {\n InventoryBalance,\n InventoryLot,\n InventoryMovement,\n InventoryReservation,\n ProductInventoryProfile,\n Warehouse,\n WarehouseLocation,\n WarehouseZone,\n} from './data/entities'\n\nexport function register(container: AppContainer) {\n container.register({\n Warehouse: asValue(Warehouse),\n WarehouseZone: asValue(WarehouseZone),\n WarehouseLocation: asValue(WarehouseLocation),\n ProductInventoryProfile: asValue(ProductInventoryProfile),\n InventoryLot: asValue(InventoryLot),\n InventoryBalance: asValue(InventoryBalance),\n InventoryReservation: asValue(InventoryReservation),\n InventoryMovement: asValue(InventoryMovement),\n })\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,eAAe;AAExB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;
|
|
4
|
+
"sourcesContent": ["import { asValue } from 'awilix'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { availabilityProviderRegistry } from '@open-mercato/shared/lib/availability'\nimport {\n InventoryBalance,\n InventoryLot,\n InventoryMovement,\n InventoryReservation,\n ProductInventoryProfile,\n Warehouse,\n WarehouseLocation,\n WarehouseZone,\n} from './data/entities'\nimport { createWmsAvailabilityProvider } from './lib/availabilityProvider'\n\nexport function register(container: AppContainer) {\n container.register({\n Warehouse: asValue(Warehouse),\n WarehouseZone: asValue(WarehouseZone),\n WarehouseLocation: asValue(WarehouseLocation),\n ProductInventoryProfile: asValue(ProductInventoryProfile),\n InventoryLot: asValue(InventoryLot),\n InventoryBalance: asValue(InventoryBalance),\n InventoryReservation: asValue(InventoryReservation),\n InventoryMovement: asValue(InventoryMovement),\n })\n\n // Explicit, idempotent (replace-by-id) registration into the shared,\n // dependency-free registry \u2014 never \"same DI key, load order wins\" (\u00A73.1).\n availabilityProviderRegistry.register(createWmsAvailabilityProvider(container))\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,eAAe;AAExB,SAAS,oCAAoC;AAC7C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,qCAAqC;AAEvC,SAAS,SAAS,WAAyB;AAChD,YAAU,SAAS;AAAA,IACjB,WAAW,QAAQ,SAAS;AAAA,IAC5B,eAAe,QAAQ,aAAa;AAAA,IACpC,mBAAmB,QAAQ,iBAAiB;AAAA,IAC5C,yBAAyB,QAAQ,uBAAuB;AAAA,IACxD,cAAc,QAAQ,YAAY;AAAA,IAClC,kBAAkB,QAAQ,gBAAgB;AAAA,IAC1C,sBAAsB,QAAQ,oBAAoB;AAAA,IAClD,mBAAmB,QAAQ,iBAAiB;AAAA,EAC9C,CAAC;AAID,+BAA6B,SAAS,8BAA8B,SAAS,CAAC;AAChF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { runWithCacheTenant } from "@open-mercato/cache";
|
|
2
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
3
|
+
import { availabilityItemKey } from "@open-mercato/shared/lib/availability";
|
|
4
|
+
import { computeAvailability } from "./availabilityCalculation.js";
|
|
5
|
+
const WMS_AVAILABILITY_CACHE_TAG = "wms:availability";
|
|
6
|
+
const WMS_AVAILABILITY_CACHE_TTL_MS = 6e4;
|
|
7
|
+
const logger = createLogger("wms").child({ component: "availability-cache" });
|
|
8
|
+
function resolveCache(container) {
|
|
9
|
+
if (!container?.resolve) return null;
|
|
10
|
+
for (const name of ["cache", "cacheService"]) {
|
|
11
|
+
try {
|
|
12
|
+
const candidate = container.resolve(name);
|
|
13
|
+
if (candidate && typeof candidate.deleteByTags === "function") return candidate;
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
function cacheKeyForItem(query, item) {
|
|
20
|
+
return [
|
|
21
|
+
"wms:availability",
|
|
22
|
+
query.organizationId,
|
|
23
|
+
query.storeId ?? "",
|
|
24
|
+
item.catalogProductId,
|
|
25
|
+
item.catalogVariantId ?? "",
|
|
26
|
+
item.quantity
|
|
27
|
+
].join(":");
|
|
28
|
+
}
|
|
29
|
+
async function computeAvailabilityCached(em, container, query) {
|
|
30
|
+
const cache = resolveCache(container);
|
|
31
|
+
if (!cache || query.bypassCache) {
|
|
32
|
+
return computeAvailability(em, container, query);
|
|
33
|
+
}
|
|
34
|
+
return runWithCacheTenant(query.tenantId, async () => {
|
|
35
|
+
const byItem = {};
|
|
36
|
+
const missed = [];
|
|
37
|
+
for (const item of query.items) {
|
|
38
|
+
const key = availabilityItemKey(item);
|
|
39
|
+
try {
|
|
40
|
+
const cached = await cache.get(cacheKeyForItem(query, item));
|
|
41
|
+
if (cached) {
|
|
42
|
+
byItem[key] = { ...cached, isAuthoritative: false };
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
logger.warn("Availability cache read failed; computing live", { key, err });
|
|
47
|
+
}
|
|
48
|
+
missed.push(item);
|
|
49
|
+
}
|
|
50
|
+
if (missed.length > 0) {
|
|
51
|
+
const live = await computeAvailability(em, container, { ...query, items: missed });
|
|
52
|
+
for (const item of missed) {
|
|
53
|
+
const key = availabilityItemKey(item);
|
|
54
|
+
const result = live.byItem[key];
|
|
55
|
+
byItem[key] = result;
|
|
56
|
+
try {
|
|
57
|
+
await cache.set(cacheKeyForItem(query, item), result, {
|
|
58
|
+
ttl: WMS_AVAILABILITY_CACHE_TTL_MS,
|
|
59
|
+
tags: [WMS_AVAILABILITY_CACHE_TAG]
|
|
60
|
+
});
|
|
61
|
+
} catch (err) {
|
|
62
|
+
logger.warn("Availability cache write failed", { key, err });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { byItem };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async function invalidateWmsAvailabilityCache(container, tenantId) {
|
|
70
|
+
if (!tenantId) return;
|
|
71
|
+
const cache = resolveCache(container);
|
|
72
|
+
if (!cache) return;
|
|
73
|
+
try {
|
|
74
|
+
await runWithCacheTenant(tenantId, () => cache.deleteByTags([WMS_AVAILABILITY_CACHE_TAG]));
|
|
75
|
+
} catch (err) {
|
|
76
|
+
logger.warn("Failed to invalidate wms availability cache", { tenantId, err });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export {
|
|
80
|
+
WMS_AVAILABILITY_CACHE_TAG,
|
|
81
|
+
WMS_AVAILABILITY_CACHE_TTL_MS,
|
|
82
|
+
computeAvailabilityCached,
|
|
83
|
+
invalidateWmsAvailabilityCache
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=availabilityCache.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/wms/lib/availabilityCache.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Read-through cache for `wms`'s `AvailabilityProvider` \u2014 \u00A76 staleness\n * budget (60s TTL for a browse-time `check`, never cached for\n * `query.bypassCache`), tag-based invalidation on `wms` balance changes.\n *\n * Tag granularity is a deliberate deviation from \u00A76's literal\n * `availability:{tenantId}:{variantId}` naming: this module already has a\n * coarse, tenant-scoped tag for the same purpose\n * (`enricherCacheTags.ts`'s `WMS_INVENTORY_CACHE_TAG`), justified there as\n * \"collection tags over-invalidate slightly; a per-warehouse scheme the\n * write side could miss would under-invalidate, which is the failure that\n * actually shows wrong stock to a user.\" The same reasoning applies here, so\n * `WMS_AVAILABILITY_CACHE_TAG` follows that established precedent rather\n * than a literal per-variant tag. `runWithCacheTenant` already scopes both\n * keys and tags to the current tenant (`packages/cache/src/service.ts`), so\n * neither needs `tenantId` embedded manually.\n */\n\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { availabilityItemKey } from '@open-mercato/shared/lib/availability'\nimport type { AvailabilityItemResult, AvailabilityQuery, AvailabilityResult } from '@open-mercato/shared/lib/availability'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { computeAvailability } from './availabilityCalculation'\n\nexport const WMS_AVAILABILITY_CACHE_TAG = 'wms:availability'\n/** \u00A76 staleness budget: a browse-time `check` may be up to 60s stale. */\nexport const WMS_AVAILABILITY_CACHE_TTL_MS = 60_000\n\nconst logger = createLogger('wms').child({ component: 'availability-cache' })\n\ntype CacheService = {\n get(key: string, options?: { returnExpired?: boolean }): Promise<unknown>\n set(key: string, value: unknown, options?: { ttl?: number; tags?: string[] }): Promise<void>\n deleteByTags(tags: string[]): Promise<number>\n}\n\ntype Resolver = { resolve: <T = unknown>(name: string) => T }\n\nfunction resolveCache(container: Resolver | null | undefined): CacheService | null {\n if (!container?.resolve) return null\n for (const name of ['cache', 'cacheService']) {\n try {\n const candidate = container.resolve<CacheService>(name)\n if (candidate && typeof candidate.deleteByTags === 'function') return candidate\n } catch {\n // try the next registration name\n }\n }\n return null\n}\n\nfunction cacheKeyForItem(query: AvailabilityQuery, item: AvailabilityQuery['items'][number]): string {\n return [\n 'wms:availability',\n query.organizationId,\n query.storeId ?? '',\n item.catalogProductId,\n item.catalogVariantId ?? '',\n item.quantity,\n ].join(':')\n}\n\n/**\n * Wraps `computeAvailability` with the read-through cache. Falls back to a\n * live computation when no cache service is registered or `bypassCache` is\n * set \u2014 the same code path either way, just without the read/write.\n */\nexport async function computeAvailabilityCached(\n em: EntityManager,\n container: Resolver,\n query: AvailabilityQuery,\n): Promise<AvailabilityResult> {\n const cache = resolveCache(container)\n if (!cache || query.bypassCache) {\n return computeAvailability(em, container, query)\n }\n\n return runWithCacheTenant(query.tenantId, async () => {\n const byItem: AvailabilityResult['byItem'] = {}\n const missed: AvailabilityQuery['items'] = []\n\n for (const item of query.items) {\n const key = availabilityItemKey(item)\n try {\n const cached = await cache.get(cacheKeyForItem(query, item))\n if (cached) {\n byItem[key] = { ...(cached as AvailabilityItemResult), isAuthoritative: false }\n continue\n }\n } catch (err) {\n logger.warn('Availability cache read failed; computing live', { key, err })\n }\n missed.push(item)\n }\n\n if (missed.length > 0) {\n const live = await computeAvailability(em, container, { ...query, items: missed })\n for (const item of missed) {\n const key = availabilityItemKey(item)\n const result = live.byItem[key]\n byItem[key] = result\n try {\n await cache.set(cacheKeyForItem(query, item), result, {\n ttl: WMS_AVAILABILITY_CACHE_TTL_MS,\n tags: [WMS_AVAILABILITY_CACHE_TAG],\n })\n } catch (err) {\n logger.warn('Availability cache write failed', { key, err })\n }\n }\n }\n\n return { byItem }\n })\n}\n\n/**\n * Best-effort tag invalidation \u2014 mirrors\n * `invalidateInventoryEnricherCache.ts`. A failure is logged and swallowed:\n * invalidation must never fail the write that triggered it, and the TTL\n * bounds how long a missed drop can serve stale data.\n */\nexport async function invalidateWmsAvailabilityCache(\n container: Resolver | null | undefined,\n tenantId: string | null | undefined,\n): Promise<void> {\n if (!tenantId) return\n const cache = resolveCache(container)\n if (!cache) return\n try {\n await runWithCacheTenant(tenantId, () => cache.deleteByTags([WMS_AVAILABILITY_CACHE_TAG]))\n } catch (err) {\n logger.warn('Failed to invalidate wms availability cache', { tenantId, err })\n }\n}\n"],
|
|
5
|
+
"mappings": "AAkBA,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AAGpC,SAAS,2BAA2B;AAE7B,MAAM,6BAA6B;AAEnC,MAAM,gCAAgC;AAE7C,MAAM,SAAS,aAAa,KAAK,EAAE,MAAM,EAAE,WAAW,qBAAqB,CAAC;AAU5E,SAAS,aAAa,WAA6D;AACjF,MAAI,CAAC,WAAW,QAAS,QAAO;AAChC,aAAW,QAAQ,CAAC,SAAS,cAAc,GAAG;AAC5C,QAAI;AACF,YAAM,YAAY,UAAU,QAAsB,IAAI;AACtD,UAAI,aAAa,OAAO,UAAU,iBAAiB,WAAY,QAAO;AAAA,IACxE,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,gBAAgB,OAA0B,MAAkD;AACnG,SAAO;AAAA,IACL;AAAA,IACA,MAAM;AAAA,IACN,MAAM,WAAW;AAAA,IACjB,KAAK;AAAA,IACL,KAAK,oBAAoB;AAAA,IACzB,KAAK;AAAA,EACP,EAAE,KAAK,GAAG;AACZ;AAOA,eAAsB,0BACpB,IACA,WACA,OAC6B;AAC7B,QAAM,QAAQ,aAAa,SAAS;AACpC,MAAI,CAAC,SAAS,MAAM,aAAa;AAC/B,WAAO,oBAAoB,IAAI,WAAW,KAAK;AAAA,EACjD;AAEA,SAAO,mBAAmB,MAAM,UAAU,YAAY;AACpD,UAAM,SAAuC,CAAC;AAC9C,UAAM,SAAqC,CAAC;AAE5C,eAAW,QAAQ,MAAM,OAAO;AAC9B,YAAM,MAAM,oBAAoB,IAAI;AACpC,UAAI;AACF,cAAM,SAAS,MAAM,MAAM,IAAI,gBAAgB,OAAO,IAAI,CAAC;AAC3D,YAAI,QAAQ;AACV,iBAAO,GAAG,IAAI,EAAE,GAAI,QAAmC,iBAAiB,MAAM;AAC9E;AAAA,QACF;AAAA,MACF,SAAS,KAAK;AACZ,eAAO,KAAK,kDAAkD,EAAE,KAAK,IAAI,CAAC;AAAA,MAC5E;AACA,aAAO,KAAK,IAAI;AAAA,IAClB;AAEA,QAAI,OAAO,SAAS,GAAG;AACrB,YAAM,OAAO,MAAM,oBAAoB,IAAI,WAAW,EAAE,GAAG,OAAO,OAAO,OAAO,CAAC;AACjF,iBAAW,QAAQ,QAAQ;AACzB,cAAM,MAAM,oBAAoB,IAAI;AACpC,cAAM,SAAS,KAAK,OAAO,GAAG;AAC9B,eAAO,GAAG,IAAI;AACd,YAAI;AACF,gBAAM,MAAM,IAAI,gBAAgB,OAAO,IAAI,GAAG,QAAQ;AAAA,YACpD,KAAK;AAAA,YACL,MAAM,CAAC,0BAA0B;AAAA,UACnC,CAAC;AAAA,QACH,SAAS,KAAK;AACZ,iBAAO,KAAK,mCAAmC,EAAE,KAAK,IAAI,CAAC;AAAA,QAC7D;AAAA,MACF;AAAA,IACF;AAEA,WAAO,EAAE,OAAO;AAAA,EAClB,CAAC;AACH;AAQA,eAAsB,+BACpB,WACA,UACe;AACf,MAAI,CAAC,SAAU;AACf,QAAM,QAAQ,aAAa,SAAS;AACpC,MAAI,CAAC,MAAO;AACZ,MAAI;AACF,UAAM,mBAAmB,UAAU,MAAM,MAAM,aAAa,CAAC,0BAA0B,CAAC,CAAC;AAAA,EAC3F,SAAS,KAAK;AACZ,WAAO,KAAK,+CAA+C,EAAE,UAAU,IAAI,CAAC;AAAA,EAC9E;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { availabilityItemKey } from "@open-mercato/shared/lib/availability";
|
|
2
|
+
import { tryResolve } from "./tryResolve.js";
|
|
3
|
+
import { buildSqlInClause } from "./sqlInClause.js";
|
|
4
|
+
const OPEN_POLICY_DEFAULT = {
|
|
5
|
+
isStockManaged: { value: true, policySourceId: null },
|
|
6
|
+
allowBackorder: { value: false, policySourceId: null },
|
|
7
|
+
backorderLeadTimeDays: { value: null, policySourceId: null },
|
|
8
|
+
preorderReleaseAt: { value: null, policySourceId: null },
|
|
9
|
+
lowStockThreshold: { value: null, policySourceId: null },
|
|
10
|
+
isActive: { value: true, policySourceId: null }
|
|
11
|
+
};
|
|
12
|
+
async function loadBalanceAggregates(em, tenantId, organizationId, variantIds, locationIds) {
|
|
13
|
+
const result = /* @__PURE__ */ new Map();
|
|
14
|
+
if (variantIds.length === 0) return result;
|
|
15
|
+
const variantClause = buildSqlInClause("b.catalog_variant_id", variantIds);
|
|
16
|
+
const params = [organizationId, tenantId, ...variantClause.params];
|
|
17
|
+
let locationClause = "";
|
|
18
|
+
if (locationIds && locationIds.length > 0) {
|
|
19
|
+
const locationIn = buildSqlInClause("b.location_id", locationIds);
|
|
20
|
+
locationClause = ` and ${locationIn.sql}`;
|
|
21
|
+
params.push(...locationIn.params);
|
|
22
|
+
}
|
|
23
|
+
const sql = `
|
|
24
|
+
select b.catalog_variant_id,
|
|
25
|
+
sum(coalesce(b.quantity_on_hand, 0) - coalesce(b.quantity_reserved, 0) - coalesce(b.quantity_allocated, 0)) as aggregate_available
|
|
26
|
+
from wms_inventory_balances b
|
|
27
|
+
where b.organization_id = ? and b.tenant_id = ? and ${variantClause.sql} and b.deleted_at is null
|
|
28
|
+
${locationClause}
|
|
29
|
+
group by b.catalog_variant_id
|
|
30
|
+
`;
|
|
31
|
+
const rows = await em.getConnection().execute(sql, params);
|
|
32
|
+
for (const row of rows) {
|
|
33
|
+
result.set(row.catalog_variant_id, Number(row.aggregate_available ?? 0));
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
async function loadProfiles(em, tenantId, organizationId, variantIds, productIds) {
|
|
38
|
+
if (variantIds.length === 0 && productIds.length === 0) return [];
|
|
39
|
+
const variantClause = buildSqlInClause("catalog_variant_id", variantIds);
|
|
40
|
+
const productClause = buildSqlInClause("catalog_product_id", productIds);
|
|
41
|
+
const sql = `
|
|
42
|
+
select catalog_product_id, catalog_variant_id, safety_stock, reorder_point
|
|
43
|
+
from wms_product_inventory_profiles
|
|
44
|
+
where organization_id = ? and tenant_id = ? and deleted_at is null
|
|
45
|
+
and (${variantClause.sql} or (${productClause.sql} and catalog_variant_id is null))
|
|
46
|
+
`;
|
|
47
|
+
return em.getConnection().execute(sql, [
|
|
48
|
+
organizationId,
|
|
49
|
+
tenantId,
|
|
50
|
+
...variantClause.params,
|
|
51
|
+
...productClause.params
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
async function loadActiveVariantsForProducts(em, tenantId, organizationId, productIds) {
|
|
55
|
+
if (productIds.length === 0) return [];
|
|
56
|
+
const productClause = buildSqlInClause("product_id", productIds);
|
|
57
|
+
const sql = `
|
|
58
|
+
select id, product_id
|
|
59
|
+
from catalog_product_variants
|
|
60
|
+
where organization_id = ? and tenant_id = ? and ${productClause.sql} and is_active = true and deleted_at is null
|
|
61
|
+
`;
|
|
62
|
+
return em.getConnection().execute(sql, [organizationId, tenantId, ...productClause.params]);
|
|
63
|
+
}
|
|
64
|
+
function findProfile(profiles, productId, variantId) {
|
|
65
|
+
if (variantId) {
|
|
66
|
+
const variantRow = profiles.find((p) => p.catalog_variant_id === variantId);
|
|
67
|
+
if (variantRow) return variantRow;
|
|
68
|
+
}
|
|
69
|
+
return profiles.find((p) => p.catalog_product_id === productId && !p.catalog_variant_id) ?? null;
|
|
70
|
+
}
|
|
71
|
+
function computeState(params) {
|
|
72
|
+
if (!params.isActive) {
|
|
73
|
+
return { state: "out_of_stock", canFulfil: false, leadTimeDays: null, releaseAt: null };
|
|
74
|
+
}
|
|
75
|
+
if (params.preorderReleaseAt && params.preorderReleaseAt.getTime() > Date.now()) {
|
|
76
|
+
return { state: "preorder", canFulfil: true, leadTimeDays: null, releaseAt: params.preorderReleaseAt.toISOString() };
|
|
77
|
+
}
|
|
78
|
+
if (params.sellable >= params.requested) {
|
|
79
|
+
const state = params.lowStockThreshold != null && params.sellable <= params.lowStockThreshold ? "low_stock" : "in_stock";
|
|
80
|
+
return { state, canFulfil: true, leadTimeDays: null, releaseAt: null };
|
|
81
|
+
}
|
|
82
|
+
if (params.allowBackorder) {
|
|
83
|
+
return { state: "backorder", canFulfil: true, leadTimeDays: params.backorderLeadTimeDays, releaseAt: null };
|
|
84
|
+
}
|
|
85
|
+
return { state: "out_of_stock", canFulfil: false, leadTimeDays: null, releaseAt: null };
|
|
86
|
+
}
|
|
87
|
+
async function computeAvailability(em, container, query) {
|
|
88
|
+
const byItem = {};
|
|
89
|
+
if (query.items.length === 0) return { byItem };
|
|
90
|
+
const variantItems = query.items.filter((item) => !!item.catalogVariantId);
|
|
91
|
+
const productItems = query.items.filter((item) => !item.catalogVariantId);
|
|
92
|
+
const productItemIds = Array.from(new Set(productItems.map((item) => item.catalogProductId)));
|
|
93
|
+
const rollupVariantRows = await loadActiveVariantsForProducts(em, query.tenantId, query.organizationId, productItemIds);
|
|
94
|
+
const variantsByProduct = /* @__PURE__ */ new Map();
|
|
95
|
+
for (const row of rollupVariantRows) {
|
|
96
|
+
const list = variantsByProduct.get(row.product_id) ?? [];
|
|
97
|
+
list.push(row.id);
|
|
98
|
+
variantsByProduct.set(row.product_id, list);
|
|
99
|
+
}
|
|
100
|
+
const allVariantIds = Array.from(
|
|
101
|
+
/* @__PURE__ */ new Set([
|
|
102
|
+
...variantItems.map((item) => item.catalogVariantId),
|
|
103
|
+
...rollupVariantRows.map((row) => row.id)
|
|
104
|
+
])
|
|
105
|
+
);
|
|
106
|
+
const allProductIds = Array.from(new Set(query.items.map((item) => item.catalogProductId)));
|
|
107
|
+
const [balances, profiles] = await Promise.all([
|
|
108
|
+
loadBalanceAggregates(em, query.tenantId, query.organizationId, allVariantIds, query.locationIds),
|
|
109
|
+
loadProfiles(em, query.tenantId, query.organizationId, allVariantIds, allProductIds)
|
|
110
|
+
]);
|
|
111
|
+
const policyService = tryResolve(container, "policyResolutionService");
|
|
112
|
+
const policyScopes = query.items.map((item) => ({
|
|
113
|
+
tenantId: query.tenantId,
|
|
114
|
+
organizationId: query.organizationId,
|
|
115
|
+
storeId: query.storeId ?? null,
|
|
116
|
+
productId: item.catalogProductId,
|
|
117
|
+
variantId: item.catalogVariantId ?? null
|
|
118
|
+
}));
|
|
119
|
+
const resolvedPolicies = policyService ? await policyService.resolveMany(em, policyScopes) : policyScopes.map(() => OPEN_POLICY_DEFAULT);
|
|
120
|
+
function sellableFor(variantId, productId) {
|
|
121
|
+
const aggregate = balances.get(variantId) ?? 0;
|
|
122
|
+
const profile = findProfile(profiles, productId, variantId);
|
|
123
|
+
const safetyStock = profile ? Number(profile.safety_stock ?? 0) : 0;
|
|
124
|
+
return Math.max(0, aggregate - safetyStock);
|
|
125
|
+
}
|
|
126
|
+
query.items.forEach((item, index) => {
|
|
127
|
+
const policy = resolvedPolicies[index] ?? OPEN_POLICY_DEFAULT;
|
|
128
|
+
const key = availabilityItemKey(item);
|
|
129
|
+
if (!policy.isStockManaged.value) {
|
|
130
|
+
byItem[key] = {
|
|
131
|
+
state: "not_tracked",
|
|
132
|
+
availableQuantity: null,
|
|
133
|
+
canFulfil: true,
|
|
134
|
+
leadTimeDays: null,
|
|
135
|
+
releaseAt: null,
|
|
136
|
+
isAuthoritative: true,
|
|
137
|
+
policySourceId: policy.isStockManaged.policySourceId
|
|
138
|
+
};
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
let sellable;
|
|
142
|
+
let reorderPoint = null;
|
|
143
|
+
if (item.catalogVariantId) {
|
|
144
|
+
sellable = sellableFor(item.catalogVariantId, item.catalogProductId);
|
|
145
|
+
const profile = findProfile(profiles, item.catalogProductId, item.catalogVariantId);
|
|
146
|
+
reorderPoint = profile && profile.reorder_point != null ? Number(profile.reorder_point) : null;
|
|
147
|
+
} else {
|
|
148
|
+
const variantIds = variantsByProduct.get(item.catalogProductId) ?? [];
|
|
149
|
+
sellable = variantIds.reduce((sum, variantId) => sum + sellableFor(variantId, item.catalogProductId), 0);
|
|
150
|
+
const profile = findProfile(profiles, item.catalogProductId, null);
|
|
151
|
+
reorderPoint = profile && profile.reorder_point != null ? Number(profile.reorder_point) : null;
|
|
152
|
+
}
|
|
153
|
+
const effectiveLowStockThreshold = policy.lowStockThreshold.value ?? reorderPoint;
|
|
154
|
+
const computed = computeState({
|
|
155
|
+
sellable,
|
|
156
|
+
requested: item.quantity,
|
|
157
|
+
lowStockThreshold: effectiveLowStockThreshold,
|
|
158
|
+
allowBackorder: policy.allowBackorder.value,
|
|
159
|
+
backorderLeadTimeDays: policy.backorderLeadTimeDays.value,
|
|
160
|
+
preorderReleaseAt: policy.preorderReleaseAt.value,
|
|
161
|
+
isActive: policy.isActive.value
|
|
162
|
+
});
|
|
163
|
+
byItem[key] = {
|
|
164
|
+
state: computed.state,
|
|
165
|
+
availableQuantity: sellable,
|
|
166
|
+
canFulfil: computed.canFulfil,
|
|
167
|
+
leadTimeDays: computed.leadTimeDays,
|
|
168
|
+
releaseAt: computed.releaseAt,
|
|
169
|
+
isAuthoritative: true,
|
|
170
|
+
policySourceId: policy.isStockManaged.policySourceId
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
return { byItem };
|
|
174
|
+
}
|
|
175
|
+
export {
|
|
176
|
+
computeAvailability
|
|
177
|
+
};
|
|
178
|
+
//# sourceMappingURL=availabilityCalculation.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/wms/lib/availabilityCalculation.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * `wms`'s `AvailabilityProvider` computation \u2014 batched sellable-quantity\n * aggregation, safety-stock handling, low-stock thresholds, and product\n * rollup.\n *\n * @see .ai/specs/2026-08-14-availability-contract.md \u00A74.2\n *\n * Batching contract (R4): exactly one balance aggregation, one profile\n * lookup, one policy lookup, and (only when the batch has product-level\n * items) one variant-rollup lookup \u2014 never per item.\n *\n * Safety-stock contract (R1): `sellable = max(0, aggregate_available -\n * safety_stock)`, with `safety_stock` subtracted ONCE per variant, after\n * aggregating every in-scope location \u2014 never once per balance row.\n */\n\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { availabilityItemKey } from '@open-mercato/shared/lib/availability'\nimport type { AvailabilityItemResult, AvailabilityQuery, AvailabilityResult, AvailabilityState } from '@open-mercato/shared/lib/availability'\nimport { tryResolve } from './tryResolve'\nimport { buildSqlInClause } from './sqlInClause'\n\ntype Resolver = { resolve: <T = unknown>(name: string) => T }\n\n/**\n * Narrow, locally-declared duck type for `availability`'s `PolicyResolutionService`\n * (soft-resolved, never statically imported \u2014 `wms` never hard-requires `availability`,\n * \u00A73.1). Structurally identical to the real service's relevant surface.\n */\ntype PolicyField<T> = { value: T; policySourceId: string | null }\ntype ResolvedPolicyOverlay = {\n isStockManaged: PolicyField<boolean>\n allowBackorder: PolicyField<boolean>\n backorderLeadTimeDays: PolicyField<number | null>\n preorderReleaseAt: PolicyField<Date | null>\n lowStockThreshold: PolicyField<number | null>\n isActive: PolicyField<boolean>\n}\ntype PolicyResolutionScopeLike = {\n tenantId: string\n organizationId: string\n storeId?: string | null\n productId: string\n variantId?: string | null\n}\ntype PolicyResolutionServiceLike = {\n resolveMany(em: EntityManager, scopes: PolicyResolutionScopeLike[]): Promise<ResolvedPolicyOverlay[]>\n}\n\nconst OPEN_POLICY_DEFAULT: ResolvedPolicyOverlay = {\n isStockManaged: { value: true, policySourceId: null },\n allowBackorder: { value: false, policySourceId: null },\n backorderLeadTimeDays: { value: null, policySourceId: null },\n preorderReleaseAt: { value: null, policySourceId: null },\n lowStockThreshold: { value: null, policySourceId: null },\n isActive: { value: true, policySourceId: null },\n}\n\ntype BalanceAggregateRow = { catalog_variant_id: string; aggregate_available: string | number | null }\ntype ProfileRow = {\n catalog_product_id: string\n catalog_variant_id: string | null\n safety_stock: string | number | null\n reorder_point: string | number | null\n}\ntype VariantRow = { id: string; product_id: string }\n\nasync function loadBalanceAggregates(\n em: EntityManager,\n tenantId: string,\n organizationId: string,\n variantIds: string[],\n locationIds?: string[] | null,\n): Promise<Map<string, number>> {\n const result = new Map<string, number>()\n if (variantIds.length === 0) return result\n\n const variantClause = buildSqlInClause('b.catalog_variant_id', variantIds)\n const params: unknown[] = [organizationId, tenantId, ...variantClause.params]\n let locationClause = ''\n if (locationIds && locationIds.length > 0) {\n const locationIn = buildSqlInClause('b.location_id', locationIds)\n locationClause = ` and ${locationIn.sql}`\n params.push(...locationIn.params)\n }\n\n const sql = `\n select b.catalog_variant_id,\n sum(coalesce(b.quantity_on_hand, 0) - coalesce(b.quantity_reserved, 0) - coalesce(b.quantity_allocated, 0)) as aggregate_available\n from wms_inventory_balances b\n where b.organization_id = ? and b.tenant_id = ? and ${variantClause.sql} and b.deleted_at is null\n ${locationClause}\n group by b.catalog_variant_id\n `\n const rows = await em.getConnection().execute<BalanceAggregateRow[]>(sql, params)\n for (const row of rows) {\n result.set(row.catalog_variant_id, Number(row.aggregate_available ?? 0))\n }\n return result\n}\n\nasync function loadProfiles(\n em: EntityManager,\n tenantId: string,\n organizationId: string,\n variantIds: string[],\n productIds: string[],\n): Promise<ProfileRow[]> {\n if (variantIds.length === 0 && productIds.length === 0) return []\n const variantClause = buildSqlInClause('catalog_variant_id', variantIds)\n const productClause = buildSqlInClause('catalog_product_id', productIds)\n const sql = `\n select catalog_product_id, catalog_variant_id, safety_stock, reorder_point\n from wms_product_inventory_profiles\n where organization_id = ? and tenant_id = ? and deleted_at is null\n and (${variantClause.sql} or (${productClause.sql} and catalog_variant_id is null))\n `\n return em.getConnection().execute<ProfileRow[]>(sql, [\n organizationId,\n tenantId,\n ...variantClause.params,\n ...productClause.params,\n ])\n}\n\nasync function loadActiveVariantsForProducts(\n em: EntityManager,\n tenantId: string,\n organizationId: string,\n productIds: string[],\n): Promise<VariantRow[]> {\n if (productIds.length === 0) return []\n const productClause = buildSqlInClause('product_id', productIds)\n const sql = `\n select id, product_id\n from catalog_product_variants\n where organization_id = ? and tenant_id = ? and ${productClause.sql} and is_active = true and deleted_at is null\n `\n return em.getConnection().execute<VariantRow[]>(sql, [organizationId, tenantId, ...productClause.params])\n}\n\nfunction findProfile(profiles: ProfileRow[], productId: string, variantId: string | null): ProfileRow | null {\n if (variantId) {\n const variantRow = profiles.find((p) => p.catalog_variant_id === variantId)\n if (variantRow) return variantRow\n }\n return profiles.find((p) => p.catalog_product_id === productId && !p.catalog_variant_id) ?? null\n}\n\nfunction computeState(params: {\n sellable: number\n requested: number\n lowStockThreshold: number | null\n allowBackorder: boolean\n backorderLeadTimeDays: number | null\n preorderReleaseAt: Date | null\n isActive: boolean\n}): { state: AvailabilityState; canFulfil: boolean; leadTimeDays: number | null; releaseAt: string | null } {\n if (!params.isActive) {\n return { state: 'out_of_stock', canFulfil: false, leadTimeDays: null, releaseAt: null }\n }\n if (params.preorderReleaseAt && params.preorderReleaseAt.getTime() > Date.now()) {\n return { state: 'preorder', canFulfil: true, leadTimeDays: null, releaseAt: params.preorderReleaseAt.toISOString() }\n }\n if (params.sellable >= params.requested) {\n const state: AvailabilityState =\n params.lowStockThreshold != null && params.sellable <= params.lowStockThreshold ? 'low_stock' : 'in_stock'\n return { state, canFulfil: true, leadTimeDays: null, releaseAt: null }\n }\n if (params.allowBackorder) {\n return { state: 'backorder', canFulfil: true, leadTimeDays: params.backorderLeadTimeDays, releaseAt: null }\n }\n return { state: 'out_of_stock', canFulfil: false, leadTimeDays: null, releaseAt: null }\n}\n\n/**\n * Computes availability for a batch of items \u2014 always live, never cached\n * (the cache wrapper lives in `availabilityCache.ts`, Phase 2 Step 3.2).\n */\nexport async function computeAvailability(\n em: EntityManager,\n container: Resolver,\n query: AvailabilityQuery,\n): Promise<AvailabilityResult> {\n const byItem: AvailabilityResult['byItem'] = {}\n if (query.items.length === 0) return { byItem }\n\n const variantItems = query.items.filter((item) => !!item.catalogVariantId)\n const productItems = query.items.filter((item) => !item.catalogVariantId)\n const productItemIds = Array.from(new Set(productItems.map((item) => item.catalogProductId)))\n\n // One batched lookup of active variants for every product-level item (R4).\n const rollupVariantRows = await loadActiveVariantsForProducts(em, query.tenantId, query.organizationId, productItemIds)\n const variantsByProduct = new Map<string, string[]>()\n for (const row of rollupVariantRows) {\n const list = variantsByProduct.get(row.product_id) ?? []\n list.push(row.id)\n variantsByProduct.set(row.product_id, list)\n }\n\n const allVariantIds = Array.from(\n new Set([\n ...variantItems.map((item) => item.catalogVariantId as string),\n ...rollupVariantRows.map((row) => row.id),\n ]),\n )\n const allProductIds = Array.from(new Set(query.items.map((item) => item.catalogProductId)))\n\n // One balance aggregation and one profile lookup for the whole batch (R4).\n const [balances, profiles] = await Promise.all([\n loadBalanceAggregates(em, query.tenantId, query.organizationId, allVariantIds, query.locationIds),\n loadProfiles(em, query.tenantId, query.organizationId, allVariantIds, allProductIds),\n ])\n\n // One batched policy resolution for the whole batch (R4).\n const policyService = tryResolve<PolicyResolutionServiceLike>(container, 'policyResolutionService')\n const policyScopes: PolicyResolutionScopeLike[] = query.items.map((item) => ({\n tenantId: query.tenantId,\n organizationId: query.organizationId,\n storeId: query.storeId ?? null,\n productId: item.catalogProductId,\n variantId: item.catalogVariantId ?? null,\n }))\n const resolvedPolicies = policyService\n ? await policyService.resolveMany(em, policyScopes)\n : policyScopes.map(() => OPEN_POLICY_DEFAULT)\n\n function sellableFor(variantId: string, productId: string): number {\n const aggregate = balances.get(variantId) ?? 0\n const profile = findProfile(profiles, productId, variantId)\n const safetyStock = profile ? Number(profile.safety_stock ?? 0) : 0\n return Math.max(0, aggregate - safetyStock)\n }\n\n query.items.forEach((item, index) => {\n const policy = resolvedPolicies[index] ?? OPEN_POLICY_DEFAULT\n const key = availabilityItemKey(item)\n\n if (!policy.isStockManaged.value) {\n byItem[key] = {\n state: 'not_tracked',\n availableQuantity: null,\n canFulfil: true,\n leadTimeDays: null,\n releaseAt: null,\n isAuthoritative: true,\n policySourceId: policy.isStockManaged.policySourceId,\n }\n return\n }\n\n let sellable: number\n let reorderPoint: number | null = null\n if (item.catalogVariantId) {\n sellable = sellableFor(item.catalogVariantId, item.catalogProductId)\n const profile = findProfile(profiles, item.catalogProductId, item.catalogVariantId)\n reorderPoint = profile && profile.reorder_point != null ? Number(profile.reorder_point) : null\n } else {\n const variantIds = variantsByProduct.get(item.catalogProductId) ?? []\n sellable = variantIds.reduce((sum, variantId) => sum + sellableFor(variantId, item.catalogProductId), 0)\n const profile = findProfile(profiles, item.catalogProductId, null)\n reorderPoint = profile && profile.reorder_point != null ? Number(profile.reorder_point) : null\n }\n\n const effectiveLowStockThreshold = policy.lowStockThreshold.value ?? reorderPoint\n\n const computed = computeState({\n sellable,\n requested: item.quantity,\n lowStockThreshold: effectiveLowStockThreshold,\n allowBackorder: policy.allowBackorder.value,\n backorderLeadTimeDays: policy.backorderLeadTimeDays.value,\n preorderReleaseAt: policy.preorderReleaseAt.value,\n isActive: policy.isActive.value,\n })\n\n byItem[key] = {\n state: computed.state,\n availableQuantity: sellable,\n canFulfil: computed.canFulfil,\n leadTimeDays: computed.leadTimeDays,\n releaseAt: computed.releaseAt,\n isAuthoritative: true,\n policySourceId: policy.isStockManaged.policySourceId,\n }\n })\n\n return { byItem }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { computeAvailabilityCached } from "./availabilityCache.js";
|
|
2
|
+
function createWmsAvailabilityProvider(container) {
|
|
3
|
+
return {
|
|
4
|
+
id: "wms",
|
|
5
|
+
async getAvailability(query) {
|
|
6
|
+
const em = container.resolve("em").fork();
|
|
7
|
+
return computeAvailabilityCached(em, container, query);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
createWmsAvailabilityProvider
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=availabilityProvider.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/wms/lib/availabilityProvider.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * `wms`'s `AvailabilityProvider` registration (`id: 'wms'`).\n *\n * @see .ai/specs/2026-08-14-availability-contract.md \u00A73.1, \u00A74.2\n *\n * Registered from `wms/di.ts`'s `register(container)`, which \u2014 like every\n * module DI registrar \u2014 runs once per `createRequestContainer()` call\n * (`packages/shared/src/lib/di/container.ts`), not once at process boot.\n * `availabilityProviderRegistry.register()` is idempotent replace-by-id, so\n * each request's registration simply re-points the shared registry entry at\n * a closure over that request's own container \u2014 the same pattern this\n * module already uses for its plain entity-class registrations in `di.ts`.\n */\n\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AvailabilityProvider, AvailabilityQuery, AvailabilityResult } from '@open-mercato/shared/lib/availability'\nimport { computeAvailabilityCached } from './availabilityCache'\n\ntype Resolver = { resolve: <T = unknown>(name: string) => T }\n\nexport function createWmsAvailabilityProvider(container: Resolver): AvailabilityProvider {\n return {\n id: 'wms',\n async getAvailability(query: AvailabilityQuery): Promise<AvailabilityResult> {\n const em = (container.resolve('em') as EntityManager).fork()\n return computeAvailabilityCached(em, container, query)\n },\n }\n}\n"],
|
|
5
|
+
"mappings": "AAgBA,SAAS,iCAAiC;AAInC,SAAS,8BAA8B,WAA2C;AACvF,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,MAAM,gBAAgB,OAAuD;AAC3E,YAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC3D,aAAO,0BAA0B,IAAI,WAAW,KAAK;AAAA,IACvD;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
function buildSqlInClause(column, values) {
|
|
2
|
+
if (values.length === 0) return { sql: `${column} IN (NULL)`, params: [] };
|
|
3
|
+
const placeholders = values.map(() => "?").join(", ");
|
|
4
|
+
return { sql: `${column} IN (${placeholders})`, params: [...values] };
|
|
5
|
+
}
|
|
6
|
+
export {
|
|
7
|
+
buildSqlInClause
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=sqlInClause.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/wms/lib/sqlInClause.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Renders an id list into a raw-SQL `IN` clause with one placeholder per member.\n *\n * MikroORM does not bind parameters at the driver level: `AbstractSqlConnection.execute`\n * calls `platform.formatQuery`, which interpolates each value through\n * `BasePostgreSqlPlatform.escape`, and that renders a JavaScript array as a bare\n * comma-separated list. `column = ANY(?)` therefore reaches PostgreSQL as\n * `= ANY('a', 'b')` \u2014 or, for a single-element array, as the bare scalar, which fails\n * with `malformed array literal`. Same fix as the dashboards aggregation builder (#4669)\n * and the staff time-tracking raw queries: one placeholder per member.\n *\n * An empty list yields `IN (NULL)` rather than a dropped clause: a caller that narrowed\n * to nothing must select nothing, never silently widen to every row.\n */\nexport type SqlInClause = { sql: string; params: unknown[] }\n\nexport function buildSqlInClause(column: string, values: readonly string[]): SqlInClause {\n if (values.length === 0) return { sql: `${column} IN (NULL)`, params: [] }\n const placeholders = values.map(() => '?').join(', ')\n return { sql: `${column} IN (${placeholders})`, params: [...values] }\n}\n"],
|
|
5
|
+
"mappings": "AAgBO,SAAS,iBAAiB,QAAgB,QAAwC;AACvF,MAAI,OAAO,WAAW,EAAG,QAAO,EAAE,KAAK,GAAG,MAAM,cAAc,QAAQ,CAAC,EAAE;AACzE,QAAM,eAAe,OAAO,IAAI,MAAM,GAAG,EAAE,KAAK,IAAI;AACpD,SAAO,EAAE,KAAK,GAAG,MAAM,QAAQ,YAAY,KAAK,QAAQ,CAAC,GAAG,MAAM,EAAE;AACtE;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/wms/lib/tryResolve.ts"],
|
|
4
|
+
"sourcesContent": ["/** Per-module local soft-resolve helper \u2014 see `packages/core/AGENTS.md` \u00A7 Cross-Module Coupling. */\nexport function tryResolve<T>(resolver: { resolve: <R = unknown>(name: string) => R }, name: string): T | undefined {\n try {\n return resolver.resolve<T>(name)\n } catch {\n return undefined\n }\n}\n"],
|
|
5
|
+
"mappings": "AACO,SAAS,WAAc,UAAyD,MAA6B;AAClH,MAAI;AACF,WAAO,SAAS,QAAW,IAAI;AAAA,EACjC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|