@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.
Files changed (162) hide show
  1. package/dist/auth/middleware.d.ts.map +1 -1
  2. package/dist/auth/middleware.js +26 -7
  3. package/dist/auth/setup.d.ts.map +1 -1
  4. package/dist/auth/setup.js +7 -5
  5. package/dist/config/defaults.d.ts.map +1 -1
  6. package/dist/config/defaults.js +3 -1
  7. package/dist/config/types.d.ts +13 -3
  8. package/dist/config/types.d.ts.map +1 -1
  9. package/dist/generated/plugin-manifest.d.ts +3 -0
  10. package/dist/generated/plugin-manifest.d.ts.map +1 -1
  11. package/dist/generated/plugin-manifest.js +1 -0
  12. package/dist/hooks/checkout-completion.d.ts.map +1 -1
  13. package/dist/hooks/checkout-completion.js +2 -2
  14. package/dist/hooks/checkout.d.ts.map +1 -1
  15. package/dist/hooks/checkout.js +1 -1
  16. package/dist/index.d.ts +5 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -0
  19. package/dist/interfaces/rest/router.d.ts.map +1 -1
  20. package/dist/interfaces/rest/router.js +2 -0
  21. package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -1
  22. package/dist/interfaces/rest/routes/admin/staff.js +37 -3
  23. package/dist/interfaces/rest/routes/checkout.d.ts +9 -0
  24. package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
  25. package/dist/interfaces/rest/routes/checkout.js +82 -22
  26. package/dist/interfaces/rest/routes/inventory.d.ts.map +1 -1
  27. package/dist/interfaces/rest/routes/inventory.js +13 -0
  28. package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
  29. package/dist/interfaces/rest/routes/orders.js +13 -0
  30. package/dist/interfaces/rest/schemas/catalog.d.ts +391 -0
  31. package/dist/interfaces/rest/schemas/catalog.d.ts.map +1 -1
  32. package/dist/interfaces/rest/schemas/orders.js +2 -2
  33. package/dist/interfaces/rest/schemas/responses.d.ts +85 -0
  34. package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
  35. package/dist/interfaces/rest/schemas/search.d.ts +17 -0
  36. package/dist/interfaces/rest/schemas/search.d.ts.map +1 -1
  37. package/dist/kernel/database/scoped-db.d.ts +2 -1
  38. package/dist/kernel/database/scoped-db.d.ts.map +1 -1
  39. package/dist/kernel/database/scoped-db.js +74 -15
  40. package/dist/kernel/errors.d.ts +12 -0
  41. package/dist/kernel/errors.d.ts.map +1 -1
  42. package/dist/kernel/errors.js +16 -0
  43. package/dist/kernel/jobs/adapter.d.ts +27 -8
  44. package/dist/kernel/jobs/adapter.d.ts.map +1 -1
  45. package/dist/kernel/jobs/adapter.js +1 -2
  46. package/dist/kernel/jobs/drizzle-adapter.d.ts +13 -3
  47. package/dist/kernel/jobs/drizzle-adapter.d.ts.map +1 -1
  48. package/dist/kernel/jobs/drizzle-adapter.js +46 -5
  49. package/dist/kernel/jobs/runner.d.ts +2 -1
  50. package/dist/kernel/jobs/runner.d.ts.map +1 -1
  51. package/dist/kernel/jobs/runner.js +100 -15
  52. package/dist/kernel/jobs/types.d.ts +13 -0
  53. package/dist/kernel/jobs/types.d.ts.map +1 -1
  54. package/dist/kernel/plugin/manifest.d.ts +5 -0
  55. package/dist/kernel/plugin/manifest.d.ts.map +1 -1
  56. package/dist/kernel/plugin/manifest.js +9 -0
  57. package/dist/modules/analytics/drizzle-adapter.d.ts.map +1 -1
  58. package/dist/modules/analytics/drizzle-adapter.js +16 -3
  59. package/dist/modules/cart/service.d.ts.map +1 -1
  60. package/dist/modules/cart/service.js +3 -2
  61. package/dist/modules/catalog/entity-service.d.ts +1 -0
  62. package/dist/modules/catalog/entity-service.d.ts.map +1 -1
  63. package/dist/modules/catalog/entity-service.js +49 -3
  64. package/dist/modules/catalog/repository/index.d.ts +2 -2
  65. package/dist/modules/catalog/repository/index.d.ts.map +1 -1
  66. package/dist/modules/catalog/repository/index.js +9 -1
  67. package/dist/modules/catalog/schema.d.ts +51 -0
  68. package/dist/modules/catalog/schema.d.ts.map +1 -1
  69. package/dist/modules/catalog/schema.js +12 -1
  70. package/dist/modules/catalog/schemas.d.ts +3 -1
  71. package/dist/modules/catalog/schemas.d.ts.map +1 -1
  72. package/dist/modules/channels/adapter.d.ts +117 -0
  73. package/dist/modules/channels/adapter.d.ts.map +1 -0
  74. package/dist/modules/channels/adapter.js +3 -0
  75. package/dist/modules/customers/service.d.ts.map +1 -1
  76. package/dist/modules/customers/service.js +6 -0
  77. package/dist/modules/inventory/repository/index.js +4 -4
  78. package/dist/modules/inventory/service.d.ts.map +1 -1
  79. package/dist/modules/inventory/service.js +7 -3
  80. package/dist/modules/orders/schema.d.ts +17 -0
  81. package/dist/modules/orders/schema.d.ts.map +1 -1
  82. package/dist/modules/orders/schema.js +4 -0
  83. package/dist/modules/orders/service.d.ts +16 -2
  84. package/dist/modules/orders/service.d.ts.map +1 -1
  85. package/dist/modules/orders/service.js +226 -26
  86. package/dist/modules/orders/stale-order-cleanup.d.ts.map +1 -1
  87. package/dist/modules/orders/stale-order-cleanup.js +37 -16
  88. package/dist/modules/pricing/service.d.ts.map +1 -1
  89. package/dist/modules/pricing/service.js +5 -5
  90. package/dist/modules/promotions/repository/index.d.ts +1 -1
  91. package/dist/modules/promotions/repository/index.d.ts.map +1 -1
  92. package/dist/modules/promotions/repository/index.js +6 -3
  93. package/dist/modules/promotions/service.d.ts +1 -0
  94. package/dist/modules/promotions/service.d.ts.map +1 -1
  95. package/dist/modules/promotions/service.js +1 -1
  96. package/dist/modules/webhooks/repository/index.d.ts +1 -1
  97. package/dist/modules/webhooks/repository/index.d.ts.map +1 -1
  98. package/dist/modules/webhooks/repository/index.js +16 -10
  99. package/dist/runtime/kernel.d.ts.map +1 -1
  100. package/dist/runtime/kernel.js +14 -2
  101. package/dist/runtime/server.d.ts +1 -4
  102. package/dist/runtime/server.d.ts.map +1 -1
  103. package/dist/runtime/server.js +16 -34
  104. package/dist/test-utils/create-test-config.d.ts.map +1 -1
  105. package/dist/test-utils/create-test-config.js +2 -0
  106. package/dist/test-utils/order-test-helpers.d.ts +12 -0
  107. package/dist/test-utils/order-test-helpers.d.ts.map +1 -0
  108. package/dist/test-utils/order-test-helpers.js +18 -0
  109. package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
  110. package/dist/test-utils/rest-api-test-utils.js +1 -0
  111. package/dist/test-utils/test-actors.js +1 -1
  112. package/dist/testing.d.ts +1 -0
  113. package/dist/testing.d.ts.map +1 -1
  114. package/dist/testing.js +1 -0
  115. package/package.json +1 -1
  116. package/src/auth/middleware.ts +26 -7
  117. package/src/auth/setup.ts +10 -4
  118. package/src/config/defaults.ts +3 -1
  119. package/src/config/types.ts +16 -3
  120. package/src/generated/plugin-capabilities.d.ts +1 -0
  121. package/src/generated/plugin-manifest.ts +1 -0
  122. package/src/generated/plugin-repositories.d.ts +1 -0
  123. package/src/hooks/checkout-completion.ts +22 -16
  124. package/src/hooks/checkout.ts +11 -8
  125. package/src/index.ts +31 -1
  126. package/src/interfaces/rest/router.ts +2 -0
  127. package/src/interfaces/rest/routes/admin/staff.ts +45 -3
  128. package/src/interfaces/rest/routes/checkout.ts +94 -28
  129. package/src/interfaces/rest/routes/inventory.ts +14 -0
  130. package/src/interfaces/rest/routes/orders.ts +11 -0
  131. package/src/interfaces/rest/schemas/orders.ts +2 -2
  132. package/src/kernel/database/scoped-db.ts +83 -15
  133. package/src/kernel/errors.ts +20 -0
  134. package/src/kernel/jobs/adapter.ts +39 -8
  135. package/src/kernel/jobs/drizzle-adapter.ts +72 -9
  136. package/src/kernel/jobs/runner.ts +194 -72
  137. package/src/kernel/jobs/types.ts +25 -0
  138. package/src/kernel/plugin/manifest.ts +15 -0
  139. package/src/modules/analytics/drizzle-adapter.ts +19 -3
  140. package/src/modules/cart/service.ts +3 -2
  141. package/src/modules/catalog/entity-service.ts +37 -2
  142. package/src/modules/catalog/repository/index.ts +13 -3
  143. package/src/modules/catalog/schema.ts +33 -18
  144. package/src/modules/catalog/schemas.ts +3 -1
  145. package/src/modules/channels/adapter.ts +139 -0
  146. package/src/modules/customers/service.ts +6 -0
  147. package/src/modules/inventory/repository/index.ts +4 -4
  148. package/src/modules/inventory/service.ts +12 -2
  149. package/src/modules/orders/schema.ts +4 -0
  150. package/src/modules/orders/service.ts +404 -64
  151. package/src/modules/orders/stale-order-cleanup.ts +52 -27
  152. package/src/modules/pricing/service.ts +5 -6
  153. package/src/modules/promotions/repository/index.ts +6 -2
  154. package/src/modules/promotions/service.ts +2 -0
  155. package/src/modules/webhooks/repository/index.ts +16 -14
  156. package/src/runtime/kernel.ts +16 -2
  157. package/src/runtime/server.ts +22 -42
  158. package/src/test-utils/create-test-config.ts +2 -0
  159. package/src/test-utils/order-test-helpers.ts +26 -0
  160. package/src/test-utils/rest-api-test-utils.ts +1 -0
  161. package/src/test-utils/test-actors.ts +1 -1
  162. package/src/testing.ts +1 -0
