cabloy 5.1.122 → 5.1.124

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 (244) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-frontend-scaffold/SKILL.md +13 -3
  3. package/.claude/skills/cabloy-workflow/SKILL.md +6 -0
  4. package/.claude/skills/cabloy-workflow/evals/evals.json +6 -0
  5. package/CHANGELOG.md +24 -0
  6. package/CLAUDE.md +1 -1
  7. package/cabloy-docs/.vitepress/config.mjs +4 -0
  8. package/cabloy-docs/frontend/form-layout-guide.md +29 -7
  9. package/cabloy-docs/fullstack/parallel-worktree-environment.md +87 -0
  10. package/e2e/scripts/e2e.ts +13 -1
  11. package/e2e/scripts/startE2eVona.ts +4 -1
  12. package/e2e/specs/a-commerce/commerce.spec.ts +129 -32
  13. package/package.json +2 -1
  14. package/playwright-report/data/24ac790ec112339c76cfb0c8e2c2539c2f953a0b.zip +0 -0
  15. package/playwright-report/data/2fddcabcdbd0b695061a565acfff243cb3a4f5e3.zip +0 -0
  16. package/playwright-report/data/4738038d9e861d63fd25937c9a3570934786a62f.zip +0 -0
  17. package/playwright-report/data/72f99c7da67c7f6ea53cab7d8681798b70d79a10.md +222 -0
  18. package/playwright-report/data/7b9684a7c793e9798b898f8ff50177b660a7a733.md +222 -0
  19. package/playwright-report/data/8b31595f5f1cc9d23863af51a592e6b90dcbc4ce.zip +0 -0
  20. package/playwright-report/data/95b578407a68806202dda917a61c50c750bd285b.md +222 -0
  21. package/playwright-report/data/ad960b3a58f224f5b4cad04a24067600064ffb7b.md +222 -0
  22. package/playwright-report/index.html +90 -0
  23. package/playwright-report/trace/assets/codeMirrorModule-LEHpjmcn.js +32 -0
  24. package/playwright-report/trace/assets/defaultSettingsView-BNmKHKpQ.js +264 -0
  25. package/playwright-report/trace/assets/urlMatch-BYQrIQwR.js +1 -0
  26. package/playwright-report/trace/codeMirrorModule.DYBRYzYX.css +1 -0
  27. package/playwright-report/trace/codicon.DCmgc-ay.ttf +0 -0
  28. package/playwright-report/trace/defaultSettingsView.CjdS-WJx.css +1 -0
  29. package/playwright-report/trace/index.CzXZzn5A.css +1 -0
  30. package/playwright-report/trace/index.DMMX1gXU.js +2 -0
  31. package/playwright-report/trace/index.html +44 -0
  32. package/playwright-report/trace/manifest.webmanifest +16 -0
  33. package/playwright-report/trace/playwright-logo.svg +9 -0
  34. package/playwright-report/trace/snapshot.html +10 -0
  35. package/playwright-report/trace/snapshot.v8KI4P3m.js +2 -0
  36. package/playwright-report/trace/sw.bundle.js +5 -0
  37. package/playwright-report/trace/uiMode.BZQ54Kgt.css +1 -0
  38. package/playwright-report/trace/uiMode.Ut8wwJNp.js +6 -0
  39. package/playwright-report/trace/uiMode.html +18 -0
  40. package/playwright-report/trace/xtermModule.DYP7pi_n.css +32 -0
  41. package/vona/env/.env +12 -0
  42. package/vona/env/.env.test +7 -0
  43. package/vona/packages-cli/cli/package.json +1 -1
  44. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}Create.tsx_ +19 -1
  45. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}Update.tsx_ +19 -1
  46. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}View.tsx_ +19 -1
  47. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/3-en-us.ts +6 -1
  48. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/4-zh-cn.ts +6 -1
  49. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}Create.tsx_ +19 -1
  50. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}Update.tsx_ +19 -1
  51. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}View.tsx_ +19 -1
  52. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/snippets/3-en-us.ts +6 -1
  53. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/snippets/4-zh-cn.ts +6 -1
  54. package/vona/packages-cli/cli-set-api/package.json +1 -1
  55. package/vona/pnpm-lock.yaml +255 -12
  56. package/vona/pnpm-workspace.yaml +1 -1
  57. package/vona/src/suite/a-commerce/modules/commerce-payment/package.json +4 -1
  58. package/vona/src/suite/a-commerce/modules/commerce-payment/src/.metadata/index.ts +45 -3
  59. package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/meta.index.ts +1 -0
  60. package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/meta.version.ts +25 -1
  61. package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/payScene.commerceOrder.ts +20 -0
  62. package/vona/src/suite/a-commerce/modules/commerce-payment/src/entity/paymentAttempt.tsx +9 -0
  63. package/vona/src/suite/a-commerce/modules/commerce-payment/src/entity/paymentAudit.tsx +5 -2
  64. package/vona/src/suite/a-commerce/modules/commerce-payment/src/entity/refundAttempt.tsx +6 -0
  65. package/vona/src/suite/a-commerce/modules/commerce-payment/src/index.ts +2 -0
  66. package/vona/src/suite/a-commerce/modules/commerce-payment/src/lib/index.ts +1 -0
  67. package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/commercePayScene.ts +54 -0
  68. package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/paymentAttempt.ts +7 -1
  69. package/vona/src/suite/a-commerce/modules/commerce-payment/src/types/index.ts +1 -0
  70. package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +21 -0
  71. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/.metadata/index.ts +30 -0
  72. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/en-us.ts +4 -0
  73. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/zh-cn.ts +4 -0
  74. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateCreate.tsx +88 -1
  75. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateUpdate.tsx +44 -1
  76. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateView.tsx +72 -1
  77. package/vona/src/suite/a-commerce/modules/commerce-promotion/test/couponTemplateFormLayout.test.ts +121 -0
  78. package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/bean/ssrMenu.home.ts +4 -1
  79. package/vona/src/suite/a-commerce/modules/commerce-trade/package.json +4 -1
  80. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +0 -21
  81. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/checkoutResult.tsx +3 -0
  82. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +79 -0
  83. package/vona/src/suite/a-commerce/modules/commerce-trade/test/orderSnapshot.test.ts +227 -0
  84. package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +121 -13
  85. package/vona/src/suite/a-commerce/modules/commerce-trade/test/refundLifecycle.test.ts +312 -0
  86. package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +76 -0
  87. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/index.ts +842 -0
  88. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/this.ts +2 -0
  89. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/bean.payProvider.ts +92 -0
  90. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/bean.payScene.ts +105 -0
  91. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.index.ts +28 -0
  92. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.redlock.ts +11 -0
  93. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.version.ts +113 -0
  94. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/queue.outboxDispatch.ts +31 -0
  95. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/schedule.outboxDispatch.ts +11 -0
  96. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/controller/paymentSession.ts +55 -0
  97. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/controller/webhook.ts +47 -0
  98. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/dto/paymentSessionStart.tsx +8 -0
  99. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/dto/paymentSessionView.tsx +69 -0
  100. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/dto/webhookReceipt.tsx +12 -0
  101. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/outboxEvent.tsx +46 -0
  102. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/paymentAudit.tsx +36 -0
  103. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/paymentSession.tsx +72 -0
  104. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/providerOperation.tsx +54 -0
  105. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/refundOperation.tsx +37 -0
  106. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/webhookInbox.tsx +59 -0
  107. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/index.ts +3 -0
  108. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/lib/index.ts +2 -0
  109. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/lib/payProvider.ts +7 -0
  110. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/lib/payScene.ts +7 -0
  111. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/model/outboxEvent.ts +10 -0
  112. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/model/paymentAudit.ts +10 -0
  113. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/model/paymentSession.ts +10 -0
  114. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/model/providerOperation.ts +10 -0
  115. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/model/refundOperation.ts +10 -0
  116. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/model/webhookInbox.ts +10 -0
  117. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/outbox.ts +156 -0
  118. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/paymentSession.ts +278 -0
  119. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/providerOperation.ts +5 -0
  120. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/webhook.ts +127 -0
  121. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/index.ts +3 -0
  122. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payProvider.ts +74 -0
  123. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payScene.ts +63 -0
  124. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payment.ts +151 -0
  125. package/vona/src/suite-vendor/a-pay/modules/a-pay/test/outbox.test.ts +209 -0
  126. package/vona/src/suite-vendor/a-pay/modules/a-pay/test/paymentSession.test.ts +153 -0
  127. package/vona/src/suite-vendor/a-pay/modules/a-pay/test/webhook.test.ts +278 -0
  128. package/vona/src/suite-vendor/a-pay/modules/a-pay/tsconfig.build.json +11 -0
  129. package/vona/src/suite-vendor/a-pay/modules/a-pay/tsconfig.json +7 -0
  130. package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +55 -0
  131. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/.metadata/index.ts +182 -0
  132. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/.metadata/this.ts +2 -0
  133. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/bean/payProvider.mock.ts +149 -0
  134. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/controller/mockPayment.ts +25 -0
  135. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/dto/mockPaymentComplete.tsx +13 -0
  136. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/dto/mockPaymentReceipt.tsx +16 -0
  137. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/index.ts +3 -0
  138. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/lib/index.ts +1 -0
  139. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/service/payMock.ts +78 -0
  140. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/types/index.ts +10 -0
  141. package/vona/src/suite-vendor/a-pay/modules/pay-mock/test/payMock.test.ts +141 -0
  142. package/vona/src/suite-vendor/a-pay/modules/pay-mock/tsconfig.build.json +11 -0
  143. package/vona/src/suite-vendor/a-pay/modules/pay-mock/tsconfig.json +7 -0
  144. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +55 -0
  145. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/.metadata/index.ts +62 -0
  146. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/.metadata/this.ts +2 -0
  147. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/bean/payProvider.paypal.ts +91 -0
  148. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/index.ts +3 -0
  149. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/lib/index.ts +1 -0
  150. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/types/index.ts +9 -0
  151. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/tsconfig.build.json +11 -0
  152. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/tsconfig.json +7 -0
  153. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +55 -0
  154. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/.metadata/index.ts +62 -0
  155. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/.metadata/this.ts +2 -0
  156. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/bean/payProvider.stripe.ts +88 -0
  157. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/index.ts +2 -0
  158. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/types/index.ts +8 -0
  159. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/tsconfig.build.json +11 -0
  160. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/tsconfig.json +7 -0
  161. package/vona/src/suite-vendor/a-pay/package.json +15 -0
  162. package/vona/src/suite-vendor/a-pay/tsconfig.base.json +4 -0
  163. package/vona/src/suite-vendor/a-pay/tsconfig.json +19 -0
  164. package/zova/env/.env +1 -0
  165. package/zova/openapi.config.ts +5 -1
  166. package/zova/package.original.json +1 -1
  167. package/zova/packages-cli/cli/package.json +2 -2
  168. package/zova/packages-cli/cli-set-front/cli/templates/openapi/config/boilerplate/project/openapi.config.ts +5 -1
  169. package/zova/packages-cli/cli-set-front/package.json +1 -1
  170. package/zova/packages-cli/cli-set-front/src/lib/bean/cli.openapi.generate.ts +8 -0
  171. package/zova/packages-cli/cli-set-front/src/lib/common/cliCreatePage.ts +7 -24
  172. package/zova/packages-cli/cli-set-front/src/lib/common/utils.ts +10 -1
  173. package/zova/packages-utils/zova-jsx/package.json +2 -2
  174. package/zova/packages-zova/zova/package.json +3 -3
  175. package/zova/packages-zova/zova-core/package.json +1 -1
  176. package/zova/packages-zova/zova-core/src/types/interface/component.ts +5 -0
  177. package/zova/pnpm-lock.yaml +96 -26
  178. package/zova/src/front/config/config/config.cabloyCommerce.ts +1 -1
  179. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/schemas.ts +110 -12
  180. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/types.ts +1292 -160
  181. package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +0 -1
  182. package/zova/src/suite/a-commerce/modules/commerce-trade/package.json +4 -1
  183. package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +1 -47
  184. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +23 -8
  185. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +249 -62
  186. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/checkout/controller.tsx +5 -1
  187. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +152 -39
  188. package/zova/src/suite/a-commerce/modules/commerce-trade/src/routes.ts +1 -1
  189. package/zova/src/suite-vendor/a-pay/modules/a-pay/cli/openapi.config.ts +9 -0
  190. package/zova/src/suite-vendor/a-pay/modules/a-pay/package.json +53 -0
  191. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/component/paymentNextAction.ts +31 -0
  192. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/index.ts +217 -0
  193. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/this.ts +2 -0
  194. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/api/openapi/baseURL.ts +5 -0
  195. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/api/openapi/index.ts +3 -0
  196. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/api/openapi/schemas.ts +584 -0
  197. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/api/openapi/types.ts +9324 -0
  198. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/api/paymentSession.ts +52 -0
  199. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/apiSchema/paymentSession.ts +20 -0
  200. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/component/paymentNextAction/controller.tsx +76 -0
  201. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/index.ts +3 -0
  202. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/lib/index.ts +1 -0
  203. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/model/paymentSession.ts +32 -0
  204. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/service/paymentCoordinator.ts +13 -0
  205. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/types/index.ts +1 -0
  206. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/types/payment.ts +25 -0
  207. package/zova/src/suite-vendor/a-pay/modules/a-pay/tsconfig.build.json +13 -0
  208. package/zova/src/suite-vendor/a-pay/modules/a-pay/tsconfig.json +5 -0
  209. package/zova/src/suite-vendor/a-pay/modules/pay-mock/cli/openapi.config.ts +9 -0
  210. package/zova/src/suite-vendor/a-pay/modules/pay-mock/package.json +53 -0
  211. package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/.metadata/index.ts +140 -0
  212. package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/.metadata/this.ts +2 -0
  213. package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/api/openapi/baseURL.ts +5 -0
  214. package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/api/openapi/index.ts +3 -0
  215. package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/api/openapi/schemas.ts +584 -0
  216. package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/api/openapi/types.ts +9324 -0
  217. package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/api/payMockPayment.ts +34 -0
  218. package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/apiSchema/payMockPayment.ts +13 -0
  219. package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/index.ts +1 -0
  220. package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/model/payMockPayment.ts +20 -0
  221. package/zova/src/suite-vendor/a-pay/modules/pay-mock/tsconfig.build.json +13 -0
  222. package/zova/src/suite-vendor/a-pay/modules/pay-mock/tsconfig.json +5 -0
  223. package/zova/src/suite-vendor/a-pay/modules/pay-paypal/package.json +52 -0
  224. package/zova/src/suite-vendor/a-pay/modules/pay-paypal/src/.metadata/index.ts +26 -0
  225. package/zova/src/suite-vendor/a-pay/modules/pay-paypal/src/.metadata/this.ts +2 -0
  226. package/zova/src/suite-vendor/a-pay/modules/pay-paypal/src/index.ts +1 -0
  227. package/zova/src/suite-vendor/a-pay/modules/pay-paypal/tsconfig.build.json +13 -0
  228. package/zova/src/suite-vendor/a-pay/modules/pay-paypal/tsconfig.json +5 -0
  229. package/zova/src/suite-vendor/a-pay/modules/pay-stripe/package.json +52 -0
  230. package/zova/src/suite-vendor/a-pay/modules/pay-stripe/src/.metadata/index.ts +26 -0
  231. package/zova/src/suite-vendor/a-pay/modules/pay-stripe/src/.metadata/this.ts +2 -0
  232. package/zova/src/suite-vendor/a-pay/modules/pay-stripe/src/index.ts +1 -0
  233. package/zova/src/suite-vendor/a-pay/modules/pay-stripe/tsconfig.build.json +13 -0
  234. package/zova/src/suite-vendor/a-pay/modules/pay-stripe/tsconfig.json +5 -0
  235. package/zova/src/suite-vendor/a-pay/package.json +15 -0
  236. package/zova/src/suite-vendor/a-pay/tsconfig.base.json +4 -0
  237. package/zova/src/suite-vendor/a-pay/tsconfig.json +4 -0
  238. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  239. package/zova/src/suite-vendor/a-zova/package.json +2 -2
  240. package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/payment.ts +0 -23
  241. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradePayment.ts +0 -36
  242. package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradePayment.ts +0 -13
  243. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/payment.ts +0 -25
  244. /package/vona/patches/{zova-core@5.1.79.patch → zova-core@5.1.81.patch} +0 -0
