@porulle/core 0.8.0 → 0.10.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/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +26 -7
- package/dist/auth/setup.d.ts.map +1 -1
- package/dist/auth/setup.js +7 -5
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +3 -1
- package/dist/config/types.d.ts +13 -3
- package/dist/config/types.d.ts.map +1 -1
- package/dist/generated/plugin-manifest.d.ts +3 -0
- package/dist/generated/plugin-manifest.d.ts.map +1 -1
- package/dist/generated/plugin-manifest.js +1 -0
- package/dist/hooks/checkout-completion.d.ts.map +1 -1
- package/dist/hooks/checkout-completion.js +2 -2
- package/dist/hooks/checkout.d.ts.map +1 -1
- package/dist/hooks/checkout.js +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/interfaces/rest/router.d.ts.map +1 -1
- package/dist/interfaces/rest/router.js +2 -0
- package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/admin/staff.js +37 -3
- package/dist/interfaces/rest/routes/checkout.d.ts +9 -0
- package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/checkout.js +82 -22
- package/dist/interfaces/rest/routes/inventory.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/inventory.js +13 -0
- package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/orders.js +13 -0
- package/dist/interfaces/rest/schemas/catalog.d.ts +391 -0
- package/dist/interfaces/rest/schemas/catalog.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/orders.js +2 -2
- package/dist/interfaces/rest/schemas/responses.d.ts +85 -0
- package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/search.d.ts +17 -0
- package/dist/interfaces/rest/schemas/search.d.ts.map +1 -1
- package/dist/kernel/database/scoped-db.d.ts +2 -1
- package/dist/kernel/database/scoped-db.d.ts.map +1 -1
- package/dist/kernel/database/scoped-db.js +74 -15
- package/dist/kernel/errors.d.ts +12 -0
- package/dist/kernel/errors.d.ts.map +1 -1
- package/dist/kernel/errors.js +16 -0
- package/dist/kernel/jobs/adapter.d.ts +27 -8
- package/dist/kernel/jobs/adapter.d.ts.map +1 -1
- package/dist/kernel/jobs/adapter.js +1 -2
- package/dist/kernel/jobs/drizzle-adapter.d.ts +13 -3
- package/dist/kernel/jobs/drizzle-adapter.d.ts.map +1 -1
- package/dist/kernel/jobs/drizzle-adapter.js +46 -5
- package/dist/kernel/jobs/runner.d.ts +2 -1
- package/dist/kernel/jobs/runner.d.ts.map +1 -1
- package/dist/kernel/jobs/runner.js +100 -15
- package/dist/kernel/jobs/types.d.ts +13 -0
- package/dist/kernel/jobs/types.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.d.ts +5 -0
- package/dist/kernel/plugin/manifest.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.js +9 -0
- package/dist/modules/analytics/drizzle-adapter.d.ts.map +1 -1
- package/dist/modules/analytics/drizzle-adapter.js +16 -3
- package/dist/modules/cart/service.d.ts.map +1 -1
- package/dist/modules/cart/service.js +3 -2
- package/dist/modules/catalog/entity-service.d.ts +1 -0
- package/dist/modules/catalog/entity-service.d.ts.map +1 -1
- package/dist/modules/catalog/entity-service.js +49 -3
- package/dist/modules/catalog/repository/index.d.ts +2 -2
- package/dist/modules/catalog/repository/index.d.ts.map +1 -1
- package/dist/modules/catalog/repository/index.js +9 -1
- package/dist/modules/catalog/schema.d.ts +51 -0
- package/dist/modules/catalog/schema.d.ts.map +1 -1
- package/dist/modules/catalog/schema.js +12 -1
- package/dist/modules/catalog/schemas.d.ts +3 -1
- package/dist/modules/catalog/schemas.d.ts.map +1 -1
- package/dist/modules/channels/adapter.d.ts +117 -0
- package/dist/modules/channels/adapter.d.ts.map +1 -0
- package/dist/modules/channels/adapter.js +3 -0
- package/dist/modules/customers/service.d.ts.map +1 -1
- package/dist/modules/customers/service.js +6 -0
- package/dist/modules/inventory/repository/index.js +4 -4
- package/dist/modules/inventory/service.d.ts.map +1 -1
- package/dist/modules/inventory/service.js +7 -3
- package/dist/modules/orders/schema.d.ts +17 -0
- package/dist/modules/orders/schema.d.ts.map +1 -1
- package/dist/modules/orders/schema.js +4 -0
- package/dist/modules/orders/service.d.ts +16 -2
- package/dist/modules/orders/service.d.ts.map +1 -1
- package/dist/modules/orders/service.js +226 -26
- package/dist/modules/orders/stale-order-cleanup.d.ts.map +1 -1
- package/dist/modules/orders/stale-order-cleanup.js +37 -16
- package/dist/modules/pricing/service.d.ts.map +1 -1
- package/dist/modules/pricing/service.js +5 -5
- package/dist/modules/promotions/repository/index.d.ts +1 -1
- package/dist/modules/promotions/repository/index.d.ts.map +1 -1
- package/dist/modules/promotions/repository/index.js +6 -3
- package/dist/modules/promotions/service.d.ts +1 -0
- package/dist/modules/promotions/service.d.ts.map +1 -1
- package/dist/modules/promotions/service.js +1 -1
- package/dist/modules/webhooks/repository/index.d.ts +1 -1
- package/dist/modules/webhooks/repository/index.d.ts.map +1 -1
- package/dist/modules/webhooks/repository/index.js +16 -10
- package/dist/runtime/kernel.d.ts.map +1 -1
- package/dist/runtime/kernel.js +14 -2
- package/dist/runtime/server.d.ts +1 -4
- package/dist/runtime/server.d.ts.map +1 -1
- package/dist/runtime/server.js +16 -34
- package/dist/test-utils/create-test-config.d.ts.map +1 -1
- package/dist/test-utils/create-test-config.js +2 -0
- package/dist/test-utils/order-test-helpers.d.ts +12 -0
- package/dist/test-utils/order-test-helpers.d.ts.map +1 -0
- package/dist/test-utils/order-test-helpers.js +18 -0
- package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
- package/dist/test-utils/rest-api-test-utils.js +1 -0
- package/dist/test-utils/test-actors.js +1 -1
- package/dist/testing.d.ts +1 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +1 -0
- package/package.json +1 -1
- package/src/auth/middleware.ts +26 -7
- package/src/auth/setup.ts +10 -4
- package/src/config/defaults.ts +3 -1
- package/src/config/types.ts +16 -3
- package/src/generated/plugin-capabilities.d.ts +1 -0
- package/src/generated/plugin-manifest.ts +1 -0
- package/src/generated/plugin-repositories.d.ts +1 -0
- package/src/hooks/checkout-completion.ts +22 -16
- package/src/hooks/checkout.ts +11 -8
- package/src/index.ts +31 -1
- package/src/interfaces/rest/router.ts +2 -0
- package/src/interfaces/rest/routes/admin/staff.ts +45 -3
- package/src/interfaces/rest/routes/checkout.ts +94 -28
- package/src/interfaces/rest/routes/inventory.ts +14 -0
- package/src/interfaces/rest/routes/orders.ts +11 -0
- package/src/interfaces/rest/schemas/orders.ts +2 -2
- package/src/kernel/database/scoped-db.ts +83 -15
- package/src/kernel/errors.ts +20 -0
- package/src/kernel/jobs/adapter.ts +39 -8
- package/src/kernel/jobs/drizzle-adapter.ts +72 -9
- package/src/kernel/jobs/runner.ts +194 -72
- package/src/kernel/jobs/types.ts +25 -0
- package/src/kernel/plugin/manifest.ts +15 -0
- package/src/modules/analytics/drizzle-adapter.ts +19 -3
- package/src/modules/cart/service.ts +3 -2
- package/src/modules/catalog/entity-service.ts +37 -2
- package/src/modules/catalog/repository/index.ts +13 -3
- package/src/modules/catalog/schema.ts +33 -18
- package/src/modules/catalog/schemas.ts +3 -1
- package/src/modules/channels/adapter.ts +139 -0
- package/src/modules/customers/service.ts +6 -0
- package/src/modules/inventory/repository/index.ts +4 -4
- package/src/modules/inventory/service.ts +12 -2
- package/src/modules/orders/schema.ts +4 -0
- package/src/modules/orders/service.ts +404 -64
- package/src/modules/orders/stale-order-cleanup.ts +52 -27
- package/src/modules/pricing/service.ts +5 -6
- package/src/modules/promotions/repository/index.ts +6 -2
- package/src/modules/promotions/service.ts +2 -0
- package/src/modules/webhooks/repository/index.ts +16 -14
- package/src/runtime/kernel.ts +16 -2
- package/src/runtime/server.ts +22 -42
- package/src/test-utils/create-test-config.ts +2 -0
- package/src/test-utils/order-test-helpers.ts +26 -0
- package/src/test-utils/rest-api-test-utils.ts +1 -0
- package/src/test-utils/test-actors.ts +1 -1
- package/src/testing.ts +1 -0
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import { eq, and, lt
|
|
1
|
+
import { eq, and, lt } from "drizzle-orm";
|
|
2
|
+
import type { Actor } from "../../auth/types.js";
|
|
2
3
|
import { orders } from "./schema.js";
|
|
3
|
-
import type { TaskDefinition
|
|
4
|
+
import type { TaskDefinition } from "../../kernel/jobs/types.js";
|
|
5
|
+
|
|
6
|
+
function systemActorForOrg(organizationId: string): Actor {
|
|
7
|
+
return {
|
|
8
|
+
type: "user",
|
|
9
|
+
userId: "system",
|
|
10
|
+
email: null,
|
|
11
|
+
name: "System",
|
|
12
|
+
vendorId: null,
|
|
13
|
+
organizationId,
|
|
14
|
+
role: "admin",
|
|
15
|
+
permissions: ["*:*"],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
4
18
|
|
|
5
19
|
/**
|
|
6
20
|
* Stale Order Cleanup Task
|
|
@@ -31,38 +45,49 @@ export const staleOrderCleanupTask: TaskDefinition<
|
|
|
31
45
|
const thresholdHours = input.thresholdHours ?? 48;
|
|
32
46
|
const cutoff = new Date(Date.now() - thresholdHours * 60 * 60 * 1000);
|
|
33
47
|
|
|
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
48
|
const cancelledIds: string[] = [];
|
|
46
49
|
|
|
47
50
|
// Cancel each stale order via the service (triggers inventory release + payment refund)
|
|
48
51
|
const orderService = ctx.services.orders as {
|
|
49
|
-
cancel(orderId: string, actor: null, reason: string): Promise<unknown>;
|
|
52
|
+
cancel(orderId: string, actor: Actor | null, reason: string): Promise<unknown>;
|
|
50
53
|
};
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
// This is a cross-org system sweep, but the row reads stay org-scoped: first
|
|
56
|
+
// enumerate the orgs that have stale orders (id-only projection), then read
|
|
57
|
+
// each org's stale orders under an explicit organizationId predicate. No
|
|
58
|
+
// query returns another tenant's order rows.
|
|
59
|
+
const staleOrgs = await ctx.db
|
|
60
|
+
.selectDistinct({ organizationId: orders.organizationId })
|
|
61
|
+
.from(orders)
|
|
62
|
+
.where(and(eq(orders.status, "pending"), lt(orders.placedAt, cutoff)));
|
|
63
|
+
|
|
64
|
+
for (const { organizationId } of staleOrgs) {
|
|
65
|
+
const staleOrders = await ctx.db
|
|
66
|
+
.select()
|
|
67
|
+
.from(orders)
|
|
68
|
+
.where(
|
|
69
|
+
and(
|
|
70
|
+
eq(orders.organizationId, organizationId),
|
|
71
|
+
eq(orders.status, "pending"),
|
|
72
|
+
lt(orders.placedAt, cutoff),
|
|
73
|
+
),
|
|
58
74
|
);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
|
|
76
|
+
for (const order of staleOrders) {
|
|
77
|
+
try {
|
|
78
|
+
await orderService.cancel(
|
|
79
|
+
order.id,
|
|
80
|
+
systemActorForOrg(order.organizationId),
|
|
81
|
+
`Auto-cancelled: pending for >${thresholdHours}h`,
|
|
82
|
+
);
|
|
83
|
+
cancelledIds.push(order.id);
|
|
84
|
+
ctx.logger.info(`Stale order ${order.orderNumber} auto-cancelled`, {
|
|
85
|
+
orderId: order.id,
|
|
86
|
+
placedAt: order.placedAt,
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
ctx.logger.error(`Failed to cancel stale order ${order.orderNumber}`, { error });
|
|
90
|
+
}
|
|
66
91
|
}
|
|
67
92
|
}
|
|
68
93
|
|
|
@@ -211,15 +211,14 @@ export class PricingService {
|
|
|
211
211
|
);
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
const
|
|
214
|
+
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
215
|
+
const entity = await this.catalogRepo.findEntityById(input.entityId, ctx, orgId);
|
|
215
216
|
if (!entity) {
|
|
216
217
|
return Err(
|
|
217
218
|
new CommerceNotFoundError("Entity not found for price assignment."),
|
|
218
219
|
);
|
|
219
220
|
}
|
|
220
221
|
|
|
221
|
-
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
222
|
-
|
|
223
222
|
const priceData: PriceInsert = {
|
|
224
223
|
organizationId: orgId,
|
|
225
224
|
entityId: input.entityId,
|
|
@@ -473,7 +472,8 @@ export class PricingService {
|
|
|
473
472
|
actor?: Actor | null,
|
|
474
473
|
ctx?: TxContext,
|
|
475
474
|
): Promise<Result<ResolvedPrice>> {
|
|
476
|
-
const
|
|
475
|
+
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
476
|
+
const entity = await this.catalogRepo.findEntityById(input.entityId, ctx, orgId);
|
|
477
477
|
if (!entity) {
|
|
478
478
|
return Err(
|
|
479
479
|
new CommerceNotFoundError(`Entity ${input.entityId} not found.`),
|
|
@@ -492,8 +492,7 @@ export class PricingService {
|
|
|
492
492
|
const currency = normalizeCurrency(input.currency);
|
|
493
493
|
const groupSet = toGroupSet(input);
|
|
494
494
|
|
|
495
|
-
// Get matching prices from repository
|
|
496
|
-
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
495
|
+
// Get matching prices from repository (orgId resolved above)
|
|
497
496
|
const allPrices = await this.repo.findPricesByEntityId(
|
|
498
497
|
orgId,
|
|
499
498
|
input.entityId,
|
|
@@ -182,6 +182,7 @@ export class PromotionsRepository {
|
|
|
182
182
|
|
|
183
183
|
async createUsage(
|
|
184
184
|
data: PromotionUsageInsert,
|
|
185
|
+
orgId: string,
|
|
185
186
|
ctx?: TxContext,
|
|
186
187
|
): Promise<PromotionUsage> {
|
|
187
188
|
const db = this.getDb(ctx);
|
|
@@ -190,10 +191,13 @@ export class PromotionsRepository {
|
|
|
190
191
|
// race conditions between concurrent checkouts using the same coupon.
|
|
191
192
|
// A plain count-then-insert has a TOCTOU gap; this single statement
|
|
192
193
|
// ensures the insert only succeeds if the limit has not been reached.
|
|
194
|
+
// Both reads are scoped by organizationId: these are raw/lock queries that
|
|
195
|
+
// bypass the scoped-db proxy, so a promotionId from another tenant must
|
|
196
|
+
// resolve to no row rather than reading/locking a foreign promotion.
|
|
193
197
|
const promo = await db
|
|
194
198
|
.select({ usageLimitTotal: promotions.usageLimitTotal })
|
|
195
199
|
.from(promotions)
|
|
196
|
-
.where(eq(promotions.id, data.promotionId));
|
|
200
|
+
.where(and(eq(promotions.id, data.promotionId), eq(promotions.organizationId, orgId)));
|
|
197
201
|
|
|
198
202
|
const limit = promo[0]?.usageLimitTotal;
|
|
199
203
|
|
|
@@ -206,7 +210,7 @@ export class PromotionsRepository {
|
|
|
206
210
|
// concurrent callers. If the caller is already inside a transaction
|
|
207
211
|
// (ctx.tx), the lock is held until that transaction commits.
|
|
208
212
|
await db.execute(
|
|
209
|
-
sql`SELECT id FROM promotions WHERE id = ${data.promotionId} FOR UPDATE`,
|
|
213
|
+
sql`SELECT id FROM promotions WHERE id = ${data.promotionId} AND organization_id = ${orgId} FOR UPDATE`,
|
|
210
214
|
);
|
|
211
215
|
|
|
212
216
|
const currentCount = await this.countUsages(data.promotionId, ctx);
|
|
@@ -500,6 +500,7 @@ export class PromotionService {
|
|
|
500
500
|
async recordUsage(
|
|
501
501
|
input: {
|
|
502
502
|
promotions: AppliedPromotion[];
|
|
503
|
+
organizationId: string;
|
|
503
504
|
customerId?: string;
|
|
504
505
|
orderId?: string;
|
|
505
506
|
},
|
|
@@ -513,6 +514,7 @@ export class PromotionService {
|
|
|
513
514
|
customerId: input.customerId ?? null,
|
|
514
515
|
orderId: input.orderId ?? null,
|
|
515
516
|
},
|
|
517
|
+
input.organizationId,
|
|
516
518
|
ctx,
|
|
517
519
|
);
|
|
518
520
|
usages.push(usage);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { eq, and, lte, isNull, or, desc, sql } from "drizzle-orm";
|
|
1
|
+
import { eq, and, lte, isNull, or, desc, sql, inArray } from "drizzle-orm";
|
|
2
2
|
import type { TxContext } from "../../../kernel/database/tx-context.js";
|
|
3
3
|
import type {
|
|
4
4
|
DrizzleDatabase,
|
|
@@ -195,28 +195,30 @@ export class WebhooksRepository {
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
async findFailedDeliveries(
|
|
198
|
+
orgId: string,
|
|
198
199
|
endpointId?: string,
|
|
199
200
|
ctx?: TxContext,
|
|
200
201
|
): Promise<WebhookDelivery[]> {
|
|
201
202
|
const db = this.getDb(ctx);
|
|
202
203
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
204
|
+
// webhook_deliveries has no organizationId column — org lives on the parent
|
|
205
|
+
// endpoint. Scope failed deliveries to endpoints owned by orgId so this can
|
|
206
|
+
// never read another tenant's deliveries.
|
|
207
|
+
const orgEndpointIds = db
|
|
208
|
+
.select({ id: webhookEndpoints.id })
|
|
209
|
+
.from(webhookEndpoints)
|
|
210
|
+
.where(eq(webhookEndpoints.organizationId, orgId));
|
|
211
|
+
|
|
212
|
+
const conditions = [
|
|
213
|
+
sql`${webhookDeliveries.failedAt} IS NOT NULL`,
|
|
214
|
+
inArray(webhookDeliveries.endpointId, orgEndpointIds),
|
|
215
|
+
];
|
|
216
|
+
if (endpointId) conditions.push(eq(webhookDeliveries.endpointId, endpointId));
|
|
215
217
|
|
|
216
218
|
return db
|
|
217
219
|
.select()
|
|
218
220
|
.from(webhookDeliveries)
|
|
219
|
-
.where(
|
|
221
|
+
.where(and(...conditions))
|
|
220
222
|
.orderBy(desc(webhookDeliveries.failedAt));
|
|
221
223
|
}
|
|
222
224
|
|
package/src/runtime/kernel.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { withTiming } from "../kernel/service-timing.js";
|
|
|
11
11
|
import { setBootDefaultOrgId } from "../auth/org.js";
|
|
12
12
|
import { setBootStrictOrgResolution } from "../auth/strict-org-resolution.js";
|
|
13
13
|
import { DrizzleJobsAdapter } from "../kernel/jobs/drizzle-adapter.js";
|
|
14
|
+
import type { ExecutionEngine } from "../kernel/jobs/adapter.js";
|
|
14
15
|
import { CompensationFailuresRepository } from "../kernel/compensation/repository.js";
|
|
15
16
|
|
|
16
17
|
import {
|
|
@@ -72,8 +73,21 @@ export function createKernel(config: CommerceConfig): Kernel {
|
|
|
72
73
|
const jobsTaskMap = new Map(
|
|
73
74
|
(config.jobs?.tasks ?? []).map((t) => [t.slug, t]),
|
|
74
75
|
);
|
|
75
|
-
const
|
|
76
|
-
|
|
76
|
+
const jobsEngine: ExecutionEngine =
|
|
77
|
+
config.jobs?.adapter ?? new DrizzleJobsAdapter(db);
|
|
78
|
+
const jobLogger = {
|
|
79
|
+
info: (message: string, data?: unknown) => logger.info(message, data),
|
|
80
|
+
warn: (message: string, data?: unknown) => logger.warn(message, data),
|
|
81
|
+
error: (message: string, data?: unknown) => logger.error(message, data),
|
|
82
|
+
};
|
|
83
|
+
jobsEngine.register({
|
|
84
|
+
tasks: jobsTaskMap,
|
|
85
|
+
context: { logger: jobLogger, db, services: serviceContainer },
|
|
86
|
+
...(config.jobs?.processingOrder !== undefined
|
|
87
|
+
? { processingOrder: config.jobs.processingOrder }
|
|
88
|
+
: {}),
|
|
89
|
+
});
|
|
90
|
+
serviceContainer.jobs = jobsEngine;
|
|
77
91
|
|
|
78
92
|
const topoGraph = kernelModulesForTopoSort();
|
|
79
93
|
const order = topoSortModules(topoGraph);
|
package/src/runtime/server.ts
CHANGED
|
@@ -17,7 +17,6 @@ import { createCustomerPortalRoutes } from "../interfaces/rest/customer-portal.j
|
|
|
17
17
|
import { createKernel } from "./kernel.js";
|
|
18
18
|
import { ensureDefaultOrg } from "../auth/org.js";
|
|
19
19
|
import { createLogger, type Logger } from "./logger.js";
|
|
20
|
-
import type { DrizzleDatabase } from "../kernel/database/drizzle-db.js";
|
|
21
20
|
import { createCommerce, type CommerceInstance } from "./commerce.js";
|
|
22
21
|
import { rewriteCommerceAliasRequest } from "./url-alias-rewrite.js";
|
|
23
22
|
import { mapErrorToResponse } from "../kernel/error-mapper.js";
|
|
@@ -408,48 +407,18 @@ export async function createServer(config: CommerceConfig) {
|
|
|
408
407
|
// 2. GET /api/jobs/run: cron endpoint (serverless — Vercel, Cloudflare)
|
|
409
408
|
// 3. runPendingJobs() export (custom worker process)
|
|
410
409
|
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
await import("../kernel/jobs/reaper.js");
|
|
414
|
-
const taskMap = new Map<string, unknown>();
|
|
415
|
-
for (const task of config.jobs?.tasks ?? []) {
|
|
416
|
-
const t = task as { slug?: string; name?: string };
|
|
417
|
-
taskMap.set(t.slug ?? t.name ?? "", task);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const jobLogger = {
|
|
421
|
-
info: (msg: string, data?: unknown) => logger.info(data != null ? { data } : {}, msg),
|
|
422
|
-
warn: (msg: string, data?: unknown) => logger.warn(data != null ? { data } : {}, msg),
|
|
423
|
-
error: (msg: string, data?: unknown) => logger.error(data != null ? { data } : {}, msg),
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
let lastStaleJobReaperAt = 0;
|
|
410
|
+
const jobsEngine = (kernel.services as Record<string, unknown>)
|
|
411
|
+
.jobs as import("../kernel/jobs/adapter.js").ExecutionEngine;
|
|
427
412
|
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
lastStaleJobReaperAt = now;
|
|
433
|
-
try {
|
|
434
|
-
await runStaleJobReaper(
|
|
435
|
-
kernel.database.db as DrizzleDatabase,
|
|
436
|
-
getJobReapThresholdMs(),
|
|
437
|
-
jobLogger,
|
|
413
|
+
const runJobs = async (queue?: string, limit?: number) => {
|
|
414
|
+
if (jobsEngine.execution.mode !== "pull") {
|
|
415
|
+
throw new Error(
|
|
416
|
+
"The configured push execution engine is driven by its native runtime and cannot be polled with runJobs().",
|
|
438
417
|
);
|
|
439
|
-
} catch (err) {
|
|
440
|
-
logger.error({ err }, "Stale job reaper failed");
|
|
441
418
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const runJobs = async (queue?: string, limit?: number) => {
|
|
445
|
-
await maybeRunStaleJobReaper();
|
|
446
|
-
return runPendingJobs({
|
|
447
|
-
db: kernel.database.db as DrizzleDatabase,
|
|
448
|
-
tasks: taskMap as Parameters<typeof runPendingJobs>[0]["tasks"],
|
|
419
|
+
return jobsEngine.execution.run({
|
|
449
420
|
queue: queue ?? "default",
|
|
450
421
|
limit: limit ?? 10,
|
|
451
|
-
logger: jobLogger,
|
|
452
|
-
services: kernel.services as Parameters<typeof runPendingJobs>[0]["services"],
|
|
453
422
|
});
|
|
454
423
|
};
|
|
455
424
|
|
|
@@ -478,6 +447,11 @@ export async function createServer(config: CommerceConfig) {
|
|
|
478
447
|
// Strategy 2: In-process polling for long-running servers (ECS, Cloud Run, Docker).
|
|
479
448
|
// Enable via config.jobs.autorun.enabled = true
|
|
480
449
|
if (config.jobs?.autorun?.enabled) {
|
|
450
|
+
if (jobsEngine.execution.mode !== "pull") {
|
|
451
|
+
throw new Error(
|
|
452
|
+
"jobs.autorun is only valid for pull execution engines. Serve the configured push engine through its native runtime.",
|
|
453
|
+
);
|
|
454
|
+
}
|
|
481
455
|
const intervalMs = config.jobs.autorun.intervalMs ?? 10_000;
|
|
482
456
|
|
|
483
457
|
const jobInterval = setInterval(async () => {
|
|
@@ -494,10 +468,16 @@ export async function createServer(config: CommerceConfig) {
|
|
|
494
468
|
|
|
495
469
|
logger.info({ intervalMs }, "Job queue autorun started (in-process polling)");
|
|
496
470
|
} else {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
471
|
+
if (jobsEngine.execution.mode === "push") {
|
|
472
|
+
logger.info(
|
|
473
|
+
"Push job engine registered. Serve it through its native runtime.",
|
|
474
|
+
);
|
|
475
|
+
} else {
|
|
476
|
+
logger.info(
|
|
477
|
+
"Job queue autorun disabled. Use GET /api/jobs/run for serverless cron, " +
|
|
478
|
+
"or set config.jobs.autorun.enabled = true for in-process polling.",
|
|
479
|
+
);
|
|
480
|
+
}
|
|
501
481
|
}
|
|
502
482
|
|
|
503
483
|
const dispatchFetch = app.fetch.bind(app);
|
|
@@ -64,6 +64,7 @@ export async function createTestConfig(
|
|
|
64
64
|
provider: "postgresql",
|
|
65
65
|
},
|
|
66
66
|
auth: {
|
|
67
|
+
allowTestActor: true,
|
|
67
68
|
defaultOrganizationId: "org_default",
|
|
68
69
|
requireEmailVerification: false,
|
|
69
70
|
apiKeys: { enabled: true, defaultPermissions: ["catalog:read"] },
|
|
@@ -81,6 +82,7 @@ export async function createTestConfig(
|
|
|
81
82
|
"orders:create",
|
|
82
83
|
"orders:read",
|
|
83
84
|
"orders:update",
|
|
85
|
+
"orders:manage",
|
|
84
86
|
"cart:create",
|
|
85
87
|
"cart:update",
|
|
86
88
|
"customers:update:self",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { orders } from "../modules/orders/schema.js";
|
|
3
|
+
import type { Kernel } from "../runtime/kernel.js";
|
|
4
|
+
import type { DrizzleDatabase } from "../kernel/database/drizzle-db.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Mark an order as paid (captured) for tests.
|
|
8
|
+
*
|
|
9
|
+
* Refund and return flows require a paid order — a refund moves collected money,
|
|
10
|
+
* and the order primitive rejects refunds on unpaid/pending orders. There is no
|
|
11
|
+
* public "set captured amount" service (capture() needs a live payment intent),
|
|
12
|
+
* so tests set the column here through a single typed helper rather than
|
|
13
|
+
* scattering raw SQL / `as any` casts across test files.
|
|
14
|
+
*/
|
|
15
|
+
export async function markOrderPaidForTest(
|
|
16
|
+
kernel: Kernel,
|
|
17
|
+
orderId: string,
|
|
18
|
+
amountCaptured: number,
|
|
19
|
+
paymentIntentId?: string,
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
const db = kernel.database.db as DrizzleDatabase;
|
|
22
|
+
await db
|
|
23
|
+
.update(orders)
|
|
24
|
+
.set({ amountCaptured, ...(paymentIntentId ? { paymentIntentId } : {}) })
|
|
25
|
+
.where(eq(orders.id, orderId));
|
|
26
|
+
}
|
|
@@ -32,7 +32,7 @@ export const testStaffActor: Actor = {
|
|
|
32
32
|
role: "staff",
|
|
33
33
|
permissions: [
|
|
34
34
|
"catalog:read", "catalog:create", "catalog:update",
|
|
35
|
-
"inventory:adjust", "orders:read", "orders:create", "orders:update",
|
|
35
|
+
"inventory:adjust", "orders:read", "orders:create", "orders:update", "orders:manage",
|
|
36
36
|
],
|
|
37
37
|
};
|
|
38
38
|
|
package/src/testing.ts
CHANGED
|
@@ -16,6 +16,7 @@ export {
|
|
|
16
16
|
jsonHeaders,
|
|
17
17
|
} from "./test-utils/test-actors.js";
|
|
18
18
|
export { beforeHook, afterHook } from "./test-utils/typed-hooks.js";
|
|
19
|
+
export { markOrderPaidForTest } from "./test-utils/order-test-helpers.js";
|
|
19
20
|
|
|
20
21
|
// Actor type re-export for plugin tests that build custom test actors.
|
|
21
22
|
export type { Actor } from "./auth/types.js";
|