cabloy 5.1.132 → 5.1.134

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 (216) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/scheduled_tasks.lock +1 -0
  3. package/.claude/skills/cabloy-worktree-environment/SKILL.md +6 -6
  4. package/.claude/skills/cabloy-worktree-environment/evals/evals.json +2 -2
  5. package/.github/workflows/vona-test-pg.yml +1 -1
  6. package/.github/workflows/vona-test-sqlite3.yml +1 -1
  7. package/CHANGELOG.md +40 -0
  8. package/cabloy-docs/backend/dto-guide.md +39 -4
  9. package/cabloy-docs/backend/dto-infer-generation.md +29 -0
  10. package/cabloy-docs/backend/openapi-guide.md +16 -0
  11. package/cabloy-docs/backend/validation-guide.md +45 -0
  12. package/cabloy-docs/frontend/environment-config-guide.md +8 -15
  13. package/cabloy-docs/frontend/ioc-and-beans.md +20 -0
  14. package/cabloy-docs/frontend/navigation-guards-guide.md +3 -3
  15. package/cabloy-docs/frontend/page-route-guide.md +4 -4
  16. package/cabloy-docs/frontend/ssr-env.md +22 -29
  17. package/cabloy-docs/frontend/theme-guide.md +4 -4
  18. package/cabloy-docs/frontend/zova-router-under-the-hood.md +1 -1
  19. package/cabloy-docs/fullstack/deploy-cloudflare-docker.md +6 -6
  20. package/cabloy-docs/fullstack/parallel-worktree-environment.md +2 -2
  21. package/e2e/specs/a-commerce/commerce.spec.ts +540 -53
  22. package/e2e/specs/cabloy-basic/basic.spec.ts +30 -5
  23. package/package.json +4 -4
  24. package/vona/.claude/skills/detect-ssr-leak/SKILL.md +1 -1
  25. package/vona/packages-vona/vona/package.json +1 -1
  26. package/vona/pnpm-lock.yaml +336 -62
  27. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuCreate.tsx +3 -1
  28. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuSelectResItem.tsx +13 -1
  29. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuUpdate.tsx +3 -1
  30. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuView.tsx +13 -1
  31. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/sku.tsx +6 -0
  32. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/skuPresentation.test.ts +227 -0
  33. package/vona/src/suite/a-commerce/modules/commerce-member/src/.metadata/index.ts +46 -36
  34. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressSelectResItem.tsx +5 -13
  35. package/vona/src/suite/a-commerce/modules/commerce-member/src/service/address.ts +16 -1
  36. package/vona/src/suite/a-commerce/modules/commerce-member/test/addressPresentation.test.ts +210 -0
  37. package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/commercePayScene.ts +13 -2
  38. package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +12 -0
  39. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/service/coupon.ts +7 -1
  40. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +45 -2
  41. package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +23 -0
  42. package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +23 -0
  43. package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/order.ts +31 -0
  44. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderAdminLineBase.tsx +56 -0
  45. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderAdminLineResItem.tsx +17 -0
  46. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderAdminLineView.tsx +13 -0
  47. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderSelectResItem.tsx +26 -2
  48. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderView.tsx +197 -3
  49. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/refundRecoveryAction.tsx +19 -0
  50. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/refundRecoveryView.tsx +70 -0
  51. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockAuditSelectResItem.tsx +7 -1
  52. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockAuditView.tsx +21 -1
  53. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockBalanceSelectResItem.tsx +7 -1
  54. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockBalanceView.tsx +3 -1
  55. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/order.tsx +12 -0
  56. package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +15 -2
  57. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +215 -3
  58. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/stockAudit.ts +25 -2
  59. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/stockBalance.ts +14 -3
  60. package/vona/src/suite/a-commerce/modules/commerce-trade/test/checkoutReservation.test.ts +4 -4
  61. package/vona/src/suite/a-commerce/modules/commerce-trade/test/orderPresentation.test.ts +358 -0
  62. package/vona/src/suite/a-commerce/modules/commerce-trade/test/orderSnapshot.test.ts +8 -8
  63. package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +5 -1
  64. package/vona/src/suite/a-commerce/modules/commerce-trade/test/refundLifecycle.test.ts +50 -1
  65. package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockPresentation.test.ts +324 -0
  66. package/vona/src/suite/a-home/modules/home-base/package.json +3 -2
  67. package/vona/src/suite/a-home/modules/home-base/src/.metadata/index.ts +93 -16
  68. package/vona/src/suite/a-home/modules/home-base/src/controller/siteCatalog.ts +21 -0
  69. package/vona/src/suite/a-home/modules/home-base/src/dto/siteCatalogSelectRes.ts +11 -0
  70. package/vona/src/suite/a-home/modules/home-base/src/dto/siteCatalogSelectResItem.ts +15 -0
  71. package/vona/src/suite/a-home/modules/home-base/src/service/siteCatalog.ts +29 -0
  72. package/vona/src/suite/cabloy-basic/modules/basic-siteadmin/src/bean/ssrSite.admin.ts +3 -0
  73. package/vona/src/suite/cabloy-basic/modules/basic-siteadmin/src/config/locale/en-us.ts +1 -0
  74. package/vona/src/suite/cabloy-basic/modules/basic-siteadmin/src/config/locale/zh-cn.ts +1 -0
  75. package/vona/src/suite/cabloy-basic/modules/basic-siteweb/src/bean/ssrSite.web.ts +3 -0
  76. package/vona/src/suite/cabloy-basic/modules/basic-siteweb/src/config/locale/en-us.ts +1 -0
  77. package/vona/src/suite/cabloy-basic/modules/basic-siteweb/src/config/locale/zh-cn.ts +1 -0
  78. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/cli/ssrSite/boilerplate/{{sceneName}}.{{beanName}}.ts_ +1 -0
  79. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/package.json +1 -1
  80. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/types/env.ts +1 -3
  81. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/types/ssrSite.ts +2 -1
  82. package/vona/src/suite-vendor/a-cabloy/package.json +1 -1
  83. package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
  84. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/index.ts +67 -0
  85. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/bean.payProvider.ts +1 -0
  86. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/bean.payScene.ts +5 -4
  87. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.index.ts +8 -0
  88. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.version.ts +22 -0
  89. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/schedule.providerOperationDispatch.ts +1 -1
  90. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/paymentSession.tsx +6 -0
  91. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/providerOperation.tsx +3 -0
  92. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/providerOperationRecoveryAudit.tsx +53 -0
  93. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/refundOperation.tsx +6 -0
  94. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/model/providerOperationRecoveryAudit.ts +12 -0
  95. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/paymentSession.ts +6 -2
  96. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/providerOperation.ts +236 -30
  97. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/refundOperation.ts +8 -5
  98. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/webhook.ts +34 -0
  99. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payProvider.ts +4 -0
  100. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payment.ts +6 -0
  101. package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
  102. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/bean/payProvider.mock.ts +15 -7
  103. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/service/payMock.ts +9 -5
  104. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +1 -1
  105. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/bean/payProvider.paypal.ts +289 -86
  106. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/lib/paypalGateway.ts +9 -1
  107. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +1 -1
  108. package/vona/src/suite-vendor/a-pay/package.json +5 -5
  109. package/vona/src/suite-vendor/a-vona/modules/a-openapi/package.json +1 -1
  110. package/vona/src/suite-vendor/a-vona/modules/a-openapi/src/types/rest.ts +1 -0
  111. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/package.json +1 -1
  112. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/lib/utils.ts +12 -0
  113. package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
  114. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoCreate.ts +5 -1
  115. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoUpdate.ts +5 -1
  116. package/vona/src/suite-vendor/a-vona/modules/a-validation/package.json +1 -1
  117. package/vona/src/suite-vendor/a-vona/modules/a-validation/src/bean/bean.validator.ts +60 -1
  118. package/vona/src/suite-vendor/a-vona/modules/a-web/package.json +1 -1
  119. package/vona/src/suite-vendor/a-vona/modules/a-web/src/lib/decorator/bean.ts +7 -4
  120. package/vona/src/suite-vendor/a-vona/modules/a-web/src/types/dto.ts +2 -1
  121. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  122. package/zova/env/.env +3 -5
  123. package/zova/env/.env.cabloyBasicAdmin +1 -4
  124. package/zova/env/.env.cabloyBasicWeb +1 -5
  125. package/zova/env/.env.cabloyCommerce +1 -5
  126. package/zova/env/.env.cabloyCommerceAdmin +1 -4
  127. package/zova/env/.env.ssr.admin +1 -4
  128. package/zova/env/.env.ssr.web +1 -5
  129. package/zova/packages-utils/zova-jsx/package.json +2 -2
  130. package/zova/packages-zova/zova/package.json +3 -3
  131. package/zova/packages-zova/zova-core/package.json +3 -1
  132. package/zova/packages-zova/zova-core/src/core/component/locale.ts +10 -4
  133. package/zova/packages-zova/zova-core/src/core/sys/config.ts +14 -6
  134. package/zova/packages-zova/zova-core/src/types/utils/env.ts +4 -2
  135. package/zova/pnpm-lock.yaml +50 -44
  136. package/zova/src/front/config/config/config.ts +22 -7
  137. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/routes.ts +2 -2
  138. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/schemas.ts +16 -8
  139. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/types.ts +150 -216
  140. package/zova/src/suite/a-commerce/modules/commerce-member/src/model/addressMine.ts +1 -2
  141. package/zova/src/suite/a-commerce/modules/commerce-member/src/page/address/controller.tsx +5 -8
  142. package/zova/src/suite/a-commerce/modules/commerce-member/src/routes.ts +1 -1
  143. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/model/coupon.ts +1 -2
  144. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/routes.ts +3 -1
  145. package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +3 -0
  146. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeOrder.ts +72 -0
  147. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +8 -0
  148. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +226 -16
  149. package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeOrder.ts +15 -0
  150. package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionRefund/controller.tsx +155 -36
  151. package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +7 -0
  152. package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +7 -0
  153. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/cart.ts +3 -4
  154. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +42 -0
  155. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/orderMine.ts +2 -4
  156. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/cart/controller.tsx +5 -1
  157. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/checkout/controller.tsx +14 -10
  158. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/order/controller.tsx +5 -8
  159. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/orders/controller.tsx +5 -8
  160. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +151 -24
  161. package/zova/src/suite/a-commerce/modules/commerce-trade/src/routes.ts +5 -5
  162. package/zova/src/suite/a-demo/modules/demo-basic/src/routes.ts +26 -7
  163. package/zova/src/suite/a-demo/modules/demo-todo/src/routes.ts +2 -2
  164. package/zova/src/suite/a-devui/modules/devui-adapter/src/bean/meta.themeHandler.ts +1 -1
  165. package/zova/src/suite/a-home/modules/home-base/package.json +1 -0
  166. package/zova/src/suite/a-home/modules/home-base/src/routes.ts +5 -0
  167. package/zova/src/suite/a-home/modules/home-base/src/service/routerGuards.ts +1 -1
  168. package/zova/src/suite/a-home/modules/home-base/src/service/ssrLayout.ts +18 -14
  169. package/zova/src/suite/a-home/modules/home-indexadmin/src/routes.ts +1 -1
  170. package/zova/src/suite/a-home/modules/home-indexweb/src/routes.ts +1 -0
  171. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/controller.tsx +12 -4
  172. package/zova/src/suite/a-home/modules/home-layoutadmin/src/config/config.ts +8 -0
  173. package/zova/src/suite/a-home/modules/home-layoutadmin/src/model/layout.ts +5 -8
  174. package/zova/src/suite/a-home/modules/home-layoutempty/src/.metadata/index.ts +10 -3
  175. package/zova/src/suite/a-home/modules/home-layoutempty/src/component/layoutEmpty/controller.tsx +9 -3
  176. package/zova/src/suite/a-home/modules/home-layoutempty/src/config/config.ts +12 -0
  177. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/controller.tsx +13 -8
  178. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.header.tsx +1 -1
  179. package/zova/src/suite/a-home/modules/home-layoutweb/src/config/config.ts +4 -0
  180. package/zova/src/suite/a-home/modules/home-layoutweb/src/model/layout.ts +5 -8
  181. package/zova/src/suite/a-home/modules/home-login/src/routes.ts +1 -0
  182. package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +2 -3
  183. package/zova/src/suite/cabloy-basic/modules/basic-metrics/src/routes.ts +3 -1
  184. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  185. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +1 -1
  186. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/routes.ts +3 -0
  187. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  188. package/zova/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
  189. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/model/paymentSession.ts +2 -2
  190. package/zova/src/suite-vendor/a-pay/package.json +2 -2
  191. package/zova/src/suite-vendor/a-zova/modules/a-interceptor/package.json +1 -1
  192. package/zova/src/suite-vendor/a-zova/modules/a-interceptor/src/bean/interceptor.jwt.ts +1 -1
  193. package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
  194. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.persister.ts +28 -1
  195. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  196. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/model/sdk.ts +1 -1
  197. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts +1 -0
  198. package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
  199. package/zova/src/suite-vendor/a-zova/modules/a-router/src/monkey.ts +23 -0
  200. package/zova/src/suite-vendor/a-zova/modules/a-router/src/monkeySys.ts +1 -1
  201. package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
  202. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/config/config.ts +1 -17
  203. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/bodyReadyObserver.ts +27 -0
  204. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/index.ts +2 -0
  205. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/ssr.ts +33 -0
  206. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/ssrMetaStore.ts +3 -35
  207. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/ssrProfile.ts +38 -0
  208. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/monkey.ts +14 -0
  209. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/types/config.ts +3 -10
  210. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/types/ssr.ts +17 -8
  211. package/zova/src/suite-vendor/a-zova/modules/a-ssrserver/package.json +1 -1
  212. package/zova/src/suite-vendor/a-zova/modules/a-ssrserver/src/service/ssrHandler.ts +33 -18
  213. package/zova/src/suite-vendor/a-zova/modules/a-style/package.json +1 -1
  214. package/zova/src/suite-vendor/a-zova/modules/a-style/src/bean/bean.theme.ts +22 -3
  215. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  216. package/zova/src/suite-vendor/a-zova/package.json +9 -9
