@porulle/core 0.8.0 → 0.9.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 (85) 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 +2 -0
  7. package/dist/config/types.d.ts +9 -0
  8. package/dist/config/types.d.ts.map +1 -1
  9. package/dist/hooks/checkout-completion.d.ts.map +1 -1
  10. package/dist/hooks/checkout-completion.js +2 -2
  11. package/dist/hooks/checkout.d.ts.map +1 -1
  12. package/dist/hooks/checkout.js +1 -1
  13. package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -1
  14. package/dist/interfaces/rest/routes/admin/staff.js +37 -3
  15. package/dist/interfaces/rest/routes/checkout.d.ts +9 -0
  16. package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
  17. package/dist/interfaces/rest/routes/checkout.js +81 -22
  18. package/dist/interfaces/rest/routes/inventory.d.ts.map +1 -1
  19. package/dist/interfaces/rest/routes/inventory.js +13 -0
  20. package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
  21. package/dist/interfaces/rest/routes/orders.js +13 -0
  22. package/dist/interfaces/rest/schemas/orders.js +2 -2
  23. package/dist/interfaces/rest/schemas/responses.d.ts +34 -0
  24. package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
  25. package/dist/kernel/database/scoped-db.d.ts +2 -1
  26. package/dist/kernel/database/scoped-db.d.ts.map +1 -1
  27. package/dist/kernel/database/scoped-db.js +62 -1
  28. package/dist/kernel/plugin/manifest.d.ts +3 -0
  29. package/dist/kernel/plugin/manifest.d.ts.map +1 -1
  30. package/dist/modules/analytics/drizzle-adapter.d.ts.map +1 -1
  31. package/dist/modules/analytics/drizzle-adapter.js +16 -3
  32. package/dist/modules/cart/service.d.ts.map +1 -1
  33. package/dist/modules/cart/service.js +3 -2
  34. package/dist/modules/catalog/entity-service.d.ts.map +1 -1
  35. package/dist/modules/catalog/entity-service.js +34 -2
  36. package/dist/modules/customers/service.d.ts.map +1 -1
  37. package/dist/modules/customers/service.js +6 -0
  38. package/dist/modules/orders/schema.d.ts +17 -0
  39. package/dist/modules/orders/schema.d.ts.map +1 -1
  40. package/dist/modules/orders/schema.js +4 -0
  41. package/dist/modules/orders/service.d.ts +16 -2
  42. package/dist/modules/orders/service.d.ts.map +1 -1
  43. package/dist/modules/orders/service.js +223 -26
  44. package/dist/modules/orders/stale-order-cleanup.d.ts.map +1 -1
  45. package/dist/modules/orders/stale-order-cleanup.js +13 -1
  46. package/dist/modules/pricing/service.d.ts.map +1 -1
  47. package/dist/modules/pricing/service.js +5 -5
  48. package/dist/test-utils/create-test-config.d.ts.map +1 -1
  49. package/dist/test-utils/create-test-config.js +2 -0
  50. package/dist/test-utils/order-test-helpers.d.ts +12 -0
  51. package/dist/test-utils/order-test-helpers.d.ts.map +1 -0
  52. package/dist/test-utils/order-test-helpers.js +18 -0
  53. package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
  54. package/dist/test-utils/rest-api-test-utils.js +1 -0
  55. package/dist/test-utils/test-actors.js +1 -1
  56. package/dist/testing.d.ts +1 -0
  57. package/dist/testing.d.ts.map +1 -1
  58. package/dist/testing.js +1 -0
  59. package/package.json +1 -1
  60. package/src/auth/middleware.ts +26 -7
  61. package/src/auth/setup.ts +10 -4
  62. package/src/config/defaults.ts +2 -0
  63. package/src/config/types.ts +9 -0
  64. package/src/hooks/checkout-completion.ts +22 -16
  65. package/src/hooks/checkout.ts +11 -8
  66. package/src/interfaces/rest/routes/admin/staff.ts +45 -3
  67. package/src/interfaces/rest/routes/checkout.ts +93 -28
  68. package/src/interfaces/rest/routes/inventory.ts +14 -0
  69. package/src/interfaces/rest/routes/orders.ts +11 -0
  70. package/src/interfaces/rest/schemas/orders.ts +2 -2
  71. package/src/kernel/database/scoped-db.ts +70 -1
  72. package/src/kernel/plugin/manifest.ts +3 -0
  73. package/src/modules/analytics/drizzle-adapter.ts +19 -3
  74. package/src/modules/cart/service.ts +3 -2
  75. package/src/modules/catalog/entity-service.ts +28 -1
  76. package/src/modules/customers/service.ts +6 -0
  77. package/src/modules/orders/schema.ts +4 -0
  78. package/src/modules/orders/service.ts +408 -64
  79. package/src/modules/orders/stale-order-cleanup.ts +18 -4
  80. package/src/modules/pricing/service.ts +5 -6
  81. package/src/test-utils/create-test-config.ts +2 -0
  82. package/src/test-utils/order-test-helpers.ts +26 -0
  83. package/src/test-utils/rest-api-test-utils.ts +1 -0
  84. package/src/test-utils/test-actors.ts +1 -1
  85. package/src/testing.ts +1 -0
@@ -7,6 +7,7 @@ import { canTransition, orderStateMachine, } from "../../kernel/state-machine/ma
7
7
  import { Err, Ok } from "../../kernel/result.js";
8
8
  import { createLogger } from "../../utils/logger.js";
9
9
  import { paginate } from "../../utils/pagination.js";
