cabloy 5.1.131 → 5.1.133

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 (210) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-worktree-environment/SKILL.md +6 -6
  3. package/.claude/skills/cabloy-worktree-environment/evals/evals.json +2 -2
  4. package/.github/workflows/vona-test-pg.yml +1 -1
  5. package/.github/workflows/vona-test-sqlite3.yml +1 -1
  6. package/CHANGELOG.md +28 -0
  7. package/cabloy-docs/backend/dto-guide.md +39 -4
  8. package/cabloy-docs/backend/dto-infer-generation.md +29 -0
  9. package/cabloy-docs/backend/openapi-guide.md +16 -0
  10. package/cabloy-docs/backend/validation-guide.md +45 -0
  11. package/cabloy-docs/frontend/environment-config-guide.md +8 -15
  12. package/cabloy-docs/frontend/navigation-guards-guide.md +3 -3
  13. package/cabloy-docs/frontend/page-route-guide.md +4 -4
  14. package/cabloy-docs/frontend/ssr-env.md +22 -29
  15. package/cabloy-docs/frontend/theme-guide.md +4 -4
  16. package/cabloy-docs/frontend/zova-router-under-the-hood.md +1 -1
  17. package/cabloy-docs/fullstack/deploy-cloudflare-docker.md +6 -6
  18. package/cabloy-docs/fullstack/parallel-worktree-environment.md +2 -2
  19. package/e2e/specs/a-commerce/commerce.spec.ts +479 -17
  20. package/e2e/specs/cabloy-basic/basic.spec.ts +30 -5
  21. package/package.json +7 -7
  22. package/vona/.claude/skills/detect-ssr-leak/SKILL.md +1 -1
  23. package/vona/packages-vona/vona/package.json +1 -1
  24. package/vona/pnpm-lock.yaml +120 -10
  25. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuCreate.tsx +3 -1
  26. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuSelectResItem.tsx +13 -1
  27. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuUpdate.tsx +3 -1
  28. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuView.tsx +13 -1
  29. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/sku.tsx +6 -0
  30. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/skuPresentation.test.ts +227 -0
  31. package/vona/src/suite/a-commerce/modules/commerce-member/src/.metadata/index.ts +46 -36
  32. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressSelectResItem.tsx +5 -13
  33. package/vona/src/suite/a-commerce/modules/commerce-member/src/service/address.ts +16 -1
  34. package/vona/src/suite/a-commerce/modules/commerce-member/test/addressPresentation.test.ts +210 -0
  35. package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/commercePayScene.ts +13 -2
  36. package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +12 -0
  37. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/service/coupon.ts +7 -1
  38. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +45 -2
  39. package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +23 -0
  40. package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +23 -0
  41. package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/order.ts +31 -0
  42. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderAdminLineBase.tsx +56 -0
  43. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderAdminLineResItem.tsx +17 -0
  44. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderAdminLineView.tsx +13 -0
  45. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderSelectResItem.tsx +26 -2
  46. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderView.tsx +197 -3
  47. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/refundRecoveryAction.tsx +19 -0
  48. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/refundRecoveryView.tsx +70 -0
  49. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockAuditSelectResItem.tsx +7 -1
  50. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockAuditView.tsx +21 -1
  51. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockBalanceSelectResItem.tsx +7 -1
  52. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockBalanceView.tsx +3 -1
  53. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/order.tsx +12 -0
  54. package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +15 -2
  55. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +215 -3
  56. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/stockAudit.ts +25 -2
  57. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/stockBalance.ts +14 -3
  58. package/vona/src/suite/a-commerce/modules/commerce-trade/test/checkoutReservation.test.ts +4 -4
  59. package/vona/src/suite/a-commerce/modules/commerce-trade/test/orderPresentation.test.ts +358 -0
  60. package/vona/src/suite/a-commerce/modules/commerce-trade/test/orderSnapshot.test.ts +8 -8
  61. package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +5 -1
  62. package/vona/src/suite/a-commerce/modules/commerce-trade/test/refundLifecycle.test.ts +50 -1
  63. package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockPresentation.test.ts +324 -0
  64. package/vona/src/suite/a-home/modules/home-base/package.json +3 -2
  65. package/vona/src/suite/a-home/modules/home-base/src/.metadata/index.ts +93 -16
  66. package/vona/src/suite/a-home/modules/home-base/src/controller/siteCatalog.ts +21 -0
  67. package/vona/src/suite/a-home/modules/home-base/src/dto/siteCatalogSelectRes.ts +11 -0
  68. package/vona/src/suite/a-home/modules/home-base/src/dto/siteCatalogSelectResItem.ts +15 -0
  69. package/vona/src/suite/a-home/modules/home-base/src/service/siteCatalog.ts +29 -0
  70. package/vona/src/suite/cabloy-basic/modules/basic-siteadmin/src/bean/ssrSite.admin.ts +3 -0
  71. package/vona/src/suite/cabloy-basic/modules/basic-siteadmin/src/config/locale/en-us.ts +1 -0
  72. package/vona/src/suite/cabloy-basic/modules/basic-siteadmin/src/config/locale/zh-cn.ts +1 -0
  73. package/vona/src/suite/cabloy-basic/modules/basic-siteweb/src/bean/ssrSite.web.ts +3 -0
  74. package/vona/src/suite/cabloy-basic/modules/basic-siteweb/src/config/locale/en-us.ts +1 -0
  75. package/vona/src/suite/cabloy-basic/modules/basic-siteweb/src/config/locale/zh-cn.ts +1 -0
  76. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/cli/ssrSite/boilerplate/{{sceneName}}.{{beanName}}.ts_ +1 -0
  77. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/package.json +1 -1
  78. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/types/env.ts +1 -3
  79. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/types/ssrSite.ts +2 -1
  80. package/vona/src/suite-vendor/a-cabloy/package.json +1 -1
  81. package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
  82. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/index.ts +67 -0
  83. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/bean.payProvider.ts +1 -0
  84. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/bean.payScene.ts +5 -4
  85. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.index.ts +8 -0
  86. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.version.ts +22 -0
  87. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/schedule.providerOperationDispatch.ts +1 -1
  88. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/paymentSession.tsx +6 -0
  89. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/providerOperation.tsx +3 -0
  90. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/providerOperationRecoveryAudit.tsx +53 -0
  91. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/refundOperation.tsx +6 -0
  92. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/model/providerOperationRecoveryAudit.ts +12 -0
  93. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/paymentSession.ts +6 -2
  94. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/providerOperation.ts +236 -30
  95. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/refundOperation.ts +8 -5
  96. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/webhook.ts +34 -0
  97. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payProvider.ts +4 -0
  98. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payment.ts +6 -0
  99. package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
  100. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/bean/payProvider.mock.ts +15 -7
  101. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/service/payMock.ts +9 -5
  102. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +1 -1
  103. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/bean/payProvider.paypal.ts +289 -86
  104. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/lib/paypalGateway.ts +9 -1
  105. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +1 -1
  106. package/vona/src/suite-vendor/a-pay/package.json +5 -5
  107. package/vona/src/suite-vendor/a-vona/modules/a-openapi/package.json +1 -1
  108. package/vona/src/suite-vendor/a-vona/modules/a-openapi/src/types/rest.ts +1 -0
  109. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/package.json +1 -1
  110. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/lib/utils.ts +12 -0
  111. package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
  112. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoCreate.ts +5 -1
  113. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoUpdate.ts +5 -1
  114. package/vona/src/suite-vendor/a-vona/modules/a-validation/package.json +1 -1
  115. package/vona/src/suite-vendor/a-vona/modules/a-validation/src/bean/bean.validator.ts +60 -1
  116. package/vona/src/suite-vendor/a-vona/modules/a-web/package.json +1 -1
  117. package/vona/src/suite-vendor/a-vona/modules/a-web/src/lib/decorator/bean.ts +7 -4
  118. package/vona/src/suite-vendor/a-vona/modules/a-web/src/types/dto.ts +2 -1
  119. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  120. package/zova/env/.env +3 -5
  121. package/zova/env/.env.cabloyBasicAdmin +1 -4
  122. package/zova/env/.env.cabloyBasicWeb +1 -5
  123. package/zova/env/.env.cabloyCommerce +1 -5
  124. package/zova/env/.env.cabloyCommerceAdmin +1 -4
  125. package/zova/env/.env.ssr.admin +1 -4
  126. package/zova/env/.env.ssr.web +1 -5
  127. package/zova/package.original.json +1 -1
  128. package/zova/packages-utils/zova-jsx/package.json +2 -2
  129. package/zova/packages-zova/zova/package.json +3 -3
  130. package/zova/packages-zova/zova-core/package.json +2 -1
  131. package/zova/packages-zova/zova-core/src/core/component/locale.ts +10 -4
  132. package/zova/packages-zova/zova-core/src/core/sys/config.ts +14 -6
  133. package/zova/packages-zova/zova-core/src/types/utils/env.ts +4 -2
  134. package/zova/pnpm-lock.yaml +24 -21
  135. package/zova/src/front/config/config/config.ts +22 -7
  136. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/routes.ts +2 -2
  137. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/schemas.ts +16 -8
  138. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/types.ts +150 -216
  139. package/zova/src/suite/a-commerce/modules/commerce-member/src/model/addressMine.ts +1 -2
  140. package/zova/src/suite/a-commerce/modules/commerce-member/src/page/address/controller.tsx +5 -8
  141. package/zova/src/suite/a-commerce/modules/commerce-member/src/routes.ts +1 -1
  142. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/model/coupon.ts +1 -2
  143. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/routes.ts +3 -1
  144. package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +3 -0
  145. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeOrder.ts +72 -0
  146. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +8 -0
  147. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +226 -16
  148. package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeOrder.ts +15 -0
  149. package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionRefund/controller.tsx +155 -36
  150. package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +7 -0
  151. package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +7 -0
  152. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/cart.ts +3 -4
  153. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +42 -0
  154. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/orderMine.ts +2 -4
  155. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/cart/controller.tsx +5 -1
  156. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/checkout/controller.tsx +14 -10
  157. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/order/controller.tsx +5 -8
  158. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/orders/controller.tsx +5 -8
  159. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +151 -24
  160. package/zova/src/suite/a-commerce/modules/commerce-trade/src/routes.ts +5 -5
  161. package/zova/src/suite/a-demo/modules/demo-basic/src/routes.ts +26 -7
  162. package/zova/src/suite/a-demo/modules/demo-todo/src/routes.ts +2 -2
  163. package/zova/src/suite/a-devui/modules/devui-adapter/src/bean/meta.themeHandler.ts +1 -1
  164. package/zova/src/suite/a-home/modules/home-base/package.json +1 -0
  165. package/zova/src/suite/a-home/modules/home-base/src/routes.ts +5 -0
  166. package/zova/src/suite/a-home/modules/home-base/src/service/routerGuards.ts +1 -1
  167. package/zova/src/suite/a-home/modules/home-base/src/service/ssrLayout.ts +18 -14
  168. package/zova/src/suite/a-home/modules/home-indexadmin/src/routes.ts +1 -1
  169. package/zova/src/suite/a-home/modules/home-indexweb/src/routes.ts +1 -0
  170. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/controller.tsx +12 -4
  171. package/zova/src/suite/a-home/modules/home-layoutadmin/src/config/config.ts +8 -0
  172. package/zova/src/suite/a-home/modules/home-layoutadmin/src/model/layout.ts +5 -8
  173. package/zova/src/suite/a-home/modules/home-layoutempty/src/.metadata/index.ts +10 -3
  174. package/zova/src/suite/a-home/modules/home-layoutempty/src/component/layoutEmpty/controller.tsx +9 -3
  175. package/zova/src/suite/a-home/modules/home-layoutempty/src/config/config.ts +12 -0
  176. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/controller.tsx +13 -8
  177. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.header.tsx +1 -1
  178. package/zova/src/suite/a-home/modules/home-layoutweb/src/config/config.ts +4 -0
  179. package/zova/src/suite/a-home/modules/home-layoutweb/src/model/layout.ts +5 -8
  180. package/zova/src/suite/a-home/modules/home-login/src/routes.ts +1 -0
  181. package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +2 -3
  182. package/zova/src/suite/cabloy-basic/modules/basic-metrics/src/routes.ts +3 -1
  183. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  184. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +1 -1
  185. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/routes.ts +3 -0
  186. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  187. package/zova/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
  188. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/model/paymentSession.ts +2 -2
  189. package/zova/src/suite-vendor/a-pay/package.json +2 -2
  190. package/zova/src/suite-vendor/a-zova/modules/a-interceptor/package.json +1 -1
  191. package/zova/src/suite-vendor/a-zova/modules/a-interceptor/src/bean/interceptor.jwt.ts +1 -1
  192. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  193. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/model/sdk.ts +1 -1
  194. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts +1 -0
  195. package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
  196. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/config/config.ts +1 -17
  197. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/bodyReadyObserver.ts +27 -0
  198. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/index.ts +2 -0
  199. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/ssr.ts +33 -0
  200. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/ssrMetaStore.ts +3 -35
  201. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/ssrProfile.ts +38 -0
  202. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/monkey.ts +10 -0
  203. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/types/config.ts +3 -10
  204. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/types/ssr.ts +17 -8
  205. package/zova/src/suite-vendor/a-zova/modules/a-ssrserver/package.json +1 -1
  206. package/zova/src/suite-vendor/a-zova/modules/a-ssrserver/src/service/ssrHandler.ts +33 -18
  207. package/zova/src/suite-vendor/a-zova/modules/a-style/package.json +1 -1
  208. package/zova/src/suite-vendor/a-zova/modules/a-style/src/bean/bean.theme.ts +2 -2
  209. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  210. package/zova/src/suite-vendor/a-zova/package.json +7 -7
