@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.
- 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 +2 -0
- package/dist/config/types.d.ts +9 -0
- package/dist/config/types.d.ts.map +1 -1
- 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/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 +81 -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/orders.js +2 -2
- package/dist/interfaces/rest/schemas/responses.d.ts +34 -0
- package/dist/interfaces/rest/schemas/responses.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 +62 -1
- package/dist/kernel/plugin/manifest.d.ts +3 -0
- package/dist/kernel/plugin/manifest.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/modules/catalog/entity-service.js +34 -2
- package/dist/modules/customers/service.d.ts.map +1 -1
- package/dist/modules/customers/service.js +6 -0
- 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 +223 -26
- package/dist/modules/orders/stale-order-cleanup.d.ts.map +1 -1
- package/dist/modules/orders/stale-order-cleanup.js +13 -1
- package/dist/modules/pricing/service.d.ts.map +1 -1
- package/dist/modules/pricing/service.js +5 -5
- 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 +2 -0
- package/src/config/types.ts +9 -0
- package/src/hooks/checkout-completion.ts +22 -16
- package/src/hooks/checkout.ts +11 -8
- package/src/interfaces/rest/routes/admin/staff.ts +45 -3
- package/src/interfaces/rest/routes/checkout.ts +93 -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 +70 -1
- package/src/kernel/plugin/manifest.ts +3 -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 +28 -1
- package/src/modules/customers/service.ts +6 -0
- package/src/modules/orders/schema.ts +4 -0
- package/src/modules/orders/service.ts +408 -64
- package/src/modules/orders/stale-order-cleanup.ts +18 -4
- package/src/modules/pricing/service.ts +5 -6
- 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
|
@@ -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
|
-
|
|
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:
|
|
160
|
+
subtotal: computedSubtotal,
|
|
65
161
|
taxTotal: processed.taxTotal,
|
|
66
162
|
shippingTotal: processed.shippingTotal,
|
|
67
163
|
discountTotal: processed.discountTotal ?? 0,
|
|
68
|
-
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
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:
|
|
761
|
-
entityType:
|
|
762
|
-
title:
|
|
763
|
-
quantity:
|
|
764
|
-
unitPrice:
|
|
765
|
-
totalPrice:
|
|
766
|
-
|
|
767
|
-
|
|
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:
|
|
770
|
-
...(
|
|
771
|
-
...(
|
|
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":"
|
|
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,
|
|
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;
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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"}
|
|
@@ -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
|
package/dist/testing.d.ts.map
CHANGED
|
@@ -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;
|
|
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
package/src/auth/middleware.ts
CHANGED
|
@@ -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"
|
|
47
|
-
//
|
|
48
|
-
//
|
|
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
|
-
|
|
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
|