@mohasinac/appkit 4.34.0 → 4.35.0

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 (248) hide show
  1. package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +32 -2
  2. package/dist/_internal/server/features/seo/sitemap.js +2 -2
  3. package/dist/_internal/server/functions/scheduled.d.ts +1 -2
  4. package/dist/_internal/server/functions/scheduled.js +8 -9
  5. package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +15 -0
  6. package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +15 -6
  7. package/dist/_internal/server/jobs/handlers/index.d.ts +0 -1
  8. package/dist/_internal/server/jobs/handlers/index.js +0 -1
  9. package/dist/features/auth/schemas/firestore.d.ts +1 -0
  10. package/dist/features/tester/components/TesterChecklistStepRow.d.ts +9 -1
  11. package/dist/features/tester/components/TesterChecklistStepRow.js +31 -2
  12. package/dist/features/tester/components/TesterHubView.js +8 -0
  13. package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +16 -1
  14. package/dist/features/tester/repository/tester-checklist-item.repository.js +32 -6
  15. package/dist/features/tester/schemas/firestore.d.ts +109 -0
  16. package/dist/features/tester/schemas/firestore.js +14 -0
  17. package/dist/features/tester/seed-data/_money-flows.d.ts +23 -0
  18. package/dist/features/tester/seed-data/_money-flows.js +430 -0
  19. package/dist/features/tester/seed-data/authored/_types.d.ts +33 -0
  20. package/dist/features/tester/seed-data/authored/_types.js +35 -0
  21. package/dist/features/tester/seed-data/authored/account-auth__profile-settings.d.ts +2 -0
  22. package/dist/features/tester/seed-data/authored/account-auth__profile-settings.js +207 -0
  23. package/dist/features/tester/seed-data/authored/account-auth__signup-login.d.ts +2 -0
  24. package/dist/features/tester/seed-data/authored/account-auth__signup-login.js +298 -0
  25. package/dist/features/tester/seed-data/authored/account-auth__testing-program--admin.d.ts +2 -0
  26. package/dist/features/tester/seed-data/authored/account-auth__testing-program--admin.js +33 -0
  27. package/dist/features/tester/seed-data/authored/account-auth__testing-program.d.ts +2 -0
  28. package/dist/features/tester/seed-data/authored/account-auth__testing-program.js +138 -0
  29. package/dist/features/tester/seed-data/authored/addresses__address-filters.d.ts +2 -0
  30. package/dist/features/tester/seed-data/authored/addresses__address-filters.js +36 -0
  31. package/dist/features/tester/seed-data/authored/addresses__postal-lookup.d.ts +2 -0
  32. package/dist/features/tester/seed-data/authored/addresses__postal-lookup.js +75 -0
  33. package/dist/features/tester/seed-data/authored/addresses__postal-validation.d.ts +2 -0
  34. package/dist/features/tester/seed-data/authored/addresses__postal-validation.js +72 -0
  35. package/dist/features/tester/seed-data/authored/addresses__state-picker.d.ts +2 -0
  36. package/dist/features/tester/seed-data/authored/addresses__state-picker.js +54 -0
  37. package/dist/features/tester/seed-data/authored/addresses__unban-request.d.ts +2 -0
  38. package/dist/features/tester/seed-data/authored/addresses__unban-request.js +39 -0
  39. package/dist/features/tester/seed-data/authored/admin__bans-and-trust.d.ts +2 -0
  40. package/dist/features/tester/seed-data/authored/admin__bans-and-trust.js +226 -0
  41. package/dist/features/tester/seed-data/authored/admin__blog-faqs.d.ts +2 -0
  42. package/dist/features/tester/seed-data/authored/admin__blog-faqs.js +82 -0
  43. package/dist/features/tester/seed-data/authored/admin__bulk-actions.d.ts +2 -0
  44. package/dist/features/tester/seed-data/authored/admin__bulk-actions.js +177 -0
  45. package/dist/features/tester/seed-data/authored/admin__bundles.d.ts +2 -0
  46. package/dist/features/tester/seed-data/authored/admin__bundles.js +97 -0
  47. package/dist/features/tester/seed-data/authored/admin__buyer-data-admin.d.ts +2 -0
  48. package/dist/features/tester/seed-data/authored/admin__buyer-data-admin.js +232 -0
  49. package/dist/features/tester/seed-data/authored/admin__catalog-listings.d.ts +2 -0
  50. package/dist/features/tester/seed-data/authored/admin__catalog-listings.js +306 -0
  51. package/dist/features/tester/seed-data/authored/admin__category-brand-authoring.d.ts +2 -0
  52. package/dist/features/tester/seed-data/authored/admin__category-brand-authoring.js +158 -0
  53. package/dist/features/tester/seed-data/authored/admin__classifieds-digitalcodes-live.d.ts +2 -0
  54. package/dist/features/tester/seed-data/authored/admin__classifieds-digitalcodes-live.js +78 -0
  55. package/dist/features/tester/seed-data/authored/admin__content-deletes.d.ts +2 -0
  56. package/dist/features/tester/seed-data/authored/admin__content-deletes.js +261 -0
  57. package/dist/features/tester/seed-data/authored/admin__content-marketing.d.ts +2 -0
  58. package/dist/features/tester/seed-data/authored/admin__content-marketing.js +151 -0
  59. package/dist/features/tester/seed-data/authored/admin__coupons.d.ts +2 -0
  60. package/dist/features/tester/seed-data/authored/admin__coupons.js +214 -0
  61. package/dist/features/tester/seed-data/authored/admin__events-raffles-spin.d.ts +2 -0
  62. package/dist/features/tester/seed-data/authored/admin__events-raffles-spin.js +160 -0
  63. package/dist/features/tester/seed-data/authored/admin__media-watermark.d.ts +2 -0
  64. package/dist/features/tester/seed-data/authored/admin__media-watermark.js +104 -0
  65. package/dist/features/tester/seed-data/authored/admin__orders-fulfillment.d.ts +2 -0
  66. package/dist/features/tester/seed-data/authored/admin__orders-fulfillment.js +350 -0
  67. package/dist/features/tester/seed-data/authored/admin__prize-draws-lotteries.d.ts +2 -0
  68. package/dist/features/tester/seed-data/authored/admin__prize-draws-lotteries.js +105 -0
  69. package/dist/features/tester/seed-data/authored/admin__site-system.d.ts +2 -0
  70. package/dist/features/tester/seed-data/authored/admin__site-system.js +590 -0
  71. package/dist/features/tester/seed-data/authored/admin__uncovered-admin-pages.d.ts +2 -0
  72. package/dist/features/tester/seed-data/authored/admin__uncovered-admin-pages.js +235 -0
  73. package/dist/features/tester/seed-data/authored/admin__users-trust.d.ts +2 -0
  74. package/dist/features/tester/seed-data/authored/admin__users-trust.js +569 -0
  75. package/dist/features/tester/seed-data/authored/buying__bidding.d.ts +2 -0
  76. package/dist/features/tester/seed-data/authored/buying__bidding.js +417 -0
  77. package/dist/features/tester/seed-data/authored/buying__browsing-search.d.ts +2 -0
  78. package/dist/features/tester/seed-data/authored/buying__browsing-search.js +653 -0
  79. package/dist/features/tester/seed-data/authored/buying__buying-checkout.d.ts +2 -0
  80. package/dist/features/tester/seed-data/authored/buying__buying-checkout.js +533 -0
  81. package/dist/features/tester/seed-data/authored/buying__buying-coupons.d.ts +2 -0
  82. package/dist/features/tester/seed-data/authored/buying__buying-coupons.js +379 -0
  83. package/dist/features/tester/seed-data/authored/buying__cart.d.ts +2 -0
  84. package/dist/features/tester/seed-data/authored/buying__cart.js +1171 -0
  85. package/dist/features/tester/seed-data/authored/buying__image-tile-layout.d.ts +2 -0
  86. package/dist/features/tester/seed-data/authored/buying__image-tile-layout.js +140 -0
  87. package/dist/features/tester/seed-data/authored/buying__my-orders.d.ts +2 -0
  88. package/dist/features/tester/seed-data/authored/buying__my-orders.js +257 -0
  89. package/dist/features/tester/seed-data/authored/buying__offers.d.ts +2 -0
  90. package/dist/features/tester/seed-data/authored/buying__offers.js +1111 -0
  91. package/dist/features/tester/seed-data/authored/buying__order-status-lifecycle.d.ts +2 -0
  92. package/dist/features/tester/seed-data/authored/buying__order-status-lifecycle.js +252 -0
  93. package/dist/features/tester/seed-data/authored/buying__product-detail.d.ts +2 -0
  94. package/dist/features/tester/seed-data/authored/buying__product-detail.js +387 -0
  95. package/dist/features/tester/seed-data/authored/buying__return-request.d.ts +2 -0
  96. package/dist/features/tester/seed-data/authored/buying__return-request.js +152 -0
  97. package/dist/features/tester/seed-data/authored/buying__reviews-pagination.d.ts +2 -0
  98. package/dist/features/tester/seed-data/authored/buying__reviews-pagination.js +242 -0
  99. package/dist/features/tester/seed-data/authored/buying__reviews.d.ts +2 -0
  100. package/dist/features/tester/seed-data/authored/buying__reviews.js +113 -0
  101. package/dist/features/tester/seed-data/authored/buying__user-dashboard-extras.d.ts +2 -0
  102. package/dist/features/tester/seed-data/authored/buying__user-dashboard-extras.js +283 -0
  103. package/dist/features/tester/seed-data/authored/buying__user-dashboard-navigation.d.ts +2 -0
  104. package/dist/features/tester/seed-data/authored/buying__user-dashboard-navigation.js +167 -0
  105. package/dist/features/tester/seed-data/authored/buying__user-uncovered-pages.d.ts +2 -0
  106. package/dist/features/tester/seed-data/authored/buying__user-uncovered-pages.js +90 -0
  107. package/dist/features/tester/seed-data/authored/buying__wishlist-history.d.ts +2 -0
  108. package/dist/features/tester/seed-data/authored/buying__wishlist-history.js +344 -0
  109. package/dist/features/tester/seed-data/authored/community-support__public-profile.d.ts +2 -0
  110. package/dist/features/tester/seed-data/authored/community-support__public-profile.js +53 -0
  111. package/dist/features/tester/seed-data/authored/community-support__support-tickets.d.ts +2 -0
  112. package/dist/features/tester/seed-data/authored/community-support__support-tickets.js +76 -0
  113. package/dist/features/tester/seed-data/authored/content-discovery__blog.d.ts +2 -0
  114. package/dist/features/tester/seed-data/authored/content-discovery__blog.js +93 -0
  115. package/dist/features/tester/seed-data/authored/content-discovery__category-brand-relations.d.ts +2 -0
  116. package/dist/features/tester/seed-data/authored/content-discovery__category-brand-relations.js +149 -0
  117. package/dist/features/tester/seed-data/authored/content-discovery__coupons.d.ts +2 -0
  118. package/dist/features/tester/seed-data/authored/content-discovery__coupons.js +121 -0
  119. package/dist/features/tester/seed-data/authored/content-discovery__event-participation.d.ts +2 -0
  120. package/dist/features/tester/seed-data/authored/content-discovery__event-participation.js +207 -0
  121. package/dist/features/tester/seed-data/authored/content-discovery__events.d.ts +2 -0
  122. package/dist/features/tester/seed-data/authored/content-discovery__events.js +318 -0
  123. package/dist/features/tester/seed-data/authored/content-discovery__faq-help.d.ts +2 -0
  124. package/dist/features/tester/seed-data/authored/content-discovery__faq-help.js +65 -0
  125. package/dist/features/tester/seed-data/authored/content-discovery__notifications.d.ts +2 -0
  126. package/dist/features/tester/seed-data/authored/content-discovery__notifications.js +147 -0
  127. package/dist/features/tester/seed-data/authored/content-discovery__search.d.ts +2 -0
  128. package/dist/features/tester/seed-data/authored/content-discovery__search.js +332 -0
  129. package/dist/features/tester/seed-data/authored/cta-layout__checkout-bottom-bar.d.ts +2 -0
  130. package/dist/features/tester/seed-data/authored/cta-layout__checkout-bottom-bar.js +53 -0
  131. package/dist/features/tester/seed-data/authored/cta-layout__dialog-footers.d.ts +2 -0
  132. package/dist/features/tester/seed-data/authored/cta-layout__dialog-footers.js +52 -0
  133. package/dist/features/tester/seed-data/authored/cta-layout__editor-action-bar.d.ts +2 -0
  134. package/dist/features/tester/seed-data/authored/cta-layout__editor-action-bar.js +35 -0
  135. package/dist/features/tester/seed-data/authored/cta-layout__navbar-ctas.d.ts +2 -0
  136. package/dist/features/tester/seed-data/authored/cta-layout__navbar-ctas.js +141 -0
  137. package/dist/features/tester/seed-data/authored/cta-layout__product-bottom-bar.d.ts +2 -0
  138. package/dist/features/tester/seed-data/authored/cta-layout__product-bottom-bar.js +35 -0
  139. package/dist/features/tester/seed-data/authored/design-ux__back-to-top-button.d.ts +2 -0
  140. package/dist/features/tester/seed-data/authored/design-ux__back-to-top-button.js +95 -0
  141. package/dist/features/tester/seed-data/authored/design-ux__carousel-arrow-bounds.d.ts +2 -0
  142. package/dist/features/tester/seed-data/authored/design-ux__carousel-arrow-bounds.js +191 -0
  143. package/dist/features/tester/seed-data/authored/design-ux__dashboard-layout.d.ts +2 -0
  144. package/dist/features/tester/seed-data/authored/design-ux__dashboard-layout.js +158 -0
  145. package/dist/features/tester/seed-data/authored/design-ux__footer-theme.d.ts +2 -0
  146. package/dist/features/tester/seed-data/authored/design-ux__footer-theme.js +106 -0
  147. package/dist/features/tester/seed-data/authored/design-ux__form-validation-errors.d.ts +2 -0
  148. package/dist/features/tester/seed-data/authored/design-ux__form-validation-errors.js +88 -0
  149. package/dist/features/tester/seed-data/authored/design-ux__general-design.d.ts +2 -0
  150. package/dist/features/tester/seed-data/authored/design-ux__general-design.js +204 -0
  151. package/dist/features/tester/seed-data/authored/design-ux__hand-mode-layout.d.ts +2 -0
  152. package/dist/features/tester/seed-data/authored/design-ux__hand-mode-layout.js +301 -0
  153. package/dist/features/tester/seed-data/authored/design-ux__homepage-carousels.d.ts +2 -0
  154. package/dist/features/tester/seed-data/authored/design-ux__homepage-carousels.js +407 -0
  155. package/dist/features/tester/seed-data/authored/design-ux__status-badge-legibility.d.ts +2 -0
  156. package/dist/features/tester/seed-data/authored/design-ux__status-badge-legibility.js +214 -0
  157. package/dist/features/tester/seed-data/authored/design-ux__sticky-cta-bar.d.ts +2 -0
  158. package/dist/features/tester/seed-data/authored/design-ux__sticky-cta-bar.js +275 -0
  159. package/dist/features/tester/seed-data/authored/index.d.ts +2 -0
  160. package/dist/features/tester/seed-data/authored/index.js +237 -0
  161. package/dist/features/tester/seed-data/authored/page-wiring__data-loss.d.ts +2 -0
  162. package/dist/features/tester/seed-data/authored/page-wiring__data-loss.js +102 -0
  163. package/dist/features/tester/seed-data/authored/page-wiring__detail-pages.d.ts +2 -0
  164. package/dist/features/tester/seed-data/authored/page-wiring__detail-pages.js +50 -0
  165. package/dist/features/tester/seed-data/authored/page-wiring__drawer-pages.d.ts +2 -0
  166. package/dist/features/tester/seed-data/authored/page-wiring__drawer-pages.js +66 -0
  167. package/dist/features/tester/seed-data/authored/page-wiring__reachability.d.ts +2 -0
  168. package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +115 -0
  169. package/dist/features/tester/seed-data/authored/public-pages__auth-error-pages.d.ts +2 -0
  170. package/dist/features/tester/seed-data/authored/public-pages__auth-error-pages.js +136 -0
  171. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.d.ts +2 -0
  172. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +69 -0
  173. package/dist/features/tester/seed-data/authored/public-pages__core-listing-pages.d.ts +2 -0
  174. package/dist/features/tester/seed-data/authored/public-pages__core-listing-pages.js +263 -0
  175. package/dist/features/tester/seed-data/authored/public-pages__help-how-it-works.d.ts +2 -0
  176. package/dist/features/tester/seed-data/authored/public-pages__help-how-it-works.js +102 -0
  177. package/dist/features/tester/seed-data/authored/public-pages__help-scams-guides-subpages.d.ts +2 -0
  178. package/dist/features/tester/seed-data/authored/public-pages__help-scams-guides-subpages.js +234 -0
  179. package/dist/features/tester/seed-data/authored/public-pages__legal-policy-pages.d.ts +2 -0
  180. package/dist/features/tester/seed-data/authored/public-pages__legal-policy-pages.js +126 -0
  181. package/dist/features/tester/seed-data/authored/public-pages__stores-sellers-directories.d.ts +2 -0
  182. package/dist/features/tester/seed-data/authored/public-pages__stores-sellers-directories.js +255 -0
  183. package/dist/features/tester/seed-data/authored/search-and-nav__employee-permissions.d.ts +2 -0
  184. package/dist/features/tester/seed-data/authored/search-and-nav__employee-permissions.js +59 -0
  185. package/dist/features/tester/seed-data/authored/search-and-nav__header-search.d.ts +2 -0
  186. package/dist/features/tester/seed-data/authored/search-and-nav__header-search.js +88 -0
  187. package/dist/features/tester/seed-data/authored/search-and-nav__settings-deep-links.d.ts +2 -0
  188. package/dist/features/tester/seed-data/authored/search-and-nav__settings-deep-links.js +51 -0
  189. package/dist/features/tester/seed-data/authored/search-and-nav__sidebar-search.d.ts +2 -0
  190. package/dist/features/tester/seed-data/authored/search-and-nav__sidebar-search.js +35 -0
  191. package/dist/features/tester/seed-data/authored/selling__become-seller.d.ts +2 -0
  192. package/dist/features/tester/seed-data/authored/selling__become-seller.js +94 -0
  193. package/dist/features/tester/seed-data/authored/selling__final-sale-authoring.d.ts +2 -0
  194. package/dist/features/tester/seed-data/authored/selling__final-sale-authoring.js +107 -0
  195. package/dist/features/tester/seed-data/authored/selling__listing-a-product.d.ts +2 -0
  196. package/dist/features/tester/seed-data/authored/selling__listing-a-product.js +285 -0
  197. package/dist/features/tester/seed-data/authored/selling__listing-lifecycle.d.ts +2 -0
  198. package/dist/features/tester/seed-data/authored/selling__listing-lifecycle.js +326 -0
  199. package/dist/features/tester/seed-data/authored/selling__listing-type-fields-roundtrip.d.ts +2 -0
  200. package/dist/features/tester/seed-data/authored/selling__listing-type-fields-roundtrip.js +136 -0
  201. package/dist/features/tester/seed-data/authored/selling__media-limits.d.ts +2 -0
  202. package/dist/features/tester/seed-data/authored/selling__media-limits.js +128 -0
  203. package/dist/features/tester/seed-data/authored/selling__sectionised-forms.d.ts +2 -0
  204. package/dist/features/tester/seed-data/authored/selling__sectionised-forms.js +167 -0
  205. package/dist/features/tester/seed-data/authored/selling__seller-analytics-payouts.d.ts +2 -0
  206. package/dist/features/tester/seed-data/authored/selling__seller-analytics-payouts.js +117 -0
  207. package/dist/features/tester/seed-data/authored/selling__seller-bids-bundles-filters.d.ts +2 -0
  208. package/dist/features/tester/seed-data/authored/selling__seller-bids-bundles-filters.js +88 -0
  209. package/dist/features/tester/seed-data/authored/selling__seller-catalog-org.d.ts +2 -0
  210. package/dist/features/tester/seed-data/authored/selling__seller-catalog-org.js +151 -0
  211. package/dist/features/tester/seed-data/authored/selling__seller-custom-brands.d.ts +2 -0
  212. package/dist/features/tester/seed-data/authored/selling__seller-custom-brands.js +105 -0
  213. package/dist/features/tester/seed-data/authored/selling__seller-guide.d.ts +2 -0
  214. package/dist/features/tester/seed-data/authored/selling__seller-guide.js +33 -0
  215. package/dist/features/tester/seed-data/authored/selling__seller-listing-types.d.ts +2 -0
  216. package/dist/features/tester/seed-data/authored/selling__seller-listing-types.js +347 -0
  217. package/dist/features/tester/seed-data/authored/selling__seller-marketing-extras.d.ts +2 -0
  218. package/dist/features/tester/seed-data/authored/selling__seller-marketing-extras.js +166 -0
  219. package/dist/features/tester/seed-data/authored/selling__seller-ops-comms.d.ts +2 -0
  220. package/dist/features/tester/seed-data/authored/selling__seller-ops-comms.js +89 -0
  221. package/dist/features/tester/seed-data/authored/selling__seller-orders.d.ts +2 -0
  222. package/dist/features/tester/seed-data/authored/selling__seller-orders.js +195 -0
  223. package/dist/features/tester/seed-data/authored/selling__seller-shipping-payouts-setup.d.ts +2 -0
  224. package/dist/features/tester/seed-data/authored/selling__seller-shipping-payouts-setup.js +180 -0
  225. package/dist/features/tester/seed-data/authored/selling__store-dashboard-navigation.d.ts +2 -0
  226. package/dist/features/tester/seed-data/authored/selling__store-dashboard-navigation.js +164 -0
  227. package/dist/features/tester/seed-data/authored/selling__store-uncovered-pages.d.ts +2 -0
  228. package/dist/features/tester/seed-data/authored/selling__store-uncovered-pages.js +116 -0
  229. package/dist/features/tester/seed-data/authored/seo__canonical-and-host.d.ts +2 -0
  230. package/dist/features/tester/seed-data/authored/seo__canonical-and-host.js +111 -0
  231. package/dist/features/tester/seed-data/authored/seo__og-images.d.ts +2 -0
  232. package/dist/features/tester/seed-data/authored/seo__og-images.js +91 -0
  233. package/dist/features/tester/seed-data/authored/seo__page-metadata.d.ts +2 -0
  234. package/dist/features/tester/seed-data/authored/seo__page-metadata.js +122 -0
  235. package/dist/features/tester/seed-data/authored/seo__sitemap-and-robots.d.ts +2 -0
  236. package/dist/features/tester/seed-data/authored/seo__sitemap-and-robots.js +105 -0
  237. package/dist/features/tester/seed-data/events-tester-seed-data.js +77 -5
  238. package/dist/features/tester/seed-data/offers-tester-seed-data.js +23 -9
  239. package/dist/features/tester/seed-data/products-tester-seed-data.js +65 -49
  240. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +744 -1
  241. package/dist/features/tester/seed-data/tester-window.d.ts +25 -0
  242. package/dist/features/tester/seed-data/tester-window.js +66 -0
  243. package/dist/seed/carousel-slides-seed-data.js +1 -1
  244. package/dist/seed/products-standard-seed-data.js +2 -2
  245. package/dist/seed/reviews-seed-data.js +6 -1
  246. package/dist/seed/users-seed-data.js +107 -4
  247. package/package.json +1 -1
  248. package/scripts/seed-cli.mjs +6 -0