@@ -11,8 +11,13 @@ const passportTestActivateCurrentPath = '/api/home/user/passportTest/activateCur
11
11
 
12
12
  interface IAddressFixture {
13
13
  addressLine1: string;
14
+ addressLine2: string;
14
15
  city: string;
16
+ countryCode: string;
17
+ phone: string;
18
+ postalCode: string;
15
19
  recipientName: string;
20
+ region: string;
16
21
  updatedCity: string;
17
22
  }
18
23
 
@@ -48,7 +53,7 @@ function waitForAddressMine(page: Page) {
48
53
  }
49
54
 
50
55
  async function getAdminOrderRow(page: Page, orderId: number): Promise<Locator> {
51
- const row = page.locator(`tr:has(td:first-child:text-is("${orderId}"))`);
56
+ const row = page.locator(`tr:has(td a:text-is("${orderId}"))`);
52
57
  await expect(row).toHaveCount(1);
53
58
  return row;
54
59
  }
@@ -123,21 +128,26 @@ function createAddressFixture(testInfo: TestInfo): IAddressFixture {
123
128
  const id = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`;
124
129
  return {
125
130
  recipientName: `E2E Address ${id}`,
126
- addressLine1: `1 Test Street ${id}`,
131
+ phone: '15555550123',
132
+ countryCode: 'US',
133
+ region: 'California',
127
134
  city: `Original City ${id}`,
135
+ postalCode: '94105',
136
+ addressLine1: `1 Test Street ${id}`,
137
+ addressLine2: 'Suite E2E',
128
138
  updatedCity: `Updated City ${id}`,
129
139
  };
130
140
  }
131
141
 
132
142
  async function fillAddressForm(page: Page, fixture: IAddressFixture) {
133
143
  await page.getByPlaceholder('Recipient name').fill(fixture.recipientName);
134
- await page.getByPlaceholder('Phone').fill('15555550123');
135
- await page.getByPlaceholder('Country code').fill('US');
136
- await page.getByPlaceholder('Region').fill('California');
144
+ await page.getByPlaceholder('Phone').fill(fixture.phone);
145
+ await page.getByPlaceholder('Country code').fill(fixture.countryCode);
146
+ await page.getByPlaceholder('Region').fill(fixture.region);
137
147
  await page.getByPlaceholder('City').fill(fixture.city);
138
- await page.getByPlaceholder('Postal code').fill('94105');
148
+ await page.getByPlaceholder('Postal code').fill(fixture.postalCode);
139
149
  await page.getByPlaceholder('Address line 1').fill(fixture.addressLine1);
140
- await page.getByPlaceholder('Address line 2').fill('Suite E2E');
150
+ await page.getByPlaceholder('Address line 2').fill(fixture.addressLine2);
141
151
  }
142
152
 
143
153
  async function createAddressThroughCustomerPage(
@@ -186,6 +196,7 @@ test(
186
196
  async ({ page, request }) => {
187
197
  const response = await request.get('/commerce');
188
198
  expect(response.ok()).toBeTruthy();
199
+ expect(response.headers()['cache-control']).toBe('public, max-age=600');
189
200
  const html = await response.text();
190
201
  const normalizedHtml = html.toLowerCase();
191
202
  expect(normalizedHtml).not.toContain('data-zova-hydrated');
@@ -207,6 +218,42 @@ test(
207
218
  },
208
219
  );
209
220
 
221
+ test(
222
+ 'Commerce theme: public local preference mirrors session cookie without hydration mismatch',
223
+ { tag: ['@web', '@ssr', '@theme'] },
224
+ async ({ browser, request }, testInfo) => {
225
+ const customer = await registerCustomer(request, testInfo);
226
+ const context = await browser.newContext();
227
+ await context.addInitScript(() => localStorage.setItem('themedark', 'true'));
228
+ const page = await context.newPage();
229
+ const pageErrors = collectPageErrors(page);
230
+ const consoleErrors: string[] = [];
231
+ page.on('console', message => {
232
+ if (message.type() === 'error' || /hydration mismatch/i.test(message.text())) {
233
+ consoleErrors.push(message.text());
234
+ }
235
+ });
236
+ try {
237
+ await page.goto('/commerce', { waitUntil: 'load' });
238
+ await page.evaluate(() => localStorage.setItem('themedark', 'true'));
239
+ await page.reload({ waitUntil: 'load' });
240
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
241
+ await expect(page.locator('body')).toHaveAttribute('data-theme', 'dark');
242
+ expect((await context.cookies()).find(cookie => cookie.name === 'themedark')?.value).toBe(
243
+ 'true',
244
+ );
245
+
246
+ await login(page, '/commerce/address', customer.username, customer.password, 'commerce');
247
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
248
+ await expect(page.locator('body')).toHaveAttribute('data-theme', 'dark');
249
+ expect(pageErrors).toEqual([]);
250
+ expect(consoleErrors).toEqual([]);
251
+ } finally {
252
+ await context.close();
253
+ }
254
+ },
255
+ );
256
+
210
257
  test(
211
258
  'Commerce catalogue: public sellable inventory renders after hydration',
212
259
  { tag: ['@web', '@flow'] },
@@ -292,15 +339,18 @@ test(
292
339
  );
293
340
 
294
341
  test(
295
- 'Commerce Address: cookie-disabled SSR renders a neutral protected entry',
342
+ 'Commerce Address: session SSR protects an anonymous document request',
296
343
  { tag: ['@web', '@cart'] },
297
344
  async ({ page, request }) => {
298
345
  const path = '/commerce/address';
299
346
  const routePath = '/address';
300
347
  const response = await request.get(path, { maxRedirects: 0 });
301
- expect(response.status()).toBe(200);
302
- expect(response.headers().location).toBeUndefined();
303
- expect((await response.text()).toLowerCase()).not.toContain('data-zova-hydrated');
348
+ expect(response.status()).toBe(302);
349
+ expect(response.headers()['cache-control']).toBe('private, no-store');
350
+ const html = await response.text();
351
+ expect(html).not.toContain('ssr-body-ready-observer');
352
+ expect(html).not.toContain('__leftDrawerOpenJS');
353
+ expect(response.headers().location).toMatch(/^\/commerce\/login\?(?:.*&)?returnTo=/);
304
354
 
305
355
  const pageErrors = collectPageErrors(page);
306
356
  const documentResponse = await page.goto(path, { waitUntil: 'load' });
@@ -324,6 +374,33 @@ test(
324
374
  testInfo,
325
375
  );
326
376
  try {
377
+ const addressResponse = await context.request.get('/commerce/address');
378
+ expect(addressResponse.ok()).toBeTruthy();
379
+ expect(addressResponse.headers()['cache-control']).toBe('private, no-store');
380
+ const addressHtml = await addressResponse.text();
381
+ expect(addressHtml.toLowerCase()).not.toContain('data-zova-hydrated');
382
+ expect(addressHtml).not.toContain('ssr-body-ready-observer');
383
+ expect(addressHtml).not.toContain('__leftDrawerOpenJS');
384
+ expect(addressHtml).toContain(fixture.recipientName);
385
+ expect(addressHtml).toContain(fixture.addressLine1);
386
+
387
+ const coldPage = await context.newPage();
388
+ try {
389
+ const coldPageErrors = collectPageErrors(coldPage);
390
+ const coldResponse = await coldPage.goto('/commerce/address', { waitUntil: 'load' });
391
+ expect(coldResponse?.ok()).toBeTruthy();
392
+ await expect(coldPage.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
393
+ await expect(coldPage.getByRole('heading', { name: 'Addresses' })).toBeVisible();
394
+ const coldCard = coldPage
395
+ .locator('article')
396
+ .filter({ has: coldPage.getByRole('heading', { name: fixture.recipientName }) });
397
+ await expect(coldCard).toContainText(fixture.addressLine1);
398
+ await expect(coldPage.getByRole('alert')).toHaveCount(0);
399
+ expect(coldPageErrors).toEqual([]);
400
+ } finally {
401
+ await coldPage.close();
402
+ }
403
+
327
404
  const card = page
328
405
  .locator('article')
329
406
  .filter({ has: page.getByRole('heading', { name: fixture.recipientName }) });
@@ -431,32 +508,25 @@ test(
431
508
  await expect(page).toHaveURL(new RegExp(`/commerce/order/${checkout.orderId}(?:/|$)`), {
432
509
  timeout: 40_000,
433
510
  });
434
- const orderDetailResponse = waitForApiResponse(
435
- page,
436
- 'GET',
437
- new RegExp(`/api/commerce/trade/order/viewMine/${checkout.orderId}$`),
438
- );
439
- await page.reload({ waitUntil: 'load' });
440
- const orderDetailResponseValue = await orderDetailResponse;
441
- expect(orderDetailResponseValue.ok()).toBeTruthy();
442
- const order = (await orderDetailResponseValue.json()).data;
443
- expect(order.id).toBe(checkout.orderId);
444
- expect(order.state).toBe('paid');
445
- expect(order.currency).toBe('USD');
446
- expect(order.discountCents).toBe(0);
447
- expect(order.payableTotalCents).toBe(4599);
448
- expect(order.addressSnapshot.recipientName).toBe(fixture.recipientName);
449
- expect(order.addressSnapshot.addressLine1).toBe(fixture.addressLine1);
450
- expect(order.lines).toEqual([
451
- expect.objectContaining({
452
- titleSnapshot: 'Pour-Over Coffee Set',
453
- skuCodeSnapshot: 'COF-SET-01',
454
- unitPriceCents: 4599,
455
- quantity: 1,
456
- lineTotalCents: 4599,
457
- }),
458
- ]);
459
-
511
+ const orderPath = `/commerce/order/${checkout.orderId}`;
512
+ const orderResponse = await context.request.get(orderPath);
513
+ expect(orderResponse.ok()).toBeTruthy();
514
+ expect(orderResponse.headers()['cache-control']).toBe('private, no-store');
515
+ const orderHtml = await orderResponse.text();
516
+ expect(orderHtml.toLowerCase()).not.toContain('data-zova-hydrated');
517
+ expect(orderHtml).toContain(`Order #${checkout.orderId}`);
518
+ expect(orderHtml).toContain('paid · $45.99');
519
+ expect(orderHtml).toContain('USD');
520
+ expect(orderHtml).toContain(fixture.recipientName);
521
+ expect(orderHtml).toContain(fixture.addressLine1);
522
+ expect(orderHtml).toContain('Discount: $0.00');
523
+ expect(orderHtml).toContain('Pour-Over Coffee Set');
524
+ expect(orderHtml).toContain('COF-SET-01');
525
+ expect(orderHtml).toContain('1 × $45.99 = $45.99');
526
+
527
+ const orderDocumentResponse = await page.reload({ waitUntil: 'load' });
528
+ expect(orderDocumentResponse?.ok()).toBeTruthy();
529
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
460
530
  await expect(page.getByRole('heading', { name: `Order #${checkout.orderId}` })).toBeVisible();
461
531
  await expect(page.getByText('paid · $45.99')).toBeVisible();
462
532
  await expect(page.getByText(fixture.recipientName)).toBeVisible();
@@ -483,6 +553,27 @@ test(
483
553
  );
484
554
  const orderRow = await getAdminOrderRow(adminPage, checkout.orderId);
485
555
  await expect(orderRow).toBeVisible();
556
+ await expect(orderRow.locator('td')).toHaveCount(6);
557
+ await expect(orderRow).toContainText('Paid');
558
+ await expect(orderRow).toContainText('45.99');
559
+ await expect(orderRow).not.toContainText(fixture.recipientName);
560
+ const orderViewResponse = waitForApiResponse(
561
+ adminPage,
562
+ 'GET',
563
+ `/api/commerce/trade/order/${checkout.orderId}`,
564
+ );
565
+ await orderRow.getByRole('link', { name: String(checkout.orderId), exact: true }).click();
566
+ expect((await orderViewResponse).ok()).toBeTruthy();
567
+ await expect(adminPage.getByText('Money summary').first()).toBeVisible();
568
+ await expect(adminPage.getByText('Delivery address').first()).toBeVisible();
569
+ await expect(adminPage.getByText('Purchased lines').first()).toBeVisible();
570
+ await expect(adminPage.getByText('addressSnapshot').first()).toBeVisible();
571
+ await expect(adminPage.getByText('couponSnapshot').first()).toBeVisible();
572
+ await expect(adminPage.getByText('Pour-Over Coffee Set')).toBeVisible();
573
+ await expect(adminPage.getByText('COF-SET-01')).toBeVisible();
574
+ await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0);
575
+ await adminPage.getByRole('button', { name: 'Back', exact: true }).click();
576
+ await expect(orderRow).toBeVisible();
486
577
  const carrierInput = orderRow.getByPlaceholder('Carrier');
