@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,619 +0,0 @@
1
- import { resolveOrgId } from "../../auth/org.js";
2
- import { assertOwnership, assertPermission } from "../../auth/permissions.js";
3
- import type { Actor } from "../../auth/types.js";
4
- import {
5
- CommerceInvalidTransitionError,
6
- CommerceNotFoundError,
7
- CommerceValidationError,
8
- toCommerceError,
9
- } from "../../kernel/errors.js";
10
- import { runAfterHooks, runBeforeHooks } from "../../kernel/hooks/executor.js";
11
- import { createHookContext } from "../../kernel/hooks/create-context.js";
12
- import type {
13
- AfterHook,
14
- BeforeHook,
15
- HookContext,
16
- } from "../../kernel/hooks/types.js";
17
- import type { HookRegistry } from "../../kernel/hooks/registry.js";
18
- import {
19
- canTransition,
20
- orderStateMachine,
21
- type OrderState,
22
- type StateDefinition,
23
- } from "../../kernel/state-machine/machine.js";
24
- import { Err, Ok, type Result } from "../../kernel/result.js";
25
- import { createLogger } from "../../utils/logger.js";
26
- import { paginate, type Pagination } from "../../utils/pagination.js";
27
- import type { TxContext } from "../../kernel/database/tx-context.js";
28
- import type { PluginDb } from "../../kernel/database/plugin-types.js";
29
- import {
30
- OrdersRepository,
31
- type Order,
32
- type OrderLineItem,
33
- type OrderStatusHistory,
34
- } from "./repository/index.js";
35
-
36
- export interface CreateOrderInput {
37
- customerId?: string;
38
- currency: string;
39
- subtotal: number;
40
- taxTotal: number;
41
- shippingTotal: number;
42
- discountTotal?: number;
43
- grandTotal: number;
44
- paymentIntentId?: string | undefined;
45
- paymentMethodId?: string | undefined;
46
- metadata?: Record<string, unknown>;
47
- lineItems: Array<{
48
- entityId: string;
49
- entityType: string;
50
- variantId?: string;
51
- sku?: string;
52
- title: string;
53
- quantity: number;
54
- unitPrice: number;
55
- totalPrice: number;
56
- taxAmount?: number;
57
- discountAmount?: number;
58
- metadata?: Record<string, unknown>;
59
- }>;
60
- }
61
-
62
- export interface ListOrdersParams {
63
- page?: number;
64
- limit?: number;
65
- status?: string;
66
- }
67
-
68
- export interface ChangeStatusInput {
69
- orderId: string;
70
- newStatus: OrderState;
71
- reason?: string;
72
- }
73
-
74
- export interface OrderServiceDeps {
75
- repository: OrdersRepository;
76
- hooks: HookRegistry;
77
- services: Record<string, unknown>;
78
- /** Custom state machine. If provided, overrides the default order transitions. */
79
- stateMachine?: StateDefinition<string>;
80
- /** Kernel database reference for hook contexts. Uses PluginDb to avoid circular Kernel import. */
81
- kernel?: { database: { db: PluginDb } };
82
- }
83
-
84
- export type HydratedOrder = Order & { lineItems: OrderLineItem[] };
85
- type OrderListResult = { items: HydratedOrder[]; pagination: Pagination };
86
- type BeforeCreateOrderHook = BeforeHook<CreateOrderInput>;
87
- type AfterCreateOrderHook = AfterHook<HydratedOrder>;
88
- type StatusChangeHookInput = {
89
- orderId: string;
90
- fromStatus: OrderState;
91
- newStatus: OrderState;
92
- reason?: string;
93
- };
94
- type BeforeStatusChangeHook = BeforeHook<StatusChangeHookInput>;
95
- type AfterStatusChangeHook = AfterHook<HydratedOrder>;
96
-
97
- function context(
98
- actor: Actor | null,
99
- services: Record<string, unknown>,
100
- tx: unknown = null,
101
- kernel: { database: { db: PluginDb } } | null = null,
102
- ): HookContext {
103
- return createHookContext({
104
- actor,
105
- tx,
106
- logger: createLogger("orders"),
107
- services,
108
- context: { moduleName: "orders" },
109
- ...(kernel != null ? { kernel } : {}),
110
- });
111
- }
112
-
113
- export class OrderService {
114
- private readonly repo: OrdersRepository;
115
- private readonly machine: StateDefinition<string>;
116
-
117
- constructor(private deps: OrderServiceDeps) {
118
- this.repo = deps.repository;
119
- this.machine = deps.stateMachine ?? orderStateMachine;
120
- }
121
-
122
- private async hydrateOrder(
123
- order: Order,
124
- ctx?: TxContext,
125
- ): Promise<HydratedOrder> {
126
- const lineItems = await this.repo.findLineItemsByOrderId(order.id, ctx);
127
- return { ...order, lineItems };
128
- }
129
-
130
- async create(
131
- input: CreateOrderInput,
132
- actor: Actor | null,
133
- ctx?: TxContext,
134
- ): Promise<Result<HydratedOrder>> {
135
- try {
136
- assertPermission(actor, "orders:create");
137
- } catch (error) {
138
- return Err(toCommerceError(error));
139
- }
140
-
141
- if (input.lineItems.length === 0) {
142
- return Err(
143
- new CommerceValidationError("Order requires at least one line item."),
144
- );
145
- }
146
-
147
- const beforeHooks = this.deps.hooks.resolve(
148
- "orders.beforeCreate",
149
- ) as BeforeCreateOrderHook[];
150
- const afterHooks = this.deps.hooks.resolve(
151
- "orders.afterCreate",
152
- ) as AfterCreateOrderHook[];
153
- const hookCtx = context(actor, this.deps.services, ctx?.tx, this.deps.kernel);
154
-
155
- const processed = await runBeforeHooks(
156
- beforeHooks,
157
- input,
158
- "create",
159
- hookCtx,
160
- );
161
-
162
- const orderNumber = await this.repo.getNextOrderNumber(ctx);
163
- const orgId = resolveOrgId(actor);
164
-
165
- const order = await this.repo.create(
166
- {
167
- organizationId: orgId,
168
- orderNumber,
169
- status: "pending",
170
- currency: processed.currency,
171
- subtotal: processed.subtotal,
172
- taxTotal: processed.taxTotal,
173
- shippingTotal: processed.shippingTotal,
174
- discountTotal: processed.discountTotal ?? 0,
175
- grandTotal: processed.grandTotal,
176
- ...(processed.paymentIntentId != null ? { paymentIntentId: processed.paymentIntentId } : {}),
177
- ...(processed.paymentMethodId != null ? { paymentMethodId: processed.paymentMethodId } : {}),
178
- metadata: processed.metadata ?? {},
179
- placedAt: new Date(),
180
- ...(processed.customerId !== undefined
181
- ? { customerId: processed.customerId }
182
- : {}),
183
- },
184
- ctx,
185
- );
186
-
187
- const lineItemsData = processed.lineItems.map((item) => ({
188
- orderId: order.id,
189
- entityId: item.entityId,
190
- entityType: item.entityType,
191
- title: item.title,
192
- quantity: item.quantity,
193
- unitPrice: item.unitPrice,
194
- totalPrice: item.totalPrice,
195
- taxAmount: item.taxAmount ?? 0,
196
- discountAmount: item.discountAmount ?? 0,
197
- fulfillmentStatus: "unfulfilled" as const,
198
- metadata: item.metadata ?? {},
199
- ...(item.variantId !== undefined ? { variantId: item.variantId } : {}),
200
- ...(item.sku !== undefined ? { sku: item.sku } : {}),
201
- }));
202
-
203
- await this.repo.createLineItems(lineItemsData, ctx);
204
-
205
- await this.repo.createStatusHistory(
206
- {
207
- orderId: order.id,
208
- fromStatus: "pending",
209
- toStatus: "pending",
210
- reason: "order_created",
211
- changedBy: actor?.userId ?? "system",
212
- },
213
- ctx,
214
- );
215
-
216
- const hydrated = await this.hydrateOrder(order, ctx);
217
- const report = await runAfterHooks(
218
- afterHooks,
219
- null,
220
- hydrated,
221
- "create",
222
- hookCtx,
223
- );
224
-
225
- return Ok(
226
- hydrated,
227
- report.hasErrors ? { hookErrors: report.errors } : undefined,
228
- );
229
- }
230
-
231
- async getById(
232
- id: string,
233
- actor: Actor | null,
234
- ctx?: TxContext,
235
- ): Promise<Result<HydratedOrder>> {
236
- const orgId = resolveOrgId(actor);
237
- const order = await this.repo.findById(orgId, id, ctx);
238
- if (!order) return Err(new CommerceNotFoundError("Order not found."));
239
-
240
- try {
241
- if (
242
- actor?.permissions.includes("orders:read") ||
243
- actor?.permissions.includes("*:*")
244
- ) {
245
- // no-op
246
- } else if (actor?.permissions.includes("orders:read:own")) {
247
- assertOwnership(actor, order.customerId ?? null);
248
- } else {
249
- assertPermission(actor, "orders:read");
250
- }
251
- } catch (error) {
252
- return Err(toCommerceError(error));
253
- }
254
-
255
- const hydrated = await this.hydrateOrder(order, ctx);
256
-
257
- // Run afterGet hooks — allows plugins to enrich the order (e.g., vendor fulfillment)
258
- const afterGetHooks = this.deps.hooks.resolve(
259
- "orders.afterGet",
260
- ) as AfterHook<HydratedOrder>[];
261
- if (afterGetHooks.length > 0) {
262
- const hookCtx = context(actor, this.deps.services, ctx?.tx, this.deps.kernel);
263
- await runAfterHooks(afterGetHooks, null, hydrated, "read", hookCtx);
264
- }
265
-
266
- return Ok(hydrated);
267
- }
268
-
269
- async getByNumber(
270
- orderNumber: string,
271
- actor: Actor | null,
272
- ctx?: TxContext,
273
- ): Promise<Result<HydratedOrder>> {
274
- const orgId = resolveOrgId(actor);
275
- const order = await this.repo.findByOrderNumber(orgId, orderNumber, ctx);
276
- if (!order) return Err(new CommerceNotFoundError("Order not found."));
277
- return this.getById(order.id, actor, ctx);
278
- }
279
-
280
- async list(
281
- params: ListOrdersParams,
282
- actor: Actor | null,
283
- ctx?: TxContext,
284
- ): Promise<Result<OrderListResult>> {
285
- try {
286
- assertPermission(actor, "orders:read");
287
- } catch (error) {
288
- return Err(toCommerceError(error));
289
- }
290
-
291
- const orgId = resolveOrgId(actor);
292
- let items: Order[];
293
- if (params.status) {
294
- items = await this.repo.findByStatus(orgId, params.status, ctx);
295
- } else {
296
- items = await this.repo.findAll(orgId, undefined, ctx);
297
- }
298
-
299
- // Sort by placedAt descending (in-memory, as findAll already sorts)
300
- items.sort((a, b) => b.placedAt.getTime() - a.placedAt.getTime());
301
-
302
- const paged = paginate(items, params.page ?? 1, params.limit ?? 20);
303
- const hydratedItems = await Promise.all(
304
- paged.items.map((order) => this.hydrateOrder(order, ctx)),
305
- );
306
-
307
- return Ok({
308
- items: hydratedItems,
309
- pagination: paged.pagination,
310
- });
311
- }
312
-
313
- async listByCustomer(
314
- customerId: string,
315
- params: ListOrdersParams,
316
- actor?: Actor | null,
317
- ctx?: TxContext,
318
- ): Promise<Result<OrderListResult>> {
319
- const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
320
- let items = await this.repo.findByCustomerId(orgId, customerId, ctx);
321
-
322
- if (params.status) {
323
- items = items.filter((order) => order.status === params.status);
324
- }
325
-
326
- items.sort((a, b) => b.placedAt.getTime() - a.placedAt.getTime());
327
- const paged = paginate(items, params.page ?? 1, params.limit ?? 20);
328
- const hydratedItems = await Promise.all(
329
- paged.items.map((order) => this.hydrateOrder(order, ctx)),
330
- );
331
-
332
- return Ok({
333
- items: hydratedItems,
334
- pagination: paged.pagination,
335
- });
336
- }
337
-
338
- async changeStatus(
339
- input: ChangeStatusInput,
340
- actor: Actor | null,
341
- ctx?: TxContext,
342
- ): Promise<Result<HydratedOrder>> {
343
- const orgId = resolveOrgId(actor);
344
- const order = await this.repo.findById(orgId, input.orderId, ctx);
345
- if (!order) return Err(new CommerceNotFoundError("Order not found."));
346
-
347
- try {
348
- assertPermission(actor, "orders:update");
349
- } catch (error) {
350
- return Err(toCommerceError(error));
351
- }
352
-
353
- if (
354
- !canTransition(
355
- this.machine,
356
- order.status as OrderState,
357
- input.newStatus,
358
- )
359
- ) {
360
- return Err(
361
- new CommerceInvalidTransitionError(
362
- `Cannot transition from ${order.status} to ${input.newStatus}.`,
363
- ),
364
- );
365
- }
366
-
367
- const beforeHooks = this.deps.hooks.resolve(
368
- "orders.beforeStatusChange",
369
- ) as BeforeStatusChangeHook[];
370
- const afterHooks = this.deps.hooks.resolve(
371
- "orders.afterStatusChange",
372
- ) as AfterStatusChangeHook[];
373
-
374
- const hookCtx = context(actor, this.deps.services, ctx?.tx, this.deps.kernel);
375
- const statusHookInput: StatusChangeHookInput = {
376
- orderId: order.id,
377
- fromStatus: order.status as OrderState,
378
- newStatus: input.newStatus,
379
- ...(input.reason !== undefined ? { reason: input.reason } : {}),
380
- };
381
-
382
- await runBeforeHooks(beforeHooks, statusHookInput, "statusChange", hookCtx);
383
-
384
- const previous = order.status;
385
- const lineItems = await this.repo.findLineItemsByOrderId(order.id, ctx);
386
-
387
- // Handle cancellation and refund side effects
388
- if (input.newStatus === "cancelled" || input.newStatus === "refunded") {
389
- // 1. Release inventory reservations
390
- const inventory = this.deps.services.inventory as
391
- | {
392
- release(input: {
393
- entityId: string;
394
- variantId?: string;
395
- quantity: number;
396
- orderId: string;
397
- performedBy?: string;
398
- }): Promise<unknown>;
399
- }
400
- | undefined;
401
-
402
- if (inventory?.release) {
403
- for (const lineItem of lineItems) {
404
- // Only release inventory for unfulfilled items.
405
- // Fulfilled items had their reservation released during the
406
- // fulfilled transition — releasing again would double-release.
407
- if (lineItem.fulfillmentStatus === "unfulfilled") {
408
- await inventory.release({
409
- entityId: lineItem.entityId,
410
- quantity: lineItem.quantity,
411
- orderId: order.id,
412
- performedBy: actor?.userId ?? "system",
413
- ...(lineItem.variantId != null
414
- ? { variantId: lineItem.variantId }
415
- : {}),
416
- });
417
- }
418
- }
419
- }
420
-
421
- // 2. Refund payment (if captured)
422
- const paymentIntentId =
423
- (order as Record<string, unknown>).paymentIntentId as string | undefined
424
- ?? (order.metadata as Record<string, unknown> | null)?.paymentIntentId as string | undefined;
425
-
426
- if (paymentIntentId) {
427
- const payments = this.deps.services.payments as
428
- | {
429
- refund(
430
- paymentId: string,
431
- amount: number,
432
- reason?: string,
433
- ): Promise<unknown>;
434
- }
435
- | undefined;
436
-
437
- if (payments?.refund) {
438
- await payments.refund(
439
- paymentIntentId,
440
- order.grandTotal,
441
- input.reason ?? `order_${input.newStatus}`,
442
- );
443
- }
444
- }
445
-
446
- // 3. Void tax transaction
447
- const tax = this.deps.services.tax as
448
- | {
449
- voidTransaction(input: { transactionId: string }): Promise<unknown>;
450
- }
451
- | undefined;
452
- if (tax?.voidTransaction) {
453
- await tax.voidTransaction({ transactionId: order.id });
454
- }
455
- }
456
-
457
- // Handle fulfillment: deduct on_hand and release reservations.
458
- // Items have been shipped — on_hand decreases (stock left the warehouse)
459
- // and reservations are cleared (no longer needed).
460
- //
461
- // Net effect per line item:
462
- // on_hand -= quantity, reserved -= quantity, available unchanged
463
- // Before: on_hand=100, reserved=5, available=95
464
- // After: on_hand=95, reserved=0, available=95
465
- if (input.newStatus === "fulfilled" || input.newStatus === "partially_fulfilled") {
466
- const inventory = this.deps.services.inventory as
467
- | {
468
- deductForFulfillment(input: {
469
- entityId: string;
470
- variantId?: string;
471
- quantity: number;
472
- orderId: string;
473
- }): Promise<unknown>;
474
- release(input: {
475
- entityId: string;
476
- variantId?: string;
477
- quantity: number;
478
- orderId: string;
479
- performedBy?: string;
480
- }): Promise<unknown>;
481
- }
482
- | undefined;
483
-
484
- if (inventory) {
485
- for (const lineItem of lineItems) {
486
- if (lineItem.fulfillmentStatus === "unfulfilled") {
487
- // Deduct on_hand (stock physically left warehouse)
488
- await inventory.deductForFulfillment({
489
- entityId: lineItem.entityId,
490
- quantity: lineItem.quantity,
491
- orderId: order.id,
492
- ...(lineItem.variantId != null
493
- ? { variantId: lineItem.variantId }
494
- : {}),
495
- });
496
-
497
- // Release reservation (no longer needed)
498
- await inventory.release({
499
- entityId: lineItem.entityId,
500
- quantity: lineItem.quantity,
501
- orderId: order.id,
502
- performedBy: actor?.userId ?? "system",
503
- ...(lineItem.variantId != null
504
- ? { variantId: lineItem.variantId }
505
- : {}),
506
- });
507
-
508
- // Mark line item as fulfilled
509
- await this.repo.updateLineItem(
510
- lineItem.id,
511
- { fulfillmentStatus: "fulfilled" },
512
- ctx,
513
- );
514
- }
515
- }
516
- }
517
- }
518
-
519
- // Update order status with atomic guard on current status
520
- const updated = await this.repo.updateStatus(
521
- order.id,
522
- previous,
523
- input.newStatus,
524
- ctx,
525
- );
526
- if (!updated) {
527
- return Err(new CommerceValidationError(
528
- `Order status changed concurrently. Refresh and retry.`,
529
- ));
530
- }
531
-
532
- await this.repo.createStatusHistory(
533
- {
534
- orderId: order.id,
535
- fromStatus: previous,
536
- toStatus: input.newStatus,
537
- changedBy: actor?.userId ?? "system",
538
- ...(input.reason !== undefined ? { reason: input.reason } : {}),
539
- },
540
- ctx,
541
- );
542
-
543
- // Audit logging is now automatic via audit hooks (RFC-005)
544
- // registered in kernel boot — no manual audit.record() needed.
545
-
546
- const hydrated = await this.hydrateOrder(updated, ctx);
547
- const report = await runAfterHooks(
548
- afterHooks,
549
- null,
550
- hydrated,
551
- "statusChange",
552
- hookCtx,
553
- );
554
-
555
- return Ok(
556
- hydrated,
557
- report.hasErrors ? { hookErrors: report.errors } : undefined,
558
- );
559
- }
560
-
561
- async cancel(
562
- orderId: string,
563
- actor: Actor | null,
564
- reason = "cancelled_by_user",
565
- ctx?: TxContext,
566
- ): Promise<Result<HydratedOrder>> {
567
- return this.changeStatus(
568
- { orderId, newStatus: "cancelled", reason },
569
- actor,
570
- ctx,
571
- );
572
- }
573
-
574
- async refund(
575
- orderId: string,
576
- actor: Actor | null,
577
- reason = "refunded",
578
- ctx?: TxContext,
579
- ): Promise<Result<HydratedOrder>> {
580
- return this.changeStatus(
581
- { orderId, newStatus: "refunded", reason },
582
- actor,
583
- ctx,
584
- );
585
- }
586
-
587
- async getStatusHistory(
588
- orderId: string,
589
- actor?: Actor | null,
590
- ctx?: TxContext,
591
- ): Promise<Result<OrderStatusHistory[]>> {
592
- const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
593
- const order = await this.repo.findById(orgId, orderId, ctx);
594
- if (!order) {
595
- return Err(new CommerceNotFoundError("Order not found."));
596
- }
597
-
598
- const items = await this.repo.findStatusHistoryByOrderId(orderId, ctx);
599
- // Sort by changedAt ascending (oldest first)
600
- items.sort((a, b) => a.changedAt.getTime() - b.changedAt.getTime());
601
-
602
- return Ok(items);
603
- }
604
-
605
- async updateOrder(
606
- orderId: string,
607
- data: { placedAt?: Date; metadata?: Record<string, unknown> },
608
- actor?: Actor | null,
609
- ctx?: TxContext,
610
- ): Promise<Result<Order>> {
611
- const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
612
- const order = await this.repo.findById(orgId, orderId, ctx);
613
- if (!order) {
614
- return Err(new CommerceNotFoundError("Order not found."));
615
- }
616
- const updated = await this.repo.update(orderId, data, ctx);
617
- return Ok(updated!);
618
- }
619
- }
@@ -1,76 +0,0 @@
1
- import { eq, and, lt, sql } from "drizzle-orm";
2
- import { orders } from "./schema.js";
3
- import type { TaskDefinition, TaskContext } from "../../kernel/jobs/types.js";
4
-
5
- /**
6
- * Stale Order Cleanup Task
7
- *
8
- * Cancels orders stuck in "pending" status for longer than the configured
9
- * threshold (default: 48 hours). This releases reserved inventory and
10
- * refunds captured payments, preventing phantom stock loss from abandoned
11
- * orders.
12
- *
13
- * Register via config.jobs.tasks:
14
- * ```ts
15
- * import { staleOrderCleanupTask } from "@unifiedcommerce/core";
16
- * defineConfig({
17
- * jobs: {
18
- * tasks: [staleOrderCleanupTask],
19
- * autorun: { enabled: true, intervalMs: 3600_000 }, // hourly
20
- * },
21
- * });
22
- * ```
23
- */
24
- export const staleOrderCleanupTask: TaskDefinition<
25
- { thresholdHours?: number },
26
- { cancelledCount: number; orderIds: string[] }
27
- > = {
28
- slug: "orders/stale-cleanup",
29
-
30
- async handler({ input, ctx }) {
31
- const thresholdHours = input.thresholdHours ?? 48;
32
- const cutoff = new Date(Date.now() - thresholdHours * 60 * 60 * 1000);
33
-
34
- // Find stale pending orders
35
- const staleOrders = await ctx.db
36
- .select()
37
- .from(orders)
38
- .where(
39
- and(
40
- eq(orders.status, "pending"),
41
- lt(orders.placedAt, cutoff),
42
- ),
43
- );
44
-
45
- const cancelledIds: string[] = [];
46
-
47
- // Cancel each stale order via the service (triggers inventory release + payment refund)
48
- const orderService = ctx.services.orders as {
49
- cancel(orderId: string, actor: null, reason: string): Promise<unknown>;
50
- };
51
-
52
- for (const order of staleOrders) {
53
- try {
54
- await orderService.cancel(
55
- order.id,
56
- null,
57
- `Auto-cancelled: pending for >${thresholdHours}h`,
58
- );
59
- cancelledIds.push(order.id);
60
- ctx.logger.info(`Stale order ${order.orderNumber} auto-cancelled`, {
61
- orderId: order.id,
62
- placedAt: order.placedAt,
63
- });
64
- } catch (error) {
65
- ctx.logger.error(`Failed to cancel stale order ${order.orderNumber}`, { error });
66
- }
67
- }
68
-
69
- return {
70
- output: {
71
- cancelledCount: cancelledIds.length,
72
- orderIds: cancelledIds,
73
- },
74
- };
75
- },
76
- };