@@ -96,8 +96,38 @@ export function LotteryAdminEditView({ eventId, initialData, onSubmit, }) {
96
96
  * `audit-code-quality`'s DEEP_NESTING rule exists to stop.
97
97
  */
98
98
  function SlotTable({ slots, showPrice, onSlotsChange, }) {
99
- const addSlot = () => onSlotsChange([...slots, { slotNumber: slots.length + 1, name: "", price: 0 }]);
100
- const removeSlot = (idx) => onSlotsChange(slots.filter((_, i) => i !== idx).map((s, i) => ({ ...s, slotNumber: i + 1 })));
99
+ /**
100
+ * Next number is max+1, not length+1.
101
+ *
102
+ * Now that removeSlot preserves numbering, `slots.length + 1` collides: delete
103
+ * slot 1 of 25 and you hold 24 slots numbered 2..25, so length+1 is 25 — a
104
+ * duplicate, which the write schema rejects outright (slot numbers must be
105
+ * unique). Deriving from the highest number in use is stable under any gaps.
106
+ */
107
+ const addSlot = () => onSlotsChange([
108
+ ...slots,
109
+ { slotNumber: slots.reduce((max, s) => Math.max(max, s.slotNumber), 0) + 1, name: "", price: 0 },
110
+ ]);
111
+ /**
112
+ * 🛑 Remove the slot. Do NOT renumber the survivors.
113
+ *
114
+ * `slotNumber` is the stable identity a booking is attached to, and renumbering
115
+ * silently defeated the server's own protection. `mergeLotteryConfig` refuses a
116
+ * save that drops a BOOKED slot by checking `isBooked && !incoming.has(slotNumber)`
117
+ * — but this function used to renumber the remaining slots to `i + 1`, so deleting
118
+ * slot 1 of 25 sent back numbers 1..24. Every booked number (1–5) still *appeared*
119
+ * in that set, the check found nothing to refuse, and the save went through.
120
+ *
121
+ * The damage was worse than the deletion: the merge re-attaches bookings by
122
+ * slotNumber, so the buyer of the deleted slot 1 had their booking moved onto
123
+ * whatever prize now occupied number 1. That is precisely the hazard the schema
124
+ * header describes — "hands slot 7's buyer the prize that used to be slot 8".
125
+ *
126
+ * Numbers may therefore be non-contiguous after a delete (2..25), which the write
127
+ * schema allows: it requires slot numbers to be UNIQUE, not gapless, and totalSlots
128
+ * is derived from slots.length rather than from the highest number.
129
+ */
130
+ const removeSlot = (idx) => onSlotsChange(slots.filter((_, i) => i !== idx));
101
131
  const updateSlot = (idx, field, value) => onSlotsChange(slots.map((s, i) => i === idx
102
132
  ? {
103
133
  ...s,
@@ -16,8 +16,8 @@ const PRODUCT_SLUG = "slug";
16
16
  const PRODUCT_UPDATED_AT = "updatedAt";
17
17
  /**
18
18
  * Tester-sandbox fixtures must never reach the public sitemap. They are created
19
- * and destroyed on a cycle by `testerSandboxCleanup` / `testerSandboxRefresh`,
20
- * so anything Google indexes from them 404s shortly after. 34 of 182 sitemap
19
+ * and destroyed on a cycle by `testerSandboxCleanup` and by each tester run's
20
+ * wipe/seed, so anything Google indexes from them 404s shortly after. 34 of 182 sitemap
21
21
  * URLs were disposable test fixtures before this filter existed.
22
22
  *
23
23
  * 🛑 This MUST be an in-memory filter, never a Firestore inequality on the
@@ -22,9 +22,8 @@ export declare const cleanupRtdbEvents: import("./types").ScheduledFunctionDefin
22
22
  export declare const mediaTmpCleanup: import("./types").ScheduledFunctionDefinition;
23
23
  export declare const draftPrune: import("./types").ScheduledFunctionDefinition;
24
24
  export declare const testerSandboxCleanup: import("./types").ScheduledFunctionDefinition;
25
- export declare const testerSandboxRefresh: import("./types").ScheduledFunctionDefinition;
26
25
  export declare const prizeDrawExpiryReveal: import("./types").ScheduledFunctionDefinition;
27
26
  export declare const bundleStockSync: import("./types").ScheduledFunctionDefinition;
28
27
  export declare const emiInstallmentReminder: import("./types").ScheduledFunctionDefinition;
29
28
  export declare const catalogueImageStalenessReminder: import("./types").ScheduledFunctionDefinition;
30
- export declare const SCHEDULED_FUNCTIONS: readonly [import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition];
29
+ export declare const SCHEDULED_FUNCTIONS: readonly [import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition];
@@ -1,6 +1,6 @@
1
1
  // Scheduled function definitions (Cloud Scheduler triggers).
2
2
  // Cron syntax accepts both Firebase shorthand and standard cron expressions.
3
- import { auctionSettlementHandler, autoPayoutEligibilityHandler, bundleStockSyncHandler, cartPruneHandler, cleanupRtdbEventsHandler, countersReconcileHandler, couponExpiryHandler, dailyDataCleanupHandler, draftPruneHandler, emiInstallmentReminderHandler, catalogueImageStalenessReminderHandler, mediaTmpCleanupHandler, notificationPruneHandler, offerExpiryHandler, payoutBatchHandler, paymentWindowTimeoutHandler, hardBanReinstatementHandler, paymentReviewAutoApproveHandler, pendingOrderTimeoutHandler, positionsReconcileHandler, prizeDrawExpiryRevealHandler, productStatsSyncHandler, revenueRollupHandler, pageViewRollupHandler, pageViewPruneHandler, dailyStatusDigestHandler, weeklyPayoutEligibilityHandler, testerSandboxCleanupHandler, testerSandboxRefreshHandler, } from "../jobs/handlers";
3
+ import { auctionSettlementHandler, autoPayoutEligibilityHandler, bundleStockSyncHandler, cartPruneHandler, cleanupRtdbEventsHandler, countersReconcileHandler, couponExpiryHandler, dailyDataCleanupHandler, draftPruneHandler, emiInstallmentReminderHandler, catalogueImageStalenessReminderHandler, mediaTmpCleanupHandler, notificationPruneHandler, offerExpiryHandler, payoutBatchHandler, paymentWindowTimeoutHandler, hardBanReinstatementHandler, paymentReviewAutoApproveHandler, pendingOrderTimeoutHandler, positionsReconcileHandler, prizeDrawExpiryRevealHandler, productStatsSyncHandler, revenueRollupHandler, pageViewRollupHandler, pageViewPruneHandler, dailyStatusDigestHandler, weeklyPayoutEligibilityHandler, testerSandboxCleanupHandler, } from "../jobs/handlers";
4
4
  import { defineFunction } from "./define";
5
5
  const REGION = "asia-south1";
6
6
  const EVERY_5_MIN = "every 5 minutes";
@@ -182,13 +182,13 @@ export const testerSandboxCleanup = defineFunction({
182
182
  handler: testerSandboxCleanupHandler,
183
183
  options: { region: REGION, timeoutSeconds: 300, memory: "256MiB", maxInstances: 1 },
184
184
  });
185
- export const testerSandboxRefresh = defineFunction({
186
- name: "testerSandboxRefresh",
187
- description: "Revert live tester QA sandbox fixtures to their canonical seed shape and prune tester-created extras (every 4 hours).",
188
- trigger: { kind: "schedule", cron: "0 */4 * * *", timeZone: "UTC" },
189
- handler: testerSandboxRefreshHandler,
190
- options: { region: REGION, timeoutSeconds: 300, memory: "256MiB", maxInstances: 1 },
191
- });
185
+ // testerSandboxRefresh (every 4h: revert fixtures to seed shape, prune extras) was
186
+ // REMOVED. The tester runner now owns fixture state — it seeds a known catalog at the
187
+ // start of a run and wipes at the end — so a background job independently rewriting
188
+ // those same documents was redundant and, worse, raced a run in progress: it reset
189
+ // currentBid and recomputed auctionEndDate underneath a test that was mid-assertion.
190
+ // Do not reinstate it without also deciding who owns fixture state; two owners is the
191
+ // bug it caused. See tester/scripts/lifecycle.mjs.
192
192
  export const prizeDrawExpiryReveal = defineFunction({
193
193
  name: "prizeDrawExpiryReveal",
194
194
  description: "Assign winners for outstanding paid orders on scheduled-mode prize draws past their expiry, then close them (every 5 minutes).",
@@ -242,7 +242,6 @@ export const SCHEDULED_FUNCTIONS = [
242
242
  mediaTmpCleanup,
243
243
  draftPrune,
244
244
  testerSandboxCleanup,
245
- testerSandboxRefresh,
246
245
  prizeDrawExpiryReveal,
247
246
  bundleStockSync,
248
247
  emiInstallmentReminder,
@@ -1,4 +1,19 @@
1
1
  import type { JobContext } from "../runtime/types";
2
+ /**
3
+ * 🛑 THE definition of "sandbox scope" — six collections, and nothing else.
4
+ *
5
+ * Everything OUTSIDE this list is permanent: neither this job nor anything else
6
+ * reclaims it. That is why `users`, `addresses`, `sessions` and `siteSettings`
7
+ * survive a tester run untouched, and why the tester lifecycle's cascade delete
8
+ * (tester/scripts/lib/collections.mjs) mirrors this list rather than inventing
9
+ * its own — a second definition is how the two silently disagree.
10
+ *
11
+ * Adding a collection here makes it disposable. Do not add one without also
12
+ * adding a seed fixture for it, or a run will delete rows nothing recreates.
13
+ *
14
+ * (This used to be shared with testerSandboxRefresh.ts, which was removed when
15
+ * the tester runner took over fixture state — see scheduled.ts.)
16
+ */
2
17
  export declare const SANDBOX_COLLECTIONS: readonly ["categories", "stores", "products", "blogPosts", "events", "offers"];
3
18
  export interface TesterSandboxCleanupOptions {
4
19
  /** true = wipe every isTestData:true doc regardless of testDataExpiresAt (manual force-purge). */
@@ -1,11 +1,20 @@
1
1
  import { batchDelete, getTestDataRefs } from "../handlers/_helpers";
2
2
  import { BID_FIELDS } from "../../../../constants/field-names";
3
- // Exported so testerSandboxRefresh.ts (the every-4h revert+prune job) reuses
4
- // the exact same collection scope instead of redefining it — one source of
5
- // truth for "what counts as sandbox scope."
6
- // Keep in sync with testerSandboxRefresh.ts's SEED_BY_COLLECTION — the daily
7
- // TTL cleanup and the 4-hourly revert+prune must agree on what "sandbox" means
8
- // (CLAUDE.md § Tester QA standards #3).
3
+ /**
4
+ * 🛑 THE definition of "sandbox scope" — six collections, and nothing else.
5
+ *
6
+ * Everything OUTSIDE this list is permanent: neither this job nor anything else
7
+ * reclaims it. That is why `users`, `addresses`, `sessions` and `siteSettings`
8
+ * survive a tester run untouched, and why the tester lifecycle's cascade delete
9
+ * (tester/scripts/lib/collections.mjs) mirrors this list rather than inventing
10
+ * its own — a second definition is how the two silently disagree.
11
+ *
12
+ * Adding a collection here makes it disposable. Do not add one without also
13
+ * adding a seed fixture for it, or a run will delete rows nothing recreates.
14
+ *
15
+ * (This used to be shared with testerSandboxRefresh.ts, which was removed when
16
+ * the tester runner took over fixture state — see scheduled.ts.)
17
+ */
9
18
  export const SANDBOX_COLLECTIONS = ["categories", "stores", "products", "blogPosts", "events", "offers"];
10
19
  const BID_CHUNK_SIZE = 30; // Firestore `in` query cap
11
20
  export async function runTesterSandboxCleanup(ctx, opts = {}) {
@@ -54,7 +54,6 @@ export { onScamReportCreateHandler } from "./onScamReportCreate";
54
54
  export { onScamReportUpdateHandler } from "./onScamReportUpdate";
55
55
  export { draftPruneHandler } from "./draftPrune";
56
56
  export { testerSandboxCleanupHandler } from "./testerSandboxCleanup";
57
- export { testerSandboxRefreshHandler } from "./testerSandboxRefresh";
58
57
  export { emiInstallmentReminderHandler } from "./emiInstallmentReminder";
59
58
  export { onShipmentItemWriteHandler } from "./onShipmentItemWrite";
60
59
  export { onShipmentLotWriteHandler, onShipmentHeaderWriteHandler } from "./onShipmentAllocationSync";
@@ -67,7 +67,6 @@ export { draftPruneHandler } from "./draftPrune";
67
67
  // Tester program — 7-day auto-expiry sweep for the shared admin-seeded test sandbox
68
68
  export { testerSandboxCleanupHandler } from "./testerSandboxCleanup";
69
69
  // Tester program — every-4h revert-live-edits + prune-extras refresh (faster than the 7-day sweep above)
70
- export { testerSandboxRefreshHandler } from "./testerSandboxRefresh";
71
70
  // EMI — installment reminder + overdue-flagging sweep
72
71
  export { emiInstallmentReminderHandler } from "./emiInstallmentReminder";
73
72
  // Procurement Shipments — 3-function recompute cascade (item→lot, lot/header→shipment allocation, cascade-delete)
@@ -69,6 +69,7 @@ export interface UserDocument extends BaseDocument {
69
69
  storeStatus?: "pending" | "approved" | "rejected";
70
70
  isTester?: boolean;
71
71
  canTestAdmin?: boolean;
72
+ isBot?: boolean;
72
73
  publicProfile?: {
73
74
  isPublic: boolean;
74
75
  showEmail: boolean;
@@ -1,10 +1,18 @@
1
1
  import React from "react";
2
- import type { TesterAnswer } from "../schemas/firestore";
2
+ import type { TesterAnswer, TesterCaseRole } from "../schemas/firestore";
3
3
  export interface TesterChecklistStepItem {
4
4
  checklistItemId: string;
5
5
  label: string;
6
6
  description?: string;
7
7
  href?: string;
8
+ roles?: TesterCaseRole[];
9
+ startPage?: string;
10
+ steps?: string[];
11
+ inputs?: Record<string, string | number | boolean>;
12
+ expectedBehaviour?: string;
13
+ expectedUiState?: string;
14
+ expectedData?: Record<string, string | number | boolean>;
15
+ endResult?: string;
8
16
  answer: TesterAnswer | null;
9
17
  comment?: string;
10
18
  screenshotUrl?: string;
@@ -1,10 +1,34 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import React from "react";
4
- import { Anchor, Button, Div, FieldTextarea, Row, Stack, Text } from "../../../ui";
4
+ import { Anchor, Button, Details, Div, FieldTextarea, Li, Ol, Row, Stack, Summary, Text } from "../../../ui";
5
5
  import { MediaUploadField } from "../../media/upload/MediaUploadField";
6
6
  import { useMediaUpload } from "../../media/hooks/useMedia";
7
7
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
8
+ /** Human-facing labels for the role chip. `seller` is specifically the listing's OWNER. */
9
+ const ROLE_LABEL = {
10
+ guest: "Signed out",
11
+ buyer: "As a buyer",
12
+ seller: "As the owning seller",
13
+ admin: "As an admin",
14
+ employee: "As an employee",
15
+ };
16
+ /**
17
+ * `inputs` / `expectedData` as a two-column list.
18
+ *
19
+ * Rendered as key/value rather than pretty-printed JSON because the reader is a
20
+ * person about to type these into a form — braces and quotes are noise, and a
21
+ * value they have to mentally unescape is a value they will get wrong.
22
+ *
23
+ * Renders nothing at all when the case has no such data, which is the common case:
24
+ * a read-only page enters nothing and has nothing checkable behind the screen.
25
+ */
26
+ function KeyValueBlock({ label, data, }) {
27
+ const entries = Object.entries(data ?? {});
28
+ if (!entries.length)
29
+ return null;
30
+ return (_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "xs", weight: "semibold", color: "muted", children: label }), entries.map(([k, v]) => (_jsxs(Row, { gap: "xs", align: "baseline", wrap: true, children: [_jsx(Text, { size: "sm", color: "muted", children: k }), _jsx(Text, { size: "sm", weight: "medium", numeric: typeof v === "number", children: String(v) })] }, k)))] }));
31
+ }
8
32
  export function TesterChecklistStepRow({ item, testerDisplayName, onAnswer, onSaveNote, }) {
9
33
  const [expanded, setExpanded] = React.useState(false);
10
34
  const [comment, setComment] = React.useState(item.comment ?? "");
@@ -25,5 +49,10 @@ export function TesterChecklistStepRow({ item, testerDisplayName, onAnswer, onSa
25
49
  setSaving(false);
26
50
  }
27
51
  };
28
- return (_jsxs(Stack, { gap: "xs", padding: "sm", border: "default", rounded: "md", children: [_jsxs(Row, { gap: "sm", justify: "between", align: "center", wrap: true, children: [_jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsx(Text, { weight: "medium", children: item.label }), item.description && (_jsx(Text, { size: "xs", color: "muted", children: item.description })), item.href && (_jsx(Anchor, { href: item.href, tone: "brand", underline: "hover", target: "_blank", rel: "noopener noreferrer", children: "Go test this \u2192" }))] }), _jsxs(Row, { gap: "xs", children: [_jsx(Button, { type: "button", size: "sm", variant: item.answer === "yes" ? "primary" : "outline", onClick: () => onAnswer(item.checklistItemId, "yes"), children: "Yes" }), _jsx(Button, { type: "button", size: "sm", variant: item.answer === "no" ? "danger" : "outline", onClick: () => onAnswer(item.checklistItemId, "no"), children: "No" }), _jsx(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => setExpanded((v) => !v), children: expanded ? "Hide note" : "Add note" })] })] }), expanded && (_jsx(Div, { padding: "t-sm", border: "default", className: "border-t", children: _jsxs(Stack, { gap: "sm", children: [_jsx(FieldTextarea, { name: `comment-${item.checklistItemId}`, label: "Comment", value: comment, onChange: setComment, placeholder: "What did you notice? Colors, styles, readability, bugs..." }), _jsx(MediaUploadField, { label: "Screenshot (optional)", value: screenshotUrl, onChange: setScreenshotUrl, onUpload: handleUpload, kind: "image" }), _jsx(Row, { children: _jsx(Button, { type: "button", action: ACTIONS.TESTER["save-note"], isLoading: saving, onClick: handleSave }) })] }) }))] }));
52
+ return (_jsxs(Stack, { gap: "xs", padding: "sm", border: "default", rounded: "md", children: [_jsxs(Row, { gap: "sm", justify: "between", align: "center", wrap: true, children: [_jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsxs(Row, { gap: "xs", align: "center", wrap: true, children: [item.roles?.map((r) => (_jsx(Text, { size: "xs", weight: "semibold", color: "muted", children: ROLE_LABEL[r] }, r))), _jsx(Text, { weight: "medium", children: item.label })] }), item.description && (_jsx(Text, { size: "xs", color: "muted", children: item.description })), item.href && (_jsx(Anchor, { href: item.href, tone: "brand", underline: "hover", target: "_blank", rel: "noopener noreferrer", children: "Go test this \u2192" }))] }), _jsxs(Row, { gap: "xs", children: [_jsx(Button, { type: "button", size: "sm", variant: item.answer === "yes" ? "primary" : "outline", onClick: () => onAnswer(item.checklistItemId, "yes"), children: "Yes" }), _jsx(Button, { type: "button", size: "sm", variant: item.answer === "no" ? "danger" : "outline", onClick: () => onAnswer(item.checklistItemId, "no"), children: "No" }), _jsx(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => setExpanded((v) => !v), children: expanded ? "Hide note" : "Add note" })] })] }), (item.steps?.length ||
53
+ item.expectedBehaviour ||
54
+ item.expectedUiState ||
55
+ item.endResult ||
56
+ item.inputs ||
57
+ item.expectedData) && (_jsxs(Details, { tone: "card", padding: "sm", defaultOpen: false, children: [_jsx(Summary, { children: item.steps?.length ? `Steps (${item.steps.length})` : "Expected result" }), _jsxs(Stack, { gap: "sm", children: [item.startPage && (_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "xs", weight: "semibold", color: "muted", children: "Start" }), _jsx(Text, { size: "sm", children: item.startPage })] })), item.steps?.length ? (_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "xs", weight: "semibold", color: "muted", children: "Steps" }), _jsx(Ol, { marker: "decimal", indent: "md", spacing: "tight", size: "sm", children: item.steps.map((step, i) => (_jsx(Li, { children: step }, i))) })] })) : null, _jsx(KeyValueBlock, { label: "Enter exactly", data: item.inputs }), item.expectedBehaviour && (_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "xs", weight: "semibold", color: "muted", children: "Expected behaviour" }), _jsx(Text, { size: "sm", children: item.expectedBehaviour })] })), item.expectedUiState && (_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "xs", weight: "semibold", color: "muted", children: "Expected UI state" }), _jsx(Text, { size: "sm", children: item.expectedUiState })] })), _jsx(KeyValueBlock, { label: "Values that must be correct", data: item.expectedData }), item.endResult && (_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "xs", weight: "semibold", color: "muted", children: "After reload" }), _jsx(Text, { size: "sm", children: item.endResult })] }))] })] })), expanded && (_jsx(Div, { padding: "t-sm", border: "default", className: "border-t", children: _jsxs(Stack, { gap: "sm", children: [_jsx(FieldTextarea, { name: `comment-${item.checklistItemId}`, label: "Comment", value: comment, onChange: setComment, placeholder: "What did you notice? Colors, styles, readability, bugs..." }), _jsx(MediaUploadField, { label: "Screenshot (optional)", value: screenshotUrl, onChange: setScreenshotUrl, onUpload: handleUpload, kind: "image" }), _jsx(Row, { children: _jsx(Button, { type: "button", action: ACTIONS.TESTER["save-note"], isLoading: saving, onClick: handleSave }) })] }) }))] }));
29
58
  }
