@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.
- package/dist/auth/setup.d.ts.map +1 -1
- package/dist/auth/setup.js +8 -3
- package/dist/config/types.d.ts +3 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/interfaces/mcp/server.d.ts +3 -5
- package/dist/interfaces/mcp/server.d.ts.map +1 -1
- package/dist/interfaces/mcp/server.js +25 -510
- package/dist/interfaces/mcp/tool-builder.d.ts +120 -0
- package/dist/interfaces/mcp/tool-builder.d.ts.map +1 -0
- package/dist/interfaces/mcp/tool-builder.js +224 -0
- package/dist/interfaces/mcp/tools/analytics.d.ts +42 -0
- package/dist/interfaces/mcp/tools/analytics.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/analytics.js +70 -0
- package/dist/interfaces/mcp/tools/cart.d.ts +14 -0
- package/dist/interfaces/mcp/tools/cart.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/cart.js +47 -0
- package/dist/interfaces/mcp/tools/catalog.d.ts +53 -0
- package/dist/interfaces/mcp/tools/catalog.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/catalog.js +284 -0
- package/dist/interfaces/mcp/tools/index.d.ts +3 -0
- package/dist/interfaces/mcp/tools/index.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/index.js +20 -0
- package/dist/interfaces/mcp/tools/inventory.d.ts +27 -0
- package/dist/interfaces/mcp/tools/inventory.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/inventory.js +143 -0
- package/dist/interfaces/mcp/tools/orders.d.ts +18 -0
- package/dist/interfaces/mcp/tools/orders.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/orders.js +82 -0
- package/dist/interfaces/mcp/tools/pricing.d.ts +29 -0
- package/dist/interfaces/mcp/tools/pricing.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/pricing.js +90 -0
- package/dist/interfaces/mcp/tools/promotions.d.ts +44 -0
- package/dist/interfaces/mcp/tools/promotions.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/promotions.js +109 -0
- package/dist/interfaces/mcp/tools/registry.d.ts +32 -0
- package/dist/interfaces/mcp/tools/registry.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/registry.js +55 -0
- package/dist/interfaces/mcp/tools/search.d.ts +14 -0
- package/dist/interfaces/mcp/tools/search.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/search.js +39 -0
- package/dist/interfaces/mcp/tools/webhooks.d.ts +15 -0
- package/dist/interfaces/mcp/tools/webhooks.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/webhooks.js +48 -0
- package/dist/interfaces/mcp/transport.d.ts +17 -2
- package/dist/interfaces/mcp/transport.d.ts.map +1 -1
- package/dist/interfaces/mcp/transport.js +91 -44
- package/dist/interfaces/rest/router.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/checkout.js +1 -1
- package/dist/interfaces/rest/routes/promotions.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/promotions.js +3 -2
- package/dist/kernel/database/adapter.d.ts +8 -0
- package/dist/kernel/database/adapter.d.ts.map +1 -1
- package/dist/kernel/factory/repository-factory.d.ts.map +1 -1
- package/dist/kernel/factory/repository-factory.js +3 -1
- package/dist/kernel/local-api.d.ts.map +1 -1
- package/dist/kernel/local-api.js +2 -0
- package/dist/kernel/plugin/manifest.d.ts +3 -3
- package/dist/kernel/plugin/manifest.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.js +36 -7
- package/dist/runtime/kernel.d.ts +1 -2
- package/dist/runtime/kernel.d.ts.map +1 -1
- package/dist/runtime/kernel.js +16 -8
- package/dist/runtime/server.d.ts.map +1 -1
- package/dist/runtime/server.js +8 -3
- package/dist/test-utils/create-pglite-adapter.d.ts.map +1 -1
- package/dist/test-utils/create-pglite-adapter.js +7 -6
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +2 -2
- package/src/adapters/console-email.ts +0 -43
- package/src/auth/access.ts +0 -187
- package/src/auth/auth-schema.ts +0 -139
- package/src/auth/middleware.ts +0 -161
- package/src/auth/org.ts +0 -41
- package/src/auth/permissions.ts +0 -28
- package/src/auth/setup.ts +0 -169
- package/src/auth/system-actor.ts +0 -19
- package/src/auth/types.ts +0 -10
- package/src/config/defaults.ts +0 -82
- package/src/config/define-config.ts +0 -53
- package/src/config/types.ts +0 -299
- package/src/generated/plugin-capabilities.d.ts +0 -20
- package/src/generated/plugin-manifest.ts +0 -23
- package/src/generated/plugin-repositories.d.ts +0 -20
- package/src/hooks/checkout-completion.ts +0 -262
- package/src/hooks/checkout.ts +0 -677
- package/src/hooks/order-emails.ts +0 -62
- package/src/index.ts +0 -214
- package/src/interfaces/mcp/agent-prompt.ts +0 -174
- package/src/interfaces/mcp/context-enrichment.ts +0 -177
- package/src/interfaces/mcp/server.ts +0 -617
- package/src/interfaces/mcp/transport.ts +0 -68
- package/src/interfaces/rest/customer-portal.ts +0 -299
- package/src/interfaces/rest/index.ts +0 -74
- package/src/interfaces/rest/router.ts +0 -334
- package/src/interfaces/rest/routes/admin-jobs.ts +0 -58
- package/src/interfaces/rest/routes/audit.ts +0 -50
- package/src/interfaces/rest/routes/carts.ts +0 -89
- package/src/interfaces/rest/routes/catalog.ts +0 -493
- package/src/interfaces/rest/routes/checkout.ts +0 -283
- package/src/interfaces/rest/routes/inventory.ts +0 -70
- package/src/interfaces/rest/routes/media.ts +0 -86
- package/src/interfaces/rest/routes/orders.ts +0 -78
- package/src/interfaces/rest/routes/payments.ts +0 -60
- package/src/interfaces/rest/routes/pricing.ts +0 -57
- package/src/interfaces/rest/routes/promotions.ts +0 -92
- package/src/interfaces/rest/routes/search.ts +0 -71
- package/src/interfaces/rest/routes/webhooks.ts +0 -46
- package/src/interfaces/rest/schemas/admin-jobs.ts +0 -40
- package/src/interfaces/rest/schemas/audit.ts +0 -46
- package/src/interfaces/rest/schemas/carts.ts +0 -125
- package/src/interfaces/rest/schemas/catalog.ts +0 -450
- package/src/interfaces/rest/schemas/checkout.ts +0 -66
- package/src/interfaces/rest/schemas/customer-portal.ts +0 -195
- package/src/interfaces/rest/schemas/inventory.ts +0 -138
- package/src/interfaces/rest/schemas/media.ts +0 -75
- package/src/interfaces/rest/schemas/orders.ts +0 -104
- package/src/interfaces/rest/schemas/pricing.ts +0 -80
- package/src/interfaces/rest/schemas/promotions.ts +0 -110
- package/src/interfaces/rest/schemas/responses.ts +0 -85
- package/src/interfaces/rest/schemas/search.ts +0 -58
- package/src/interfaces/rest/schemas/shared.ts +0 -62
- package/src/interfaces/rest/schemas/webhooks.ts +0 -68
- package/src/interfaces/rest/utils.ts +0 -104
- package/src/interfaces/rest/webhook-router.ts +0 -50
- package/src/kernel/compensation/executor.ts +0 -61
- package/src/kernel/compensation/types.ts +0 -26
- package/src/kernel/database/adapter.ts +0 -13
- package/src/kernel/database/drizzle-db.ts +0 -56
- package/src/kernel/database/migrate.ts +0 -76
- package/src/kernel/database/plugin-types.ts +0 -34
- package/src/kernel/database/schema.ts +0 -49
- package/src/kernel/database/scoped-db.ts +0 -68
- package/src/kernel/database/tx-context.ts +0 -46
- package/src/kernel/error-mapper.ts +0 -15
- package/src/kernel/errors.ts +0 -89
- package/src/kernel/factory/repository-factory.ts +0 -242
- package/src/kernel/hooks/create-context.ts +0 -43
- package/src/kernel/hooks/executor.ts +0 -88
- package/src/kernel/hooks/registry.ts +0 -74
- package/src/kernel/hooks/types.ts +0 -52
- package/src/kernel/http-error.ts +0 -44
- package/src/kernel/jobs/adapter.ts +0 -36
- package/src/kernel/jobs/drizzle-adapter.ts +0 -58
- package/src/kernel/jobs/runner.ts +0 -153
- package/src/kernel/jobs/schema.ts +0 -46
- package/src/kernel/jobs/types.ts +0 -30
- package/src/kernel/local-api.ts +0 -185
- package/src/kernel/plugin/manifest.ts +0 -253
- package/src/kernel/query/executor.ts +0 -184
- package/src/kernel/query/registry.ts +0 -46
- package/src/kernel/result.ts +0 -33
- package/src/kernel/schema/extra-columns.ts +0 -37
- package/src/kernel/service-registry.ts +0 -76
- package/src/kernel/service-timing.ts +0 -89
- package/src/kernel/state-machine/machine.ts +0 -101
- package/src/modules/analytics/drizzle-adapter.ts +0 -426
- package/src/modules/analytics/hooks.ts +0 -11
- package/src/modules/analytics/models.ts +0 -125
- package/src/modules/analytics/repository/index.ts +0 -6
- package/src/modules/analytics/service.ts +0 -245
- package/src/modules/analytics/types.ts +0 -180
- package/src/modules/audit/hooks.ts +0 -78
- package/src/modules/audit/schema.ts +0 -33
- package/src/modules/audit/service.ts +0 -151
- package/src/modules/cart/access.ts +0 -27
- package/src/modules/cart/matcher.ts +0 -26
- package/src/modules/cart/repository/index.ts +0 -234
- package/src/modules/cart/schema.ts +0 -42
- package/src/modules/cart/schemas.ts +0 -38
- package/src/modules/cart/service.ts +0 -541
- package/src/modules/catalog/repository/index.ts +0 -772
- package/src/modules/catalog/schema.ts +0 -203
- package/src/modules/catalog/schemas.ts +0 -104
- package/src/modules/catalog/service.ts +0 -1544
- package/src/modules/customers/repository/index.ts +0 -327
- package/src/modules/customers/schema.ts +0 -64
- package/src/modules/customers/service.ts +0 -171
- package/src/modules/fulfillment/repository/index.ts +0 -426
- package/src/modules/fulfillment/schema.ts +0 -101
- package/src/modules/fulfillment/service.ts +0 -555
- package/src/modules/fulfillment/types.ts +0 -59
- package/src/modules/inventory/repository/index.ts +0 -509
- package/src/modules/inventory/schema.ts +0 -94
- package/src/modules/inventory/schemas.ts +0 -38
- package/src/modules/inventory/service.ts +0 -490
- package/src/modules/media/adapter.ts +0 -17
- package/src/modules/media/repository/index.ts +0 -274
- package/src/modules/media/schema.ts +0 -41
- package/src/modules/media/service.ts +0 -151
- package/src/modules/orders/repository/index.ts +0 -287
- package/src/modules/orders/schema.ts +0 -66
- package/src/modules/orders/service.ts +0 -619
- package/src/modules/orders/stale-order-cleanup.ts +0 -76
- package/src/modules/organization/service.ts +0 -191
- package/src/modules/payments/adapter.ts +0 -47
- package/src/modules/payments/repository/index.ts +0 -6
- package/src/modules/payments/service.ts +0 -107
- package/src/modules/pricing/repository/index.ts +0 -291
- package/src/modules/pricing/schema.ts +0 -71
- package/src/modules/pricing/schemas.ts +0 -38
- package/src/modules/pricing/service.ts +0 -494
- package/src/modules/promotions/repository/index.ts +0 -325
- package/src/modules/promotions/schema.ts +0 -62
- package/src/modules/promotions/schemas.ts +0 -38
- package/src/modules/promotions/service.ts +0 -598
- package/src/modules/search/adapter.ts +0 -57
- package/src/modules/search/hooks.ts +0 -12
- package/src/modules/search/repository/index.ts +0 -6
- package/src/modules/search/service.ts +0 -315
- package/src/modules/shipping/calculator.ts +0 -188
- package/src/modules/shipping/repository/index.ts +0 -6
- package/src/modules/shipping/service.ts +0 -51
- package/src/modules/tax/adapter.ts +0 -60
- package/src/modules/tax/repository/index.ts +0 -6
- package/src/modules/tax/service.ts +0 -53
- package/src/modules/webhooks/hook.ts +0 -34
- package/src/modules/webhooks/repository/index.ts +0 -278
- package/src/modules/webhooks/schema.ts +0 -56
- package/src/modules/webhooks/service.ts +0 -117
- package/src/modules/webhooks/signing.ts +0 -6
- package/src/modules/webhooks/ssrf-guard.ts +0 -71
- package/src/modules/webhooks/tasks.ts +0 -52
- package/src/modules/webhooks/worker.ts +0 -134
- package/src/runtime/commerce.ts +0 -145
- package/src/runtime/kernel.ts +0 -419
- package/src/runtime/logger.ts +0 -36
- package/src/runtime/server.ts +0 -349
- package/src/runtime/shutdown.ts +0 -43
- package/src/test-utils/create-pglite-adapter.ts +0 -129
- package/src/test-utils/create-plugin-test-app.ts +0 -128
- package/src/test-utils/create-repository-test-harness.ts +0 -16
- package/src/test-utils/create-test-config.ts +0 -190
- package/src/test-utils/create-test-kernel.ts +0 -7
- package/src/test-utils/create-test-plugin-context.ts +0 -75
- package/src/test-utils/rest-api-test-utils.ts +0 -265
- package/src/test-utils/test-actors.ts +0 -62
- package/src/test-utils/typed-hooks.ts +0 -54
- package/src/types/commerce-types.ts +0 -34
- package/src/utils/id.ts +0 -3
- package/src/utils/logger.ts +0 -18
- 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
|
-
};
|