@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.
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +32 -2
- package/dist/_internal/server/features/seo/sitemap.js +2 -2
- package/dist/_internal/server/functions/scheduled.d.ts +1 -2
- package/dist/_internal/server/functions/scheduled.js +8 -9
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +15 -0
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +15 -6
- package/dist/_internal/server/jobs/handlers/index.d.ts +0 -1
- package/dist/_internal/server/jobs/handlers/index.js +0 -1
- package/dist/features/auth/schemas/firestore.d.ts +1 -0
- package/dist/features/tester/components/TesterChecklistStepRow.d.ts +9 -1
- package/dist/features/tester/components/TesterChecklistStepRow.js +31 -2
- package/dist/features/tester/components/TesterHubView.js +8 -0
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +16 -1
- package/dist/features/tester/repository/tester-checklist-item.repository.js +32 -6
- package/dist/features/tester/schemas/firestore.d.ts +109 -0
- package/dist/features/tester/schemas/firestore.js +14 -0
- package/dist/features/tester/seed-data/_money-flows.d.ts +23 -0
- package/dist/features/tester/seed-data/_money-flows.js +430 -0
- package/dist/features/tester/seed-data/authored/_types.d.ts +33 -0
- package/dist/features/tester/seed-data/authored/_types.js +35 -0
- package/dist/features/tester/seed-data/authored/account-auth__profile-settings.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/account-auth__profile-settings.js +207 -0
- package/dist/features/tester/seed-data/authored/account-auth__signup-login.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/account-auth__signup-login.js +298 -0
- package/dist/features/tester/seed-data/authored/account-auth__testing-program--admin.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/account-auth__testing-program--admin.js +33 -0
- package/dist/features/tester/seed-data/authored/account-auth__testing-program.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/account-auth__testing-program.js +138 -0
- package/dist/features/tester/seed-data/authored/addresses__address-filters.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/addresses__address-filters.js +36 -0
- package/dist/features/tester/seed-data/authored/addresses__postal-lookup.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/addresses__postal-lookup.js +75 -0
- package/dist/features/tester/seed-data/authored/addresses__postal-validation.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/addresses__postal-validation.js +72 -0
- package/dist/features/tester/seed-data/authored/addresses__state-picker.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/addresses__state-picker.js +54 -0
- package/dist/features/tester/seed-data/authored/addresses__unban-request.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/addresses__unban-request.js +39 -0
- package/dist/features/tester/seed-data/authored/admin__bans-and-trust.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__bans-and-trust.js +226 -0
- package/dist/features/tester/seed-data/authored/admin__blog-faqs.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__blog-faqs.js +82 -0
- package/dist/features/tester/seed-data/authored/admin__bulk-actions.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__bulk-actions.js +177 -0
- package/dist/features/tester/seed-data/authored/admin__bundles.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__bundles.js +97 -0
- package/dist/features/tester/seed-data/authored/admin__buyer-data-admin.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__buyer-data-admin.js +232 -0
- package/dist/features/tester/seed-data/authored/admin__catalog-listings.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__catalog-listings.js +306 -0
- package/dist/features/tester/seed-data/authored/admin__category-brand-authoring.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__category-brand-authoring.js +158 -0
- package/dist/features/tester/seed-data/authored/admin__classifieds-digitalcodes-live.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__classifieds-digitalcodes-live.js +78 -0
- package/dist/features/tester/seed-data/authored/admin__content-deletes.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__content-deletes.js +261 -0
- package/dist/features/tester/seed-data/authored/admin__content-marketing.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__content-marketing.js +151 -0
- package/dist/features/tester/seed-data/authored/admin__coupons.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__coupons.js +214 -0
- package/dist/features/tester/seed-data/authored/admin__events-raffles-spin.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__events-raffles-spin.js +160 -0
- package/dist/features/tester/seed-data/authored/admin__media-watermark.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__media-watermark.js +104 -0
- package/dist/features/tester/seed-data/authored/admin__orders-fulfillment.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__orders-fulfillment.js +350 -0
- package/dist/features/tester/seed-data/authored/admin__prize-draws-lotteries.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__prize-draws-lotteries.js +105 -0
- package/dist/features/tester/seed-data/authored/admin__site-system.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__site-system.js +590 -0
- package/dist/features/tester/seed-data/authored/admin__uncovered-admin-pages.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__uncovered-admin-pages.js +235 -0
- package/dist/features/tester/seed-data/authored/admin__users-trust.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/admin__users-trust.js +569 -0
- package/dist/features/tester/seed-data/authored/buying__bidding.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/buying__bidding.js +417 -0
- package/dist/features/tester/seed-data/authored/buying__browsing-search.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/buying__browsing-search.js +653 -0
- package/dist/features/tester/seed-data/authored/buying__buying-checkout.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/buying__buying-checkout.js +533 -0
- package/dist/features/tester/seed-data/authored/buying__buying-coupons.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/buying__buying-coupons.js +379 -0
- package/dist/features/tester/seed-data/authored/buying__cart.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/buying__cart.js +1171 -0
- package/dist/features/tester/seed-data/authored/buying__image-tile-layout.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/buying__image-tile-layout.js +140 -0
- package/dist/features/tester/seed-data/authored/buying__my-orders.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/buying__my-orders.js +257 -0
- package/dist/features/tester/seed-data/authored/buying__offers.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/buying__offers.js +1111 -0
- package/dist/features/tester/seed-data/authored/buying__order-status-lifecycle.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/buying__order-status-lifecycle.js +252 -0
- package/dist/features/tester/seed-data/authored/buying__product-detail.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/buying__product-detail.js +387 -0
- package/dist/features/tester/seed-data/authored/buying__return-request.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/buying__return-request.js +152 -0
- package/dist/features/tester/seed-data/authored/buying__reviews-pagination.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/buying__reviews-pagination.js +242 -0
- package/dist/features/tester/seed-data/authored/buying__reviews.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/buying__reviews.js +113 -0
- package/dist/features/tester/seed-data/authored/buying__user-dashboard-extras.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/buying__user-dashboard-extras.js +283 -0
- package/dist/features/tester/seed-data/authored/buying__user-dashboard-navigation.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/buying__user-dashboard-navigation.js +167 -0
- package/dist/features/tester/seed-data/authored/buying__user-uncovered-pages.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/buying__user-uncovered-pages.js +90 -0
- package/dist/features/tester/seed-data/authored/buying__wishlist-history.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/buying__wishlist-history.js +344 -0
- package/dist/features/tester/seed-data/authored/community-support__public-profile.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/community-support__public-profile.js +53 -0
- package/dist/features/tester/seed-data/authored/community-support__support-tickets.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/community-support__support-tickets.js +76 -0
- package/dist/features/tester/seed-data/authored/content-discovery__blog.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__blog.js +93 -0
- package/dist/features/tester/seed-data/authored/content-discovery__category-brand-relations.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__category-brand-relations.js +149 -0
- package/dist/features/tester/seed-data/authored/content-discovery__coupons.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__coupons.js +121 -0
- package/dist/features/tester/seed-data/authored/content-discovery__event-participation.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__event-participation.js +207 -0
- package/dist/features/tester/seed-data/authored/content-discovery__events.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__events.js +318 -0
- package/dist/features/tester/seed-data/authored/content-discovery__faq-help.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__faq-help.js +65 -0
- package/dist/features/tester/seed-data/authored/content-discovery__notifications.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__notifications.js +147 -0
- package/dist/features/tester/seed-data/authored/content-discovery__search.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__search.js +332 -0
- package/dist/features/tester/seed-data/authored/cta-layout__checkout-bottom-bar.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/cta-layout__checkout-bottom-bar.js +53 -0
- package/dist/features/tester/seed-data/authored/cta-layout__dialog-footers.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/cta-layout__dialog-footers.js +52 -0
- package/dist/features/tester/seed-data/authored/cta-layout__editor-action-bar.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/cta-layout__editor-action-bar.js +35 -0
- package/dist/features/tester/seed-data/authored/cta-layout__navbar-ctas.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/cta-layout__navbar-ctas.js +141 -0
- package/dist/features/tester/seed-data/authored/cta-layout__product-bottom-bar.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/cta-layout__product-bottom-bar.js +35 -0
- package/dist/features/tester/seed-data/authored/design-ux__back-to-top-button.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/design-ux__back-to-top-button.js +95 -0
- package/dist/features/tester/seed-data/authored/design-ux__carousel-arrow-bounds.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/design-ux__carousel-arrow-bounds.js +191 -0
- package/dist/features/tester/seed-data/authored/design-ux__dashboard-layout.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/design-ux__dashboard-layout.js +158 -0
- package/dist/features/tester/seed-data/authored/design-ux__footer-theme.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/design-ux__footer-theme.js +106 -0
- package/dist/features/tester/seed-data/authored/design-ux__form-validation-errors.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/design-ux__form-validation-errors.js +88 -0
- package/dist/features/tester/seed-data/authored/design-ux__general-design.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/design-ux__general-design.js +204 -0
- package/dist/features/tester/seed-data/authored/design-ux__hand-mode-layout.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/design-ux__hand-mode-layout.js +301 -0
- package/dist/features/tester/seed-data/authored/design-ux__homepage-carousels.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/design-ux__homepage-carousels.js +407 -0
- package/dist/features/tester/seed-data/authored/design-ux__status-badge-legibility.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/design-ux__status-badge-legibility.js +214 -0
- package/dist/features/tester/seed-data/authored/design-ux__sticky-cta-bar.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/design-ux__sticky-cta-bar.js +275 -0
- package/dist/features/tester/seed-data/authored/index.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/index.js +237 -0
- package/dist/features/tester/seed-data/authored/page-wiring__data-loss.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/page-wiring__data-loss.js +102 -0
- package/dist/features/tester/seed-data/authored/page-wiring__detail-pages.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/page-wiring__detail-pages.js +50 -0
- package/dist/features/tester/seed-data/authored/page-wiring__drawer-pages.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/page-wiring__drawer-pages.js +66 -0
- package/dist/features/tester/seed-data/authored/page-wiring__reachability.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +115 -0
- package/dist/features/tester/seed-data/authored/public-pages__auth-error-pages.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/public-pages__auth-error-pages.js +136 -0
- package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +69 -0
- package/dist/features/tester/seed-data/authored/public-pages__core-listing-pages.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/public-pages__core-listing-pages.js +263 -0
- package/dist/features/tester/seed-data/authored/public-pages__help-how-it-works.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/public-pages__help-how-it-works.js +102 -0
- package/dist/features/tester/seed-data/authored/public-pages__help-scams-guides-subpages.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/public-pages__help-scams-guides-subpages.js +234 -0
- package/dist/features/tester/seed-data/authored/public-pages__legal-policy-pages.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/public-pages__legal-policy-pages.js +126 -0
- package/dist/features/tester/seed-data/authored/public-pages__stores-sellers-directories.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/public-pages__stores-sellers-directories.js +255 -0
- package/dist/features/tester/seed-data/authored/search-and-nav__employee-permissions.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/search-and-nav__employee-permissions.js +59 -0
- package/dist/features/tester/seed-data/authored/search-and-nav__header-search.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/search-and-nav__header-search.js +88 -0
- package/dist/features/tester/seed-data/authored/search-and-nav__settings-deep-links.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/search-and-nav__settings-deep-links.js +51 -0
- package/dist/features/tester/seed-data/authored/search-and-nav__sidebar-search.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/search-and-nav__sidebar-search.js +35 -0
- package/dist/features/tester/seed-data/authored/selling__become-seller.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__become-seller.js +94 -0
- package/dist/features/tester/seed-data/authored/selling__final-sale-authoring.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__final-sale-authoring.js +107 -0
- package/dist/features/tester/seed-data/authored/selling__listing-a-product.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__listing-a-product.js +285 -0
- package/dist/features/tester/seed-data/authored/selling__listing-lifecycle.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__listing-lifecycle.js +326 -0
- package/dist/features/tester/seed-data/authored/selling__listing-type-fields-roundtrip.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__listing-type-fields-roundtrip.js +136 -0
- package/dist/features/tester/seed-data/authored/selling__media-limits.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__media-limits.js +128 -0
- package/dist/features/tester/seed-data/authored/selling__sectionised-forms.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__sectionised-forms.js +167 -0
- package/dist/features/tester/seed-data/authored/selling__seller-analytics-payouts.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__seller-analytics-payouts.js +117 -0
- package/dist/features/tester/seed-data/authored/selling__seller-bids-bundles-filters.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__seller-bids-bundles-filters.js +88 -0
- package/dist/features/tester/seed-data/authored/selling__seller-catalog-org.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__seller-catalog-org.js +151 -0
- package/dist/features/tester/seed-data/authored/selling__seller-custom-brands.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__seller-custom-brands.js +105 -0
- package/dist/features/tester/seed-data/authored/selling__seller-guide.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__seller-guide.js +33 -0
- package/dist/features/tester/seed-data/authored/selling__seller-listing-types.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__seller-listing-types.js +347 -0
- package/dist/features/tester/seed-data/authored/selling__seller-marketing-extras.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__seller-marketing-extras.js +166 -0
- package/dist/features/tester/seed-data/authored/selling__seller-ops-comms.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__seller-ops-comms.js +89 -0
- package/dist/features/tester/seed-data/authored/selling__seller-orders.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__seller-orders.js +195 -0
- package/dist/features/tester/seed-data/authored/selling__seller-shipping-payouts-setup.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__seller-shipping-payouts-setup.js +180 -0
- package/dist/features/tester/seed-data/authored/selling__store-dashboard-navigation.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__store-dashboard-navigation.js +164 -0
- package/dist/features/tester/seed-data/authored/selling__store-uncovered-pages.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__store-uncovered-pages.js +116 -0
- package/dist/features/tester/seed-data/authored/seo__canonical-and-host.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/seo__canonical-and-host.js +111 -0
- package/dist/features/tester/seed-data/authored/seo__og-images.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/seo__og-images.js +91 -0
- package/dist/features/tester/seed-data/authored/seo__page-metadata.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/seo__page-metadata.js +122 -0
- package/dist/features/tester/seed-data/authored/seo__sitemap-and-robots.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/seo__sitemap-and-robots.js +105 -0
- package/dist/features/tester/seed-data/events-tester-seed-data.js +77 -5
- package/dist/features/tester/seed-data/offers-tester-seed-data.js +23 -9
- package/dist/features/tester/seed-data/products-tester-seed-data.js +65 -49
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +744 -1
- package/dist/features/tester/seed-data/tester-window.d.ts +25 -0
- package/dist/features/tester/seed-data/tester-window.js +66 -0
- package/dist/seed/carousel-slides-seed-data.js +1 -1
- package/dist/seed/products-standard-seed-data.js +2 -2
- package/dist/seed/reviews-seed-data.js +6 -1
- package/dist/seed/users-seed-data.js +107 -4
- package/package.json +1 -1
- 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
|
-
|
|
100
|
-
|
|
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`
|
|
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
|
|
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,
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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)
|
|
@@ -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" })] })] }),
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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 {};
|