@@ -0,0 +1,70 @@
1
+ import type { TableIdentity } from 'table-identity';
2
+ import type { IDecoratorDtoOptions } from 'vona-module-a-web';
3
+
4
+ import { Api, v } from 'vona-module-a-openapiutils';
5
+ import { Dto } from 'vona-module-a-web';
6
+ import { z } from 'zod';
7
+
8
+ export interface IDtoOptionsRefundRecoveryView extends IDecoratorDtoOptions {}
9
+
10
+ @Dto<IDtoOptionsRefundRecoveryView>()
11
+ export class DtoRefundRecoveryView {
12
+ @Api.field(v.tableIdentity())
13
+ orderId: TableIdentity;
14
+
15
+ @Api.field(v.tableIdentity())
16
+ refundRequestId: TableIdentity;
17
+
18
+ @Api.field(v.tableIdentity())
19
+ refundAttemptId: TableIdentity;
20
+
21
+ @Api.field(v.tableIdentity())
22
+ refundOperationId: TableIdentity;
23
+
24
+ @Api.field(v.tableIdentity())
25
+ providerOperationId: TableIdentity;
26
+
27
+ @Api.field(v.required(), v.min(1), v.max(100))
28
+ providerName: string;
29
+
30
+ @Api.field(z.enum(['sandbox', 'live']))
31
+ environment: 'sandbox' | 'live';
32
+
33
+ @Api.field(z.enum(['created', 'submitting', 'pending', 'succeeded', 'failed', 'cancelled']))
34
+ refundOperationState: 'created' | 'submitting' | 'pending' | 'succeeded' | 'failed' | 'cancelled';
35
+
36
+ @Api.field(
37
+ z.enum(['created', 'claimed', 'submitted', 'succeeded', 'failed', 'reconciliation_required']),
38
+ )
39
+ providerOperationState:
40
+ | 'created'
41
+ | 'claimed'
42
+ | 'submitted'
43
+ | 'succeeded'
44
+ | 'failed'
45
+ | 'reconciliation_required';
46
+
47
+ @Api.field(z.number().int().nonnegative())
48
+ attemptCount: number;
49
+
50
+ @Api.field(v.optional(), v.max(255))
51
+ providerRefundId?: string;
52
+
53
+ @Api.field(v.optional(), v.max(100))
54
+ errorCode?: string;
55
+
56
+ @Api.field(v.optional(), v.max(255))
57
+ errorSummary?: string;
58
+
59
+ @Api.field(v.optional())
60
+ submittedAt?: Date;
61
+
62
+ @Api.field(v.optional())
63
+ finalizedAt?: Date;
64
+
65
+ @Api.field(z.enum(['none', 'reconcile_only', 'query_only', 'retry_same_key']))
66
+ recoveryDisposition: 'none' | 'reconcile_only' | 'query_only' | 'retry_same_key';
67
+
68
+ @Api.field(v.required())
69
+ recoveryMessage: string;
70
+ }
@@ -6,6 +6,7 @@ import { Dto } from 'vona-module-a-web';
6
6
  import { ZovaRender } from 'zova-rest-cabloy-basic-admin';
