cabloy 5.1.111 → 5.1.112

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 (369) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-backend-scaffold/SKILL.md +29 -1
  3. package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +12 -0
  4. package/.claude/skills/cabloy-frontend-scaffold/SKILL.md +3 -0
  5. package/.claude/skills/cabloy-frontend-scaffold/references/follow-up-checklist.md +4 -0
  6. package/.claude/skills/cabloy-master-detail/SKILL.md +13 -2
  7. package/.github/workflows/playwright-e2e.yml +45 -0
  8. package/.gitignore +1 -0
  9. package/CHANGELOG.md +17 -0
  10. package/CLAUDE.md +4 -1
  11. package/README.md +8 -8
  12. package/cabloy-docs/backend/crud-workflow.md +10 -0
  13. package/cabloy-docs/backend/foundation.md +29 -4
  14. package/cabloy-docs/backend/introduction.md +1 -1
  15. package/cabloy-docs/backend/model-guide.md +9 -11
  16. package/cabloy-docs/backend/multi-instance-and-instance-resolution.md +18 -0
  17. package/cabloy-docs/backend/service-guide.md +1 -1
  18. package/cabloy-docs/frontend/a-router-guide.md +1 -1
  19. package/cabloy-docs/frontend/introduction.md +1 -1
  20. package/cabloy-docs/frontend/navigation-guards-guide.md +20 -11
  21. package/cabloy-docs/frontend/route-alias-guide.md +24 -0
  22. package/cabloy-docs/frontend/scripts.md +71 -24
  23. package/cabloy-docs/frontend/ssr-init-data.md +25 -0
  24. package/cabloy-docs/frontend/ssr-review-checklist.md +17 -7
  25. package/cabloy-docs/frontend/use-state-data-best-practices.md +4 -0
  26. package/cabloy-docs/frontend/zova-router-under-the-hood.md +4 -1
  27. package/cabloy-docs/fullstack/comparison-with-other-frameworks.md +19 -13
  28. package/cabloy-docs/fullstack/introduction.md +10 -10
  29. package/cabloy-docs/fullstack/quickstart.md +23 -1
  30. package/cabloy-docs/index.md +14 -14
  31. package/cabloy-docs/reference/package-map.md +11 -7
  32. package/cabloy-docs/reference/repo-scripts.md +108 -23
  33. package/e2e/scripts/e2e.ts +1 -1
  34. package/e2e/specs/a-commerce/commerce.spec.ts +124 -1
  35. package/package.json +2 -1
  36. package/scripts/init.ts +6 -12
  37. package/scripts/upgrade.ts +3 -1
  38. package/vona/packages-vona/vona/package.json +1 -1
  39. package/vona/pnpm-lock.yaml +39 -108
  40. package/vona/src/backend/config/config/config.ts +11 -0
  41. package/vona/src/suite/a-commerce/modules/commerce-catalog/package.json +4 -2
  42. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +732 -2
  43. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/locales.ts +18 -0
  44. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/bean/meta.index.ts +14 -0
  45. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/bean/meta.version.ts +52 -0
  46. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/bean/ssrMenu.category.ts +30 -0
  47. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/bean/ssrMenu.product.ts +30 -0
  48. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/bean/ssrMenu.sku.ts +30 -0
  49. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +19 -0
  50. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +19 -0
  51. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/controller/category.ts +65 -0
  52. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/controller/product.ts +86 -0
  53. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/controller/sku.ts +65 -0
  54. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/categoryCreate.tsx +28 -0
  55. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/categorySelectReq.tsx +25 -0
  56. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/categorySelectRes.tsx +11 -0
  57. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/categorySelectResItem.tsx +39 -0
  58. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/categoryUpdate.tsx +28 -0
  59. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/categoryView.tsx +25 -0
  60. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productCreate.tsx +28 -0
  61. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productPublic.tsx +38 -0
  62. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productPublicSelectReq.tsx +23 -0
  63. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productPublicSelectRes.tsx +11 -0
  64. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productPublicSku.tsx +25 -0
  65. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productSelectReq.tsx +25 -0
  66. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productSelectRes.tsx +11 -0
  67. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productSelectResItem.tsx +39 -0
  68. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productUpdate.tsx +28 -0
  69. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productView.tsx +25 -0
  70. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuCreate.tsx +28 -0
  71. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuSelectReq.tsx +25 -0
  72. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuSelectRes.tsx +11 -0
  73. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuSelectResItem.tsx +39 -0
  74. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuUpdate.tsx +28 -0
  75. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuView.tsx +25 -0
  76. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/category.tsx +53 -0
  77. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/product.tsx +48 -0
  78. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/sku.tsx +66 -0
  79. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/index.ts +1 -0
  80. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/model/category.ts +15 -0
  81. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/model/product.ts +46 -0
  82. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/model/sku.ts +18 -0
  83. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/category.ts +47 -0
  84. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/product.ts +162 -0
  85. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/sku.ts +76 -0
  86. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/catalog.test.ts +358 -0
  87. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/category.test.ts +91 -0
  88. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/product.test.ts +154 -0
  89. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/sku.test.ts +190 -0
  90. package/vona/src/suite/a-commerce/modules/commerce-member/package.json +2 -1
  91. package/vona/src/suite/a-commerce/modules/commerce-member/src/.metadata/index.ts +342 -5
  92. package/vona/src/suite/a-commerce/modules/commerce-member/src/.metadata/locales.ts +18 -0
  93. package/vona/src/suite/a-commerce/modules/commerce-member/src/bean/meta.index.ts +12 -0
  94. package/vona/src/suite/a-commerce/modules/commerce-member/src/bean/meta.version.ts +31 -0
  95. package/vona/src/suite/a-commerce/modules/commerce-member/src/config/locale/en-us.ts +12 -0
  96. package/vona/src/suite/a-commerce/modules/commerce-member/src/config/locale/zh-cn.ts +12 -0
  97. package/vona/src/suite/a-commerce/modules/commerce-member/src/controller/address.ts +59 -0
  98. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressCreate.tsx +39 -0
  99. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressSelectReq.tsx +30 -0
  100. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressSelectRes.tsx +11 -0
  101. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressSelectResItem.tsx +53 -0
  102. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressUpdate.tsx +39 -0
  103. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressView.tsx +39 -0
  104. package/vona/src/suite/a-commerce/modules/commerce-member/src/entity/address.tsx +76 -0
  105. package/vona/src/suite/a-commerce/modules/commerce-member/src/index.ts +1 -0
  106. package/vona/src/suite/a-commerce/modules/commerce-member/src/model/address.ts +10 -0
  107. package/vona/src/suite/a-commerce/modules/commerce-member/src/service/address.ts +84 -0
  108. package/vona/src/suite/a-commerce/modules/commerce-member/test/addressOwnership.test.ts +203 -0
  109. package/vona/src/suite/a-commerce/modules/commerce-seed/package.json +52 -0
  110. package/vona/src/suite/a-commerce/modules/commerce-seed/src/.metadata/index.ts +55 -0
  111. package/vona/src/suite/a-commerce/modules/commerce-seed/src/.metadata/this.ts +2 -0
  112. package/vona/src/suite/a-commerce/modules/commerce-seed/src/bean/meta.version.ts +80 -0
  113. package/vona/src/suite/a-commerce/modules/commerce-seed/src/index.ts +1 -0
  114. package/vona/src/suite/a-commerce/modules/commerce-seed/tsconfig.build.json +11 -0
  115. package/vona/src/suite/a-commerce/modules/commerce-seed/tsconfig.json +7 -0
  116. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/package.json +4 -2
  117. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/.metadata/index.ts +192 -2
  118. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/.metadata/locales.ts +18 -0
  119. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/bean/meta.version.ts +32 -0
  120. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/bean/ssrMenu.home.ts +3 -1
  121. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/bean/ssrMenuGroup.catalog.ts +21 -0
  122. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/bean/ssrMenuGroup.inventory.ts +21 -0
  123. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/config/locale/en-us.ts +5 -0
  124. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/config/locale/zh-cn.ts +5 -0
  125. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/controller/operator.ts +21 -0
  126. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/dto/operatorContext.ts +21 -0
  127. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/index.ts +1 -0
  128. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/service/operator.ts +18 -0
  129. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/test/meta.version.test.ts +44 -0
  130. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/test/operatorAccess.test.ts +55 -0
  131. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/test/ssrMenu.test.ts +77 -0
  132. package/vona/src/suite/a-commerce/modules/commerce-trade/package.json +2 -1
  133. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +773 -2
  134. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/locales.ts +18 -0
  135. package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/meta.index.ts +23 -0
  136. package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/meta.version.ts +79 -0
  137. package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/ssrMenu.stockAudit.ts +30 -0
  138. package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/ssrMenu.stockBalance.ts +30 -0
  139. package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +25 -0
  140. package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +25 -0
  141. package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/cart.ts +48 -0
  142. package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/stockAudit.ts +38 -0
  143. package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/stockBalance.ts +49 -0
  144. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/cartAddItem.tsx +19 -0
  145. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/cartItem.tsx +34 -0
  146. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/cartUpdateItem.tsx +15 -0
  147. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/cartView.tsx +18 -0
  148. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockAdjust.tsx +32 -0
  149. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockAuditCreate.tsx +28 -0
  150. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockAuditSelectReq.tsx +28 -0
  151. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockAuditSelectRes.tsx +11 -0
  152. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockAuditSelectResItem.tsx +33 -0
  153. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockAuditUpdate.tsx +28 -0
  154. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockAuditView.tsx +25 -0
  155. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockBalanceCreate.tsx +28 -0
  156. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockBalanceSelectReq.tsx +28 -0
  157. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockBalanceSelectRes.tsx +11 -0
  158. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockBalanceSelectResItem.tsx +33 -0
  159. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockBalanceUpdate.tsx +28 -0
  160. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockBalanceView.tsx +25 -0
  161. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/cart.tsx +24 -0
  162. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/cartItem.tsx +30 -0
  163. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/stockAudit.tsx +86 -0
  164. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/stockBalance.tsx +53 -0
  165. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/stockReservation.tsx +63 -0
  166. package/vona/src/suite/a-commerce/modules/commerce-trade/src/index.ts +1 -0
  167. package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/cart.ts +17 -0
  168. package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/cartItem.ts +10 -0
  169. package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/stockAudit.ts +10 -0
  170. package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/stockBalance.ts +15 -0
  171. package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/stockReservation.ts +10 -0
  172. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/cart.ts +152 -0
  173. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/stockAudit.ts +20 -0
  174. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/stockBalance.ts +305 -0
  175. package/vona/src/suite/a-commerce/modules/commerce-trade/test/cartOwnership.test.ts +174 -0
  176. package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockAudit.test.ts +37 -0
  177. package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockBalance.test.ts +186 -0
  178. package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockReservation.test.ts +262 -0
  179. package/vona/src/suite/a-commerce/package.json +1 -0
  180. package/vona/src/suite/a-commerce/tsconfig.json +3 -0
  181. package/vona/src/suite/a-home/modules/home-user/src/.metadata/index.ts +7 -1
  182. package/vona/src/suite/a-home/modules/home-user/test/role.test.ts +14 -23
  183. package/vona/src/suite/a-training/modules/training-record/src/.metadata/index.ts +10 -8
  184. package/vona/src/suite/a-training/modules/training-recordsubject/src/.metadata/index.ts +2 -0
  185. package/vona/src/suite/a-training/modules/training-student/src/.metadata/index.ts +3 -1
  186. package/vona/src/suite-vendor/a-auth/modules/a-auth/package.json +1 -1
  187. package/vona/src/suite-vendor/a-auth/modules/a-auth/src/.metadata/index.ts +7 -3
  188. package/vona/src/suite-vendor/a-auth/modules/auth-simple/package.json +1 -1
  189. package/vona/src/suite-vendor/a-auth/modules/auth-simple/src/.metadata/index.ts +2 -0
  190. package/vona/src/suite-vendor/a-auth/package.json +1 -1
  191. package/vona/src/suite-vendor/a-cabloy/modules/a-datasharding/package.json +1 -1
  192. package/vona/src/suite-vendor/a-cabloy/modules/a-datasharding/src/.metadata/index.ts +1 -1
  193. package/vona/src/suite-vendor/a-cabloy/modules/a-datasource/package.json +1 -1
  194. package/vona/src/suite-vendor/a-cabloy/modules/a-datasource/src/.metadata/index.ts +3 -1
  195. package/vona/src/suite-vendor/a-cabloy/modules/a-socket/package.json +1 -1
  196. package/vona/src/suite-vendor/a-cabloy/modules/a-socket/src/.metadata/index.ts +1 -1
  197. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/package.json +1 -1
  198. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/.metadata/index.ts +47 -47
  199. package/vona/src/suite-vendor/a-cabloy/modules/a-ssrhmr/package.json +1 -1
  200. package/vona/src/suite-vendor/a-cabloy/modules/a-ssrhmr/src/.metadata/index.ts +1 -1
  201. package/vona/src/suite-vendor/a-cabloy/modules/a-status/package.json +1 -1
  202. package/vona/src/suite-vendor/a-cabloy/modules/a-status/src/.metadata/index.ts +2 -0
  203. package/vona/src/suite-vendor/a-cabloy/package.json +1 -1
  204. package/vona/src/suite-vendor/a-captcha/modules/a-captcha/package.json +1 -1
  205. package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/.metadata/index.ts +1 -1
  206. package/vona/src/suite-vendor/a-captcha/package.json +1 -1
  207. package/vona/src/suite-vendor/a-file/modules/a-file/package.json +1 -1
  208. package/vona/src/suite-vendor/a-file/modules/a-file/src/.metadata/index.ts +42 -38
  209. package/vona/src/suite-vendor/a-file/package.json +2 -2
  210. package/vona/src/suite-vendor/a-image/modules/a-image/package.json +1 -1
  211. package/vona/src/suite-vendor/a-image/modules/a-image/src/.metadata/index.ts +42 -38
  212. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/package.json +1 -1
  213. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/.metadata/index.ts +42 -27
  214. package/vona/src/suite-vendor/a-image/package.json +1 -1
  215. package/vona/src/suite-vendor/a-paypal/modules/a-paypal/package.json +1 -1
  216. package/vona/src/suite-vendor/a-paypal/modules/a-paypal/src/.metadata/index.ts +3 -1
  217. package/vona/src/suite-vendor/a-paypal/package.json +1 -1
  218. package/vona/src/suite-vendor/a-vona/modules/a-beanmutate/package.json +1 -1
  219. package/vona/src/suite-vendor/a-vona/modules/a-beanmutate/src/.metadata/index.ts +1 -1
  220. package/vona/src/suite-vendor/a-vona/modules/a-body/package.json +1 -1
  221. package/vona/src/suite-vendor/a-vona/modules/a-body/src/.metadata/index.ts +2 -2
  222. package/vona/src/suite-vendor/a-vona/modules/a-cache/package.json +1 -1
  223. package/vona/src/suite-vendor/a-vona/modules/a-cache/src/.metadata/index.ts +1 -1
  224. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  225. package/vona/src/suite-vendor/a-vona/modules/a-core/src/.metadata/index.ts +1 -1
  226. package/vona/src/suite-vendor/a-vona/modules/a-error/package.json +1 -1
  227. package/vona/src/suite-vendor/a-vona/modules/a-error/src/.metadata/index.ts +1 -1
  228. package/vona/src/suite-vendor/a-vona/modules/a-event/package.json +1 -1
  229. package/vona/src/suite-vendor/a-vona/modules/a-event/src/.metadata/index.ts +1 -1
  230. package/vona/src/suite-vendor/a-vona/modules/a-executor/package.json +1 -1
  231. package/vona/src/suite-vendor/a-vona/modules/a-executor/src/.metadata/index.ts +1 -1
  232. package/vona/src/suite-vendor/a-vona/modules/a-hmr/package.json +1 -1
  233. package/vona/src/suite-vendor/a-vona/modules/a-hmr/src/.metadata/index.ts +1 -1
  234. package/vona/src/suite-vendor/a-vona/modules/a-instance/package.json +1 -1
  235. package/vona/src/suite-vendor/a-vona/modules/a-instance/src/.metadata/index.ts +3 -1
  236. package/vona/src/suite-vendor/a-vona/modules/a-jwt/package.json +1 -1
  237. package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/.metadata/index.ts +1 -1
  238. package/vona/src/suite-vendor/a-vona/modules/a-logger/package.json +1 -1
  239. package/vona/src/suite-vendor/a-vona/modules/a-logger/src/.metadata/index.ts +1 -1
  240. package/vona/src/suite-vendor/a-vona/modules/a-mail/package.json +1 -1
  241. package/vona/src/suite-vendor/a-vona/modules/a-mail/src/.metadata/index.ts +3 -1
  242. package/vona/src/suite-vendor/a-vona/modules/a-mailconfirm/package.json +1 -1
  243. package/vona/src/suite-vendor/a-vona/modules/a-mailconfirm/src/.metadata/index.ts +1 -1
  244. package/vona/src/suite-vendor/a-vona/modules/a-onion/package.json +1 -1
  245. package/vona/src/suite-vendor/a-vona/modules/a-onion/src/.metadata/index.ts +1 -1
  246. package/vona/src/suite-vendor/a-vona/modules/a-openapi/package.json +1 -1
  247. package/vona/src/suite-vendor/a-vona/modules/a-openapi/src/.metadata/index.ts +1 -1
  248. package/vona/src/suite-vendor/a-vona/modules/a-orm/cli/model/metadata/generate.ts +5 -11
  249. package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
  250. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_cache.ts +52 -0
  251. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoGet.ts +4 -10
  252. package/vona/src/suite-vendor/a-vona/modules/a-redis/package.json +1 -1
  253. package/vona/src/suite-vendor/a-vona/modules/a-redis/src/.metadata/index.ts +1 -1
  254. package/vona/src/suite-vendor/a-vona/modules/a-security/package.json +1 -1
  255. package/vona/src/suite-vendor/a-vona/modules/a-security/src/.metadata/index.ts +1 -1
  256. package/vona/src/suite-vendor/a-vona/modules/a-serialization/package.json +1 -1
  257. package/vona/src/suite-vendor/a-vona/modules/a-serialization/src/.metadata/index.ts +1 -1
  258. package/vona/src/suite-vendor/a-vona/modules/a-summer/package.json +1 -1
  259. package/vona/src/suite-vendor/a-vona/modules/a-summer/src/.metadata/index.ts +1 -1
  260. package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
  261. package/vona/src/suite-vendor/a-vona/modules/a-user/src/.metadata/index.ts +4 -5
  262. package/vona/src/suite-vendor/a-vona/modules/a-validation/package.json +1 -1
  263. package/vona/src/suite-vendor/a-vona/modules/a-validation/src/.metadata/index.ts +1 -1
  264. package/vona/src/suite-vendor/a-vona/modules/a-version/package.json +1 -1
  265. package/vona/src/suite-vendor/a-vona/modules/a-version/src/.metadata/index.ts +6 -0
  266. package/vona/src/suite-vendor/a-vona/modules/a-web/package.json +1 -1
  267. package/vona/src/suite-vendor/a-vona/modules/a-web/src/.metadata/index.ts +1 -1
  268. package/vona/src/suite-vendor/a-vona/modules/a-worker/package.json +1 -1
  269. package/vona/src/suite-vendor/a-vona/modules/a-worker/src/.metadata/index.ts +1 -1
  270. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  271. package/zova/packages-cli/cli/package.json +2 -2
  272. package/zova/packages-cli/cli-set-front/package.json +1 -1
  273. package/zova/packages-cli/cli-set-front/src/lib/bean/toolsMetadata/generateScope.ts +1 -1
  274. package/zova/packages-zova/zova/package.json +2 -2
  275. package/zova/pnpm-lock.yaml +9 -9
  276. package/zova/src/front/config/config/config.cabloyCommerce.ts +5 -1
  277. package/zova/src/front/config/config/config.cabloyCommerceAdmin.ts +2 -1
  278. package/zova/src/suite/a-commerce/modules/commerce-catalog/cli/openapi.config.ts +9 -0
  279. package/zova/src/suite/a-commerce/modules/commerce-catalog/package.json +2 -1
  280. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +202 -3
  281. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/locales.ts +7 -0
  282. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/page/catalogue.ts +23 -0
  283. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/page/product.ts +19 -0
  284. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/api/commerceCatalogProduct.ts +52 -0
  285. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/api/openapi/baseURL.ts +5 -0
  286. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/api/openapi/index.ts +3 -0
  287. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/api/openapi/schemas.ts +412 -0
  288. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/api/openapi/types.ts +7159 -0
  289. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/apiSchema/commerceCatalogProduct.ts +20 -0
  290. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +8 -0
  291. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +8 -0
  292. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/index.ts +1 -0
  293. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/model/catalogue.ts +38 -0
  294. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/page/catalogue/controller.tsx +71 -0
  295. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/page/product/controller.tsx +108 -0
  296. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/routes.ts +19 -0
  297. package/zova/src/suite/a-commerce/modules/commerce-member/cli/openapi.config.ts +9 -0
  298. package/zova/src/suite/a-commerce/modules/commerce-member/package.json +2 -1
  299. package/zova/src/suite/a-commerce/modules/commerce-member/src/.metadata/index.ts +182 -3
  300. package/zova/src/suite/a-commerce/modules/commerce-member/src/.metadata/locales.ts +7 -0
  301. package/zova/src/suite/a-commerce/modules/commerce-member/src/.metadata/page/address.ts +19 -0
  302. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/commerceMemberAddress.ts +111 -0
  303. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/baseURL.ts +5 -0
  304. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/index.ts +3 -0
  305. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/schemas.ts +432 -0
  306. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/types.ts +7445 -0
  307. package/zova/src/suite/a-commerce/modules/commerce-member/src/apiSchema/commerceMemberAddress.ts +35 -0
  308. package/zova/src/suite/a-commerce/modules/commerce-member/src/config/locale/en-us.ts +17 -0
  309. package/zova/src/suite/a-commerce/modules/commerce-member/src/config/locale/zh-cn.ts +17 -0
  310. package/zova/src/suite/a-commerce/modules/commerce-member/src/index.ts +1 -0
  311. package/zova/src/suite/a-commerce/modules/commerce-member/src/model/address.ts +38 -0
  312. package/zova/src/suite/a-commerce/modules/commerce-member/src/page/address/controller.tsx +194 -0
  313. package/zova/src/suite/a-commerce/modules/commerce-member/src/routes.ts +12 -0
  314. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/cli/openapi.config.ts +9 -0
  315. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/package.json +2 -1
  316. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/.metadata/index.ts +175 -3
  317. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/.metadata/locales.ts +7 -0
  318. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/.metadata/page/dashboard.ts +9 -0
  319. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/api/commerceSiteadminOperator.ts +25 -0
  320. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/api/openapi/baseURL.ts +5 -0
  321. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/api/openapi/index.ts +3 -0
  322. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/api/openapi/schemas.ts +304 -0
  323. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/api/openapi/types.ts +5710 -0
  324. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/apiSchema/commerceSiteadminOperator.ts +13 -0
  325. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/config/locale/en-us.ts +5 -0
  326. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/config/locale/zh-cn.ts +5 -0
  327. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/index.ts +1 -0
  328. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/model/operator.ts +17 -0
  329. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/page/dashboard/controller.tsx +34 -0
  330. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/routes.ts +5 -0
  331. package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +15 -0
  332. package/zova/src/suite/a-commerce/modules/commerce-trade/package.json +2 -1
  333. package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +182 -3
  334. package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/locales.ts +7 -0
  335. package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/page/cart.ts +19 -0
  336. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeCart.ts +99 -0
  337. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/baseURL.ts +5 -0
  338. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/index.ts +3 -0
  339. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +446 -0
  340. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +7651 -0
  341. package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeCart.ts +35 -0
  342. package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +8 -0
  343. package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +8 -0
  344. package/zova/src/suite/a-commerce/modules/commerce-trade/src/index.ts +1 -0
  345. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/cart.ts +77 -0
  346. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/cart/controller.tsx +108 -0
  347. package/zova/src/suite/a-commerce/modules/commerce-trade/src/routes.ts +12 -0
  348. package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/index.ts +5 -5
  349. package/zova/src/suite/a-demo/modules/demo-todo/src/.metadata/index.ts +2 -2
  350. package/zova/src/suite/a-home/modules/home-base/src/service/routerGuards.ts +18 -10
  351. package/zova/src/suite/a-home/modules/home-indexweb/src/.metadata/index.ts +2 -2
  352. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  353. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/.metadata/index.ts +4 -4
  354. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +1 -0
  355. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  356. package/zova/src/suite-vendor/a-zova/modules/a-bean/cli/controller/metadata/generateMetaPage.ts +3 -1
  357. package/zova/src/suite-vendor/a-zova/modules/a-bean/package.json +1 -1
  358. package/zova/src/suite-vendor/a-zova/modules/a-fetch/package.json +2 -2
  359. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  360. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/model/sdk.ts +3 -1
  361. package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
  362. package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/sys.router.ts +35 -8
  363. package/zova/src/suite-vendor/a-zova/modules/a-router/src/monkeySys.ts +28 -12
  364. package/zova/src/suite-vendor/a-zova/modules/a-router/src/service/routerGuards.ts +6 -8
  365. package/zova/src/suite-vendor/a-zova/modules/a-router/src/types/router.ts +14 -2
  366. package/zova/src/suite-vendor/a-zova/modules/a-router/src/types/utils.ts +4 -1
  367. package/zova/src/suite-vendor/a-zova/package.json +5 -5
  368. package/CLAUDE.local.md +0 -4
  369. /package/e2e/specs/{a-basic → cabloy-basic}/basic.spec.ts +0 -0