487
578
  const trackingNumberInput = orderRow.getByPlaceholder('Tracking number');
488
579
  const confirmationCheckbox = orderRow.getByRole('checkbox');
@@ -499,28 +590,24 @@ test(
499
590
  );
500
591
  await orderRow.getByRole('button', { name: 'Ship order', exact: true }).click();
501
592
  expect((await shipmentResponse).ok()).toBeTruthy();
502
- await expect(orderRow).toContainText('shipped');
593
+ await expect(orderRow).toContainText('Shipped');
503
594
  expect(adminPageErrors).toEqual([]);
504
595
  } finally {
505
596
  await adminContext.close().catch(() => {});
506
597
  }
507
598
 
508
- const shippedDetailResponse = waitForApiResponse(
509
- page,
510
- 'GET',
511
- new RegExp(`/api/commerce/trade/order/viewMine/${checkout.orderId}$`),
512
- );
513
- await page.reload({ waitUntil: 'load' });
514
- await shippedDetailResponse;
599
+ const shippedDocumentResponse = await page.reload({ waitUntil: 'load' });
600
+ expect(shippedDocumentResponse?.ok()).toBeTruthy();
601
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
515
602
  await expect(page.getByText('shipped · $45.99')).toBeVisible();
516
603
  await expect(page.getByRole('heading', { name: 'Shipment' })).toBeVisible();
