@open-mercato/core 0.10.1-develop.7773.1.a41b0f6c03 → 0.10.1-develop.7775.1.2a1f553826
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 +1 -1
- package/dist/modules/cart/lib/promotionContext.js +3 -2
- package/dist/modules/cart/lib/promotionContext.js.map +2 -2
- package/dist/modules/customers/ai-tools.js.map +2 -2
- package/dist/modules/promotions/lib/promotions-service.js +5 -1
- package/dist/modules/promotions/lib/promotions-service.js.map +2 -2
- package/dist/modules/promotions/services/promotion-cache.js +35 -2
- package/dist/modules/promotions/services/promotion-cache.js.map +3 -3
- package/dist/modules/sales/api/documents/factory.js +1 -1
- package/dist/modules/sales/api/documents/factory.js.map +2 -2
- package/dist/modules/sales/api/orders/route.js +5 -4
- package/dist/modules/sales/api/orders/route.js.map +2 -2
- package/dist/modules/sales/api/quotes/route.js +5 -4
- package/dist/modules/sales/api/quotes/route.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +23 -1
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentExtensionSpot.js +26 -0
- package/dist/modules/sales/components/documents/SalesDocumentExtensionSpot.js.map +7 -0
- package/dist/modules/sales/extension-points.js +26 -0
- package/dist/modules/sales/extension-points.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/cart/lib/promotionContext.ts +3 -7
- package/src/modules/catalog/AGENTS.md +2 -2
- package/src/modules/customers/AGENTS.md +2 -2
- package/src/modules/customers/ai-tools.ts +6 -9
- package/src/modules/promotions/lib/promotions-service.ts +5 -3
- package/src/modules/promotions/services/promotion-cache.ts +41 -4
- package/src/modules/sales/api/documents/factory.ts +1 -1
- package/src/modules/sales/api/orders/route.ts +4 -3
- package/src/modules/sales/api/quotes/route.ts +4 -3
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +19 -1
- package/src/modules/sales/components/documents/SalesDocumentExtensionSpot.tsx +44 -0
- package/src/modules/sales/extension-points.ts +26 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { extensionPoints } from "@open-mercato/core/modules/sales/extension-points";
|
|
4
|
+
import { resolveExtensionPointPattern } from "@open-mercato/shared/modules/widgets/extension-points";
|
|
5
|
+
import { InjectionSpot } from "@open-mercato/ui/backend/injection/InjectionSpot";
|
|
6
|
+
function SalesDocumentExtensionSpot({
|
|
7
|
+
surface,
|
|
8
|
+
context,
|
|
9
|
+
data,
|
|
10
|
+
onDataChange
|
|
11
|
+
}) {
|
|
12
|
+
const pattern = surface === "stage-bar" ? extensionPoints.hosts.documentStageBar.pattern : extensionPoints.hosts.documentClosure.pattern;
|
|
13
|
+
return /* @__PURE__ */ jsx(
|
|
14
|
+
InjectionSpot,
|
|
15
|
+
{
|
|
16
|
+
spotId: resolveExtensionPointPattern(pattern, { kind: context.kind }),
|
|
17
|
+
context,
|
|
18
|
+
data,
|
|
19
|
+
onDataChange
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
SalesDocumentExtensionSpot
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=SalesDocumentExtensionSpot.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/sales/components/documents/SalesDocumentExtensionSpot.tsx"],
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport { extensionPoints } from '@open-mercato/core/modules/sales/extension-points'\nimport { resolveExtensionPointPattern } from '@open-mercato/shared/modules/widgets/extension-points'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\n\nexport type SalesDocumentExtensionContext<TRecord extends { id: string }> = {\n kind: 'order' | 'quote'\n documentId: string | null\n record: TRecord | null\n formId: string\n resourceKind: 'sales.order' | 'sales.quote'\n resourceId?: string\n retryLastMutation: () => Promise<boolean>\n organizationId: string | null\n tenantId: string | null\n}\n\ntype SalesDocumentExtensionSpotProps<TRecord extends { id: string }> = {\n surface: 'stage-bar' | 'closure'\n context: SalesDocumentExtensionContext<TRecord>\n data: TRecord\n onDataChange: (data: TRecord) => void\n}\n\nexport function SalesDocumentExtensionSpot<TRecord extends { id: string }>({\n surface,\n context,\n data,\n onDataChange,\n}: SalesDocumentExtensionSpotProps<TRecord>) {\n const pattern = surface === 'stage-bar'\n ? extensionPoints.hosts.documentStageBar.pattern\n : extensionPoints.hosts.documentClosure.pattern\n\n return (\n <InjectionSpot\n spotId={resolveExtensionPointPattern(pattern, { kind: context.kind })}\n context={context}\n data={data}\n onDataChange={onDataChange}\n />\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAoCI;AAlCJ,SAAS,uBAAuB;AAChC,SAAS,oCAAoC;AAC7C,SAAS,qBAAqB;AAqBvB,SAAS,2BAA2D;AAAA,EACzE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA6C;AAC3C,QAAM,UAAU,YAAY,cACxB,gBAAgB,MAAM,iBAAiB,UACvC,gBAAgB,MAAM,gBAAgB;AAE1C,SACE;AAAA,IAAC;AAAA;AAAA,MACC,QAAQ,6BAA6B,SAAS,EAAE,MAAM,QAAQ,KAAK,CAAC;AAAA,MACpE;AAAA,MACA;AAAA,MACA;AAAA;AAAA,EACF;AAEJ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -17,6 +17,32 @@ const extensionPoints = defineModuleExtensionPoints({
|
|
|
17
17
|
supported: ["render-widget"],
|
|
18
18
|
source: "backend/sales/documents/[id]/page.tsx"
|
|
19
19
|
}),
|
|
20
|
+
documentStageBar: injectionExtensionHost({
|
|
21
|
+
family: "detail",
|
|
22
|
+
pattern: "detail:sales.{kind}:stage-bar",
|
|
23
|
+
parameters: {
|
|
24
|
+
kind: { source: "SalesDocumentExtensionContext.kind", pattern: "^(order|quote)$" }
|
|
25
|
+
},
|
|
26
|
+
supported: ["render-widget"],
|
|
27
|
+
source: ["components/documents/SalesDocumentExtensionSpot.tsx", "backend/sales/documents/[id]/page.tsx"],
|
|
28
|
+
contextContract: "SalesDocumentExtensionContext: kind, documentId, record, formId, resourceKind, resourceId, retryLastMutation, organizationId, tenantId",
|
|
29
|
+
dataContract: "Current sales order or quote record; onDataChange updates the in-page record only",
|
|
30
|
+
scopeContract: "organizationId and tenantId from the loaded, server-scoped document; widget writes must enforce their own authorization",
|
|
31
|
+
runtimeContract: "Rendered below the document header; no native stage transition or mutation lifecycle is added"
|
|
32
|
+
}),
|
|
33
|
+
documentClosure: injectionExtensionHost({
|
|
34
|
+
family: "detail",
|
|
35
|
+
pattern: "detail:sales.{kind}:closure",
|
|
36
|
+
parameters: {
|
|
37
|
+
kind: { source: "SalesDocumentExtensionContext.kind", pattern: "^(order|quote)$" }
|
|
38
|
+
},
|
|
39
|
+
supported: ["render-widget"],
|
|
40
|
+
source: ["components/documents/SalesDocumentExtensionSpot.tsx", "backend/sales/documents/[id]/page.tsx"],
|
|
41
|
+
contextContract: "SalesDocumentExtensionContext: kind, documentId, record, formId, resourceKind, resourceId, retryLastMutation, organizationId, tenantId",
|
|
42
|
+
dataContract: "Current sales order or quote record; onDataChange updates the in-page record only",
|
|
43
|
+
scopeContract: "organizationId and tenantId from the loaded, server-scoped document; widget writes must enforce their own authorization",
|
|
44
|
+
runtimeContract: "Rendered below document details for closure extensions; no native closure dialog or mutation lifecycle is added"
|
|
45
|
+
}),
|
|
20
46
|
documentDetail: injectionExtensionHost({
|
|
21
47
|
family: "detail",
|
|
22
48
|
pattern: "sales.document.detail.{kind}:{surface}",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/sales/extension-points.ts"],
|
|
4
|
-
"sourcesContent": ["import { dataTableExtensionHost, defineModuleExtensionPoints, injectionExtensionHost } from '@open-mercato/shared/modules/widgets/extension-points'\n\nexport const extensionPoints = defineModuleExtensionPoints({\n moduleId: 'sales',\n hosts: {\n ordersTable: dataTableExtensionHost({ tableId: 'sales.orders', source: 'components/documents/SalesDocumentsTable.tsx' }),\n quotesTable: dataTableExtensionHost({ tableId: 'sales.quotes', source: 'components/documents/SalesDocumentsTable.tsx' }),\n paymentsTable: dataTableExtensionHost({ tableId: 'sales.payments', source: 'components/documents/PaymentsSection.tsx' }),\n orderItemColumns: injectionExtensionHost({\n family: 'detail',\n spotId: 'data-table:sales.order.items:columns',\n supported: ['column-widget'],\n source: 'components/documents/ItemsSection.tsx',\n }),\n orderShipping: injectionExtensionHost({\n family: 'detail',\n spotId: 'detail:sales.order:shipping',\n supported: ['render-widget'],\n source: 'backend/sales/documents/[id]/page.tsx',\n }),\n documentDetail: injectionExtensionHost({\n family: 'detail',\n pattern: 'sales.document.detail.{kind}:{surface}',\n parameters: {\n kind: { source: 'SalesDocumentKind', pattern: '^(order|quote)$' },\n surface: { source: 'host declaration', pattern: '^(tabs|details)$' },\n },\n supported: ['render-widget'],\n source: 'backend/sales/documents/[id]/page.tsx',\n }),\n },\n})\n\nexport default extensionPoints\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,wBAAwB,6BAA6B,8BAA8B;AAErF,MAAM,kBAAkB,4BAA4B;AAAA,EACzD,UAAU;AAAA,EACV,OAAO;AAAA,IACL,aAAa,uBAAuB,EAAE,SAAS,gBAAgB,QAAQ,+CAA+C,CAAC;AAAA,IACvH,aAAa,uBAAuB,EAAE,SAAS,gBAAgB,QAAQ,+CAA+C,CAAC;AAAA,IACvH,eAAe,uBAAuB,EAAE,SAAS,kBAAkB,QAAQ,2CAA2C,CAAC;AAAA,IACvH,kBAAkB,uBAAuB;AAAA,MACvC,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,eAAe,uBAAuB;AAAA,MACpC,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,gBAAgB,uBAAuB;AAAA,MACrC,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,YAAY;AAAA,QACV,MAAM,EAAE,QAAQ,qBAAqB,SAAS,kBAAkB;AAAA,QAChE,SAAS,EAAE,QAAQ,oBAAoB,SAAS,mBAAmB;AAAA,MACrE;AAAA,MACA,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ;AAAA,IACV,CAAC;AAAA,EACH;AACF,CAAC;AAED,IAAO,2BAAQ;",
|
|
4
|
+
"sourcesContent": ["import { dataTableExtensionHost, defineModuleExtensionPoints, injectionExtensionHost } from '@open-mercato/shared/modules/widgets/extension-points'\n\nexport const extensionPoints = defineModuleExtensionPoints({\n moduleId: 'sales',\n hosts: {\n ordersTable: dataTableExtensionHost({ tableId: 'sales.orders', source: 'components/documents/SalesDocumentsTable.tsx' }),\n quotesTable: dataTableExtensionHost({ tableId: 'sales.quotes', source: 'components/documents/SalesDocumentsTable.tsx' }),\n paymentsTable: dataTableExtensionHost({ tableId: 'sales.payments', source: 'components/documents/PaymentsSection.tsx' }),\n orderItemColumns: injectionExtensionHost({\n family: 'detail',\n spotId: 'data-table:sales.order.items:columns',\n supported: ['column-widget'],\n source: 'components/documents/ItemsSection.tsx',\n }),\n orderShipping: injectionExtensionHost({\n family: 'detail',\n spotId: 'detail:sales.order:shipping',\n supported: ['render-widget'],\n source: 'backend/sales/documents/[id]/page.tsx',\n }),\n documentStageBar: injectionExtensionHost({\n family: 'detail',\n pattern: 'detail:sales.{kind}:stage-bar',\n parameters: {\n kind: { source: 'SalesDocumentExtensionContext.kind', pattern: '^(order|quote)$' },\n },\n supported: ['render-widget'],\n source: ['components/documents/SalesDocumentExtensionSpot.tsx', 'backend/sales/documents/[id]/page.tsx'],\n contextContract: 'SalesDocumentExtensionContext: kind, documentId, record, formId, resourceKind, resourceId, retryLastMutation, organizationId, tenantId',\n dataContract: 'Current sales order or quote record; onDataChange updates the in-page record only',\n scopeContract: 'organizationId and tenantId from the loaded, server-scoped document; widget writes must enforce their own authorization',\n runtimeContract: 'Rendered below the document header; no native stage transition or mutation lifecycle is added',\n }),\n documentClosure: injectionExtensionHost({\n family: 'detail',\n pattern: 'detail:sales.{kind}:closure',\n parameters: {\n kind: { source: 'SalesDocumentExtensionContext.kind', pattern: '^(order|quote)$' },\n },\n supported: ['render-widget'],\n source: ['components/documents/SalesDocumentExtensionSpot.tsx', 'backend/sales/documents/[id]/page.tsx'],\n contextContract: 'SalesDocumentExtensionContext: kind, documentId, record, formId, resourceKind, resourceId, retryLastMutation, organizationId, tenantId',\n dataContract: 'Current sales order or quote record; onDataChange updates the in-page record only',\n scopeContract: 'organizationId and tenantId from the loaded, server-scoped document; widget writes must enforce their own authorization',\n runtimeContract: 'Rendered below document details for closure extensions; no native closure dialog or mutation lifecycle is added',\n }),\n documentDetail: injectionExtensionHost({\n family: 'detail',\n pattern: 'sales.document.detail.{kind}:{surface}',\n parameters: {\n kind: { source: 'SalesDocumentKind', pattern: '^(order|quote)$' },\n surface: { source: 'host declaration', pattern: '^(tabs|details)$' },\n },\n supported: ['render-widget'],\n source: 'backend/sales/documents/[id]/page.tsx',\n }),\n },\n})\n\nexport default extensionPoints\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,wBAAwB,6BAA6B,8BAA8B;AAErF,MAAM,kBAAkB,4BAA4B;AAAA,EACzD,UAAU;AAAA,EACV,OAAO;AAAA,IACL,aAAa,uBAAuB,EAAE,SAAS,gBAAgB,QAAQ,+CAA+C,CAAC;AAAA,IACvH,aAAa,uBAAuB,EAAE,SAAS,gBAAgB,QAAQ,+CAA+C,CAAC;AAAA,IACvH,eAAe,uBAAuB,EAAE,SAAS,kBAAkB,QAAQ,2CAA2C,CAAC;AAAA,IACvH,kBAAkB,uBAAuB;AAAA,MACvC,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,eAAe,uBAAuB;AAAA,MACpC,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,kBAAkB,uBAAuB;AAAA,MACvC,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,YAAY;AAAA,QACV,MAAM,EAAE,QAAQ,sCAAsC,SAAS,kBAAkB;AAAA,MACnF;AAAA,MACA,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ,CAAC,uDAAuD,uCAAuC;AAAA,MACvG,iBAAiB;AAAA,MACjB,cAAc;AAAA,MACd,eAAe;AAAA,MACf,iBAAiB;AAAA,IACnB,CAAC;AAAA,IACD,iBAAiB,uBAAuB;AAAA,MACtC,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,YAAY;AAAA,QACV,MAAM,EAAE,QAAQ,sCAAsC,SAAS,kBAAkB;AAAA,MACnF;AAAA,MACA,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ,CAAC,uDAAuD,uCAAuC;AAAA,MACvG,iBAAiB;AAAA,MACjB,cAAc;AAAA,MACd,eAAe;AAAA,MACf,iBAAiB;AAAA,IACnB,CAAC;AAAA,IACD,gBAAgB,uBAAuB;AAAA,MACrC,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,YAAY;AAAA,QACV,MAAM,EAAE,QAAQ,qBAAqB,SAAS,kBAAkB;AAAA,QAChE,SAAS,EAAE,QAAQ,oBAAoB,SAAS,mBAAmB;AAAA,MACrE;AAAA,MACA,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ;AAAA,IACV,CAAC;AAAA,EACH;AACF,CAAC;AAED,IAAO,2BAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.7775.1.2a1f553826",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -266,16 +266,16 @@
|
|
|
266
266
|
"zod": "^4.4.3"
|
|
267
267
|
},
|
|
268
268
|
"peerDependencies": {
|
|
269
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
270
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
271
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
269
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7775.1.2a1f553826",
|
|
270
|
+
"@open-mercato/shared": "0.10.1-develop.7775.1.2a1f553826",
|
|
271
|
+
"@open-mercato/ui": "0.10.1-develop.7775.1.2a1f553826",
|
|
272
272
|
"react": "^19.0.0",
|
|
273
273
|
"react-dom": "^19.0.0"
|
|
274
274
|
},
|
|
275
275
|
"devDependencies": {
|
|
276
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
277
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
278
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
276
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7775.1.2a1f553826",
|
|
277
|
+
"@open-mercato/shared": "0.10.1-develop.7775.1.2a1f553826",
|
|
278
|
+
"@open-mercato/ui": "0.10.1-develop.7775.1.2a1f553826",
|
|
279
279
|
"@testing-library/dom": "^10.4.1",
|
|
280
280
|
"@testing-library/jest-dom": "^7.0.1",
|
|
281
281
|
"@testing-library/react": "^16.3.3",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TransactionContext } from '@mikro-orm/core'
|
|
1
2
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
3
|
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
3
4
|
import { CatalogProduct, CatalogProductVariant } from '@open-mercato/core/modules/catalog/data/entities'
|
|
@@ -163,7 +164,7 @@ export type BuildPromotionsEffectsPhaseParams = {
|
|
|
163
164
|
export function buildPromotionsEffectsPhase(params: BuildPromotionsEffectsPhaseParams): () => Promise<void> {
|
|
164
165
|
const { em, container, cart, lines, assortmentScope, tenantScope, now, adjustments, warnings, createdGiftLines, removedGiftLines } = params
|
|
165
166
|
|
|
166
|
-
return async () => {
|
|
167
|
+
return () => TransactionContext.create(em, async () => {
|
|
167
168
|
const promotionsService = container.resolve<PromotionsService>('promotionsService')
|
|
168
169
|
const salesCalculationService = container.resolve<SalesCalculationService>('salesCalculationService')
|
|
169
170
|
|
|
@@ -181,11 +182,6 @@ export function buildPromotionsEffectsPhase(params: BuildPromotionsEffectsPhaseP
|
|
|
181
182
|
const codeIdsInUse = Array.from(
|
|
182
183
|
new Set(existingApplications.map((application) => application.codeId).filter((id): id is string => !!id)),
|
|
183
184
|
)
|
|
184
|
-
// A code-id's reservation lives on `promotions`' own tables, not this cart's `em` — so
|
|
185
|
-
// extending it and then re-checking it still exists is two round trips against a
|
|
186
|
-
// DIFFERENT EntityManager (`extendReservation`'s own `resolvePromotionsEm` fork) plus a
|
|
187
|
-
// plain cross-module read on THIS `em`, never a read-after-mutate hazard on this phase's
|
|
188
|
-
// own pending changes (there are none yet at this point in the phase).
|
|
189
185
|
const lapsedCodeIds = new Set<string>()
|
|
190
186
|
for (const codeId of codeIdsInUse) {
|
|
191
187
|
await promotionsService.extendReservation(codeId, cart.id)
|
|
@@ -566,5 +562,5 @@ export function buildPromotionsEffectsPhase(params: BuildPromotionsEffectsPhaseP
|
|
|
566
562
|
// has the full `appliedPromotions` shape to read from, same "found and closed before
|
|
567
563
|
// anyone needed it" style this run's earlier steps use for a half-wired field.
|
|
568
564
|
cart.promotionEffects = result.appliedPromotions
|
|
569
|
-
}
|
|
565
|
+
})
|
|
570
566
|
}
|
|
@@ -112,6 +112,6 @@ Two typed AI agents ship from `ai-agents.ts`. See `/framework/ai-assistant/agent
|
|
|
112
112
|
| Agent ID | Mode | Policy | Purpose |
|
|
113
113
|
|----------|------|--------|---------|
|
|
114
114
|
| `catalog.catalog_assistant` | chat | `read-only` | General operator explorer for products, categories, variants, prices, offers, product media, tags, option schemas, and unit conversions via the base catalog tool pack + general-purpose packs. |
|
|
115
|
-
| `catalog.merchandising_assistant` | chat | `
|
|
115
|
+
| `catalog.merchandising_assistant` | chat | `confirm-required`; tenant overrides may restrict to `read-only`, never escalate | D18 agent: proposes descriptions, attribute extractions, title variants, and price adjustments; whitelisted product, batch, attribute, and media writes require approval. See `ai-agents.ts`. |
|
|
116
116
|
|
|
117
|
-
The merchandising assistant is the
|
|
117
|
+
The merchandising assistant is the D18 demo. `<AiChat agent="catalog.merchandising_assistant" />` is injected via `MerchandisingAssistantSheet.tsx` on `/backend/catalog/products` (see `packages/core/src/modules/catalog/backend/catalog/products/page.tsx`). Mutation tools route through `prepareMutation` + the approval-card contract; confirmed batches refresh the DataTable via `catalog.product.updated` events on the DOM event bridge.
|
|
@@ -125,8 +125,8 @@ This module is the reference implementation for the AI framework. Copy `ai-agent
|
|
|
125
125
|
|
|
126
126
|
| Agent ID | Mode | Policy | Purpose |
|
|
127
127
|
|----------|------|--------|---------|
|
|
128
|
-
| `customers.account_assistant` | chat |
|
|
129
|
-
| `customers.update_deal_stage` | tool (mutation) |
|
|
128
|
+
| `customers.account_assistant` | chat | `confirm-required`; tenant overrides may restrict to `read-only`, never escalate | Explores people, companies, deals, activities, tasks, addresses, tags, and settings; whitelisted stage, comment, and activity writes require approval. See `ai-agents.ts`. |
|
|
129
|
+
| `customers.update_deal_stage` | tool (mutation) | Agent mutation policy applies through `prepareMutation` + the approval card | Moves deals between stages or open/won/lost statuses. Declared in `ai-tools/deals-pack.ts`; blocked when the agent is read-only. |
|
|
130
130
|
| `customers.deal_analyzer` | chat, `executionEngine: 'stream-text'` | `confirm-required` | Multi-step agentic demo that exercises loop primitives from PRs #1856..#1869. Calls `customers.analyze_deals`, ranks deals by health score, then calls `customers.update_deal_stage` for the highest-value stalled deal — loop stops immediately after (loop.stopWhen). `loop.prepareStep` scopes the active tool set by step without overriding the model. It intentionally leaves `defaultProvider` and `defaultModel` unset so runtime resolution uses `OM_AI_PROVIDER` / `OM_AI_MODEL` or tenant/runtime overrides. |
|
|
131
131
|
| `customers.deal_analyzer_tool_loop` | chat, `executionEngine: 'tool-loop-agent'` | `confirm-required` | Sibling agent identical to `customers.deal_analyzer` but dispatched via `ToolLoopAgent`. Proves the mutation-approval gate holds for both execution engines (TC-AI-AGENT-LOOP-006). |
|
|
132
132
|
|
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Module-root AI tool contribution for the customers module
|
|
3
|
-
* (Phase 1 WS-C, Step 3.9 — read-only Phase 1 surface).
|
|
2
|
+
* Module-root AI tool contribution for the customers module.
|
|
4
3
|
*
|
|
5
4
|
* The generator walks every module for a top-level `ai-tools.ts` and takes
|
|
6
5
|
* the default/`aiTools` export as the contribution. This file aggregates the
|
|
7
|
-
*
|
|
8
|
-
* addresses+tags, settings) so they all flow through the existing
|
|
6
|
+
* customers packs (people, companies, deals, activities+tasks,
|
|
7
|
+
* addresses+tags, settings, deal analysis) so they all flow through the existing
|
|
9
8
|
* `ai-tools.generated.ts` pipeline without any generator changes.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `.ai/runs/2026-04-18-ai-framework-unification/step-3.9-checks.md` for the
|
|
15
|
-
* matrix of required features and decisions.
|
|
10
|
+
* Read and mutation tools enforce tenant + organization scoping. Mutations
|
|
11
|
+
* use the pending-action approval contract and command-backed writes;
|
|
12
|
+
* read-only agents cannot expose them. See the framework AI agents guide.
|
|
16
13
|
*/
|
|
17
14
|
import peopleAiTools from './ai-tools/people-pack'
|
|
18
15
|
import companiesAiTools from './ai-tools/companies-pack'
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TransactionContext } from '@mikro-orm/core'
|
|
1
2
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
3
|
import { computeProductScopeKeys, PRODUCT_SCOPE_CATEGORY_PREFIX } from '@open-mercato/core/modules/catalog/lib/productScopeKeys'
|
|
3
4
|
import { Currency } from '@open-mercato/core/modules/currencies/data/entities'
|
|
@@ -91,9 +92,10 @@ function resolvePromotionsServiceScope(container: PromotionsServiceContainer): P
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
function resolvePromotionsEm(container: PromotionsServiceContainer): EntityManager {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
const registeredEm = container.resolve<EntityManager>('em')
|
|
96
|
+
const contextualEm = TransactionContext.getEntityManager(registeredEm.name) as EntityManager | undefined
|
|
97
|
+
const em = contextualEm && contextualEm.getDriver() === registeredEm.getDriver() ? contextualEm : registeredEm
|
|
98
|
+
return em.fork({ keepTransactionContext: true, disableContextResolution: Boolean(contextualEm && contextualEm !== em) })
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
// Step 6.2's five code-service methods need the full `auth` (not just tenant/org scope)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TransactionContext } from '@mikro-orm/core'
|
|
1
2
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
3
|
import { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'
|
|
3
4
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
@@ -284,6 +285,7 @@ export type PromotionCacheContainer = {
|
|
|
284
285
|
}
|
|
285
286
|
|
|
286
287
|
function resolvePromotionCacheStrategy(container: PromotionCacheContainer): CacheStrategy | null {
|
|
288
|
+
if (resolvePromotionEm(container).isInTransaction()) return null
|
|
287
289
|
try {
|
|
288
290
|
return container.resolve<CacheStrategy>('cache') ?? null
|
|
289
291
|
} catch {
|
|
@@ -292,15 +294,21 @@ function resolvePromotionCacheStrategy(container: PromotionCacheContainer): Cach
|
|
|
292
294
|
}
|
|
293
295
|
|
|
294
296
|
function resolvePromotionEm(container: PromotionCacheContainer): EntityManager {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
297
|
+
const registeredEm = container.resolve<EntityManager>('em')
|
|
298
|
+
const contextualEm = TransactionContext.getEntityManager(registeredEm.name) as EntityManager | undefined
|
|
299
|
+
const em = contextualEm && contextualEm.getDriver() === registeredEm.getDriver() ? contextualEm : registeredEm
|
|
300
|
+
return em.fork({ keepTransactionContext: true, disableContextResolution: Boolean(contextualEm && contextualEm !== em) })
|
|
298
301
|
}
|
|
299
302
|
|
|
300
303
|
export class PromotionCacheService {
|
|
301
304
|
constructor(private readonly container: PromotionCacheContainer) {}
|
|
302
305
|
|
|
303
306
|
async getActivePromotions(tenantId: string, organizationId: string): Promise<NormalizedPromotion[]> {
|
|
307
|
+
const em = resolvePromotionEm(this.container)
|
|
308
|
+
if (em.isInTransaction()) {
|
|
309
|
+
const promotions = await this.buildActivePromotionTrees(tenantId, organizationId)
|
|
310
|
+
return this.buildTransactionalUsageSnapshot(em, promotions, tenantId, organizationId)
|
|
311
|
+
}
|
|
304
312
|
const cache = resolvePromotionCacheStrategy(this.container)
|
|
305
313
|
const treeKey = buildPromotionTreeCacheKey(tenantId, organizationId)
|
|
306
314
|
|
|
@@ -317,6 +325,35 @@ export class PromotionCacheService {
|
|
|
317
325
|
)
|
|
318
326
|
}
|
|
319
327
|
|
|
328
|
+
private async buildTransactionalUsageSnapshot(
|
|
329
|
+
em: EntityManager,
|
|
330
|
+
promotions: NormalizedPromotion[],
|
|
331
|
+
tenantId: string,
|
|
332
|
+
organizationId: string,
|
|
333
|
+
): Promise<NormalizedPromotion[]> {
|
|
334
|
+
const budgeted = promotions.filter((promotion) => promotion.maxBudget !== null)
|
|
335
|
+
const currencies = [...new Set(budgeted.map((promotion) => promotion.budgetCurrency).filter((currency): currency is string => !!currency))]
|
|
336
|
+
const totals = new Map<string, string>()
|
|
337
|
+
if (budgeted.length > 0 && currencies.length > 0) {
|
|
338
|
+
const promotionIds = budgeted.map((promotion) => promotion.id)
|
|
339
|
+
const promotionSlots = promotionIds.map(() => '?').join(', ')
|
|
340
|
+
const currencySlots = currencies.map(() => '?').join(', ')
|
|
341
|
+
const rows = await em.execute<Array<{ promotion_id: string; currency: string; total: string }>>(
|
|
342
|
+
`SELECT promotion_id, currency, COALESCE(SUM(total_discount_amount), 0)::text AS total
|
|
343
|
+
FROM promotion_usages
|
|
344
|
+
WHERE promotion_id IN (${promotionSlots}) AND currency IN (${currencySlots})
|
|
345
|
+
AND organization_id = ? AND tenant_id = ? AND reverted_at IS NULL
|
|
346
|
+
GROUP BY promotion_id, currency`,
|
|
347
|
+
[...promotionIds, ...currencies, organizationId, tenantId],
|
|
348
|
+
)
|
|
349
|
+
for (const row of rows) totals.set(`${row.promotion_id}:${row.currency}`, row.total)
|
|
350
|
+
}
|
|
351
|
+
return promotions.map((promotion) => promotion.maxBudget === null ? promotion : freezeOutsideProduction({
|
|
352
|
+
...promotion,
|
|
353
|
+
totalDiscountGranted: totals.get(`${promotion.id}:${promotion.budgetCurrency}`) ?? '0',
|
|
354
|
+
}))
|
|
355
|
+
}
|
|
356
|
+
|
|
320
357
|
// §Cache Build Contract item 4: "read only for budget-capped promotions" — an
|
|
321
358
|
// uncapped promotion's `totalDiscountGranted` is never read back from
|
|
322
359
|
// `promotion_usages`, it stays the inert '0' baked into the tree cache.
|
|
@@ -369,7 +406,7 @@ export class PromotionCacheService {
|
|
|
369
406
|
): Promise<string> {
|
|
370
407
|
if (!budgetCurrency) return '0'
|
|
371
408
|
const em = resolvePromotionEm(this.container)
|
|
372
|
-
const rows = (await em.
|
|
409
|
+
const rows = (await em.execute(
|
|
373
410
|
`SELECT COALESCE(SUM(total_discount_amount), 0)::text AS total
|
|
374
411
|
FROM promotion_usages
|
|
375
412
|
WHERE promotion_id = ? AND organization_id = ? AND tenant_id = ? AND currency = ? AND reverted_at IS NULL`,
|
|
@@ -473,7 +473,7 @@ export function buildDocumentCrudOptions(binding: DocumentBinding) {
|
|
|
473
473
|
indexer: {
|
|
474
474
|
entityType: binding.entityId,
|
|
475
475
|
},
|
|
476
|
-
enrichers:
|
|
476
|
+
enrichers: { entityId: binding.entityId },
|
|
477
477
|
list: {
|
|
478
478
|
schema: listSchema,
|
|
479
479
|
entityId: binding.entityId,
|
|
@@ -3,8 +3,8 @@ import { SalesOrder } from '../../data/entities'
|
|
|
3
3
|
import { E } from '#generated/entities.ids.generated'
|
|
4
4
|
import { buildDocumentCrudOptions, buildDocumentOpenApi } from '../documents/factory'
|
|
5
5
|
|
|
6
|
-
const crud = makeCrudRoute(
|
|
7
|
-
buildDocumentCrudOptions({
|
|
6
|
+
const crud = makeCrudRoute({
|
|
7
|
+
...buildDocumentCrudOptions({
|
|
8
8
|
kind: 'order',
|
|
9
9
|
entity: SalesOrder,
|
|
10
10
|
entityId: E.sales.sales_order,
|
|
@@ -15,7 +15,8 @@ const crud = makeCrudRoute(
|
|
|
15
15
|
manageFeature: 'sales.orders.manage',
|
|
16
16
|
viewFeature: 'sales.orders.view',
|
|
17
17
|
}),
|
|
18
|
-
|
|
18
|
+
enrichers: { entityId: E.sales.sales_order },
|
|
19
|
+
})
|
|
19
20
|
|
|
20
21
|
export const metadata = crud.metadata
|
|
21
22
|
export const GET = crud.GET
|
|
@@ -3,8 +3,8 @@ import { SalesQuote } from '../../data/entities'
|
|
|
3
3
|
import { E } from '#generated/entities.ids.generated'
|
|
4
4
|
import { buildDocumentCrudOptions, buildDocumentOpenApi } from '../documents/factory'
|
|
5
5
|
|
|
6
|
-
const crud = makeCrudRoute(
|
|
7
|
-
buildDocumentCrudOptions({
|
|
6
|
+
const crud = makeCrudRoute({
|
|
7
|
+
...buildDocumentCrudOptions({
|
|
8
8
|
kind: 'quote',
|
|
9
9
|
entity: SalesQuote,
|
|
10
10
|
entityId: E.sales.sales_quote,
|
|
@@ -15,7 +15,8 @@ const crud = makeCrudRoute(
|
|
|
15
15
|
manageFeature: 'sales.quotes.manage',
|
|
16
16
|
viewFeature: 'sales.quotes.view',
|
|
17
17
|
}),
|
|
18
|
-
|
|
18
|
+
enrichers: { entityId: E.sales.sales_quote },
|
|
19
|
+
})
|
|
19
20
|
|
|
20
21
|
export const metadata = crud.metadata
|
|
21
22
|
export const GET = crud.GET
|
|
@@ -52,6 +52,7 @@ import type { AdjustmentRowData } from '@open-mercato/core/modules/sales/compone
|
|
|
52
52
|
import { SalesShipmentsSection } from '@open-mercato/core/modules/sales/components/documents/ShipmentsSection'
|
|
53
53
|
import { SalesReturnsSection } from '@open-mercato/core/modules/sales/components/documents/ReturnsSection'
|
|
54
54
|
import { DocumentTotals } from '@open-mercato/core/modules/sales/components/documents/DocumentTotals'
|
|
55
|
+
import { SalesDocumentExtensionSpot, type SalesDocumentExtensionContext } from '@open-mercato/core/modules/sales/components/documents/SalesDocumentExtensionSpot'
|
|
55
56
|
import { E } from '#generated/entities.ids.generated'
|
|
56
57
|
import type { DictionarySelectLabels } from '@open-mercato/core/modules/dictionaries/components/DictionaryEntrySelect'
|
|
57
58
|
import { useCurrencyDictionary } from '@open-mercato/core/modules/customers/components/detail/hooks/useCurrencyDictionary'
|
|
@@ -862,6 +863,8 @@ type StatusOption = {
|
|
|
862
863
|
|
|
863
864
|
type DocumentRecord = {
|
|
864
865
|
id: string
|
|
866
|
+
organizationId?: string | null
|
|
867
|
+
tenantId?: string | null
|
|
865
868
|
orderNumber?: string | null
|
|
866
869
|
quoteNumber?: string | null
|
|
867
870
|
status?: string | null
|
|
@@ -1992,14 +1995,17 @@ export default function SalesDocumentDetailPage({
|
|
|
1992
1995
|
contextId: mutationContextId,
|
|
1993
1996
|
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
1994
1997
|
})
|
|
1995
|
-
const detailInjectionContext = React.useMemo(
|
|
1998
|
+
const detailInjectionContext = React.useMemo<SalesDocumentExtensionContext<DocumentRecord>>(
|
|
1996
1999
|
() => ({
|
|
1997
2000
|
kind,
|
|
2001
|
+
documentId: record?.id ?? null,
|
|
1998
2002
|
record,
|
|
1999
2003
|
formId: mutationContextId,
|
|
2000
2004
|
resourceKind: `sales.${kind}`,
|
|
2001
2005
|
resourceId: record?.id ?? undefined,
|
|
2002
2006
|
retryLastMutation,
|
|
2007
|
+
organizationId: record?.organizationId ?? null,
|
|
2008
|
+
tenantId: record?.tenantId ?? null,
|
|
2003
2009
|
}),
|
|
2004
2010
|
[kind, mutationContextId, record, retryLastMutation],
|
|
2005
2011
|
)
|
|
@@ -4779,6 +4785,12 @@ export default function SalesDocumentDetailPage({
|
|
|
4779
4785
|
isDeleting={deleting}
|
|
4780
4786
|
deleteLabel={t('sales.documents.detail.delete', 'Delete')}
|
|
4781
4787
|
/>
|
|
4788
|
+
<SalesDocumentExtensionSpot
|
|
4789
|
+
surface="stage-bar"
|
|
4790
|
+
context={detailInjectionContext}
|
|
4791
|
+
data={record}
|
|
4792
|
+
onDataChange={setRecord}
|
|
4793
|
+
/>
|
|
4782
4794
|
<div className="grid gap-4 md:grid-cols-4">
|
|
4783
4795
|
<div className="md:col-span-3">
|
|
4784
4796
|
<CustomerInlineEditor
|
|
@@ -5023,6 +5035,12 @@ export default function SalesDocumentDetailPage({
|
|
|
5023
5035
|
/>
|
|
5024
5036
|
</div>
|
|
5025
5037
|
|
|
5038
|
+
<SalesDocumentExtensionSpot
|
|
5039
|
+
surface="closure"
|
|
5040
|
+
context={detailInjectionContext}
|
|
5041
|
+
data={record}
|
|
5042
|
+
onDataChange={setRecord}
|
|
5043
|
+
/>
|
|
5026
5044
|
<div className="space-y-4">
|
|
5027
5045
|
<CustomDataSection
|
|
5028
5046
|
title={t('sales.documents.detail.customData', 'Custom data')}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { extensionPoints } from '@open-mercato/core/modules/sales/extension-points'
|
|
4
|
+
import { resolveExtensionPointPattern } from '@open-mercato/shared/modules/widgets/extension-points'
|
|
5
|
+
import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
|
|
6
|
+
|
|
7
|
+
export type SalesDocumentExtensionContext<TRecord extends { id: string }> = {
|
|
8
|
+
kind: 'order' | 'quote'
|
|
9
|
+
documentId: string | null
|
|
10
|
+
record: TRecord | null
|
|
11
|
+
formId: string
|
|
12
|
+
resourceKind: 'sales.order' | 'sales.quote'
|
|
13
|
+
resourceId?: string
|
|
14
|
+
retryLastMutation: () => Promise<boolean>
|
|
15
|
+
organizationId: string | null
|
|
16
|
+
tenantId: string | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type SalesDocumentExtensionSpotProps<TRecord extends { id: string }> = {
|
|
20
|
+
surface: 'stage-bar' | 'closure'
|
|
21
|
+
context: SalesDocumentExtensionContext<TRecord>
|
|
22
|
+
data: TRecord
|
|
23
|
+
onDataChange: (data: TRecord) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function SalesDocumentExtensionSpot<TRecord extends { id: string }>({
|
|
27
|
+
surface,
|
|
28
|
+
context,
|
|
29
|
+
data,
|
|
30
|
+
onDataChange,
|
|
31
|
+
}: SalesDocumentExtensionSpotProps<TRecord>) {
|
|
32
|
+
const pattern = surface === 'stage-bar'
|
|
33
|
+
? extensionPoints.hosts.documentStageBar.pattern
|
|
34
|
+
: extensionPoints.hosts.documentClosure.pattern
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<InjectionSpot
|
|
38
|
+
spotId={resolveExtensionPointPattern(pattern, { kind: context.kind })}
|
|
39
|
+
context={context}
|
|
40
|
+
data={data}
|
|
41
|
+
onDataChange={onDataChange}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -18,6 +18,32 @@ export const extensionPoints = defineModuleExtensionPoints({
|
|
|
18
18
|
supported: ['render-widget'],
|
|
19
19
|
source: 'backend/sales/documents/[id]/page.tsx',
|
|
20
20
|
}),
|
|
21
|
+
documentStageBar: injectionExtensionHost({
|
|
22
|
+
family: 'detail',
|
|
23
|
+
pattern: 'detail:sales.{kind}:stage-bar',
|
|
24
|
+
parameters: {
|
|
25
|
+
kind: { source: 'SalesDocumentExtensionContext.kind', pattern: '^(order|quote)$' },
|
|
26
|
+
},
|
|
27
|
+
supported: ['render-widget'],
|
|
28
|
+
source: ['components/documents/SalesDocumentExtensionSpot.tsx', 'backend/sales/documents/[id]/page.tsx'],
|
|
29
|
+
contextContract: 'SalesDocumentExtensionContext: kind, documentId, record, formId, resourceKind, resourceId, retryLastMutation, organizationId, tenantId',
|
|
30
|
+
dataContract: 'Current sales order or quote record; onDataChange updates the in-page record only',
|
|
31
|
+
scopeContract: 'organizationId and tenantId from the loaded, server-scoped document; widget writes must enforce their own authorization',
|
|
32
|
+
runtimeContract: 'Rendered below the document header; no native stage transition or mutation lifecycle is added',
|
|
33
|
+
}),
|
|
34
|
+
documentClosure: injectionExtensionHost({
|
|
35
|
+
family: 'detail',
|
|
36
|
+
pattern: 'detail:sales.{kind}:closure',
|
|
37
|
+
parameters: {
|
|
38
|
+
kind: { source: 'SalesDocumentExtensionContext.kind', pattern: '^(order|quote)$' },
|
|
39
|
+
},
|
|
40
|
+
supported: ['render-widget'],
|
|
41
|
+
source: ['components/documents/SalesDocumentExtensionSpot.tsx', 'backend/sales/documents/[id]/page.tsx'],
|
|
42
|
+
contextContract: 'SalesDocumentExtensionContext: kind, documentId, record, formId, resourceKind, resourceId, retryLastMutation, organizationId, tenantId',
|
|
43
|
+
dataContract: 'Current sales order or quote record; onDataChange updates the in-page record only',
|
|
44
|
+
scopeContract: 'organizationId and tenantId from the loaded, server-scoped document; widget writes must enforce their own authorization',
|
|
45
|
+
runtimeContract: 'Rendered below document details for closure extensions; no native closure dialog or mutation lifecycle is added',
|
|
46
|
+
}),
|
|
21
47
|
documentDetail: injectionExtensionHost({
|
|
22
48
|
family: 'detail',
|
|
23
49
|
pattern: 'sales.document.detail.{kind}:{surface}',
|