@izumisy-tailor/omakase-modules 0.2.0 → 0.3.0

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.
@@ -1,9 +1,4 @@
1
1
  import { ConfiguredModule } from "../builder/helpers";
2
- import {
3
- clearModuleRegistry,
4
- getConfiguredModule,
5
- registerConfiguredModules,
6
- } from "./module-registry";
7
2
 
8
3
  /**
9
4
  * Module loader
@@ -40,6 +35,16 @@ export type LoadedModules = {
40
35
  }) => {
41
36
  config: C;
42
37
  };
38
+ /**
39
+ * Get the tables from a dependency module by calling its factory function.
40
+ * This is used when an executor or resolver needs to reference tables from another module.
41
+ *
42
+ * @param factory The factory function exported by the dependency module's tailordb/index.ts
43
+ * @returns The tables created by the factory
44
+ */
45
+ getTables: <T>(
46
+ factory: (loadedModules: LoadedModules) => Promise<T>
47
+ ) => Promise<T>;
43
48
  };
44
49
 
45
50
  /**
@@ -50,12 +55,8 @@ export type LoadedModules = {
50
55
  export const loadModules = (
51
56
  configurator: (loader: ModuleLoader) => ModuleLoader
52
57
  ): LoadedModules => {
53
- clearModuleRegistry();
54
-
55
58
  const emptyLoader = new ModuleLoader();
56
- const modules = registerConfiguredModules(
57
- configurator(emptyLoader)._getModules()
58
- );
59
+ const modules = configurator(emptyLoader)._getModules();
59
60
 
60
61
  const loadedModules = modules.reduce<Record<string, ConfiguredModule<any>>>(
61
62
  (acc, module) => {
@@ -65,12 +66,12 @@ export const loadModules = (
65
66
  {}
66
67
  );
67
68
 
68
- return {
69
+ const loadedModulesResult: LoadedModules = {
69
70
  loadedModules,
70
71
  loadConfig: <C extends Record<string, unknown>>(module: {
71
72
  packageName: string;
72
73
  }) => {
73
- const loadedModule = getConfiguredModule(module.packageName);
74
+ const loadedModule = loadedModules[module.packageName];
74
75
  if (!loadedModule) {
75
76
  throw new Error(
76
77
  `Module "${module.packageName}" has not been configured. Ensure it is added via loadModules.`
@@ -81,5 +82,12 @@ export const loadModules = (
81
82
  config: loadedModule.moduleProps.config as C,
82
83
  };
83
84
  },
85
+ getTables: async <T>(
86
+ factory: (loadedModules: LoadedModules) => Promise<T>
87
+ ): Promise<T> => {
88
+ return factory(loadedModulesResult);
89
+ },
84
90
  };
91
+
92
+ return loadedModulesResult;
85
93
  };
@@ -1,20 +1,49 @@
1
1
  import path from "node:path";
2
2
  import { LoadedModules } from "../module-loader";
3
+ import { generateWrapperFiles, OMAKASE_WRAPPER_DIR } from "./wrapper/generator";
3
4
 
4
- export const getModulesReference = (loadedModules: LoadedModules) => {
5
- const modulePackageNames = Object.keys(loadedModules.loadedModules);
5
+ export type GetModulesReferenceOptions = {
6
+ /**
7
+ * Base path for the application (defaults to process.cwd())
8
+ */
9
+ basePath?: string;
10
+ /**
11
+ * Whether to suppress log output (defaults to false)
12
+ */
13
+ silent?: boolean;
14
+ };
6
15
 