@@ -0,0 +1,162 @@
1
+ import type { Knex } from 'knex';
2
+ import type { TableIdentity } from 'table-identity';
3
+ import type { IQueryParams } from 'vona-module-a-orm';
4
+
5
+ import { BeanBase } from 'vona';
6
+ import { Service } from 'vona-module-a-bean';
7
+
8
+ import type { DtoProductCreate } from '../dto/productCreate.tsx';
9
+ import type { DtoProductPublic } from '../dto/productPublic.tsx';
10
+ import type { DtoProductPublicSelectRes } from '../dto/productPublicSelectRes.tsx';
11
+ import type { DtoProductSelectRes } from '../dto/productSelectRes.tsx';
12
+ import type { DtoProductUpdate } from '../dto/productUpdate.tsx';
13
+ import type { DtoProductView } from '../dto/productView.tsx';
14
+ import type { EntityProduct } from '../entity/product.tsx';
15
+ import type { ModelProduct } from '../model/product.ts';
16
+
17
+ @Service()
18
+ export class ServiceProduct extends BeanBase {
19
+ async create(product: DtoProductCreate): Promise<EntityProduct> {
20
+ await this._ensureCategoryExists(product.categoryId);
21
+ return await this.scope.model.product.insert(product);
22
+ }
23
+
24
+ async select(params?: IQueryParams<ModelProduct>): Promise<DtoProductSelectRes> {
25
+ return await this.scope.model.product.selectAndCount(params);
26
+ }
27
+
28
+ async selectPublic(params: IQueryParams<ModelProduct>): Promise<DtoProductPublicSelectRes> {
29
+ const result = await this.scope.model.product.selectAndCount({
30
+ columns: ['id', 'title', 'description', 'categoryId'],
31
+ where: this._getPublicProductWhere(params.where),
32
+ orders: [['id', 'asc']],
33
+ limit: params.limit ?? 20,
34
+ offset: params.offset ?? 0,
35
+ include: {
36
+ category: { columns: ['id', 'name'] },
37
+ skuAvailables: true,
38
+ },
39
+ });
40
+ const balancesBySkuId = await this._getBalancesBySkuId(
41
+ result.list.flatMap((product: any) => product.skuAvailables.map((sku: any) => sku.id)),
42
+ );
43
+ return {
44
+ ...result,
45
+ list: result.list.flatMap((product: any) => {
46
+ const item = this._toPublicProduct(product, balancesBySkuId);
47
+ return item ? [item] : [];
48
+ }),
49
+ };
50
+ }
51
+
52
+ async view(id: TableIdentity): Promise<DtoProductView | undefined> {
53
+ return await this.scope.model.product.getById(id);
54
+ }
55
+
56
+ async viewPublic(id: TableIdentity): Promise<DtoProductPublic | undefined> {
57
+ const product = await this.scope.model.product.get(this._getPublicProductWhere({ id }), {
58
+ columns: ['id', 'title', 'description', 'categoryId'],
59
+ include: {
60
+ category: { columns: ['id', 'name'] },
61
+ skuAvailables: true,
62
+ },
63
+ });
64
+ if (!product) return undefined;
65
+ const balancesBySkuId = await this._getBalancesBySkuId(
66
+ product.skuAvailables.map(sku => sku.id),
67
+ );
68
+ return this._toPublicProduct(product, balancesBySkuId);
69
+ }
70
+
71
+ async update(id: TableIdentity, product: DtoProductUpdate) {
72
+ if (product.categoryId !== undefined) await this._ensureCategoryExists(product.categoryId);
73
+ return await this.scope.model.product.updateById(id, product);
74
+ }
75
+
76
+ async delete(id: TableIdentity) {
77
+ return await this.scope.model.product.deleteById(id);
78
+ }
79
+
80
+ private _getPublicProductWhere(where?: Record<string, unknown>) {
81
+ const productModel = this.scope.model.product;
82
+ const categoryModel = this.scope.model.category;
83
+ const skuModel = this.scope.model.sku;
84
+ const stockBalanceModel = this.$scope.commerceTrade.model.stockBalance;
85
+ return {
86
+ ...where,
87
+ published: true,
88
+ _exists_: function (this: Knex.QueryBuilder) {
89
+ this.select(1)
90
+ .from('commerceCatalogSku')
91
+ .innerJoin('commerceTradeStockBalance', function () {
92
+ this.on('commerceCatalogSku.id', '=', 'commerceTradeStockBalance.skuId').andOn(
93
+ 'commerceCatalogSku.iid',
94
+ '=',
95
+ 'commerceTradeStockBalance.iid',
96
+ );
97
+ });
98
+ skuModel.prepareWhere(this, 'commerceCatalogSku', { lifecycle: 'active' });
99
+ this.where('commerceCatalogSku.productId', productModel.ref('commerceCatalogProduct.id'));
100
+ stockBalanceModel.prepareWhere(this, 'commerceTradeStockBalance', {
101
+ available: { _gt_: 0 },
102
+ });
103
+ this.whereExists(function (this: Knex.QueryBuilder) {
104
+ this.select(1).from('commerceCatalogCategory');
105
+ categoryModel.prepareWhere(this, 'commerceCatalogCategory', { published: true });
106
+ this.where(
107
+ 'commerceCatalogCategory.id',
108
+ productModel.ref('commerceCatalogProduct.categoryId'),
109
+ );
110
+ });
111
+ } as any,
112
+ };
113
+ }
114
+
115
+ private async _getBalancesBySkuId(skuIds: TableIdentity[]) {
116
+ if (skuIds.length === 0) return new Map<string, number>();
117
+ const balances = await this.$scope.commerceTrade.model.stockBalance.select({
118
+ columns: ['skuId', 'available'],
119
+ where: {
120
+ skuId: skuIds,
121
+ available: { _gt_: 0 },
122
+ },
123
+ });
124
+ return new Map(balances.map(balance => [String(balance.skuId), balance.available]));
125
+ }
126
+
127
+ private _toPublicProduct(
128
+ product: any,
129
+ balancesBySkuId: Map<string, number>,
130
+ ): DtoProductPublic | undefined {
131
+ if (!product.category) return undefined;
132
+ const skuAvailables = product.skuAvailables.flatMap((sku: any) => {
133
+ const available = balancesBySkuId.get(String(sku.id));
134
+ if (!available) return [];
135
+ return [
136
+ {
137
+ id: sku.id,
138
+ code: sku.code,
139
+ priceCents: sku.priceCents,
140
+ available,
141
+ },
142
+ ];
143
+ });
144
+ if (skuAvailables.length === 0) return undefined;
145
+ return {
146
+ id: product.id,
147
+ title: product.title,
148
+ description: product.description,
149
+ categoryId: product.category.id,
150
+ categoryName: product.category.name,
151
+ priceCents: Math.min(...skuAvailables.map((sku: any) => sku.priceCents)),
152
+ available: skuAvailables.reduce((total: number, sku: any) => total + sku.available, 0),
153
+ skuAvailables,
154
+ };
155
+ }
156
+
157
+ private async _ensureCategoryExists(categoryId: TableIdentity) {
158
+ if (!(await this.scope.model.category.getById(categoryId))) {
159
+ this.app.throw(404, 'Product category not found');
160
+ }
161
+ }
162
+ }
@@ -0,0 +1,76 @@
1
+ import type { TableIdentity } from 'table-identity';
2
+ import type { IQueryParams } from 'vona-module-a-orm';
3
+
4
+ import { BeanBase } from 'vona';
5
+ import { Service } from 'vona-module-a-bean';
6
+ import { Core } from 'vona-module-a-core';
7
+
8
+ import type { DtoSkuCreate } from '../dto/skuCreate.tsx';
9
+ import type { DtoSkuSelectRes } from '../dto/skuSelectRes.tsx';
10
+ import type { DtoSkuUpdate } from '../dto/skuUpdate.tsx';
11
+ import type { DtoSkuView } from '../dto/skuView.tsx';
12
+ import type { EntitySku } from '../entity/sku.tsx';
13
+ import type { ModelSku } from '../model/sku.ts';
14
+
15
+ const allowedLifecycleTransitions: Record<EntitySku['lifecycle'], EntitySku['lifecycle'][]> = {
16
+ draft: ['active', 'archived'],
17
+ active: ['inactive', 'archived'],
18
+ inactive: ['active', 'archived'],
19
+ archived: [],
20
+ };
21
+
22
+ @Service()
23
+ export class ServiceSku extends BeanBase {
24
+ async create(sku: DtoSkuCreate): Promise<EntitySku> {
25
+ await this._ensureProductExists(sku.productId);
26
+ await this._ensureCodeAvailable(sku.code);
27
+ return await this.scope.model.sku.insert(sku);
28
+ }
29
+
30
+ async select(params?: IQueryParams<ModelSku>): Promise<DtoSkuSelectRes> {
31
+ return await this.scope.model.sku.selectAndCount(params);
32
+ }
33
+
34
+ async view(id: TableIdentity): Promise<DtoSkuView | undefined> {
35
+ return await this.scope.model.sku.getById(id);
36
+ }
37
+
38
+ @Core.transaction({ isolationLevel: 'SERIALIZABLE' })
39
+ async update(id: TableIdentity, sku: DtoSkuUpdate) {
40
+ if (sku.lifecycle !== undefined) {
41
+ const currentSku = await this.scope.model.sku.getByIdForUpdate(id);
42
+ if (!currentSku) {
43
+ this.app.throw(404, 'SKU not found');
44
+ }
45
+ if (
46
+ sku.lifecycle !== currentSku.lifecycle &&
47
+ !allowedLifecycleTransitions[currentSku.lifecycle].includes(sku.lifecycle)
48
+ ) {
49
+ this.app.throw(
50
+ 409,
51
+ `cannot transition SKU lifecycle from ${currentSku.lifecycle} to ${sku.lifecycle}`,
52
+ );
53
+ }
54
+ }
55
+ if (sku.productId !== undefined) await this._ensureProductExists(sku.productId);
56
+ if (sku.code) await this._ensureCodeAvailable(sku.code, id);
57
+ return await this.scope.model.sku.updateById(id, sku);
58
+ }
59
+
60
+ async delete(id: TableIdentity) {
61
+ return await this.scope.model.sku.deleteById(id);
62
+ }
63
+
64
+ private async _ensureProductExists(productId: TableIdentity) {
65
+ if (!(await this.scope.model.product.getById(productId))) {
66
+ this.app.throw(404, 'SKU product not found');
67
+ }
68
+ }
69
+
70
+ private async _ensureCodeAvailable(code: string, excludedId?: TableIdentity) {
71
+ const sku = await this.scope.model.sku.get({ code });
72
+ if (sku && String(sku.id) !== String(excludedId)) {
73
+ this.app.throw(409, `SKU code already exists: ${code}`);
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,358 @@
1
+ import assert from 'node:assert';
2
+ import { describe, it } from 'node:test';
3
+ import { app } from 'vona-mock';
4
+
5
+ async function createCatalog(
6
+ prefix: string,
7
+ lifecycle: 'draft' | 'active' | 'inactive' | 'archived',
8
+ ) {
9
+ await app.bean.passport.signinMock();
10
+ try {
11
+ const categoryId = await app.bean.executor.performAction('post', '/commerce/catalog/category', {
12
+ body: { name: `${prefix} category`, published: true },
13
+ });
14
+ const productId = await app.bean.executor.performAction('post', '/commerce/catalog/product', {
15
+ body: { categoryId, title: `${prefix} product`, published: true },
16
+ });
17
+ const skuId = await app.bean.executor.performAction('post', '/commerce/catalog/sku', {
18
+ body: { code: `${prefix}-sku`, productId, priceCents: 1299, lifecycle },
19
+ });
20
+ return { categoryId, productId, skuId };
21
+ } finally {
22
+ await app.bean.passport.signout();
23
+ }
24
+ }
25
+
26
+ describe('catalog.test.ts', () => {
27
+ it('enforces operator access and category/product relations', async () => {
28
+ await app.bean.executor.mockCtx(async () => {
29
+ await app.bean.passport.signinMock();
30
+ try {
31
+ app.bean.passport.current!.roles = [];
32
+ const permissions = await Promise.all([
33
+ app.bean.permission.retrievePermissionAction('commerce-catalog:category', 'create'),
34
+ app.bean.permission.retrievePermissionAction('commerce-catalog:product', 'create'),
35
+ app.bean.permission.retrievePermissionAction('commerce-catalog:sku', 'create'),
36
+ ]);
37
+ assert.deepEqual(permissions, [false, false, false]);
38
+ } finally {
39
+ await app.bean.passport.signout();
40
+ }
41
+
42
+ await app.bean.passport.signinMock();
43
+ try {
44
+ await assert.rejects(() =>
45
+ app.bean.executor.performAction('post', '/commerce/catalog/product', {
46
+ body: { categoryId: 999999, title: 'Missing category', published: false },
47
+ }),
48
+ );
49
+ } finally {
50
+ await app.bean.passport.signout();
51
+ }
52
+ });
53
+ });
54
+
55
+ it('stores integer cents and exposes only sellable products to customers', async () => {
56
+ await app.bean.executor.mockCtx(async () => {
57
+ const active = await createCatalog('__catalog-active__', 'active');
58
+ const draft = await createCatalog('__catalog-draft__', 'draft');
59
+ const inactive = await createCatalog('__catalog-inactive__', 'inactive');
60
+ const archived = await createCatalog('__catalog-archived__', 'archived');
61
+ const unpublishedProduct = await createCatalog('__catalog-unpublished-product__', 'active');
62
+ const unpublishedCategory = await createCatalog('__catalog-unpublished-category__', 'active');
63
+ const zeroStock = await createCatalog('__catalog-zero-stock__', 'active');
64
+
65
+ for (const catalog of [
66
+ active,
67
+ draft,
68
+ inactive,
69
+ archived,
70
+ unpublishedProduct,
71
+ unpublishedCategory,
72
+ ]) {
73
+ await app.scope('commerce-trade').service.stockBalance.adjustStock({
74
+ skuId: catalog.skuId,
75
+ delta: 2,
76
+ reason: 'catalog public fixture',
77
+ correlationId: `catalog-public-${catalog.skuId}`,
78
+ });
79
+ }
80
+ await app.scope('commerce-catalog').model.product.updateById(unpublishedProduct.productId, {
81
+ published: false,
82
+ });
83
+ await app
84
+ .scope('commerce-catalog')
85
+ .model.category.updateById(unpublishedCategory.categoryId, {
86
+ published: false,
87
+ });
88
+
89
+ const publicProducts = await app.bean.executor.performAction(
90
+ 'get',
91
+ '/commerce/catalog/product/public',
92
+ { innerAccess: false },
93
+ );
94
+ assert.equal(
95
+ publicProducts.list.some((product: any) => String(product.id) === String(active.productId)),
96
+ true,
97
+ );
98
+ for (const catalog of [
99
+ draft,
100
+ inactive,
101
+ archived,
102
+ unpublishedProduct,
103
+ unpublishedCategory,
104
+ zeroStock,
105
+ ]) {
106
+ assert.equal(
107
+ publicProducts.list.some(
108
+ (product: any) => String(product.id) === String(catalog.productId),
109
+ ),
110
+ false,
111
+ );
112
+ }
113
+ const catalogPublicProduct = publicProducts.list.find(
114
+ (product: any) => String(product.id) === String(active.productId),
115
+ );
116
+ assert.deepEqual(Object.keys(catalogPublicProduct).sort(), [
117
+ 'available',
118
+ 'categoryId',
119
+ 'categoryName',
120
+ 'description',
121
+ 'id',
122
+ 'priceCents',
123
+ 'skuAvailables',
124
+ 'title',
125
+ ]);
126
+ assert.equal(catalogPublicProduct.priceCents, 1299);
127
+ assert.equal(catalogPublicProduct.available, 2);
128
+ const publicProduct = await app.bean.executor.performAction(
129
+ 'get',
130
+ '/commerce/catalog/product/public/:id',
131
+ { params: { id: active.productId }, innerAccess: false },
132
+ );
133
+ assert.equal(String(publicProduct.id), String(active.productId));
134
+ for (const catalog of [
135
+ draft,
136
+ inactive,
137
+ archived,
138
+ unpublishedProduct,
139
+ unpublishedCategory,
140
+ zeroStock,
141
+ ]) {
142
+ const publicProduct = await app.bean.executor.performAction(
143
+ 'get',
144
+ '/commerce/catalog/product/public/:id',
145
+ { params: { id: catalog.productId }, innerAccess: false },
146
+ );
147
+ assert.equal(publicProduct, undefined);
148
+ }
149
+ });
150
+ });
151
+
152
+ it('paginates and aggregates public Products without duplicate roots', async () => {
153
+ await app.bean.executor.mockCtx(async () => {
154
+ const first = await createCatalog('__catalog-page-first__', 'active');
155
+ const second = await createCatalog('__catalog-page-second__', 'active');
156
+ const scopeCatalog = app.scope('commerce-catalog');
157
+ const additionalSku = await scopeCatalog.model.sku.insert({
158
+ code: '__catalog-page-first-additional-sku__',
159
+ productId: first.productId,
160
+ priceCents: 999,
161
+ lifecycle: 'active',
162
+ });
163
+ const inactiveSku = await scopeCatalog.model.sku.insert({
164
+ code: '__catalog-page-first-inactive-sku__',
165
+ productId: first.productId,
166
+ priceCents: 100,
167
+ lifecycle: 'inactive',
168
+ });
169
+ for (const [skuId, delta] of [
170
+ [first.skuId, 2],
171
+ [additionalSku.id, 3],
172
+ [inactiveSku.id, 4],
173
+ [second.skuId, 1],
174
+ ] as const) {
175
+ await app.scope('commerce-trade').service.stockBalance.adjustStock({
176
+ skuId,
177
+ delta,
178
+ reason: 'catalog pagination fixture',
179
+ correlationId: `catalog-pagination-${skuId}`,
180
+ });
181
+ }
182
+ const query = { title: '__catalog-page-', pageSize: 1 };
183
+ const pageOne = await app.bean.executor.performAction(
184
+ 'get',
185
+ '/commerce/catalog/product/public',
186
+ {
187
+ query: { ...query, pageNo: 1 },
188
+ innerAccess: false,
189
+ },
190
+ );
191
+ const pageTwo = await app.bean.executor.performAction(
192
+ 'get',
193
+ '/commerce/catalog/product/public',
194
+ {
195
+ query: { ...query, pageNo: 2 },
196
+ innerAccess: false,
197
+ },
198
+ );
199
+ assert.equal(pageOne.total, '2');
200
+ assert.equal(pageOne.pageCount, 2);
201
+ assert.equal(pageOne.pageSize, 1);
202
+ assert.equal(pageOne.pageNo, 1);
203
+ assert.equal(pageTwo.total, '2');
204
+ assert.equal(pageTwo.pageNo, 2);
205
+ assert.notEqual(String(pageOne.list[0].id), String(pageTwo.list[0].id));
206
+ const firstPublicProduct = [pageOne, pageTwo]
207
+ .flatMap(page => page.list)
208
+ .find(product => String(product.id) === String(first.productId));
209
+ assert.equal(firstPublicProduct.priceCents, 999);
210
+ assert.equal(firstPublicProduct.available, 5);
211
+ assert.deepEqual(
212
+ firstPublicProduct.skuAvailables.map(sku => String(sku.id)).sort(),
213
+ [first.skuId, additionalSku.id].map(String).sort(),
214
+ );
215
+ const categoryProducts = await app.bean.executor.performAction(
216
+ 'get',
217
+ '/commerce/catalog/product/public',
218
+ {
219
+ query: { categoryId: first.categoryId },
220
+ innerAccess: false,
221
+ },
222
+ );
223
+ assert.equal(categoryProducts.total, '1');
224
+ assert.equal(String(categoryProducts.list[0].id), String(first.productId));
225
+ });
226
+ });
227
+
228
+ it('refreshes public eligibility after Category, SKU, and stock changes', async () => {
229
+ await app.bean.executor.mockCtx(async () => {
230
+ const catalog = await createCatalog('__catalog-cache__', 'active');
231
+ const query = { title: '__catalog-cache__' };
232
+ const selectPublic = async () => {
233
+ return await app.bean.executor.performAction('get', '/commerce/catalog/product/public', {
234
+ query,
235
+ innerAccess: false,
236
+ });
237
+ };
238
+ await app.scope('commerce-trade').service.stockBalance.adjustStock({
239
+ skuId: catalog.skuId,
240
+ delta: 1,
241
+ reason: 'catalog cache fixture',
242
+ correlationId: `catalog-cache-add-${catalog.skuId}`,
243
+ });
244
+ assert.equal((await selectPublic()).total, '1');
245
+ await app.scope('commerce-trade').service.stockBalance.adjustStock({
246
+ skuId: catalog.skuId,
247
+ delta: -1,
248
+ reason: 'catalog cache fixture',
249
+ correlationId: `catalog-cache-remove-${catalog.skuId}`,
250
+ });
251
+ assert.equal((await selectPublic()).total, '0');
252
+ await app.scope('commerce-trade').service.stockBalance.adjustStock({
253
+ skuId: catalog.skuId,
254
+ delta: 1,
255
+ reason: 'catalog cache fixture',
256
+ correlationId: `catalog-cache-restore-${catalog.skuId}`,
257
+ });
258
+ assert.equal((await selectPublic()).total, '1');
259
+ await app.scope('commerce-catalog').model.sku.updateById(catalog.skuId, {
260
+ lifecycle: 'inactive',
261
+ });
262
+ assert.equal((await selectPublic()).total, '0');
263
+ await app.scope('commerce-catalog').model.sku.updateById(catalog.skuId, {
264
+ lifecycle: 'active',
265
+ });
266
+ assert.equal((await selectPublic()).total, '1');
267
+ await app.scope('commerce-catalog').model.category.updateById(catalog.categoryId, {
268
+ published: false,
269
+ });
270
+ assert.equal((await selectPublic()).total, '0');
271
+ });
272
+ });
273
+
274
+ it('isolates catalog data and SKU uniqueness by tenant', async () => {
275
+ let defaultProductId: unknown;
276
+ await app.bean.executor.mockCtx(async () => {
277
+ const catalog = await createCatalog('__catalog-default__', 'active');
278
+ defaultProductId = catalog.productId;
279
+
280
+ await app.bean.passport.signinMock();
281
+ try {
282
+ await assert.rejects(() =>
283
+ app.bean.executor.performAction('post', '/commerce/catalog/sku', {
284
+ body: {
285
+ code: '__catalog-default__-sku',
286
+ productId: catalog.productId,
287
+ priceCents: 2000,
288
+ lifecycle: 'draft',
289
+ },
290
+ }),
291
+ );
292
+ } finally {
293
+ await app.bean.passport.signout();
294
+ }
295
+ });
296
+
297
+ await app.bean.executor.mockCtx(
298
+ async () => {
299
+ const catalog = await createCatalog('__catalog-share__', 'active');
300
+ await app.bean.passport.signinMock();
301
+ try {
302
+ const duplicateCodeSkuId = await app.bean.executor.performAction(
303
+ 'post',
304
+ '/commerce/catalog/sku',
305
+ {
306
+ body: {
307
+ code: '__catalog-default__-sku',
308
+ productId: catalog.productId,
309
+ priceCents: 2500,
310
+ lifecycle: 'active',
311
+ },
312
+ },
313
+ );
314
+ assert.equal(!!duplicateCodeSkuId, true);
315
+ } finally {
316
+ await app.bean.passport.signout();
317
+ }
318
+
319
+ await app.scope('commerce-trade').service.stockBalance.adjustStock({
320
+ skuId: catalog.skuId,
321
+ delta: 1,
322
+ reason: 'catalog tenant fixture',
323
+ correlationId: `catalog-tenant-${catalog.skuId}`,
324
+ });
325
+ const publicProducts = await app.bean.executor.performAction(
326
+ 'get',
327
+ '/commerce/catalog/product/public',
328
+ {
329
+ innerAccess: false,
330
+ },
331
+ );
332
+ assert.equal(
333
+ publicProducts.list.some(
334
+ (product: any) => String(product.id) === String(defaultProductId),
335
+ ),
336
+ false,
337
+ );
338
+ assert.equal(
339
+ publicProducts.list.some((product: any) => product.title === '__catalog-share__ product'),
340
+ true,
341
+ );
342
+ const foreignProduct = await app.bean.executor.performAction(
343
+ 'get',
344
+ '/commerce/catalog/product/public/:id',
345
+ { params: { id: defaultProductId }, innerAccess: false },
346
+ );
347
+ assert.equal(foreignProduct, undefined);
348
+ const publicProduct = await app.bean.executor.performAction(
349
+ 'get',
350
+ '/commerce/catalog/product/public/:id',
351
+ { params: { id: catalog.productId }, innerAccess: false },
352
+ );
353
+ assert.equal(String(publicProduct.id), String(catalog.productId));
354
+ },
355
+ { instanceName: 'shareTest' as any },
356
+ );
357
+ });
358
+ });
@@ -0,0 +1,91 @@
1
+ import type {
2
+ DtoCategoryCreate,
3
+ DtoCategorySelectRes,
4
+ DtoCategoryUpdate,
5
+ EntityCategory,
6
+ } from 'vona-module-commerce-catalog';
7
+
8
+ import assert from 'node:assert';
9
+ import { describe, it } from 'node:test';
10
+ import { app } from 'vona-mock';
11
+
12
+ describe('category.test.ts', () => {
13
+ it('action:category', async () => {
14
+ await app.bean.executor.mockCtx(async () => {
15
+ // data
16
+ const data: DtoCategoryCreate = {
17
+ name: '__Tom__',
18
+ published: false,
19
+ description: 'This is a test',
20
+ };
21
+ const dataUpdate: DtoCategoryUpdate = {
22
+ name: '__TomNew__',
23
+ published: true,
24
+ description: 'This is a test',
25
+ };
26
+ // role-less authenticated users cannot access generated admin actions
27
+ await app.bean.passport.signinMock();
28
+ try {
29
+ app.bean.passport.current!.roles = [];
30
+ const actions = ['create', 'select', 'view', 'update', 'delete'];
31
+ const permissions = await Promise.all(
32
+ actions.map(action =>
33
+ app.bean.permission.retrievePermissionAction('commerce-catalog:category', action),
34
+ ),
35
+ );
36
+ assert.deepEqual(
37
+ permissions,
38
+ actions.map(() => false),
39
+ );
40
+ } finally {
41
+ await app.bean.passport.signout();
42
+ }
43
+ // login as system admin
44
+ await app.bean.passport.signinMock();
45
+ // create
46
+ const categoryId = await app.bean.executor.performAction(
47
+ 'post',
48
+ '/commerce/catalog/category',
49
+ { body: data },
50
+ );
51
+ assert.equal(!!categoryId, true);
52
+ // findMany
53
+ const selectRes: DtoCategorySelectRes = await app.bean.executor.performAction(
54
+ 'get',
55
+ '/commerce/catalog/category',
56
+ );
57
+ assert.equal(selectRes.list.findIndex(item => item.name === data.name) > -1, true);
58
+ // update
59
+ const updateRes = await app.bean.executor.performAction(
60
+ 'patch',
61
+ '/commerce/catalog/category/:id',
62
+ {
63
+ params: { id: categoryId },
64
+ body: dataUpdate,
65
+ },
66
+ );
67
+ assert.equal(updateRes, null);
68
+ // findOne
69
+ let category: EntityCategory = await app.bean.executor.performAction(
70
+ 'get',
71
+ '/commerce/catalog/category/:id',
72
+ { params: { id: categoryId } },
73
+ );
74
+ assert.equal(category.name, dataUpdate.name);
75
+ // delete
76
+ const deleteRes = await app.bean.executor.performAction(
77
+ 'delete',
78
+ '/commerce/catalog/category/:id',
79
+ { params: { id: category.id } },
80
+ );
81
+ assert.equal(deleteRes, null);
82
+ // findOne
83
+ category = await app.bean.executor.performAction('get', '/commerce/catalog/category/:id', {
84
+ params: { id: category.id },
85
+ });
86
+ assert.equal(category, undefined);
87
+ // logout
88
+ await app.bean.passport.signout();
89
+ });
90
+ });
91
+ });