@@ -1153,13 +1153,11 @@ declare module 'vona-module-commerce-trade' {
1153
1153
  export * from '../controller/cart.ts';
1154
1154
  export * from '../controller/checkout.ts';
1155
1155
  export * from '../controller/order.ts';
1156
- export * from '../controller/payment.ts';
1157
1156
  export * from '../controller/stockAudit.ts';
1158
1157
  export * from '../controller/stockBalance.ts';
1159
1158
  import type { IControllerOptionsCart } from '../controller/cart.ts';
1160
1159
  import type { IControllerOptionsCheckout } from '../controller/checkout.ts';
1161
1160
  import type { IControllerOptionsOrder } from '../controller/order.ts';
1162
- import type { IControllerOptionsPayment } from '../controller/payment.ts';
1163
1161
  import type { IControllerOptionsStockAudit } from '../controller/stockAudit.ts';
1164
1162
  import type { IControllerOptionsStockBalance } from '../controller/stockBalance.ts';
1165
1163
  import 'vona-module-a-web';
@@ -1169,7 +1167,6 @@ declare module 'vona-module-a-web' {
1169
1167
  'commerce-trade:cart': IControllerOptionsCart;
1170
1168
  'commerce-trade:checkout': IControllerOptionsCheckout;
1171
1169
  'commerce-trade:order': IControllerOptionsOrder;
1172
- 'commerce-trade:payment': IControllerOptionsPayment;
1173
1170
  'commerce-trade:stockAudit': IControllerOptionsStockAudit;
1174
1171
  'commerce-trade:stockBalance': IControllerOptionsStockBalance;
1175
1172
  }
@@ -1211,17 +1208,6 @@ declare module 'vona-module-commerce-trade' {
1211
1208
  get $onionOptions(): IControllerOptionsOrder;
1212
1209
  }
1213
1210
 
1214
- export interface ControllerPayment {
1215
- /** @internal */
1216
- get scope(): ScopeModuleCommerceTrade;
1217
- }
1218
-
1219
- export interface ControllerPayment {
1220
- get $beanFullName(): 'commerce-trade.controller.payment';
1221
- get $onionName(): 'commerce-trade:payment';
1222
- get $onionOptions(): IControllerOptionsPayment;
1223
- }
1224
-
1225
1211
  export interface ControllerStockAudit {
1226
1212
  /** @internal */
1227
1213
  get scope(): ScopeModuleCommerceTrade;
@@ -1253,8 +1239,6 @@ import type { ControllerCheckout } from '../controller/checkout.ts';
1253
1239
  // @ts-ignore ignore
1254
1240
  import type { ControllerOrder } from '../controller/order.ts';
1255
1241
  // @ts-ignore ignore
1256
- import type { ControllerPayment } from '../controller/payment.ts';
1257
- // @ts-ignore ignore
1258
1242
  import type { ControllerStockAudit } from '../controller/stockAudit.ts';
1259
1243
  // @ts-ignore ignore
1260
1244
  import type { ControllerStockBalance } from '../controller/stockBalance.ts';
@@ -1272,10 +1256,6 @@ declare module 'vona-module-commerce-trade' {
1272
1256
  actions?: TypeControllerOptionsActions<ControllerOrder>;
1273
1257
  }
1274
1258
 
1275
- export interface IControllerOptionsPayment {
1276
- actions?: TypeControllerOptionsActions<ControllerPayment>;
1277
- }
1278
-
1279
1259
  export interface IControllerOptionsStockAudit {
1280
1260
  actions?: TypeControllerOptionsActions<ControllerStockAudit>;
1281
1261
  }
@@ -1304,7 +1284,6 @@ export interface IApiPathPostRecord{
1304
1284
  '/commerce/trade/order/:id/rejectRefund': undefined;
1305
1285
  '/commerce/trade/order/:id/refundOutcome': undefined;
1306
1286
  '/commerce/trade/order/:id/ship': undefined;
1307
- '/commerce/trade/payment/:attemptId/outcome': undefined;
1308
1287
  '/commerce/trade/stockBalance/adjustStock': undefined;
1309
1288
  }
1310
1289
  export interface IApiPathPatchRecord{
@@ -15,6 +15,9 @@ export class DtoCheckoutResult {
15
15
  @Api.field(v.tableIdentity())
16
16
  paymentAttemptId: TableIdentity;
17
17
 
18
+ @Api.field(v.tableIdentity())
19
+ paymentSessionId: TableIdentity;
20
+
18
21
  @Api.field(z.enum(['awaiting_payment', 'paid', 'cancelled', 'expired']))
19
22
  state: 'awaiting_payment' | 'paid' | 'cancelled' | 'expired';
20
23
 
@@ -1,5 +1,6 @@
1
1
  import type { TableIdentity } from 'table-identity';
2
2
  import type { IQueryParams } from 'vona-module-a-orm';
3
+ import type { IPaymentOutcomeEvent } from 'vona-module-a-pay';
3
4
  import type { EntityPaymentAttempt } from 'vona-module-commerce-payment';
4
5
  import type {
5
6
  EntityRefundAttempt,
@@ -106,6 +107,7 @@ export type ShipmentFailureStage =
106
107
  type ShipmentStageCallback = (stage: ShipmentFailureStage) => void | Promise<void>;
107
108
 
108
109
  export type RefundFailureStage =
110
+ | 'beforeRefundOutcomeOrderLock'
109
111
  | 'afterRefundRequestInsert'
110
112
  | 'afterRefundRequestOrderState'
111
113
  | 'afterRefundRequestAudit'
@@ -336,6 +338,7 @@ export class ServiceOrder extends BeanBase {
336
338
  return {
337
339
  orderId: order.id,
338
340
  paymentAttemptId: paymentAttempt.id,
341
+ paymentSessionId: paymentAttempt.paymentSessionId!,
339
342
  state: 'awaiting_payment',
340
343
  paymentAttemptState: 'created',
341
344
  currency: order.currency,
@@ -344,6 +347,80 @@ export class ServiceOrder extends BeanBase {
344
347
  };
345
348
  }
346
349
 
350
+ @Core.transaction({ isolationLevel: 'SERIALIZABLE' })
351
+ @Core.retryable(serializationRetryOptions)
352
+ async settlePaymentFromProvider(event: IPaymentOutcomeEvent): Promise<DtoPaymentOutcomeResult> {
353
+ const attempt = await this.$scope.commercePayment.model.paymentAttempt.get({
354
+ paymentSessionId: event.paymentSessionId,
355
+ });
356
+ if (!attempt || String(attempt.id) !== event.businessReference) {
357
+ this.app.throw(404, 'commerce payment attempt not found');
358
+ }
359
+ const order = await this.scope.model.order.getByIdForUpdate(attempt.orderId);
360
+ if (!order) this.app.throw(404, 'order not found');
361
+ const lockedAttempt = await this.$scope.commercePayment.model.paymentAttempt.getByIdForUpdate(
362
+ attempt.id,
363
+ );
364
+ if (!lockedAttempt) this.app.throw(404, 'commerce payment attempt not found');
365
+ if (
366
+ lockedAttempt.amountCents !== event.amountMinor ||
367
+ lockedAttempt.currency !== event.currency ||
368
+ String(lockedAttempt.paymentSessionId) !== String(event.paymentSessionId) ||
369
+ lockedAttempt.providerName !== event.providerName
370
+ ) {
371
+ this.app.throw(409, 'provider event conflicts with the commerce payment attempt');
372
+ }
373
+ const existingAudit = await this.$scope.commercePayment.model.paymentAudit.get({
374
+ paymentAttemptId: lockedAttempt.id,
375
+ providerEventId: event.eventId,
376
+ });
377
+ if (existingAudit) return this._paymentOutcomeResult(order, lockedAttempt);
378
+ if (order.state !== 'awaiting_payment' || lockedAttempt.state !== 'created') {
379
+ this.app.throw(409, 'payment attempt is no longer available');
380
+ }
381
+ const correlationId = `${order.correlationId}:provider:${event.eventId}`;
382
+ const orderState = event.state === 'succeeded' ? 'paid' : 'cancelled';
383
+ const reason = `provider payment ${event.state}`;
384
+ await this.scope.model.order.updateById(order.id, { state: orderState });
385
+ const finalizedAttempt = await this.$scope.commercePayment.service.paymentAttempt.finalize(
386
+ order.id,
387
+ event.state,
388
+ );
389
+ if (!finalizedAttempt) this.app.throw(404, 'commerce payment attempt not found');
390
+ await this.$scope.commercePayment.model.paymentAttempt.updateById(finalizedAttempt.id, {
391
+ providerName: event.providerName,
392
+ providerCaptureId: event.providerCaptureId,
393
+ });
394
+ if (event.state === 'succeeded') {
395
+ await this._consumeReservedOrderResources(order, correlationId, reason);
396
+ } else {
397
+ await this._releaseReservedOrderResources(order, correlationId, reason);
398
+ }
399
+ await this.$scope.commercePayment.model.paymentAudit.insert({
400
+ paymentAttemptId: finalizedAttempt.id,
401
+ orderId: order.id,
402
+ userId: order.userId,
403
+ provider: event.providerName,
404
+ providerEventId: event.eventId,
405
+ outcome: event.state,
406
+ fromAttemptState: 'created',
407
+ toOrderState: orderState,
408
+ idempotencyKey: event.eventId.slice(0, 100),
409
+ correlationId,
410
+ reason,
411
+ processedAt: new Date(),
412
+ });
413
+ await this._appendAudit({
414
+ orderId: order.id,
415
+ operation: orderState === 'paid' ? 'paid' : 'cancelled',
416
+ fromState: 'awaiting_payment',
417
+ toState: orderState,
418
+ correlationId,
419
+ reason,
420
+ });
421
+ return this._paymentOutcomeResult({ ...order, state: orderState }, finalizedAttempt);
422
+ }
423
+
347
424
  @Core.transaction({ isolationLevel: 'SERIALIZABLE' })
348
425
  @Core.retryable(serializationRetryOptions)
349
426
  async applyPaymentOutcome(
@@ -585,6 +662,7 @@ export class ServiceOrder extends BeanBase {
585
662
  ): Promise<DtoRefundResult> {
586
663
  const idempotencyKey = command.idempotencyKey.trim();
587
664
  if (!idempotencyKey) this.app.throw(400, 'refund idempotencyKey is required');
665
+ await onStage?.('beforeRefundOutcomeOrderLock');
588
666
  const order = await this.scope.model.order.getByIdForUpdate(orderId);
589
667
  if (!order) this.app.throw(404, 'order not found');
590
668
  const existingAudit = await this.$scope.commercePayment.model.refundAudit.get({
@@ -1132,6 +1210,7 @@ export class ServiceOrder extends BeanBase {
1132
1210
  return {
1133
1211
  orderId: order.id,
1134
1212
  paymentAttemptId: paymentAttempt.id,
1213
+ paymentSessionId: paymentAttempt.paymentSessionId!,
1135
1214
  state: order.state,
1136
1215
  paymentAttemptState: paymentAttempt.state,
1137
1216
  currency: order.currency,
@@ -9,7 +9,9 @@ interface IFixture {
9
9
  productId?: number;
10
10
  skuId?: number;
11
11
  addressId?: number;
12
+ cartId?: number;
12
13
  orderId?: number;
14
+ paymentAttemptId?: number;
13
15
  couponTemplateId?: number;
14
16
  couponGrantId?: number;
15
17
  userId?: number;
@@ -17,10 +19,27 @@ interface IFixture {
17
19
 
18
20
  async function cleanup(fixture: IFixture) {
19
21
  const promotion = app.scope('commerce-promotion');
22
+ const payment = app.scope('commerce-payment');
20
23
  const trade = app.scope('commerce-trade');
21
24
  const catalog = app.scope('commerce-catalog');
22
25
  const member = app.scope('commerce-member');
23
26
  if (fixture.orderId !== undefined) {
27
+ const requests = await payment.model.refundRequest.select({
28
+ where: { orderId: fixture.orderId },
29
+ });
30
+ for (const request of requests) {
31
+ await payment.model.refundAudit.delete({ refundRequestId: request.id });
32
+ await payment.model.refundAttempt.delete({ refundRequestId: request.id });
33
+ }
34
+ await payment.model.refundRequest.delete({ orderId: fixture.orderId });
35
+ await trade.model.shipment.delete({ orderId: fixture.orderId });
36
+ }
37
+ if (fixture.paymentAttemptId !== undefined) {
38
+ await payment.model.paymentAudit.delete({ paymentAttemptId: fixture.paymentAttemptId });
39
+ await payment.model.paymentAttempt.delete({ id: fixture.paymentAttemptId });
40
+ }
41
+ if (fixture.orderId !== undefined) {
42
+ await trade.model.orderAudit.delete({ orderId: fixture.orderId });
24
43
  const lines = await trade.model.orderLine.select({ where: { orderId: fixture.orderId } });
25
44
  for (const line of lines) {
26
45
  await trade.model.stockReservation.delete({ orderLineId: line.id });
@@ -28,6 +47,10 @@ async function cleanup(fixture: IFixture) {
28
47
  await trade.model.orderLine.delete({ orderId: fixture.orderId });
29
48
  await trade.model.order.delete({ id: fixture.orderId });
30
49
  }
50
+ if (fixture.cartId !== undefined) {
51
+ await trade.model.cartItem.delete({ cartId: fixture.cartId });
52
+ await trade.model.cart.delete({ id: fixture.cartId });
53
+ }
31
54
  if (fixture.couponGrantId !== undefined) {
32
55
  await promotion.model.couponAudit.delete({ couponGrantId: fixture.couponGrantId });
33
56
  await promotion.model.couponGrant.delete({ id: fixture.couponGrantId });
@@ -222,4 +245,208 @@ describe('orderSnapshot.test.ts', { concurrency: false }, () => {
222
245
  }
223
246
  });
224
247
  });
248
+
249
+ it('keeps immutable purchase facts in the refunded customer order view', async () => {
250
+ await app.bean.executor.mockCtx(async () => {
251
+ const fixture: IFixture = {};
252
+ const suffix = randomUUID().slice(0, 12);
253
+ const original = {
254
+ addressCity: 'San Francisco',
255
+ addressLine1: '1 Market Street',
256
+ couponName: `refund snapshot coupon ${suffix}`,
257
+ productTitle: `refund snapshot product ${suffix}`,
258
+ skuCode: `refund-snapshot-sku-${suffix}`,
259
+ attributes: [{ name: 'Color', value: 'Black' }],
260
+ };
261
+ try {
262
+ await app.bean.passport.signinMock();
263
+ fixture.categoryId = await app.bean.executor.performAction(
264
+ 'post',
265
+ '/commerce/catalog/category',
266
+ { body: { name: `refund snapshot category ${suffix}`, published: true } },
267
+ );
268
+ fixture.productId = await app.bean.executor.performAction(
269
+ 'post',
270
+ '/commerce/catalog/product',
271
+ {
272
+ body: { categoryId: fixture.categoryId, title: original.productTitle, published: true },
273
+ },
274
+ );
275
+ fixture.skuId = await app.bean.executor.performAction('post', '/commerce/catalog/sku', {
276
+ body: {
277
+ productId: fixture.productId,
278
+ code: original.skuCode,
279
+ priceCents: 1299,
280
+ attributes: original.attributes,
281
+ lifecycle: 'active',
282
+ },
283
+ });
284
+ await app.scope('commerce-trade').service.stockBalance.adjustStock({
285
+ skuId: fixture.skuId,
286
+ delta: 1,
287
+ reason: 'refund snapshot fixture',
288
+ correlationId: `refund-snapshot-stock-${suffix}`,
289
+ });
290
+ await app.bean.passport.signout();
291
+
292
+ const customerName = `refund-snapshot-customer-${suffix}`;
293
+ const customer = await app.bean.user.register({ name: customerName }, true);
294
+ fixture.userId = customer.id as number;
295
+ await app.bean.passport.signinMock(customerName as any);
296
+ const userId = app.bean.passport.currentUser!.id;
297
+ fixture.couponTemplateId = (
298
+ await app.scope('commerce-promotion').model.couponTemplate.insert({
299
+ name: original.couponName,
300
+ state: 'active',
301
+ currency: 'USD',
302
+ discountCents: 500,
303
+ minSpendCents: 1_000,
304
+ validFrom: new Date(Date.now() - 1_000),
305
+ validUntil: new Date(Date.now() + 60_000),
306
+ issuedCount: 0,
307
+ redeemedCount: 0,
308
+ })
309
+ ).id as number;
310
+ fixture.couponGrantId = (
311
+ await app.scope('commerce-promotion').service.coupon.issue({
312
+ templateId: fixture.couponTemplateId,
313
+ userId,
314
+ correlationId: `refund-snapshot-coupon-${suffix}`,
315
+ reason: 'refund snapshot fixture',
316
+ })
317
+ ).id as number;
318
+ fixture.addressId = await app.bean.executor.performAction(
319
+ 'post',
320
+ '/commerce/member/address/createMine',
321
+ {
322
+ body: {
323
+ recipientName: 'Refund Snapshot Recipient',
324
+ phone: '15555550123',
325
+ countryCode: 'US',
326
+ region: 'California',
327
+ city: original.addressCity,
328
+ postalCode: '94105',
329
+ addressLine1: original.addressLine1,
330
+ },
331
+ },
332
+ );
333
+ const cart = await app.scope('commerce-trade').model.cart.insert({ userId });
334
+ fixture.cartId = cart.id as number;
335
+ await app.scope('commerce-trade').model.cartItem.insert({
336
+ cartId: cart.id,
337
+ skuId: fixture.skuId,
338
+ quantity: 1,
339
+ });
340
+ const checkout = await app.scope('commerce-trade').service.order.checkout({
341
+ addressId: fixture.addressId,
342
+ couponGrantId: fixture.couponGrantId,
343
+ correlationId: `refund-snapshot-checkout-${suffix}`,
344
+ });
345
+ fixture.orderId = checkout.orderId as number;
346
+ fixture.paymentAttemptId = checkout.paymentAttemptId as number;
347
+ await app
348
+ .scope('commerce-trade')
349
+ .service.order.applyPaymentOutcome(fixture.paymentAttemptId, {
350
+ outcome: 'succeeded',
351
+ idempotencyKey: `refund-snapshot-payment-${suffix}`,
352
+ });
353
+
354
+ await app.scope('commerce-catalog').model.product.updateById(fixture.productId, {
355
+ title: 'Changed refund product title',
356
+ published: false,
357
+ });
358
+ await app.scope('commerce-catalog').model.sku.updateById(fixture.skuId, {
359
+ code: 'changed-refund-sku-code',
360
+ priceCents: 9999,
361
+ attributes: [{ name: 'Color', value: 'Blue' }],
362
+ lifecycle: 'inactive',
363
+ });
364
+ await app.scope('commerce-member').model.address.updateById(fixture.addressId, {
365
+ city: 'Oakland',
366
+ addressLine1: 'Changed refund address',
367
+ });
368
+ await app
369
+ .scope('commerce-promotion')
370
+ .model.couponTemplate.updateById(fixture.couponTemplateId, {
371
+ name: 'Changed refund coupon',
372
+ discountCents: 999,
373
+ });
374
+
375
+ await app.scope('commerce-trade').service.order.requestRefund(fixture.orderId, {
376
+ reason: 'snapshot refund request',
377
+ idempotencyKey: `refund-snapshot-request-${suffix}`,
378
+ });
379
+ await app.bean.passport.signout();
380
+ await app.bean.passport.signinMock();
381
+ await app.scope('commerce-trade').service.order.approveRefund(fixture.orderId, {
382
+ reason: 'snapshot refund approval',
383
+ idempotencyKey: `refund-snapshot-approval-${suffix}`,
384
+ });
385
+ await app.scope('commerce-trade').service.order.applyRefundOutcome(fixture.orderId, {
386
+ outcome: 'succeeded',
387
+ idempotencyKey: `refund-snapshot-outcome-${suffix}`,
388
+ });
389
+ await app.bean.passport.signout();
390
+ await app.bean.passport.signinMock(customerName as any);
391
+
392
+ const detail = await app.scope('commerce-trade').service.order.viewMine(fixture.orderId);
393
+ assert.equal(detail?.state, 'refunded');
394
+ assert.equal(detail?.shipment, undefined);
395
+ assert.equal(detail?.addressSnapshot.city, original.addressCity);
396
+ assert.equal(detail?.addressSnapshot.addressLine1, original.addressLine1);
397
+ assert.equal(detail?.couponSnapshot?.templateName, original.couponName);
398
+ assert.equal(detail?.couponSnapshot?.fixedDiscountCents, 500);
399
+ assert.equal(detail?.discountCents, 500);
400
+ assert.equal(detail?.payableTotalCents, 799);
401
+ assert.equal(detail?.lines.length, 1);
402
+ assert.deepEqual(
403
+ detail?.lines[0] && {
404
+ skuCodeSnapshot: detail.lines[0].skuCodeSnapshot,
405
+ titleSnapshot: detail.lines[0].titleSnapshot,
406
+ skuAttributesSnapshot: detail.lines[0].skuAttributesSnapshot,
407
+ unitPriceCents: detail.lines[0].unitPriceCents,
408
+ quantity: detail.lines[0].quantity,
409
+ lineTotalCents: detail.lines[0].lineTotalCents,
410
+ },
411
+ {
412
+ skuCodeSnapshot: original.skuCode,
413
+ titleSnapshot: original.productTitle,
414
+ skuAttributesSnapshot: original.attributes,
415
+ unitPriceCents: 1299,
416
+ quantity: 1,
417
+ lineTotalCents: 1299,
418
+ },
419
+ );
420
+ const liveProduct = await app
421
+ .scope('commerce-catalog')
422
+ .model.product.getById(fixture.productId);
423
+ const liveSku = await app.scope('commerce-catalog').model.sku.getById(fixture.skuId);
424
+ const liveAddress = await app
425
+ .scope('commerce-member')
426
+ .model.address.getById(fixture.addressId);
427
+ const liveCoupon = await app
428
+ .scope('commerce-promotion')
429
+ .model.couponTemplate.getById(fixture.couponTemplateId);
430
+ assert.deepEqual(
431
+ [
432
+ liveProduct?.title,
433
+ liveSku?.code,
434
+ liveSku?.priceCents,
435
+ liveAddress?.city,
436
+ liveCoupon?.name,
437
+ ],
438
+ [
439
+ 'Changed refund product title',
440
+ 'changed-refund-sku-code',
441
+ 9999,
442
+ 'Oakland',
443
+ 'Changed refund coupon',
444
+ ],
445
+ );
446
+ } finally {
447
+ await cleanup(fixture);
448
+ await app.bean.passport.signout();
449
+ }
450
+ });
451
+ });
225
452
  });
@@ -1,7 +1,7 @@
1
1
  import assert from 'node:assert';
2
- import { randomUUID } from 'node:crypto';
3
- import { after, before, describe, it } from 'node:test';
4
- import { acquireTestLock, app } from 'vona-mock';
2
+ import { createHmac, randomUUID } from 'node:crypto';
3
+ import { describe, it } from 'node:test';
4
+ import { app } from 'vona-mock';
5
5
 
6
6
  interface IFixture {
7
7
  categoryId?: number;
@@ -17,6 +17,7 @@ interface IFixture {
17
17
  checkoutCorrelationId?: string;
18
18
  orderId?: number;
19
19
  paymentAttemptId?: number;
20
+ paymentSessionId?: number;
20
21
  }
21
22
 
22
23
  async function cleanup(fixture: IFixture) {
@@ -25,10 +26,18 @@ async function cleanup(fixture: IFixture) {
25
26
  const promotion = app.scope('commerce-promotion');
26
27
  const catalog = app.scope('commerce-catalog');
27
28
  const member = app.scope('commerce-member');
29
+ const pay = app.scope('a-pay');
30
+ if (fixture.paymentSessionId !== undefined) {
31
+ await pay.model.outboxEvent.delete({ paymentSessionId: fixture.paymentSessionId });
32
+ await pay.model.paymentAudit.delete({ paymentSessionId: fixture.paymentSessionId });
33
+ await pay.model.webhookInbox.delete({ paymentSessionId: fixture.paymentSessionId });
34
+ }
28
35
  if (fixture.paymentAttemptId !== undefined)
29
36
  await payment.model.paymentAudit.delete({ paymentAttemptId: fixture.paymentAttemptId });
30
37
  if (fixture.paymentAttemptId !== undefined)
31
38
  await payment.model.paymentAttempt.delete({ id: fixture.paymentAttemptId });
39
+ if (fixture.paymentSessionId !== undefined)
40
+ await pay.model.paymentSession.delete({ id: fixture.paymentSessionId });
32
41
  if (fixture.orderId !== undefined)
33
42
  await trade.model.orderAudit.delete({ orderId: fixture.orderId });
34
43
  if (fixture.skuId !== undefined) await trade.model.stockAudit.delete({ skuId: fixture.skuId });
@@ -152,18 +161,81 @@ async function createCheckoutFixture(suffix: string, withCoupon = true): Promise
152
161
  });
153
162
  fixture.orderId = checkout.orderId as number;
154
163
  fixture.paymentAttemptId = checkout.paymentAttemptId as number;
164
+ fixture.paymentSessionId = checkout.paymentSessionId as number;
155
165
  return fixture;
156
166
  }
157
167
 
158
168
  describe('paymentOutcome.test.ts', { concurrency: false, sequential: true }, () => {
159
- let releaseTestLock: (() => void) | undefined;
160
-
161
- before(async () => {
162
- releaseTestLock = await acquireTestLock('a-commerce');
163
- });
164
-
165
- after(() => {
166
- releaseTestLock?.();
169
+ it('delivers a signed webhook through the durable outbox to Commerce exactly once', async () => {
170
+ await app.bean.executor.mockCtx(async () => {
171
+ const fixture: IFixture = {};
172
+ try {
173
+ Object.assign(fixture, await createCheckoutFixture(randomUUID().slice(0, 12)));
174
+ const eventId = `provider-${randomUUID().slice(0, 12)}`;
175
+ const rawBody = JSON.stringify({
176
+ paymentSessionId: String(fixture.paymentSessionId),
177
+ currency: 'USD',
178
+ amountMinor: 799,
179
+ eventType: 'payment.succeeded',
180
+ eventId,
181
+ state: 'succeeded',
182
+ providerPaymentId: `payment-${eventId}`,
183
+ providerCaptureId: `capture-${eventId}`,
184
+ });
185
+ const options = app.bean.payProvider.getOptions('pay-mock:mock', 'default');
186
+ assert.equal(typeof options.secretWebhook, 'string');
187
+ const signature = createHmac('sha256', options.secretWebhook).update(rawBody).digest('hex');
188
+ const webhookPath = app.util.getAbsoluteUrlByApiPath('/pay/webhook/pay-mock:mock/default');
189
+ const webhookUrl = webhookPath.startsWith('http')
190
+ ? webhookPath
191
+ : `http://127.0.0.1:${app.config.server.listen.port}${webhookPath}`;
192
+ const response = await fetch(webhookUrl, {
193
+ method: 'POST',
194
+ headers: {
195
+ 'Content-Type': 'application/json',
196
+ 'x-pay-mock-signature': signature,
197
+ },
198
+ body: rawBody,
199
+ });
200
+ assert.equal(response.status, 200);
201
+ const pay = app.scope('a-pay');
202
+ const outbox = await pay.model.outboxEvent.get({
203
+ paymentSessionId: fixture.paymentSessionId,
204
+ });
205
+ assert.ok(outbox);
206
+ await pay.queue.outboxDispatch.pushAsync({ outboxEventId: outbox.id });
207
+ await pay.queue.outboxDispatch.pushAsync({ outboxEventId: outbox.id });
208
+ const [dispatched, order, attempt, line, balance, grant, paymentAudits] = await Promise.all(
209
+ [
210
+ pay.model.outboxEvent.getById(outbox.id),
211
+ app.scope('commerce-trade').model.order.getById(fixture.orderId!),
212
+ app.scope('commerce-payment').model.paymentAttempt.getById(fixture.paymentAttemptId!),
213
+ app.scope('commerce-trade').model.orderLine.get({ orderId: fixture.orderId }),
214
+ app.scope('commerce-trade').model.stockBalance.getById(fixture.balanceId!),
215
+ app.scope('commerce-promotion').model.couponGrant.getById(fixture.grantId!),
216
+ app.scope('commerce-payment').model.paymentAudit.select({
217
+ where: { paymentAttemptId: fixture.paymentAttemptId },
218
+ }),
219
+ ],
220
+ );
221
+ const reservation = await app.scope('commerce-trade').model.stockReservation.get({
222
+ orderLineId: line?.id,
223
+ });
224
+ assert.deepEqual([dispatched?.state, dispatched?.attemptCount], ['dispatched', 1]);
225
+ assert.equal(dispatched?.claimToken, undefined);
226
+ assert.deepEqual(
227
+ [order?.state, attempt?.state, reservation?.state],
228
+ ['paid', 'succeeded', 'consumed'],
229
+ );
230
+ assert.deepEqual([balance?.onHand, balance?.reserved, balance?.available], [0, 0, 0]);
231
+ assert.equal(grant?.state, 'redeemed');
232
+ assert.equal(paymentAudits.length, 1);
233
+ assert.equal(paymentAudits[0]?.providerEventId, eventId);
234
+ } finally {
235
+ await cleanup(fixture);
236
+ await app.bean.passport.signout();
237
+ }
238
+ });
167
239
  });
168
240
 
169
241
  it('settles a successful payment exactly once with stock and coupon consumption', async () => {
@@ -214,6 +286,43 @@ describe('paymentOutcome.test.ts', { concurrency: false, sequential: true }, ()
214
286
  });
215
287
  });
216
288
 
289
+ it('settles a provider event without customer Passport authority', async () => {
290
+ await app.bean.executor.mockCtx(async () => {
291
+ const fixture: IFixture = {};
292
+ try {
293
+ Object.assign(fixture, await createCheckoutFixture(randomUUID().slice(0, 12)));
294
+ await app.bean.passport.signout();
295
+ const event = {
296
+ eventId: `provider-${randomUUID().slice(0, 12)}`,
297
+ paymentSessionId: fixture.paymentSessionId!,
298
+ businessReference: String(fixture.paymentAttemptId),
299
+ providerName: 'pay-mock:mock',
300
+ state: 'succeeded' as const,
301
+ providerCaptureId: `capture-${randomUUID().slice(0, 12)}`,
302
+ amountMinor: 799,
303
+ currency: 'USD',
304
+ };
305
+ const first = await app
306
+ .scope('commerce-trade')
307
+ .service.order.settlePaymentFromProvider(event);
308
+ const replay = await app
309
+ .scope('commerce-trade')
310
+ .service.order.settlePaymentFromProvider(event);
311
+ assert.deepEqual(replay, first);
312
+ assert.deepEqual([first.orderState, first.paymentAttemptState], ['paid', 'succeeded']);
313
+ const audits = await app.scope('commerce-payment').model.paymentAudit.select({
314
+ where: { paymentAttemptId: fixture.paymentAttemptId },
315
+ });
316
+ assert.equal(audits.length, 1);
317
+ assert.equal(audits[0]?.providerEventId, event.eventId);
318
+ assert.equal(audits[0]?.actorId, undefined);
319
+ } finally {
320
+ await cleanup(fixture);
321
+ await app.bean.passport.signout();
322
+ }
323
+ });
324
+ });
325
+
217
326
  it('accepts one concurrent PostgreSQL success event without duplicate effects', async t => {
218
327
  if (process.env.DATABASE_DEFAULT_CLIENT !== 'pg') {
219
328
  t.skip('requires PostgreSQL row-lock contention');
@@ -438,9 +547,8 @@ describe('paymentOutcome.test.ts', { concurrency: false, sequential: true }, ()
438
547
  innerAccess: false,
439
548
  }),
440
549
  () =>
441
- app.bean.executor.performAction('post', '/commerce/trade/payment/1/outcome', {
550
+ app.bean.executor.performAction('get', '/payment-session/1', {
442
551
  innerAccess: false,
443
- body: { outcome: 'succeeded', idempotencyKey: 'anonymous-payment-1' },
444
552
  }),
445
553
  ];
446
554
  for (const request of requests) {