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