@open-mercato/core 0.10.1-develop.7773.1.a41b0f6c03 → 0.10.1-develop.7774.1.9a13ef7580

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.
@@ -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.7773.1.a41b0f6c03",
3
+ "version": "0.10.1-develop.7774.1.9a13ef7580",
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.7773.1.a41b0f6c03",
270
- "@open-mercato/shared": "0.10.1-develop.7773.1.a41b0f6c03",
271
- "@open-mercato/ui": "0.10.1-develop.7773.1.a41b0f6c03",
269
+ "@open-mercato/ai-assistant": "0.10.1-develop.7774.1.9a13ef7580",
270
+ "@open-mercato/shared": "0.10.1-develop.7774.1.9a13ef7580",
271
+ "@open-mercato/ui": "0.10.1-develop.7774.1.9a13ef7580",
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.7773.1.a41b0f6c03",
277
- "@open-mercato/shared": "0.10.1-develop.7773.1.a41b0f6c03",
278
- "@open-mercato/ui": "0.10.1-develop.7773.1.a41b0f6c03",
276
+ "@open-mercato/ai-assistant": "0.10.1-develop.7774.1.9a13ef7580",
277
+ "@open-mercato/shared": "0.10.1-develop.7774.1.9a13ef7580",
278
+ "@open-mercato/ui": "0.10.1-develop.7774.1.9a13ef7580",
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",
@@ -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 | `read-only` (mutation-capable via per-tenant override unlocking `catalog.update_product`, `catalog.bulk_update_products`, `catalog.apply_attribute_extraction`, `catalog.update_product_media_descriptions`) | D18 demo agent: proposes descriptions, attribute extractions, title variants, and price adjustments for the current selection on the products list page. |
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 Phase 2 D18 demo. `<AiChat agent="catalog.merchandising_assistant" />` is injected via `MerchandisingAssistantSheet.tsx` on `/backend/catalog/catalog/products` (see `packages/core/src/modules/catalog/backend/catalog/products/page.tsx`). Mutation-capable authoring tools route through `prepareMutation` + the approval-card contract; confirmed batches refresh the DataTable via `catalog.product.updated` events on the DOM event bridge.
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 | read-only (mutation-capable via per-tenant override that unlocks `customers.update_deal_stage`; see `packages/core/src/modules/customers/ai-agents.ts` for the whitelist and prompt) | Operator-facing assistant that explores people, companies, deals, activities, tasks, addresses, tags, and settings through the customers tool pack. |
129
- | `customers.update_deal_stage` | tool (mutation) | `destructive-confirm-required` — goes through `prepareMutation` + the approval card | Moves a deal between stages / flips status between open, won, lost. Declared via `defineAiTool` in `ai-tools.ts` and exposed only when the tenant mutation-policy override promotes the agent above read-only. |
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
- * six customers packs (people, companies, deals, activities+tasks,
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
- * Mutation tools are deferred to Step 5.13+ under the pending-action contract;
12
- * every tool here is read-only and enforces tenant + organization scoping via
13
- * the existing encryption helpers. See
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'
@@ -473,7 +473,7 @@ export function buildDocumentCrudOptions(binding: DocumentBinding) {
473
473
  indexer: {
474
474
  entityType: binding.entityId,
475
475
  },
476
- enrichers: binding.kind === 'order' ? { entityId: binding.entityId } : undefined,
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}',