@porulle/core 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/setup.d.ts.map +1 -1
- package/dist/auth/setup.js +1 -0
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +5 -0
- package/dist/config/types.d.ts +17 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/generated/plugin-manifest.d.ts +3 -0
- package/dist/generated/plugin-manifest.d.ts.map +1 -1
- package/dist/generated/plugin-manifest.js +1 -0
- package/dist/hooks/checkout.d.ts.map +1 -1
- package/dist/hooks/checkout.js +16 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/interfaces/rest/index.d.ts.map +1 -1
- package/dist/interfaces/rest/index.js +12 -0
- package/dist/interfaces/rest/routes/admin/staff.d.ts +12 -0
- package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/admin/staff.js +172 -0
- package/dist/interfaces/rest/routes/analytics.d.ts +5 -0
- package/dist/interfaces/rest/routes/analytics.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/analytics.js +24 -0
- package/dist/interfaces/rest/routes/carts.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/carts.js +39 -2
- package/dist/interfaces/rest/routes/catalog.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/catalog.js +17 -1
- package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/checkout.js +17 -0
- package/dist/interfaces/rest/routes/documents.d.ts +10 -0
- package/dist/interfaces/rest/routes/documents.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/documents.js +40 -0
- package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/orders.js +98 -1
- package/dist/interfaces/rest/routes/pricing.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/pricing.js +36 -1
- package/dist/interfaces/rest/routes/settings.d.ts +5 -0
- package/dist/interfaces/rest/routes/settings.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/settings.js +50 -0
- package/dist/interfaces/rest/routes/shipping.d.ts +5 -0
- package/dist/interfaces/rest/routes/shipping.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/shipping.js +68 -0
- package/dist/interfaces/rest/routes/tax.d.ts +5 -0
- package/dist/interfaces/rest/routes/tax.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/tax.js +68 -0
- package/dist/interfaces/rest/schemas/admin-staff.d.ts +899 -0
- package/dist/interfaces/rest/schemas/admin-staff.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/admin-staff.js +98 -0
- package/dist/interfaces/rest/schemas/analytics.d.ts +252 -0
- package/dist/interfaces/rest/schemas/analytics.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/analytics.js +33 -0
- package/dist/interfaces/rest/schemas/carts.d.ts +656 -0
- package/dist/interfaces/rest/schemas/carts.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/carts.js +38 -0
- package/dist/interfaces/rest/schemas/catalog.d.ts +1218 -1
- package/dist/interfaces/rest/schemas/catalog.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/catalog.js +48 -0
- package/dist/interfaces/rest/schemas/checkout.d.ts +2 -0
- package/dist/interfaces/rest/schemas/checkout.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/checkout.js +4 -0
- package/dist/interfaces/rest/schemas/customer-portal.d.ts +35 -0
- package/dist/interfaces/rest/schemas/customer-portal.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/documents.d.ts +509 -0
- package/dist/interfaces/rest/schemas/documents.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/documents.js +65 -0
- package/dist/interfaces/rest/schemas/orders.d.ts +3110 -43
- package/dist/interfaces/rest/schemas/orders.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/orders.js +233 -0
- package/dist/interfaces/rest/schemas/pricing.d.ts +405 -0
- package/dist/interfaces/rest/schemas/pricing.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/pricing.js +70 -0
- package/dist/interfaces/rest/schemas/responses.d.ts +156 -0
- package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/search.d.ts +17 -0
- package/dist/interfaces/rest/schemas/search.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/settings.d.ts +397 -0
- package/dist/interfaces/rest/schemas/settings.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/settings.js +79 -0
- package/dist/interfaces/rest/schemas/shipping.d.ts +1093 -0
- package/dist/interfaces/rest/schemas/shipping.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/shipping.js +146 -0
- package/dist/interfaces/rest/schemas/tax.d.ts +1074 -0
- package/dist/interfaces/rest/schemas/tax.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/tax.js +132 -0
- package/dist/kernel/database/schema.d.ts +4 -0
- package/dist/kernel/database/schema.d.ts.map +1 -1
- package/dist/kernel/database/schema.js +8 -0
- package/dist/kernel/hooks/types.d.ts +1 -1
- package/dist/kernel/hooks/types.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.d.ts +23 -0
- package/dist/kernel/plugin/manifest.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.js +17 -2
- package/dist/modules/analytics/module.d.ts.map +1 -1
- package/dist/modules/analytics/module.js +6 -0
- package/dist/modules/analytics/reports.d.ts +58 -0
- package/dist/modules/analytics/reports.d.ts.map +1 -0
- package/dist/modules/analytics/reports.js +249 -0
- package/dist/modules/analytics/service.d.ts +10 -0
- package/dist/modules/analytics/service.d.ts.map +1 -1
- package/dist/modules/analytics/service.js +14 -0
- package/dist/modules/cart/repository/index.d.ts +7 -0
- package/dist/modules/cart/repository/index.d.ts.map +1 -1
- package/dist/modules/cart/repository/index.js +25 -1
- package/dist/modules/cart/schema.d.ts +17 -0
- package/dist/modules/cart/schema.d.ts.map +1 -1
- package/dist/modules/cart/schema.js +2 -0
- package/dist/modules/cart/schemas.d.ts +1 -0
- package/dist/modules/cart/schemas.d.ts.map +1 -1
- package/dist/modules/cart/schemas.js +1 -0
- package/dist/modules/cart/service.d.ts +32 -0
- package/dist/modules/cart/service.d.ts.map +1 -1
- package/dist/modules/cart/service.js +58 -0
- package/dist/modules/catalog/entity-service.d.ts +21 -0
- package/dist/modules/catalog/entity-service.d.ts.map +1 -1
- package/dist/modules/catalog/entity-service.js +132 -3
- package/dist/modules/catalog/schema.d.ts +34 -0
- package/dist/modules/catalog/schema.d.ts.map +1 -1
- package/dist/modules/catalog/schema.js +5 -0
- package/dist/modules/catalog/schemas.d.ts +3 -0
- package/dist/modules/catalog/schemas.d.ts.map +1 -1
- package/dist/modules/catalog/schemas.js +3 -0
- package/dist/modules/catalog/service.d.ts +36 -0
- package/dist/modules/catalog/service.d.ts.map +1 -1
- package/dist/modules/catalog/service.js +6 -0
- package/dist/modules/documents/module.d.ts +7 -0
- package/dist/modules/documents/module.d.ts.map +1 -0
- package/dist/modules/documents/module.js +12 -0
- package/dist/modules/documents/pdf.d.ts +25 -0
- package/dist/modules/documents/pdf.d.ts.map +1 -0
- package/dist/modules/documents/pdf.js +80 -0
- package/dist/modules/documents/render.d.ts +32 -0
- package/dist/modules/documents/render.d.ts.map +1 -0
- package/dist/modules/documents/render.js +190 -0
- package/dist/modules/documents/repository/index.d.ts +25 -0
- package/dist/modules/documents/repository/index.d.ts.map +1 -0
- package/dist/modules/documents/repository/index.js +47 -0
- package/dist/modules/documents/schema.d.ts +214 -0
- package/dist/modules/documents/schema.d.ts.map +1 -0
- package/dist/modules/documents/schema.js +42 -0
- package/dist/modules/documents/service.d.ts +51 -0
- package/dist/modules/documents/service.d.ts.map +1 -0
- package/dist/modules/documents/service.js +119 -0
- package/dist/modules/fulfillment/service.d.ts +18 -0
- package/dist/modules/fulfillment/service.d.ts.map +1 -1
- package/dist/modules/fulfillment/service.js +85 -1
- package/dist/modules/orders/repository/index.d.ts +18 -1
- package/dist/modules/orders/repository/index.d.ts.map +1 -1
- package/dist/modules/orders/repository/index.js +86 -1
- package/dist/modules/orders/schema.d.ts +374 -0
- package/dist/modules/orders/schema.d.ts.map +1 -1
- package/dist/modules/orders/schema.js +52 -1
- package/dist/modules/orders/service.d.ts +86 -1
- package/dist/modules/orders/service.d.ts.map +1 -1
- package/dist/modules/orders/service.js +447 -20
- package/dist/modules/pricing/repository/index.d.ts +5 -0
- package/dist/modules/pricing/repository/index.d.ts.map +1 -1
- package/dist/modules/pricing/repository/index.js +19 -0
- package/dist/modules/pricing/service.d.ts +18 -0
- package/dist/modules/pricing/service.d.ts.map +1 -1
- package/dist/modules/pricing/service.js +38 -0
- package/dist/modules/settings/module.d.ts +6 -0
- package/dist/modules/settings/module.d.ts.map +1 -0
- package/dist/modules/settings/module.js +11 -0
- package/dist/modules/settings/repository/index.d.ts +16 -0
- package/dist/modules/settings/repository/index.d.ts.map +1 -0
- package/dist/modules/settings/repository/index.js +41 -0
- package/dist/modules/settings/schema.d.ts +121 -0
- package/dist/modules/settings/schema.d.ts.map +1 -0
- package/dist/modules/settings/schema.js +24 -0
- package/dist/modules/settings/service.d.ts +26 -0
- package/dist/modules/settings/service.d.ts.map +1 -0
- package/dist/modules/settings/service.js +51 -0
- package/dist/modules/shipping/calculator.d.ts +5 -0
- package/dist/modules/shipping/calculator.d.ts.map +1 -1
- package/dist/modules/shipping/calculator.js +10 -0
- package/dist/modules/shipping/module.d.ts +5 -1
- package/dist/modules/shipping/module.d.ts.map +1 -1
- package/dist/modules/shipping/module.js +4 -1
- package/dist/modules/shipping/repository/index.d.ts +27 -2
- package/dist/modules/shipping/repository/index.d.ts.map +1 -1
- package/dist/modules/shipping/repository/index.js +105 -1
- package/dist/modules/shipping/schema.d.ts +419 -0
- package/dist/modules/shipping/schema.d.ts.map +1 -0
- package/dist/modules/shipping/schema.js +56 -0
- package/dist/modules/shipping/service.d.ts +54 -0
- package/dist/modules/shipping/service.d.ts.map +1 -1
- package/dist/modules/shipping/service.js +161 -2
- package/dist/modules/tax/adapter.d.ts +13 -0
- package/dist/modules/tax/adapter.d.ts.map +1 -1
- package/dist/modules/tax/module.d.ts +5 -1
- package/dist/modules/tax/module.d.ts.map +1 -1
- package/dist/modules/tax/module.js +4 -1
- package/dist/modules/tax/repository/index.d.ts +38 -2
- package/dist/modules/tax/repository/index.d.ts.map +1 -1
- package/dist/modules/tax/repository/index.js +138 -1
- package/dist/modules/tax/schema.d.ts +355 -0
- package/dist/modules/tax/schema.d.ts.map +1 -0
- package/dist/modules/tax/schema.js +55 -0
- package/dist/modules/tax/service.d.ts +60 -1
- package/dist/modules/tax/service.d.ts.map +1 -1
- package/dist/modules/tax/service.js +227 -2
- package/dist/runtime/kernel-modules.d.ts +15 -2
- package/dist/runtime/kernel-modules.d.ts.map +1 -1
- package/dist/runtime/kernel-modules.js +4 -0
- package/dist/runtime/kernel-types.d.ts +5 -1
- package/dist/runtime/kernel-types.d.ts.map +1 -1
- package/dist/runtime/kernel-types.js +2 -0
- package/dist/runtime/server.js +1 -1
- package/dist/test-utils/create-plugin-test-app.d.ts +3 -0
- package/dist/test-utils/create-plugin-test-app.d.ts.map +1 -1
- package/dist/test-utils/create-plugin-test-app.js +15 -3
- package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
- package/dist/test-utils/rest-api-test-utils.js +6 -0
- package/package.json +3 -3
- package/src/auth/setup.ts +1 -0
- package/src/config/defaults.ts +5 -0
- package/src/config/types.ts +17 -1
- package/src/generated/plugin-capabilities.d.ts +1 -0
- package/src/generated/plugin-manifest.ts +1 -0
- package/src/generated/plugin-repositories.d.ts +1 -0
- package/src/hooks/checkout.ts +25 -3
- package/src/index.ts +1 -0
- package/src/interfaces/rest/index.ts +12 -0
- package/src/interfaces/rest/routes/admin/staff.ts +218 -0
- package/src/interfaces/rest/routes/analytics.ts +33 -0
- package/src/interfaces/rest/routes/carts.ts +52 -2
- package/src/interfaces/rest/routes/catalog.ts +34 -0
- package/src/interfaces/rest/routes/checkout.ts +22 -0
- package/src/interfaces/rest/routes/documents.ts +61 -0
- package/src/interfaces/rest/routes/orders.ts +137 -1
- package/src/interfaces/rest/routes/pricing.ts +44 -1
- package/src/interfaces/rest/routes/settings.ts +61 -0
- package/src/interfaces/rest/routes/shipping.ts +92 -0
- package/src/interfaces/rest/routes/tax.ts +90 -0
- package/src/interfaces/rest/schemas/admin-staff.ts +110 -0
- package/src/interfaces/rest/schemas/analytics.ts +39 -0
- package/src/interfaces/rest/schemas/carts.ts +40 -0
- package/src/interfaces/rest/schemas/catalog.ts +53 -0
- package/src/interfaces/rest/schemas/checkout.ts +4 -0
- package/src/interfaces/rest/schemas/documents.ts +72 -0
- package/src/interfaces/rest/schemas/orders.ts +257 -0
- package/src/interfaces/rest/schemas/pricing.ts +75 -0
- package/src/interfaces/rest/schemas/settings.ts +93 -0
- package/src/interfaces/rest/schemas/shipping.ts +164 -0
- package/src/interfaces/rest/schemas/tax.ts +148 -0
- package/src/kernel/database/schema.ts +12 -0
- package/src/kernel/hooks/types.ts +1 -0
- package/src/kernel/plugin/manifest.ts +38 -2
- package/src/modules/analytics/module.ts +10 -0
- package/src/modules/analytics/reports.ts +311 -0
- package/src/modules/analytics/service.ts +23 -0
- package/src/modules/cart/repository/index.ts +30 -1
- package/src/modules/cart/schema.ts +2 -0
- package/src/modules/cart/schemas.ts +1 -0
- package/src/modules/cart/service.ts +101 -0
- package/src/modules/catalog/entity-service.ts +168 -3
- package/src/modules/catalog/schema.ts +5 -0
- package/src/modules/catalog/schemas.ts +3 -0
- package/src/modules/catalog/service.ts +30 -0
- package/src/modules/documents/module.ts +19 -0
- package/src/modules/documents/pdf.ts +101 -0
- package/src/modules/documents/render.ts +231 -0
- package/src/modules/documents/repository/index.ts +68 -0
- package/src/modules/documents/schema.ts +44 -0
- package/src/modules/documents/service.ts +176 -0
- package/src/modules/fulfillment/service.ts +139 -1
- package/src/modules/orders/repository/index.ts +127 -1
- package/src/modules/orders/schema.ts +54 -1
- package/src/modules/orders/service.ts +654 -22
- package/src/modules/pricing/repository/index.ts +34 -0
- package/src/modules/pricing/service.ts +71 -0
- package/src/modules/settings/module.ts +18 -0
- package/src/modules/settings/repository/index.ts +59 -0
- package/src/modules/settings/schema.ts +25 -0
- package/src/modules/settings/service.ts +82 -0
- package/src/modules/shipping/calculator.ts +27 -0
- package/src/modules/shipping/module.ts +6 -2
- package/src/modules/shipping/repository/index.ts +136 -2
- package/src/modules/shipping/schema.ts +58 -0
- package/src/modules/shipping/service.ts +260 -1
- package/src/modules/tax/adapter.ts +9 -0
- package/src/modules/tax/module.ts +6 -2
- package/src/modules/tax/repository/index.ts +169 -2
- package/src/modules/tax/schema.ts +57 -0
- package/src/modules/tax/service.ts +313 -2
- package/src/runtime/kernel-modules.ts +4 -0
- package/src/runtime/kernel-types.ts +6 -0
- package/src/runtime/server.ts +1 -1
- package/src/test-utils/create-plugin-test-app.ts +17 -4
- package/src/test-utils/rest-api-test-utils.ts +6 -0
package/src/hooks/checkout.ts
CHANGED
|
@@ -359,28 +359,40 @@ export const calculateTax: BeforeHook<CheckoutData> = async ({
|
|
|
359
359
|
currency: string;
|
|
360
360
|
customerId?: string;
|
|
361
361
|
shippingAmount: number;
|
|
362
|
+
orderDiscount?: number;
|
|
362
363
|
fromAddress?: ShippingAddress;
|
|
363
364
|
toAddress?: ShippingAddress;
|
|
364
365
|
lineItems: Array<{
|
|
365
366
|
id: string;
|
|
366
367
|
entityId: string;
|
|
368
|
+
variantId?: string;
|
|
367
369
|
description: string;
|
|
368
370
|
quantity: number;
|
|
369
371
|
unitPrice: number;
|
|
370
372
|
discount?: number;
|
|
371
373
|
}>;
|
|
372
|
-
}): Promise<
|
|
373
|
-
| { ok: true; value: { amountToCollect: number } }
|
|
374
|
+
}, orgId?: string): Promise<
|
|
375
|
+
| { ok: true; value: { amountToCollect: number; lines?: Array<{ id: string; taxClass: string | null; taxAmount: number }> } }
|
|
374
376
|
| { ok: false; error: Error }
|
|
375
377
|
>;
|
|
376
378
|
};
|
|
377
379
|
|
|
380
|
+
// Cart-level discount not already attributed to lines — pro-rated across
|
|
381
|
+
// lines before class-based tax (issue #57).
|
|
382
|
+
const lineDiscounts = data.lineItems.reduce(
|
|
383
|
+
(sum, lineItem) => sum + (lineItem.discountAmount ?? 0),
|
|
384
|
+
0,
|
|
385
|
+
);
|
|
386
|
+
const orderDiscount = Math.max(0, data.discountTotal - lineDiscounts);
|
|
387
|
+
|
|
378
388
|
const calculated = await tax.calculate({
|
|
379
389
|
currency: data.currency,
|
|
380
390
|
shippingAmount: data.shippingTotal,
|
|
391
|
+
orderDiscount,
|
|
381
392
|
lineItems: data.lineItems.map((lineItem) => ({
|
|
382
393
|
id: lineItem.id ?? `${lineItem.entityId}:${lineItem.variantId ?? "_"}`,
|
|
383
394
|
entityId: lineItem.entityId,
|
|
395
|
+
...(lineItem.variantId !== undefined ? { variantId: lineItem.variantId } : {}),
|
|
384
396
|
description: lineItem.title ?? lineItem.entityId,
|
|
385
397
|
quantity: lineItem.quantity,
|
|
386
398
|
unitPrice: lineItem.resolvedUnitPrice ?? 0,
|
|
@@ -392,7 +404,7 @@ export const calculateTax: BeforeHook<CheckoutData> = async ({
|
|
|
392
404
|
...(data.shippingAddress !== undefined
|
|
393
405
|
? { toAddress: data.shippingAddress }
|
|
394
406
|
: {}),
|
|
395
|
-
});
|
|
407
|
+
}, resolveOrgId(context.actor ?? null));
|
|
396
408
|
|
|
397
409
|
if (!calculated.ok) {
|
|
398
410
|
throw new CommerceValidationError(
|
|
@@ -400,6 +412,14 @@ export const calculateTax: BeforeHook<CheckoutData> = async ({
|
|
|
400
412
|
);
|
|
401
413
|
}
|
|
402
414
|
|
|
415
|
+
// Product tax classes return per-line results — store them so the order's
|
|
416
|
+
// line items carry taxAmount (issue #57). Lines arrive in request order.
|
|
417
|
+
if (calculated.value.lines) {
|
|
418
|
+
for (const [i, line] of calculated.value.lines.entries()) {
|
|
419
|
+
if (data.lineItems[i]) data.lineItems[i]!.taxAmount = line.taxAmount;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
403
423
|
data.taxTotal = Math.max(0, Math.round(calculated.value.amountToCollect));
|
|
404
424
|
recalculateTotals(data);
|
|
405
425
|
return data;
|
|
@@ -421,6 +441,7 @@ export const calculateShipping: BeforeHook<CheckoutData> = async ({
|
|
|
421
441
|
currency: string;
|
|
422
442
|
address?: ShippingAddress;
|
|
423
443
|
isFreeShipping?: boolean;
|
|
444
|
+
orgId?: string;
|
|
424
445
|
}): Promise<
|
|
425
446
|
| {
|
|
426
447
|
ok: true;
|
|
@@ -441,6 +462,7 @@ export const calculateShipping: BeforeHook<CheckoutData> = async ({
|
|
|
441
462
|
})),
|
|
442
463
|
subtotalAfterDiscount: Math.max(0, data.subtotal - data.discountTotal),
|
|
443
464
|
currency: data.currency,
|
|
465
|
+
orgId: resolveOrgId(context.actor ?? null),
|
|
444
466
|
...(data.shippingAddress !== undefined
|
|
445
467
|
? { address: data.shippingAddress }
|
|
446
468
|
: {}),
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type {
|
|
|
15
15
|
} from "./kernel/plugin/manifest.js";
|
|
16
16
|
|
|
17
17
|
export { router } from "./interfaces/rest/router.js";
|
|
18
|
+
export { requirePerm } from "./interfaces/rest/utils.js";
|
|
18
19
|
export { webhookRouter, type WebhookRouterResult } from "./interfaces/rest/webhook-router.js";
|
|
19
20
|
export { isPrivateUrl, isPrivateIp } from "./modules/webhooks/ssrf-guard.js";
|
|
20
21
|
export { createServer } from "./runtime/server.js";
|
|
@@ -4,6 +4,7 @@ import { sql } from "drizzle-orm";
|
|
|
4
4
|
import type { Kernel } from "../../runtime/kernel.js";
|
|
5
5
|
import type { AppEnv } from "./utils.js";
|
|
6
6
|
import { mapErrorToResponse } from "../../kernel/error-mapper.js";
|
|
7
|
+
import { analyticsRoutes } from "./routes/analytics.js";
|
|
7
8
|
import { catalogRoutes } from "./routes/catalog.js";
|
|
8
9
|
import { inventoryRoutes } from "./routes/inventory.js";
|
|
9
10
|
import { mediaRoutes } from "./routes/media.js";
|
|
@@ -19,7 +20,12 @@ import { auditRoutes } from "./routes/audit.js";
|
|
|
19
20
|
import { adminJobRoutes } from "./routes/admin-jobs.js";
|
|
20
21
|
import { compensationFailureAdminRoutes } from "./routes/admin/compensation-failures.js";
|
|
21
22
|
import { adminPermissionsRoutes } from "./routes/admin/permissions.js";
|
|
23
|
+
import { adminStaffRoutes } from "./routes/admin/staff.js";
|
|
22
24
|
import { customerRoutes } from "./routes/customers.js";
|
|
25
|
+
import { documentRoutes } from "./routes/documents.js";
|
|
26
|
+
import { settingsRoutes } from "./routes/settings.js";
|
|
27
|
+
import { shippingRoutes } from "./routes/shipping.js";
|
|
28
|
+
import { taxRoutes } from "./routes/tax.js";
|
|
23
29
|
export function createRestRoutes(kernel: Kernel) {
|
|
24
30
|
const router = new OpenAPIHono<AppEnv>({
|
|
25
31
|
// Standardize Zod validation error responses across all routes
|
|
@@ -72,9 +78,14 @@ export function createRestRoutes(kernel: Kernel) {
|
|
|
72
78
|
router.route("/carts", cartRoutes(kernel));
|
|
73
79
|
router.route("/checkout", checkoutRoutes(kernel));
|
|
74
80
|
router.route("/orders", orderRoutes(kernel));
|
|
81
|
+
router.route("/orders", documentRoutes(kernel));
|
|
75
82
|
router.route("/payments", paymentRoutes(kernel));
|
|
76
83
|
router.route("/webhooks", webhookRoutes(kernel));
|
|
77
84
|
router.route("/pricing", pricingRoutes(kernel));
|
|
85
|
+
router.route("/shipping", shippingRoutes(kernel));
|
|
86
|
+
router.route("/tax", taxRoutes(kernel));
|
|
87
|
+
router.route("/settings", settingsRoutes(kernel));
|
|
88
|
+
router.route("/analytics", analyticsRoutes(kernel));
|
|
78
89
|
router.route("/promotions", promotionRoutes(kernel));
|
|
79
90
|
router.route("/search", searchRoutes(kernel));
|
|
80
91
|
router.route("/customers", customerRoutes(kernel));
|
|
@@ -82,6 +93,7 @@ export function createRestRoutes(kernel: Kernel) {
|
|
|
82
93
|
router.route("/admin", adminJobRoutes(kernel));
|
|
83
94
|
router.route("/admin", compensationFailureAdminRoutes(kernel));
|
|
84
95
|
router.route("/admin", adminPermissionsRoutes(kernel));
|
|
96
|
+
router.route("/admin", adminStaffRoutes(kernel));
|
|
85
97
|
|
|
86
98
|
// API Reference (Scalar) — disabled in production unless config.exposeOpenApiSpec is true
|
|
87
99
|
const exposeSpec = kernel.config.exposeOpenApiSpec ?? (process.env.NODE_ENV !== "production");
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
|
+
import { eq, and } from "drizzle-orm";
|
|
3
|
+
import type { Kernel } from "../../../../runtime/kernel.js";
|
|
4
|
+
import type { CommerceConfig } from "../../../../config/types.js";
|
|
5
|
+
import type { DrizzleDatabase } from "../../../../kernel/database/drizzle-db.js";
|
|
6
|
+
import { member, user, invitation } from "../../../../auth/auth-schema.js";
|
|
7
|
+
import { resolveOrgId } from "../../../../auth/org.js";
|
|
8
|
+
import { makeId } from "../../../../utils/id.js";
|
|
9
|
+
import {
|
|
10
|
+
listStaffRoute,
|
|
11
|
+
createStaffRoute,
|
|
12
|
+
inviteStaffRoute,
|
|
13
|
+
listStaffInvitationsRoute,
|
|
14
|
+
listStaffRolesRoute,
|
|
15
|
+
updateStaffRoleRoute,
|
|
16
|
+
revokeStaffRoute,
|
|
17
|
+
} from "../../schemas/admin-staff.js";
|
|
18
|
+
import { type AppEnv, requirePerm } from "../../utils.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Admin staff / RBAC surface (issue #46).
|
|
22
|
+
*
|
|
23
|
+
* Surfaces the Better Auth `member` table as first-class admin REST:
|
|
24
|
+
* list staff, add an existing user with a role, invite by email, change
|
|
25
|
+
* role, revoke — plus the role → permission mapping roles resolve to.
|
|
26
|
+
*/
|
|
27
|
+
export function adminStaffRoutes(kernel: Kernel) {
|
|
28
|
+
const router = new OpenAPIHono<AppEnv>();
|
|
29
|
+
const db = kernel.database.db as DrizzleDatabase;
|
|
30
|
+
const config = kernel.config as CommerceConfig;
|
|
31
|
+
|
|
32
|
+
router.use("/staff", requirePerm("staff:manage"));
|
|
33
|
+
router.use("/staff/:id", requirePerm("staff:manage"));
|
|
34
|
+
router.use("/staff/invitations", requirePerm("staff:manage"));
|
|
35
|
+
router.use("/staff/roles", requirePerm("staff:manage"));
|
|
36
|
+
|
|
37
|
+
function validRoles(): Set<string> {
|
|
38
|
+
return new Set([
|
|
39
|
+
...Object.keys(config.auth?.roles ?? {}),
|
|
40
|
+
"owner",
|
|
41
|
+
"admin",
|
|
42
|
+
"member",
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function invalidRole(c: { json: (d: unknown, s: number) => unknown }, role: string) {
|
|
47
|
+
return c.json(
|
|
48
|
+
{
|
|
49
|
+
error: {
|
|
50
|
+
code: "VALIDATION_FAILED",
|
|
51
|
+
message: `Unknown role "${role}". Valid roles: ${[...validRoles()].join(", ")}.`,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
422,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function countOwners(orgId: string): Promise<number> {
|
|
59
|
+
const rows = await db
|
|
60
|
+
.select({ id: member.id })
|
|
61
|
+
.from(member)
|
|
62
|
+
.where(and(eq(member.organizationId, orgId), eq(member.role, "owner")));
|
|
63
|
+
return rows.length;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
router.openapi(listStaffRoute, async (c) => {
|
|
67
|
+
const orgId = resolveOrgId(c.get("actor"));
|
|
68
|
+
const rows = await db
|
|
69
|
+
.select({
|
|
70
|
+
id: member.id,
|
|
71
|
+
userId: member.userId,
|
|
72
|
+
role: member.role,
|
|
73
|
+
createdAt: member.createdAt,
|
|
74
|
+
email: user.email,
|
|
75
|
+
name: user.name,
|
|
76
|
+
})
|
|
77
|
+
.from(member)
|
|
78
|
+
.leftJoin(user, eq(member.userId, user.id))
|
|
79
|
+
.where(eq(member.organizationId, orgId));
|
|
80
|
+
return c.json({ data: rows });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// @ts-expect-error -- openapi handler union return type
|
|
84
|
+
router.openapi(createStaffRoute, async (c) => {
|
|
85
|
+
const body = c.req.valid("json") as { userId: string; role: string };
|
|
86
|
+
const orgId = resolveOrgId(c.get("actor"));
|
|
87
|
+
|
|
88
|
+
if (!validRoles().has(body.role)) return invalidRole(c, body.role);
|
|
89
|
+
|
|
90
|
+
const users = await db.select().from(user).where(eq(user.id, body.userId));
|
|
91
|
+
if (users.length === 0) {
|
|
92
|
+
return c.json({ error: { code: "NOT_FOUND", message: "User not found." } }, 404);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const existing = await db
|
|
96
|
+
.select({ id: member.id })
|
|
97
|
+
.from(member)
|
|
98
|
+
.where(and(eq(member.organizationId, orgId), eq(member.userId, body.userId)));
|
|
99
|
+
if (existing.length > 0) {
|
|
100
|
+
return c.json(
|
|
101
|
+
{ error: { code: "VALIDATION_FAILED", message: "User is already a member of this organization." } },
|
|
102
|
+
422,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const rows = await db
|
|
107
|
+
.insert(member)
|
|
108
|
+
.values({
|
|
109
|
+
id: makeId(),
|
|
110
|
+
organizationId: orgId,
|
|
111
|
+
userId: body.userId,
|
|
112
|
+
role: body.role,
|
|
113
|
+
createdAt: new Date(),
|
|
114
|
+
})
|
|
115
|
+
.returning();
|
|
116
|
+
return c.json({ data: rows[0] }, 201);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// @ts-expect-error -- openapi handler union return type
|
|
120
|
+
router.openapi(inviteStaffRoute, async (c) => {
|
|
121
|
+
const body = c.req.valid("json") as { email: string; role: string };
|
|
122
|
+
const actor = c.get("actor");
|
|
123
|
+
const orgId = resolveOrgId(actor);
|
|
124
|
+
|
|
125
|
+
if (!validRoles().has(body.role)) return invalidRole(c, body.role);
|
|
126
|
+
|
|
127
|
+
const rows = await db
|
|
128
|
+
.insert(invitation)
|
|
129
|
+
.values({
|
|
130
|
+
id: makeId(),
|
|
131
|
+
organizationId: orgId,
|
|
132
|
+
email: body.email,
|
|
133
|
+
role: body.role,
|
|
134
|
+
status: "pending",
|
|
135
|
+
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
136
|
+
inviterId: actor!.userId,
|
|
137
|
+
})
|
|
138
|
+
.returning();
|
|
139
|
+
return c.json({ data: rows[0] }, 201);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
router.openapi(listStaffInvitationsRoute, async (c) => {
|
|
143
|
+
const orgId = resolveOrgId(c.get("actor"));
|
|
144
|
+
const rows = await db
|
|
145
|
+
.select()
|
|
146
|
+
.from(invitation)
|
|
147
|
+
.where(and(eq(invitation.organizationId, orgId), eq(invitation.status, "pending")));
|
|
148
|
+
return c.json({ data: rows });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
router.openapi(listStaffRolesRoute, async (c) => {
|
|
152
|
+
const roles = config.auth?.roles ?? {};
|
|
153
|
+
const data = Object.entries(roles).map(([role, def]) => ({
|
|
154
|
+
role,
|
|
155
|
+
permissions: def.permissions,
|
|
156
|
+
}));
|
|
157
|
+
return c.json({ data });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// @ts-expect-error -- openapi handler union return type
|
|
161
|
+
router.openapi(updateStaffRoleRoute, async (c) => {
|
|
162
|
+
const body = c.req.valid("json") as { role: string };
|
|
163
|
+
const orgId = resolveOrgId(c.get("actor"));
|
|
164
|
+
const id = c.req.param("id");
|
|
165
|
+
|
|
166
|
+
if (!validRoles().has(body.role)) return invalidRole(c, body.role);
|
|
167
|
+
|
|
168
|
+
const rows = await db
|
|
169
|
+
.select()
|
|
170
|
+
.from(member)
|
|
171
|
+
.where(and(eq(member.organizationId, orgId), eq(member.id, id)));
|
|
172
|
+
const target = rows[0];
|
|
173
|
+
if (!target) {
|
|
174
|
+
return c.json({ error: { code: "NOT_FOUND", message: "Staff member not found." } }, 404);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (target.role === "owner" && body.role !== "owner" && (await countOwners(orgId)) <= 1) {
|
|
178
|
+
return c.json(
|
|
179
|
+
{ error: { code: "VALIDATION_FAILED", message: "Cannot demote the organization's last owner." } },
|
|
180
|
+
422,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const updated = await db
|
|
185
|
+
.update(member)
|
|
186
|
+
.set({ role: body.role })
|
|
187
|
+
.where(eq(member.id, id))
|
|
188
|
+
.returning();
|
|
189
|
+
return c.json({ data: updated[0] });
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// @ts-expect-error -- openapi handler union return type
|
|
193
|
+
router.openapi(revokeStaffRoute, async (c) => {
|
|
194
|
+
const orgId = resolveOrgId(c.get("actor"));
|
|
195
|
+
const id = c.req.param("id");
|
|
196
|
+
|
|
197
|
+
const rows = await db
|
|
198
|
+
.select()
|
|
199
|
+
.from(member)
|
|
200
|
+
.where(and(eq(member.organizationId, orgId), eq(member.id, id)));
|
|
201
|
+
const target = rows[0];
|
|
202
|
+
if (!target) {
|
|
203
|
+
return c.json({ error: { code: "NOT_FOUND", message: "Staff member not found." } }, 404);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (target.role === "owner" && (await countOwners(orgId)) <= 1) {
|
|
207
|
+
return c.json(
|
|
208
|
+
{ error: { code: "VALIDATION_FAILED", message: "Cannot revoke the organization's last owner." } },
|
|
209
|
+
422,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
await db.delete(member).where(eq(member.id, id));
|
|
214
|
+
return c.json({ data: { deleted: true } });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return router;
|
|
218
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
|
+
import type { Kernel } from "../../../runtime/kernel.js";
|
|
3
|
+
import { getReportRoute, listReportsRoute } from "../schemas/analytics.js";
|
|
4
|
+
import { type AppEnv, mapErrorToResponse, mapErrorToStatus, requirePerm } from "../utils.js";
|
|
5
|
+
|
|
6
|
+
export function analyticsRoutes(kernel: Kernel) {
|
|
7
|
+
const router = new OpenAPIHono<AppEnv>();
|
|
8
|
+
|
|
9
|
+
router.use("/reports", requirePerm("analytics:read"));
|
|
10
|
+
router.use("/reports/:name", requirePerm("analytics:read"));
|
|
11
|
+
|
|
12
|
+
router.openapi(listReportsRoute, (c) => {
|
|
13
|
+
return c.json({ data: kernel.services.analytics.listReports() });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// @ts-expect-error -- openapi handler union return type
|
|
17
|
+
router.openapi(getReportRoute, async (c) => {
|
|
18
|
+
const { date, from, to } = c.req.valid("query");
|
|
19
|
+
const result = await kernel.services.analytics.getReport(
|
|
20
|
+
c.req.param("name"),
|
|
21
|
+
{
|
|
22
|
+
...(date !== undefined ? { date } : {}),
|
|
23
|
+
...(from !== undefined ? { from } : {}),
|
|
24
|
+
...(to !== undefined ? { to } : {}),
|
|
25
|
+
},
|
|
26
|
+
c.get("actor"),
|
|
27
|
+
);
|
|
28
|
+
if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
29
|
+
return c.json({ data: result.value });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return router;
|
|
33
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
2
|
import type { Kernel } from "../../../runtime/kernel.js";
|
|
3
3
|
import type { CreateCartInput, AddCartItemInput } from "../../../modules/cart/schemas.js";
|
|
4
|
-
import { createCartRoute, addCartItemRoute, updateCartItemQuantityRoute, getCartRoute, removeCartItemRoute } from "../schemas/carts.js";
|
|
5
|
-
import { type AppEnv, mapErrorToResponse, mapErrorToStatus } from "../utils.js";
|
|
4
|
+
import { createCartRoute, addCartItemRoute, updateCartItemQuantityRoute, getCartRoute, removeCartItemRoute, listCartsRoute, recoverCartRoute } from "../schemas/carts.js";
|
|
5
|
+
import { type AppEnv, mapErrorToResponse, mapErrorToStatus, parsePagination } from "../utils.js";
|
|
6
6
|
|
|
7
7
|
export function cartRoutes(kernel: Kernel) {
|
|
8
8
|
const router = new OpenAPIHono<AppEnv>();
|
|
@@ -21,6 +21,56 @@ export function cartRoutes(kernel: Kernel) {
|
|
|
21
21
|
return c.json({ data: result.value }, 201);
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
+
// @ts-expect-error -- openapi handler union return type
|
|
25
|
+
router.openapi(listCartsRoute, async (c) => {
|
|
26
|
+
const actor = c.get("actor");
|
|
27
|
+
const pagination = parsePagination(c.req.query());
|
|
28
|
+
const status = c.req.query("status");
|
|
29
|
+
const olderThanRaw = c.req.query("olderThan");
|
|
30
|
+
const hasCustomerRaw = c.req.query("hasCustomer");
|
|
31
|
+
|
|
32
|
+
let olderThan: Date | undefined;
|
|
33
|
+
if (olderThanRaw) {
|
|
34
|
+
const d = new Date(olderThanRaw);
|
|
35
|
+
if (Number.isNaN(d.getTime())) {
|
|
36
|
+
return c.json({ error: { code: "VALIDATION_FAILED", message: "olderThan must be an ISO timestamp." } }, 422);
|
|
37
|
+
}
|
|
38
|
+
olderThan = d;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const result = await kernel.services.cart.list(
|
|
42
|
+
{
|
|
43
|
+
page: pagination.page,
|
|
44
|
+
limit: pagination.limit,
|
|
45
|
+
...(status !== undefined ? { status } : {}),
|
|
46
|
+
...(olderThan !== undefined ? { olderThan } : {}),
|
|
47
|
+
...(hasCustomerRaw === "true" ? { hasCustomer: true } : {}),
|
|
48
|
+
...(hasCustomerRaw === "false" ? { hasCustomer: false } : {}),
|
|
49
|
+
},
|
|
50
|
+
actor,
|
|
51
|
+
);
|
|
52
|
+
if (!result.ok)
|
|
53
|
+
return c.json(
|
|
54
|
+
mapErrorToResponse(result.error),
|
|
55
|
+
mapErrorToStatus(result.error),
|
|
56
|
+
);
|
|
57
|
+
return c.json({
|
|
58
|
+
data: result.value.items,
|
|
59
|
+
meta: { pagination: result.value.pagination },
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// @ts-expect-error -- openapi handler union return type
|
|
64
|
+
router.openapi(recoverCartRoute, async (c) => {
|
|
65
|
+
const result = await kernel.services.cart.recover(c.req.param("id"), c.get("actor"));
|
|
66
|
+
if (!result.ok)
|
|
67
|
+
return c.json(
|
|
68
|
+
mapErrorToResponse(result.error),
|
|
69
|
+
mapErrorToStatus(result.error),
|
|
70
|
+
);
|
|
71
|
+
return c.json({ data: result.value });
|
|
72
|
+
});
|
|
73
|
+
|
|
24
74
|
// @ts-expect-error -- openapi handler union return type
|
|
25
75
|
router.openapi(getCartRoute, async (c) => {
|
|
26
76
|
const actor = c.get("actor");
|
|
@@ -44,6 +44,8 @@ import {
|
|
|
44
44
|
createOptionValueRoute,
|
|
45
45
|
createVariantRoute,
|
|
46
46
|
generateVariantsRoute,
|
|
47
|
+
quickVariantRoute,
|
|
48
|
+
bulkVariantsRoute,
|
|
47
49
|
} from "../schemas/catalog.js";
|
|
48
50
|
import {
|
|
49
51
|
type AppEnv,
|
|
@@ -508,6 +510,38 @@ export function catalogRoutes(kernel: Kernel) {
|
|
|
508
510
|
return c.json({ data: result.value }, 201);
|
|
509
511
|
});
|
|
510
512
|
|
|
513
|
+
// @ts-expect-error -- openapi handler union return type
|
|
514
|
+
router.openapi(quickVariantRoute, async (c) => {
|
|
515
|
+
const body = c.req.valid("json") as { options: Record<string, string>; sku?: string; barcode?: string };
|
|
516
|
+
const result = await kernel.services.catalog.quickCreateVariant(
|
|
517
|
+
c.req.param("id"),
|
|
518
|
+
body,
|
|
519
|
+
c.get("actor"),
|
|
520
|
+
);
|
|
521
|
+
if (!result.ok)
|
|
522
|
+
return c.json(
|
|
523
|
+
mapErrorToResponse(result.error),
|
|
524
|
+
mapErrorToStatus(result.error),
|
|
525
|
+
);
|
|
526
|
+
return c.json({ data: result.value }, result.value.created ? 201 : 200);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// @ts-expect-error -- openapi handler union return type
|
|
530
|
+
router.openapi(bulkVariantsRoute, async (c) => {
|
|
531
|
+
const body = c.req.valid("json") as { axes: Array<{ name: string; values: string[] }>; skuPrefix?: string };
|
|
532
|
+
const result = await kernel.services.catalog.bulkCreateVariants(
|
|
533
|
+
c.req.param("id"),
|
|
534
|
+
body,
|
|
535
|
+
c.get("actor"),
|
|
536
|
+
);
|
|
537
|
+
if (!result.ok)
|
|
538
|
+
return c.json(
|
|
539
|
+
mapErrorToResponse(result.error),
|
|
540
|
+
mapErrorToStatus(result.error),
|
|
541
|
+
);
|
|
542
|
+
return c.json({ data: result.value }, 201);
|
|
543
|
+
});
|
|
544
|
+
|
|
511
545
|
// @ts-expect-error -- openapi handler union return type
|
|
512
546
|
router.openapi(generateVariantsRoute, async (c) => {
|
|
513
547
|
const body = c.req.valid("json") as VariantGenerationStrategy;
|
|
@@ -34,6 +34,20 @@ export function checkoutRoutes(kernel: Kernel) {
|
|
|
34
34
|
const body = c.req.valid("json");
|
|
35
35
|
|
|
36
36
|
const actor = c.get("actor");
|
|
37
|
+
|
|
38
|
+
// Idempotent replay: a re-submitted checkout (offline POS queue, network
|
|
39
|
+
// retry) returns the already-created order BEFORE the pipeline runs — no
|
|
40
|
+
// double payment authorization, no duplicate order.
|
|
41
|
+
if (body.idempotencyKey) {
|
|
42
|
+
const replay = await kernel.services.orders.getByIdempotencyKey(
|
|
43
|
+
body.idempotencyKey,
|
|
44
|
+
actor,
|
|
45
|
+
);
|
|
46
|
+
if (replay.ok && replay.value) {
|
|
47
|
+
return c.json({ data: replay.value }, 201);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
37
51
|
const checkoutData: CheckoutData = {
|
|
38
52
|
checkoutId: makeId(),
|
|
39
53
|
cartId: body.cartId,
|
|
@@ -149,6 +163,9 @@ export function checkoutRoutes(kernel: Kernel) {
|
|
|
149
163
|
}
|
|
150
164
|
|
|
151
165
|
const orderPayload = {
|
|
166
|
+
...(body.idempotencyKey !== undefined
|
|
167
|
+
? { idempotencyKey: body.idempotencyKey }
|
|
168
|
+
: {}),
|
|
152
169
|
currency: processed.currency,
|
|
153
170
|
subtotal: processed.subtotal,
|
|
154
171
|
taxTotal: processed.taxTotal,
|
|
@@ -177,6 +194,11 @@ export function checkoutRoutes(kernel: Kernel) {
|
|
|
177
194
|
quantity: lineItem.quantity,
|
|
178
195
|
unitPrice: lineItem.resolvedUnitPrice ?? 0,
|
|
179
196
|
totalPrice: lineItem.resolvedTotal ?? 0,
|
|
197
|
+
// Per-line tax from product tax classes (issue #57)
|
|
198
|
+
taxAmount: lineItem.taxAmount ?? 0,
|
|
199
|
+
...(lineItem.discountAmount !== undefined
|
|
200
|
+
? { discountAmount: lineItem.discountAmount }
|
|
201
|
+
: {}),
|
|
180
202
|
};
|
|
181
203
|
return lineItem.variantId !== undefined
|
|
182
204
|
? { ...payload, variantId: lineItem.variantId }
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
|
+
import type { Kernel } from "../../../runtime/kernel.js";
|
|
3
|
+
import {
|
|
4
|
+
emailInvoiceRoute,
|
|
5
|
+
invoiceHtmlRoute,
|
|
6
|
+
invoicePdfRoute,
|
|
7
|
+
receiptHtmlRoute,
|
|
8
|
+
} from "../schemas/documents.js";
|
|
9
|
+
import { type AppEnv, mapErrorToResponse, mapErrorToStatus } from "../utils.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Order document rendering (issue #47). Mounted under /orders. Access
|
|
13
|
+
* control rides on OrderService.getById (orders:read, or orders:read:own
|
|
14
|
+
* for the order's customer), so no extra scope is needed here.
|
|
15
|
+
*/
|
|
16
|
+
export function documentRoutes(kernel: Kernel) {
|
|
17
|
+
const router = new OpenAPIHono<AppEnv>();
|
|
18
|
+
|
|
19
|
+
router.openapi(invoicePdfRoute, async (c) => {
|
|
20
|
+
const result = await kernel.services.documents.renderInvoicePdf(
|
|
21
|
+
c.req.param("id"),
|
|
22
|
+
c.get("actor"),
|
|
23
|
+
);
|
|
24
|
+
if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
25
|
+
return c.body(result.value.pdf.slice().buffer as ArrayBuffer, 200, {
|
|
26
|
+
"content-type": "application/pdf",
|
|
27
|
+
"content-disposition": `inline; filename="invoice-${result.value.invoiceNumber}.pdf"`,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
router.openapi(invoiceHtmlRoute, async (c) => {
|
|
32
|
+
const result = await kernel.services.documents.renderInvoiceHtml(
|
|
33
|
+
c.req.param("id"),
|
|
34
|
+
c.get("actor"),
|
|
35
|
+
);
|
|
36
|
+
if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
37
|
+
return c.html(result.value.html);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
router.openapi(receiptHtmlRoute, async (c) => {
|
|
41
|
+
const result = await kernel.services.documents.renderReceiptHtml(
|
|
42
|
+
c.req.param("id"),
|
|
43
|
+
c.get("actor"),
|
|
44
|
+
);
|
|
45
|
+
if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
46
|
+
return c.html(result.value.html);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// @ts-expect-error -- openapi handler union return type
|
|
50
|
+
router.openapi(emailInvoiceRoute, async (c) => {
|
|
51
|
+
const result = await kernel.services.documents.emailInvoice(
|
|
52
|
+
c.req.param("id"),
|
|
53
|
+
c.req.valid("json").to,
|
|
54
|
+
c.get("actor"),
|
|
55
|
+
);
|
|
56
|
+
if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
57
|
+
return c.json({ data: result.value });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return router;
|
|
61
|
+
}
|