7
7
 
8
8
  import { $locale } from '../.metadata/locales.ts';
9
+ import { EntityStockAudit } from '../entity/stockAudit.tsx';
9
10
  import { ModelStockAudit } from '../model/stockAudit.ts';
10
11
 
11
12
  export interface IDtoOptionsStockAuditSelectResItem extends IDecoratorDtoOptions {}
@@ -43,7 +44,12 @@ export interface IDtoOptionsStockAuditSelectResItem extends IDecoratorDtoOptions
43
44
  }),
44
45
  ],
45
46
  })
46
- export class DtoStockAuditSelectResItem extends $Dto.get(() => ModelStockAudit) {
47
+ export class DtoStockAuditSelectResItem extends $Dto.get(() => ModelStockAudit, {
48
+ columns: ['id', 'skuId', 'operation', 'delta', 'createdAt'],
49
+ }) {
50
+ @Api.field(ZovaRender.order(1, 'core'), ZovaRender.cell('basic-table:actionView'))
51
+ declare id: EntityStockAudit['id'];
52
+
47
53
  @Api.field(
48
54
  v.title($locale('Operations')),
49
55
  ZovaRender.order(1, 'max'),
@@ -22,4 +22,24 @@ export interface IDtoOptionsStockAuditView extends IDecoratorDtoOptions {}
22
22
  }),
23
23
  ],
