@stacksjs/commerce 0.70.55 → 0.70.57

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 (297) hide show
  1. package/package.json +10 -7
  2. package/src/carts/destroy.ts +0 -54
  3. package/src/carts/fetch.ts +0 -20
  4. package/src/carts/index.ts +0 -13
  5. package/src/carts/store.ts +0 -75
  6. package/src/carts/update.ts +0 -78
  7. package/src/coupons/destroy.ts +0 -49
  8. package/src/coupons/fetch.ts +0 -510
  9. package/src/coupons/index.ts +0 -30
  10. package/src/coupons/store.ts +0 -46
  11. package/src/coupons/update.ts +0 -147
  12. package/src/customers/destroy.ts +0 -62
  13. package/src/customers/fetch.ts +0 -20
  14. package/src/customers/index.ts +0 -17
  15. package/src/customers/store.ts +0 -45
  16. package/src/customers/update.ts +0 -44
  17. package/src/devices/destroy.ts +0 -54
  18. package/src/devices/export.ts +0 -98
  19. package/src/devices/fetch.ts +0 -125
  20. package/src/devices/index.ts +0 -31
  21. package/src/devices/store.ts +0 -82
  22. package/src/devices/update.ts +0 -125
  23. package/src/errors/destroy.ts +0 -54
  24. package/src/errors/fetch.ts +0 -197
  25. package/src/errors/index.ts +0 -28
  26. package/src/errors/update.ts +0 -116
  27. package/src/gift-cards/destroy.ts +0 -87
  28. package/src/gift-cards/fetch.ts +0 -431
  29. package/src/gift-cards/index.ts +0 -26
  30. package/src/gift-cards/store.ts +0 -56
  31. package/src/gift-cards/update.ts +0 -102
  32. package/src/index.ts +0 -78
  33. package/src/orders/destroy.ts +0 -111
  34. package/src/orders/events.ts +0 -121
  35. package/src/orders/export.ts +0 -158
  36. package/src/orders/fetch.ts +0 -379
  37. package/src/orders/guards.ts +0 -261
  38. package/src/orders/index.ts +0 -91
  39. package/src/orders/place-order.ts +0 -274
  40. package/src/orders/store.ts +0 -51
  41. package/src/orders/totals.ts +0 -190
  42. package/src/orders/update.ts +0 -164
  43. package/src/orders/webhook.ts +0 -312
  44. package/src/payments/destroy.ts +0 -56
  45. package/src/payments/fetch.ts +0 -288
  46. package/src/payments/index.ts +0 -16
  47. package/src/payments/store.ts +0 -63
  48. package/src/payments/update.ts +0 -52
  49. package/src/product.ts +0 -0
  50. package/src/products/categories/destroy.ts +0 -103
  51. package/src/products/categories/fetch.ts +0 -285
  52. package/src/products/categories/index.ts +0 -36
  53. package/src/products/categories/store.ts +0 -87
  54. package/src/products/categories/update.ts +0 -251
  55. package/src/products/index.ts +0 -35
  56. package/src/products/items/destroy.ts +0 -54
  57. package/src/products/items/fetch.ts +0 -282
  58. package/src/products/items/index.ts +0 -26
  59. package/src/products/items/store.ts +0 -84
  60. package/src/products/items/update.ts +0 -324
  61. package/src/products/manufacturers/destroy.ts +0 -56
  62. package/src/products/manufacturers/fetch.ts +0 -117
  63. package/src/products/manufacturers/index.ts +0 -24
  64. package/src/products/manufacturers/store.ts +0 -82
  65. package/src/products/manufacturers/update.ts +0 -113
  66. package/src/products/reviews/destroy.ts +0 -56
  67. package/src/products/reviews/fetch.ts +0 -63
  68. package/src/products/reviews/index.ts +0 -25
  69. package/src/products/reviews/store.ts +0 -46
  70. package/src/products/reviews/update.ts +0 -82
  71. package/src/products/units/destroy.ts +0 -56
  72. package/src/products/units/fetch.ts +0 -20
  73. package/src/products/units/index.ts +0 -13
  74. package/src/products/units/store.ts +0 -162
  75. package/src/products/units/update.ts +0 -159
  76. package/src/products/variants/destroy.ts +0 -56
  77. package/src/products/variants/fetch.ts +0 -20
  78. package/src/products/variants/index.ts +0 -21
  79. package/src/products/variants/store.ts +0 -136
  80. package/src/products/variants/update.ts +0 -104
  81. package/src/receipts/destroy.ts +0 -54
  82. package/src/receipts/export.ts +0 -0
  83. package/src/receipts/fetch.ts +0 -222
  84. package/src/receipts/index.ts +0 -24
  85. package/src/receipts/printer.ts +0 -69
  86. package/src/receipts/store.ts +0 -69
  87. package/src/receipts/update.ts +0 -124
  88. package/src/shippings/delivery-routes/destroy.ts +0 -54
  89. package/src/shippings/delivery-routes/fetch.ts +0 -42
  90. package/src/shippings/delivery-routes/index.ts +0 -26
  91. package/src/shippings/delivery-routes/store.ts +0 -69
  92. package/src/shippings/delivery-routes/update.ts +0 -120
  93. package/src/shippings/digital-deliveries/destroy.ts +0 -111
  94. package/src/shippings/digital-deliveries/fetch.ts +0 -20
  95. package/src/shippings/digital-deliveries/index.ts +0 -26
  96. package/src/shippings/digital-deliveries/store.ts +0 -76
  97. package/src/shippings/digital-deliveries/update.ts +0 -129
  98. package/src/shippings/drivers/destroy.ts +0 -78
  99. package/src/shippings/drivers/fetch.ts +0 -20
  100. package/src/shippings/drivers/index.ts +0 -24
  101. package/src/shippings/drivers/store.ts +0 -69
  102. package/src/shippings/drivers/update.ts +0 -117
  103. package/src/shippings/index.ts +0 -39
  104. package/src/shippings/license-keys/destroy.ts +0 -111
  105. package/src/shippings/license-keys/fetch.ts +0 -20
  106. package/src/shippings/license-keys/index.ts +0 -26
  107. package/src/shippings/license-keys/store.ts +0 -77
  108. package/src/shippings/license-keys/update.ts +0 -117
  109. package/src/shippings/shipping-methods/destroy.ts +0 -111
  110. package/src/shippings/shipping-methods/fetch.ts +0 -62
  111. package/src/shippings/shipping-methods/index.ts +0 -28
  112. package/src/shippings/shipping-methods/store.ts +0 -123
  113. package/src/shippings/shipping-methods/update.ts +0 -120
  114. package/src/shippings/shipping-rates/destroy.ts +0 -102
  115. package/src/shippings/shipping-rates/fetch.ts +0 -320
  116. package/src/shippings/shipping-rates/index.ts +0 -36
  117. package/src/shippings/shipping-rates/store.ts +0 -102
  118. package/src/shippings/shipping-rates/update.ts +0 -139
  119. package/src/shippings/shipping-zones/destroy.ts +0 -111
  120. package/src/shippings/shipping-zones/fetch.ts +0 -96
  121. package/src/shippings/shipping-zones/index.ts +0 -26
  122. package/src/shippings/shipping-zones/store.ts +0 -74
  123. package/src/shippings/shipping-zones/update.ts +0 -156
  124. package/src/tax/destroy.ts +0 -54
  125. package/src/tax/fetch.ts +0 -20
  126. package/src/tax/index.ts +0 -24
  127. package/src/tax/store.ts +0 -69
  128. package/src/tax/update.ts +0 -119
  129. package/src/tests/coupons.test.ts +0 -90
  130. package/src/tests/customers.test.ts +0 -39
  131. package/src/tests/device.test.ts +0 -111
  132. package/src/tests/digitals.test.ts +0 -31
  133. package/src/tests/drivers.test.ts +0 -25
  134. package/src/tests/gift-cards.test.ts +0 -36
  135. package/src/tests/license.test.ts +0 -31
  136. package/src/tests/manufacturers.test.ts +0 -80
  137. package/src/tests/orders.test.ts +0 -25
  138. package/src/tests/payments.test.ts +0 -75
  139. package/src/tests/rates.test.ts +0 -367
  140. package/src/tests/receipts.test.ts +0 -287
  141. package/src/tests/restaurant.test.ts +0 -522
  142. package/src/tests/reviews.test.ts +0 -165
  143. package/src/tests/routes.test.ts +0 -51
  144. package/src/tests/setup.ts +0 -149
  145. package/src/tests/shipping.test.ts +0 -88
  146. package/src/tests/tax.test.ts +0 -25
  147. package/src/tests/units.test.ts +0 -110
  148. package/src/tests/variants.test.ts +0 -133
  149. package/src/tests/waitlist.test.ts +0 -303
  150. package/src/tests/zones.test.ts +0 -86
  151. package/src/types.ts +0 -312
  152. package/src/utils/typed-stats.ts +0 -67
  153. package/src/waitlists/index.ts +0 -19
  154. package/src/waitlists/products/destroy.ts +0 -65
  155. package/src/waitlists/products/export.ts +0 -113
  156. package/src/waitlists/products/fetch.ts +0 -362
  157. package/src/waitlists/products/index.ts +0 -33
  158. package/src/waitlists/products/store.ts +0 -112
  159. package/src/waitlists/products/update.ts +0 -125
  160. package/src/waitlists/restaurant/destroy.ts +0 -65
  161. package/src/waitlists/restaurant/export.ts +0 -107
  162. package/src/waitlists/restaurant/fetch.ts +0 -538
  163. package/src/waitlists/restaurant/index.ts +0 -43
  164. package/src/waitlists/restaurant/store.ts +0 -109
  165. package/src/waitlists/restaurant/update.ts +0 -198
  166. /package/dist/{src/coupons → coupons}/destroy.d.ts +0 -0
  167. /package/dist/{src/coupons → coupons}/fetch.d.ts +0 -0
  168. /package/dist/{src/coupons → coupons}/index.d.ts +0 -0
  169. /package/dist/{src/coupons → coupons}/store.d.ts +0 -0
  170. /package/dist/{src/coupons → coupons}/update.d.ts +0 -0
  171. /package/dist/{src/customers → customers}/destroy.d.ts +0 -0
  172. /package/dist/{src/customers → customers}/fetch.d.ts +0 -0
  173. /package/dist/{src/customers → customers}/index.d.ts +0 -0
  174. /package/dist/{src/customers → customers}/store.d.ts +0 -0
  175. /package/dist/{src/customers → customers}/update.d.ts +0 -0
  176. /package/dist/{src/devices → devices}/destroy.d.ts +0 -0
  177. /package/dist/{src/devices → devices}/export.d.ts +0 -0
  178. /package/dist/{src/devices → devices}/fetch.d.ts +0 -0
  179. /package/dist/{src/devices → devices}/index.d.ts +0 -0
  180. /package/dist/{src/devices → devices}/store.d.ts +0 -0
  181. /package/dist/{src/devices → devices}/update.d.ts +0 -0
  182. /package/dist/{src/errors → errors}/destroy.d.ts +0 -0
  183. /package/dist/{src/errors → errors}/fetch.d.ts +0 -0
  184. /package/dist/{src/errors → errors}/index.d.ts +0 -0
  185. /package/dist/{src/errors → errors}/update.d.ts +0 -0
  186. /package/dist/{src/gift-cards → gift-cards}/destroy.d.ts +0 -0
  187. /package/dist/{src/gift-cards → gift-cards}/fetch.d.ts +0 -0
  188. /package/dist/{src/gift-cards → gift-cards}/index.d.ts +0 -0
  189. /package/dist/{src/gift-cards → gift-cards}/store.d.ts +0 -0
  190. /package/dist/{src/gift-cards → gift-cards}/update.d.ts +0 -0
  191. /package/dist/{src/index.d.ts → index.d.ts} +0 -0
  192. /package/dist/{src/orders → orders}/destroy.d.ts +0 -0
  193. /package/dist/{src/orders → orders}/events.d.ts +0 -0
  194. /package/dist/{src/orders → orders}/export.d.ts +0 -0
  195. /package/dist/{src/orders → orders}/fetch.d.ts +0 -0
  196. /package/dist/{src/orders → orders}/guards.d.ts +0 -0
  197. /package/dist/{src/orders → orders}/index.d.ts +0 -0
  198. /package/dist/{src/orders → orders}/place-order.d.ts +0 -0
  199. /package/dist/{src/orders → orders}/store.d.ts +0 -0
  200. /package/dist/{src/orders → orders}/totals.d.ts +0 -0
  201. /package/dist/{src/orders → orders}/update.d.ts +0 -0
  202. /package/dist/{src/orders → orders}/webhook.d.ts +0 -0
  203. /package/dist/{src/payments → payments}/destroy.d.ts +0 -0
  204. /package/dist/{src/payments → payments}/fetch.d.ts +0 -0
  205. /package/dist/{src/payments → payments}/index.d.ts +0 -0
  206. /package/dist/{src/payments → payments}/store.d.ts +0 -0
  207. /package/dist/{src/payments → payments}/update.d.ts +0 -0
  208. /package/dist/{src/products → products}/categories/destroy.d.ts +0 -0
  209. /package/dist/{src/products → products}/categories/fetch.d.ts +0 -0
  210. /package/dist/{src/products → products}/categories/index.d.ts +0 -0
  211. /package/dist/{src/products → products}/categories/store.d.ts +0 -0
  212. /package/dist/{src/products → products}/categories/update.d.ts +0 -0
  213. /package/dist/{src/products → products}/index.d.ts +0 -0
  214. /package/dist/{src/products → products}/items/destroy.d.ts +0 -0
  215. /package/dist/{src/products → products}/items/fetch.d.ts +0 -0
  216. /package/dist/{src/products → products}/items/index.d.ts +0 -0
  217. /package/dist/{src/products → products}/items/store.d.ts +0 -0
  218. /package/dist/{src/products → products}/items/update.d.ts +0 -0
  219. /package/dist/{src/products → products}/manufacturers/destroy.d.ts +0 -0
  220. /package/dist/{src/products → products}/manufacturers/fetch.d.ts +0 -0
  221. /package/dist/{src/products → products}/manufacturers/index.d.ts +0 -0
  222. /package/dist/{src/products → products}/manufacturers/store.d.ts +0 -0
  223. /package/dist/{src/products → products}/manufacturers/update.d.ts +0 -0
  224. /package/dist/{src/products → products}/reviews/destroy.d.ts +0 -0
  225. /package/dist/{src/products → products}/reviews/fetch.d.ts +0 -0
  226. /package/dist/{src/products → products}/reviews/index.d.ts +0 -0
  227. /package/dist/{src/products → products}/reviews/store.d.ts +0 -0
  228. /package/dist/{src/products → products}/reviews/update.d.ts +0 -0
  229. /package/dist/{src/products → products}/units/destroy.d.ts +0 -0
  230. /package/dist/{src/products → products}/units/fetch.d.ts +0 -0
  231. /package/dist/{src/products → products}/units/index.d.ts +0 -0
  232. /package/dist/{src/products → products}/units/store.d.ts +0 -0
  233. /package/dist/{src/products → products}/units/update.d.ts +0 -0
  234. /package/dist/{src/products → products}/variants/destroy.d.ts +0 -0
  235. /package/dist/{src/products → products}/variants/fetch.d.ts +0 -0
  236. /package/dist/{src/products → products}/variants/index.d.ts +0 -0
  237. /package/dist/{src/products → products}/variants/store.d.ts +0 -0
  238. /package/dist/{src/products → products}/variants/update.d.ts +0 -0
  239. /package/dist/{src/receipts → receipts}/destroy.d.ts +0 -0
  240. /package/dist/{src/receipts → receipts}/fetch.d.ts +0 -0
  241. /package/dist/{src/receipts → receipts}/index.d.ts +0 -0
  242. /package/dist/{src/receipts → receipts}/store.d.ts +0 -0
  243. /package/dist/{src/receipts → receipts}/update.d.ts +0 -0
  244. /package/dist/{src/shippings → shippings}/delivery-routes/destroy.d.ts +0 -0
  245. /package/dist/{src/shippings → shippings}/delivery-routes/fetch.d.ts +0 -0
  246. /package/dist/{src/shippings → shippings}/delivery-routes/index.d.ts +0 -0
  247. /package/dist/{src/shippings → shippings}/delivery-routes/store.d.ts +0 -0
  248. /package/dist/{src/shippings → shippings}/delivery-routes/update.d.ts +0 -0
  249. /package/dist/{src/shippings → shippings}/digital-deliveries/destroy.d.ts +0 -0
  250. /package/dist/{src/shippings → shippings}/digital-deliveries/fetch.d.ts +0 -0
  251. /package/dist/{src/shippings → shippings}/digital-deliveries/index.d.ts +0 -0
  252. /package/dist/{src/shippings → shippings}/digital-deliveries/store.d.ts +0 -0
  253. /package/dist/{src/shippings → shippings}/digital-deliveries/update.d.ts +0 -0
  254. /package/dist/{src/shippings → shippings}/drivers/destroy.d.ts +0 -0
  255. /package/dist/{src/shippings → shippings}/drivers/fetch.d.ts +0 -0
  256. /package/dist/{src/shippings → shippings}/drivers/index.d.ts +0 -0
  257. /package/dist/{src/shippings → shippings}/drivers/store.d.ts +0 -0
  258. /package/dist/{src/shippings → shippings}/drivers/update.d.ts +0 -0
  259. /package/dist/{src/shippings → shippings}/index.d.ts +0 -0
  260. /package/dist/{src/shippings → shippings}/license-keys/destroy.d.ts +0 -0
  261. /package/dist/{src/shippings → shippings}/license-keys/fetch.d.ts +0 -0
  262. /package/dist/{src/shippings → shippings}/license-keys/index.d.ts +0 -0
  263. /package/dist/{src/shippings → shippings}/license-keys/store.d.ts +0 -0
  264. /package/dist/{src/shippings → shippings}/license-keys/update.d.ts +0 -0
  265. /package/dist/{src/shippings → shippings}/shipping-methods/destroy.d.ts +0 -0
  266. /package/dist/{src/shippings → shippings}/shipping-methods/fetch.d.ts +0 -0
  267. /package/dist/{src/shippings → shippings}/shipping-methods/index.d.ts +0 -0
  268. /package/dist/{src/shippings → shippings}/shipping-methods/store.d.ts +0 -0
  269. /package/dist/{src/shippings → shippings}/shipping-methods/update.d.ts +0 -0
  270. /package/dist/{src/shippings → shippings}/shipping-rates/destroy.d.ts +0 -0
  271. /package/dist/{src/shippings → shippings}/shipping-rates/fetch.d.ts +0 -0
  272. /package/dist/{src/shippings → shippings}/shipping-rates/index.d.ts +0 -0
  273. /package/dist/{src/shippings → shippings}/shipping-rates/store.d.ts +0 -0
  274. /package/dist/{src/shippings → shippings}/shipping-rates/update.d.ts +0 -0
  275. /package/dist/{src/shippings → shippings}/shipping-zones/destroy.d.ts +0 -0
  276. /package/dist/{src/shippings → shippings}/shipping-zones/fetch.d.ts +0 -0
  277. /package/dist/{src/shippings → shippings}/shipping-zones/index.d.ts +0 -0
  278. /package/dist/{src/shippings → shippings}/shipping-zones/store.d.ts +0 -0
  279. /package/dist/{src/shippings → shippings}/shipping-zones/update.d.ts +0 -0
  280. /package/dist/{src/tax → tax}/destroy.d.ts +0 -0
  281. /package/dist/{src/tax → tax}/fetch.d.ts +0 -0
  282. /package/dist/{src/tax → tax}/index.d.ts +0 -0
  283. /package/dist/{src/tax → tax}/store.d.ts +0 -0
  284. /package/dist/{src/tax → tax}/update.d.ts +0 -0
  285. /package/dist/{src/types.d.ts → types.d.ts} +0 -0
  286. /package/dist/{src/utils → utils}/typed-stats.d.ts +0 -0
  287. /package/dist/{src/waitlists → waitlists}/index.d.ts +0 -0
  288. /package/dist/{src/waitlists → waitlists}/products/destroy.d.ts +0 -0
  289. /package/dist/{src/waitlists → waitlists}/products/fetch.d.ts +0 -0
  290. /package/dist/{src/waitlists → waitlists}/products/index.d.ts +0 -0
  291. /package/dist/{src/waitlists → waitlists}/products/store.d.ts +0 -0
  292. /package/dist/{src/waitlists → waitlists}/products/update.d.ts +0 -0
  293. /package/dist/{src/waitlists → waitlists}/restaurant/destroy.d.ts +0 -0
  294. /package/dist/{src/waitlists → waitlists}/restaurant/fetch.d.ts +0 -0
  295. /package/dist/{src/waitlists → waitlists}/restaurant/index.d.ts +0 -0
  296. /package/dist/{src/waitlists → waitlists}/restaurant/store.d.ts +0 -0
  297. /package/dist/{src/waitlists → waitlists}/restaurant/update.d.ts +0 -0
