@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.
Files changed (289) hide show
  1. package/dist/auth/setup.d.ts.map +1 -1
  2. package/dist/auth/setup.js +1 -0
  3. package/dist/config/defaults.d.ts.map +1 -1
  4. package/dist/config/defaults.js +5 -0
  5. package/dist/config/types.d.ts +17 -1
  6. package/dist/config/types.d.ts.map +1 -1
  7. package/dist/generated/plugin-manifest.d.ts +3 -0
  8. package/dist/generated/plugin-manifest.d.ts.map +1 -1
  9. package/dist/generated/plugin-manifest.js +1 -0
  10. package/dist/hooks/checkout.d.ts.map +1 -1
  11. package/dist/hooks/checkout.js +16 -1
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1 -0
  15. package/dist/interfaces/rest/index.d.ts.map +1 -1
  16. package/dist/interfaces/rest/index.js +12 -0
  17. package/dist/interfaces/rest/routes/admin/staff.d.ts +12 -0
  18. package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -0
  19. package/dist/interfaces/rest/routes/admin/staff.js +172 -0
  20. package/dist/interfaces/rest/routes/analytics.d.ts +5 -0
  21. package/dist/interfaces/rest/routes/analytics.d.ts.map +1 -0
  22. package/dist/interfaces/rest/routes/analytics.js +24 -0
  23. package/dist/interfaces/rest/routes/carts.d.ts.map +1 -1
  24. package/dist/interfaces/rest/routes/carts.js +39 -2
  25. package/dist/interfaces/rest/routes/catalog.d.ts.map +1 -1
  26. package/dist/interfaces/rest/routes/catalog.js +17 -1
  27. package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
  28. package/dist/interfaces/rest/routes/checkout.js +17 -0
  29. package/dist/interfaces/rest/routes/documents.d.ts +10 -0
  30. package/dist/interfaces/rest/routes/documents.d.ts.map +1 -0
  31. package/dist/interfaces/rest/routes/documents.js +40 -0
  32. package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
  33. package/dist/interfaces/rest/routes/orders.js +98 -1
  34. package/dist/interfaces/rest/routes/pricing.d.ts.map +1 -1
  35. package/dist/interfaces/rest/routes/pricing.js +36 -1
  36. package/dist/interfaces/rest/routes/settings.d.ts +5 -0
  37. package/dist/interfaces/rest/routes/settings.d.ts.map +1 -0
  38. package/dist/interfaces/rest/routes/settings.js +50 -0
  39. package/dist/interfaces/rest/routes/shipping.d.ts +5 -0
  40. package/dist/interfaces/rest/routes/shipping.d.ts.map +1 -0
  41. package/dist/interfaces/rest/routes/shipping.js +68 -0
  42. package/dist/interfaces/rest/routes/tax.d.ts +5 -0
  43. package/dist/interfaces/rest/routes/tax.d.ts.map +1 -0
  44. package/dist/interfaces/rest/routes/tax.js +68 -0
  45. package/dist/interfaces/rest/schemas/admin-staff.d.ts +899 -0
  46. package/dist/interfaces/rest/schemas/admin-staff.d.ts.map +1 -0
  47. package/dist/interfaces/rest/schemas/admin-staff.js +98 -0
  48. package/dist/interfaces/rest/schemas/analytics.d.ts +252 -0
  49. package/dist/interfaces/rest/schemas/analytics.d.ts.map +1 -0
  50. package/dist/interfaces/rest/schemas/analytics.js +33 -0
  51. package/dist/interfaces/rest/schemas/carts.d.ts +656 -0
  52. package/dist/interfaces/rest/schemas/carts.d.ts.map +1 -1
  53. package/dist/interfaces/rest/schemas/carts.js +38 -0
  54. package/dist/interfaces/rest/schemas/catalog.d.ts +1218 -1
  55. package/dist/interfaces/rest/schemas/catalog.d.ts.map +1 -1
  56. package/dist/interfaces/rest/schemas/catalog.js +48 -0
  57. package/dist/interfaces/rest/schemas/checkout.d.ts +2 -0
  58. package/dist/interfaces/rest/schemas/checkout.d.ts.map +1 -1
  59. package/dist/interfaces/rest/schemas/checkout.js +4 -0
  60. package/dist/interfaces/rest/schemas/customer-portal.d.ts +35 -0
  61. package/dist/interfaces/rest/schemas/customer-portal.d.ts.map +1 -1
  62. package/dist/interfaces/rest/schemas/documents.d.ts +509 -0
  63. package/dist/interfaces/rest/schemas/documents.d.ts.map +1 -0
  64. package/dist/interfaces/rest/schemas/documents.js +65 -0
  65. package/dist/interfaces/rest/schemas/orders.d.ts +3110 -43
  66. package/dist/interfaces/rest/schemas/orders.d.ts.map +1 -1
  67. package/dist/interfaces/rest/schemas/orders.js +233 -0
  68. package/dist/interfaces/rest/schemas/pricing.d.ts +405 -0
  69. package/dist/interfaces/rest/schemas/pricing.d.ts.map +1 -1
  70. package/dist/interfaces/rest/schemas/pricing.js +70 -0
  71. package/dist/interfaces/rest/schemas/responses.d.ts +156 -0
  72. package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
  73. package/dist/interfaces/rest/schemas/search.d.ts +17 -0
  74. package/dist/interfaces/rest/schemas/search.d.ts.map +1 -1
  75. package/dist/interfaces/rest/schemas/settings.d.ts +397 -0
  76. package/dist/interfaces/rest/schemas/settings.d.ts.map +1 -0
  77. package/dist/interfaces/rest/schemas/settings.js +79 -0
  78. package/dist/interfaces/rest/schemas/shipping.d.ts +1093 -0
  79. package/dist/interfaces/rest/schemas/shipping.d.ts.map +1 -0
  80. package/dist/interfaces/rest/schemas/shipping.js +146 -0
  81. package/dist/interfaces/rest/schemas/tax.d.ts +1074 -0
  82. package/dist/interfaces/rest/schemas/tax.d.ts.map +1 -0
  83. package/dist/interfaces/rest/schemas/tax.js +132 -0
  84. package/dist/kernel/database/schema.d.ts +4 -0
  85. package/dist/kernel/database/schema.d.ts.map +1 -1
  86. package/dist/kernel/database/schema.js +8 -0
  87. package/dist/kernel/hooks/types.d.ts +1 -1
  88. package/dist/kernel/hooks/types.d.ts.map +1 -1
  89. package/dist/kernel/plugin/manifest.d.ts +23 -0
  90. package/dist/kernel/plugin/manifest.d.ts.map +1 -1
  91. package/dist/kernel/plugin/manifest.js +17 -2
  92. package/dist/modules/analytics/module.d.ts.map +1 -1
  93. package/dist/modules/analytics/module.js +6 -0
  94. package/dist/modules/analytics/reports.d.ts +58 -0
  95. package/dist/modules/analytics/reports.d.ts.map +1 -0
  96. package/dist/modules/analytics/reports.js +249 -0
  97. package/dist/modules/analytics/service.d.ts +10 -0
  98. package/dist/modules/analytics/service.d.ts.map +1 -1
  99. package/dist/modules/analytics/service.js +14 -0
  100. package/dist/modules/cart/repository/index.d.ts +7 -0
  101. package/dist/modules/cart/repository/index.d.ts.map +1 -1
  102. package/dist/modules/cart/repository/index.js +25 -1
  103. package/dist/modules/cart/schema.d.ts +17 -0
  104. package/dist/modules/cart/schema.d.ts.map +1 -1
  105. package/dist/modules/cart/schema.js +2 -0
  106. package/dist/modules/cart/schemas.d.ts +1 -0
  107. package/dist/modules/cart/schemas.d.ts.map +1 -1
  108. package/dist/modules/cart/schemas.js +1 -0
  109. package/dist/modules/cart/service.d.ts +32 -0
  110. package/dist/modules/cart/service.d.ts.map +1 -1
  111. package/dist/modules/cart/service.js +58 -0
  112. package/dist/modules/catalog/entity-service.d.ts +21 -0
  113. package/dist/modules/catalog/entity-service.d.ts.map +1 -1
  114. package/dist/modules/catalog/entity-service.js +132 -3
  115. package/dist/modules/catalog/schema.d.ts +34 -0
  116. package/dist/modules/catalog/schema.d.ts.map +1 -1
  117. package/dist/modules/catalog/schema.js +5 -0
  118. package/dist/modules/catalog/schemas.d.ts +3 -0
  119. package/dist/modules/catalog/schemas.d.ts.map +1 -1
  120. package/dist/modules/catalog/schemas.js +3 -0
  121. package/dist/modules/catalog/service.d.ts +36 -0
  122. package/dist/modules/catalog/service.d.ts.map +1 -1
  123. package/dist/modules/catalog/service.js +6 -0
  124. package/dist/modules/documents/module.d.ts +7 -0
  125. package/dist/modules/documents/module.d.ts.map +1 -0
  126. package/dist/modules/documents/module.js +12 -0
  127. package/dist/modules/documents/pdf.d.ts +25 -0
  128. package/dist/modules/documents/pdf.d.ts.map +1 -0
  129. package/dist/modules/documents/pdf.js +80 -0
  130. package/dist/modules/documents/render.d.ts +32 -0
  131. package/dist/modules/documents/render.d.ts.map +1 -0
  132. package/dist/modules/documents/render.js +190 -0
  133. package/dist/modules/documents/repository/index.d.ts +25 -0
  134. package/dist/modules/documents/repository/index.d.ts.map +1 -0
  135. package/dist/modules/documents/repository/index.js +47 -0
  136. package/dist/modules/documents/schema.d.ts +214 -0
  137. package/dist/modules/documents/schema.d.ts.map +1 -0
  138. package/dist/modules/documents/schema.js +42 -0
  139. package/dist/modules/documents/service.d.ts +51 -0
  140. package/dist/modules/documents/service.d.ts.map +1 -0
  141. package/dist/modules/documents/service.js +119 -0
  142. package/dist/modules/fulfillment/service.d.ts +18 -0
  143. package/dist/modules/fulfillment/service.d.ts.map +1 -1
  144. package/dist/modules/fulfillment/service.js +85 -1
  145. package/dist/modules/orders/repository/index.d.ts +18 -1
  146. package/dist/modules/orders/repository/index.d.ts.map +1 -1
  147. package/dist/modules/orders/repository/index.js +86 -1
  148. package/dist/modules/orders/schema.d.ts +374 -0
  149. package/dist/modules/orders/schema.d.ts.map +1 -1
  150. package/dist/modules/orders/schema.js +52 -1
  151. package/dist/modules/orders/service.d.ts +86 -1
  152. package/dist/modules/orders/service.d.ts.map +1 -1
  153. package/dist/modules/orders/service.js +447 -20
  154. package/dist/modules/pricing/repository/index.d.ts +5 -0
  155. package/dist/modules/pricing/repository/index.d.ts.map +1 -1
  156. package/dist/modules/pricing/repository/index.js +19 -0
  157. package/dist/modules/pricing/service.d.ts +18 -0
  158. package/dist/modules/pricing/service.d.ts.map +1 -1
  159. package/dist/modules/pricing/service.js +38 -0
  160. package/dist/modules/settings/module.d.ts +6 -0
  161. package/dist/modules/settings/module.d.ts.map +1 -0
  162. package/dist/modules/settings/module.js +11 -0
  163. package/dist/modules/settings/repository/index.d.ts +16 -0
  164. package/dist/modules/settings/repository/index.d.ts.map +1 -0
  165. package/dist/modules/settings/repository/index.js +41 -0
  166. package/dist/modules/settings/schema.d.ts +121 -0
  167. package/dist/modules/settings/schema.d.ts.map +1 -0
  168. package/dist/modules/settings/schema.js +24 -0
  169. package/dist/modules/settings/service.d.ts +26 -0
  170. package/dist/modules/settings/service.d.ts.map +1 -0
  171. package/dist/modules/settings/service.js +51 -0
  172. package/dist/modules/shipping/calculator.d.ts +5 -0
  173. package/dist/modules/shipping/calculator.d.ts.map +1 -1
  174. package/dist/modules/shipping/calculator.js +10 -0
  175. package/dist/modules/shipping/module.d.ts +5 -1
  176. package/dist/modules/shipping/module.d.ts.map +1 -1
  177. package/dist/modules/shipping/module.js +4 -1
  178. package/dist/modules/shipping/repository/index.d.ts +27 -2
  179. package/dist/modules/shipping/repository/index.d.ts.map +1 -1
  180. package/dist/modules/shipping/repository/index.js +105 -1
  181. package/dist/modules/shipping/schema.d.ts +419 -0
  182. package/dist/modules/shipping/schema.d.ts.map +1 -0
  183. package/dist/modules/shipping/schema.js +56 -0
  184. package/dist/modules/shipping/service.d.ts +54 -0
  185. package/dist/modules/shipping/service.d.ts.map +1 -1
  186. package/dist/modules/shipping/service.js +161 -2
  187. package/dist/modules/tax/adapter.d.ts +13 -0
  188. package/dist/modules/tax/adapter.d.ts.map +1 -1
  189. package/dist/modules/tax/module.d.ts +5 -1
  190. package/dist/modules/tax/module.d.ts.map +1 -1
  191. package/dist/modules/tax/module.js +4 -1
  192. package/dist/modules/tax/repository/index.d.ts +38 -2
  193. package/dist/modules/tax/repository/index.d.ts.map +1 -1
  194. package/dist/modules/tax/repository/index.js +138 -1
  195. package/dist/modules/tax/schema.d.ts +355 -0
  196. package/dist/modules/tax/schema.d.ts.map +1 -0
  197. package/dist/modules/tax/schema.js +55 -0
  198. package/dist/modules/tax/service.d.ts +60 -1
  199. package/dist/modules/tax/service.d.ts.map +1 -1
  200. package/dist/modules/tax/service.js +227 -2
  201. package/dist/runtime/kernel-modules.d.ts +15 -2
  202. package/dist/runtime/kernel-modules.d.ts.map +1 -1
  203. package/dist/runtime/kernel-modules.js +4 -0
  204. package/dist/runtime/kernel-types.d.ts +5 -1
  205. package/dist/runtime/kernel-types.d.ts.map +1 -1
  206. package/dist/runtime/kernel-types.js +2 -0
  207. package/dist/runtime/server.js +1 -1
  208. package/dist/test-utils/create-plugin-test-app.d.ts +3 -0
  209. package/dist/test-utils/create-plugin-test-app.d.ts.map +1 -1
  210. package/dist/test-utils/create-plugin-test-app.js +15 -3
  211. package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
  212. package/dist/test-utils/rest-api-test-utils.js +6 -0
  213. package/package.json +3 -3
  214. package/src/auth/setup.ts +1 -0
  215. package/src/config/defaults.ts +5 -0
  216. package/src/config/types.ts +17 -1
  217. package/src/generated/plugin-capabilities.d.ts +1 -0
  218. package/src/generated/plugin-manifest.ts +1 -0
  219. package/src/generated/plugin-repositories.d.ts +1 -0
  220. package/src/hooks/checkout.ts +25 -3
  221. package/src/index.ts +1 -0
  222. package/src/interfaces/rest/index.ts +12 -0
  223. package/src/interfaces/rest/routes/admin/staff.ts +218 -0
  224. package/src/interfaces/rest/routes/analytics.ts +33 -0
  225. package/src/interfaces/rest/routes/carts.ts +52 -2
  226. package/src/interfaces/rest/routes/catalog.ts +34 -0
  227. package/src/interfaces/rest/routes/checkout.ts +22 -0
  228. package/src/interfaces/rest/routes/documents.ts +61 -0
  229. package/src/interfaces/rest/routes/orders.ts +137 -1
  230. package/src/interfaces/rest/routes/pricing.ts +44 -1
  231. package/src/interfaces/rest/routes/settings.ts +61 -0
  232. package/src/interfaces/rest/routes/shipping.ts +92 -0
  233. package/src/interfaces/rest/routes/tax.ts +90 -0
  234. package/src/interfaces/rest/schemas/admin-staff.ts +110 -0
  235. package/src/interfaces/rest/schemas/analytics.ts +39 -0
  236. package/src/interfaces/rest/schemas/carts.ts +40 -0
  237. package/src/interfaces/rest/schemas/catalog.ts +53 -0
  238. package/src/interfaces/rest/schemas/checkout.ts +4 -0
  239. package/src/interfaces/rest/schemas/documents.ts +72 -0
  240. package/src/interfaces/rest/schemas/orders.ts +257 -0
  241. package/src/interfaces/rest/schemas/pricing.ts +75 -0
  242. package/src/interfaces/rest/schemas/settings.ts +93 -0
  243. package/src/interfaces/rest/schemas/shipping.ts +164 -0
  244. package/src/interfaces/rest/schemas/tax.ts +148 -0
  245. package/src/kernel/database/schema.ts +12 -0
  246. package/src/kernel/hooks/types.ts +1 -0
  247. package/src/kernel/plugin/manifest.ts +38 -2
  248. package/src/modules/analytics/module.ts +10 -0
  249. package/src/modules/analytics/reports.ts +311 -0
  250. package/src/modules/analytics/service.ts +23 -0
  251. package/src/modules/cart/repository/index.ts +30 -1
  252. package/src/modules/cart/schema.ts +2 -0
  253. package/src/modules/cart/schemas.ts +1 -0
  254. package/src/modules/cart/service.ts +101 -0
  255. package/src/modules/catalog/entity-service.ts +168 -3
  256. package/src/modules/catalog/schema.ts +5 -0
  257. package/src/modules/catalog/schemas.ts +3 -0
  258. package/src/modules/catalog/service.ts +30 -0
  259. package/src/modules/documents/module.ts +19 -0
  260. package/src/modules/documents/pdf.ts +101 -0
  261. package/src/modules/documents/render.ts +231 -0
  262. package/src/modules/documents/repository/index.ts +68 -0
  263. package/src/modules/documents/schema.ts +44 -0
  264. package/src/modules/documents/service.ts +176 -0
  265. package/src/modules/fulfillment/service.ts +139 -1
  266. package/src/modules/orders/repository/index.ts +127 -1
  267. package/src/modules/orders/schema.ts +54 -1
  268. package/src/modules/orders/service.ts +654 -22
  269. package/src/modules/pricing/repository/index.ts +34 -0
  270. package/src/modules/pricing/service.ts +71 -0
  271. package/src/modules/settings/module.ts +18 -0
  272. package/src/modules/settings/repository/index.ts +59 -0
  273. package/src/modules/settings/schema.ts +25 -0
  274. package/src/modules/settings/service.ts +82 -0
  275. package/src/modules/shipping/calculator.ts +27 -0
  276. package/src/modules/shipping/module.ts +6 -2
  277. package/src/modules/shipping/repository/index.ts +136 -2
  278. package/src/modules/shipping/schema.ts +58 -0
  279. package/src/modules/shipping/service.ts +260 -1
  280. package/src/modules/tax/adapter.ts +9 -0
  281. package/src/modules/tax/module.ts +6 -2
  282. package/src/modules/tax/repository/index.ts +169 -2
  283. package/src/modules/tax/schema.ts +57 -0
  284. package/src/modules/tax/service.ts +313 -2
  285. package/src/runtime/kernel-modules.ts +4 -0
  286. package/src/runtime/kernel-types.ts +6 -0
  287. package/src/runtime/server.ts +1 -1
  288. package/src/test-utils/create-plugin-test-app.ts +17 -4
  289. package/src/test-utils/rest-api-test-utils.ts +6 -0
@@ -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
+ }