24
24
  })
25
- export class DtoStockAuditView extends $Dto.get(() => ModelStockAudit) {}
25
+ export class DtoStockAuditView extends $Dto.get(() => ModelStockAudit, {
26
+ columns: [
27
+ 'id',
28
+ 'stockBalanceId',
29
+ 'skuId',
30
+ 'stockReservationId',
31
+ 'actorId',
32
+ 'operation',
33
+ 'delta',
34
+ 'reason',
35
+ 'correlationId',
36
+ 'priorOnHand',
37
+ 'priorReserved',
38
+ 'priorAvailable',
39
+ 'onHand',
40
+ 'reserved',
41
+ 'available',
42
+ 'createdAt',
43
+ 'updatedAt',
44
+ ],
45
+ }) {}
@@ -6,6 +6,7 @@ import { Dto } from 'vona-module-a-web';
6
6
  import { ZovaRender } from 'zova-rest-cabloy-basic-admin';
7
7
 
8
8
  import { $locale } from '../.metadata/locales.ts';
9
+ import { EntityStockBalance } from '../entity/stockBalance.tsx';
9
10
  import { ModelStockBalance } from '../model/stockBalance.ts';
10
11
 
11
12
  export interface IDtoOptionsStockBalanceSelectResItem extends IDecoratorDtoOptions {}
@@ -43,7 +44,12 @@ export interface IDtoOptionsStockBalanceSelectResItem extends IDecoratorDtoOptio
43
44
  }),
44
45
  ],
45
46
  })
46
- export class DtoStockBalanceSelectResItem extends $Dto.get(() => ModelStockBalance) {
47
+ export class DtoStockBalanceSelectResItem extends $Dto.get(() => ModelStockBalance, {
48
+ columns: ['id', 'skuId', 'onHand', 'reserved', 'available'],
49
+ }) {
50
+ @Api.field(ZovaRender.order(1, 'core'), ZovaRender.cell('basic-table:actionView'))
51
+ declare id: EntityStockBalance['id'];
52
+
47
53
  @Api.field(
48
54
  v.title($locale('Operations')),
49
55
  ZovaRender.order(1, 'max'),
@@ -22,4 +22,6 @@ export interface IDtoOptionsStockBalanceView extends IDecoratorDtoOptions {}
22
22
  }),
23
23
  ],
24
24
  })
25
- export class DtoStockBalanceView extends $Dto.get(() => ModelStockBalance) {}
25
+ export class DtoStockBalanceView extends $Dto.get(() => ModelStockBalance, {
26
+ columns: ['id', 'skuId', 'onHand', 'reserved', 'available', 'createdAt', 'updatedAt'],
27
+ }) {}
@@ -10,6 +10,18 @@ import { $locale } from '../.metadata/locales.ts';
10
10
  import { DtoOrderAddressSnapshot } from '../dto/orderAddressSnapshot.tsx';