@@ -1,91 +0,0 @@
1
- export {
2
- bulkDestroy,
3
- bulkSoftDelete,
4
- destroy,
5
- softDelete,
6
- } from './destroy'
7
-
8
- export {
9
- downloadOrders,
10
- exportOrders,
11
- storeOrdersExport,
12
- } from './export'
13
-
14
- export {
15
- calculateOrderMetrics,
16
- compareOrdersByPeriod,
17
- fetchAll,
18
- fetchById,
19
- fetchDailyOrderTrends,
20
- fetchStats,
21
- } from './fetch'
22
-
23
- export {
24
- store,
25
- } from './store'
26
-
27
- // Atomic order placement (stacksjs/stacks#1879 Co-1).
28
- // Wraps order + payment + inventory decrement in a single
29
- // transaction so any failure rolls back the rest.
30
- export { placeOrder } from './place-order'
31
- export type { PlaceOrderInput, PlaceOrderResult } from './place-order'
32
-
33
- // Event bus integration + status-transition state machine
34
- // (stacksjs/stacks#1879 Co-18, Co-4). Emit helpers fire
35
- // order:created / order:paid / order:shipped / order:delivered /
36
- // order:cancelled / order:refunded for downstream subscribers;
37
- // `canTransition` rejects illegal state changes.
38
- export {
39
- canTransition,
40
- emitForStatus,
41
- emitOrderCancelled,
42
- emitOrderCreated,
43
- emitOrderDelivered,
44
- emitOrderPaid,
45
- emitOrderRefunded,
46
- emitOrderShipped,
47
- } from './events'
48
- export type { OrderStatus } from './events'
49
-
50
- export {
51
- update,
52
- updateDeliveryInfo,
53
- updateStatus,
54
- } from './update'
55
-
56
- // Stripe webhook handlers (stacksjs/stacks#1879 Co-17). Call
57
- // registerCommerceWebhookHandlers() once at boot; the payments
58
- // package's processWebhook() verifies signatures + dispatches.
59
- export {
60
- handleChargeRefunded,
61
- handlePaymentIntentFailed,
62
- handlePaymentIntentSucceeded,
63
- registerCommerceWebhookHandlers,
64
- } from './webhook'
65
-
66
- // Server-side cart-to-order total recompute (stacksjs/stacks#1879 Co-13).
67
- // Apps pre-flight the cart through this before placeOrder to detect
68
- // client-supplied drift and reject "price changed" cases.
69
- export { recomputeOrderTotals } from './totals'
70
- export type {
71
- RecomputeLineItem,
72
- RecomputeOrderInput,
73
- RecomputeOrderResult,
74
- } from './totals'
75
-
76
- // Cart / order policy guards (stacksjs/stacks#1879 Co-7, Co-14, Co-15).
77
- // Pre-flight the cart through these before placeOrder; abandoned-
78
- // cart cleanup runs as a scheduled job.
79
- export {
80
- cleanupAbandonedCarts,
81
- validateCartQuantities,
82
- validateCouponStacking,
83
- validateQuantityBounds,
84
- } from './guards'
85
- export type {
86
- CleanupAbandonedCartsOptions,
87
- CleanupAbandonedCartsResult,
88
- CouponStackingPolicy,
89
- CouponStackingResult,
90
- QuantityBoundsResult,
91
- } from './guards'
@@ -1,274 +0,0 @@
1
- /**
2
- * Atomic order placement (stacksjs/stacks#1879 Co-1).
3
- *
4
- * Background: order creation, payment recording, and inventory
5
- * decrement happened as three separate writes. A failure between
6
- * steps left the system in a split-brain state:
7
- * - Order created but no payment row → reconciliation churn
8
- * - Payment created but no inventory decrement → over-sale
9
- * - Inventory decremented but order rolled back → phantom stock loss
10
- *
11
- * Fix: wrap all three writes in a single `db.transaction`. Either
12
- * everything commits or nothing does. The Stripe API call (a
13
- * network round-trip, not a DB write) stays OUTSIDE the transaction
14
- * to keep the transaction window short — apps that need
15
- * Stripe-side atomicity already get it via #1876 X-1's idempotency
16
- * keys (a retry of the local DB after a successful Stripe call
17
- * returns the same Stripe object).
18
- *
19
- * @example
20
- * ```ts
21
- * const result = await placeOrder({
22
- * order: { customer_id: user.id, total_amount: 12345, ... },
23
- * payment: { transaction_id: stripeIntent.id, amount: 12345, ... },
24
- * inventory: [
25
- * { id: cart.itemA, delta: -2 },
26
- * { id: cart.itemB, delta: -1 },
27
- * ],
28
- * })
29
- * if (!result.ok) {
30
- * // Stock changed between cart view and checkout, or payment
31
- * // duplicated — caller can show a useful error.
32
- * throw new HttpError(409, result.reason)
33
- * }
34
- * await sendOrderConfirmation(result.order)
35
- * ```
36
- */
37
-
38
- import { randomUUIDv7 } from 'bun'
39
- import { db } from '@stacksjs/database'
40
- import { formatDate } from '@stacksjs/orm'
41
- import { sql } from '@stacksjs/database'
42
- import { emitOrderCreated, emitOrderPaid } from './events'
43
-
44
- type OrderJsonResponse = ModelRow<typeof Order>
45
- type NewOrder = NewModelData<typeof Order>
46
- type PaymentJsonResponse = ModelRow<typeof Payment>
47
- type NewPayment = NewModelData<typeof Payment>
48
-
49
- export interface PlaceOrderInput {
50
- /** Order row fields. UUID / created_at / updated_at are filled in. */
51
- order: NewOrder
52
- /** Optional payment row to record alongside the order. */
53
- payment?: NewPayment
54
- /** Optional inventory decrements to apply (each delta is signed). */
55
- inventory?: ReadonlyArray<{ id: number, delta: number }>
56
- /**
57
- * Optional caller-supplied idempotency key (stacksjs/stacks#1879
58
- * Co-3). When set, the order is recorded against this key in a
59
- * dedup side-table; a retry with the same key returns the
60
- * original order id instead of creating a duplicate. Use this
61
- * when a user might double-click "place order" (network blip,
62
- * impatient retry, mobile background-sync race). UUID per
63
- * logical checkout attempt is typical; server-side `userId +
64
- * cartId` also works.
65
- */
66
- idempotencyKey?: string
67
- }
68
-
69
- /**
70
- * Discriminated result. `ok: true` means the order, payment, and
71
- * inventory all committed; `ok: false` carries a `reason` so the
72
- * caller can surface a specific error to the user.
73
- */
74
- export type PlaceOrderResult =
75
- | { ok: true, order: OrderJsonResponse, payment?: PaymentJsonResponse }
76
- | { ok: false, reason: 'out-of-stock' | 'duplicate-payment' | 'duplicate-idempotency-key' | 'unknown', failedAt?: string, error?: unknown }
77
-
78
- /**
79
- * Look up an existing order by idempotency key — returns the
80
- * original order when this key has been seen before, null when it
81
- * hasn't. Degrades to "always null" with a startup warn when the
82
- * `order_idempotency` dedup table isn't migrated yet.
83
- */
84
- let warnedAboutMissingOrderIdempotencyTable = false
85
- async function findOrderByIdempotencyKey(key: string): Promise<OrderJsonResponse | null> {
86
- try {
87
- const row = await (db as any)
88
- .selectFrom('order_idempotency')
89
- .where('idempotency_key', '=', key)
90
- .selectAll()
91
- .executeTakeFirst()
92
- if (!row) return null
93
- const order = await (db as any)
94
- .selectFrom('orders')
95
- .where('id', '=', row.order_id)
96
- .selectAll()
97
- .executeTakeFirst()
98
- return (order ?? null) as OrderJsonResponse | null
99
- }
100
- catch (err: any) {
101
- if (err?.message?.includes('no such table') || err?.message?.includes("doesn't exist")) {
102
- if (!warnedAboutMissingOrderIdempotencyTable) {
103
- warnedAboutMissingOrderIdempotencyTable = true
104
- // eslint-disable-next-line no-console
105
- console.warn('[commerce/orders] order_idempotency table missing — idempotency keys are accepted but NOT enforced. Run migrations to enable dedup.')
106
- }
107
- return null
108
- }
109
- throw err
110
- }
111
- }
112
-
113
- export async function placeOrder(input: PlaceOrderInput): Promise<PlaceOrderResult> {
114
- // Idempotency-key short-circuit (stacksjs/stacks#1879 Co-3).
115
- // Before doing any work, check the dedup side-table — a retry
116
- // with the same key returns the original order without
117
- // re-running the transaction. Insert-into-dedup happens AFTER
118
- // the order successfully commits, so a partial failure (e.g.,
119
- // the original placeOrder threw mid-transaction) doesn't
120
- // permanently lock the key.
121
- if (input.idempotencyKey) {
122
- const existing = await findOrderByIdempotencyKey(input.idempotencyKey)
123
- if (existing)
124
- return { ok: true, order: existing }
125
- }
126
-
127
- try {
128
- const result = await db.transaction().execute(async (trx: any) => {
129
- const now = formatDate(new Date())
130
-
131
- // 1. Insert the order row.
132
- const orderData = {
133
- ...input.order,
134
- status: input.order.status || 'PENDING',
135
- uuid: randomUUIDv7(),
136
- created_at: now,
137
- updated_at: now,
138
- }
139
- const orderInsert: any = await trx
140
- .insertInto('orders')
141
- .values(orderData as NewOrder)
142
- .executeTakeFirst()
143
-
144
- const orderId = Number(orderInsert?.insertId) || Number(orderInsert?.numInsertedOrUpdatedRows)
145
- if (!orderId)
146
- throw Object.assign(new Error('order insert returned no id'), { __placeFail: true, failedAt: 'order', reason: 'unknown' })
147
-
148
- // 2. Insert the payment row if provided. The transaction_id
149
- // unique constraint catches duplicate Stripe webhook deliveries
150
- // (the pre-fix code's Co-19 backstop) — we surface it as a
151
- // structured result instead of a generic SQL error.
152
- let paymentRow: PaymentJsonResponse | undefined
153
- if (input.payment) {
154
- try {
155
- const paymentInsert: any = await trx
156
- .insertInto('payments')
157
- .values({
158
- ...input.payment,
159
- order_id: orderId,
160
- created_at: now,
161
- updated_at: now,
162
- } as NewPayment)
163
- .executeTakeFirst()
164
- const paymentId = Number(paymentInsert?.insertId) || Number(paymentInsert?.numInsertedOrUpdatedRows)
165
- if (paymentId)
166
- paymentRow = await trx.selectFrom('payments').where('id', '=', paymentId).selectAll().executeTakeFirst() as PaymentJsonResponse | undefined
167
- }
168
- catch (err: any) {
169
- if (err?.message?.includes('UNIQUE constraint') || err?.message?.includes('Duplicate entry'))
170
- throw Object.assign(new Error('duplicate payment transaction_id'), { __placeFail: true, failedAt: 'payment', reason: 'duplicate-payment' })
171
- throw Object.assign(err, { __placeFail: true, failedAt: 'payment', reason: 'unknown' })
172
- }
173
- }
174
-
175
- // 3. Decrement inventory atomically. Same conditional UPDATE
176
- // shape as adjustInventoryMany — `inventory_count + delta >=
177
- // 0` enforces the precondition in the WHERE clause so two
178
- // concurrent placeOrders for the same last-in-stock item don't
179
- // both succeed.
180
- if (input.inventory && input.inventory.length > 0) {
181
- for (let i = 0; i < input.inventory.length; i++) {
182
- const { id, delta } = input.inventory[i]!
183
- if (!Number.isFinite(delta) || delta === 0)
184
- throw Object.assign(new Error('inventory delta must be a non-zero finite number'), { __placeFail: true, failedAt: 'inventory', reason: 'unknown' })
185
-
186
- const adjResult: any = await trx
187
- .updateTable('products')
188
- .set({
189
- inventory_count: sql`inventory_count + ${delta}`,
190
- updated_at: now,
191
- })
192
- .where('id', '=', id)
193
- .where(sql`inventory_count + ${delta}`, '>=', 0)
194
- .execute()
195
-
196
- const affected = Number(
197
- adjResult?.numUpdatedRows
198
- ?? adjResult?.[0]?.numUpdatedRows
199
- ?? adjResult?.numAffectedRows
200
- ?? 0,
201
- )
202
- if (!affected)
203
- throw Object.assign(new Error(`inventory adjust failed for product ${id}`), { __placeFail: true, failedAt: 'inventory', reason: 'out-of-stock' })
204
- }
205
- }
206
-
207
- // 4. Record the idempotency key inside the transaction so a
208
- // concurrent retry of the same key (the classic double-click
209
- // scenario) hits the unique constraint and rolls back here
210
- // instead of creating a second order. Skipped silently when
211
- // the table isn't migrated yet — same opt-in pattern as the
212
- // Stripe webhook dedup.
213
- if (input.idempotencyKey) {
214
- try {
215
- await trx
216
- .insertInto('order_idempotency')
217
- .values({
218
- idempotency_key: input.idempotencyKey,
219
- order_id: orderId,
220
- created_at: now,
221
- })
222
- .execute()
223
- }
224
- catch (err: any) {
225
- // Unique-constraint collision = concurrent retry won the
226
- // race; abort this transaction so the caller falls back
227
- // to the existing order via the pre-transaction lookup
228
- // on the next attempt (or right now if they re-call).
229
- if (err?.message?.includes('UNIQUE constraint') || err?.message?.includes('Duplicate entry'))
230
- throw Object.assign(new Error('idempotency-key collision'), { __placeFail: true, failedAt: 'idempotency', reason: 'duplicate-idempotency-key' })
231
- // Missing table: degrade silently (the pre-transaction
232
- // lookup already warned).
233
- if (!(err?.message?.includes('no such table') || err?.message?.includes("doesn't exist")))
234
- throw err
235
- }
236
- }
237
-
238
- // 5. Refetch the order so callers get the post-write canonical row.
239
- const order = await trx.selectFrom('orders').where('id', '=', orderId).selectAll().executeTakeFirst() as OrderJsonResponse | undefined
240
- if (!order)
241
- throw Object.assign(new Error('order disappeared mid-transaction'), { __placeFail: true, failedAt: 'order', reason: 'unknown' })
242
-
243
- return { order, payment: paymentRow }
244
- })
245
-
246
- // Fire `order:created` AFTER the transaction commits (#1879
247
- // Co-18). If a payment row also landed, also emit `order:paid`
248
- // — the placeOrder path is the typical "checkout completed
249
- // successfully" entry point. Both fire-and-forget; emission
250
- // failures never undo the write.
251
- void emitOrderCreated(result.order as unknown as Record<string, unknown>)
252
- if (result.payment) {
253
- void emitOrderPaid(
254
- result.order as unknown as Record<string, unknown>,
255
- result.payment as unknown as Record<string, unknown>,
256
- )
257
- }
258
-
259
- return { ok: true, order: result.order, payment: result.payment }
260
- }
261
- catch (err: any) {
262
- if (err?.__placeFail) {
263
- return {
264
- ok: false,
265
- reason: err.reason as PlaceOrderResult['reason'] extends infer R ? R : never,
266
- failedAt: err.failedAt,
267
- error: err,
268
- }
269
- }
270
- // Genuinely unexpected error — surface up so the global error
271
- // handler can log it. Transaction already rolled back.
272
- throw err
273
- }
274
- }
@@ -1,51 +0,0 @@
1
- // Import dependencies
2
- import { randomUUIDv7 } from 'bun'
3
- import { db } from '@stacksjs/database'
4
- import { formatDate } from '@stacksjs/orm'
5
- type OrderJsonResponse = ModelRow<typeof Order>
6
- type NewOrder = NewModelData<typeof Order>
7
-
8
- /**
9
- * Create a new order
10
- *
11
- * @param data The order data to store
12
- * @returns The newly created order record
13
- */
14
- export async function store(data: NewOrder): Promise<OrderJsonResponse | undefined> {
15
- const orderData = {
16
- ...data,
17
- status: data.status || 'PENDING',
18
- uuid: randomUUIDv7(),
19
- created_at: formatDate(new Date()),
20
- updated_at: formatDate(new Date()),
21
- }
22
-
23
- try {
24
- // Insert the order record
25
- const createdOrder = await db
26
- .insertInto('orders')
27
- .values(orderData as NewOrder)
28
- .executeTakeFirst()
29
-
30
- const insertId = Number(createdOrder?.insertId) || Number(createdOrder?.numInsertedOrUpdatedRows)
31
-
32
- // If insert was successful, retrieve the newly created order
33
- if (insertId) {
34
- const order = await db
35
- .selectFrom('orders')
36
- .where('id', '=', insertId)
37
- .selectAll()
38
- .executeTakeFirst()
39
-
40
- return order as OrderJsonResponse | undefined
41
- }
42
-
43
- return undefined
44
- }
45
- catch (error) {
46
- if (error instanceof Error)
47
- throw new Error(`Failed to create order: ${error.message}`)
48
-
49
- throw error
50
- }
51
- }
@@ -1,190 +0,0 @@
1
- /**
2
- * Server-side order-total recomputation (stacksjs/stacks#1879 Co-13).
3
- *
4
- * Background: the pre-fix `placeOrder` path trusted whatever
5
- * `total_amount` the client supplied on the order row. A client
6
- * could submit `total_amount: 0.01` and the order would persist
7
- * at that amount (Stripe-side might catch it if the payment
8
- * intent's amount field disagrees, but the LOCAL order record
9
- * was already wrong). Worse: a benign client whose cart total
10
- * drifted (price change between cart view and checkout) would
11
- * silently charge the wrong amount with no signal.
12
- *
13
- * This module ships a `recomputeOrderTotals(input)` helper that
14
- * derives subtotal + tax + shipping + discount from the line
15
- * items + current product prices + current tax rates + current
16
- * shipping rates, returns the canonical totals, and (optionally)
17
- * compares against a client-supplied total to flag drift.
18
- *
19
- * Caller pattern:
20
- * 1. Build the line-items array from the cart (server-side)
21
- * 2. Call `recomputeOrderTotals({ items, ... })` to get canonical numbers
22
- * 3. If `result.diffCents > MAX_DRIFT_CENTS`, reject with "price changed"
23
- * 4. Use `result.total` as the authoritative order.total_amount
24
- *
25
- * Currency: all amounts are integer cents (or your currency's smallest
26
- * unit). Float arithmetic on money is a known footgun (#1879 Co-12);
27
- * cents avoid the rounding-error class entirely.
28
- */
29
-
30
- import { db } from '@stacksjs/database'
31
-
32
- export interface RecomputeLineItem {
33
- /** Product ID — used to look up the current canonical unit price. */
34
- productId: number
35
- /** Quantity ordered. */
36
- quantity: number
37
- /**
38
- * Optional override unit price in cents. When set, used INSTEAD
39
- * of the product table lookup — for grandfathered prices, admin
40
- * discounts, or B2B contract pricing. Document the override in
41
- * the order metadata so the audit trail is traceable.
42
- */
43
- unitPriceCents?: number
44
- }
45
-
46
- export interface RecomputeOrderInput {
47
- /** Server-built line items. Quantities and product IDs only — prices
48
- * default to the products table. */
49
- items: ReadonlyArray<RecomputeLineItem>
50
- /** Optional tax rate ID to apply. Looked up server-side; rate is
51
- * applied to subtotal AFTER discount. */
52
- taxRateId?: number
53
- /** Optional shipping rate cents. Apps that pre-validate the zone
54
- * (#1879 Co-11) pass the resolved cost here. */
55
- shippingCents?: number
56
- /** Optional flat discount in cents (e.g. from coupon). Applied to
57
- * subtotal before tax. */
58
- discountCents?: number
59
- /** Optional client-supplied total in cents for drift detection. */
60
- clientTotalCents?: number
61
- /** Max acceptable drift between client total and canonical total,
62
- * default 5 cents. Drift past this returns `priceChanged: true`. */
63
- maxDriftCents?: number
64
- }
65
-
66
- export interface RecomputeOrderResult {
67
- /** Sum of line-item subtotals (qty × unit price) in cents. */
68
- subtotalCents: number
69
- /** Discount applied (>= 0). */
70
- discountCents: number
71
- /** Tax in cents, computed from (subtotal - discount) × rate. */
72
- taxCents: number
73
- /** Shipping cost in cents. */
74
- shippingCents: number
75
- /** Final total: subtotal - discount + tax + shipping. */
76
- totalCents: number
77
- /** Per-line resolved unit prices, for the audit trail. */
78
- resolvedItems: Array<{ productId: number, quantity: number, unitPriceCents: number, lineCents: number }>
79
- /** Set when `clientTotalCents` was provided and differs from
80
- * canonical by more than `maxDriftCents`. Caller should reject
81
- * the checkout and ask the user to re-confirm. */
82
- priceChanged: boolean
83
- /** Absolute drift in cents (always >= 0). */
84
- diffCents: number
85
- }
86
-
87
- /**
88
- * Fetch the current canonical unit price for each product id.
89
- * Bulk single-query so a 10-item cart isn't a 10-query stampede.
90
- * Returns a Map keyed by product id.
91
- */
92
- async function fetchProductPrices(ids: number[]): Promise<Map<number, number>> {
93
- if (ids.length === 0) return new Map()
94
- const rows = await (db as any)
95
- .selectFrom('products')
96
- .where('id', 'in', ids)
97
- .select(['id', 'price'])
98
- .execute() as Array<{ id: number, price: number | string }>
99
- const out = new Map<number, number>()
100
- for (const r of rows) {
101
- // Some schemas store price as DECIMAL/string — coerce to cents.
102
- // We assume `price` is in cents already (that's the framework
103
- // convention since money math should never touch floats); a
104
- // string-stored numeric coerces cleanly via Number().
105
- const cents = Number(r.price)
106
- if (Number.isFinite(cents))
107
- out.set(r.id, Math.round(cents))
108
- }
109
- return out
110
- }
111
-
112
- /**
113
- * Look up a tax rate by id and return its decimal multiplier
114
- * (e.g. `0.0875` for 8.75% sales tax). Returns 0 when the id is
115
- * missing or the lookup fails — the caller already passed
116
- * `taxRateId` deliberately, so a missing rate is a config bug
117
- * worth surfacing via the canonical total being wrong.
118
- */
119
- async function fetchTaxRate(id: number): Promise<number> {
120
- try {
121
- const row = await (db as any)
122
- .selectFrom('tax_rates')
123
- .where('id', '=', id)
124
- .select(['rate'])
125
- .executeTakeFirst()
126
- const rate = Number(row?.rate ?? 0)
127
- return Number.isFinite(rate) ? rate : 0
128
- }
129
- catch {
130
- return 0
131
- }
132
- }
133
-
134
- export async function recomputeOrderTotals(input: RecomputeOrderInput): Promise<RecomputeOrderResult> {
135
- // Lookups happen up-front so the rest of the function is pure
136
- // arithmetic against in-memory data — easier to reason about
137
- // and easier to mock in tests.
138
- const productIds = input.items.map(i => i.productId)
139
- const priceMap = await fetchProductPrices(productIds)
140
- const taxRate = input.taxRateId != null ? await fetchTaxRate(input.taxRateId) : 0
141
-
142
- let subtotalCents = 0
143
- const resolvedItems: RecomputeOrderResult['resolvedItems'] = []
144
-
145
- for (const item of input.items) {
146
- // Prefer the caller's explicit override (admin discount, B2B
147
- // contract pricing) over the products-table lookup.
148
- const unitCents = item.unitPriceCents ?? priceMap.get(item.productId) ?? 0
149
- const lineCents = Math.round(unitCents * item.quantity)
150
- subtotalCents += lineCents
151
- resolvedItems.push({
152
- productId: item.productId,
153
- quantity: item.quantity,
154
- unitPriceCents: unitCents,
155
- lineCents,
156
- })
157
- }
158
-
159
- const discountCents = Math.max(0, input.discountCents ?? 0)
160
- // Tax applies to (subtotal - discount), clamped to non-negative.
161
- const taxableCents = Math.max(0, subtotalCents - discountCents)
162
- // Round half-up at the cent boundary to match how most payment
163
- // processors round (Stripe documents this).
164
- const taxCents = Math.round(taxableCents * taxRate)
165
- const shippingCents = Math.max(0, input.shippingCents ?? 0)
166
- const totalCents = subtotalCents - discountCents + taxCents + shippingCents
167
-
168
- // Drift detection against client-supplied total. Default tolerance
169
- // is 5 cents — covers most rounding-disagreement cases (one side
170
- // rounded a quarter-cent up, the other rounded it down) without
171
- // hiding genuine price changes.
172
- const maxDrift = input.maxDriftCents ?? 5
173
- let priceChanged = false
174
- let diffCents = 0
175
- if (input.clientTotalCents != null) {
176
- diffCents = Math.abs(totalCents - input.clientTotalCents)
177
- priceChanged = diffCents > maxDrift
178
- }
179
-
180
- return {
181
- subtotalCents,
182
- discountCents,
183
- taxCents,
184
- shippingCents,
185
- totalCents,
186
- resolvedItems,
187
- priceChanged,
188
- diffCents,
189
- }
190
- }