517
604
  await expect(page.getByText(shipmentCarrier)).toBeVisible();
518
605
  await expect(page.getByText(shipmentTrackingNumber)).toBeVisible();
519
606
 
520
- const ordersResponse = waitForApiResponse(page, 'GET', '/api/commerce/trade/order/mine');
521
- await page.goto('/commerce/orders', { waitUntil: 'load' });
607
+ const ordersDocumentResponse = await page.goto('/commerce/orders', { waitUntil: 'load' });
608
+ expect(ordersDocumentResponse?.ok()).toBeTruthy();
522
609
  await expect(page).toHaveURL(/\/commerce\/orders(?:\/|$)/);
523
- await ordersResponse;
610
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
524
611
  await expect(page.getByRole('heading', { name: 'My orders' })).toBeVisible();
525
612
  const orderCard = page
526
613
  .locator('article')
@@ -545,6 +632,233 @@ test(
545
632
  },
546
633
  );
547
634
 
635
+ test(
636
+ 'Payment callback: authenticated direct route reconciles after hydration',
637
+ { tag: ['@web', '@flow', '@payment'] },
638
+ async ({ browser, request }, testInfo) => {
639
+ test.setTimeout(60_000);
640
+ const { context, fixture, page, pageErrors } = await createAddressThroughCustomerPage(
641
+ browser,
642
+ request,
643
+ testInfo,
644
+ );
645
+ try {
646
+ await page.goto('/commerce', { waitUntil: 'load' });
647
+ const productLink = page.getByRole('link', { name: 'Pour-Over Coffee Set', exact: true });
648
+ await expect(productLink).toBeVisible();
649
+ await productLink.click();
650
+ await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/);
651
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
652
+ await expect(page.getByRole('heading', { name: 'Pour-Over Coffee Set' })).toBeVisible();
653
+ await expect(page.getByText('COF-SET-01')).toBeVisible();
654
+ const addToCartButton = page.getByRole('button', { name: 'Add to cart', exact: true });
655
+ await expect(addToCartButton).toBeEnabled();
656
+ const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items');
657
+ await addToCartButton.click();
658
+ expect((await addResponse).ok()).toBeTruthy();
659
+ await page.getByRole('link', { name: /^Cart/ }).click();
660
+ await page.getByRole('link', { name: 'Checkout', exact: true }).click();
661
+ const addressChoice = page.getByRole('radio', { name: new RegExp(fixture.recipientName) });
662
+ await expect(addressChoice).toBeVisible();
663
+ await addressChoice.check();
664
+ await page.getByRole('radio', { name: 'No coupon', exact: true }).check();
665
+ const checkoutResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/checkout');
666
+ await page.getByRole('button', { name: 'Create order', exact: true }).click();
667
+ const checkoutResponseValue = await checkoutResponse;
668
+ expect(checkoutResponseValue.ok()).toBeTruthy();
669
+ const checkout = (await checkoutResponseValue.json()).data;
670
+ expectTableIdentity(checkout.orderId);
671
+ expectTableIdentity(checkout.paymentSessionId);
672
+ const startResponse = waitForApiResponse(
673
+ page,
674
+ 'POST',
675
+ new RegExp(`/api/pay/payment-session/${checkout.paymentSessionId}/start$`),
676
+ );
677
+ await page.getByRole('button', { name: 'Start payment', exact: true }).click();
678
+ expect((await startResponse).ok()).toBeTruthy();
679
+ await expect(page.getByRole('heading', { name: 'Mock payment simulator' })).toBeVisible();
680
+ const completeResponse = waitForApiResponse(
681
+ page,
682
+ 'POST',
683
+ new RegExp(`/api/pay/mock/payment-session/${checkout.paymentSessionId}/complete$`),
684
+ );
685
+ await page.getByRole('button', { name: 'Payment succeeded', exact: true }).click();
686
+ expect((await completeResponse).ok()).toBeTruthy();
687
+ await expect(page).toHaveURL(new RegExp(`/commerce/order/${checkout.orderId}(?:/|$)`), {
688
+ timeout: 40_000,
689
+ });
690
+
691
+ const callbackPath = `/commerce/payment/${checkout.paymentSessionId}/${checkout.orderId}?providerResult=return`;
692
+ await page.evaluate(() => {
693
+ localStorage.clear();
694
+ sessionStorage.clear();
695
+ });
696
+ const callbackResponse = await context.request.get(callbackPath);
697
+ expect(callbackResponse.ok()).toBeTruthy();
698
+ const callbackHtml = (await callbackResponse.text()).toLowerCase();
699
+ expect(callbackHtml).not.toContain('data-zova-hydrated');
700
+ expect(callbackHtml).not.toContain('mock payment simulator');
701
+
702
+ const reconcilePath = new RegExp(
703
+ `/api/pay/payment-session/${checkout.paymentSessionId}/reconcile$`,
704
+ );
705
+ const paymentViewPath = new RegExp(`/api/pay/payment-session/${checkout.paymentSessionId}$`);
706
+ const orderViewPath = new RegExp(`/api/commerce/trade/order/viewMine/${checkout.orderId}$`);
707
+ let reconcileCount = 0;
708
+ let paymentViewCount = 0;
709
+ let orderViewCount = 0;
710
+ page.on('request', request => {
711
+ const url = new URL(request.url());
712
+ if (request.method() !== 'GET') {
713
+ if (request.method() === 'POST' && reconcilePath.test(url.pathname)) reconcileCount++;
714
+ return;
715
+ }
716
+ if (paymentViewPath.test(url.pathname)) paymentViewCount++;
717
+ if (orderViewPath.test(url.pathname)) orderViewCount++;
718
+ });
719
+ const reconcileResponse = waitForApiResponse(page, 'POST', reconcilePath);
720
+ const documentResponse = await page.goto(callbackPath, { waitUntil: 'load' });
721
+ expect(documentResponse?.ok()).toBeTruthy();
722
+ expect((await reconcileResponse).ok()).toBeTruthy();
723
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
724
+ await expect(page).toHaveURL(new RegExp(`/commerce/order/${checkout.orderId}(?:/|$)`));
725
+ expect(reconcileCount).toBe(1);
726
+ expect(paymentViewCount).toBeLessThanOrEqual(10);
727
+ expect(orderViewCount).toBeLessThanOrEqual(5);
728
+ await expect(page.getByText('paid · $45.99')).toBeVisible();
729
+ await expect(page.getByRole('alert')).toHaveCount(0);
730
+ expect(pageErrors).toEqual([]);
731
+ } finally {
732
+ await context.close().catch(() => {});
733
+ }
734
+ },
735
+ );
736
+
737
+ test(
738
+ 'PayPal browser cancel: customer can continue payment or open an awaiting order',
739
+ { tag: ['@web', '@flow', '@payment'] },
740
+ async ({ browser, request }, testInfo) => {
741
+ test.setTimeout(60_000);
742
+ const { context, fixture, page, pageErrors } = await createAddressThroughCustomerPage(
743
+ browser,
744
+ request,
745
+ testInfo,
746
+ );
747
+ try {
748
+ await page.goto('/commerce', { waitUntil: 'load' });
749
+ const productLink = page.getByRole('link', { name: 'Pour-Over Coffee Set', exact: true });
750
+ await expect(productLink).toBeVisible();
751
+ await productLink.click();
752
+ await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/);
753
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
754
+ await expect(page.getByRole('heading', { name: 'Pour-Over Coffee Set' })).toBeVisible();
755
+ await expect(page.getByText('COF-SET-01')).toBeVisible();
756
+ const addToCartButton = page.getByRole('button', { name: 'Add to cart', exact: true });
757
+ await expect(addToCartButton).toBeEnabled();
758
+ const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items');
759
+ await addToCartButton.click();
760
+ expect((await addResponse).ok()).toBeTruthy();
761
+ await page.getByRole('link', { name: /^Cart/ }).click();
762
+ await page.getByRole('link', { name: 'Checkout', exact: true }).click();
763
+ const addressChoice = page.getByRole('radio', { name: new RegExp(fixture.recipientName) });
764
+ await expect(addressChoice).toBeVisible();
765
+ await addressChoice.check();
766
+ await page.getByRole('radio', { name: 'No coupon', exact: true }).check();
767
+ const checkoutResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/checkout');
768
+ await page.getByRole('button', { name: 'Create order', exact: true }).click();
769
+ const checkoutResponseValue = await checkoutResponse;
770
+ expect(checkoutResponseValue.ok()).toBeTruthy();
771
+ const checkout = (await checkoutResponseValue.json()).data;
772
+ expectTableIdentity(checkout.orderId);
773
+ expectTableIdentity(checkout.paymentSessionId);
774
+ expect(checkout.state).toBe('awaiting_payment');
775
+
776
+ const callbackPath = `/commerce/payment/${checkout.paymentSessionId}/${checkout.orderId}?providerResult=cancel`;
777
+ const paymentSessionPath = new RegExp(
778
+ `/api/pay/payment-session/${checkout.paymentSessionId}$`,
779
+ );
780
+ const reconcilePath = new RegExp(
781
+ `/api/pay/payment-session/${checkout.paymentSessionId}/reconcile$`,
782
+ );
783
+ const requiresActionSession = {
784
+ id: checkout.paymentSessionId,
785
+ state: 'requires_action',
786
+ providerName: 'pay-paypal:paypal',
787
+ nextAction: { kind: 'redirect', url: 'https://example.test/paypal-approval' },
788
+ amountMinor: 4599,
789
+ currency: 'USD',
790
+ };
791
+ const callbackResponse = await context.request.get(callbackPath);
792
+ expect(callbackResponse.ok()).toBeTruthy();
793
+ const callbackHtml = await callbackResponse.text();
794
+ expect(callbackHtml).toContain('Start payment');
795
+ expect(callbackHtml).not.toContain(
796
+ 'You returned from the payment provider without completing payment.',
797
+ );
798
+
799
+ let reconcileCount = 0;
800
+ let reconcileSeen = false;
801
+ let resolvePaymentSessionRefresh: () => void;
802
+ const paymentSessionRefresh = new Promise<void>(resolve => {
803
+ resolvePaymentSessionRefresh = resolve;
804
+ });
805
+ await page.route(
806
+ url => paymentSessionPath.test(url.pathname) || reconcilePath.test(url.pathname),
807
+ async route => {
808
+ const request = route.request();
809
+ const pathname = new URL(request.url()).pathname;
810
+ if (request.method() === 'POST' && reconcilePath.test(pathname)) {
811
+ reconcileCount++;
812
+ reconcileSeen = true;
813
+ } else if (
814
+ reconcileSeen &&
815
+ request.method() === 'GET' &&
816
+ paymentSessionPath.test(pathname)
817
+ ) {
818
+ resolvePaymentSessionRefresh();
819
+ }
820
+ await route.fulfill({
821
+ contentType: 'application/json',
822
+ body: JSON.stringify({
823
+ code: 0,
824
+ message: '',
825
+ data: requiresActionSession,
826
+ }),
827
+ });
828
+ },
829
+ );
830
+
831
+ const reconcileResponse = waitForApiResponse(page, 'POST', reconcilePath);
832
+ await page.goto(callbackPath, { waitUntil: 'load' });
833
+ expect((await reconcileResponse).ok()).toBeTruthy();
834
+ await paymentSessionRefresh;
835
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
836
+ await expect(
837
+ page.getByText(
838
+ 'You returned from the payment provider without completing payment. Your payment status has been refreshed. You can continue to payment or open your order.',
839
+ ),
840
+ ).toBeVisible();
841
+ await expect(page.getByRole('button', { name: 'Start payment', exact: true })).toHaveCount(0);
842
+ await expect(
843
+ page.getByRole('button', { name: 'Continue to payment', exact: true }),
844
+ ).toBeVisible();
845
+ await expect(page.getByRole('button', { name: 'Open order', exact: true })).toBeVisible();
846
+ await expect(
847
+ page.getByText('Waiting for verified provider confirmation and order settlement…'),
848
+ ).toHaveCount(0);
849
+ await expect(page.getByRole('alert')).toHaveCount(0);
850
+ expect(reconcileCount).toBe(1);
851
+
852
+ await page.getByRole('button', { name: 'Open order', exact: true }).click();
853
+ await expect(page).toHaveURL(new RegExp(`/commerce/order/${checkout.orderId}(?:/|$)`));
854
+ await expect(page.getByText('awaiting_payment · $45.99')).toBeVisible();
855
+ expect(pageErrors).toEqual([]);
856
+ } finally {
857
+ await context.close().catch(() => {});
858
+ }
859
+ },
860
+ );
861
+
548
862
  test(
549
863
  'Payment cancellation: customer observes the cancelled order after verified provider confirmation',
550
864
  { tag: ['@web', '@flow', '@payment'] },
@@ -701,7 +1015,7 @@ test(
701
1015
  );
702
1016
  await orderRow.getByRole('button', { name: 'Approve refund', exact: true }).click();
703
1017
  expect((await approveResponse).ok()).toBeTruthy();
704
- await expect(orderRow).toContainText('refund_approved');
1018
+ await expect(orderRow).toContainText('Refund approved');
705
1019
  const executeResponse = waitForApiResponse(
706
1020
  adminPage,
707
1021
  'POST',
@@ -735,8 +1049,8 @@ test(
735
1049
  },
736
1050
  { timeout: 40_000 },
737
1051
  )
738
- .toContain('refunded');
739
- await expect(await getAdminOrderRow(adminPage, checkout.orderId)).toContainText('refunded');
1052
+ .toContain('Refunded');
1053
+ await expect(await getAdminOrderRow(adminPage, checkout.orderId)).toContainText('Refunded');
740
1054
  expect(adminPageErrors).toEqual([]);
741
1055
  } finally {
742
1056
  await adminContext.close().catch(() => {});
@@ -761,7 +1075,7 @@ test(
761
1075
  );
762
1076
 
763
1077
  test(
764
- 'ATP-ADDR-01: systemAdmin inspects Address Resource without mutation controls',
1078
+ 'ATP-SPC-05: systemAdmin inspects readonly Address Resource without mutation controls',
765
1079
  { tag: ['@admin', '@flow', '@address'] },
766
1080
  async ({ browser, request }, testInfo) => {
767
1081
  test.setTimeout(60_000);
@@ -774,7 +1088,7 @@ test(
774
1088
  const url = new URL(request.url());
775
1089
  if (
776
1090
  url.pathname.startsWith(addressActionPath) &&
777
- ['POST', 'PATCH', 'DELETE'].includes(request.method())
1091
+ ['POST', 'PATCH', 'PUT', 'DELETE'].includes(request.method())
778
1092
  ) {
779
1093
  addressMutationMethods.push(request.method());
780
1094
  }
@@ -789,6 +1103,14 @@ test(
789
1103
  await expect(adminPage).toHaveURL(addressResourceUrl);
790
1104
  const fixtureRow = adminPage.getByRole('row', { name: customer.fixture.recipientName });
791
1105
  await expect(fixtureRow).toBeVisible();
1106
+ await expect(fixtureRow).toContainText(customer.fixture.recipientName);
1107
+ await expect(fixtureRow).toContainText(customer.fixture.phone);
1108
+ await expect(fixtureRow).toContainText(customer.fixture.countryCode);
1109
+ await expect(fixtureRow).toContainText(customer.fixture.city);
1110
+ await expect(fixtureRow).not.toContainText(customer.fixture.region);
1111
+ await expect(fixtureRow).not.toContainText(customer.fixture.postalCode);
1112
+ await expect(fixtureRow).not.toContainText(customer.fixture.addressLine1);
1113
+ await expect(fixtureRow).not.toContainText(customer.fixture.addressLine2);
792
1114
  await expect(adminPage.getByRole('button', { name: 'Create', exact: true })).toHaveCount(0);
793
1115
  await expect(
794
1116
  adminPage.locator('a[href*="/rest/resource/"]').filter({ hasText: 'Create' }),
@@ -796,6 +1118,7 @@ test(
796
1118
 
797
1119
  const addressId = await fixtureRow.getByRole('cell').first().textContent();
798
1120
  expect(addressId).toMatch(/^\d+$/);
1121
+ await expect(fixtureRow.getByRole('link', { name: addressId! })).toHaveCount(1);
799
1122
  await adminPage.goto(`/commerce-admin/rest/resource/commerce-member%3Aaddress/${addressId}`, {
800
1123
  waitUntil: 'load',
801
1124
  });
@@ -1101,6 +1424,170 @@ test(
1101
1424
  },
1102
1425
  );
1103
1426
 
1427
+ test(
1428
+ 'ATP-SPC-02: SKU renders semantic Admin currency and lifecycle controls',
1429
+ { tag: ['@admin', '@flow', '@sku'] },
1430
+ async ({ browser }, testInfo) => {
1431
+ test.setTimeout(60_000);
1432
+ const suffix = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`;
1433
+ const categoryName = `E2E SKU Category ${suffix}`;
1434
+ const productTitle = `E2E SKU Product ${suffix}`;
1435
+ const skuCode = `E2E-SKU-${suffix}`;
1436
+ const adminContext = await browser.newContext();
1437
+ const adminPage = await adminContext.newPage();
1438
+ const adminPageErrors = collectPageErrors(adminPage);
1439
+ const skuCreatePath = '/commerce-admin/rest/resource/commerce-catalog%3Asku/create';
1440
+ const skuListPath = '/commerce-admin/rest/resource/commerce-catalog%3Asku';
1441
+ const categoryActionPath = '/api/commerce/catalog/category';
1442
+ const productActionPath = '/api/commerce/catalog/product';
1443
+ const skuActionPath = '/api/commerce/catalog/sku';
1444
+ let categoryId: number | string | undefined;
1445
+ let productId: number | string | undefined;
1446
+ let skuId: number | string | undefined;
1447
+ let headers: { Authorization: string } | undefined;
1448
+ try {
1449
+ await adminPage.setViewportSize({ width: 1440, height: 900 });
1450
+ await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
1451
+ const accessToken = (await adminContext.cookies()).find(
1452
+ cookie => cookie.name === 'token',
1453
+ )?.value;
1454
+ expect(accessToken).toBeTruthy();
1455
+ headers = { Authorization: `Bearer ${accessToken}` };
1456
+
1457
+ const categoryResponse = await adminPage.request.post(categoryActionPath, {
1458
+ data: { name: categoryName, published: true },
1459
+ headers,
1460
+ });
1461
+ expect(categoryResponse.ok()).toBeTruthy();
1462
+ categoryId = (await categoryResponse.json()).data;
1463
+ expectTableIdentity(categoryId);
1464
+
1465
+ const productResponse = await adminPage.request.post(productActionPath, {
1466
+ data: { categoryId, title: productTitle, published: true },
1467
+ headers,
1468
+ });
1469
+ expect(productResponse.ok()).toBeTruthy();
1470
+ productId = (await productResponse.json()).data;
1471
+ expectTableIdentity(productId);
1472
+
1473
+ await adminPage.goto(skuCreatePath, { waitUntil: 'load' });
1474
+ const productField = adminPage.getByRole('group', { name: 'Product' });
1475
+ await expect(productField).toBeVisible();
1476
+ await expect(productField.getByRole('combobox')).toHaveCount(0);
1477
+ await expect(productField.getByRole('textbox')).toBeVisible();
1478
+ const lifecycle = adminPage
1479
+ .getByRole('group', { name: 'SKU lifecycle' })
1480
+ .getByRole('combobox');
1481
+ await expect(lifecycle).toBeVisible();
1482
+ await expect(lifecycle.locator('option')).toHaveText([
1483
+ '',
1484
+ 'Draft',
1485
+ 'Active',
1486
+ 'Inactive',
1487
+ 'Archived',
1488
+ ]);
1489
+ await expect(adminPage.getByRole('group', { name: 'Price (cents)' })).toBeVisible();
1490
+
1491
+ const skuResponse = await adminPage.request.post(skuActionPath, {
1492
+ data: { code: skuCode, productId, priceCents: 1234, lifecycle: 'draft' },
1493
+ headers,
1494
+ });
1495
+ expect(skuResponse.ok()).toBeTruthy();
1496
+ skuId = (await skuResponse.json()).data;
1497
+ expectTableIdentity(skuId);
1498
+
1499
+ await adminPage.goto(skuListPath, { waitUntil: 'load' });
1500
+ const skuRow = adminPage.getByRole('row', { name: new RegExp(skuCode) });
1501
+ await expect(skuRow).toBeVisible();
1502
+ await expect(skuRow.getByText('12.34', { exact: true })).toBeVisible();
1503
+ await expect(skuRow.getByText('Draft', { exact: true })).toBeVisible();
1504
+ await expect(skuRow.getByRole('link', { name: skuCode, exact: true })).toBeVisible();
1505
+ await expect(
1506
+ adminPage.locator('section').getByText('SKU code', { exact: true }),
1507
+ ).toBeVisible();
1508
+ await expect(adminPage.getByText('Created At', { exact: true })).toHaveCount(1);
1509
+ await expect(adminPage.getByRole('group', { name: 'Attributes' })).toHaveCount(0);
1510
+
1511
+ await adminPage.goto(`${skuListPath}/${skuId}`, { waitUntil: 'load' });
1512
+ await expect(
1513
+ adminPage.getByRole('group', { name: 'SKU lifecycle' }).getByRole('textbox'),
1514
+ ).toHaveValue('Draft');
1515
+ await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0);
1516
+ await expect(adminPage.getByRole('button', { name: 'Back', exact: true })).toBeVisible();
1517
+ expect(adminPageErrors).toEqual([]);
1518
+ } finally {
1519
+ if (skuId && headers) {
1520
+ const response = await adminPage.request.delete(`${skuActionPath}/${skuId}`, { headers });
1521
+ expect(response.ok()).toBeTruthy();
1522
+ }
1523
+ if (productId && headers) {
1524
+ const response = await adminPage.request.delete(`${productActionPath}/${productId}`, {
1525
+ headers,
1526
+ });
1527
+ expect(response.ok()).toBeTruthy();
1528
+ }
1529
+ if (categoryId && headers) {
1530
+ const response = await adminPage.request.delete(`${categoryActionPath}/${categoryId}`, {
1531
+ headers,
1532
+ });
1533
+ expect(response.ok()).toBeTruthy();
1534
+ }
1535
+ await adminContext.close().catch(() => {});
1536
+ }
1537
+ },
1538
+ );
1539
+
1540
+ test(
1541
+ 'ATP-SPC-04: Stock Admin resources remain readonly and mutation-free',
1542
+ { tag: ['@admin', '@flow', '@stock'] },
1543
+ async ({ browser }) => {
1544
+ test.setTimeout(60_000);
1545
+ const adminContext = await browser.newContext();
1546
+ const adminPage = await adminContext.newPage();
1547
+ const adminPageErrors = collectPageErrors(adminPage);
1548
+ const mutationMethods: string[] = [];
1549
+ adminPage.on('request', request => {
1550
+ const url = new URL(request.url());
1551
+ if (
1552
+ url.pathname.includes('/api/commerce/trade/stock') &&
1553
+ ['POST', 'PATCH', 'PUT', 'DELETE'].includes(request.method())
1554
+ ) {
1555
+ mutationMethods.push(request.method());
1556
+ }
1557
+ });
1558
+
1559
+ try {
1560
+ await adminPage.setViewportSize({ width: 1440, height: 900 });
1561
+ await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
1562
+
1563
+ for (const [resourcePath, title] of [
1564
+ ['commerce-trade%3AstockBalance', 'Stock Balance'],
1565
+ ['commerce-trade%3AstockAudit', 'Stock Audit'],
1566
+ ] as const) {
1567
+ await adminPage.goto(`/commerce-admin/rest/resource/${resourcePath}`, {
1568
+ waitUntil: 'load',
1569
+ });
1570
+ await expect(adminPage).toHaveURL(
1571
+ new RegExp(
1572
+ `/commerce-admin/rest/resource/commerce-trade(?:%3A|:|%253A)${title === 'Stock Balance' ? 'stockBalance' : 'stockAudit'}(?:[/?#]|$)`,
1573
+ ),
1574
+ );
1575
+ await expect(adminPage.getByText(title, { exact: true }).first()).toBeVisible();
1576
+ await expect(adminPage.getByRole('button', { name: 'Create', exact: true })).toHaveCount(0);
1577
+ await expect(
1578
+ adminPage.locator('a[href*="/rest/resource/"]').filter({ hasText: 'Create' }),
1579
+ ).toHaveCount(0);
1580
+ await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0);
1581
+ }
1582
+
1583
+ expect(mutationMethods).toEqual([]);
1584
+ expect(adminPageErrors).toEqual([]);
1585
+ } finally {
1586
+ await adminContext.close().catch(() => {});
1587
+ }
1588
+ },
1589
+ );
1590
+
1104
1591
  test(
1105
1592
  'Commerce Cart: anonymous browser is redirected to login',
1106
1593
  { tag: ['@web', '@cart'] },