@unifiedcommerce/core 0.1.0 → 0.2.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 (245) hide show
  1. package/dist/auth/setup.d.ts.map +1 -1
  2. package/dist/auth/setup.js +8 -3
  3. package/dist/config/types.d.ts +3 -1
  4. package/dist/config/types.d.ts.map +1 -1
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -0
  8. package/dist/interfaces/mcp/server.d.ts +3 -5
  9. package/dist/interfaces/mcp/server.d.ts.map +1 -1
  10. package/dist/interfaces/mcp/server.js +25 -510
  11. package/dist/interfaces/mcp/tool-builder.d.ts +120 -0
  12. package/dist/interfaces/mcp/tool-builder.d.ts.map +1 -0
  13. package/dist/interfaces/mcp/tool-builder.js +224 -0
  14. package/dist/interfaces/mcp/tools/analytics.d.ts +42 -0
  15. package/dist/interfaces/mcp/tools/analytics.d.ts.map +1 -0
  16. package/dist/interfaces/mcp/tools/analytics.js +70 -0
  17. package/dist/interfaces/mcp/tools/cart.d.ts +14 -0
  18. package/dist/interfaces/mcp/tools/cart.d.ts.map +1 -0
  19. package/dist/interfaces/mcp/tools/cart.js +47 -0
  20. package/dist/interfaces/mcp/tools/catalog.d.ts +53 -0
  21. package/dist/interfaces/mcp/tools/catalog.d.ts.map +1 -0
  22. package/dist/interfaces/mcp/tools/catalog.js +284 -0
  23. package/dist/interfaces/mcp/tools/index.d.ts +3 -0
  24. package/dist/interfaces/mcp/tools/index.d.ts.map +1 -0
  25. package/dist/interfaces/mcp/tools/index.js +20 -0
  26. package/dist/interfaces/mcp/tools/inventory.d.ts +27 -0
  27. package/dist/interfaces/mcp/tools/inventory.d.ts.map +1 -0
  28. package/dist/interfaces/mcp/tools/inventory.js +143 -0
  29. package/dist/interfaces/mcp/tools/orders.d.ts +18 -0
  30. package/dist/interfaces/mcp/tools/orders.d.ts.map +1 -0
  31. package/dist/interfaces/mcp/tools/orders.js +82 -0
  32. package/dist/interfaces/mcp/tools/pricing.d.ts +29 -0
  33. package/dist/interfaces/mcp/tools/pricing.d.ts.map +1 -0
  34. package/dist/interfaces/mcp/tools/pricing.js +90 -0
  35. package/dist/interfaces/mcp/tools/promotions.d.ts +44 -0
  36. package/dist/interfaces/mcp/tools/promotions.d.ts.map +1 -0
  37. package/dist/interfaces/mcp/tools/promotions.js +109 -0
  38. package/dist/interfaces/mcp/tools/registry.d.ts +32 -0
  39. package/dist/interfaces/mcp/tools/registry.d.ts.map +1 -0
  40. package/dist/interfaces/mcp/tools/registry.js +55 -0
  41. package/dist/interfaces/mcp/tools/search.d.ts +14 -0
  42. package/dist/interfaces/mcp/tools/search.d.ts.map +1 -0
  43. package/dist/interfaces/mcp/tools/search.js +39 -0
  44. package/dist/interfaces/mcp/tools/webhooks.d.ts +15 -0
  45. package/dist/interfaces/mcp/tools/webhooks.d.ts.map +1 -0
  46. package/dist/interfaces/mcp/tools/webhooks.js +48 -0
  47. package/dist/interfaces/mcp/transport.d.ts +17 -2
  48. package/dist/interfaces/mcp/transport.d.ts.map +1 -1
  49. package/dist/interfaces/mcp/transport.js +91 -44
  50. package/dist/interfaces/rest/router.d.ts.map +1 -1
  51. package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
  52. package/dist/interfaces/rest/routes/checkout.js +1 -1
  53. package/dist/interfaces/rest/routes/promotions.d.ts.map +1 -1
  54. package/dist/interfaces/rest/routes/promotions.js +3 -2
  55. package/dist/kernel/database/adapter.d.ts +8 -0
  56. package/dist/kernel/database/adapter.d.ts.map +1 -1
  57. package/dist/kernel/factory/repository-factory.d.ts.map +1 -1
  58. package/dist/kernel/factory/repository-factory.js +3 -1
  59. package/dist/kernel/local-api.d.ts.map +1 -1
  60. package/dist/kernel/local-api.js +2 -0
  61. package/dist/kernel/plugin/manifest.d.ts +3 -3
  62. package/dist/kernel/plugin/manifest.d.ts.map +1 -1
  63. package/dist/kernel/plugin/manifest.js +36 -7
  64. package/dist/runtime/kernel.d.ts +1 -2
  65. package/dist/runtime/kernel.d.ts.map +1 -1
  66. package/dist/runtime/kernel.js +16 -8
  67. package/dist/runtime/server.d.ts.map +1 -1
  68. package/dist/runtime/server.js +8 -3
  69. package/dist/test-utils/create-pglite-adapter.d.ts.map +1 -1
  70. package/dist/test-utils/create-pglite-adapter.js +7 -6
  71. package/dist/tsconfig.tsbuildinfo +1 -0
  72. package/package.json +2 -2
  73. package/src/adapters/console-email.ts +0 -43
  74. package/src/auth/access.ts +0 -187
  75. package/src/auth/auth-schema.ts +0 -139
  76. package/src/auth/middleware.ts +0 -161
  77. package/src/auth/org.ts +0 -41
  78. package/src/auth/permissions.ts +0 -28
  79. package/src/auth/setup.ts +0 -169
  80. package/src/auth/system-actor.ts +0 -19
  81. package/src/auth/types.ts +0 -10
  82. package/src/config/defaults.ts +0 -82
  83. package/src/config/define-config.ts +0 -53
  84. package/src/config/types.ts +0 -299
  85. package/src/generated/plugin-capabilities.d.ts +0 -20
  86. package/src/generated/plugin-manifest.ts +0 -23
  87. package/src/generated/plugin-repositories.d.ts +0 -20
  88. package/src/hooks/checkout-completion.ts +0 -262
  89. package/src/hooks/checkout.ts +0 -677
  90. package/src/hooks/order-emails.ts +0 -62
  91. package/src/index.ts +0 -214
  92. package/src/interfaces/mcp/agent-prompt.ts +0 -174
  93. package/src/interfaces/mcp/context-enrichment.ts +0 -177
  94. package/src/interfaces/mcp/server.ts +0 -617
  95. package/src/interfaces/mcp/transport.ts +0 -68
  96. package/src/interfaces/rest/customer-portal.ts +0 -299
  97. package/src/interfaces/rest/index.ts +0 -74
  98. package/src/interfaces/rest/router.ts +0 -334
  99. package/src/interfaces/rest/routes/admin-jobs.ts +0 -58
  100. package/src/interfaces/rest/routes/audit.ts +0 -50
  101. package/src/interfaces/rest/routes/carts.ts +0 -89
  102. package/src/interfaces/rest/routes/catalog.ts +0 -493
  103. package/src/interfaces/rest/routes/checkout.ts +0 -283
  104. package/src/interfaces/rest/routes/inventory.ts +0 -70
  105. package/src/interfaces/rest/routes/media.ts +0 -86
  106. package/src/interfaces/rest/routes/orders.ts +0 -78
  107. package/src/interfaces/rest/routes/payments.ts +0 -60
  108. package/src/interfaces/rest/routes/pricing.ts +0 -57
  109. package/src/interfaces/rest/routes/promotions.ts +0 -92
  110. package/src/interfaces/rest/routes/search.ts +0 -71
  111. package/src/interfaces/rest/routes/webhooks.ts +0 -46
  112. package/src/interfaces/rest/schemas/admin-jobs.ts +0 -40
  113. package/src/interfaces/rest/schemas/audit.ts +0 -46
  114. package/src/interfaces/rest/schemas/carts.ts +0 -125
  115. package/src/interfaces/rest/schemas/catalog.ts +0 -450
  116. package/src/interfaces/rest/schemas/checkout.ts +0 -66
  117. package/src/interfaces/rest/schemas/customer-portal.ts +0 -195
  118. package/src/interfaces/rest/schemas/inventory.ts +0 -138
  119. package/src/interfaces/rest/schemas/media.ts +0 -75
  120. package/src/interfaces/rest/schemas/orders.ts +0 -104
  121. package/src/interfaces/rest/schemas/pricing.ts +0 -80
  122. package/src/interfaces/rest/schemas/promotions.ts +0 -110
  123. package/src/interfaces/rest/schemas/responses.ts +0 -85
  124. package/src/interfaces/rest/schemas/search.ts +0 -58
  125. package/src/interfaces/rest/schemas/shared.ts +0 -62
  126. package/src/interfaces/rest/schemas/webhooks.ts +0 -68
  127. package/src/interfaces/rest/utils.ts +0 -104
  128. package/src/interfaces/rest/webhook-router.ts +0 -50
  129. package/src/kernel/compensation/executor.ts +0 -61
  130. package/src/kernel/compensation/types.ts +0 -26
  131. package/src/kernel/database/adapter.ts +0 -13
  132. package/src/kernel/database/drizzle-db.ts +0 -56
  133. package/src/kernel/database/migrate.ts +0 -76
  134. package/src/kernel/database/plugin-types.ts +0 -34
  135. package/src/kernel/database/schema.ts +0 -49
  136. package/src/kernel/database/scoped-db.ts +0 -68
  137. package/src/kernel/database/tx-context.ts +0 -46
  138. package/src/kernel/error-mapper.ts +0 -15
  139. package/src/kernel/errors.ts +0 -89
  140. package/src/kernel/factory/repository-factory.ts +0 -242
  141. package/src/kernel/hooks/create-context.ts +0 -43
  142. package/src/kernel/hooks/executor.ts +0 -88
  143. package/src/kernel/hooks/registry.ts +0 -74
  144. package/src/kernel/hooks/types.ts +0 -52
  145. package/src/kernel/http-error.ts +0 -44
  146. package/src/kernel/jobs/adapter.ts +0 -36
  147. package/src/kernel/jobs/drizzle-adapter.ts +0 -58
  148. package/src/kernel/jobs/runner.ts +0 -153
  149. package/src/kernel/jobs/schema.ts +0 -46
  150. package/src/kernel/jobs/types.ts +0 -30
  151. package/src/kernel/local-api.ts +0 -185
  152. package/src/kernel/plugin/manifest.ts +0 -253
  153. package/src/kernel/query/executor.ts +0 -184
  154. package/src/kernel/query/registry.ts +0 -46
  155. package/src/kernel/result.ts +0 -33
  156. package/src/kernel/schema/extra-columns.ts +0 -37
  157. package/src/kernel/service-registry.ts +0 -76
  158. package/src/kernel/service-timing.ts +0 -89
  159. package/src/kernel/state-machine/machine.ts +0 -101
  160. package/src/modules/analytics/drizzle-adapter.ts +0 -426
  161. package/src/modules/analytics/hooks.ts +0 -11
  162. package/src/modules/analytics/models.ts +0 -125
  163. package/src/modules/analytics/repository/index.ts +0 -6
  164. package/src/modules/analytics/service.ts +0 -245
  165. package/src/modules/analytics/types.ts +0 -180
  166. package/src/modules/audit/hooks.ts +0 -78
  167. package/src/modules/audit/schema.ts +0 -33
  168. package/src/modules/audit/service.ts +0 -151
  169. package/src/modules/cart/access.ts +0 -27
  170. package/src/modules/cart/matcher.ts +0 -26
  171. package/src/modules/cart/repository/index.ts +0 -234
  172. package/src/modules/cart/schema.ts +0 -42
  173. package/src/modules/cart/schemas.ts +0 -38
  174. package/src/modules/cart/service.ts +0 -541
  175. package/src/modules/catalog/repository/index.ts +0 -772
  176. package/src/modules/catalog/schema.ts +0 -203
  177. package/src/modules/catalog/schemas.ts +0 -104
  178. package/src/modules/catalog/service.ts +0 -1544
  179. package/src/modules/customers/repository/index.ts +0 -327
  180. package/src/modules/customers/schema.ts +0 -64
  181. package/src/modules/customers/service.ts +0 -171
  182. package/src/modules/fulfillment/repository/index.ts +0 -426
  183. package/src/modules/fulfillment/schema.ts +0 -101
  184. package/src/modules/fulfillment/service.ts +0 -555
  185. package/src/modules/fulfillment/types.ts +0 -59
  186. package/src/modules/inventory/repository/index.ts +0 -509
  187. package/src/modules/inventory/schema.ts +0 -94
  188. package/src/modules/inventory/schemas.ts +0 -38
  189. package/src/modules/inventory/service.ts +0 -490
  190. package/src/modules/media/adapter.ts +0 -17
  191. package/src/modules/media/repository/index.ts +0 -274
  192. package/src/modules/media/schema.ts +0 -41
  193. package/src/modules/media/service.ts +0 -151
  194. package/src/modules/orders/repository/index.ts +0 -287
  195. package/src/modules/orders/schema.ts +0 -66
  196. package/src/modules/orders/service.ts +0 -619
  197. package/src/modules/orders/stale-order-cleanup.ts +0 -76
  198. package/src/modules/organization/service.ts +0 -191
  199. package/src/modules/payments/adapter.ts +0 -47
  200. package/src/modules/payments/repository/index.ts +0 -6
  201. package/src/modules/payments/service.ts +0 -107
  202. package/src/modules/pricing/repository/index.ts +0 -291
  203. package/src/modules/pricing/schema.ts +0 -71
  204. package/src/modules/pricing/schemas.ts +0 -38
  205. package/src/modules/pricing/service.ts +0 -494
  206. package/src/modules/promotions/repository/index.ts +0 -325
  207. package/src/modules/promotions/schema.ts +0 -62
  208. package/src/modules/promotions/schemas.ts +0 -38
  209. package/src/modules/promotions/service.ts +0 -598
  210. package/src/modules/search/adapter.ts +0 -57
  211. package/src/modules/search/hooks.ts +0 -12
  212. package/src/modules/search/repository/index.ts +0 -6
  213. package/src/modules/search/service.ts +0 -315
  214. package/src/modules/shipping/calculator.ts +0 -188
  215. package/src/modules/shipping/repository/index.ts +0 -6
  216. package/src/modules/shipping/service.ts +0 -51
  217. package/src/modules/tax/adapter.ts +0 -60
  218. package/src/modules/tax/repository/index.ts +0 -6
  219. package/src/modules/tax/service.ts +0 -53
  220. package/src/modules/webhooks/hook.ts +0 -34
  221. package/src/modules/webhooks/repository/index.ts +0 -278
  222. package/src/modules/webhooks/schema.ts +0 -56
  223. package/src/modules/webhooks/service.ts +0 -117
  224. package/src/modules/webhooks/signing.ts +0 -6
  225. package/src/modules/webhooks/ssrf-guard.ts +0 -71
  226. package/src/modules/webhooks/tasks.ts +0 -52
  227. package/src/modules/webhooks/worker.ts +0 -134
  228. package/src/runtime/commerce.ts +0 -145
  229. package/src/runtime/kernel.ts +0 -419
  230. package/src/runtime/logger.ts +0 -36
  231. package/src/runtime/server.ts +0 -349
  232. package/src/runtime/shutdown.ts +0 -43
  233. package/src/test-utils/create-pglite-adapter.ts +0 -129
  234. package/src/test-utils/create-plugin-test-app.ts +0 -128
  235. package/src/test-utils/create-repository-test-harness.ts +0 -16
  236. package/src/test-utils/create-test-config.ts +0 -190
  237. package/src/test-utils/create-test-kernel.ts +0 -7
  238. package/src/test-utils/create-test-plugin-context.ts +0 -75
  239. package/src/test-utils/rest-api-test-utils.ts +0 -265
  240. package/src/test-utils/test-actors.ts +0 -62
  241. package/src/test-utils/typed-hooks.ts +0 -54
  242. package/src/types/commerce-types.ts +0 -34
  243. package/src/utils/id.ts +0 -3
  244. package/src/utils/logger.ts +0 -18
  245. package/src/utils/pagination.ts +0 -22
