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
@@ -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');
@@ -292,15 +303,18 @@ test(
292
303
  );
293
304
 
294
305
  test(
295
- 'Commerce Address: cookie-disabled SSR renders a neutral protected entry',
306
+ 'Commerce Address: session SSR protects an anonymous document request',
296
307
  { tag: ['@web', '@cart'] },
297
308
  async ({ page, request }) => {
298
309
  const path = '/commerce/address';
299
310
  const routePath = '/address';
300
311
  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');
312
+ expect(response.status()).toBe(302);
313
+ expect(response.headers()['cache-control']).toBe('private, no-store');
314
+ const html = await response.text();
315
+ expect(html).not.toContain('ssr-body-ready-observer');
316
+ expect(html).not.toContain('__leftDrawerOpenJS');
317
+ expect(response.headers().location).toMatch(/^\/commerce\/login\?(?:.*&)?returnTo=/);
304
318
 
305
319
  const pageErrors = collectPageErrors(page);
306
320
  const documentResponse = await page.goto(path, { waitUntil: 'load' });
@@ -324,6 +338,33 @@ test(
324
338
  testInfo,
325
339
  );
326
340
  try {
341
+ const addressResponse = await context.request.get('/commerce/address');
342
+ expect(addressResponse.ok()).toBeTruthy();
343
+ expect(addressResponse.headers()['cache-control']).toBe('private, no-store');
344
+ const addressHtml = await addressResponse.text();
345
+ expect(addressHtml.toLowerCase()).not.toContain('data-zova-hydrated');
346
+ expect(addressHtml).not.toContain('ssr-body-ready-observer');
347
+ expect(addressHtml).not.toContain('__leftDrawerOpenJS');
348
+ expect(addressHtml).toContain(fixture.recipientName);
349
+ expect(addressHtml).toContain(fixture.addressLine1);
350
+
351
+ const coldPage = await context.newPage();
352
+ try {
353
+ const coldPageErrors = collectPageErrors(coldPage);
354
+ const coldResponse = await coldPage.goto('/commerce/address', { waitUntil: 'load' });
355
+ expect(coldResponse?.ok()).toBeTruthy();
356
+ await expect(coldPage.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
357
+ await expect(coldPage.getByRole('heading', { name: 'Addresses' })).toBeVisible();
358
+ const coldCard = coldPage
359
+ .locator('article')
360
+ .filter({ has: coldPage.getByRole('heading', { name: fixture.recipientName }) });
361
+ await expect(coldCard).toContainText(fixture.addressLine1);
362
+ await expect(coldPage.getByRole('alert')).toHaveCount(0);
363
+ expect(coldPageErrors).toEqual([]);
364
+ } finally {
365
+ await coldPage.close();
366
+ }
367
+
327
368
  const card = page
328
369
  .locator('article')
329
370
  .filter({ has: page.getByRole('heading', { name: fixture.recipientName }) });
@@ -483,6 +524,27 @@ test(
483
524
  );
484
525
  const orderRow = await getAdminOrderRow(adminPage, checkout.orderId);
485
526
  await expect(orderRow).toBeVisible();
527
+ await expect(orderRow.locator('td')).toHaveCount(6);
528
+ await expect(orderRow).toContainText('Paid');
529
+ await expect(orderRow).toContainText('45.99');
530
+ await expect(orderRow).not.toContainText(fixture.recipientName);
531
+ const orderViewResponse = waitForApiResponse(
532
+ adminPage,
533
+ 'GET',
534
+ `/api/commerce/trade/order/${checkout.orderId}`,
535
+ );
536
+ await orderRow.getByRole('link', { name: String(checkout.orderId), exact: true }).click();
537
+ expect((await orderViewResponse).ok()).toBeTruthy();
538
+ await expect(adminPage.getByText('Money summary').first()).toBeVisible();
539
+ await expect(adminPage.getByText('Delivery address').first()).toBeVisible();
540
+ await expect(adminPage.getByText('Purchased lines').first()).toBeVisible();
541
+ await expect(adminPage.getByText('addressSnapshot').first()).toBeVisible();
542
+ await expect(adminPage.getByText('couponSnapshot').first()).toBeVisible();
543
+ await expect(adminPage.getByText('Pour-Over Coffee Set')).toBeVisible();
544
+ await expect(adminPage.getByText('COF-SET-01')).toBeVisible();
545
+ await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0);
546
+ await adminPage.getByRole('button', { name: 'Back', exact: true }).click();
547
+ await expect(orderRow).toBeVisible();
486
548
  const carrierInput = orderRow.getByPlaceholder('Carrier');
487
549
  const trackingNumberInput = orderRow.getByPlaceholder('Tracking number');
488
550
  const confirmationCheckbox = orderRow.getByRole('checkbox');
@@ -499,7 +561,7 @@ test(
499
561
  );
500
562
  await orderRow.getByRole('button', { name: 'Ship order', exact: true }).click();
501
563
  expect((await shipmentResponse).ok()).toBeTruthy();
502
- await expect(orderRow).toContainText('shipped');
564
+ await expect(orderRow).toContainText('Shipped');
503
565
  expect(adminPageErrors).toEqual([]);
504
566
  } finally {
505
567
  await adminContext.close().catch(() => {});
@@ -545,6 +607,233 @@ test(
545
607
  },
546
608
  );
547
609
 
610
+ test(
611
+ 'Payment callback: authenticated direct route reconciles after hydration',
612
+ { tag: ['@web', '@flow', '@payment'] },
613
+ async ({ browser, request }, testInfo) => {
614
+ test.setTimeout(60_000);
615
+ const { context, fixture, page, pageErrors } = await createAddressThroughCustomerPage(
616
+ browser,
617
+ request,
618
+ testInfo,
619
+ );
620
+ try {
621
+ await page.goto('/commerce', { waitUntil: 'load' });
622
+ const productLink = page.getByRole('link', { name: 'Pour-Over Coffee Set', exact: true });
623
+ await expect(productLink).toBeVisible();
624
+ await productLink.click();
625
+ await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/);
626
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
627
+ await expect(page.getByRole('heading', { name: 'Pour-Over Coffee Set' })).toBeVisible();
628
+ await expect(page.getByText('COF-SET-01')).toBeVisible();
629
+ const addToCartButton = page.getByRole('button', { name: 'Add to cart', exact: true });
630
+ await expect(addToCartButton).toBeEnabled();
631
+ const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items');
632
+ await addToCartButton.click();
633
+ expect((await addResponse).ok()).toBeTruthy();
634
+ await page.getByRole('link', { name: /^Cart/ }).click();
635
+ await page.getByRole('link', { name: 'Checkout', exact: true }).click();
636
+ const addressChoice = page.getByRole('radio', { name: new RegExp(fixture.recipientName) });
637
+ await expect(addressChoice).toBeVisible();
638
+ await addressChoice.check();
639
+ await page.getByRole('radio', { name: 'No coupon', exact: true }).check();
640
+ const checkoutResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/checkout');
641
+ await page.getByRole('button', { name: 'Create order', exact: true }).click();
642
+ const checkoutResponseValue = await checkoutResponse;
643
+ expect(checkoutResponseValue.ok()).toBeTruthy();
644
+ const checkout = (await checkoutResponseValue.json()).data;
645
+ expectTableIdentity(checkout.orderId);
646
+ expectTableIdentity(checkout.paymentSessionId);
647
+ const startResponse = waitForApiResponse(
648
+ page,
649
+ 'POST',
650
+ new RegExp(`/api/pay/payment-session/${checkout.paymentSessionId}/start$`),
651
+ );
652
+ await page.getByRole('button', { name: 'Start payment', exact: true }).click();
653
+ expect((await startResponse).ok()).toBeTruthy();
654
+ await expect(page.getByRole('heading', { name: 'Mock payment simulator' })).toBeVisible();
655
+ const completeResponse = waitForApiResponse(
656
+ page,
657
+ 'POST',
658
+ new RegExp(`/api/pay/mock/payment-session/${checkout.paymentSessionId}/complete$`),
659
+ );
660
+ await page.getByRole('button', { name: 'Payment succeeded', exact: true }).click();
661
+ expect((await completeResponse).ok()).toBeTruthy();
662
+ await expect(page).toHaveURL(new RegExp(`/commerce/order/${checkout.orderId}(?:/|$)`), {
663
+ timeout: 40_000,
664
+ });
665
+
666
+ const callbackPath = `/commerce/payment/${checkout.paymentSessionId}/${checkout.orderId}?providerResult=return`;
667
+ await page.evaluate(() => {
668
+ localStorage.clear();
669
+ sessionStorage.clear();
670
+ });
671
+ const callbackResponse = await context.request.get(callbackPath);
672
+ expect(callbackResponse.ok()).toBeTruthy();
673
+ const callbackHtml = (await callbackResponse.text()).toLowerCase();
674
+ expect(callbackHtml).not.toContain('data-zova-hydrated');
675
+ expect(callbackHtml).not.toContain('mock payment simulator');
676
+
677
+ const reconcilePath = new RegExp(
678
+ `/api/pay/payment-session/${checkout.paymentSessionId}/reconcile$`,
679
+ );
680
+ const paymentViewPath = new RegExp(`/api/pay/payment-session/${checkout.paymentSessionId}$`);
681
+ const orderViewPath = new RegExp(`/api/commerce/trade/order/viewMine/${checkout.orderId}$`);
682
+ let reconcileCount = 0;
683
+ let paymentViewCount = 0;
684
+ let orderViewCount = 0;
685
+ page.on('request', request => {
686
+ const url = new URL(request.url());
687
+ if (request.method() !== 'GET') {
688
+ if (request.method() === 'POST' && reconcilePath.test(url.pathname)) reconcileCount++;
689
+ return;
690
+ }
691
+ if (paymentViewPath.test(url.pathname)) paymentViewCount++;
692
+ if (orderViewPath.test(url.pathname)) orderViewCount++;
693
+ });
694
+ const reconcileResponse = waitForApiResponse(page, 'POST', reconcilePath);
695
+ const documentResponse = await page.goto(callbackPath, { waitUntil: 'load' });
696
+ expect(documentResponse?.ok()).toBeTruthy();
697
+ expect((await reconcileResponse).ok()).toBeTruthy();
698
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
699
+ await expect(page).toHaveURL(new RegExp(`/commerce/order/${checkout.orderId}(?:/|$)`));
700
+ expect(reconcileCount).toBe(1);
701
+ expect(paymentViewCount).toBeLessThanOrEqual(10);
702
+ expect(orderViewCount).toBeLessThanOrEqual(5);
703
+ await expect(page.getByText('paid · $45.99')).toBeVisible();
704
+ await expect(page.getByRole('alert')).toHaveCount(0);
705
+ expect(pageErrors).toEqual([]);
706
+ } finally {
707
+ await context.close().catch(() => {});
708
+ }
709
+ },
710
+ );
711
+
712
+ test(
713
+ 'PayPal browser cancel: customer can continue payment or open an awaiting order',
714
+ { tag: ['@web', '@flow', '@payment'] },
715
+ async ({ browser, request }, testInfo) => {
716
+ test.setTimeout(60_000);
717
+ const { context, fixture, page, pageErrors } = await createAddressThroughCustomerPage(
718
+ browser,
719
+ request,
720
+ testInfo,
721
+ );
722
+ try {
723
+ await page.goto('/commerce', { waitUntil: 'load' });
724
+ const productLink = page.getByRole('link', { name: 'Pour-Over Coffee Set', exact: true });
725
+ await expect(productLink).toBeVisible();
726
+ await productLink.click();
727
+ await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/);
728
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
729
+ await expect(page.getByRole('heading', { name: 'Pour-Over Coffee Set' })).toBeVisible();
730
+ await expect(page.getByText('COF-SET-01')).toBeVisible();
731
+ const addToCartButton = page.getByRole('button', { name: 'Add to cart', exact: true });
732
+ await expect(addToCartButton).toBeEnabled();
733
+ const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items');
734
+ await addToCartButton.click();
735
+ expect((await addResponse).ok()).toBeTruthy();
736
+ await page.getByRole('link', { name: /^Cart/ }).click();
737
+ await page.getByRole('link', { name: 'Checkout', exact: true }).click();
738
+ const addressChoice = page.getByRole('radio', { name: new RegExp(fixture.recipientName) });
739
+ await expect(addressChoice).toBeVisible();
740
+ await addressChoice.check();
741
+ await page.getByRole('radio', { name: 'No coupon', exact: true }).check();
742
+ const checkoutResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/checkout');
743
+ await page.getByRole('button', { name: 'Create order', exact: true }).click();
744
+ const checkoutResponseValue = await checkoutResponse;
745
+ expect(checkoutResponseValue.ok()).toBeTruthy();
746
+ const checkout = (await checkoutResponseValue.json()).data;
747
+ expectTableIdentity(checkout.orderId);
748
+ expectTableIdentity(checkout.paymentSessionId);
749
+ expect(checkout.state).toBe('awaiting_payment');
750
+
751
+ const callbackPath = `/commerce/payment/${checkout.paymentSessionId}/${checkout.orderId}?providerResult=cancel`;
752
+ const paymentSessionPath = new RegExp(
753
+ `/api/pay/payment-session/${checkout.paymentSessionId}$`,
754
+ );
755
+ const reconcilePath = new RegExp(
756
+ `/api/pay/payment-session/${checkout.paymentSessionId}/reconcile$`,
757
+ );
758
+ const requiresActionSession = {
759
+ id: checkout.paymentSessionId,
760
+ state: 'requires_action',
761
+ providerName: 'pay-paypal:paypal',
762
+ nextAction: { kind: 'redirect', url: 'https://example.test/paypal-approval' },
763
+ amountMinor: 4599,
764
+ currency: 'USD',
765
+ };
766
+ const callbackResponse = await context.request.get(callbackPath);
767
+ expect(callbackResponse.ok()).toBeTruthy();
768
+ const callbackHtml = await callbackResponse.text();
769
+ expect(callbackHtml).toContain('Start payment');
770
+ expect(callbackHtml).not.toContain(
771
+ 'You returned from the payment provider without completing payment.',
772
+ );
773
+
774
+ let reconcileCount = 0;
775
+ let reconcileSeen = false;
776
+ let resolvePaymentSessionRefresh: () => void;
777
+ const paymentSessionRefresh = new Promise<void>(resolve => {
778
+ resolvePaymentSessionRefresh = resolve;
779
+ });
780
+ await page.route(
781
+ url => paymentSessionPath.test(url.pathname) || reconcilePath.test(url.pathname),
782
+ async route => {
783
+ const request = route.request();
784
+ const pathname = new URL(request.url()).pathname;
785
+ if (request.method() === 'POST' && reconcilePath.test(pathname)) {
786
+ reconcileCount++;
787
+ reconcileSeen = true;
788
+ } else if (
789
+ reconcileSeen &&
790
+ request.method() === 'GET' &&
791
+ paymentSessionPath.test(pathname)
792
+ ) {
793
+ resolvePaymentSessionRefresh();
794
+ }
795
+ await route.fulfill({
796
+ contentType: 'application/json',
797
+ body: JSON.stringify({
798
+ code: 0,
799
+ message: '',
800
+ data: requiresActionSession,
801
+ }),
802
+ });
803
+ },
804
+ );
805
+
806
+ const reconcileResponse = waitForApiResponse(page, 'POST', reconcilePath);
807
+ await page.goto(callbackPath, { waitUntil: 'load' });
808
+ expect((await reconcileResponse).ok()).toBeTruthy();
809
+ await paymentSessionRefresh;
810
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
811
+ await expect(
812
+ page.getByText(
813
+ 'You returned from the payment provider without completing payment. Your payment status has been refreshed. You can continue to payment or open your order.',
814
+ ),
815
+ ).toBeVisible();
816
+ await expect(page.getByRole('button', { name: 'Start payment', exact: true })).toHaveCount(0);
817
+ await expect(
818
+ page.getByRole('button', { name: 'Continue to payment', exact: true }),
819
+ ).toBeVisible();
820
+ await expect(page.getByRole('button', { name: 'Open order', exact: true })).toBeVisible();
821
+ await expect(
822
+ page.getByText('Waiting for verified provider confirmation and order settlement…'),
823
+ ).toHaveCount(0);
824
+ await expect(page.getByRole('alert')).toHaveCount(0);
825
+ expect(reconcileCount).toBe(1);
826
+
827
+ await page.getByRole('button', { name: 'Open order', exact: true }).click();
828
+ await expect(page).toHaveURL(new RegExp(`/commerce/order/${checkout.orderId}(?:/|$)`));
829
+ await expect(page.getByText('awaiting_payment · $45.99')).toBeVisible();
830
+ expect(pageErrors).toEqual([]);
831
+ } finally {
832
+ await context.close().catch(() => {});
833
+ }
834
+ },
835
+ );
836
+
548
837
  test(
549
838
  'Payment cancellation: customer observes the cancelled order after verified provider confirmation',
550
839
  { tag: ['@web', '@flow', '@payment'] },
@@ -701,7 +990,7 @@ test(
701
990
  );
702
991
  await orderRow.getByRole('button', { name: 'Approve refund', exact: true }).click();
703
992
  expect((await approveResponse).ok()).toBeTruthy();
704
- await expect(orderRow).toContainText('refund_approved');
993
+ await expect(orderRow).toContainText('Refund approved');
705
994
  const executeResponse = waitForApiResponse(
706
995
  adminPage,
707
996
  'POST',
@@ -735,8 +1024,8 @@ test(
735
1024
  },
736
1025
  { timeout: 40_000 },
737
1026
  )
738
- .toContain('refunded');
739
- await expect(await getAdminOrderRow(adminPage, checkout.orderId)).toContainText('refunded');
1027
+ .toContain('Refunded');
1028
+ await expect(await getAdminOrderRow(adminPage, checkout.orderId)).toContainText('Refunded');
740
1029
  expect(adminPageErrors).toEqual([]);
741
1030
  } finally {
742
1031
  await adminContext.close().catch(() => {});
@@ -761,7 +1050,7 @@ test(
761
1050
  );
762
1051
 
763
1052
  test(
764
- 'ATP-ADDR-01: systemAdmin inspects Address Resource without mutation controls',
1053
+ 'ATP-SPC-05: systemAdmin inspects readonly Address Resource without mutation controls',
765
1054
  { tag: ['@admin', '@flow', '@address'] },
766
1055
  async ({ browser, request }, testInfo) => {
767
1056
  test.setTimeout(60_000);
@@ -774,7 +1063,7 @@ test(
774
1063
  const url = new URL(request.url());
775
1064
  if (
776
1065
  url.pathname.startsWith(addressActionPath) &&
777
- ['POST', 'PATCH', 'DELETE'].includes(request.method())
1066
+ ['POST', 'PATCH', 'PUT', 'DELETE'].includes(request.method())
778
1067
  ) {
779
1068
  addressMutationMethods.push(request.method());
780
1069
  }
@@ -789,6 +1078,14 @@ test(
789
1078
  await expect(adminPage).toHaveURL(addressResourceUrl);
790
1079
  const fixtureRow = adminPage.getByRole('row', { name: customer.fixture.recipientName });
791
1080
  await expect(fixtureRow).toBeVisible();
1081
+ await expect(fixtureRow).toContainText(customer.fixture.recipientName);
1082
+ await expect(fixtureRow).toContainText(customer.fixture.phone);
1083
+ await expect(fixtureRow).toContainText(customer.fixture.countryCode);
1084
+ await expect(fixtureRow).toContainText(customer.fixture.city);
1085
+ await expect(fixtureRow).not.toContainText(customer.fixture.region);
1086
+ await expect(fixtureRow).not.toContainText(customer.fixture.postalCode);
1087
+ await expect(fixtureRow).not.toContainText(customer.fixture.addressLine1);
1088
+ await expect(fixtureRow).not.toContainText(customer.fixture.addressLine2);
792
1089
  await expect(adminPage.getByRole('button', { name: 'Create', exact: true })).toHaveCount(0);
793
1090
  await expect(
794
1091
  adminPage.locator('a[href*="/rest/resource/"]').filter({ hasText: 'Create' }),
@@ -796,6 +1093,7 @@ test(
796
1093
 
797
1094
  const addressId = await fixtureRow.getByRole('cell').first().textContent();
798
1095
  expect(addressId).toMatch(/^\d+$/);
1096
+ await expect(fixtureRow.getByRole('link', { name: addressId! })).toHaveCount(1);
799
1097
  await adminPage.goto(`/commerce-admin/rest/resource/commerce-member%3Aaddress/${addressId}`, {
800
1098
  waitUntil: 'load',
801
1099
  });
@@ -1101,6 +1399,170 @@ test(
1101
1399
  },
1102
1400
  );
1103
1401
 
1402
+ test(
1403
+ 'ATP-SPC-02: SKU renders semantic Admin currency and lifecycle controls',
1404
+ { tag: ['@admin', '@flow', '@sku'] },
1405
+ async ({ browser }, testInfo) => {
1406
+ test.setTimeout(60_000);
1407
+ const suffix = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`;
1408
+ const categoryName = `E2E SKU Category ${suffix}`;
1409
+ const productTitle = `E2E SKU Product ${suffix}`;
1410
+ const skuCode = `E2E-SKU-${suffix}`;
1411
+ const adminContext = await browser.newContext();
1412
+ const adminPage = await adminContext.newPage();
1413
+ const adminPageErrors = collectPageErrors(adminPage);
1414
+ const skuCreatePath = '/commerce-admin/rest/resource/commerce-catalog%3Asku/create';
1415
+ const skuListPath = '/commerce-admin/rest/resource/commerce-catalog%3Asku';
1416
+ const categoryActionPath = '/api/commerce/catalog/category';
1417
+ const productActionPath = '/api/commerce/catalog/product';
1418
+ const skuActionPath = '/api/commerce/catalog/sku';
1419
+ let categoryId: number | string | undefined;
1420
+ let productId: number | string | undefined;
1421
+ let skuId: number | string | undefined;
1422
+ let headers: { Authorization: string } | undefined;
1423
+ try {
1424
+ await adminPage.setViewportSize({ width: 1440, height: 900 });
1425
+ await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
1426
+ const accessToken = (await adminContext.cookies()).find(
1427
+ cookie => cookie.name === 'token',
1428
+ )?.value;
1429
+ expect(accessToken).toBeTruthy();
1430
+ headers = { Authorization: `Bearer ${accessToken}` };
1431
+
1432
+ const categoryResponse = await adminPage.request.post(categoryActionPath, {
1433
+ data: { name: categoryName, published: true },
1434
+ headers,
1435
+ });
1436
+ expect(categoryResponse.ok()).toBeTruthy();
1437
+ categoryId = (await categoryResponse.json()).data;
1438
+ expectTableIdentity(categoryId);
1439
+
1440
+ const productResponse = await adminPage.request.post(productActionPath, {
1441
+ data: { categoryId, title: productTitle, published: true },
1442
+ headers,
1443
+ });
1444
+ expect(productResponse.ok()).toBeTruthy();
1445
+ productId = (await productResponse.json()).data;
1446
+ expectTableIdentity(productId);
1447
+
1448
+ await adminPage.goto(skuCreatePath, { waitUntil: 'load' });
1449
+ const productField = adminPage.getByRole('group', { name: 'Product' });
1450
+ await expect(productField).toBeVisible();
1451
+ await expect(productField.getByRole('combobox')).toHaveCount(0);
1452
+ await expect(productField.getByRole('textbox')).toBeVisible();
1453
+ const lifecycle = adminPage
1454
+ .getByRole('group', { name: 'SKU lifecycle' })
1455
+ .getByRole('combobox');
1456
+ await expect(lifecycle).toBeVisible();
1457
+ await expect(lifecycle.locator('option')).toHaveText([
1458
+ '',
1459
+ 'Draft',
1460
+ 'Active',
1461
+ 'Inactive',
1462
+ 'Archived',
1463
+ ]);
1464
+ await expect(adminPage.getByRole('group', { name: 'Price (cents)' })).toBeVisible();
1465
+
1466
+ const skuResponse = await adminPage.request.post(skuActionPath, {
1467
+ data: { code: skuCode, productId, priceCents: 1234, lifecycle: 'draft' },
1468
+ headers,
1469
+ });
1470
+ expect(skuResponse.ok()).toBeTruthy();
1471
+ skuId = (await skuResponse.json()).data;
1472
+ expectTableIdentity(skuId);
1473
+
1474
+ await adminPage.goto(skuListPath, { waitUntil: 'load' });
1475
+ const skuRow = adminPage.getByRole('row', { name: new RegExp(skuCode) });
1476
+ await expect(skuRow).toBeVisible();
1477
+ await expect(skuRow.getByText('12.34', { exact: true })).toBeVisible();
1478
+ await expect(skuRow.getByText('Draft', { exact: true })).toBeVisible();
1479
+ await expect(skuRow.getByRole('link', { name: skuCode, exact: true })).toBeVisible();
1480
+ await expect(
1481
+ adminPage.locator('section').getByText('SKU code', { exact: true }),
1482
+ ).toBeVisible();
1483
+ await expect(adminPage.getByText('Created At', { exact: true })).toHaveCount(1);
1484
+ await expect(adminPage.getByRole('group', { name: 'Attributes' })).toHaveCount(0);
1485
+
1486
+ await adminPage.goto(`${skuListPath}/${skuId}`, { waitUntil: 'load' });
1487
+ await expect(
1488
+ adminPage.getByRole('group', { name: 'SKU lifecycle' }).getByRole('textbox'),
1489
+ ).toHaveValue('Draft');
1490
+ await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0);
1491
+ await expect(adminPage.getByRole('button', { name: 'Back', exact: true })).toBeVisible();
1492
+ expect(adminPageErrors).toEqual([]);
1493
+ } finally {
1494
+ if (skuId && headers) {
1495
+ const response = await adminPage.request.delete(`${skuActionPath}/${skuId}`, { headers });
1496
+ expect(response.ok()).toBeTruthy();
1497
+ }
1498
+ if (productId && headers) {
1499
+ const response = await adminPage.request.delete(`${productActionPath}/${productId}`, {
1500
+ headers,
1501
+ });
1502
+ expect(response.ok()).toBeTruthy();
1503
+ }
1504
+ if (categoryId && headers) {
1505
+ const response = await adminPage.request.delete(`${categoryActionPath}/${categoryId}`, {
1506
+ headers,
1507
+ });
1508
+ expect(response.ok()).toBeTruthy();
1509
+ }
1510
+ await adminContext.close().catch(() => {});
1511
+ }
1512
+ },
1513
+ );
1514
+
1515
+ test(
1516
+ 'ATP-SPC-04: Stock Admin resources remain readonly and mutation-free',
1517
+ { tag: ['@admin', '@flow', '@stock'] },
1518
+ async ({ browser }) => {
1519
+ test.setTimeout(60_000);
1520
+ const adminContext = await browser.newContext();
1521
+ const adminPage = await adminContext.newPage();
1522
+ const adminPageErrors = collectPageErrors(adminPage);
1523
+ const mutationMethods: string[] = [];
1524
+ adminPage.on('request', request => {
1525
+ const url = new URL(request.url());
1526
+ if (
1527
+ url.pathname.includes('/api/commerce/trade/stock') &&
1528
+ ['POST', 'PATCH', 'PUT', 'DELETE'].includes(request.method())
1529
+ ) {
1530
+ mutationMethods.push(request.method());
1531
+ }
1532
+ });
1533
+
1534
+ try {
1535
+ await adminPage.setViewportSize({ width: 1440, height: 900 });
1536
+ await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
1537
+
1538
+ for (const [resourcePath, title] of [
1539
+ ['commerce-trade%3AstockBalance', 'Stock Balance'],
1540
+ ['commerce-trade%3AstockAudit', 'Stock Audit'],
1541
+ ] as const) {
1542
+ await adminPage.goto(`/commerce-admin/rest/resource/${resourcePath}`, {
1543
+ waitUntil: 'load',
1544
+ });
1545
+ await expect(adminPage).toHaveURL(
1546
+ new RegExp(
1547
+ `/commerce-admin/rest/resource/commerce-trade(?:%3A|:|%253A)${title === 'Stock Balance' ? 'stockBalance' : 'stockAudit'}(?:[/?#]|$)`,
1548
+ ),
1549
+ );
1550
+ await expect(adminPage.getByText(title, { exact: true }).first()).toBeVisible();
1551
+ await expect(adminPage.getByRole('button', { name: 'Create', exact: true })).toHaveCount(0);
1552
+ await expect(
1553
+ adminPage.locator('a[href*="/rest/resource/"]').filter({ hasText: 'Create' }),
1554
+ ).toHaveCount(0);
1555
+ await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0);
1556
+ }
1557
+
1558
+ expect(mutationMethods).toEqual([]);
1559
+ expect(adminPageErrors).toEqual([]);
1560
+ } finally {
1561
+ await adminContext.close().catch(() => {});
1562
+ }
1563
+ },
1564
+ );
1565
+
1104
1566
  test(
1105
1567
  'Commerce Cart: anonymous browser is redirected to login',
1106
1568
  { tag: ['@web', '@cart'] },