@unifiedcommerce/core 0.0.4 → 0.1.1

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 (179) hide show
  1. package/dist/auth/auth-schema.d.ts +92 -0
  2. package/dist/auth/auth-schema.d.ts.map +1 -1
  3. package/dist/auth/auth-schema.js +7 -0
  4. package/dist/auth/setup.d.ts.map +1 -1
  5. package/dist/auth/setup.js +3 -1
  6. package/package.json +1 -2
  7. package/src/adapters/console-email.ts +0 -43
  8. package/src/auth/access.ts +0 -187
  9. package/src/auth/auth-schema.ts +0 -131
  10. package/src/auth/middleware.ts +0 -161
  11. package/src/auth/org.ts +0 -41
  12. package/src/auth/permissions.ts +0 -28
  13. package/src/auth/setup.ts +0 -165
  14. package/src/auth/system-actor.ts +0 -19
  15. package/src/auth/types.ts +0 -10
  16. package/src/config/defaults.ts +0 -82
  17. package/src/config/define-config.ts +0 -53
  18. package/src/config/types.ts +0 -299
  19. package/src/generated/plugin-capabilities.d.ts +0 -20
  20. package/src/generated/plugin-manifest.ts +0 -23
  21. package/src/generated/plugin-repositories.d.ts +0 -20
  22. package/src/hooks/checkout-completion.ts +0 -262
  23. package/src/hooks/checkout.ts +0 -677
  24. package/src/hooks/order-emails.ts +0 -62
  25. package/src/index.ts +0 -214
  26. package/src/interfaces/mcp/agent-prompt.ts +0 -174
  27. package/src/interfaces/mcp/context-enrichment.ts +0 -177
  28. package/src/interfaces/mcp/server.ts +0 -617
  29. package/src/interfaces/mcp/transport.ts +0 -68
  30. package/src/interfaces/rest/customer-portal.ts +0 -299
  31. package/src/interfaces/rest/index.ts +0 -74
  32. package/src/interfaces/rest/router.ts +0 -334
  33. package/src/interfaces/rest/routes/admin-jobs.ts +0 -58
  34. package/src/interfaces/rest/routes/audit.ts +0 -50
  35. package/src/interfaces/rest/routes/carts.ts +0 -89
  36. package/src/interfaces/rest/routes/catalog.ts +0 -493
  37. package/src/interfaces/rest/routes/checkout.ts +0 -283
  38. package/src/interfaces/rest/routes/inventory.ts +0 -70
  39. package/src/interfaces/rest/routes/media.ts +0 -86
  40. package/src/interfaces/rest/routes/orders.ts +0 -78
  41. package/src/interfaces/rest/routes/payments.ts +0 -60
  42. package/src/interfaces/rest/routes/pricing.ts +0 -57
  43. package/src/interfaces/rest/routes/promotions.ts +0 -92
  44. package/src/interfaces/rest/routes/search.ts +0 -71
  45. package/src/interfaces/rest/routes/webhooks.ts +0 -46
  46. package/src/interfaces/rest/schemas/admin-jobs.ts +0 -40
  47. package/src/interfaces/rest/schemas/audit.ts +0 -46
  48. package/src/interfaces/rest/schemas/carts.ts +0 -125
  49. package/src/interfaces/rest/schemas/catalog.ts +0 -450
  50. package/src/interfaces/rest/schemas/checkout.ts +0 -66
  51. package/src/interfaces/rest/schemas/customer-portal.ts +0 -195
  52. package/src/interfaces/rest/schemas/inventory.ts +0 -138
  53. package/src/interfaces/rest/schemas/media.ts +0 -75
  54. package/src/interfaces/rest/schemas/orders.ts +0 -104
  55. package/src/interfaces/rest/schemas/pricing.ts +0 -80
  56. package/src/interfaces/rest/schemas/promotions.ts +0 -110
  57. package/src/interfaces/rest/schemas/responses.ts +0 -85
  58. package/src/interfaces/rest/schemas/search.ts +0 -58
  59. package/src/interfaces/rest/schemas/shared.ts +0 -62
  60. package/src/interfaces/rest/schemas/webhooks.ts +0 -68
  61. package/src/interfaces/rest/utils.ts +0 -104
  62. package/src/interfaces/rest/webhook-router.ts +0 -50
  63. package/src/kernel/compensation/executor.ts +0 -61
  64. package/src/kernel/compensation/types.ts +0 -26
  65. package/src/kernel/database/adapter.ts +0 -13
  66. package/src/kernel/database/drizzle-db.ts +0 -56
  67. package/src/kernel/database/migrate.ts +0 -76
  68. package/src/kernel/database/plugin-types.ts +0 -34
  69. package/src/kernel/database/schema.ts +0 -49
  70. package/src/kernel/database/scoped-db.ts +0 -68
  71. package/src/kernel/database/tx-context.ts +0 -46
  72. package/src/kernel/error-mapper.ts +0 -15
  73. package/src/kernel/errors.ts +0 -89
  74. package/src/kernel/factory/repository-factory.ts +0 -242
  75. package/src/kernel/hooks/create-context.ts +0 -43
  76. package/src/kernel/hooks/executor.ts +0 -88
  77. package/src/kernel/hooks/registry.ts +0 -74
  78. package/src/kernel/hooks/types.ts +0 -52
  79. package/src/kernel/http-error.ts +0 -44
  80. package/src/kernel/jobs/adapter.ts +0 -36
  81. package/src/kernel/jobs/drizzle-adapter.ts +0 -58
  82. package/src/kernel/jobs/runner.ts +0 -153
  83. package/src/kernel/jobs/schema.ts +0 -46
  84. package/src/kernel/jobs/types.ts +0 -30
  85. package/src/kernel/local-api.ts +0 -185
  86. package/src/kernel/plugin/manifest.ts +0 -253
  87. package/src/kernel/query/executor.ts +0 -184
  88. package/src/kernel/query/registry.ts +0 -46
  89. package/src/kernel/result.ts +0 -33
  90. package/src/kernel/schema/extra-columns.ts +0 -37
  91. package/src/kernel/service-registry.ts +0 -76
  92. package/src/kernel/service-timing.ts +0 -89
  93. package/src/kernel/state-machine/machine.ts +0 -101
  94. package/src/modules/analytics/drizzle-adapter.ts +0 -426
  95. package/src/modules/analytics/hooks.ts +0 -11
  96. package/src/modules/analytics/models.ts +0 -125
  97. package/src/modules/analytics/repository/index.ts +0 -6
  98. package/src/modules/analytics/service.ts +0 -245
  99. package/src/modules/analytics/types.ts +0 -180
  100. package/src/modules/audit/hooks.ts +0 -78
  101. package/src/modules/audit/schema.ts +0 -33
  102. package/src/modules/audit/service.ts +0 -151
  103. package/src/modules/cart/access.ts +0 -27
  104. package/src/modules/cart/matcher.ts +0 -26
  105. package/src/modules/cart/repository/index.ts +0 -234
  106. package/src/modules/cart/schema.ts +0 -42
  107. package/src/modules/cart/schemas.ts +0 -38
  108. package/src/modules/cart/service.ts +0 -541
  109. package/src/modules/catalog/repository/index.ts +0 -772
  110. package/src/modules/catalog/schema.ts +0 -203
  111. package/src/modules/catalog/schemas.ts +0 -104
  112. package/src/modules/catalog/service.ts +0 -1544
  113. package/src/modules/customers/repository/index.ts +0 -327
  114. package/src/modules/customers/schema.ts +0 -64
  115. package/src/modules/customers/service.ts +0 -171
  116. package/src/modules/fulfillment/repository/index.ts +0 -426
  117. package/src/modules/fulfillment/schema.ts +0 -101
  118. package/src/modules/fulfillment/service.ts +0 -555
  119. package/src/modules/fulfillment/types.ts +0 -59
  120. package/src/modules/inventory/repository/index.ts +0 -509
  121. package/src/modules/inventory/schema.ts +0 -94
  122. package/src/modules/inventory/schemas.ts +0 -38
  123. package/src/modules/inventory/service.ts +0 -490
  124. package/src/modules/media/adapter.ts +0 -17
  125. package/src/modules/media/repository/index.ts +0 -274
  126. package/src/modules/media/schema.ts +0 -41
  127. package/src/modules/media/service.ts +0 -151
  128. package/src/modules/orders/repository/index.ts +0 -287
  129. package/src/modules/orders/schema.ts +0 -66
  130. package/src/modules/orders/service.ts +0 -619
  131. package/src/modules/orders/stale-order-cleanup.ts +0 -76
  132. package/src/modules/organization/service.ts +0 -191
  133. package/src/modules/payments/adapter.ts +0 -47
  134. package/src/modules/payments/repository/index.ts +0 -6
  135. package/src/modules/payments/service.ts +0 -107
  136. package/src/modules/pricing/repository/index.ts +0 -291
  137. package/src/modules/pricing/schema.ts +0 -71
  138. package/src/modules/pricing/schemas.ts +0 -38
  139. package/src/modules/pricing/service.ts +0 -494
  140. package/src/modules/promotions/repository/index.ts +0 -325
  141. package/src/modules/promotions/schema.ts +0 -62
  142. package/src/modules/promotions/schemas.ts +0 -38
  143. package/src/modules/promotions/service.ts +0 -598
  144. package/src/modules/search/adapter.ts +0 -57
  145. package/src/modules/search/hooks.ts +0 -12
  146. package/src/modules/search/repository/index.ts +0 -6
  147. package/src/modules/search/service.ts +0 -315
  148. package/src/modules/shipping/calculator.ts +0 -188
  149. package/src/modules/shipping/repository/index.ts +0 -6
  150. package/src/modules/shipping/service.ts +0 -51
  151. package/src/modules/tax/adapter.ts +0 -60
  152. package/src/modules/tax/repository/index.ts +0 -6
  153. package/src/modules/tax/service.ts +0 -53
  154. package/src/modules/webhooks/hook.ts +0 -34
  155. package/src/modules/webhooks/repository/index.ts +0 -278
  156. package/src/modules/webhooks/schema.ts +0 -56
  157. package/src/modules/webhooks/service.ts +0 -117
  158. package/src/modules/webhooks/signing.ts +0 -6
  159. package/src/modules/webhooks/ssrf-guard.ts +0 -71
  160. package/src/modules/webhooks/tasks.ts +0 -52
  161. package/src/modules/webhooks/worker.ts +0 -134
  162. package/src/runtime/commerce.ts +0 -145
  163. package/src/runtime/kernel.ts +0 -419
  164. package/src/runtime/logger.ts +0 -36
  165. package/src/runtime/server.ts +0 -349
  166. package/src/runtime/shutdown.ts +0 -43
  167. package/src/test-utils/create-pglite-adapter.ts +0 -129
  168. package/src/test-utils/create-plugin-test-app.ts +0 -128
  169. package/src/test-utils/create-repository-test-harness.ts +0 -16
  170. package/src/test-utils/create-test-config.ts +0 -190
  171. package/src/test-utils/create-test-kernel.ts +0 -7
  172. package/src/test-utils/create-test-plugin-context.ts +0 -75
  173. package/src/test-utils/rest-api-test-utils.ts +0 -265
  174. package/src/test-utils/test-actors.ts +0 -62
  175. package/src/test-utils/typed-hooks.ts +0 -54
  176. package/src/types/commerce-types.ts +0 -34
  177. package/src/utils/id.ts +0 -3
  178. package/src/utils/logger.ts +0 -18
  179. 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>;