7
- return {
8
- tailordb: modulePackageNames.map((name) =>
9
- path.join("node_modules", name, "src", "tailordb", "*.ts")
10
- ),
16
+ export const getModulesReference = async (
17
+ loadedModules: LoadedModules,
18
+ options: GetModulesReferenceOptions = {}
19
+ ) => {
20
+ const { basePath = process.cwd(), silent = false } = options;
11
21
 
12
- resolver: modulePackageNames.map((name) =>
13
- path.join("node_modules", name, "src", "resolvers", "*.ts")
14
- ),
22
+ // Log loaded modules information
23
+ const modulePackageNames = Object.keys(loadedModules.loadedModules);
24
+ if (!silent) {
25
+ console.log(`[omakase] Loaded ${modulePackageNames.length} module(s):\n`);
26
+ for (const name of modulePackageNames) {
27
+ console.log(` * ${name}`);
28
+ }
29
+ console.log("");
30
+ }
31
+
32
+ // Generate wrapper files and return paths to them
33
+ const wrapperPaths = await generateWrapperFiles(loadedModules, basePath);
15
34
 
16
- executor: modulePackageNames.map((name) =>
17
- path.join("node_modules", name, "src", "executors", "*.ts")
18
- ),
35
+ return {
36
+ tailordb:
37
+ wrapperPaths.tailordb.length > 0
38
+ ? [path.join(OMAKASE_WRAPPER_DIR, "*", "tailordb", "*.ts")]
39
+ : [],
40
+ resolver:
41
+ wrapperPaths.resolver.length > 0
42
+ ? [path.join(OMAKASE_WRAPPER_DIR, "*", "resolvers", "*.ts")]
43
+ : [],
44
+ executor:
45
+ wrapperPaths.executor.length > 0
46
+ ? [path.join(OMAKASE_WRAPPER_DIR, "*", "executors", "*.ts")]
47
+ : [],
19
48
  };
20
49
  };
@@ -0,0 +1,141 @@
1
+ import dedent from "dedent";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ /**
6
+ * Path alias for importing modules config.
7
+ * App's tsconfig.json should have: "@omakase-modules/config": ["./modules"]
8
+ */
9
+ export const MODULES_IMPORT_PATH = "@omakase-modules/config";
10
+
11
+ /**
12
+ * Category types for wrapper generation
13
+ */
14
+ export type Category = "tailordb" | "resolvers" | "executors";
15
+
16
+ /**
17
+ * Abstract base class for category-specific wrapper generation
18
+ */
19
+ export abstract class WrapperStrategy {
20
+ constructor(
21
+ protected readonly packageName: string,
22
+ protected readonly basePath: string
23
+ ) {}
24
+
25
+ /**
26
+ * The category this strategy handles
27
+ */
28
+ abstract readonly category: Category;
29
+
30
+ /**
31
+ * Filter files to process for this category
32
+ */
33
+ abstract filterFiles(files: string[]): string[];
34
+
35
+ /**
36
+ * Get the module export path
37
+ */
38
+ abstract getExportPath(fileName: string): string;
39
+
40
+ /**
41
+ * Generate the export statements using the factory result.
42
+ * Default implementation exports the result as default export.
43
+ */
44
+ protected generateExports(): Promise<string> {
45
+ return Promise.resolve("export default result;");
46
+ }
47
+
48
+ /**
49
+ * Generate wrapper file content using template method
50
+ *
51
+ * The common header, imports, and factory call are included here.
52
+ */
53
+ async generateContent(moduleExportPath: string): Promise<string> {
54
+ const exports = await this.generateExports();
55
+
56
+ return dedent`
57
+ /**
58
+ * Auto-generated wrapper file by @izumisy-tailor/omakase-modules
59
+ * DO NOT EDIT THIS FILE MANUALLY
60
+ *
61
+ * This file calls the module's factory function with the app's loadModules result,
62
+ * ensuring proper configuration injection and avoiding tree-shaking issues.
63
+ */
64
+
65
+ // Import the loadModules result from the app's modules.ts
66
+ import modules from "${MODULES_IMPORT_PATH}";
67
+
68
+ // Import the factory function from the module
69
+ import createFactory from "${moduleExportPath}";
70
+
71
+ // Call the factory with loadModules result
72
+ const result = await createFactory(modules);
73
+
74
+ ${exports}
75
+ `;
76
+ }
77
+
78
+ /**
79
+ * Generate wrapper files for this category
80
+ */
81
+ async generateFiles(wrapperDir: string): Promise<string[]> {
82
+ const sourcePath = path.join(
83
+ this.basePath,
84
+ "node_modules",
85
+ this.packageName,
86
+ "src",
87
+ this.category
88
+ );
89
+
90
+ // Check if the source directory exists
91
+ if (!(await this.exists(sourcePath))) {
92
+ return [];
93
+ }
94
+
95
+ const allFiles = await fs.readdir(sourcePath);
96
+ const files = this.filterFiles(allFiles);
97
+ const wrapperPaths: string[] = [];
98
+
99
+ for (const file of files) {
100
+ const wrapperPath = await this.generateFile(wrapperDir, file);
101
+ wrapperPaths.push(wrapperPath);
102
+ }
103
+
104
+ return wrapperPaths;
105
+ }
106
+
107
+ /**
108
+ * Generate a single wrapper file for this category
109
+ */
110
+ private async generateFile(
111
+ wrapperDir: string,
112
+ fileName: string
113
+ ): Promise<string> {
114
+ const categoryWrapperDir = path.join(
115
+ wrapperDir,
116
+ this.packageName,
117
+ this.category
118
+ );
119
+ await fs.mkdir(categoryWrapperDir, { recursive: true });
120
+
121
+ const wrapperFilePath = path.join(categoryWrapperDir, fileName);
122
+ const moduleExportPath = this.getExportPath(fileName);
123
+ const content = await this.generateContent(moduleExportPath);
124
+
125
+ await fs.writeFile(wrapperFilePath, content, "utf-8");
126
+
127
+ return wrapperFilePath;
128
+ }
129
+
130
+ /**
131
+ * Check if a path exists
132
+ */
133
+ private async exists(filePath: string): Promise<boolean> {
134
+ try {
135
+ await fs.access(filePath);
136
+ return true;
137
+ } catch {
138
+ return false;
139
+ }
140
+ }
141
+ }
@@ -0,0 +1,52 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { LoadedModules } from "../../module-loader";
4
+ import { createStrategy } from "./strategies";
5
+
6
+ /**
7
+ * Directory where wrapper files are generated
8
+ */
9
+ export const OMAKASE_WRAPPER_DIR = ".tailor-sdk/.omakase";
10
+
11
+ /**
12
+ * Generates wrapper files for each module's tailordb, executor, and resolver files.
13
+ * These wrappers import the factory functions from modules and call them with
14
+ * the loadModules result from the app's modules.ts.
15
+ */
16
+ export const generateWrapperFiles = async (
17
+ loadedModules: LoadedModules,
18
+ basePath: string = process.cwd()
19
+ ) => {
20
+ const wrapperDir = path.join(basePath, OMAKASE_WRAPPER_DIR);
21
+ const modulePackageNames = Object.keys(loadedModules.loadedModules);
22
+
23
+ // Clean up existing wrapper directory
24
+ try {
25
+ await fs.access(wrapperDir);
26
+ await fs.rm(wrapperDir, { recursive: true });
27
+ } catch {
28
+ // Directory doesn't exist, no need to clean up
29
+ }
30
+
31
+ const result = {
32
+ tailordb: [] as string[],
33
+ resolver: [] as string[],
34
+ executor: [] as string[],
35
+ };
36
+
37
+ for (const packageName of modulePackageNames) {
38
+ const strategyFor = createStrategy(packageName, basePath);
39
+
40
+ result.tailordb.push(
41
+ ...(await strategyFor("tailordb").generateFiles(wrapperDir))
42
+ );
43
+ result.resolver.push(
44
+ ...(await strategyFor("resolvers").generateFiles(wrapperDir))
45
+ );
46
+ result.executor.push(
47
+ ...(await strategyFor("executors").generateFiles(wrapperDir))
48
+ );
49
+ }
50
+
51
+ return result;
52
+ };
@@ -0,0 +1,102 @@
1
+ import { createRequire } from "node:module";
2
+ import path from "node:path";
3
+ import { Category, WrapperStrategy } from "./base";
4
+
5
+ /**
6
+ * Strategy for tailordb category
7
+ */
8
+ class TailorDBStrategy extends WrapperStrategy {
9
+ readonly category = "tailordb" as const;
10
+ filterFiles(files: string[]) {
11
+ return files.filter((file) => file === "index.ts");
12
+ }
13
+
14
+ getExportPath() {
15
+ return `${this.packageName}/backend/tailordb`;
16
+ }
17
+
18
+ protected async generateExports() {
19
+ const tableNames = await this.importTableNames();
20
+ return tableNames
21
+ .map((name) => `export const ${name}Table = result.${name};`)
22
+ .join("\n");
23
+ }
24
+
25
+ /**
26
+ * Import tableNames from a module's tailordb/index.ts using dynamic import.
27
+ * Uses createRequire to leverage Node.js module resolution from the app's context.
28
+ */
29
+ private async importTableNames(): Promise<readonly string[]> {
30
+ try {
31
+ const appRequire = createRequire(
32
+ path.join(this.basePath, "package.json")
33
+ );
34
+ const modulePath = appRequire.resolve(
35
+ `${this.packageName}/backend/tailordb`
36
+ );
37
+ const module = await import(`file://${modulePath}`);
38
+ if (!module.tableNames) {
39
+ console.warn(
40
+ `[warn] tableNames not found in ${this.packageName}/backend/tailordb. Expected: export const tableNames = [...] as const;`
41
+ );
42
+ return [];
43
+ }
44
+ return module.tableNames;
45
+ } catch (error) {
46
+ console.warn(
47
+ `[warn] Failed to import ${this.packageName}/backend/tailordb:`,
48
+ error
49
+ );
50
+ return [];
51
+ }
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Strategy for resolvers category
57
+ */
58
+ class ResolversStrategy extends WrapperStrategy {
59
+ readonly category = "resolvers" as const;
60
+
61
+ filterFiles(files: string[]) {
62
+ return files.filter((file) => file.endsWith(".ts") && file !== "index.ts");
63
+ }
64
+
65
+ getExportPath(fileName: string) {
66
+ const baseName = fileName.replace(/\.ts$/, "");
67
+ return `${this.packageName}/backend/resolvers/${baseName}`;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Strategy for executors category
73
+ */
74
+ class ExecutorsStrategy extends WrapperStrategy {
75
+ readonly category = "executors" as const;
76
+
77
+ filterFiles(files: string[]) {
78
+ return files.filter((file) => file.endsWith(".ts") && file !== "index.ts");
79
+ }
80
+
81
+ getExportPath(fileName: string) {
82
+ const baseName = fileName.replace(/\.ts$/, "");
83
+ return `${this.packageName}/backend/executors/${baseName}`;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Factory to create strategy instance for each category.
89
+ * Returns a function that takes a category and returns a strategy.
90
+ */
91
+ export const createStrategy =
92
+ (packageName: string, basePath: string) =>
93
+ (category: Category): WrapperStrategy => {
94
+ switch (category) {
95
+ case "tailordb":
96
+ return new TailorDBStrategy(packageName, basePath);
97
+ case "resolvers":
98
+ return new ResolversStrategy(packageName, basePath);
99
+ case "executors":
100
+ return new ExecutorsStrategy(packageName, basePath);
101
+ }
102
+ };
@@ -1,230 +0,0 @@
1
- # Inventory Core Contract
2
-
3
- ## Goal
4
- Define the minimal shared layer of inventory management so industry-specific and automation modules can reuse a consistent contract. The aim is to standardise SKU- and location-level quantities, reservations, and fulfilment tasks, while coordinating downstream modules through status history.
5
-
6
- ## Module Boundary
7
- - Persist core metadata for SKU, location, and inventory buckets (for example on-hand, reserved).
8
- - Publish "intent → confirmation" contracts for reservations, stock adjustments, and fulfilment tasks so callers write intents instead of mutating inventory directly.
9
- - Provide `inventory.statusHistory` (exposed as the `inventoryStatusHistory` view where needed) so business modules append their own statuses for auditing and visibility.
10
- - Delegate integrations with external systems (channel sync, procurement, warehouse automation, etc.) to extension modules; the core remains the source of truth for quantities and history.
11
-
12
- ## Published Contracts
13
-
14
- All tables implicitly include an auto-generated `id` primary key; only domain-specific columns are listed below.
15
-
16
- ### `inventory.stock`
17
- Represents the latest stock balance per location.
18
-
19
- | Field | Description |
20
- | --- | --- |
21
- | `skuId` | Identifier of the SKU. |
22
- | `locationId` | Identifier of the warehouse, store, or virtual location. |
23
- | `availableQuantity` | Sellable quantity after subtracting active reservations. |
24
- | `onHandQuantity` | Physical on-hand quantity. |
25
- | `reservedQuantity` | Quantity locked by reservations. |
26
- | `inboundQuantity` | Expected inbound quantity (optional). |
27
- | `outboundQuantity` | Planned outbound quantity (optional). |
28
- | `attributes` | JSON extension point for domain metadata (temperature band, storage condition, etc.). |
29
- | `updatedAt` | Timestamp of the latest update. |
30
-
31
- **Responsibilities**
32
- - Core maintains the balance calculation and consistency; external modules must request changes via intents.
33
- - Extension modules append industry-specific metadata (lot, serial, expiry, etc.) in `attributes`.
34
-
35
- ### `inventory.reservationIntent`
36
- Intent table for requesting an inventory reservation.
37
-
38
- | Field | Description |
39
- | --- | --- |
40
- | `reservationIntentId` | Stable ID generated by the caller (per SKU or line item). |
41
- | `sourceSystem` | Origin of the request (for example `order`, `procurement`, `manual`). |
42
- | `orderId` / `sourceRef` | External reference identifier. |
43
- | `skuId` | SKU to reserve. |
44
- | `quantity` | Quantity requested for reservation. |
45
- | `locationPreference` | Preferred location when stock is distributed. |
46
- | `priority` | Reservation priority so urgent orders can be fulfilled first. |
47
- | `requestedAt` | Timestamp when the request was issued. |
48
- | `payload` | Arbitrary supplemental metadata. |
49
-
50
- **Responsibilities**
51
- - Callers insert intents to request reservation; enforce idempotency with `reservationIntentId` + `sourceSystem`.
52
- - Intent can be updated until confirmed; after confirmation, changes require a new intent.
53
-
54
- ### `inventory.reservation`
55
- Confirmed record that stores the reservation outcome.
56
-
57
- | Field | Description |
58
- | --- | --- |
59
- | `reservationIntentId` | Reference back to the originating intent. |
60
- | `skuId` | SKU that was reserved. |
61
- | `locationId` | Location assigned to fulfil the reservation. |
62
- | `reservedQuantity` | Quantity secured; may differ from requested quantity if partially fulfilled. |
63
- | `status` | Core-defined status such as `confirmed`, `backordered`, `cancelled`. |
64
- | `expiresAt` | Reservation expiry timestamp (optional). |
65
- | `payload` | Supplemental metadata (batch number, operator notes, etc.). |
66
- | `createdAt` / `updatedAt` | Creation and update timestamps. |
67
-
68
- **Responsibilities**
69
- - Core finalises the intent, writes status updates to `inventory.statusHistory`, and emits CDC events.
70
- - Callers read the outcome and handle follow-up actions such as backorders when short allocations occur.
71
-
72
- ### `inventory.adjustmentIntent`
73
- Intent that requests a stock increase or decrease, used for returns, cycle counts, manual corrections, etc.
74
-
75
- | Field | Description |
76
- | --- | --- |
77
- | `adjustmentIntentId` | Stable ID generated by the caller. |
78
- | `sourceSystem` | Origin of the adjustment request. |
79
- | `skuId` | SKU being adjusted. |
80
- | `locationId` | Location being adjusted. |
81
- | `quantityDelta` | Quantity delta (positive for increment, negative for decrement). |
82
- | `reasonCode` | Reason code (for example `cycle_count`, `return`, `damage`). |
83
- | `payload` | Additional metadata (document number, operator, etc.). |
84
- | `requestedAt` | Timestamp when the intent was created. |
85
-
86
- ### `inventory.stockLedger`
87
- Authoritative event log for confirmed stock movements. Centralises adjustments, inbound receipts, outbound shipments, and any other inventory change.
88
-
89
- | Field | Description |
90
- | --- | --- |
91
- | `sourceType` | Origin of the event (for example `manual`, `procurement`, `fulfillment`). |
92
- | `sourceRef` | Reference `id` (reservation, purchase order, etc.). |
93
- | `skuId` | SKU affected. |
94
- | `locationId` | Location affected. |
95
- | `quantityDelta` | Quantity delta applied. |
96
- | `reasonCode` | Reason for the event (propagated from the intent or assigned by Core). |
97
- | `payload` | Optional extension metadata. |
98
- | `recordedAt` | Timestamp when the event was recorded. |
99
-
100
- `inventory.stockLedger` captures quantitative deltas only; downstream services should pair these entries with `inventory.statusHistory` to understand business context and progression.
101
-
102
- ### `inventory.statusHistory`
103
- Timeline of status transitions emitted by Inventory Core and its extensions.
104
-
105
- | Field | Description |
106
- | --- | --- |
107
- | `entityType` | Scoped entity such as `reservation`, `fulfillmentTask`, `stock`. |
108
- | `entityId` | Auto-generated `id` of the entity whose status changed. |
109
- | `statusCode` | Vocabulary entry (see Inventory Status Vocabulary). |
110
- | `payload` | Optional metadata for UI or audit trails. |
111
- | `writtenBy` | Writer of the status (core executor, extension module, etc.). |
112
- | `writtenAt` | Timestamp when the status was appended. |
113
-
114
- While `inventory.stockLedger` answers "how much stock moved", `inventory.statusHistory` focuses on "what lifecycle state changed" so that UI, workflow, and auditing layers can present human-readable progress independent of quantitative adjustments.
115
-
116
- ### `inventory.fulfillmentTask`
117
- Executable task for picking, packing, and shipping.
118
-
119
- | Field | Description |
120
- | --- | --- |
121
- | `sourceSystem` | System that generated the task (for example `order`, `channel`). |
122
- | `reservationId` | Linked reservation; use a join table when bundling multiple reservations. |
123
- | `skuId` / `quantity` | Item to pick and its quantity. |
124
- | `locationId` | Location from which the item should be picked. |
125
- | `status` | Lifecycle status such as `created`, `picking`, `packed`, `shipped`. |
126
- | `assignee` | Human or automated assignee. |
127
- | `payload` | Additional instructions (packing notes, carrier info, etc.). |
128
- | `updatedAt` | Timestamp of the latest update. |
129
-
130
- ## Core Executors
131
-
132
- | Executor name | Trigger source | Key responsibilities | Current status |
133
- | --- | --- | --- | --- |
134
- | `inventory-bootstrap-product-variant` | `commerce-core.productVariant` created | Seed `inventory.stock` for the new SKU/variant with zeroed quantities and default thresholds. | Implemented (GraphQL mutation) |
135
- | `inventory-reserve-on-order-item-created` | `order.orderItem` created (CDC) | Listen to order-side CDC, enqueue reservation intent, and emit reservation confirmation/backorder statuses without the order module calling Inventory directly. | Stubbed in code; behaviour to be completed |
136
-
137
- > Fulfilment progression (picking → shipped) is typically handled by a warehouse extension executor such as `warehouse-fulfillment-task-progress`, which appends lifecycle statuses and ledger entries once physical operations occur.
138
-
139
- ## Inventory Status Vocabulary
140
-
141
- Statuses below are persisted in `inventory.statusHistory` (or projected as `inventoryStatusHistory`) and streamed via CDC to downstream consumers.
142
-
143
- | Label | Writer | Trigger timing | Notes |
144
- | --- | --- | --- | --- |
145
- | `inventory:reservation_created` | Inventory core | Immediately after receiving a `reservationIntent`; signals reservation processing has started. |
146
- | `inventory:reservation_confirmed` | Inventory core | When stock is secured; use `payload` to surface short allocations. |
147
- | `inventory:reservation_backordered` | Inventory core | When the reservation is pending because available stock is insufficient. |
148
- | `inventory:reservation_cancelled` | Inventory core / caller | When the reservation is cancelled. |
149
- | `inventory:stock_adjusted` | Inventory core | When an `adjustmentIntent` is confirmed and the ledger records the delta. |
150
- | `inventory:fulfillment_task_created` | Inventory core | When a fulfilment task is generated. |
151
- | `inventory:picked` | Inventory core / warehouse module | When picking completes. |
152
- | `inventory:shipped` | Inventory core / warehouse module | When the shipment is handed off. |
153
-
154
- Industry modules follow the `inventory:*` namespace and may add codes such as `inventory:lotted_reserved` or `inventory:expiry_hold` for specialised flows.
155
-
156
- ## Ingestion Pattern
157
- 1. External modules (orders, procurement, manual operations, etc.) write intents to request inventory actions.
158
- 2. Inventory Core executors validate the intents and apply them to the confirmation tables according to availability and business rules (priority, location selection).
159
- 3. When confirmed, Core appends statuses to `inventory.statusHistory` and emits CDC events.
160
- 4. Derived projections (for example latest stock summary, shortage report) refresh as needed.
161
-
162
- ## Downstream Consumption
163
- - Order and channel integration modules read `inventory.reservation` and `inventory.statusHistory` to reflect reservation outcomes and shipping progress in their UIs.
164
- - Procurement confirms inbound receipts through `inventory.stockLedger` and reconciles stock increases.
165
- - Analytics and forecasting modules consume `inventory.stock` alongside `inventory.stockLedger` to evaluate inventory levels and turns.
166
-
167
- ## CDC Flow Overview
168
- ### Scenario: Order Line to Shipped
169
- ```mermaid
170
- sequenceDiagram
171
- autonumber
172
- participant Order as Order Module
173
- participant OrderItem as order.orderItem
174
- participant ReserveExecutor as inventory-reserve-on-order-item-created
175
- participant ReservationIntent as inventory.reservationIntent
176
- participant Reservation as inventory.reservation
177
- participant FulfillmentExecutor as warehouse-fulfillment-task-progress
178
- participant FulfillmentTask as inventory.fulfillmentTask
179
- participant StatusHistory as inventory.statusHistory
180
- participant StockLedger as inventory.stockLedger
181
- participant Stock as inventory.stock
182
-
183
- Order->>OrderItem: persist order line
184
- OrderItem-->>ReserveExecutor: CDC new order item event
185
- ReserveExecutor->>ReservationIntent: create reservation intent (sourceRef = orderItem.id)
186
- ReservationIntent-->>ReserveExecutor: CDC new intent event
187
- ReserveExecutor->>Reservation: persist reservation
188
- ReserveExecutor->>StatusHistory: write reservation_confirmed
189
- ReserveExecutor->>StockLedger: hold quantity delta
190
- StockLedger-->>Stock: CDC projection updates available/on-hand
191
-
192
- FulfillmentExecutor->>FulfillmentTask: create task (reservationId)
193
- FulfillmentExecutor->>StatusHistory: write fulfillment_task_created
194
-
195
- FulfillmentExecutor->>FulfillmentTask: mark picking complete
196
- FulfillmentExecutor->>StatusHistory: write picked
197
-
198
- FulfillmentExecutor->>FulfillmentTask: mark shipped
199
- FulfillmentExecutor->>StatusHistory: write shipped
200
- FulfillmentExecutor->>StockLedger: deduct shipment quantity
201
- StockLedger-->>Stock: CDC projection updates available/on-hand
202
- ```
203
-
204
- The shipping portion of this flow relies on the warehouse extension executor (`warehouse-fulfillment-task-progress`) to reflect physical operations; Inventory Core focuses on intents, reservations, ledger accuracy, and projecting those ledger deltas into `inventory.stock`.
205
-
206
- This scenario keeps the dependency pointing from Inventory back to Order: the order module publishes its own state change (`order.orderItem` row + CDC), and Inventory Core reacts via its executor without requiring an API call from Order into Inventory.
207
-
208
- ## Dependency Guidelines
209
- - Inventory Core is the single source of truth for stock; only Core mutates quantities while other modules issue intents.
210
- - Core depends on shared SKU and location masters (typically from Commerce Core) but does not create reverse dependencies.
211
- - Industry modules extend the contracts, yet Core itself remains free of industry-specific fields or logic.
212
-
213
- ## Versioning & Change Management
214
- - Introduce new optional fields on intent/confirmation tables and allow consumers a migration window.
215
- - Document naming guidelines and semantics whenever status vocabulary expands, and notify downstream modules.
216
- - When breaking changes are unavoidable, ship a parallel contract version (for example `inventory.reservationIntent.v2`).
217
-
218
- ## Operational Considerations
219
- - Provide reconciliation tools (replay/rebuild) so `inventory.stock` can be regenerated from intents and the ledger.
220
- - Define locking strategies at SKU and location granularity to minimise contention.
221
- - Offer batch intents or aggregate APIs for high-volume updates to balance performance and consistency.
222
- - Establish retention and archiving policies for the ledger and status history based on audit requirements.
223
-
224
- ## Extension Points for Industry Modules
225
- - **Lot/serial management**: Attach extension tables with foreign keys such as `lotId` / `serialId` and expose hooks within intent and ledger processing.
226
- - **Quality inspection**: Append quality-related statuses (for example `inventory:quality_hold`) to `inventory.statusHistory` and integrate with warehouse execution modules.
227
- - **Construction and bulky goods**: Extend location hierarchies so locations can represent job sites or delivery vehicles.
228
- - **Healthcare and food**: Track compliance attributes (expiry date, temperature band, etc.) via `attributes` and statuses; implement expiry blocking rules in the extension module.
229
-
230
- With this contract in place, scenario documents (channel integration, procurement, inventory reservation, etc.) can gradually migrate to reference Inventory Core. Industry modules achieve required granularity through schema extensions and additional statuses.