@mohasinac/appkit 4.43.0 → 4.44.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 (220) hide show
  1. package/dist/_internal/server/features/categories/adapters.d.ts +47 -0
  2. package/dist/_internal/server/features/categories/adapters.js +235 -0
  3. package/dist/_internal/server/features/checkout/actions.js +161 -22
  4. package/dist/_internal/server/features/checkout/expand-order-items.js +34 -0
  5. package/dist/_internal/server/features/orders/adapters.js +21 -0
  6. package/dist/_internal/server/features/refunds/actions.js +1 -1
  7. package/dist/_internal/server/features/seo/sitemap.js +54 -15
  8. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  9. package/dist/_internal/server/features/stores/adapters.js +6 -0
  10. package/dist/_internal/server/functions/scheduled.d.ts +4 -2
  11. package/dist/_internal/server/functions/scheduled.js +64 -6
  12. package/dist/_internal/server/jobs/core/adminAnalytics.d.ts +0 -1
  13. package/dist/_internal/server/jobs/core/adminAnalytics.js +9 -1
  14. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.d.ts +47 -0
  15. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +468 -0
  16. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +14 -1
  17. package/dist/_internal/server/jobs/core/countersReconcile.js +24 -8
  18. package/dist/_internal/server/jobs/core/onCategoryWrite.js +54 -0
  19. package/dist/_internal/server/jobs/core/priceIndexRollup.d.ts +47 -0
  20. package/dist/_internal/server/jobs/core/priceIndexRollup.js +234 -0
  21. package/dist/_internal/server/jobs/core/{testerSandboxCleanup.d.ts → testDataCleanup.d.ts} +3 -3
  22. package/dist/_internal/server/jobs/core/{testerSandboxCleanup.js → testDataCleanup.js} +3 -3
  23. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +5 -1
  24. package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.d.ts +2 -0
  25. package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.js +2 -0
  26. package/dist/_internal/server/jobs/handlers/index.d.ts +3 -1
  27. package/dist/_internal/server/jobs/handlers/index.js +3 -1
  28. package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -2
  29. package/dist/_internal/server/jobs/handlers/messages.js +5 -2
  30. package/dist/_internal/server/jobs/handlers/priceIndexRollup.d.ts +2 -0
  31. package/dist/_internal/server/jobs/handlers/priceIndexRollup.js +2 -0
  32. package/dist/_internal/server/jobs/handlers/testDataCleanup.d.ts +2 -0
  33. package/dist/_internal/server/jobs/handlers/testDataCleanup.js +2 -0
  34. package/dist/_internal/shared/actions/action-registry.js +34 -0
  35. package/dist/_internal/shared/checkout/order-math.d.ts +113 -3
  36. package/dist/_internal/shared/checkout/order-math.js +194 -7
  37. package/dist/_internal/shared/features/categories/price-guidance.d.ts +110 -0
  38. package/dist/_internal/shared/features/categories/price-guidance.js +136 -0
  39. package/dist/_internal/shared/features/site-settings/gst-registration.d.ts +90 -0
  40. package/dist/_internal/shared/features/site-settings/gst-registration.js +126 -0
  41. package/dist/_internal/shared/fees/calculator.d.ts +105 -0
  42. package/dist/_internal/shared/fees/calculator.js +143 -0
  43. package/dist/_internal/shared/listing-types/art/config.d.ts +0 -6
  44. package/dist/_internal/shared/listing-types/classified/config.d.ts +0 -6
  45. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +0 -6
  46. package/dist/_internal/shared/listing-types/live/config.d.ts +0 -6
  47. package/dist/_internal/shared/listing-types/standard/config.d.ts +0 -6
  48. package/dist/_internal/shared/listing-types/stickers/config.d.ts +0 -6
  49. package/dist/_internal/shared/templating/placeholders.d.ts +70 -0
  50. package/dist/_internal/shared/templating/placeholders.js +73 -0
  51. package/dist/client.d.ts +2 -0
  52. package/dist/client.js +1 -0
  53. package/dist/constants/api-endpoints.d.ts +42 -0
  54. package/dist/constants/api-endpoints.js +14 -0
  55. package/dist/features/admin/actions/admin-actions.d.ts +1 -1
  56. package/dist/features/admin/actions/admin-actions.js +1 -1
  57. package/dist/features/admin/components/AdminCategoryEditorView.js +269 -3
  58. package/dist/features/admin/components/AdminProductEditorView.js +8 -1
  59. package/dist/features/admin/components/AdminProductsView.js +78 -7
  60. package/dist/features/admin/components/AdminSiteSettingsView.js +20 -1
  61. package/dist/features/admin/components/AdminTaxCodeEditorView.d.ts +10 -0
  62. package/dist/features/admin/components/AdminTaxCodeEditorView.js +160 -0
  63. package/dist/features/admin/components/AdminTaxCodesView.d.ts +13 -0
  64. package/dist/features/admin/components/AdminTaxCodesView.js +86 -0
  65. package/dist/features/admin/components/AdminTesterChecklistView.js +1 -1
  66. package/dist/features/admin/components/analytics/AdminTopProductsTable.js +1 -1
  67. package/dist/features/auth/schemas/firestore.d.ts +0 -1
  68. package/dist/features/blog/schemas/firestore.d.ts +2 -2
  69. package/dist/features/catalogue/components/CatalogueItemEditorView.js +14 -3
  70. package/dist/features/categories/api/[id]/description-template/route.d.ts +45 -0
  71. package/dist/features/categories/api/[id]/description-template/route.js +252 -0
  72. package/dist/features/categories/api/route.js +156 -11
  73. package/dist/features/categories/components/CategoryDetailPageView.js +43 -10
  74. package/dist/features/categories/components/CategoryTemplatePicker.d.ts +65 -0
  75. package/dist/features/categories/components/CategoryTemplatePicker.js +170 -0
  76. package/dist/features/categories/constants/listing-categories.d.ts +17 -0
  77. package/dist/features/categories/constants/listing-categories.js +53 -0
  78. package/dist/features/categories/hooks/useCategoryTree.js +28 -2
  79. package/dist/features/categories/repository/categories.repository.d.ts +62 -11
  80. package/dist/features/categories/repository/categories.repository.js +178 -35
  81. package/dist/features/categories/repository/price-index.repository.d.ts +115 -0
  82. package/dist/features/categories/repository/price-index.repository.js +85 -0
  83. package/dist/features/categories/schemas/category-content.d.ts +206 -0
  84. package/dist/features/categories/schemas/category-content.js +28 -0
  85. package/dist/features/categories/schemas/firestore.d.ts +74 -5
  86. package/dist/features/categories/schemas/firestore.js +74 -3
  87. package/dist/features/categories/schemas/index.d.ts +1047 -0
  88. package/dist/features/categories/schemas/index.js +96 -0
  89. package/dist/features/categories/server.d.ts +1 -0
  90. package/dist/features/categories/server.js +7 -0
  91. package/dist/features/categories/types/index.d.ts +9 -0
  92. package/dist/features/events/schemas/firestore.d.ts +2 -2
  93. package/dist/features/faq/schemas/firestore.d.ts +2 -1
  94. package/dist/features/faq/schemas/firestore.js +20 -8
  95. package/dist/features/orders/schemas/firestore.d.ts +70 -0
  96. package/dist/features/orders/types/index.d.ts +55 -0
  97. package/dist/features/products/columns/index.d.ts +0 -1
  98. package/dist/features/products/columns/index.js +5 -1
  99. package/dist/features/products/components/ProductFilters.d.ts +0 -9
  100. package/dist/features/products/components/ProductForm.d.ts +27 -1
  101. package/dist/features/products/components/ProductForm.js +26 -12
  102. package/dist/features/products/constants/sieve.d.ts +0 -6
  103. package/dist/features/products/constants/sieve.js +35 -2
  104. package/dist/features/products/repository/products.repository.d.ts +115 -1
  105. package/dist/features/products/repository/products.repository.js +491 -39
  106. package/dist/features/products/schemas/firestore.d.ts +65 -5
  107. package/dist/features/products/schemas/firestore.js +9 -0
  108. package/dist/features/products/schemas/product-features.d.ts +140 -2
  109. package/dist/features/products/schemas/product-features.js +62 -1
  110. package/dist/features/promotions/repository/coupons.repository.js +6 -1
  111. package/dist/features/promotions/schemas/firestore.d.ts +44 -1
  112. package/dist/features/promotions/schemas/firestore.js +76 -2
  113. package/dist/features/seller/actions/seller-actions.js +17 -2
  114. package/dist/features/seller/components/PrintCenterView.d.ts +3 -2
  115. package/dist/features/seller/components/PrintCenterView.js +3 -2
  116. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +21 -11
  117. package/dist/features/seller/components/SellerOrdersView.js +6 -1
  118. package/dist/features/seller/components/SellerPayoutSettingsView.js +19 -3
  119. package/dist/features/seller/components/SellerProductShell.d.ts +50 -2
  120. package/dist/features/seller/components/SellerProductShell.js +101 -9
  121. package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +13 -1
  122. package/dist/features/seller/components/SellerProductsFilterDrawer.js +2 -2
  123. package/dist/features/seller/components/SellerProductsView.js +108 -3
  124. package/dist/features/seller/types/index.d.ts +17 -0
  125. package/dist/features/seller/utils/product-draft-mapping.d.ts +13 -0
  126. package/dist/features/seller/utils/product-draft-mapping.js +17 -0
  127. package/dist/features/store-extensions/constants/alert-metrics.d.ts +86 -0
  128. package/dist/features/store-extensions/constants/alert-metrics.js +142 -0
  129. package/dist/features/store-extensions/schemas/firestore.d.ts +19 -1
  130. package/dist/features/stores/schemas/firestore.d.ts +31 -2
  131. package/dist/features/tax-codes/repository/tax-codes.repository.d.ts +55 -0
  132. package/dist/features/tax-codes/repository/tax-codes.repository.js +131 -0
  133. package/dist/features/tax-codes/schemas/firestore.d.ts +125 -0
  134. package/dist/features/tax-codes/schemas/firestore.js +82 -0
  135. package/dist/features/tax-codes/schemas/tax-code-form.d.ts +63 -0
  136. package/dist/features/tax-codes/schemas/tax-code-form.js +152 -0
  137. package/dist/features/tax-codes/schemas/tax-codes.validators.d.ts +107 -0
  138. package/dist/features/tax-codes/schemas/tax-codes.validators.js +85 -0
  139. package/dist/features/tester/actions/index.d.ts +0 -1
  140. package/dist/features/tester/actions/index.js +0 -1
  141. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +1 -1
  142. package/dist/features/tester/components/AdminTesterFeedbackListView.js +1 -1
  143. package/dist/features/tester/components/index.d.ts +0 -4
  144. package/dist/features/tester/components/index.js +0 -2
  145. package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +1 -20
  146. package/dist/features/tester/repository/tester-checklist-item.repository.js +0 -52
  147. package/dist/features/tester/schemas/firestore.d.ts +0 -6
  148. package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +3 -3
  149. package/dist/features/tester/seed-data/authored/admin__site-system.js +4 -5
  150. package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +2 -2
  151. package/dist/features/tester/seed-data/authored/index.js +0 -2
  152. package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +0 -19
  153. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +8 -27
  154. package/dist/index.d.ts +21 -7
  155. package/dist/index.js +53 -7
  156. package/dist/next/routing/route-map.d.ts +2 -4
  157. package/dist/next/routing/route-map.js +1 -2
  158. package/dist/repositories/index.d.ts +1 -0
  159. package/dist/repositories/index.js +1 -0
  160. package/dist/schemas/registry.d.ts +506 -0
  161. package/dist/seed/_helpers/brand-rows.d.ts +35 -0
  162. package/dist/seed/_helpers/brand-rows.js +576 -0
  163. package/dist/seed/_helpers/category-content.d.ts +39 -0
  164. package/dist/seed/_helpers/category-content.js +634 -0
  165. package/dist/seed/_helpers/category-forest.d.ts +2 -0
  166. package/dist/seed/_helpers/category-forest.js +585 -243
  167. package/dist/seed/_helpers/category-models.d.ts +5 -0
  168. package/dist/seed/_helpers/category-models.js +1364 -0
  169. package/dist/seed/_helpers/category-tree.js +17 -1
  170. package/dist/seed/_helpers/character-features.d.ts +10 -0
  171. package/dist/seed/_helpers/character-features.js +118 -0
  172. package/dist/seed/_helpers/search-txt-wrappers.d.ts +10 -0
  173. package/dist/seed/_helpers/search-txt-wrappers.js +12 -1
  174. package/dist/seed/categories-seed-data.js +147 -169
  175. package/dist/seed/index.d.ts +1 -0
  176. package/dist/seed/index.js +1 -0
  177. package/dist/seed/manifest.d.ts +1 -1
  178. package/dist/seed/manifest.js +5 -0
  179. package/dist/seed/product-features-seed-data.d.ts +1 -8
  180. package/dist/seed/product-features-seed-data.js +358 -154
  181. package/dist/seed/products-art-seed-data.js +16 -4
  182. package/dist/seed/products-auctions-seed-data.js +6 -6
  183. package/dist/seed/products-classifieds-seed-data.js +17 -7
  184. package/dist/seed/products-digital-codes-seed-data.js +12 -12
  185. package/dist/seed/products-preorders-seed-data.js +8 -8
  186. package/dist/seed/products-prize-draws-seed-data.js +25 -8
  187. package/dist/seed/products-standard-seed-data.js +10 -60
  188. package/dist/seed/products-stickers-seed-data.js +6 -6
  189. package/dist/seed/site-settings-seed-data.js +24 -0
  190. package/dist/seed/store-extensions-seed-data.js +10 -4
  191. package/dist/seed/tax-codes-seed-data.d.ts +34 -0
  192. package/dist/seed/tax-codes-seed-data.js +100 -0
  193. package/dist/seed/test-data-ttl.d.ts +29 -0
  194. package/dist/seed/test-data-ttl.js +31 -0
  195. package/dist/server-entry.d.ts +1 -1
  196. package/dist/server-entry.js +6 -1
  197. package/dist/server.d.ts +2 -1
  198. package/dist/server.js +5 -2
  199. package/dist/styles.css +40 -0
  200. package/dist/ui/components/PaginatedSelect.d.ts +14 -0
  201. package/dist/ui/components/PaginatedSelect.js +0 -0
  202. package/dist/ui/components/PaginatedSelect.style.css +40 -0
  203. package/dist/utils/physical-location.d.ts +23 -0
  204. package/dist/utils/physical-location.js +52 -0
  205. package/dist/utils/search-txt-builders.d.ts +20 -0
  206. package/dist/utils/search-txt-builders.js +36 -0
  207. package/package.json +1 -1
  208. package/scripts/seed-cli.mjs +4 -1
  209. package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.d.ts +0 -2
  210. package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.js +0 -2
  211. package/dist/features/tester/actions/leaderboard-actions.d.ts +0 -3
  212. package/dist/features/tester/actions/leaderboard-actions.js +0 -5
  213. package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +0 -23
  214. package/dist/features/tester/components/BugHunterLeaderboardView.js +0 -18
  215. package/dist/features/tester/components/TesterHubView.d.ts +0 -5
  216. package/dist/features/tester/components/TesterHubView.js +0 -110
  217. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.d.ts +0 -2
  218. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +0 -78
  219. package/dist/features/tester/seed-data/tester-ttl.d.ts +0 -4
  220. package/dist/features/tester/seed-data/tester-ttl.js +0 -6
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The TTL stamp every disposable test fixture must carry.
3
+ *
4
+ * 🛑 `isTestData: true` WITHOUT `testDataExpiresAt` means PERMANENT — nothing
5
+ * ever reclaims it (see `jobs/handlers/_helpers.ts`, which documents the same
6
+ * rule from the sweep's side). That is not a theoretical hazard: until
7
+ * 2026-10-10 the only two `isTestData` rows in the whole seed,
8
+ * `product-tester-crossstore-a` / `-b`, were in exactly that state — live in
9
+ * the public catalogue of 72 products, never purged, and one of them was
10
+ * `store-letitrip-official`'s ONLY buyable standard product, so it was hidden
11
+ * from every real buyer by `hidePublicTestData` while being the only thing
12
+ * that store had to sell. Both are deleted.
13
+ *
14
+ * And the reason they were permanent is that **this module had zero callers**.
15
+ * The helper that enforces the rule existed, was documented as the rule that
16
+ * matters most, and no fixture used it. So: a fixture's three obligations are
17
+ * an id containing `-test-`, `isTestData: true`, and an expiry FROM HERE.
18
+ *
19
+ * Recomputed fresh on every seed run (seed files are re-imported per
20
+ * invocation), so re-seeding naturally refreshes the expiry rather than
21
+ * leaving a fixture that expired between runs.
22
+ *
23
+ * 🛑 Do NOT re-declare the day count elsewhere. It used to be declared twice —
24
+ * here and in `jobs/handlers/messages.ts` — with a comment as the only thing
25
+ * keeping them equal, which is the drift shape Root Cause #42 describes.
26
+ * `messages.ts` now re-exports this one.
27
+ */
28
+ export const TEST_DATA_TTL_DAYS = 7;
29
+ export function testDataExpiresAt() {
30
+ return new Date(Date.now() + TEST_DATA_TTL_DAYS * 24 * 60 * 60 * 1000);
31
+ }
@@ -67,7 +67,7 @@ export { getSearchResults, searchAction, type SearchQuery, } from "./_internal/s
67
67
  export { getCategoryForDetail, listRootCategories, listFeaturedCategories, listMenuCategories, getCategoryTree, listSitemapCategories, CATEGORIES_PAGE_SIZE, CATEGORIES_ROOT_TIER, CATEGORIES_MAX_DEPTH, CATEGORIES_SITEMAP_LIMIT, CATEGORIES_FEATURED_LIMIT, CATEGORIES_MENU_LIMIT, } from "./_internal/server/features/categories/index";
68
68
  export { getGroupedListingForDetail, getGroupedListingWithItems, listGroupedListings, listFeaturedGroupedListings, listSitemapGroupedListings, getGroupsForProduct, getGroupsWithItemsForProduct, GROUPED_LISTINGS_PAGE_SIZE, GROUPED_LISTINGS_FEATURED_LIMIT, GROUPED_LISTINGS_SITEMAP_LIMIT, buildGroupedListingMetadata, renderGroupedListingOg, type GroupedListingWithItems, type ListGroupedListingsParams, type SitemapGroupedListing, type GroupedListingMetadataOptions, } from "./_internal/server/features/grouped/index";
69
69
  export { createCheckoutOrderAction, attachPaymentAction, verifyAndPlacePhonePeOrderAction, previewCheckoutPricing, resolveShippingCost, formatShippingAddress, CHECKOUT_DEFAULT_COMMISSIONS, CHECKOUT_PAYMENT_METHODS, type CreateCheckoutOrderInput, type VerifyAndPlacePhonePeOrderInput, type CheckoutOrderResult, type CheckoutPaymentMethod, type CheckoutPricingPreviewInput, type CheckoutPricingPreview, } from "./_internal/server/features/checkout/index";
70
- export { unitPriceFor, lineTotalFor, computePreOrderDepositAmount, } from "./_internal/shared/checkout/order-math";
70
+ export { unitPriceFor, lineTotalFor, sumGroupGst, computePreOrderDepositAmount, type GroupGstSummary, type GstRateSlice, } from "./_internal/shared/checkout/order-math";
71
71
  export { processRefundAction, type ProcessRefundInput, } from "./_internal/server/features/refunds/actions";
72
72
  export { getSublistingCategoryForDetail } from "./_internal/server/features/sublisting-categories/index";
73
73
  export { getStoreForDetail, listStoreProductsInitial, listStoreAuctionsInitial, listStorePreOrdersInitial, listSitemapStores, STORES_PAGE_SIZE, STORES_PRODUCTS_PAGE_SIZE, STORES_SITEMAP_LIMIT, STORES_FEATURED_LIMIT, } from "./_internal/server/features/stores/index";
@@ -101,7 +101,12 @@ export { createCheckoutOrderAction, attachPaymentAction, verifyAndPlacePhonePeOr
101
101
  // consumer's Razorpay create-order route needs it: that route used to hand-roll
102
102
  // the rule and reproduced only the bundle branch, so it captured the LIST price
103
103
  // for an accepted offer. Pure math — no Firestore, no server-only imports.
104
- export { unitPriceFor, lineTotalFor, computePreOrderDepositAmount, } from "./_internal/shared/checkout/order-math";
104
+ // `sumGroupGst` joins them for exactly the same reason, one defect later: that
105
+ // same create-order route decides what the gateway CAPTURES and omitted product
106
+ // GST entirely, so the buyer was shown the tax, charged without it, and the
107
+ // order recorded none — with the verify-side mismatch guard omitting it too, so
108
+ // the three were consistently wrong and the undercharge was undetectable.
109
+ export { unitPriceFor, lineTotalFor, sumGroupGst, computePreOrderDepositAmount, } from "./_internal/shared/checkout/order-math";
105
110
  // S-SBUNI-RULES: refund action
106
111
  export { processRefundAction, } from "./_internal/server/features/refunds/actions";
107
112
  // S3: sublisting categories data layer
package/dist/server.d.ts CHANGED
@@ -231,6 +231,7 @@ export { POST } from "./features/categories/server";
231
231
  export { categoriesGET } from "./features/categories/server";
232
232
  export { categoryItemDELETE } from "./features/categories/server";
233
233
  export { categoryItemGET } from "./features/categories/server";
234
+ export { categoryDescriptionTemplateGET } from "./features/categories/server";
234
235
  export { categoryItemPATCH } from "./features/categories/server";
235
236
  export { createCategory } from "./features/categories/server";
236
237
  export { deleteCategory } from "./features/categories/server";
@@ -267,7 +268,6 @@ export { getEventPollResults } from "./features/events/server";
267
268
  export type { PollResultEntry, SpinResultEntry } from "./features/events/index";
268
269
  export { getEventSpinResults } from "./features/events/server";
269
270
  export { getUserSpinResultsForEvent } from "./features/events/server";
270
- export { getBugHunterLeaderboard } from "./features/tester/server";
271
271
  export { getPublicEventById } from "./features/events/server";
272
272
  export { listPublicEvents } from "./features/events/server";
273
273
  export { getRelatedEvents } from "./features/events/server";
@@ -427,6 +427,7 @@ export { removeFromWishlist } from "./features/wishlist/server";
427
427
  export { siteSettingsRepository } from "./repositories/index";
428
428
  export { storeRepository } from "./repositories/index";
429
429
  export { orderRepository } from "./repositories/index";
430
+ export { taxCodesRepository } from "./repositories/index";
430
431
  export type { OrderDocument } from "./features/orders/index";
431
432
  export type { OrderDocumentItem } from "./features/orders/index";
432
433
  export { serverLogger } from "./monitoring/index";
package/dist/server.js CHANGED
@@ -665,6 +665,8 @@ export { categoryItemDELETE } from "./features/categories/server";
665
665
  // [SERVER-ONLY]-Server-only — uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
666
666
  // categoryItemGET - Shared export for category item get.
667
667
  export { categoryItemGET } from "./features/categories/server";
668
+ // categoryDescriptionTemplateGET - the seller form's inherited-template read.
669
+ export { categoryDescriptionTemplateGET } from "./features/categories/server";
668
670
  // [SERVER-ONLY]-Server-only — uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
669
671
  // categoryItemPATCH - Shared export for category item patch.
670
672
  export { categoryItemPATCH } from "./features/categories/server";
@@ -780,8 +782,6 @@ export { getEventSpinResults } from "./features/events/server";
780
782
  // getUserSpinResultsForEvent - The CALLER'S OWN spins for an event, unmasked. Companion to the masked public feed above.
781
783
  export { getUserSpinResultsForEvent } from "./features/events/server";
782
784
  // [SERVER-ONLY]-Server-only — uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
783
- // getBugHunterLeaderboard - Ranks testers by confirmed-bug count.
784
- export { getBugHunterLeaderboard } from "./features/tester/server";
785
785
  // [SERVER-ONLY]-Server-only — uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
786
786
  // getPublicEventById - Helper for get public event by id.
787
787
  export { getPublicEventById } from "./features/events/server";
@@ -1248,6 +1248,9 @@ export { siteSettingsRepository } from "./repositories/index";
1248
1248
  export { storeRepository } from "./repositories/index";
1249
1249
  // orderRepository - Shared export for order repository (needed by consumer Firebase Functions, e.g. invoice PDF generation).
1250
1250
  export { orderRepository } from "./repositories/index";
1251
+ // taxCodesRepository (B3) — resolved by deriveTaxonomy on every product write
1252
+ // that names a taxonomy field, so it is a server-only read on a hot path.
1253
+ export { taxCodesRepository } from "./repositories/index";
1251
1254
  // SB-UNI-V — bundlesRepository deleted; use categoriesRepository with categoryType:"bundle".
1252
1255
  // -- Monitoring --------------------------------------------------------------
1253
1256
  // serverLogger - Structured server-side logger.
package/dist/styles.css CHANGED
@@ -7369,6 +7369,46 @@
7369
7369
  background-color: var(--appkit-color-slate-800);
7370
7370
  }
7371
7371
 
7372
+ /* ── edit the selected option (C2c) ──
7373
+ Absolutely positioned inside `.appkit-ps` (which is `position: relative`)
7374
+ rather than being a flex sibling of the trigger: the trigger is a
7375
+ full-width button and the pencil has to sit ON it visually while remaining
7376
+ OUTSIDE it in the DOM. A button nested inside a button is invalid HTML and
7377
+ the browser's recovery hoists it out, after which which element receives
7378
+ the click is an engine decision — so the pencil would open the dropdown on
7379
+ some browsers and the editor on others.
7380
+
7381
+ `right` clears the ▾ caret. The hover fill uses the theme-inverting
7382
+ surface-hover token, never a named palette tint: `hover:bg-zinc-50` and
7383
+ friends are the SAME near-white in every theme, which is white-on-white
7384
+ under dark ink (Root Cause #79). */
7385
+ .appkit-ps__edit-btn {
7386
+ position: absolute;
7387
+ top: 50%;
7388
+ right: 2rem;
7389
+ transform: translateY(-50%);
7390
+ display: inline-flex;
7391
+ align-items: center;
7392
+ justify-content: center;
7393
+ width: 1.75rem;
7394
+ height: 1.75rem;
7395
+ border-radius: var(--appkit-radius-md, 0.375rem);
7396
+ font-size: 0.875rem;
7397
+ line-height: 1;
7398
+ color: var(--appkit-color-text-muted);
7399
+ transition: background-color 0.1s, color 0.1s;
7400
+ }
7401
+
7402
+ .appkit-ps__edit-btn:hover {
7403
+ background-color: var(--appkit-color-surface-hover);
7404
+ color: var(--appkit-color-primary);
7405
+ }
7406
+
7407
+ .appkit-ps__edit-btn:focus-visible {
7408
+ outline: 2px solid var(--appkit-color-focus-ring);
7409
+ outline-offset: 1px;
7410
+ }
7411
+
7372
7412
  /* SideDrawer — accessible slide-in panel with focus trap + unsaved-changes */
7373
7413
 
7374
7414
  .appkit-side-drawer__backdrop {
@@ -32,6 +32,20 @@ interface BaseProps<V> {
32
32
  createSchema?: import("zod").ZodType<FormValues> | import("zod").ZodType<unknown>;
33
33
  onCreateSubmit?: (values: FormValues) => Promise<PaginatedSelectOption<V>>;
34
34
  createSubmitLabel?: string;
35
+ editLabel?: string;
36
+ editDrawerTitle?: string;
37
+ renderEditForm?: (props: {
38
+ value: V;
39
+ option: PaginatedSelectOption<V> | null;
40
+ onEdited: (option: PaginatedSelectOption<V>) => void;
41
+ onCancel: () => void;
42
+ }) => ReactNode;
43
+ editFields?: QuickFieldDef[];
44
+ editSchema?: import("zod").ZodType<FormValues> | import("zod").ZodType<unknown>;
45
+ /** Seed the quick form from the row being edited. */
46
+ onEditLoad?: (value: V) => Promise<FormValues>;
47
+ onEditSubmit?: (value: V, values: FormValues) => Promise<PaginatedSelectOption<V>>;
48
+ editSubmitLabel?: string;
35
49
  }
36
50
  interface SingleProps<V> extends BaseProps<V> {
37
51
  multiple?: false;
@@ -260,3 +260,43 @@
260
260
  :is(.dark) .appkit-ps__create-btn:hover {
261
261
  background-color: var(--appkit-color-slate-800);
262
262
  }
263
+
264
+ /* ── edit the selected option (C2c) ──
265
+ Absolutely positioned inside `.appkit-ps` (which is `position: relative`)
266
+ rather than being a flex sibling of the trigger: the trigger is a
267
+ full-width button and the pencil has to sit ON it visually while remaining
268
+ OUTSIDE it in the DOM. A button nested inside a button is invalid HTML and
269
+ the browser's recovery hoists it out, after which which element receives
270
+ the click is an engine decision — so the pencil would open the dropdown on
271
+ some browsers and the editor on others.
272
+
273
+ `right` clears the ▾ caret. The hover fill uses the theme-inverting
274
+ surface-hover token, never a named palette tint: `hover:bg-zinc-50` and
275
+ friends are the SAME near-white in every theme, which is white-on-white
276
+ under dark ink (Root Cause #79). */
277
+ .appkit-ps__edit-btn {
278
+ position: absolute;
279
+ top: 50%;
280
+ right: 2rem;
281
+ transform: translateY(-50%);
282
+ display: inline-flex;
283
+ align-items: center;
284
+ justify-content: center;
285
+ width: 1.75rem;
286
+ height: 1.75rem;
287
+ border-radius: var(--appkit-radius-md, 0.375rem);
288
+ font-size: 0.875rem;
289
+ line-height: 1;
290
+ color: var(--appkit-color-text-muted);
291
+ transition: background-color 0.1s, color 0.1s;
292
+ }
293
+
294
+ .appkit-ps__edit-btn:hover {
295
+ background-color: var(--appkit-color-surface-hover);
296
+ color: var(--appkit-color-primary);
297
+ }
298
+
299
+ .appkit-ps__edit-btn:focus-visible {
300
+ outline: 2px solid var(--appkit-color-focus-ring);
301
+ outline-offset: 1px;
302
+ }
@@ -0,0 +1,23 @@
1
+ /** The parts a location can have. All optional — see the blank-parts note. */
2
+ export interface PhysicalLocationParts {
3
+ zone?: string | null;
4
+ shelf?: string | null;
5
+ bin?: string | null;
6
+ }
7
+ /**
8
+ * Render a location as `zone/shelf/bin`, or `undefined` when there is nothing
9
+ * to show.
10
+ *
11
+ * 🛑 Returns `undefined` rather than `"//"` for an all-blank location, and
12
+ * that case is REACHABLE, not defensive: `PhysicalLocationModal` tells the
13
+ * user "All fields are optional" while both bulk-location routes only check
14
+ * `typeof === "string"`, so `{zone:"",shelf:"",bin:""}` saves successfully.
15
+ * Every caller already branches on falsy before rendering, so returning
16
+ * `undefined` makes those rows fall through to their em-dash instead of
17
+ * printing punctuation with no content.
18
+ *
19
+ * Partial locations keep their separators (`A//3`) on purpose — the gap is
20
+ * information, and collapsing it would make "zone A, bin 3" indistinguishable
21
+ * from "zone A, shelf 3".
22
+ */
23
+ export declare function formatPhysicalLocation(loc: PhysicalLocationParts | null | undefined): string | undefined;
@@ -0,0 +1,52 @@
1
+ /*
2
+ * WHY: `physicalLocation` is a `{zone, shelf, bin}` OBJECT on both
3
+ * `ProductDocument` and `OrderDocument`, and three surfaces render it.
4
+ * Two had hand-written the same `a/b/c` interpolation
5
+ * (`SellerProductsView`, `SellerOrdersView`) and the third —
6
+ * `PrintCenterView` — declared the field as a `string` and rendered the
7
+ * object as a bare React child.
8
+ *
9
+ * 🛑 THAT THIRD ONE IS A CRASH. Rendering an object as a React child throws
10
+ * "Objects are not valid as a React child", so the Print Centre would die
11
+ * the first time any seller used the Set Location bulk action. It is
12
+ * LATENT, not live: measured on production, 0 of 72 products and 0 of 67
13
+ * orders carry the field, so nobody has used that feature yet. It was
14
+ * invisible because BOTH mappers in `PrintCenterPanel` are typed `(p: any)`
15
+ * and the products array is passed `as any` — so neither the wrong
16
+ * declaration nor the wrong value had anything to trip over.
17
+ *
18
+ * WHAT: one formatter. Pure, no imports, structural parameter so a caller can
19
+ * pass a `ProductDocument`'s field, an `OrderDocument`'s, or the
20
+ * `PhysicalLocation` client type without a cast.
21
+ *
22
+ * @tag domain:inventory
23
+ * @tag layer:util
24
+ * @tag access:isomorphic
25
+ * @tag sideEffects:none
26
+ */
27
+ /**
28
+ * Render a location as `zone/shelf/bin`, or `undefined` when there is nothing
29
+ * to show.
30
+ *
31
+ * 🛑 Returns `undefined` rather than `"//"` for an all-blank location, and
32
+ * that case is REACHABLE, not defensive: `PhysicalLocationModal` tells the
33
+ * user "All fields are optional" while both bulk-location routes only check
34
+ * `typeof === "string"`, so `{zone:"",shelf:"",bin:""}` saves successfully.
35
+ * Every caller already branches on falsy before rendering, so returning
36
+ * `undefined` makes those rows fall through to their em-dash instead of
37
+ * printing punctuation with no content.
38
+ *
39
+ * Partial locations keep their separators (`A//3`) on purpose — the gap is
40
+ * information, and collapsing it would make "zone A, bin 3" indistinguishable
41
+ * from "zone A, shelf 3".
42
+ */
43
+ export function formatPhysicalLocation(loc) {
44
+ if (!loc)
45
+ return undefined;
46
+ const zone = (loc.zone ?? "").trim();
47
+ const shelf = (loc.shelf ?? "").trim();
48
+ const bin = (loc.bin ?? "").trim();
49
+ if (!zone && !shelf && !bin)
50
+ return undefined;
51
+ return `${zone}/${shelf}/${bin}`;
52
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ProductDocument } from "../features/products/schemas/firestore";
2
+ import type { CategoryDocument } from "../features/categories/schemas/firestore";
2
3
  import type { ScammerDocument } from "../features/scams/schemas/firestore";
3
4
  import type { StoreDocument } from "../features/stores/schemas/firestore";
4
5
  import type { EventDocument } from "../features/events/schemas/firestore";
@@ -9,6 +10,25 @@ import type { CouponDocument } from "../features/promotions/schemas/firestore";
9
10
  import type { OfferDocument } from "../features/seller/schemas/firestore";
10
11
  /** products — title, description, brand, taxonomy, condition, card/grading, specs. */
11
12
  export declare function buildProductSearchTxt(p: Partial<ProductDocument>): string[];
13
+ /**
14
+ * categories — name, slug, path, ancestor names, item code, editorial copy.
15
+ *
16
+ * Covers brand and bundle rows too, since they share the collection: hence
17
+ * `brandCountry` and `itemCode`, which only one kind ever carries.
18
+ *
19
+ * 🛑 `ancestors[].name` is in here deliberately, and it is what makes a leaf
20
+ * findable by its lineage — typing "burst" reaches `category-lost-longinus`
21
+ * because Beyblade Burst is one of its ancestors. Without it, the only way to
22
+ * find a tier-4 model is to already know its exact name.
23
+ *
24
+ * `description` is stripped of HTML because `contentBody`-style copy is
25
+ * keyword-rich markup and the tags are not search terms.
26
+ *
27
+ * NOT indexed: `createdBy` (a raw uid), `createdByStoreName` (operator
28
+ * identity), `position`/`subtreeSize` (internal coordinates), and the
29
+ * `metrics` counters (numbers nobody searches by).
30
+ */
31
+ export declare function buildCategorySearchTxt(c: Partial<CategoryDocument>): string[];
12
32
  /** stores — name, description, category. No payout details, no contact. */
13
33
  export declare function buildStoreSearchTxt(s: Partial<StoreDocument>): string[];
14
34
  /** events — title, description, type, tags. */
@@ -46,6 +46,42 @@ export function buildProductSearchTxt(p) {
46
46
  p.specifications?.map((s) => `${s.name} ${s.value}`),
47
47
  ]);
48
48
  }
49
+ /**
50
+ * categories — name, slug, path, ancestor names, item code, editorial copy.
51
+ *
52
+ * Covers brand and bundle rows too, since they share the collection: hence
53
+ * `brandCountry` and `itemCode`, which only one kind ever carries.
54
+ *
55
+ * 🛑 `ancestors[].name` is in here deliberately, and it is what makes a leaf
56
+ * findable by its lineage — typing "burst" reaches `category-lost-longinus`
57
+ * because Beyblade Burst is one of its ancestors. Without it, the only way to
58
+ * find a tier-4 model is to already know its exact name.
59
+ *
60
+ * `description` is stripped of HTML because `contentBody`-style copy is
61
+ * keyword-rich markup and the tags are not search terms.
62
+ *
63
+ * NOT indexed: `createdBy` (a raw uid), `createdByStoreName` (operator
64
+ * identity), `position`/`subtreeSize` (internal coordinates), and the
65
+ * `metrics` counters (numbers nobody searches by).
66
+ */
67
+ export function buildCategorySearchTxt(c) {
68
+ return buildSearchTxt([
69
+ c.name,
70
+ c.slug,
71
+ stripHtml(c.description),
72
+ c.path,
73
+ c.itemCode,
74
+ c.brandCountry,
75
+ ...(c.ancestors ?? []).map((a) => a.name),
76
+ /*
77
+ * B5 — LAST on purpose. `buildSearchTxt` truncates at 600 tokens and does
78
+ * it silently, so whatever sits at the end of this array is what gets
79
+ * dropped first. A model leaf's own name and its lineage must survive
80
+ * ahead of "also known as" spellings.
81
+ */
82
+ ...(c.aliases ?? []),
83
+ ]);
84
+ }
49
85
  /** stores — name, description, category. No payout details, no contact. */
50
86
  export function buildStoreSearchTxt(s) {
51
87
  return buildSearchTxt([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.43.0",
3
+ "version": "4.44.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -195,7 +195,7 @@ const {
195
195
  siteSettingsSeedData, faqSeedData,
196
196
  wishlistsSeedData, historySeedData,
197
197
  groupedListingsSeedData,
198
- scammersSeedData, productFeaturesSeedData,
198
+ scammersSeedData, productFeaturesSeedData, taxCodesSeedData,
199
199
  testerChecklistSeedData, testerResponsesSeedData,
200
200
  supportTicketsSeedData, catalogueSeedData,
201
201
  shipmentsSeedData, shipmentLotsSeedData, shipmentItemsSeedData,
@@ -210,6 +210,7 @@ const {
210
210
  CART_COLLECTION,
211
211
  GROUPED_LISTINGS_COLLECTION, SCAMMER_COLLECTION,
212
212
  WISHLIST_COLLECTION, HISTORY_COLLECTION, PRODUCT_FEATURES_COLLECTION,
213
+ TAX_CODES_COLLECTION,
213
214
  TESTER_CHECKLIST_ITEM_COLLECTION,
214
215
  TESTER_CHECKLIST_RESPONSE_COLLECTION,
215
216
  // PII field/index constants (plain data, still in the main barrel — the
@@ -285,6 +286,7 @@ const COLLECTION_MAP = {
285
286
  groupedListings: GROUPED_LISTINGS_COLLECTION,
286
287
  scammerProfiles: SCAMMER_COLLECTION,
287
288
  productFeatures: PRODUCT_FEATURES_COLLECTION,
289
+ taxCodes: TAX_CODES_COLLECTION,
288
290
  testerChecklistItems: TESTER_CHECKLIST_ITEM_COLLECTION,
289
291
  /*
290
292
  * Tester submissions. DERIVED tier in the tester's own map (wiped at setup so
@@ -370,6 +372,7 @@ const SEED_DATA_MAP = {
370
372
  groupedListings: groupedListingsSeedData,
371
373
  scammerProfiles: scammersSeedData,
372
374
  productFeatures: productFeaturesSeedData,
375
+ taxCodes: taxCodesSeedData,
373
376
  testerChecklistItems: testerChecklistSeedData,
374
377
  testerChecklistResponses: testerResponsesSeedData,
375
378
  };
@@ -1,2 +0,0 @@
1
- import type { ScheduleHandler } from "../runtime/types";
2
- export declare const testerSandboxCleanupHandler: ScheduleHandler;
@@ -1,2 +0,0 @@
1
- import { runTesterSandboxCleanup } from "../core/testerSandboxCleanup";
2
- export const testerSandboxCleanupHandler = (ctx) => runTesterSandboxCleanup(ctx);
@@ -1,3 +0,0 @@
1
- import type { BugHunterLeaderboardEntry } from "../schemas/firestore";
2
- /** Ranks testers by confirmed-bug count. Mirrors getEventLeaderboard()'s shape. */
3
- export declare function getBugHunterLeaderboard(limit?: number): Promise<BugHunterLeaderboardEntry[]>;
@@ -1,5 +0,0 @@
1
- import { testerChecklistItemRepository } from "../repository/tester-checklist-item.repository";
2
- /** Ranks testers by confirmed-bug count. Mirrors getEventLeaderboard()'s shape. */
3
- export async function getBugHunterLeaderboard(limit = 50) {
4
- return testerChecklistItemRepository.getBugHunterLeaderboard(limit);
5
- }
@@ -1,23 +0,0 @@
1
- import React from "react";
2
- import type { BugHunterLeaderboardEntry } from "../schemas/firestore";
3
- export interface BugHunterLeaderboardViewProps {
4
- isLoading?: boolean;
5
- isEmpty?: boolean;
6
- /** Render the full ranked list — caller provides entries via hook */
7
- renderList?: () => React.ReactNode;
8
- /** Render a single row; used when caller wants item-level control */
9
- renderEntry?: (entry: BugHunterLeaderboardEntry, index: number) => React.ReactNode;
10
- /** Entries — used only when renderEntry is provided */
11
- entries?: BugHunterLeaderboardEntry[];
12
- /** Empty state node */
13
- renderEmpty?: () => React.ReactNode;
14
- /** Loading spinner / skeleton */
15
- renderSkeleton?: () => React.ReactNode;
16
- labels?: {
17
- title?: string;
18
- noEntries?: string;
19
- bugs?: string;
20
- };
21
- className?: string;
22
- }
23
- export declare function BugHunterLeaderboardView({ isLoading, isEmpty, renderList, renderEntry, entries, renderEmpty, renderSkeleton, labels, className, }: BugHunterLeaderboardViewProps): React.JSX.Element;
@@ -1,18 +0,0 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from "react";
3
- import { Div, Row, Span, Stack, Text } from "../../../ui";
4
- export function BugHunterLeaderboardView({ isLoading = false, isEmpty = false, renderList, renderEntry, entries = [], renderEmpty, renderSkeleton, labels = {}, className = "", }) {
5
- if (isLoading) {
6
- if (renderSkeleton)
7
- return _jsx(_Fragment, { children: renderSkeleton() });
8
- return _jsx(Row, { justify: "center", padding: "y-xl", children: "Loading\u2026" });
9
- }
10
- if (isEmpty) {
11
- if (renderEmpty)
12
- return _jsx(_Fragment, { children: renderEmpty() });
13
- return (_jsx(Text, { paddingY: "md", variant: "secondary", size: "sm", align: "start", children: labels.noEntries ?? "No confirmed bugs yet." }));
14
- }
15
- if (renderList)
16
- return _jsx(Div, { className: className, children: renderList() });
17
- return (_jsx(Stack, { className: `${className}`, gap: "sm", children: entries.map((entry, i) => renderEntry ? (_jsx(React.Fragment, { children: renderEntry(entry, i) }, entry.hunterId)) : (_jsxs(Div, { layout: "flex", align: "center", justify: "between", rounded: "xl", padding: "sm", className: "border", "data-section": "bughunterleaderboard-div", children: [_jsxs(Span, { weight: "medium", children: ["#", entry.rank, " ", entry.hunterName] }), _jsxs(Span, { children: [entry.bugCount, " ", labels.bugs ?? "bugs"] })] }, entry.hunterId))) }));
18
- }
@@ -1,5 +0,0 @@
1
- import React from "react";
2
- export interface TesterHubViewProps {
3
- sandboxExpiresAt?: string | null;
4
- }
5
- export declare function TesterHubView({ sandboxExpiresAt }: TesterHubViewProps): React.JSX.Element;
@@ -1,110 +0,0 @@
1
- "use client";
2
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
3
- import React from "react";
4
- import { useQuery, useQueryClient } from "@tanstack/react-query";
5
- import { Alert, Details, Div, FieldInput, Heading, Stack, Summary, Text, TextLink } from "../../../ui";
6
- import { apiClient } from "../../../http";
7
- import { useApiMutation } from "../../../client";
8
- import { useSession } from "../../../react";
9
- import { ACCOUNT_ENDPOINTS } from "../../../constants/api-endpoints";
10
- import { ROUTES } from "../../../next/routing/route-map";
11
- import { TesterChecklistStepRow } from "./TesterChecklistStepRow";
12
- function matchesQuery(item, query) {
13
- if (!query)
14
- return true;
15
- const haystack = `${item.label} ${item.description ?? ""} ${item.groupLabel} ${item.pageLabel} ${item.href ?? ""}`.toLowerCase();
16
- return haystack.includes(query.toLowerCase());
17
- }
18
- function TesterChecklistPageSection({ page, testerDisplayName, onAnswer, onSaveNote }) {
19
- const answeredCount = page.items.filter((i) => i.answer).length;
20
- return (_jsxs(Details, { tone: "card", defaultOpen: false, children: [_jsxs(Summary, { children: [page.pageLabel, " (", answeredCount, "/", page.items.length, " answered)"] }), _jsx(Div, { padding: "t-sm", children: _jsx(Stack, { gap: "sm", children: page.items
21
- .slice()
22
- .sort((a, b) => a.order - b.order)
23
- .map((item) => (_jsx(TesterChecklistStepRow, { testerDisplayName: testerDisplayName, item: {
24
- checklistItemId: item.id,
25
- label: item.label,
26
- description: item.description,
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,
36
- answer: item.answer,
37
- comment: item.comment,
38
- screenshotUrl: item.screenshotUrl,
39
- }, onAnswer: onAnswer, onSaveNote: onSaveNote }, item.id))) }) })] }));
40
- }
41
- export function TesterHubView({ sandboxExpiresAt }) {
42
- const { user } = useSession();
43
- const queryClient = useQueryClient();
44
- const [search, setSearch] = React.useState("");
45
- const query = useQuery({
46
- queryKey: ["user", "tester-checklist"],
47
- queryFn: async () => {
48
- const res = await apiClient.get(ACCOUNT_ENDPOINTS.TESTER_CHECKLIST);
49
- return res?.data ?? res;
50
- },
51
- // The API route resolves isTester/canTestAdmin fresh from Firestore on
52
- // every request, so retrying a 403 (a role that genuinely lacks access)
53
- // wastes calls for no benefit — but a real access grant should still be
54
- // visible on the very next request, not cached as a permanent failure.
55
- retry: false,
56
- });
57
- const upsertMutation = useApiMutation({
58
- mutationFn: async (vars) => apiClient.put(ACCOUNT_ENDPOINTS.TESTER_CHECKLIST_ITEM_BY_ID(vars.checklistItemId), vars.patch),
59
- onSuccess: () => {
60
- void queryClient.invalidateQueries({ queryKey: ["user", "tester-checklist"] });
61
- },
62
- });
63
- // Gate on the live API response, not the client-cached session snapshot.
64
- // SessionContext only refreshes role/isTester/canTestAdmin periodically
65
- // (every 5 minutes) or on a hard reload/re-login — never on ordinary
66
- // client-side navigation — so a flag an admin just granted would
67
- // otherwise still read as "Testers only" here for up to 5 minutes even
68
- // though the same request already succeeds server-side. The tester-
69
- // checklist API resolves these fields fresh from Firestore on every call,
70
- // so trusting its actual response is both more accurate and immediate.
71
- const isForbidden = query.isError && query.error?.status === 403;
72
- if (isForbidden) {
73
- return (_jsx(Alert, { variant: "warning", title: "Testers only", children: "This page is only available to accounts flagged as testers (and admins). Contact an admin if you believe this is a mistake." }));
74
- }
75
- if (query.isLoading) {
76
- return _jsx(Text, { color: "muted", children: "Loading checklist\u2026" });
77
- }
78
- // Items arrive pre-sorted by catalog `order` (listActive() orders by it).
79
- const items = query.data?.items ?? [];
80
- const filtered = items.filter((item) => matchesQuery(item, search));
81
- const phases = new Map();
82
- for (const item of filtered) {
83
- if (!phases.has(item.phase))
84
- phases.set(item.phase, new Map());
85
- const groups = phases.get(item.phase);
86
- if (!groups.has(item.groupKey))
87
- groups.set(item.groupKey, { groupLabel: item.groupLabel, pages: new Map() });
88
- const group = groups.get(item.groupKey);
89
- if (!group.pages.has(item.pageKey))
90
- group.pages.set(item.pageKey, { pageLabel: item.pageLabel, items: [] });
91
- group.pages.get(item.pageKey).items.push(item);
92
- }
93
- const sortedPhaseNumbers = Array.from(phases.keys()).sort((a, b) => a - b);
94
- const handleAnswer = (checklistItemId, answer) => {
95
- upsertMutation.mutate({ checklistItemId, patch: { answer } });
96
- };
97
- const handleSaveNote = async (checklistItemId, comment, screenshotUrl) => {
98
- await upsertMutation.mutateAsync({ checklistItemId, patch: { comment, screenshotUrl } });
99
- };
100
- return (_jsxs(Stack, { gap: "lg", children: [_jsx(Heading, { level: 1, children: "Tester Hub" }), _jsx(Text, { color: "muted", children: "Work through the checklist below and answer Yes/No for each test case. Add a comment and screenshot wherever something looks off \u2014 colors, styles, readability, bugs. Your answers save automatically." }), _jsx(TextLink, { href: String(ROUTES.PUBLIC.BUG_HUNTERS), variant: "underline", weight: "medium", children: "View Bug Hunters Leaderboard \u2192" }), sandboxExpiresAt && (_jsxs(Alert, { variant: "info", title: "Shared test sandbox", children: ["The shared test store/products/categories expire on ", new Date(sandboxExpiresAt).toLocaleDateString(), "."] })), _jsx(FieldInput, { name: "checklist-search", label: "Search test cases", value: search, onChange: setSearch, placeholder: "Search by title or route, e.g. checkout, /store/payouts..." }), sortedPhaseNumbers.map((phaseNumber) => {
101
- const groups = phases.get(phaseNumber);
102
- const phaseItemCount = Array.from(groups.values())
103
- .flatMap((g) => Array.from(g.pages.values()))
104
- .reduce((sum, page) => sum + page.items.length, 0);
105
- const phaseAnswered = Array.from(groups.values())
106
- .flatMap((g) => Array.from(g.pages.values()))
107
- .reduce((sum, page) => sum + page.items.filter((i) => i.answer).length, 0);
108
- return (_jsxs(Details, { tone: "card", defaultOpen: false, children: [_jsxs(Summary, { size: "lg", weight: "bold", children: ["Phase ", phaseNumber, " (", phaseAnswered, "/", phaseItemCount, " answered)"] }), _jsx(Div, { padding: "t-sm", children: _jsx(Stack, { gap: "md", children: Array.from(groups.entries()).map(([groupKey, group]) => (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 4, children: group.groupLabel }), Array.from(group.pages.entries()).map(([pageKey, page]) => (_jsx(TesterChecklistPageSection, { page: page, testerDisplayName: user?.displayName ?? user?.email ?? "tester", onAnswer: handleAnswer, onSaveNote: handleSaveNote }, pageKey)))] }, groupKey))) }) })] }, phaseNumber));
109
- })] }));
110
- }
@@ -1,2 +0,0 @@
1
- import type { AuthoredCase } from "./_types";
2
- export declare const authored: Record<string, AuthoredCase>;