@@ -25,6 +25,14 @@ function TesterChecklistPageSection({ page, testerDisplayName, onAnswer, onSaveN
25
25
  label: item.label,
26
26
  description: item.description,
27
27
  href: item.href,
28
+ roles: item.roles,
29
+ startPage: item.startPage,
30
+ steps: item.steps,
31
+ inputs: item.inputs,
32
+ expectedBehaviour: item.expectedBehaviour,
33
+ expectedUiState: item.expectedUiState,
34
+ expectedData: item.expectedData,
35
+ endResult: item.endResult,
28
36
  answer: item.answer,
29
37
  comment: item.comment,
30
38
  screenshotUrl: item.screenshotUrl,
@@ -15,9 +15,24 @@ export declare class TesterChecklistItemRepository extends BaseRepository<Tester
15
15
  /** Reopens a fixed, bug-confirmed case as a new, active version for retest.
16
16
  * The old item stays disabled forever with its bug-hunter credit intact. */
17
17
  reopenAsNewVersion(oldItemId: string): Promise<TesterChecklistItemDocument>;
18
+ /**
19
+ * Ids of automated (non-human) accounts, excluded from the PUBLIC leaderboard.
20
+ *
21
+ * Read as a bare collection query rather than through `userRepository` on purpose:
22
+ * no repository in this codebase imports another, and this needs a key set, not
23
+ * user documents. `.select()` with no fields returns refs only, so the read is as
24
+ * cheap as Firestore allows, and `isBot == true` is a single-field equality served
25
+ * by the automatic index — no composite index to declare.
26
+ */
27
+ private botHunterIds;
18
28
  /** Single-query, in-memory aggregation of bug credits per hunter — mirrors
19
29
  * EventEntryRepository.getLeaderboard()'s shape. Includes old/disabled/
20
- * superseded items, since bug credit is permanent. */
30
+ * superseded items, since bug credit is permanent.
31
+ *
32
+ * Bot accounts are aggregated out (not merely ranked last): the board exists to
33
+ * credit people, and a runner working all 943 cases would otherwise dominate it.
34
+ * The credit still lives on the item, so admin triage and the item's own
35
+ * `bugHunterName` are unaffected. */
21
36
  getBugHunterLeaderboard(limit?: number): Promise<BugHunterLeaderboardEntry[]>;
22
37
  }
23
38
  export declare const testerChecklistItemRepository: TesterChecklistItemRepository;
@@ -1,6 +1,7 @@
1
1
  import { BaseRepository, parseSieveDateValue } from "../../../providers/db-firebase";
2
2
  import { buildSearchTxt } from "../../../utils/search-txt";
3
3
  import { DatabaseError } from "../../../errors";
4
+ import { USER_COLLECTION } from "../../auth/schemas/firestore";
4
5
  import { TESTER_CHECKLIST_ITEM_COLLECTION, TESTER_CHECKLIST_ITEM_FIELDS, createChecklistItemId, } from "../schemas/firestore";
5
6
  export class TesterChecklistItemRepository extends BaseRepository {
6
7
  constructor() {
@@ -86,18 +87,43 @@ export class TesterChecklistItemRepository extends BaseRepository {
86
87
  await this.update(old.id, { supersededByItemId: newId });
87
88
  return newItem;
88
89
  }
90
+ /**
91
+ * Ids of automated (non-human) accounts, excluded from the PUBLIC leaderboard.
92
+ *
93
+ * Read as a bare collection query rather than through `userRepository` on purpose:
94
+ * no repository in this codebase imports another, and this needs a key set, not
95
+ * user documents. `.select()` with no fields returns refs only, so the read is as
96
+ * cheap as Firestore allows, and `isBot == true` is a single-field equality served
97
+ * by the automatic index — no composite index to declare.
98
+ */
99
+ async botHunterIds() {
100
+ const snapshot = await this.db
101
+ .collection(USER_COLLECTION)
102
+ .where("isBot", "==", true)
103
+ .select()
104
+ .get();
105
+ return new Set(snapshot.docs.map((d) => d.id));
106
+ }
89
107
  /** Single-query, in-memory aggregation of bug credits per hunter — mirrors
90
108
  * EventEntryRepository.getLeaderboard()'s shape. Includes old/disabled/
91
- * superseded items, since bug credit is permanent. */
109
+ * superseded items, since bug credit is permanent.
110
+ *
111
+ * Bot accounts are aggregated out (not merely ranked last): the board exists to
112
+ * credit people, and a runner working all 943 cases would otherwise dominate it.
113
+ * The credit still lives on the item, so admin triage and the item's own
114
+ * `bugHunterName` are unaffected. */
92
115
  async getBugHunterLeaderboard(limit = 50) {
93
- const snapshot = await this.db
94
- .collection(this.collection)
95
- .where(TESTER_CHECKLIST_ITEM_FIELDS.BUG_CONFIRMED, "==", true)
96
- .get();
116
+ const [snapshot, botIds] = await Promise.all([
117
+ this.db
118
+ .collection(this.collection)
119
+ .where(TESTER_CHECKLIST_ITEM_FIELDS.BUG_CONFIRMED, "==", true)
120
+ .get(),
121
+ this.botHunterIds(),
122
+ ]);
97
123
  const byHunter = new Map();
98
124
  for (const doc of snapshot.docs) {
99
125
  const item = this.mapDoc(doc);
100
- if (!item.bugHunterId)
126
+ if (!item.bugHunterId || botIds.has(item.bugHunterId))
101
127
  continue;
102
128
  const entry = byHunter.get(item.bugHunterId) ?? {
103
129
  name: item.bugHunterName ?? "Unknown tester",
@@ -13,6 +13,101 @@ export interface TesterChecklistItemDocument extends BaseDocument {
13
13
  pageLabel: string;
14
14
  label: string;
15
15
  description?: string;
16
+ /**
17
+ * WHICH ROLES this case affects. Not decoration — it changes what "correct" means.
18
+ *
19
+ * The same page is a different page per role: /products/<slug> shows a buyer
20
+ * Add to Cart and Make Offer, shows the OWNING seller Edit listing and no Make
21
+ * Offer (you cannot bid on your own listing), shows an admin moderation
22
+ * controls, and shows a guest a sign-in prompt. So `expectedUiState` is
23
+ * meaningless without this, and a case omitting it is really three untested
24
+ * cases wearing one label.
25
+ *
26
+ * 🛑 A LIST, because the interesting content is usually the DIFFERENCE between
27
+ * roles — "the owning seller must NOT see Make Offer where a buyer does" is one
28
+ * case, not two, and splitting it loses the comparison that is the whole point.
29
+ * Where roles differ, `expectedBehaviour` and `expectedUiState` say so in prose
30
+ * ("Buyer: … Owning seller: …"). Deliberately NOT a parallel per-role structure:
31
+ * that would be four more fields to keep in sync for something a sentence
32
+ * expresses perfectly well.
33
+ *
34
+ * Also drives which session the automated harness browses with.
35
+ */
36
+ roles?: TesterCaseRole[];
37
+ /** WHERE the tester begins. One unambiguous URL, unprefixed (/products, not /en/products). */
38
+ startPage?: string;
39
+ /**
40
+ * WHAT the tester does. One action per entry, concrete fixtures named, literal
41
+ * values ("Enter 780", not "enter an amount below the listed price") so two
42
+ * runs are comparable and a regression can be spotted by diffing.
43
+ *
44
+ * 🛑 No assertions in here — they belong in the three expectation fields. A
45
+ * step reading "click Save and confirm it worked" is two things wearing one
46
+ * number, and the confirming half silently goes unchecked.
47
+ *
48
+ * Length varies with the case. "This page renders" is one honest step; a
49
+ * multi-role negotiation is fifteen. There is no target, and padding toward one
50
+ * produces the mechanical "Open X. Verify Y." shape that fills the field while
51
+ * encoding nothing the label did not already say.
52
+ */
53
+ steps?: string[];
54
+ /**
55
+ * The exact data the tester enters or selects, as flat key/value pairs.
56
+ *
57
+ * 🛑 WHY THIS IS NOT JUST PROSE
58
+ *
59
+ * "Enter 780, the listed price is 999" reads fine and still lets two testers type
60
+ * two different numbers, and gives an automated runner nothing to consume but a
61
+ * sentence. The steps say WHAT TO DO; this says WITH WHAT — so the procedure is
62
+ * repeatable by construction and a runner can fill a form from it directly.
63
+ *
64
+ * Flat and scalar on purpose: nested objects invite a shape nobody validates, and
65
+ * every real value here is a string, number or boolean.
66
+ *
67
+ * Omitted entirely by a case that enters nothing — a read-only page has no inputs.
68
+ */
69
+ inputs?: Record<string, string | number | boolean>;
70
+ /**
71
+ * What the system must DO — the functional outcome, including side effects no
72
+ * screen shows (a notification sent, stock decremented, an order created).
73
+ *
74
+ * Kept apart from `expectedUiState` because the gap between them is a real bug
75
+ * class: the write succeeds and the screen never updates.
76
+ */
77
+ expectedBehaviour?: string;
78
+ /**
79
+ * What the tester must SEE — exact on-screen text and control states.
80
+ *
81
+ * Quote the screen ("the button reads 'Offer sent'"), never "the UI updates
82
+ * correctly", which cannot be checked against a screenshot. This is what makes
83
+ * the mandatory screenshot reviewable: the image is compared to a written
84
+ * claim instead of to a guess about what "works" looked like.
85
+ */
86
+ expectedUiState?: string;
87
+ /**
88
+ * The values that must be CORRECT afterwards, as flat key/value pairs.
89
+ *
90
+ * `expectedUiState` settles what the screen looks like; this settles whether the
91
+ * data behind it is right. The two are different questions, and a screen can be
92
+ * entirely plausible over a wrong number — which is most of this codebase's bug
93
+ * history, where a defect returns HTTP 200 with believable rows.
94
+ *
95
+ * Machine-readable so a runner asserts against it rather than re-reading English,
96
+ * and so "it looked right" becomes a comparison.
97
+ *
98
+ * Omitted by a case with nothing checkable behind the screen.
99
+ */
100
+ expectedData?: Record<string, string | number | boolean>;
101
+ /**
102
+ * What is still true after a RELOAD — the persistence oracle.
103
+ *
104
+ * Several of the worst bugs here returned a normal 200, rendered
105
+ * optimistically, and wrote nothing (the grouped-listing rename, the dropped
106
+ * address landmark). Reloading is the only thing that catches them. Where a
107
+ * case genuinely persists nothing, say so explicitly rather than leaving the
108
+ * field to mean two different things.
109
+ */
110
+ endResult?: string;
16
111
  href?: string;
17
112
  order: number;
18
113
  phase: number;
@@ -48,6 +143,20 @@ export interface BugHunterLeaderboardEntry {
48
143
  }
49
144
  export declare function slugifyChecklistLabel(label: string): string;
50
145
  export declare function createChecklistItemId(groupKey: string, pageKey: string, label: string): string;
146
+ /**
147
+ * Whose point of view a checklist case is written from.
148
+ *
149
+ * `guest` is a real role, not the absence of one — a signed-out visitor sees a
150
+ * sign-in prompt where a buyer sees a wishlist heart, and that path had never
151
+ * been exercised before this field existed.
152
+ *
153
+ * `seller` means the seller who OWNS the listing under test. A different
154
+ * seller looking at someone else's listing is a `buyer` for that case — the
155
+ * distinction matters because the owning seller is precisely who must NOT see
156
+ * Make Offer.
157
+ */
158
+ export declare const TESTER_CASE_ROLES: readonly ["guest", "buyer", "seller", "admin", "employee"];
159
+ export type TesterCaseRole = (typeof TESTER_CASE_ROLES)[number];
51
160
  export type TesterAnswer = "yes" | "no";
52
161
  export type TesterFeedbackStatus = "new" | "reviewed";
53
162
  export interface TesterChecklistResponseDocument extends BaseDocument {
@@ -42,6 +42,20 @@ export function slugifyChecklistLabel(label) {
42
42
  export function createChecklistItemId(groupKey, pageKey, label) {
43
43
  return generateChecklistItemId({ groupKey, pageKey, label });
44
44
  }
45
+ // --- Checklist response (tester answers) --------------------------------------
46
+ /**
47
+ * Whose point of view a checklist case is written from.
48
+ *
49
+ * `guest` is a real role, not the absence of one — a signed-out visitor sees a
50
+ * sign-in prompt where a buyer sees a wishlist heart, and that path had never
51
+ * been exercised before this field existed.
52
+ *
53
+ * `seller` means the seller who OWNS the listing under test. A different
54
+ * seller looking at someone else's listing is a `buyer` for that case — the
55
+ * distinction matters because the owning seller is precisely who must NOT see
56
+ * Make Offer.
57
+ */
58
+ export const TESTER_CASE_ROLES = ["guest", "buyer", "seller", "admin", "employee"];
45
59
  export const TESTER_CHECKLIST_RESPONSE_COLLECTION = "testerChecklistResponses";
46
60
  export const TESTER_CHECKLIST_RESPONSE_INDEXED_FIELDS = [
47
61
  "testerId",
@@ -0,0 +1,23 @@
1
+ import type { TesterCaseRole } from "../schemas";
2
+ interface MoneyFlowCase {
3
+ key: string;
4
+ label: string;
5
+ /** Every role the case affects; per-role detail goes inline in the expectations. */
6
+ roles: TesterCaseRole[];
7
+ startPage?: string;
8
+ steps: string[];
9
+ inputs?: Record<string, string | number | boolean>;
10
+ expectedBehaviour: string;
11
+ expectedUiState: string;
12
+ expectedData?: Record<string, string | number | boolean>;
13
+ endResult: string;
14
+ href?: string;
15
+ }
16
+ interface MoneyFlowPage {
17
+ pageKey: string;
18
+ pageLabel: string;
19
+ href?: string;
20
+ cases: MoneyFlowCase[];
21
+ }
22
+ export declare const moneyFlowsPages: MoneyFlowPage[];
23
+ export {};