11
11
  import { DtoOrderCouponSnapshot } from '../dto/orderCouponSnapshot.tsx';
12
12
 
13
+ export const orderStateItems = [
14
+ { value: 'awaiting_payment', title: $locale('OrderStateAwaitingPayment') },
15
+ { value: 'paid', title: $locale('OrderStatePaid') },
16
+ { value: 'refund_requested', title: $locale('OrderStateRefundRequested') },
17
+ { value: 'refund_approved', title: $locale('OrderStateRefundApproved') },
18
+ { value: 'refund_rejected', title: $locale('OrderStateRefundRejected') },
19
+ { value: 'shipped', title: $locale('OrderStateShipped') },
20
+ { value: 'refunded', title: $locale('OrderStateRefunded') },
21
+ { value: 'cancelled', title: $locale('OrderStateCancelled') },
22
+ { value: 'expired', title: $locale('OrderStateExpired') },
23
+ ];
24
+
13
25
  export type TypeOrderState =
14
26
  | 'awaiting_payment'
15
27
  | 'paid'
@@ -9,8 +9,21 @@ export interface IModelOptionsOrder extends IDecoratorModelOptions<EntityOrder>
9
9
  @Model<IModelOptionsOrder>({
10
10
  entity: EntityOrder,
11
11
  relations: {
12
- lines: $relation.hasMany('commerce-trade:orderLine', 'orderId'),
13
- shipment: $relation.hasOne('commerce-trade:shipment', 'orderId'),
12
+ lines: $relation.hasMany('commerce-trade:orderLine', 'orderId', {
13
+ columns: [
14
+ 'id',
15
+ 'skuCodeSnapshot',
16
+ 'titleSnapshot',
17
+ 'skuAttributesSnapshot',
18
+ 'unitPriceCents',
19
+ 'quantity',
20
+ 'eligibleSubtotalCents',
21
+ 'lineTotalCents',
22
+ ],
23
+ }),
24
+ shipment: $relation.hasOne('commerce-trade:shipment', 'orderId', {
25
+ columns: ['id', 'carrier', 'trackingNumber', 'shippedAt'],
26
+ }),
14
27
  },
15
28
  })
16
29
  export class ModelOrder extends BeanModelBase<EntityOrder> {}
@@ -15,6 +15,7 @@ import { Core } from 'vona-module-a-core';
15
15
  import type { DtoCheckoutCreate } from '../dto/checkoutCreate.tsx';
16
16
  import type { DtoCheckoutResult } from '../dto/checkoutResult.tsx';
17
17
  import type { DtoOrderAddressSnapshot } from '../dto/orderAddressSnapshot.tsx';
18
+ import type { DtoOrderAdminLineResItem } from '../dto/orderAdminLineResItem.tsx';
18
19
  import type { DtoOrderCouponSnapshot } from '../dto/orderCouponSnapshot.tsx';
19
20
  import type { DtoOrderDetail } from '../dto/orderDetail.tsx';
20
21
  import type { DtoOrderLineSkuAttributeSnapshot } from '../dto/orderLineSkuAttributeSnapshot.tsx';
@@ -25,6 +26,8 @@ import type { DtoOrderView } from '../dto/orderView.tsx';
25
26
  import type { DtoPaymentOutcomeCreate } from '../dto/paymentOutcomeCreate.tsx';
26
27
  import type { DtoPaymentOutcomeResult } from '../dto/paymentOutcomeResult.tsx';
27
28
  import type { DtoRefundOutcomeCreate } from '../dto/refundOutcomeCreate.tsx';
29
+ import type { DtoRefundRecoveryAction } from '../dto/refundRecoveryAction.tsx';
30
+ import type { DtoRefundRecoveryView } from '../dto/refundRecoveryView.tsx';
28
31
  import type { DtoRefundRequestCreate } from '../dto/refundRequestCreate.tsx';
29
32
  import type { DtoRefundResult } from '../dto/refundResult.tsx';
30
33
  import type { DtoRefundReview } from '../dto/refundReview.tsx';
@@ -55,7 +58,13 @@ const serializationRetryOptions = {
55
58
  minTimeout: 0,
56
59
  maxTimeout: 0,
57
60
  randomize: false,
58
- errorCodes: ['40001', 'ER_LOCK_DEADLOCK', 'ER_LOCK_WAIT_TIMEOUT'],
61
+ errorCodes: [
62
+ '40001',
63
+ 'ER_LOCK_DEADLOCK',
64
+ 'ER_LOCK_WAIT_TIMEOUT',
65
+ 'SQLITE_BUSY',
66
+ 'SQLITE_BUSY_SNAPSHOT',
67
+ ],
59
68
  };
60
69
 
