@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.
- package/README.md +0 -12
- package/docs/tutorials/creating-modules.md +110 -70
- package/docs/tutorials/using-modules.md +58 -8
- package/package.json +11 -16
- package/src/builder/helpers.ts +40 -0
- package/src/builder/index.ts +2 -0
- package/src/builder/register.ts +38 -22
- package/src/config/module-loader.ts +20 -12
- package/src/config/sdk/paths.ts +41 -12
- package/src/config/sdk/wrapper/base.ts +141 -0
- package/src/config/sdk/wrapper/generator.ts +52 -0
- package/src/config/sdk/wrapper/strategies.ts +102 -0
- package/docs/examples/data-models/core/inventory-module.md +0 -230
- package/docs/examples/data-models/core/order-module.md +0 -132
- package/docs/examples/data-models/scenarios/inventory-reservation-scenario.md +0 -73
- package/docs/examples/data-models/scenarios/multi-storefront-order-scenario.md +0 -99
- package/docs/examples/data-models/scenarios/order-payment-status-scenario.md +0 -92
- package/docs/examples/data-models/scenarios/procurement-order-scenario.md +0 -95
- package/src/config/module-registry.ts +0 -22
- package/src/stub-loader/index.ts +0 -3
- package/src/stub-loader/interface.ts +0 -40
|
@@ -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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
};
|
package/src/config/sdk/paths.ts
CHANGED
|
@@ -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
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
16
|
+
export const getModulesReference = async (
|
|
17
|
+
loadedModules: LoadedModules,
|
|
18
|
+
options: GetModulesReferenceOptions = {}
|
|
19
|
+
) => {
|
|
20
|
+
const { basePath = process.cwd(), silent = false } = options;
|
|
11
21
|
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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.
|