10
+ import { withTransaction, } from "../../kernel/database/tx-context.js";
10
11
  function context(actor, services, database, tx = null) {
11
12
  return createHookContext({
12
13
  actor,
@@ -30,7 +31,70 @@ export class OrderService {
30
31
  const lineItems = await this.repo.findLineItemsByOrderId(order.id, ctx);
31
32
  return { ...order, lineItems };
32
33
  }
33
- async create(input, actor, ctx) {
34
+ /**
35
+ * Resolve every line item's price with org-integrity + price provenance,
36
+ * mirroring how our forefathers handle this (Vendure: order lines are always
37
+ * server-priced from the variant; Medusa: an admin override is honored but
38
+ * flagged `is_custom_price`). Three modes:
39
+ * - trusted caller (checkout / POS exchange, already server-priced) → prices honored as-is.
40
+ * - actor with `orders:manage` → explicit price honored as a manual override, flagged isCustomPrice.
41
+ * - otherwise → price re-derived server-side from the catalog; client prices ignored.
42
+ * The referenced entity MUST belong to the order's org in every mode.
43
+ */
44
+ async resolveLines(input, actor, trusted, ctx) {
45
+ let canOverride = trusted;
46
+ if (!canOverride) {
47
+ try {
48
+ assertPermission(actor, "orders:manage");
49
+ canOverride = true;
50
+ }
51
+ catch {
52
+ canOverride = false;
53
+ }
54
+ }
55
+ const catalog = this.deps.services.catalog;
56
+ const pricing = this.deps.services.pricing;
57
+ const resolved = [];
58
+ for (const item of input.lineItems) {
59
+ // Org-integrity — the referenced entity must belong to this order's org.
60
+ const owned = await catalog.getById(item.entityId, { includeVariants: item.variantId !== undefined }, actor, ctx);
61
+ if (!owned.ok) {
62
+ return Err(new CommerceValidationError(`Line item entity ${item.entityId} does not belong to this organization.`));
63
+ }
64
+ if (item.variantId !== undefined &&
65
+ !owned.value?.variants?.some((variant) => variant.id === item.variantId)) {
66
+ return Err(new CommerceValidationError(`Line item variant ${item.variantId} does not belong to entity ${item.entityId}.`));
67
+ }
68
+ if (trusted || canOverride) {
69
+ // Trusted pipeline totals, or a staff manual/negotiated override.
70
+ resolved.push({ ...item, isCustomPrice: !trusted && canOverride });
71
+ continue;
72
+ }
73
+ // Server-derive the price; the client-supplied unitPrice/totalPrice is ignored.
74
+ const priced = await pricing.resolve({
75
+ entityId: item.entityId,
76
+ currency: input.currency,
77
+ quantity: item.quantity,
78
+ ...(item.variantId ? { variantId: item.variantId } : {}),
79
+ ...(input.customerId ? { customerId: input.customerId } : {}),
80
+ }, actor, ctx);
81
+ if (!priced.ok || !priced.value) {
82
+ return Err(new CommerceValidationError(`Cannot resolve price for ${item.entityId}.`));
83
+ }
84
+ const unitPrice = priced.value.finalAmount;
85
+ resolved.push({
86
+ ...item,
87
+ unitPrice,
88
+ totalPrice: unitPrice * item.quantity,
89
+ isCustomPrice: false,
90
+ });
91
+ }
92
+ return Ok(resolved);
93
+ }
94
+ async create(input, actor, ctx, opts) {
95
+ if (opts?.stockPolicy === "reserve" && !ctx) {
96
+ return withTransaction(this.deps.database, { actor }, (txCtx) => this.create(input, actor, txCtx, opts));
97
+ }
34
98
  try {
35
99
  assertPermission(actor, "orders:create");
36
100
  }
@@ -53,6 +117,38 @@ export class OrderService {
53
117
  return Ok(await this.hydrateOrder(existing, ctx));
54
118
  }
55
119
  }
120
+ // Resolve line prices with org-integrity + provenance (see resolveLines).
121
+ const trusted = opts?.trustedPricing === true;
122
+ const resolvedResult = await this.resolveLines(processed, actor, trusted, ctx);
123
+ if (!resolvedResult.ok)
124
+ return Err(resolvedResult.error);
125
+ const resolvedLines = resolvedResult.value;
126
+ // Order totals: trusted callers (checkout / exchange) supply rich totals
127
+ // (promotions, tax, shipping); otherwise derive from the server-priced lines.
128
+ const computedSubtotal = trusted
129
+ ? processed.subtotal
130
+ : resolvedLines.reduce((sum, l) => sum + (l.totalPrice ?? l.unitPrice * l.quantity), 0);
131
+ const computedGrandTotal = trusted
132
+ ? processed.grandTotal
133
+ : computedSubtotal +
134
+ processed.taxTotal +
135
+ processed.shippingTotal -
136
+ (processed.discountTotal ?? 0);
137
+ // Stock: order creation does NOT reserve by default. Stock is allocated at
138
+ // the lifecycle transition to a committed state — checkout reserves in its
139
+ // own pipeline, mirroring Vendure's StockAllocationStrategy (allocate on
140
+ // PaymentAuthorized, not on creation). A caller that represents an immediate
141
+ // committed sale (e.g. a POS exchange) opts in with stockPolicy: "reserve".
142
+ const stockPolicy = opts?.stockPolicy;
143
+ if (stockPolicy === "reserve") {
144
+ const inventory = this.deps.services.inventory;
145
+ for (const line of resolvedLines) {
146
+ const avail = await inventory.getAvailable(line.entityId, line.variantId, ctx, actor);
147
+ if (!avail.ok || (avail.value ?? 0) < line.quantity) {
148
+ return Err(new CommerceValidationError(`Insufficient stock for ${line.title ?? line.entityId}. Available: ${avail.ok ? (avail.value ?? 0) : 0}, requested: ${line.quantity}.`));
149
+ }
150
+ }
151
+ }
56
152
  const orderNumber = await this.repo.getNextOrderNumber(ctx);
57
153
  let order;
58
154
  try {
@@ -61,11 +157,11 @@ export class OrderService {
61
157
  orderNumber,
62
158
  status: "pending",
63
159
  currency: processed.currency,
64
- subtotal: processed.subtotal,
160
+ subtotal: computedSubtotal,
65
161
  taxTotal: processed.taxTotal,
66
162
  shippingTotal: processed.shippingTotal,
67
163
  discountTotal: processed.discountTotal ?? 0,
68
- grandTotal: processed.grandTotal,
164
+ grandTotal: computedGrandTotal,
69
165
  ...(processed.paymentIntentId != null ? { paymentIntentId: processed.paymentIntentId } : {}),
70
166
  ...(processed.paymentMethodId != null ? { paymentMethodId: processed.paymentMethodId } : {}),
71
167
  ...(processed.idempotencyKey != null ? { idempotencyKey: processed.idempotencyKey } : {}),
@@ -85,14 +181,15 @@ export class OrderService {
85
181
  }
86
182
  throw error;
87
183
  }
88
- const lineItemsData = processed.lineItems.map((item) => ({
184
+ const lineItemsData = resolvedLines.map((item) => ({
89
185
  orderId: order.id,
90
186
  entityId: item.entityId,
91
187
  entityType: item.entityType,
92
188
  title: item.title,
93
189
  quantity: item.quantity,
94
190
  unitPrice: item.unitPrice,
95
- totalPrice: item.totalPrice,
191
+ totalPrice: item.totalPrice ?? item.unitPrice * item.quantity,
192
+ isCustomPrice: item.isCustomPrice,
96
193
  taxAmount: item.taxAmount ?? 0,
97
194
  discountAmount: item.discountAmount ?? 0,
98
195
  fulfillmentStatus: "unfulfilled",
@@ -101,6 +198,23 @@ export class OrderService {
101
198
  ...(item.sku !== undefined ? { sku: item.sku } : {}),
102
199
  }));
103
200
  await this.repo.createLineItems(lineItemsData, ctx);
201
+ // Reserve stock for the resolved lines when the reserve policy is active
202
+ // (availability was verified above). Trusted callers reserve themselves.
203
+ if (stockPolicy === "reserve") {
204
+ const inventory = this.deps.services.inventory;
205
+ for (const line of resolvedLines) {
206
+ const reserved = await inventory.reserve({
207
+ entityId: line.entityId,
208
+ quantity: line.quantity,
209
+ orderId: order.id,
210
+ ...(line.variantId ? { variantId: line.variantId } : {}),
211
+ }, actor, ctx);
212
+ if (!reserved.ok) {
213
+ return Err(toCommerceError(reserved.error ??
214
+ new CommerceValidationError(`Unable to reserve stock for ${line.title ?? line.entityId}.`)));
215
+ }
216
+ }
217
+ }
104
218
  await this.repo.createStatusHistory({
105
219
  orderId: order.id,
106
220
  fromStatus: "pending",
@@ -270,6 +384,12 @@ export class OrderService {
270
384
  await runBeforeHooks(beforeHooks, statusHookInput, "statusChange", hookCtx);
271
385
  const previous = order.status;
272
386
  const lineItems = await this.repo.findLineItemsByOrderId(order.id, ctx);
387
+ // R-04: goods must not ship against refunded lines. Reject the fulfilled
388
+ // transition if any line has been (partially) refunded.
389
+ if (input.newStatus === "fulfilled" &&
390
+ lineItems.some((li) => li.refundedQuantity > 0)) {
391
+ return Err(new CommerceValidationError("Cannot fulfill an order that has refunded line items."));
392
+ }
273
393
  // VAPT r2 (codex) finding: side effects (inventory release, payment
274
394
  // refund, tax void) used to run BEFORE the atomic compare-and-swap on
275
395
  // updateStatus. Two parallel cancel/refund requests on the same order
@@ -294,7 +414,7 @@ export class OrderService {
294
414
  // Fulfilled items had their reservation released during the
295
415
  // fulfilled transition — releasing again would double-release.
296
416
  if (lineItem.fulfillmentStatus === "unfulfilled") {
297
- await inventory.release({
417
+ const releaseResult = await inventory.release({
298
418
  entityId: lineItem.entityId,
299
419
  quantity: lineItem.quantity,
300
420
  orderId: order.id,
@@ -302,7 +422,14 @@ export class OrderService {
302
422
  ...(lineItem.variantId != null
303
423
  ? { variantId: lineItem.variantId }
304
424
  : {}),
305
- });
425
+ }, actor, ctx);
426
+ if (!releaseResult.ok) {
427
+ const err = toCommerceError(releaseResult.error);
428
+ if (!(err instanceof CommerceValidationError &&
429
+ err.message === "No inventory record found for this entity.")) {
430
+ return Err(err);
431
+ }
432
+ }
306
433
  }
307
434
  }
308
435
  }
@@ -312,11 +439,19 @@ export class OrderService {
312
439
  if (paymentIntentId) {
313
440
  const payments = this.deps.services.payments;
314
441
  if (payments?.refund) {
315
- const maxRefund = Math.min(order.grandTotal, order.amountCaptured ?? order.grandTotal);
442
+ // R-01: subtract prior gateway refunds (line-level refundLines, and
443
+ // undone refunds — the gateway was never reversed) so this order-level
444
+ // refund can't double-pay on top of what has already been refunded.
445
+ const priorRefunds = await this.repo.findRefundsByOrderId(order.id, ctx);
446
+ const alreadyRefunded = priorRefunds.reduce((sum, r) => sum + r.amount, 0);
447
+ const maxRefund = Math.max(0, Math.min(order.grandTotal, order.amountCaptured ?? order.grandTotal) -
448
+ alreadyRefunded);
316
449
  const refundAmount = input.refundAmount != null
317
450
  ? Math.min(input.refundAmount, maxRefund)
318
451
  : maxRefund;
319
- await payments.refund(paymentIntentId, refundAmount, input.reason ?? `order_${input.newStatus}`);
452
+ if (refundAmount > 0) {
453
+ await payments.refund(paymentIntentId, refundAmount, input.reason ?? `order_${input.newStatus}`);
454
+ }
320
455
  }
321
456
  }
322
457
  // 3. Void tax transaction
@@ -349,7 +484,7 @@ export class OrderService {
349
484
  : {}),
350
485
  });
351
486
  // Release reservation (no longer needed)
352
- await inventory.release({
487
+ const releaseResult = await inventory.release({
353
488
  entityId: lineItem.entityId,
354
489
  quantity: lineItem.quantity,
355
490
  orderId: order.id,
@@ -357,7 +492,14 @@ export class OrderService {
357
492
  ...(lineItem.variantId != null
358
493
  ? { variantId: lineItem.variantId }
359
494
  : {}),
360
- });
495
+ }, actor, ctx);
496
+ if (!releaseResult.ok) {
497
+ const err = toCommerceError(releaseResult.error);
498
+ if (!(err instanceof CommerceValidationError &&
499
+ err.message === "No inventory record found for this entity.")) {
500
+ return Err(err);
501
+ }
502
+ }
361
503
  // Mark line item as fulfilled
362
504
  await this.repo.updateLineItem(lineItem.id, { fulfillmentStatus: "fulfilled" }, ctx);
363
505
  }
@@ -423,6 +565,18 @@ export class OrderService {
423
565
  if (input.lines.length === 0) {
424
566
  return Err(new CommerceValidationError("At least one line is required."));
425
567
  }
568
+ // R-02: a terminal order (cancelled / fully refunded) cannot be line-refunded.
569
+ if (this.machine.terminal.includes(found.order.status)) {
570
+ return Err(new CommerceValidationError(`Cannot refund an order in terminal status "${found.order.status}".`));
571
+ }
572
+ // R-03: a refund moves collected money — reject an unpaid order (otherwise a
573
+ // "refund" is recorded against funds never taken). "Paid" is either a core
574
+ // captured payment (checkout: amountCaptured > 0) or the order having left
575
+ // the initial pending state (e.g. a completed POS/in-store sale). Only a
576
+ // still-pending, uncaptured order is treated as unpaid.
577
+ if ((found.order.amountCaptured ?? 0) <= 0 && found.order.status === "pending") {
578
+ return Err(new CommerceValidationError("Cannot refund an unpaid order (no captured payment and still pending)."));
579
+ }
426
580
  const byId = new Map(found.lineItems.map((li) => [li.id, li]));
427
581
  const refundLines = [];
428
582
  for (const line of input.lines) {
@@ -456,11 +610,16 @@ export class OrderService {
456
610
  const payments = this.deps.services.payments;
457
611
  if (payments?.refund) {
458
612
  const priorRefunds = await this.repo.findRefundsByOrderId(orderId, ctx);
459
- const alreadyRefunded = priorRefunds
460
- .filter((r) => r.status === "completed")
461
- .reduce((sum, r) => sum + r.amount, 0);
613
+ // Gross total incl. UNDONE refunds: undoRefund reverses the local ledger
614
+ // but not the payment gateway, so that money already left. Counting only
615
+ // "completed" would let refund undo → refund re-issue a gateway refund
616
+ // (F-04 / R-05 / R-07). Capping on gross keeps total payout ≤ captured.
617
+ const alreadyRefunded = priorRefunds.reduce((sum, r) => sum + r.amount, 0);
462
618
  const refundable = Math.max(0, (found.order.amountCaptured ?? 0) - alreadyRefunded);
463
- await payments.refund(found.order.paymentIntentId, Math.min(totalAmount, refundable), input.reason ?? "line_refund");
619
+ const payAmount = Math.min(totalAmount, refundable);
620
+ if (payAmount > 0) {
621
+ await payments.refund(found.order.paymentIntentId, payAmount, input.reason ?? "line_refund");
622
+ }
464
623
  }
465
624
  }
466
625
  for (const line of refundLines) {
@@ -718,6 +877,12 @@ export class OrderService {
718
877
  order.amountCaptured >= order.grandTotal) {
719
878
  return new CommerceValidationError("Order payment is fully captured; line items cannot be edited.");
720
879
  }
880
+ // R-06: a checkout order carries a paymentIntentId. If amountCaptured is null
881
+ // (auth-only capture, or a mock adapter reporting 0), treat it as authorized —
882
+ // an order with a live payment must not have its lines silently edited/inflated.
883
+ if (order.paymentIntentId != null && order.amountCaptured == null) {
884
+ return new CommerceValidationError("Order has an authorized payment; line items cannot be edited.");
885
+ }
721
886
  return null;
722
887
  }
723
888
  /**
@@ -754,21 +919,53 @@ export class OrderService {
754
919
  const guard = this.lineItemEditGuard(order);
755
920
  if (guard)
756
921
  return Err(guard);
922
+ // Org-integrity + price provenance for the added line (same rules as create):
923
+ // the entity must belong to the order's org, and the price is server-derived
924
+ // unless the actor holds `orders:manage` (then honored + flagged isCustomPrice).
925
+ const resolvedResult = await this.resolveLines({
926
+ currency: order.currency,
927
+ subtotal: 0,
928
+ taxTotal: 0,
929
+ shippingTotal: 0,
930
+ grandTotal: 0,
931
+ lineItems: [
932
+ {
933
+ entityId: input.entityId,
934
+ entityType: input.entityType,
935
+ title: input.title,
936
+ quantity: input.quantity,
937
+ unitPrice: input.unitPrice,
938
+ totalPrice: input.totalPrice ?? input.unitPrice * input.quantity,
939
+ ...(input.variantId !== undefined ? { variantId: input.variantId } : {}),
940
+ ...(input.sku !== undefined ? { sku: input.sku } : {}),
941
+ ...(input.taxAmount !== undefined ? { taxAmount: input.taxAmount } : {}),
942
+ ...(input.discountAmount !== undefined
943
+ ? { discountAmount: input.discountAmount }
944
+ : {}),
945
+ ...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
946
+ },
947
+ ],
948
+ ...(order.customerId ? { customerId: order.customerId } : {}),
949
+ }, actor, false, ctx);
950
+ if (!resolvedResult.ok)
951
+ return Err(resolvedResult.error);
952
+ const line = resolvedResult.value[0];
757
953
  await this.repo.createLineItems([
758
954
  {
759
955
  orderId: order.id,
760
- entityId: input.entityId,
761
- entityType: input.entityType,
762
- title: input.title,
763
- quantity: input.quantity,
764
- unitPrice: input.unitPrice,
765
- totalPrice: input.totalPrice ?? input.unitPrice * input.quantity,
766
- taxAmount: input.taxAmount ?? 0,
767
- discountAmount: input.discountAmount ?? 0,
956
+ entityId: line.entityId,
957
+ entityType: line.entityType,
958
+ title: line.title,
959
+ quantity: line.quantity,
960
+ unitPrice: line.unitPrice,
961
+ totalPrice: line.totalPrice ?? line.unitPrice * line.quantity,
962
+ isCustomPrice: line.isCustomPrice,
963
+ taxAmount: line.taxAmount ?? 0,
964
+ discountAmount: line.discountAmount ?? 0,
768
965
  fulfillmentStatus: "unfulfilled",
769
- metadata: input.metadata ?? {},
770
- ...(input.variantId !== undefined ? { variantId: input.variantId } : {}),
771
- ...(input.sku !== undefined ? { sku: input.sku } : {}),
966
+ metadata: line.metadata ?? {},
967
+ ...(line.variantId !== undefined ? { variantId: line.variantId } : {}),
968
+ ...(line.sku !== undefined ? { sku: line.sku } : {}),
772
969
  },
773
970
  ], ctx);
774
971
  return Ok(await this.recalcOrderTotals(order, actor, "line_item_added", ctx));
@@ -1 +1 @@
1
- {"version":3,"file":"stale-order-cleanup.d.ts","sourceRoot":"","sources":["../../../src/modules/orders/stale-order-cleanup.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAe,MAAM,4BAA4B,CAAC;AAE9E;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,qBAAqB,EAAE,cAAc,CAChD;IAAE,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,EAC3B;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAkD/C,CAAC"}
1
+ {"version":3,"file":"stale-order-cleanup.d.ts","sourceRoot":"","sources":["../../../src/modules/orders/stale-order-cleanup.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAejE;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,qBAAqB,EAAE,cAAc,CAChD;IAAE,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,EAC3B;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAkD/C,CAAC"}
@@ -1,5 +1,17 @@
1
1
  import { eq, and, lt } from "drizzle-orm";
2
2
  import { orders } from "./schema.js";
3
+ function systemActorForOrg(organizationId) {
4
+ return {
5
+ type: "user",
6
+ userId: "system",
7
+ email: null,
8
+ name: "System",
9
+ vendorId: null,
10
+ organizationId,
11
+ role: "admin",
12
+ permissions: ["*:*"],
13
+ };
14
+ }
3
15
  /**
4
16
  * Stale Order Cleanup Task
5
17
  *
@@ -34,7 +46,7 @@ export const staleOrderCleanupTask = {
34
46
  const orderService = ctx.services.orders;
35
47
  for (const order of staleOrders) {
36
48
  try {
37
- await orderService.cancel(order.id, null, `Auto-cancelled: pending for >${thresholdHours}h`);
49
+ await orderService.cancel(order.id, systemActorForOrg(order.organizationId), `Auto-cancelled: pending for >${thresholdHours}h`);
38
50
  cancelledIds.push(order.id);
39
51
  ctx.logger.info(`Stale order ${order.orderNumber} auto-cancelled`, {
40
52
  orderId: order.id,
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/modules/pricing/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AAExE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qCAAqC,CAAC;AAOrE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAEnE,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAE9D,OAAO,KAAK,EACV,iBAAiB,EACjB,KAAK,EACL,aAAa,EAGd,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAGxE,MAAM,MAAM,iBAAiB,GACzB,qBAAqB,GACrB,gBAAgB,GAChB,QAAQ,GACR,UAAU,CAAC;AAEf,UAAU,kBAAkB;IAC1B,UAAU,EAAE,iBAAiB,CAAC;IAC9B,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,KAAK,EAAE,YAAY,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,QAAQ,EAAE,eAAe,CAAC;CAC3B;AAkBD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,SAAS,CAAC,EAAE,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,KAAK,CAAC;QACtB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,iBAAiB,CAAC;QACxB,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC,CAAC;IACH,SAAS,EAAE,kBAAkB,EAAE,CAAC;IAChC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,YAAY,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAChF,OAAO,KAAK,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAuGhF,qBAAa,cAAc;IAIb,OAAO,CAAC,IAAI;IAHxB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAoB;IACzC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoB;gBAE5B,IAAI,EAAE,kBAAkB;IAKtC,YAAY,CAChB,KAAK,EAAE,iBAAiB,EACxB,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,EACpB,GAAG,CAAC,EAAE,SAAS,GACd,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAqEnB,cAAc,CAClB,KAAK,EAAE,wBAAwB,EAC/B,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,EACpB,GAAG,CAAC,EAAE,SAAS,GACd,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IAwD3B,aAAa,CACjB,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,EACnE,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,EACpB,GAAG,CAAC,EAAE,SAAS,GACd,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC;IAgB7B,cAAc,CAClB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE;QACL,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC1B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC3B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC9B,SAAS,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,SAAS,CAAC;QACpC,UAAU,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,SAAS,CAAC;QACrC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;QACxC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;QACxC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;KAChD,EACD,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,EACpB,GAAG,CAAC,EAAE,SAAS,GACd,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IAuB3B,cAAc,CAClB,EAAE,EAAE,MAAM,EACV,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,EACpB,GAAG,CAAC,EAAE,SAAS,GACd,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,IAAI,CAAA;KAAE,CAAC,CAAC;IAU/B,UAAU,CACd,MAAM,CAAC,EAAE;QACP,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,EACD,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,EACpB,GAAG,CAAC,EAAE,SAAS,GACd,OAAO,CAAC,MAAM,CAAC;QAAE,MAAM,EAAE,KAAK,EAAE,CAAC;QAAC,SAAS,EAAE,aAAa,EAAE,CAAA;KAAE,CAAC,CAAC;IAuD7D,OAAO,CACX,KAAK,EAAE,sBAAsB,EAC7B,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,EACpB,GAAG,CAAC,EAAE,SAAS,GACd,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;CAqLlC"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/modules/pricing/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AAExE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qCAAqC,CAAC;AAOrE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAEnE,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAE9D,OAAO,KAAK,EACV,iBAAiB,EACjB,KAAK,EACL,aAAa,EAGd,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAGxE,MAAM,MAAM,iBAAiB,GACzB,qBAAqB,GACrB,gBAAgB,GAChB,QAAQ,GACR,UAAU,CAAC;AAEf,UAAU,kBAAkB;IAC1B,UAAU,EAAE,iBAAiB,CAAC;IAC9B,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,KAAK,EAAE,YAAY,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,QAAQ,EAAE,eAAe,CAAC;CAC3B;AAkBD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,SAAS,CAAC,EAAE,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,KAAK,CAAC;QACtB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,iBAAiB,CAAC;QACxB,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC,CAAC;IACH,SAAS,EAAE,kBAAkB,EAAE,CAAC;IAChC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,YAAY,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAChF,OAAO,KAAK,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAuGhF,qBAAa,cAAc;IAIb,OAAO,CAAC,IAAI;IAHxB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAoB;IACzC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoB;gBAE5B,IAAI,EAAE,kBAAkB;IAKtC,YAAY,CAChB,KAAK,EAAE,iBAAiB,EACxB,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,EACpB,GAAG,CAAC,EAAE,SAAS,GACd,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAoEnB,cAAc,CAClB,KAAK,EAAE,wBAAwB,EAC/B,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,EACpB,GAAG,CAAC,EAAE,SAAS,GACd,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IAwD3B,aAAa,CACjB,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,EACnE,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,EACpB,GAAG,CAAC,EAAE,SAAS,GACd,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC;IAgB7B,cAAc,CAClB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE;QACL,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC1B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC3B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC9B,SAAS,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,SAAS,CAAC;QACpC,UAAU,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,SAAS,CAAC;QACrC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;QACxC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;QACxC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;KAChD,EACD,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,EACpB,GAAG,CAAC,EAAE,SAAS,GACd,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IAuB3B,cAAc,CAClB,EAAE,EAAE,MAAM,EACV,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,EACpB,GAAG,CAAC,EAAE,SAAS,GACd,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,IAAI,CAAA;KAAE,CAAC,CAAC;IAU/B,UAAU,CACd,MAAM,CAAC,EAAE;QACP,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,EACD,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,EACpB,GAAG,CAAC,EAAE,SAAS,GACd,OAAO,CAAC,MAAM,CAAC;QAAE,MAAM,EAAE,KAAK,EAAE,CAAC;QAAC,SAAS,EAAE,aAAa,EAAE,CAAA;KAAE,CAAC,CAAC;IAuD7D,OAAO,CACX,KAAK,EAAE,sBAAsB,EAC7B,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,EACpB,GAAG,CAAC,EAAE,SAAS,GACd,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;CAqLlC"}
@@ -103,11 +103,11 @@ export class PricingService {
103
103
  if (input.amount < 0) {
104
104
  return Err(new CommerceValidationError("Base price amount cannot be negative."));
105
105
  }
106
- const entity = await this.catalogRepo.findEntityById(input.entityId, ctx);
106
+ const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
107
+ const entity = await this.catalogRepo.findEntityById(input.entityId, ctx, orgId);
107
108
  if (!entity) {
108
109
  return Err(new CommerceNotFoundError("Entity not found for price assignment."));
109
110
  }
110
- const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
111
111
  const priceData = {
112
112
  organizationId: orgId,
113
113
  entityId: input.entityId,
@@ -256,7 +256,8 @@ export class PricingService {
256
256
  return Ok({ prices, modifiers });
257
257
  }
258
258
  async resolve(input, actor, ctx) {
259
- const entity = await this.catalogRepo.findEntityById(input.entityId, ctx);
259
+ const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
260
+ const entity = await this.catalogRepo.findEntityById(input.entityId, ctx, orgId);
260
261
  if (!entity) {
261
262
  return Err(new CommerceNotFoundError(`Entity ${input.entityId} not found.`));
262
263
  }
@@ -266,8 +267,7 @@ export class PricingService {
266
267
  const timestamp = input.timestamp ?? new Date();
267
268
  const currency = normalizeCurrency(input.currency);
268
269
  const groupSet = toGroupSet(input);
269
- // Get matching prices from repository
270
- const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
270
+ // Get matching prices from repository (orgId resolved above)
271
271
  const allPrices = await this.repo.findPricesByEntityId(orgId, input.entityId, ctx);
272
272
  const matchingPrices = allPrices.filter((price) => {
273
273
  if (input.variantId !== undefined) {
@@ -1 +1 @@
1
- {"version":3,"file":"create-test-config.d.ts","sourceRoot":"","sources":["../../src/test-utils/create-test-config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAgDzD,wBAAsB,gBAAgB,CACpC,SAAS,GAAE,OAAO,CAAC,cAAc,CAAM,GACtC,OAAO,CAAC,cAAc,CAAC,CA+GzB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,sBAAsB,CAC1C,SAAS,GAAE,OAAO,CAAC,cAAc,CAAM,GACtC,OAAO,CAAC;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,CAUnE"}
1
+ {"version":3,"file":"create-test-config.d.ts","sourceRoot":"","sources":["../../src/test-utils/create-test-config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAgDzD,wBAAsB,gBAAgB,CACpC,SAAS,GAAE,OAAO,CAAC,cAAc,CAAM,GACtC,OAAO,CAAC,cAAc,CAAC,CAiHzB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,sBAAsB,CAC1C,SAAS,GAAE,OAAO,CAAC,cAAc,CAAM,GACtC,OAAO,CAAC;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,CAUnE"}
@@ -53,6 +53,7 @@ export async function createTestConfig(overrides = {}) {
53
53
  provider: "postgresql",
54
54
  },
55
55
  auth: {
56
+ allowTestActor: true,
56
57
  defaultOrganizationId: "org_default",
57
58
  requireEmailVerification: false,
58
59
  apiKeys: { enabled: true, defaultPermissions: ["catalog:read"] },
@@ -70,6 +71,7 @@ export async function createTestConfig(overrides = {}) {
70
71
  "orders:create",
71
72
  "orders:read",
72
73
  "orders:update",
74
+ "orders:manage",
73
75
  "cart:create",
74
76
  "cart:update",
75
77
  "customers:update:self",
@@ -0,0 +1,12 @@
1
+ import type { Kernel } from "../runtime/kernel.js";
2
+ /**
3
+ * Mark an order as paid (captured) for tests.
4
+ *
5
+ * Refund and return flows require a paid order — a refund moves collected money,
6
+ * and the order primitive rejects refunds on unpaid/pending orders. There is no
7
+ * public "set captured amount" service (capture() needs a live payment intent),
8
+ * so tests set the column here through a single typed helper rather than
9
+ * scattering raw SQL / `as any` casts across test files.
10
+ */
11
+ export declare function markOrderPaidForTest(kernel: Kernel, orderId: string, amountCaptured: number, paymentIntentId?: string): Promise<void>;
12
+ //# sourceMappingURL=order-test-helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"order-test-helpers.d.ts","sourceRoot":"","sources":["../../src/test-utils/order-test-helpers.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAGnD;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,MAAM,EACtB,eAAe,CAAC,EAAE,MAAM,GACvB,OAAO,CAAC,IAAI,CAAC,CAMf"}
@@ -0,0 +1,18 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { orders } from "../modules/orders/schema.js";
3
+ /**
4
+ * Mark an order as paid (captured) for tests.
5
+ *
6
+ * Refund and return flows require a paid order — a refund moves collected money,
7
+ * and the order primitive rejects refunds on unpaid/pending orders. There is no
8
+ * public "set captured amount" service (capture() needs a live payment intent),
9
+ * so tests set the column here through a single typed helper rather than
10
+ * scattering raw SQL / `as any` casts across test files.
11
+ */
12
+ export async function markOrderPaidForTest(kernel, orderId, amountCaptured, paymentIntentId) {
13
+ const db = kernel.database.db;
14
+ await db
15
+ .update(orders)
16
+ .set({ amountCaptured, ...(paymentIntentId ? { paymentIntentId } : {}) })
17
+ .where(eq(orders.id, orderId));
18
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"rest-api-test-utils.d.ts","sourceRoot":"","sources":["../../src/test-utils/rest-api-test-utils.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAOpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,KAAK,SAAS,GAAG;IACf,SAAS,EAAE;QACT,IAAI,EAAE,YAAY,CAAC;QACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;KACrB,CAAC;CACH,CAAC;AAiDF;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,SAAS,GAAE,OAAO,CAAC,cAAc,CAAM,GACtC,OAAO,CAAC;IACT,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IACxB,MAAM,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;IACxC,IAAI,EAAE,YAAY,CAAC;IACnB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC,CAqDD;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,GAAG,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,CAEnF;AAED;;GAEG;AACH,eAAO,MAAM,SAAS,EAAE,KAmCvB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,KAS3B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,EAAE,KASzB,CAAC;AAEF;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE;IAClE,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC;CACf,WAwBA;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,EACvB,OAAO,EAAE;IACP,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC;CACf,qBAYF"}
1
+ {"version":3,"file":"rest-api-test-utils.d.ts","sourceRoot":"","sources":["../../src/test-utils/rest-api-test-utils.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAOpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,KAAK,SAAS,GAAG;IACf,SAAS,EAAE;QACT,IAAI,EAAE,YAAY,CAAC;QACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;KACrB,CAAC;CACH,CAAC;AAiDF;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,SAAS,GAAE,OAAO,CAAC,cAAc,CAAM,GACtC,OAAO,CAAC;IACT,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IACxB,MAAM,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;IACxC,IAAI,EAAE,YAAY,CAAC;IACnB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC,CAqDD;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,GAAG,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,CAEnF;AAED;;GAEG;AACH,eAAO,MAAM,SAAS,EAAE,KAoCvB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,KAS3B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,EAAE,KASzB,CAAC;AAEF;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE;IAClE,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC;CACf,WAwBA;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,EACvB,OAAO,EAAE;IACP,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC;CACf,qBAYF"}
@@ -130,6 +130,7 @@ export const testActor = {
130
130
  "orders:create",
131
131
  "orders:read",
132
132
  "orders:update",
133
+ "orders:manage",
133
134
  "cart:create",
134
135
  "cart:update",
135
136
  "cart:read",
@@ -28,7 +28,7 @@ export const testStaffActor = {
28
28
  role: "staff",
29
29
  permissions: [
30
30
  "catalog:read", "catalog:create", "catalog:update",
31
- "inventory:adjust", "orders:read", "orders:create", "orders:update",
31
+ "inventory:adjust", "orders:read", "orders:create", "orders:update", "orders:manage",
32
32
  ],
33
33
  };
34
34
  /** Customer with minimal read/write-own permissions. */
package/dist/testing.d.ts CHANGED
@@ -11,5 +11,6 @@ export { createRepositoryTestHarness } from "./test-utils/create-repository-test
11
11
  export { createPluginTestApp, type PluginTestApp, type TestAppEnv } from "./test-utils/create-plugin-test-app.js";
12
12
  export { TEST_ORG_ID, testAdminActor, testStaffActor, testCustomerActor, testNoPermActor, jsonHeaders, } from "./test-utils/test-actors.js";
13
13
  export { beforeHook, afterHook } from "./test-utils/typed-hooks.js";
14
+ export { markOrderPaidForTest } from "./test-utils/order-test-helpers.js";
14
15
  export type { Actor } from "./auth/types.js";
15
16
  //# sourceMappingURL=testing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AACtE,OAAO,EAAE,uBAAuB,EAAE,MAAM,4CAA4C,CAAC;AACrF,OAAO,EAAE,2BAA2B,EAAE,MAAM,gDAAgD,CAAC;AAC7F,OAAO,EAAE,mBAAmB,EAAE,KAAK,aAAa,EAAE,KAAK,UAAU,EAAE,MAAM,wCAAwC,CAAC;AAClH,OAAO,EACL,WAAW,EACX,cAAc,EAAE,cAAc,EAAE,iBAAiB,EAAE,eAAe,EAClE,WAAW,GACZ,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAGpE,YAAY,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AACtE,OAAO,EAAE,uBAAuB,EAAE,MAAM,4CAA4C,CAAC;AACrF,OAAO,EAAE,2BAA2B,EAAE,MAAM,gDAAgD,CAAC;AAC7F,OAAO,EAAE,mBAAmB,EAAE,KAAK,aAAa,EAAE,KAAK,UAAU,EAAE,MAAM,wCAAwC,CAAC;AAClH,OAAO,EACL,WAAW,EACX,cAAc,EAAE,cAAc,EAAE,iBAAiB,EAAE,eAAe,EAClE,WAAW,GACZ,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAG1E,YAAY,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/testing.js CHANGED
@@ -11,3 +11,4 @@ export { createRepositoryTestHarness } from "./test-utils/create-repository-test
11
11
  export { createPluginTestApp } from "./test-utils/create-plugin-test-app.js";
12
12
  export { TEST_ORG_ID, testAdminActor, testStaffActor, testCustomerActor, testNoPermActor, jsonHeaders, } from "./test-utils/test-actors.js";
13
13
  export { beforeHook, afterHook } from "./test-utils/typed-hooks.js";
14
+ export { markOrderPaidForTest } from "./test-utils/order-test-helpers.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@porulle/core",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -43,10 +43,10 @@ export function authMiddleware(
43
43
  // Resolve the default org from config, falling back to deprecated constant
44
44
  const defaultOrgId = config.auth?.defaultOrganizationId ?? DEFAULT_ORG_ID;
45
45
 
46
- // Test-only actor injection: NODE_ENV === "test" honors x-test-actor for
47
- // route-level integration tests that bypass the Better Auth session
48
- // round-trip. Production/non-test environments ignore this header entirely.
49
- if (process.env.NODE_ENV === "test") {
46
+ // Test-only actor injection: requires NODE_ENV === "test" AND an explicit
47
+ // config opt-in so staging/preview deployments left as NODE_ENV=test do not
48
+ // silently become a full auth bypass.
49
+ if (process.env.NODE_ENV === "test" && config.auth?.allowTestActor) {
50
50
  const testActorHeader = c.req.header("x-test-actor");
51
51
  if (testActorHeader) {
52
52
  try {
@@ -172,10 +172,29 @@ export function authMiddleware(
172
172
  if (result?.valid && result.key) {
173
173
  const apiKey = result.key as Record<string, unknown>;
174
174
 
175
- // v1.5+ renamed userId → referenceId on the apikey table.
176
- const userId = (apiKey.referenceId ?? "") as string;
177
175
  const name = (apiKey.name ?? "API Key") as string;
178
- const orgId = (apiKey.organizationId ?? defaultOrgId) as string;
176
+ // Read the org (and operator identity) from key metadata when present.
177
+ // POS shift keys carry { organizationId, operatorId } in metadata so the
178
+ // operator is scoped to their store WITHOUT any org membership/role
179
+ // (SEC-16 / R-01). Other keys fall back to the key's organizationId.
180
+ const rawMeta = apiKey.metadata;
181
+ const meta =
182
+ rawMeta && typeof rawMeta === "object"
183
+ ? (rawMeta as Record<string, unknown>)
184
+ : typeof rawMeta === "string"
185
+ ? (() => {
186
+ try {
187
+ return JSON.parse(rawMeta) as Record<string, unknown>;
188
+ } catch {
189
+ return null;
190
+ }
191
+ })()
192
+ : null;
193
+ const metaOrg = typeof meta?.organizationId === "string" ? meta.organizationId : undefined;
194
+ const orgId = (metaOrg ?? apiKey.organizationId ?? defaultOrgId) as string;
195
+ const userId = ((typeof meta?.operatorId === "string" ? meta.operatorId : undefined)
196
+ ?? apiKey.referenceId
197
+ ?? "") as string;
179
198
 
180
199
  // Better Auth stores permissions as Record<string, string[]>
181
200
  // (e.g. {"catalog":["read","create"]}). Flatten to the