@izumisy-tailor/omakase-modules 0.2.0 → 0.4.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.
Files changed (47) hide show
  1. package/README.md +0 -28
  2. package/docs/generated/README.md +35 -0
  3. package/docs/generated/_media/creating-modules.md +471 -0
  4. package/docs/{tutorials → generated/_media}/using-modules.md +69 -20
  5. package/docs/generated/builder/README.md +25 -0
  6. package/docs/generated/builder/functions/defineModule.md +60 -0
  7. package/docs/generated/builder/functions/withModuleConfiguration.md +68 -0
  8. package/docs/generated/builder/type-aliases/AnyDefinedModule.md +57 -0
  9. package/docs/generated/builder/type-aliases/ConfiguredDependencies.md +44 -0
  10. package/docs/generated/builder/type-aliases/ConfiguredModule.md +60 -0
  11. package/docs/generated/builder/type-aliases/DefinedModule.md +93 -0
  12. package/docs/generated/builder/type-aliases/DependencyModules.md +29 -0
  13. package/docs/generated/builder/type-aliases/EmptyDependencies.md +34 -0
  14. package/docs/generated/builder/type-aliases/ModuleBuilder.md +124 -0
  15. package/docs/generated/builder/type-aliases/ModuleBuilderProps.md +42 -0
  16. package/docs/generated/builder/type-aliases/ModuleFactoryContext.md +40 -0
  17. package/docs/generated/builder/type-aliases/TablesFromNames.md +28 -0
  18. package/docs/generated/config/README.md +19 -0
  19. package/docs/generated/config/classes/ModuleLoader.md +128 -0
  20. package/docs/generated/config/functions/loadModules.md +79 -0
  21. package/docs/generated/config/sdk/README.md +16 -0
  22. package/docs/generated/config/sdk/functions/getModulesReference.md +81 -0
  23. package/docs/generated/config/sdk/functions/loadModuleForDev.md +53 -0
  24. package/docs/generated/config/sdk/type-aliases/GetModulesReferenceOptions.md +60 -0
  25. package/docs/generated/config/type-aliases/LoadedModules.md +162 -0
  26. package/docs/generated/modules.md +11 -0
  27. package/package.json +17 -18
  28. package/src/builder/helpers.ts +388 -28
  29. package/src/builder/index.ts +8 -1
  30. package/src/builder/register.ts +38 -25
  31. package/src/config/module-loader.ts +251 -21
  32. package/src/config/sdk/dev-context.ts +82 -0
  33. package/src/config/sdk/index.ts +2 -1
  34. package/src/config/sdk/paths.ts +124 -13
  35. package/src/config/sdk/wrapper/base.ts +185 -0
  36. package/src/config/sdk/wrapper/generator.ts +89 -0
  37. package/src/config/sdk/wrapper/strategies.ts +121 -0
  38. package/docs/examples/data-models/core/inventory-module.md +0 -230
  39. package/docs/examples/data-models/core/order-module.md +0 -132
  40. package/docs/examples/data-models/scenarios/inventory-reservation-scenario.md +0 -73
  41. package/docs/examples/data-models/scenarios/multi-storefront-order-scenario.md +0 -99
  42. package/docs/examples/data-models/scenarios/order-payment-status-scenario.md +0 -92
  43. package/docs/examples/data-models/scenarios/procurement-order-scenario.md +0 -95
  44. package/docs/tutorials/creating-modules.md +0 -256
  45. package/src/config/module-registry.ts +0 -22
  46. package/src/stub-loader/index.ts +0 -3
  47. package/src/stub-loader/interface.ts +0 -40