@@ -1,509 +0,0 @@
1
- import { eq, and, inArray, isNull, sql } from "drizzle-orm";
2
- import type { TxContext } from "../../../kernel/database/tx-context.js";
3
- import type {
4
- DrizzleDatabase,
5
- DbOrTx,
6
- } from "../../../kernel/database/drizzle-db.js";
7
- import { warehouses, inventoryLevels, inventoryMovements } from "../schema.js";
8
-
9
- // Infer types from Drizzle schema
10
- export type Warehouse = typeof warehouses.$inferSelect;
11
- export type WarehouseInsert = typeof warehouses.$inferInsert;
12
- export type InventoryLevel = typeof inventoryLevels.$inferSelect;
13
- export type InventoryLevelInsert = typeof inventoryLevels.$inferInsert;
14
- export type InventoryMovement = typeof inventoryMovements.$inferSelect;
15
- export type InventoryMovementInsert = typeof inventoryMovements.$inferInsert;
16
-
17
- /**
18
- * InventoryRepository provides type-safe database operations for inventory entities.
19
- *
20
- * This repository manages warehouses, inventory levels, and inventory movements.
21
- * All methods support an optional TxContext parameter for transaction participation.
22
- */
23
- export class InventoryRepository {
24
- constructor(private readonly db: DrizzleDatabase) {}
25
-
26
- private getDb(ctx?: TxContext): DbOrTx {
27
- return (ctx?.tx as DbOrTx | undefined) ?? this.db;
28
- }
29
-
30
- // ─────────────────────────────────────────────────────────────────────────────
31
- // Warehouses
32
- // ─────────────────────────────────────────────────────────────────────────────
33
-
34
- async findWarehouseById(
35
- id: string,
36
- ctx?: TxContext,
37
- ): Promise<Warehouse | undefined> {
38
- const db = this.getDb(ctx);
39
- const rows = await db
40
- .select()
41
- .from(warehouses)
42
- .where(eq(warehouses.id, id));
43
- return rows[0];
44
- }
45
-
46
- async findWarehouseByCode(
47
- orgId: string,
48
- code: string,
49
- ctx?: TxContext,
50
- ): Promise<Warehouse | undefined> {
51
- const db = this.getDb(ctx);
52
- const rows = await db
53
- .select()
54
- .from(warehouses)
55
- .where(
56
- and(
57
- eq(warehouses.organizationId, orgId),
58
- eq(warehouses.code, code),
59
- ),
60
- );
61
- return rows[0];
62
- }
63
-
64
- async findAllWarehouses(
65
- orgId: string,
66
- ctx?: TxContext,
67
- ): Promise<Warehouse[]> {
68
- const db = this.getDb(ctx);
69
- return db
70
- .select()
71
- .from(warehouses)
72
- .where(eq(warehouses.organizationId, orgId));
73
- }
74
-
75
- async findActiveWarehouses(
76
- orgId: string,
77
- ctx?: TxContext,
78
- ): Promise<Warehouse[]> {
79
- const db = this.getDb(ctx);
80
- return db
81
- .select()
82
- .from(warehouses)
83
- .where(
84
- and(
85
- eq(warehouses.organizationId, orgId),
86
- eq(warehouses.isActive, true),
87
- ),
88
- );
89
- }
90
-
91
- async createWarehouse(
92
- data: WarehouseInsert,
93
- ctx?: TxContext,
94
- ): Promise<Warehouse> {
95
- const db = this.getDb(ctx);
96
- const rows = await db.insert(warehouses).values(data).returning();
97
- return rows[0]!;
98
- }
99
-
100
- async updateWarehouse(
101
- id: string,
102
- data: Partial<Omit<WarehouseInsert, "id">>,
103
- ctx?: TxContext,
104
- ): Promise<Warehouse | undefined> {
105
- const db = this.getDb(ctx);
106
- const rows = await db
107
- .update(warehouses)
108
- .set(data)
109
- .where(eq(warehouses.id, id))
110
- .returning();
111
- return rows[0];
112
- }
113
-
114
- async deleteWarehouse(id: string, ctx?: TxContext): Promise<boolean> {
115
- const db = this.getDb(ctx);
116
- const result = await db
117
- .delete(warehouses)
118
- .where(eq(warehouses.id, id))
119
- .returning();
120
- return result.length > 0;
121
- }
122
-
123
- // ─────────────────────────────────────────────────────────────────────────────
124
- // Inventory Levels
125
- // ─────────────────────────────────────────────────────────────────────────────
126
-
127
- async findAllLevels(ctx?: TxContext): Promise<InventoryLevel[]> {
128
- const db = this.getDb(ctx);
129
- return db.select().from(inventoryLevels);
130
- }
131
-
132
- async findLevelById(
133
- id: string,
134
- ctx?: TxContext,
135
- ): Promise<InventoryLevel | undefined> {
136
- const db = this.getDb(ctx);
137
- const rows = await db
138
- .select()
139
- .from(inventoryLevels)
140
- .where(eq(inventoryLevels.id, id));
141
- return rows[0];
142
- }
143
-
144
- async findLevelByKey(
145
- entityId: string,
146
- warehouseId: string,
147
- variantId?: string | null,
148
- ctx?: TxContext,
149
- ): Promise<InventoryLevel | undefined> {
150
- const db = this.getDb(ctx);
151
- const conditions = [
152
- eq(inventoryLevels.entityId, entityId),
153
- eq(inventoryLevels.warehouseId, warehouseId),
154
- ];
155
-
156
- // Only add variantId condition when it's a real string value — never pass null to eq()
157
- if (variantId != null) {
158
- conditions.push(eq(inventoryLevels.variantId, variantId));
159
- }
160
-
161
- const rows = await db
162
- .select()
163
- .from(inventoryLevels)
164
- .where(and(...conditions));
165
-
166
- // Post-filter for exact variantId match (handles SQL NULL correctly)
167
- return rows.find((r) => r.variantId === (variantId ?? null));
168
- }
169
-
170
- async findLevelsByEntityId(
171
- entityId: string,
172
- ctx?: TxContext,
173
- ): Promise<InventoryLevel[]> {
174
- const db = this.getDb(ctx);
175
- return db
176
- .select()
177
- .from(inventoryLevels)
178
- .where(eq(inventoryLevels.entityId, entityId));
179
- }
180
-
181
- async findLevelsByEntityAndVariant(
182
- entityId: string,
183
- variantId?: string | null,
184
- ctx?: TxContext,
185
- ): Promise<InventoryLevel[]> {
186
- const db = this.getDb(ctx);
187
- const conditions = [eq(inventoryLevels.entityId, entityId)];
188
-
189
- // Only add variantId condition when it's a real string value — never pass null to eq()
190
- if (variantId != null) {
191
- conditions.push(eq(inventoryLevels.variantId, variantId));
192
- }
193
-
194
- const rows = await db
195
- .select()
196
- .from(inventoryLevels)
197
- .where(and(...conditions));
198
-
199
- // Post-filter for exact variantId match (handles SQL NULL correctly in JS)
200
- return rows.filter((r) =>
201
- variantId == null ? r.variantId === null : r.variantId === variantId,
202
- );
203
- }
204
-
205
- async findLevelsByWarehouseId(
206
- warehouseId: string,
207
- ctx?: TxContext,
208
- ): Promise<InventoryLevel[]> {
209
- const db = this.getDb(ctx);
210
- return db
211
- .select()
212
- .from(inventoryLevels)
213
- .where(eq(inventoryLevels.warehouseId, warehouseId));
214
- }
215
-
216
- async createLevel(
217
- data: InventoryLevelInsert,
218
- ctx?: TxContext,
219
- ): Promise<InventoryLevel> {
220
- const db = this.getDb(ctx);
221
- const rows = await db.insert(inventoryLevels).values(data).returning();
222
- return rows[0]!;
223
- }
224
-
225
- async updateLevel(
226
- id: string,
227
- data: Partial<Omit<InventoryLevelInsert, "id">>,
228
- ctx?: TxContext,
229
- ): Promise<InventoryLevel | undefined> {
230
- const db = this.getDb(ctx);
231
- const rows = await db
232
- .update(inventoryLevels)
233
- .set({ ...data, updatedAt: new Date() })
234
- .where(eq(inventoryLevels.id, id))
235
- .returning();
236
- return rows[0];
237
- }
238
-
239
- async upsertLevel(
240
- entityId: string,
241
- warehouseId: string,
242
- variantId: string | undefined,
243
- data: Omit<
244
- InventoryLevelInsert,
245
- "id" | "entityId" | "warehouseId" | "variantId"
246
- >,
247
- ctx?: TxContext,
248
- ): Promise<InventoryLevel> {
249
- const existing = await this.findLevelByKey(
250
- entityId,
251
- warehouseId,
252
- variantId,
253
- ctx,
254
- );
255
- if (existing) {
256
- const updated = await this.updateLevel(existing.id, data, ctx);
257
- return updated!;
258
- }
259
- return this.createLevel(
260
- {
261
- ...data,
262
- entityId,
263
- warehouseId,
264
- ...(variantId !== undefined ? { variantId } : {}),
265
- },
266
- ctx,
267
- );
268
- }
269
-
270
- async deleteLevel(id: string, ctx?: TxContext): Promise<boolean> {
271
- const db = this.getDb(ctx);
272
- const result = await db
273
- .delete(inventoryLevels)
274
- .where(eq(inventoryLevels.id, id))
275
- .returning();
276
- return result.length > 0;
277
- }
278
-
279
- // ─────────────────────────────────────────────────────────────────────────────
280
- // Inventory Movements
281
- // ─────────────────────────────────────────────────────────────────────────────
282
-
283
- async findMovementById(
284
- id: string,
285
- ctx?: TxContext,
286
- ): Promise<InventoryMovement | undefined> {
287
- const db = this.getDb(ctx);
288
- const rows = await db
289
- .select()
290
- .from(inventoryMovements)
291
- .where(eq(inventoryMovements.id, id));
292
- return rows[0];
293
- }
294
-
295
- async findMovementsByEntityId(
296
- entityId: string,
297
- ctx?: TxContext,
298
- ): Promise<InventoryMovement[]> {
299
- const db = this.getDb(ctx);
300
- return db
301
- .select()
302
- .from(inventoryMovements)
303
- .where(eq(inventoryMovements.entityId, entityId));
304
- }
305
-
306
- async findMovementsByReference(
307
- referenceType: string,
308
- referenceId: string,
309
- ctx?: TxContext,
310
- ): Promise<InventoryMovement[]> {
311
- const db = this.getDb(ctx);
312
- return db
313
- .select()
314
- .from(inventoryMovements)
315
- .where(
316
- and(
317
- eq(inventoryMovements.referenceType, referenceType),
318
- eq(inventoryMovements.referenceId, referenceId),
319
- ),
320
- );
321
- }
322
-
323
- async createMovement(
324
- data: InventoryMovementInsert,
325
- ctx?: TxContext,
326
- ): Promise<InventoryMovement> {
327
- const db = this.getDb(ctx);
328
- const rows = await db.insert(inventoryMovements).values(data).returning();
329
- return rows[0]!;
330
- }
331
-
332
- // ─────────────────────────────────────────────────────────────────────────────
333
- // Concurrency-Safe Operations (SELECT FOR UPDATE)
334
- // ─────────────────────────────────────────────────────────────────────────────
335
-
336
- /**
337
- * Issues SELECT ... FOR UPDATE on the inventory_levels row matching
338
- * the given entity, variant, and warehouse within the provided transaction.
339
- *
340
- * MUST be called inside an active transaction (ctx.tx must be set).
341
- * Calling outside a transaction provides no locking guarantee.
342
- *
343
- * Uses isNull() for null variantId instead of eq() to generate correct
344
- * SQL (IS NULL instead of = NULL).
345
- */
346
- async findLevelForUpdate(
347
- entityId: string,
348
- variantId: string | null,
349
- warehouseId: string,
350
- ctx: TxContext,
351
- ): Promise<InventoryLevel | undefined> {
352
- const db = this.getDb(ctx);
353
-
354
- const conditions = [
355
- eq(inventoryLevels.entityId, entityId),
356
- eq(inventoryLevels.warehouseId, warehouseId),
357
- variantId != null
358
- ? eq(inventoryLevels.variantId, variantId)
359
- : isNull(inventoryLevels.variantId),
360
- ];
361
-
362
- // Use raw SQL for FOR UPDATE since Drizzle's .for() may not be available
363
- // on all query builder paths. This is the most portable approach.
364
- const rows = await db
365
- .select()
366
- .from(inventoryLevels)
367
- .where(and(...conditions))
368
- .for("update");
369
-
370
- return rows[0];
371
- }
372
-
373
- /**
374
- * Performs a read-modify-write under a row-level lock.
375
- * This is the ONLY correct method for modifying quantityReserved
376
- * in a concurrent environment. Must be called inside a transaction.
377
- *
378
- * The lock is held for the duration of the enclosing transaction,
379
- * which is typically just the checkout reservation — microsecond-level.
380
- */
381
- async reserveWithLock(
382
- entityId: string,
383
- variantId: string | null,
384
- warehouseId: string,
385
- quantity: number,
386
- ctx: TxContext,
387
- ): Promise<
388
- { ok: true; level: InventoryLevel } | { ok: false; reason: string }
389
- > {
390
- const level = await this.findLevelForUpdate(
391
- entityId,
392
- variantId,
393
- warehouseId,
394
- ctx,
395
- );
396
-
397
- if (!level) {
398
- return {
399
- ok: false,
400
- reason: "No inventory record found for this entity.",
401
- };
402
- }
403
-
404
- const available = level.quantityOnHand - level.quantityReserved;
405
- if (available < quantity) {
406
- return {
407
- ok: false,
408
- reason: `Insufficient stock. Available: ${available}, requested: ${quantity}.`,
409
- };
410
- }
411
-
412
- const updated = await this.getDb(ctx)
413
- .update(inventoryLevels)
414
- .set({
415
- quantityReserved: level.quantityReserved + quantity,
416
- updatedAt: new Date(),
417
- version: level.version + 1,
418
- })
419
- .where(eq(inventoryLevels.id, level.id))
420
- .returning();
421
-
422
- return { ok: true, level: updated[0]! };
423
- }
424
-
425
- /**
426
- * Performs a release under a row-level lock, mirroring reserveWithLock.
427
- * Used by compensation chains to undo a reservation.
428
- */
429
- async releaseWithLock(
430
- entityId: string,
431
- variantId: string | null,
432
- warehouseId: string,
433
- quantity: number,
434
- ctx: TxContext,
435
- ): Promise<
436
- { ok: true; level: InventoryLevel } | { ok: false; reason: string }
437
- > {
438
- const level = await this.findLevelForUpdate(
439
- entityId,
440
- variantId,
441
- warehouseId,
442
- ctx,
443
- );
444
-
445
- if (!level) {
446
- return {
447
- ok: false,
448
- reason: "No inventory record found for this entity.",
449
- };
450
- }
451
-
452
- const updated = await this.getDb(ctx)
453
- .update(inventoryLevels)
454
- .set({
455
- quantityReserved: Math.max(0, level.quantityReserved - quantity),
456
- updatedAt: new Date(),
457
- version: level.version + 1,
458
- })
459
- .where(eq(inventoryLevels.id, level.id))
460
- .returning();
461
-
462
- return { ok: true, level: updated[0]! };
463
- }
464
-
465
- // ─────────────────────────────────────────────────────────────────────────────
466
- // Aggregate Queries
467
- // ─────────────────────────────────────────────────────────────────────────────
468
-
469
- async getAvailableQuantity(
470
- entityId: string,
471
- variantId?: string | null,
472
- ctx?: TxContext,
473
- ): Promise<number> {
474
- const levels = await this.findLevelsByEntityAndVariant(
475
- entityId,
476
- variantId,
477
- ctx,
478
- );
479
- return levels.reduce(
480
- (sum, level) => sum + (level.quantityOnHand - level.quantityReserved),
481
- 0,
482
- );
483
- }
484
-
485
- async getAvailableQuantities(
486
- entityIds: string[],
487
- ctx?: TxContext,
488
- ): Promise<Record<string, number>> {
489
- if (entityIds.length === 0) return {};
490
-
491
- const db = this.getDb(ctx);
492
- const rows = await db
493
- .select()
494
- .from(inventoryLevels)
495
- .where(inArray(inventoryLevels.entityId, entityIds));
496
-
497
- const result: Record<string, number> = {};
498
- for (const id of entityIds) {
499
- result[id] = 0;
500
- }
501
-
502
- for (const row of rows) {
503
- const available = row.quantityOnHand - row.quantityReserved;
504
- result[row.entityId] = (result[row.entityId] ?? 0) + available;
505
- }
506
-
507
- return result;
508
- }
509
- }
@@ -1,94 +0,0 @@
1
- import {
2
- boolean,
3
- index,
4
- integer,
5
- jsonb,
6
- pgTable,
7
- text,
8
- timestamp,
9
- uniqueIndex,
10
- uuid,
11
- } from "drizzle-orm/pg-core";
12
- import { organization } from "../../auth/auth-schema.js";
13
- import { sellableEntities, variants } from "../catalog/schema.js";
14
-
15
- export const warehouses = pgTable(
16
- "warehouses",
17
- {
18
- id: uuid("id").defaultRandom().primaryKey(),
19
- organizationId: text("organization_id")
20
- .notNull()
21
- .references(() => organization.id, { onDelete: "cascade" }),
22
- name: text("name").notNull(),
23
- code: text("code").notNull(),
24
- address: jsonb("address").$type<Record<string, unknown>>(),
25
- isActive: boolean("is_active").notNull().default(true),
26
- priority: integer("priority").notNull().default(0),
27
- metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}),
28
- },
29
- (table) => ({
30
- orgIdx: index("idx_warehouses_org").on(table.organizationId),
31
- orgCodeUnique: uniqueIndex("warehouses_org_code_unique").on(table.organizationId, table.code),
32
- }),
33
- );
34
-
35
- export const inventoryLevels = pgTable(
36
- "inventory_levels",
37
- {
38
- id: uuid("id").defaultRandom().primaryKey(),
39
- entityId: uuid("entity_id")
40
- .references(() => sellableEntities.id, { onDelete: "cascade" })
41
- .notNull(),
42
- variantId: uuid("variant_id").references(() => variants.id, {
43
- onDelete: "cascade",
44
- }),
45
- warehouseId: uuid("warehouse_id")
46
- .references(() => warehouses.id)
47
- .notNull(),
48
- quantityOnHand: integer("quantity_on_hand").notNull().default(0),
49
- quantityReserved: integer("quantity_reserved").notNull().default(0),
50
- quantityIncoming: integer("quantity_incoming").notNull().default(0),
51
- unitCost: integer("unit_cost"),
52
- reorderThreshold: integer("reorder_threshold"),
53
- reorderQuantity: integer("reorder_quantity"),
54
- version: integer("version").notNull().default(0),
55
- lastRestockedAt: timestamp("last_restocked_at", { withTimezone: true }),
56
- updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
57
- },
58
- (table) => ({
59
- entityVariantWarehouseIdx: index("idx_inventory_entity_variant_warehouse").on(
60
- table.entityId,
61
- table.variantId,
62
- table.warehouseId,
63
- ),
64
- }),
65
- );
66
-
67
- export const inventoryMovements = pgTable("inventory_movements", {
68
- id: uuid("id").defaultRandom().primaryKey(),
69
- entityId: uuid("entity_id")
70
- .references(() => sellableEntities.id)
71
- .notNull(),
72
- variantId: uuid("variant_id").references(() => variants.id),
73
- warehouseId: uuid("warehouse_id")
74
- .references(() => warehouses.id)
75
- .notNull(),
76
- type: text("type", {
77
- enum: [
78
- "receipt",
79
- "sale",
80
- "return",
81
- "adjustment",
82
- "transfer",
83
- "reservation",
84
- "release",
85
- "fulfillment",
86
- ],
87
- }).notNull(),
88
- quantity: integer("quantity").notNull(),
89
- referenceType: text("reference_type"),
90
- referenceId: text("reference_id"),
91
- reason: text("reason"),
92
- performedBy: text("performed_by").notNull(),
93
- performedAt: timestamp("performed_at", { withTimezone: true }).defaultNow().notNull(),
94
- });
@@ -1,38 +0,0 @@
1
- import { z } from "@hono/zod-openapi";
2
-
3
- // ─── Zod Body Schemas (single source of truth) ─────────────────────────────
4
-
5
- export const InventoryAdjustBodySchema = z.object({
6
- entityId: z.string().openapi({ example: "550e8400-e29b-41d4-a716-446655440000" }),
7
- variantId: z.string().optional().openapi({ example: "variant-uuid" }),
8
- warehouseId: z.string().optional().openapi({ example: "warehouse-uuid" }),
9
- adjustment: z.number().int().refine((v) => v !== 0, { message: "Adjustment cannot be zero" }).openapi({ example: 10 }),
10
- reason: z.string().openapi({ example: "Restock from supplier" }),
11
- performedBy: z.string().optional(),
12
- referenceType: z.string().optional(),
13
- referenceId: z.string().optional(),
14
- }).openapi("InventoryAdjustRequest");
15
-
16
- export const InventoryReserveBodySchema = z.object({
17
- entityId: z.string().openapi({ example: "550e8400-e29b-41d4-a716-446655440000" }),
18
- variantId: z.string().optional(),
19
- warehouseId: z.string().optional(),
20
- quantity: z.number().int().min(1).openapi({ example: 2 }),
21
- orderId: z.string().openapi({ example: "order-uuid" }),
22
- performedBy: z.string().optional(),
23
- }).openapi("InventoryReserveRequest");
24
-
25
- export const InventoryReleaseBodySchema = z.object({
26
- entityId: z.string().openapi({ example: "550e8400-e29b-41d4-a716-446655440000" }),
27
- variantId: z.string().optional(),
28
- warehouseId: z.string().optional(),
29
- quantity: z.number().int().min(1).openapi({ example: 2 }),
30
- orderId: z.string().openapi({ example: "order-uuid" }),
31
- performedBy: z.string().optional(),
32
- }).openapi("InventoryReleaseRequest");
33
-
34
- // ─── Derived Input Types ────────────────────────────────────────────────────
35
-
36
- export type InventoryAdjustInput = z.infer<typeof InventoryAdjustBodySchema>;
37
- export type InventoryReserveInput = z.infer<typeof InventoryReserveBodySchema>;
38
- export type InventoryReleaseInput = z.infer<typeof InventoryReleaseBodySchema>;