61
70
  const checkoutSerializationRetryOptions = {
@@ -726,6 +735,43 @@ export class ServiceOrder extends BeanBase {
726
735
  return await this._reviewRefund(orderId, command, decision, onStage);
727
736
  }
728
737
 
738
+ @Core.transaction({ isolationLevel: 'SERIALIZABLE' })
739
+ @Core.retryable(serializationRetryOptions)
740
+ async refundRecovery(orderId: TableIdentity): Promise<DtoRefundRecoveryView> {
741
+ const linked = await this.refundRecoveryLinkage(orderId);
742
+ return this._refundRecoveryView(linked);
743
+ }
744
+
745
+ @Core.transaction({ isolationLevel: 'SERIALIZABLE' })
746
+ @Core.retryable(serializationRetryOptions)
747
+ async reconcileRefund(
748
+ orderId: TableIdentity,
749
+ command: DtoRefundRecoveryAction,
750
+ ): Promise<DtoRefundRecoveryView> {
751
+ const linked = await this.refundRecoveryLinkage(orderId);
752
+ await this.$scope.pay.service.providerOperation.reconcileRefund(linked.providerOperation.id, {
753
+ ...command,
754
+ actorId: this.bean.passport.currentUser!.id,
755
+ });
756
+ return await this.refundRecovery(orderId);
757
+ }
758
+
759
+ @Core.transaction({ isolationLevel: 'SERIALIZABLE' })
760
+ @Core.retryable(serializationRetryOptions)
761
+ async retryRefund(
762
+ orderId: TableIdentity,
763
+ command: DtoRefundRecoveryAction,
764
+ ): Promise<DtoRefundRecoveryView> {
765
+ const linked = await this.refundRecoveryLinkage(orderId);
766
+ await this.$scope.pay.service.providerOperation.retryRefund(linked.providerOperation.id, {
767
+ ...command,
768
+ actorId: this.bean.passport.currentUser!.id,
769
+ });
770
+ return await this.refundRecovery(orderId);
771
+ }
772
+
773
+ @Core.transaction({ isolationLevel: 'SERIALIZABLE' })
774
+ @Core.retryable(serializationRetryOptions)
729
775
  async executeRefund(orderId: TableIdentity): Promise<DtoRefundResult> {
730
776
  const prepared = await this._prepareRefundExecution(orderId);
731
777
  await this.$scope.commercePayment.service.commercePayScene.createRefundOperation(prepared.id);
@@ -743,6 +789,140 @@ export class ServiceOrder extends BeanBase {
743
789
 
744
790
  @Core.transaction({ isolationLevel: 'SERIALIZABLE' })
745
791
  @Core.retryable(serializationRetryOptions)
792
+ async refundRecoveryLinkage(orderId: TableIdentity) {
793
+ const order = await this.scope.model.order.getByIdForUpdate(orderId);
794
+ if (!order) this.app.throw(404, 'order not found');
795
+ const refundRequest = await this.$scope.commercePayment.model.refundRequest.getForUpdate({
796
+ orderId: order.id,
797
+ state: 'approved',
798
+ });
799
+ if (!refundRequest) this.app.throw(404, 'approved refund request not found');
800
+ const refundAttempt = await this.$scope.commercePayment.model.refundAttempt.getForUpdate({
801
+ refundRequestId: refundRequest.id,
802
+ });
803
+ if (!refundAttempt?.refundOperationId) this.app.throw(404, 'refund recovery is not available');
804
+ const paymentAttempt = await this.$scope.commercePayment.model.paymentAttempt.getForUpdate({
805
+ orderId: order.id,
806
+ });
807
+ if (!paymentAttempt?.paymentSessionId)
808
+ this.app.throw(404, 'commerce payment session not found');
809
+ const refundOperation = await this.$scope.pay.model.refundOperation.getByIdForUpdate(
810
+ refundAttempt.refundOperationId,
811
+ );
812
+ if (!refundOperation) this.app.throw(404, 'refund operation not found');
813
+ const paymentSession = await this.$scope.pay.model.paymentSession.getByIdForUpdate(
814
+ paymentAttempt.paymentSessionId,
815
+ );
816
+ if (!paymentSession) this.app.throw(404, 'payment session not found');
817
+ const providerOperation = await this.$scope.pay.model.providerOperation.getForUpdate({
818
+ refundOperationId: refundOperation.id,
819
+ kind: 'refund',
820
+ });
821
+ if (!providerOperation) this.app.throw(404, 'refund provider operation not found');
822
+ const unresolvedReconciliations =
823
+ await this.$scope.pay.model.providerOperationRecoveryAudit.select({
824
+ where: {
825
+ providerOperationId: providerOperation.id,
826
+ action: 'reconcile',
827
+ resolution: 'unresolved',
828
+ },
829
+ limit: 1,
830
+ });
831
+ if (
832
+ order.state !== 'refund_approved' ||
833
+ refundRequest.state !== 'approved' ||
834
+ refundAttempt.state !== 'created' ||
835
+ String(refundAttempt.orderId) !== String(order.id) ||
836
+ String(refundAttempt.refundRequestId) !== String(refundRequest.id) ||
837
+ String(refundOperation.paymentSessionId) !== String(paymentSession.id) ||
838
+ String(providerOperation.paymentSessionId) !== String(paymentSession.id) ||
839
+ refundOperation.businessReference !== String(refundAttempt.id) ||
840
+ refundAttempt.amountCents !== refundRequest.amountCents ||
841
+ refundAttempt.currency !== refundRequest.currency ||
842
+ refundOperation.amountMinor !== refundAttempt.amountCents ||
843
+ refundOperation.currency !== refundAttempt.currency ||
844
+ paymentSession.currency !== refundOperation.currency ||
845
+ !paymentSession.providerCaptureId
846
+ ) {
847
+ this.app.throw(409, 'refund recovery linkage conflicts with immutable payment facts');
848
+ }
849
+ return {
850
+ order,
851
+ refundRequest,
852
+ refundAttempt,
853
+ paymentAttempt,
854
+ paymentSession,
855
+ refundOperation,
856
+ providerOperation,
857
+ hasUnresolvedReconciliation: unresolvedReconciliations.length > 0,
858
+ };
859
+ }
860
+
861
+ private _refundRecoveryView(
862
+ linked: Awaited<ReturnType<typeof this.refundRecoveryLinkage>>,
863
+ ): DtoRefundRecoveryView {
864
+ const {
865
+ order,
866
+ refundRequest,
867
+ refundAttempt,
868
+ paymentSession,
869
+ refundOperation,
870
+ providerOperation,
871
+ hasUnresolvedReconciliation,
872
+ } = linked;
873
+ const canQuery = Boolean(
874
+ refundOperation.providerRefundId && providerOperation.state !== 'succeeded',
875
+ );
876
+ const canRetry =
877
+ !refundOperation.providerRefundId &&
878
+ providerOperation.state === 'reconciliation_required' &&
879
+ providerOperation.errorCode === 'refund_submission_outcome_unknown' &&
880
+ Boolean(providerOperation.submittedAt) &&
881
+ !providerOperation.recoveryRetryGrantedAt &&
882
+ hasUnresolvedReconciliation &&
883
+ providerOperation.submittedAt!.getTime() + 45 * 24 * 60 * 60 * 1_000 >= Date.now();
884
+ const canReconcile =
885
+ !refundOperation.providerRefundId &&
886
+ providerOperation.state === 'reconciliation_required' &&
887
+ providerOperation.errorCode === 'refund_submission_outcome_unknown' &&
888
+ Boolean(providerOperation.submittedAt) &&
889
+ !hasUnresolvedReconciliation;
890
+ const recoveryDisposition = canQuery
891
+ ? 'query_only'
892
+ : canRetry
893
+ ? 'retry_same_key'
894
+ : canReconcile
895
+ ? 'reconcile_only'
896
+ : 'none';
897
+ const recoveryMessage =
898
+ recoveryDisposition === 'query_only'
899
+ ? 'A verified provider refund identifier is available. Reconcile without resubmitting.'
900
+ : recoveryDisposition === 'reconcile_only'
901
+ ? 'Provider refund outcome is unknown. Record reconciliation before considering a retry.'
902
+ : recoveryDisposition === 'retry_same_key'
903
+ ? 'Reconciliation was unresolved. A single acknowledged retry may reuse the original provider request.'
904
+ : 'No provider refund submission is available. Do not create or manually declare a replacement refund.';
905
+ return {
906
+ orderId: order.id,
907
+ refundRequestId: refundRequest.id,
908
+ refundAttemptId: refundAttempt.id,
909
+ refundOperationId: refundOperation.id,
910
+ providerOperationId: providerOperation.id,
911
+ providerName: paymentSession.providerName,
912
+ environment: paymentSession.environment,
913
+ refundOperationState: refundOperation.state,
914
+ providerOperationState: providerOperation.state,
915
+ attemptCount: providerOperation.attemptCount,
916
+ providerRefundId: refundOperation.providerRefundId,
917
+ errorCode: providerOperation.errorCode,
918
+ errorSummary: providerOperation.errorSummary,
919
+ submittedAt: providerOperation.submittedAt,
920
+ finalizedAt: refundOperation.finalizedAt ?? providerOperation.finalizedAt,
921
+ recoveryDisposition,
922
+ recoveryMessage,
923
+ };
924
+ }
925
+
746
926
  private async _prepareRefundExecution(orderId: TableIdentity) {
747
927
  const order = await this.scope.model.order.getByIdForUpdate(orderId);
748
928
  if (!order) this.app.throw(404, 'order not found');
@@ -758,6 +938,9 @@ export class ServiceOrder extends BeanBase {
758
938
  if (order.state !== 'refund_approved' || attempt.state !== 'created') {
759
939
  this.app.throw(409, 'refund is not available for execution');
760
940
  }
941
+ if (attempt.refundOperationId) {
942
+ this.app.throw(409, 'refund submission requires recovery inspection');
943
+ }
761
944
  return attempt;
762
945
  }
763
946
 
@@ -939,14 +1122,43 @@ export class ServiceOrder extends BeanBase {
939
1122
  }
940
1123
 
941
1124
  async select(params?: IQueryParams<ModelOrder>): Promise<DtoOrderSelectRes> {
942
- return await this.scope.model.order.selectAndCount({
1125
+ const result = await this.scope.model.order.selectAndCount({
943
1126
  ...params,
1127
+ columns: ['id', 'state', 'payableTotalCents', 'reservationExpiresAt', 'createdAt'],
944
1128
  orders: params?.orders ?? [['id', 'desc']],
945
1129
  });
1130
+ return result as DtoOrderSelectRes;
946
1131
  }
947
1132
 
948
1133
  async view(id: TableIdentity): Promise<DtoOrderView | undefined> {
949
- return await this.scope.model.order.getById(id, { include: { shipment: true } });
1134
+ const order = await this.scope.model.order.getById(id, {
1135
+ columns: [
1136
+ 'id',
1137
+ 'state',
1138
+ 'currency',
1139
+ 'eligibleSubtotalCents',
1140
+ 'discountCents',
1141
+ 'payableTotalCents',
1142
+ 'reservationExpiresAt',
1143
+ 'addressSnapshot',
1144
+ 'couponSnapshot',
1145
+ 'createdAt',
1146
+ 'updatedAt',
1147
+ ],
1148
+ include: { shipment: true, lines: true },
1149
+ });
1150
+ if (!order) return;
1151
+ const lines: DtoOrderAdminLineResItem[] = order.lines.map(line => ({
1152
+ id: line.id,
1153
+ skuCodeSnapshot: line.skuCodeSnapshot,
1154
+ titleSnapshot: line.titleSnapshot,
1155
+ skuAttributesSnapshot: line.skuAttributesSnapshot,
1156
+ unitPriceCents: line.unitPriceCents,
1157
+ quantity: line.quantity,
1158
+ eligibleSubtotalCents: line.eligibleSubtotalCents,
1159
+ lineTotalCents: line.lineTotalCents,
1160
+ }));
1161
+ return { ...order, lines } as DtoOrderView;
950
1162
  }
951
1163
 
952
1164
  async mine(params?: IQueryParams<ModelOrder>): Promise<DtoOrderMineRes> {
@@ -11,10 +11,33 @@ import type { ModelStockAudit } from '../model/stockAudit.ts';
11
11
  @Service()
12
12
  export class ServiceStockAudit extends BeanBase {
13
13
  async select(params?: IQueryParams<ModelStockAudit>): Promise<DtoStockAuditSelectRes> {
14
- return await this.scope.model.stockAudit.selectAndCount(params);
14
+ return await this.scope.model.stockAudit.selectAndCount({
15
+ ...params,
16
+ columns: ['id', 'skuId', 'operation', 'delta', 'createdAt'],
17
+ });
15
18
  }
16
19
 
17
20
  async view(id: TableIdentity): Promise<DtoStockAuditView | undefined> {
18
- return await this.scope.model.stockAudit.getById(id);
21
+ return await this.scope.model.stockAudit.getById(id, {
22
+ columns: [
23
+ 'id',
24
+ 'stockBalanceId',
25
+ 'skuId',
26
+ 'stockReservationId',
27
+ 'actorId',
28
+ 'operation',
29
+ 'delta',
30
+ 'reason',
31
+ 'correlationId',
32
+ 'priorOnHand',
33
+ 'priorReserved',
34
+ 'priorAvailable',
35
+ 'onHand',
36
+ 'reserved',
37
+ 'available',
38
+ 'createdAt',
39
+ 'updatedAt',
40
+ ],
41
+ });
19
42
  }
20
43
  }
@@ -34,18 +34,29 @@ const serializationRetryOptions = {
34
34
  minTimeout: 0,
35
35
  maxTimeout: 0,
36
36
  randomize: false,
37
- errorCodes: ['40001', 'ER_LOCK_DEADLOCK', 'ER_LOCK_WAIT_TIMEOUT'],
37
+ errorCodes: [
38
+ '40001',
39
+ 'ER_LOCK_DEADLOCK',
40
+ 'ER_LOCK_WAIT_TIMEOUT',
41
+ 'SQLITE_BUSY',
42
+ 'SQLITE_BUSY_SNAPSHOT',
43
+ ],
38
44
  ownerOnly: true,
39
45
  };
40
46
 
41
47
  @Service()
42
48
  export class ServiceStockBalance extends BeanBase {
43
49
  async select(params?: IQueryParams<ModelStockBalance>): Promise<DtoStockBalanceSelectRes> {
44
- return await this.scope.model.stockBalance.selectAndCount(params);
50
+ return await this.scope.model.stockBalance.selectAndCount({
51
+ ...params,
52
+ columns: ['id', 'skuId', 'onHand', 'reserved', 'available'],
53
+ });
45
54
  }
46
55
 
47
56
  async view(id: TableIdentity): Promise<DtoStockBalanceView | undefined> {
48
- return await this.scope.model.stockBalance.getById(id);
57
+ return await this.scope.model.stockBalance.getById(id, {
58
+ columns: ['id', 'skuId', 'onHand', 'reserved', 'available', 'createdAt', 'updatedAt'],
59
+ });
49
60
  }
50
61
 
51
62
  @Core.transaction({ isolationLevel: 'SERIALIZABLE' })
@@ -426,15 +426,15 @@ describe('checkoutReservation.test.ts', { concurrency: false }, () => {
426
426
  },
427
427
  },
428
428
  );
429
- fixture.skuId = await app.bean.executor.performAction('post', '/commerce/catalog/sku', {
430
- body: {
429
+ fixture.skuId = (
430
+ await app.scope('commerce-catalog').model.sku.insert({
431
431
  productId: fixture.productId,
432
432
  code: `checkout-snapshot-sku-${suffix}`,
433
433
  priceCents: 1299,
434
434
  attributes: [{ name: 'Color', value: 'Black' }],
435
435
  lifecycle: 'active',
436
- },
437
- });
436
+ })
437
+ ).id as number;
438
438
  fixture.balanceId = (
439
439
  await app.scope('commerce-trade').service.stockBalance.adjustStock({
440
440
  skuId: fixture.skuId,