@@ -0,0 +1,185 @@
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
+ * Module's tsconfig.json should have: "@omakase-modules/config": ["./src/module"]
9
+ */
10
+ export const MODULES_IMPORT_PATH = "@omakase-modules/config";
11
+
12
+ /**
13
+ * Relative path from wrapper files to dev-modules.ts for dev context.
14
+ * Wrapper files are at: .tailor-sdk/.omakase/{packageName}/{category}/file.ts
15
+ * dev-modules.ts is at: .tailor-sdk/.omakase/dev-modules.ts
16
+ */
17
+ export const DEV_MODULES_RELATIVE_PATH = "../../dev-modules";
18
+
19
+ /**
20
+ * Category types for wrapper generation
21
+ */
22
+ export type Category = "tailordb" | "resolvers" | "executors";
23
+
24
+ /**
25
+ * Abstract base class for category-specific wrapper generation
26
+ */
27
+ export abstract class WrapperStrategy {
28
+ /**
29
+ * Whether this module is being developed locally (not from node_modules)
30
+ */
31
+ protected readonly isLocalModule: boolean;
32
+
33
+ /**
34
+ * The import path for modules (either alias or relative path for dev context)
35
+ */
36
+ protected readonly modulesImportPath: string;
37
+
38
+ constructor(
39
+ protected readonly packageName: string,
40
+ protected readonly basePath: string,
41
+ /**
42
+ * The package name of the module being developed locally.
43
+ * When set, the strategy will use local src paths for this module
44
+ * and relative import path for dev-modules.ts.
45
+ */
46
+ protected readonly devModuleName?: string
47
+ ) {
48
+ this.isLocalModule = devModuleName === packageName;
49
+ // In dev context, use relative path to auto-generated dev-modules.ts
50
+ // In app context, use the @omakase-modules/config alias
51
+ this.modulesImportPath = devModuleName
52
+ ? DEV_MODULES_RELATIVE_PATH
53
+ : MODULES_IMPORT_PATH;
54
+ }
55
+
56
+ /**
57
+ * The category this strategy handles
58
+ */
59
+ abstract readonly category: Category;
60
+
61
+ /**
62
+ * Filter files to process for this category
63
+ */
64
+ abstract filterFiles(files: string[]): string[];
65
+
66
+ /**
67
+ * Get the module export path for npm packages
68
+ */
69
+ abstract getExportPath(fileName: string): string;
70
+
71
+ /**
72
+ * Get the local export path for the module being developed.
73
+ * Uses @/ alias which maps to src/ in module's tsconfig.
74
+ */
75
+ getLocalExportPath(fileName: string): string {
76
+ return `@/${this.category}/${fileName.replace(/\.ts$/, "")}`;
77
+ }
78
+
79
+ /**
80
+ * Generate the export statements using the factory result.
81
+ * Default implementation exports the result as default export.
82
+ */
83
+ protected generateExports(): Promise<string> {
84
+ return Promise.resolve("export default result;");
85
+ }
86
+
87
+ /**
88
+ * Generate wrapper file content using template method
89
+ *
90
+ * The common header, imports, and factory call are included here.
91
+ */
92
+ async generateContent(moduleExportPath: string): Promise<string> {
93
+ const exports = await this.generateExports();
94
+
95
+ return dedent`
96
+ /**
97
+ * Auto-generated wrapper file by @izumisy-tailor/omakase-modules
98
+ * DO NOT EDIT THIS FILE MANUALLY
99
+ *
100
+ * This file calls the module's factory function with the app's loadModules result,
101
+ * ensuring proper configuration injection and avoiding tree-shaking issues.
102
+ */
103
+
104
+ // Import the loadModules result from the app's modules.ts (or auto-generated dev-modules.ts)
105
+ import modules from "${this.modulesImportPath}";
106
+
107
+ // Import the factory function from the module
108
+ import createFactory from "${moduleExportPath}";
109
+
110
+ // Call the factory with loadModules result
111
+ const result = await createFactory(modules);
112
+
113
+ ${exports}
114
+ `;
115
+ }
116
+
117
+ /**
118
+ * Generate wrapper files for this category
119
+ */
120
+ async generateFiles(wrapperDir: string): Promise<string[]> {
121
+ // Determine source path based on whether this is a local module or a dependency
122
+ const sourcePath = this.isLocalModule
123
+ ? path.join(this.basePath, "src", this.category)
124
+ : path.join(
125
+ this.basePath,
126
+ "node_modules",
127
+ this.packageName,
128
+ "src",
129
+ this.category
130
+ );
131
+
132
+ if (!(await this.exists(sourcePath))) {
133
+ return [];
134
+ }
135
+
136
+ const allFiles = await fs.readdir(sourcePath);
137
+ const files = this.filterFiles(allFiles);
138
+ const wrapperPaths: string[] = [];
139
+
140
+ for (const file of files) {
141
+ const wrapperPath = await this.generateFile(wrapperDir, file);
142
+ wrapperPaths.push(wrapperPath);
143
+ }
144
+
145
+ return wrapperPaths;
146
+ }
147
+
148
+ /**
149
+ * Generate a single wrapper file for this category
150
+ */
151
+ private async generateFile(
152
+ wrapperDir: string,
153
+ fileName: string
154
+ ): Promise<string> {
155
+ const categoryWrapperDir = path.join(
156
+ wrapperDir,
157
+ this.packageName,
158
+ this.category
159
+ );
160
+ await fs.mkdir(categoryWrapperDir, { recursive: true });
161
+
162
+ const wrapperFilePath = path.join(categoryWrapperDir, fileName);
163
+ // Use local path for the module being developed, npm path for dependencies
164
+ const moduleExportPath = this.isLocalModule
165
+ ? this.getLocalExportPath(fileName)
166
+ : this.getExportPath(fileName);
167
+ const content = await this.generateContent(moduleExportPath);
168
+
169
+ await fs.writeFile(wrapperFilePath, content, "utf-8");
170
+
171
+ return wrapperFilePath;
172
+ }
173
+
174
+ /**
175
+ * Check if a path exists
176
+ */
177
+ private async exists(filePath: string): Promise<boolean> {
178
+ try {
179
+ await fs.access(filePath);
180
+ return true;
181
+ } catch {
182
+ return false;
183
+ }
184
+ }
185
+ }
@@ -0,0 +1,89 @@
1
+ import dedent from "dedent";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import type { LoadedModules } from "../../module-loader";
5
+ import { createStrategy } from "./strategies";
6
+
7
+ /**
8
+ * Directory where wrapper files are generated
9
+ */
10
+ export const OMAKASE_WRAPPER_DIR = ".tailor-sdk/.omakase";
11
+
12
+ /**
13
+ * Generates the dev-modules.ts file for module development.
14
+ * This file is auto-generated and imports the module via @omakase-modules/config alias,
15
+ * then calls loadModuleForDev to create the LoadedModules context.
16
+ */
17
+ const generateDevModulesFile = async (wrapperDir: string) => {
18
+ const devModulesPath = path.join(wrapperDir, "dev-modules.ts");
19
+
20
+ const content = dedent`
21
+ /**
22
+ * Auto-generated dev-modules file by @izumisy-tailor/omakase-modules
23
+ * DO NOT EDIT THIS FILE MANUALLY
24
+ *
25
+ * This file imports the module definition and creates a LoadedModules context
26
+ * for wrapper files to use during module development.
27
+ */
28
+ import { loadModuleForDev } from "@izumisy-tailor/omakase-modules/config/sdk";
29
+ import module from "@omakase-modules/config";
30
+
31
+ export default loadModuleForDev(module);
32
+ `;
33
+
34
+ await fs.writeFile(devModulesPath, content, "utf-8");
35
+ };
36
+
37
+ /**
38
+ * Generates wrapper files for each module's tailordb, executor, and resolver files.
39
+ * These wrappers import the factory functions from modules and call them with
40
+ * the loadModules result from the app's modules.ts (or dev-modules.ts for module development).
41
+ */
42
+ export const generateWrapperFiles = async (
43
+ loadedModules: LoadedModules,
44
+ basePath: string = process.cwd()
45
+ ) => {
46
+ const wrapperDir = path.join(basePath, OMAKASE_WRAPPER_DIR);
47
+ const modulePackageNames = Object.keys(loadedModules.loadedModules);
48
+
49
+ // Clean up existing wrapper directory
50
+ try {
51
+ await fs.access(wrapperDir);
52
+ await fs.rm(wrapperDir, { recursive: true });
53
+ } catch {
54
+ // Directory doesn't exist, no need to clean up
55
+ }
56
+
57
+ // Ensure wrapper directory exists
58
+ await fs.mkdir(wrapperDir, { recursive: true });
59
+
60
+ const result = {
61
+ tailordb: [] as string[],
62
+ resolver: [] as string[],
63
+ executor: [] as string[],
64
+ };
65
+
66
+ // Get dev module name if this is a dev context
67
+ const devModuleName = loadedModules.__devContext?.moduleName;
68
+
69
+ // Generate dev-modules.ts for dev context
70
+ if (devModuleName) {
71
+ await generateDevModulesFile(wrapperDir);
72
+ }
73
+
74
+ for (const packageName of modulePackageNames) {
75
+ const strategyFor = createStrategy(packageName, basePath, devModuleName);
76
+
77
+ result.tailordb.push(
78
+ ...(await strategyFor("tailordb").generateFiles(wrapperDir))
79
+ );
80
+ result.resolver.push(
81
+ ...(await strategyFor("resolvers").generateFiles(wrapperDir))
82
+ );
83
+ result.executor.push(
84
+ ...(await strategyFor("executors").generateFiles(wrapperDir))
85
+ );
86
+ }
87
+
88
+ return result;
89
+ };
@@ -0,0 +1,121 @@
1
+ import { createRequire } from "node:module";
2
+ import path from "node:path";
3
+ import { WrapperStrategy, type Category } from "./base";
4
+
5
+ /**
6
+ * Strategy for tailordb category
7
+ */
8
+ class TailorDBStrategy extends WrapperStrategy {
9
+ readonly category = "tailordb" as const;
10
+
11
+ filterFiles(files: string[]) {
12
+ return files.filter((file) => file === "index.ts");
13
+ }
14
+
15
+ getExportPath() {
16
+ return `${this.packageName}/backend/tailordb`;
17
+ }
18
+
19
+ protected async generateExports() {
20
+ const tableNames = await this.importTableNames();
21
+ return tableNames
22
+ .map((name) => `export const ${name}Table = result.${name};`)
23
+ .join("\n");
24
+ }
25
+
26
+ /**
27
+ * Import tableNames from a module's tailordb/index.ts using dynamic import.
28
+ * Uses createRequire to leverage Node.js module resolution from the app's context,
29
+ * or local src path for the module being developed.
30
+ */
31
+ private async importTableNames(): Promise<readonly string[]> {
32
+ try {
33
+ const modulePath = this.resolveTailorDBModulePath();
34
+ const module = await import(`file://${modulePath}`);
35
+ if (!module.tableNames) {
36
+ console.warn(
37
+ `[warn] tableNames not found in ${
38
+ this.isLocalModule
39
+ ? "local src/tailordb/index.ts"
40
+ : `${this.packageName}/backend/tailordb`
41
+ }. Expected: export const tableNames = [...] as const;`
42
+ );
43
+ return [];
44
+ }
45
+ return module.tableNames;
46
+ } catch (error) {
47
+ console.warn(
48
+ `[warn] Failed to import ${this.packageName}/backend/tailordb:`,
49
+ error
50
+ );
51
+ return [];
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Resolve the module path for tailordb/index.ts.
57
+ * Returns local src path for the module being developed,
58
+ * or uses Node.js module resolution for dependencies.
59
+ */
60
+ private resolveTailorDBModulePath(): string {
61
+ if (this.isLocalModule) {
62
+ return path.join(this.basePath, "src", "tailordb", "index.ts");
63
+ }
64
+
65
+ const appRequire = createRequire(path.join(this.basePath, "package.json"));
66
+ return appRequire.resolve(`${this.packageName}/backend/tailordb`);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Strategy for resolvers category
72
+ */
73
+ class ResolversStrategy extends WrapperStrategy {
74
+ readonly category = "resolvers" as const;
75
+
76
+ filterFiles(files: string[]) {
77
+ return files.filter((file) => file.endsWith(".ts") && file !== "index.ts");
78
+ }
79
+
80
+ getExportPath(fileName: string) {
81
+ const baseName = fileName.replace(/\.ts$/, "");
82
+ return `${this.packageName}/backend/resolvers/${baseName}`;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Strategy for executors category
88
+ */
89
+ class ExecutorsStrategy extends WrapperStrategy {
90
+ readonly category = "executors" as const;
91
+
92
+ filterFiles(files: string[]) {
93
+ return files.filter((file) => file.endsWith(".ts") && file !== "index.ts");
94
+ }
95
+
96
+ getExportPath(fileName: string) {
97
+ const baseName = fileName.replace(/\.ts$/, "");
98
+ return `${this.packageName}/backend/executors/${baseName}`;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Factory to create strategy instance for each category.
104
+ * Returns a function that takes a category and returns a strategy.
105
+ *
106
+ * @param packageName The package name of the module to generate wrappers for
107
+ * @param basePath The base path of the project
108
+ * @param devModuleName The package name of the module being developed locally (from __devContext)
109
+ */
110
+ export const createStrategy =
111
+ (packageName: string, basePath: string, devModuleName?: string) =>
112
+ (category: Category): WrapperStrategy => {
113
+ switch (category) {
114
+ case "tailordb":
115
+ return new TailorDBStrategy(packageName, basePath, devModuleName);
116
+ case "resolvers":
117
+ return new ResolversStrategy(packageName, basePath, devModuleName);
118
+ case "executors":
119
+ return new ExecutorsStrategy(packageName, basePath, devModuleName);
120
+ }
121
+ };
@@ -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.