@@ -1,6 +1,20 @@
1
- import { eq, and, lt, sql } from "drizzle-orm";
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, TaskContext } from "../../kernel/jobs/types.js";
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
- for (const order of staleOrders) {
53
- try {
54
- await orderService.cancel(
55
- order.id,
56
- null,
57
- `Auto-cancelled: pending for >${thresholdHours}h`,
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
- 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 });
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 entity = await this.catalogRepo.findEntityById(input.entityId, ctx);
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 entity = await this.catalogRepo.findEntityById(input.entityId, ctx);
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
- if (endpointId) {
204
- return db
205
- .select()
206
- .from(webhookDeliveries)
207
- .where(
208
- and(
209
- eq(webhookDeliveries.endpointId, endpointId),
210
- sql`${webhookDeliveries.failedAt} IS NOT NULL`,
211
- ),
212
- )
213
- .orderBy(desc(webhookDeliveries.failedAt));
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(sql`${webhookDeliveries.failedAt} IS NOT NULL`)
221
+ .where(and(...conditions))
220
222
  .orderBy(desc(webhookDeliveries.failedAt));
221
223
  }
222
224
 
@@ -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 jobsAdapter = new DrizzleJobsAdapter(db, jobsTaskMap);
76
- serviceContainer.jobs = jobsAdapter;
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);
@@ -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 { runPendingJobs } = await import("../kernel/jobs/runner.js");
412
- const { runStaleJobReaper, getJobReapThresholdMs, getJobsReaperIntervalMs } =
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 maybeRunStaleJobReaper = async () => {
429
- const interval = getJobsReaperIntervalMs();
430
- const now = Date.now();
431
- if (now - lastStaleJobReaperAt < interval) return;
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
- logger.info(
498
- "Job queue autorun disabled. Use GET /api/jobs/run for serverless cron, " +
499
- "or set config.jobs.autorun.enabled = true for in-process polling.",
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
+ }
@@ -163,6 +163,7 @@ export const testActor: Actor = {
163
163
  "orders:create",
164
164
  "orders:read",
165
165
  "orders:update",
166
+ "orders:manage",
166
167
  "cart:create",
167
168
  "cart:update",
168
169
  "cart:read",
@@ -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";