@mohasinac/appkit 4.42.18 → 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.
- package/dist/_internal/client/features/lottery/LotteryDetailView.js +22 -1
- package/dist/_internal/server/features/categories/adapters.d.ts +47 -0
- package/dist/_internal/server/features/categories/adapters.js +235 -0
- package/dist/_internal/server/features/checkout/actions.js +161 -22
- package/dist/_internal/server/features/checkout/expand-order-items.js +34 -0
- package/dist/_internal/server/features/orders/adapters.js +21 -0
- package/dist/_internal/server/features/products/list-public.d.ts +24 -0
- package/dist/_internal/server/features/products/list-public.js +10 -2
- package/dist/_internal/server/features/refunds/actions.js +1 -1
- package/dist/_internal/server/features/seo/sitemap.js +54 -15
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +6 -0
- package/dist/_internal/server/functions/scheduled.d.ts +4 -2
- package/dist/_internal/server/functions/scheduled.js +64 -6
- package/dist/_internal/server/jobs/core/adminAnalytics.d.ts +0 -1
- package/dist/_internal/server/jobs/core/adminAnalytics.js +9 -1
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.d.ts +47 -0
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +468 -0
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +14 -1
- package/dist/_internal/server/jobs/core/countersReconcile.js +24 -8
- package/dist/_internal/server/jobs/core/onCategoryWrite.js +59 -0
- package/dist/_internal/server/jobs/core/onProductWrite.js +31 -0
- package/dist/_internal/server/jobs/core/onReviewWrite.js +8 -0
- package/dist/_internal/server/jobs/core/onStoreWrite.d.ts +1 -1
- package/dist/_internal/server/jobs/core/onStoreWrite.js +8 -2
- package/dist/_internal/server/jobs/core/priceIndexRollup.d.ts +47 -0
- package/dist/_internal/server/jobs/core/priceIndexRollup.js +234 -0
- package/dist/_internal/server/jobs/core/revalidateNotify.d.ts +28 -0
- package/dist/_internal/server/jobs/core/revalidateNotify.js +125 -0
- package/dist/_internal/server/jobs/core/{testerSandboxCleanup.d.ts → testDataCleanup.d.ts} +3 -3
- package/dist/_internal/server/jobs/core/{testerSandboxCleanup.js → testDataCleanup.js} +3 -3
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +5 -1
- package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.js +2 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +3 -1
- package/dist/_internal/server/jobs/handlers/index.js +3 -1
- package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -2
- package/dist/_internal/server/jobs/handlers/messages.js +5 -2
- package/dist/_internal/server/jobs/handlers/priceIndexRollup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/priceIndexRollup.js +2 -0
- package/dist/_internal/server/jobs/handlers/testDataCleanup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/testDataCleanup.js +2 -0
- package/dist/_internal/shared/actions/action-registry.js +34 -0
- package/dist/_internal/shared/cache/revalidate-targets.d.ts +49 -0
- package/dist/_internal/shared/cache/revalidate-targets.js +157 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +113 -3
- package/dist/_internal/shared/checkout/order-math.js +194 -7
- package/dist/_internal/shared/features/categories/price-guidance.d.ts +110 -0
- package/dist/_internal/shared/features/categories/price-guidance.js +136 -0
- package/dist/_internal/shared/features/site-settings/gst-registration.d.ts +90 -0
- package/dist/_internal/shared/features/site-settings/gst-registration.js +126 -0
- package/dist/_internal/shared/fees/calculator.d.ts +105 -0
- package/dist/_internal/shared/fees/calculator.js +143 -0
- package/dist/_internal/shared/listing-types/art/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/classified/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/live/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/standard/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +0 -6
- package/dist/_internal/shared/templating/placeholders.d.ts +70 -0
- package/dist/_internal/shared/templating/placeholders.js +73 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +1 -0
- package/dist/constants/api-endpoints.d.ts +42 -0
- package/dist/constants/api-endpoints.js +14 -0
- package/dist/features/admin/actions/admin-actions.d.ts +1 -1
- package/dist/features/admin/actions/admin-actions.js +1 -1
- package/dist/features/admin/components/AdminCategoryEditorView.js +269 -3
- package/dist/features/admin/components/AdminProductEditorView.js +8 -1
- package/dist/features/admin/components/AdminProductsView.js +78 -7
- package/dist/features/admin/components/AdminSiteSettingsView.js +20 -1
- package/dist/features/admin/components/AdminTaxCodeEditorView.d.ts +10 -0
- package/dist/features/admin/components/AdminTaxCodeEditorView.js +160 -0
- package/dist/features/admin/components/AdminTaxCodesView.d.ts +13 -0
- package/dist/features/admin/components/AdminTaxCodesView.js +86 -0
- package/dist/features/admin/components/AdminTesterChecklistView.js +1 -1
- package/dist/features/admin/components/analytics/AdminTopProductsTable.js +1 -1
- package/dist/features/auctions/components/AuctionBottomActions.js +10 -1
- package/dist/features/auctions/components/AuctionDetailPageView.js +2 -1
- package/dist/features/auctions/components/AuctionLiveStatusBadge.d.ts +20 -0
- package/dist/features/auctions/components/AuctionLiveStatusBadge.js +21 -0
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -1
- package/dist/features/auctions/components/PlaceBidFormClient.js +20 -2
- package/dist/features/auctions/hooks/useIsAuctionEnded.d.ts +41 -0
- package/dist/features/auctions/hooks/useIsAuctionEnded.js +76 -0
- package/dist/features/auth/schemas/firestore.d.ts +0 -1
- package/dist/features/blog/api/route.js +16 -1
- package/dist/features/blog/schemas/firestore.d.ts +2 -2
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +14 -3
- package/dist/features/categories/actions/category-actions.d.ts +2 -2
- package/dist/features/categories/actions/category-actions.js +22 -10
- package/dist/features/categories/api/[id]/description-template/route.d.ts +45 -0
- package/dist/features/categories/api/[id]/description-template/route.js +252 -0
- package/dist/features/categories/api/route.js +156 -11
- package/dist/features/categories/components/CategoriesIndexListing.d.ts +1 -3
- package/dist/features/categories/components/CategoriesIndexListing.js +1 -1
- package/dist/features/categories/components/CategoriesIndexPageView.d.ts +27 -1
- package/dist/features/categories/components/CategoriesIndexPageView.js +24 -33
- package/dist/features/categories/components/CategoryDetailPageView.js +43 -10
- package/dist/features/categories/components/CategoryTemplatePicker.d.ts +65 -0
- package/dist/features/categories/components/CategoryTemplatePicker.js +170 -0
- package/dist/features/categories/constants/listing-categories.d.ts +17 -0
- package/dist/features/categories/constants/listing-categories.js +53 -0
- package/dist/features/categories/hooks/useCategoryTree.js +28 -2
- package/dist/features/categories/repository/categories.repository.d.ts +73 -12
- package/dist/features/categories/repository/categories.repository.js +203 -42
- package/dist/features/categories/repository/price-index.repository.d.ts +115 -0
- package/dist/features/categories/repository/price-index.repository.js +85 -0
- package/dist/features/categories/schemas/category-content.d.ts +206 -0
- package/dist/features/categories/schemas/category-content.js +28 -0
- package/dist/features/categories/schemas/firestore.d.ts +74 -5
- package/dist/features/categories/schemas/firestore.js +74 -3
- package/dist/features/categories/schemas/index.d.ts +1047 -0
- package/dist/features/categories/schemas/index.js +96 -0
- package/dist/features/categories/server.d.ts +1 -0
- package/dist/features/categories/server.js +7 -0
- package/dist/features/categories/types/index.d.ts +9 -0
- package/dist/features/events/schemas/firestore.d.ts +2 -2
- package/dist/features/faq/schemas/firestore.d.ts +2 -1
- package/dist/features/faq/schemas/firestore.js +20 -8
- package/dist/features/faq/utils/getHomepageFaqsCached.d.ts +20 -0
- package/dist/features/faq/utils/getHomepageFaqsCached.js +22 -0
- package/dist/features/homepage/components/MarketplaceHomepageView.js +13 -3
- package/dist/features/homepage/lib/live-stats.js +18 -8
- package/dist/features/media/MediaImage.js +19 -2
- package/dist/features/orders/schemas/firestore.d.ts +70 -0
- package/dist/features/orders/types/index.d.ts +55 -0
- package/dist/features/products/actions/product-actions.d.ts +0 -9
- package/dist/features/products/actions/product-actions.js +25 -0
- package/dist/features/products/columns/index.d.ts +0 -1
- package/dist/features/products/columns/index.js +5 -1
- package/dist/features/products/components/ArtStickersListView.js +7 -1
- package/dist/features/products/components/PrizeDrawsSection.js +5 -0
- package/dist/features/products/components/ProductFilters.d.ts +0 -9
- package/dist/features/products/components/ProductForm.d.ts +27 -1
- package/dist/features/products/components/ProductForm.js +26 -12
- package/dist/features/products/components/ProductsIndexPageView.js +1 -1
- package/dist/features/products/constants/sieve.d.ts +0 -6
- package/dist/features/products/constants/sieve.js +35 -2
- package/dist/features/products/repository/products.repository.d.ts +115 -1
- package/dist/features/products/repository/products.repository.js +491 -39
- package/dist/features/products/schemas/firestore.d.ts +65 -5
- package/dist/features/products/schemas/firestore.js +9 -0
- package/dist/features/products/schemas/product-features.d.ts +140 -2
- package/dist/features/products/schemas/product-features.js +62 -1
- package/dist/features/promotions/repository/coupons.repository.js +6 -1
- package/dist/features/promotions/schemas/firestore.d.ts +44 -1
- package/dist/features/promotions/schemas/firestore.js +76 -2
- package/dist/features/reviews/repository/reviews.repository.d.ts +22 -0
- package/dist/features/reviews/repository/reviews.repository.js +35 -0
- package/dist/features/seller/actions/seller-actions.js +17 -2
- package/dist/features/seller/components/PrintCenterView.d.ts +3 -2
- package/dist/features/seller/components/PrintCenterView.js +3 -2
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +21 -11
- package/dist/features/seller/components/SellerOrdersView.js +6 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +19 -3
- package/dist/features/seller/components/SellerProductShell.d.ts +50 -2
- package/dist/features/seller/components/SellerProductShell.js +101 -9
- package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +13 -1
- package/dist/features/seller/components/SellerProductsFilterDrawer.js +2 -2
- package/dist/features/seller/components/SellerProductsView.js +108 -3
- package/dist/features/seller/types/index.d.ts +17 -0
- package/dist/features/seller/utils/product-draft-mapping.d.ts +13 -0
- package/dist/features/seller/utils/product-draft-mapping.js +17 -0
- package/dist/features/store-extensions/constants/alert-metrics.d.ts +86 -0
- package/dist/features/store-extensions/constants/alert-metrics.js +142 -0
- package/dist/features/store-extensions/schemas/firestore.d.ts +19 -1
- package/dist/features/stores/schemas/firestore.d.ts +31 -2
- package/dist/features/tax-codes/repository/tax-codes.repository.d.ts +55 -0
- package/dist/features/tax-codes/repository/tax-codes.repository.js +131 -0
- package/dist/features/tax-codes/schemas/firestore.d.ts +125 -0
- package/dist/features/tax-codes/schemas/firestore.js +82 -0
- package/dist/features/tax-codes/schemas/tax-code-form.d.ts +63 -0
- package/dist/features/tax-codes/schemas/tax-code-form.js +152 -0
- package/dist/features/tax-codes/schemas/tax-codes.validators.d.ts +107 -0
- package/dist/features/tax-codes/schemas/tax-codes.validators.js +85 -0
- package/dist/features/tester/actions/index.d.ts +0 -1
- package/dist/features/tester/actions/index.js +0 -1
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +1 -1
- package/dist/features/tester/components/AdminTesterFeedbackListView.js +1 -1
- package/dist/features/tester/components/index.d.ts +0 -4
- package/dist/features/tester/components/index.js +0 -2
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +1 -20
- package/dist/features/tester/repository/tester-checklist-item.repository.js +0 -52
- package/dist/features/tester/schemas/firestore.d.ts +0 -6
- package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +3 -3
- package/dist/features/tester/seed-data/authored/admin__site-system.js +4 -5
- package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +2 -2
- package/dist/features/tester/seed-data/authored/index.js +0 -2
- package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +0 -19
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +10 -29
- package/dist/index.d.ts +21 -7
- package/dist/index.js +53 -7
- package/dist/next/routing/route-map.d.ts +2 -4
- package/dist/next/routing/route-map.js +1 -2
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +1 -0
- package/dist/schemas/registry.d.ts +506 -0
- package/dist/seed/_helpers/brand-rows.d.ts +35 -0
- package/dist/seed/_helpers/brand-rows.js +576 -0
- package/dist/seed/_helpers/category-content.d.ts +39 -0
- package/dist/seed/_helpers/category-content.js +634 -0
- package/dist/seed/_helpers/category-forest.d.ts +2 -0
- package/dist/seed/_helpers/category-forest.js +585 -243
- package/dist/seed/_helpers/category-models.d.ts +5 -0
- package/dist/seed/_helpers/category-models.js +1364 -0
- package/dist/seed/_helpers/category-tree.js +17 -1
- package/dist/seed/_helpers/character-features.d.ts +10 -0
- package/dist/seed/_helpers/character-features.js +118 -0
- package/dist/seed/_helpers/media.d.ts +1 -1
- package/dist/seed/_helpers/media.js +44 -8
- package/dist/seed/_helpers/search-txt-wrappers.d.ts +10 -0
- package/dist/seed/_helpers/search-txt-wrappers.js +12 -1
- package/dist/seed/categories-seed-data.js +147 -169
- package/dist/seed/index.d.ts +1 -0
- package/dist/seed/index.js +1 -0
- package/dist/seed/manifest.d.ts +1 -1
- package/dist/seed/manifest.js +5 -0
- package/dist/seed/orders-seed-data.js +5 -3
- package/dist/seed/product-features-seed-data.d.ts +1 -8
- package/dist/seed/product-features-seed-data.js +358 -154
- package/dist/seed/products-art-seed-data.js +16 -4
- package/dist/seed/products-auctions-seed-data.js +6 -6
- package/dist/seed/products-classifieds-seed-data.js +17 -7
- package/dist/seed/products-digital-codes-seed-data.js +12 -12
- package/dist/seed/products-preorders-seed-data.js +8 -8
- package/dist/seed/products-prize-draws-seed-data.js +25 -8
- package/dist/seed/products-standard-seed-data.js +10 -60
- package/dist/seed/products-stickers-seed-data.js +6 -6
- package/dist/seed/site-settings-seed-data.js +24 -0
- package/dist/seed/store-extensions-seed-data.js +10 -4
- package/dist/seed/tax-codes-seed-data.d.ts +34 -0
- package/dist/seed/tax-codes-seed-data.js +100 -0
- package/dist/seed/test-data-ttl.d.ts +29 -0
- package/dist/seed/test-data-ttl.js +31 -0
- package/dist/server-entry.d.ts +1 -1
- package/dist/server-entry.js +6 -1
- package/dist/server.d.ts +4 -1
- package/dist/server.js +11 -2
- package/dist/styles.css +40 -0
- package/dist/ui/components/PaginatedSelect.d.ts +14 -0
- package/dist/ui/components/PaginatedSelect.js +0 -0
- package/dist/ui/components/PaginatedSelect.style.css +40 -0
- package/dist/utils/media-url.d.ts +26 -0
- package/dist/utils/media-url.js +144 -0
- package/dist/utils/physical-location.d.ts +23 -0
- package/dist/utils/physical-location.js +52 -0
- package/dist/utils/search-txt-builders.d.ts +20 -0
- package/dist/utils/search-txt-builders.js +36 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +4 -1
- package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.js +0 -2
- package/dist/features/tester/actions/leaderboard-actions.d.ts +0 -3
- package/dist/features/tester/actions/leaderboard-actions.js +0 -5
- package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +0 -23
- package/dist/features/tester/components/BugHunterLeaderboardView.js +0 -18
- package/dist/features/tester/components/TesterHubView.d.ts +0 -5
- package/dist/features/tester/components/TesterHubView.js +0 -110
- package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.d.ts +0 -2
- package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +0 -78
- package/dist/features/tester/seed-data/tester-ttl.d.ts +0 -4
- package/dist/features/tester/seed-data/tester-ttl.js +0 -6
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: ~382 category nodes need description templates and product defaults,
|
|
3
|
+
* and inlining them per node is 382 places a fix has to land. The crawl
|
|
4
|
+
* is the evidence that this is the right shape: worldhobbyshop reuses
|
|
5
|
+
* exactly TWO bodies across 1,195 listings, so the bodies are a small
|
|
6
|
+
* closed set and the per-category part is which set applies.
|
|
7
|
+
* WHAT: The five reusable description bodies, the measured day-1 market bands,
|
|
8
|
+
* and per-vertical `productDefaults` factories. Authored ONCE.
|
|
9
|
+
*
|
|
10
|
+
* EXPORTS:
|
|
11
|
+
* TEMPLATES — the five bodies, by variant
|
|
12
|
+
* templateSet — pick a subset by variant for a category
|
|
13
|
+
* marketBand — a measured {p25, median, p75, sampleSize, asOf} band
|
|
14
|
+
* beyDefaults / partDefaults / launcherDefaults / stadiumDefaults
|
|
15
|
+
* tcgDefaults / figureDefaults / diecastDefaults / accessoryDefaults
|
|
16
|
+
*
|
|
17
|
+
* 🛑 Every number in MARKET_BANDS is MEASURED, from
|
|
18
|
+
* docs/research/competitor-crawl-2026-10/price-bands.md, with its real
|
|
19
|
+
* sample size. A band with no evidence is omitted rather than guessed —
|
|
20
|
+
* a fabricated "typical price" is worse than no hint, because a seller
|
|
21
|
+
* prices against it.
|
|
22
|
+
*
|
|
23
|
+
* 🛑 `sold` is never seeded. It is OUR completed sales and we have none for
|
|
24
|
+
* most leaves; `priceIndexRollup` (C1) is its only writer, and it must
|
|
25
|
+
* never touch `market`.
|
|
26
|
+
*
|
|
27
|
+
* @tag domain:categories
|
|
28
|
+
* @tag layer:seed
|
|
29
|
+
* @tag pattern:shared-literal
|
|
30
|
+
* @tag access:server-only
|
|
31
|
+
* @tag consumers:seed/_helpers/category-forest.ts
|
|
32
|
+
* @tag sideEffects:none
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* When the competitor crawl was taken. Fixed, never `new Date()` — a seed file
|
|
36
|
+
* is re-executed on every `appkit-seed` invocation, so a moving timestamp makes
|
|
37
|
+
* `load` non-idempotent (Root Cause #25).
|
|
38
|
+
*/
|
|
39
|
+
export const CRAWL_AS_OF = new Date("2026-10-09T00:00:00.000Z");
|
|
40
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
// The five description bodies
|
|
42
|
+
//
|
|
43
|
+
// Shapes taken from the crawl; every word is ours. The counter-example is
|
|
44
|
+
// raikagesbeybladestore, whose 114-char bodies are lifted verbatim from the
|
|
45
|
+
// Beyblade Wiki and one of which ships with the literal UI string "Add to
|
|
46
|
+
// Wishlist Add to Wishlist" welded into the product body.
|
|
47
|
+
//
|
|
48
|
+
// 🛑 No `{{price}}` slot, deliberately. A price in free text is an ungated
|
|
49
|
+
// public price that `<GatedPrice>` cannot wrap, and `audit-guest-price-leak`
|
|
50
|
+
// R2 blocks it.
|
|
51
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
const PRE_OWNED_BODY = `<h2>{{title}}</h2>
|
|
53
|
+
<p>A pre-owned {{category}} listing from the {{series}} line, sold as pictured.</p>
|
|
54
|
+
<h3>Important things to know</h3>
|
|
55
|
+
<ul>
|
|
56
|
+
<li><strong>Condition:</strong> {{condition}}. The photographs are of the actual item, not a stock image.</li>
|
|
57
|
+
<li><strong>What is included:</strong> only what is shown. Launchers, ripcords and stadiums are sold separately unless pictured.</li>
|
|
58
|
+
<li><strong>Wear:</strong> a used {{category}} will show handling marks. Anything structural — a chip, a crack, a stripped tooth — is called out above and photographed.</li>
|
|
59
|
+
<li><strong>Authenticity:</strong> stated on this listing. Reproduction parts are labelled as such and never described as original.</li>
|
|
60
|
+
</ul>
|
|
61
|
+
<p>Sold by a verified seller on {{siteName}}, with buyer protection on every order.</p>`;
|
|
62
|
+
const NEW_IN_BOX_BODY = `<h2>{{title}}</h2>
|
|
63
|
+
<p>Sealed, unopened {{category}} from the {{series}} line.</p>
|
|
64
|
+
<h3>Specification</h3>
|
|
65
|
+
<ul>
|
|
66
|
+
<li><strong>Manufacturer:</strong> {{brand}}</li>
|
|
67
|
+
<li><strong>Condition:</strong> {{condition}}</li>
|
|
68
|
+
<li><strong>Series:</strong> {{series}}</li>
|
|
69
|
+
<li><strong>System:</strong> {{system}}</li>
|
|
70
|
+
<li><strong>Category:</strong> {{categoryPath}}</li>
|
|
71
|
+
</ul>
|
|
72
|
+
<h3>What is in the box?</h3>
|
|
73
|
+
<p>Factory contents, unopened. Where a release includes a launcher or ripcord it is listed in the specification above; where it does not, it is a top-only release.</p>
|
|
74
|
+
<p>Listed by a verified seller on {{siteName}}.</p>`;
|
|
75
|
+
const NEW_IN_PACKET_BODY = `<h2>{{title}}</h2>
|
|
76
|
+
<p>New {{category}} on its original card or in its original polybag — never used, packaging unopened.</p>
|
|
77
|
+
<ul>
|
|
78
|
+
<li><strong>Manufacturer:</strong> {{brand}}</li>
|
|
79
|
+
<li><strong>Condition:</strong> {{condition}}</li>
|
|
80
|
+
<li><strong>Series:</strong> {{series}}</li>
|
|
81
|
+
</ul>
|
|
82
|
+
<p>Card and bag wear is normal on stock of this age and is photographed where present. The item itself is untouched.</p>`;
|
|
83
|
+
const REPLICA_BODY = `<h2>{{title}}</h2>
|
|
84
|
+
<p><strong>This is a reproduction, not an original {{brand}} release.</strong> It is listed as one deliberately, because knowing which you are buying is the point.</p>
|
|
85
|
+
<h3>What that means</h3>
|
|
86
|
+
<ul>
|
|
87
|
+
<li>A reproduction is an unlicensed copy of an original design. It carries no manufacturer's marque.</li>
|
|
88
|
+
<li>Build quality on reproductions varies. Weight and balance may differ from the original, which matters in competitive play.</li>
|
|
89
|
+
<li>Some tournament formats permit certain reproduction parts and some do not — check your local format before buying to compete.</li>
|
|
90
|
+
<li>This is <em>not</em> a counterfeit sold as genuine. If you believe a listing misrepresents a reproduction as original, report it.</li>
|
|
91
|
+
</ul>
|
|
92
|
+
<p><strong>Condition:</strong> {{condition}}. Sold as pictured.</p>`;
|
|
93
|
+
const DEFAULT_BODY = `<h2>{{title}}</h2>
|
|
94
|
+
<p>{{category}} from the {{series}} line, listed by a verified seller on {{siteName}}.</p>
|
|
95
|
+
<ul>
|
|
96
|
+
<li><strong>Manufacturer:</strong> {{brand}}</li>
|
|
97
|
+
<li><strong>Condition:</strong> {{condition}}</li>
|
|
98
|
+
<li><strong>Category:</strong> {{categoryPath}}</li>
|
|
99
|
+
</ul>
|
|
100
|
+
<p>Condition and authenticity are stated on every listing. Photographs are of the actual item.</p>`;
|
|
101
|
+
/**
|
|
102
|
+
* The five bodies, by variant.
|
|
103
|
+
*
|
|
104
|
+
* `placeholders` is deliberately LEFT ABSENT on all five: the field's contract
|
|
105
|
+
* is that absent means "re-parse", and `extractPlaceholders(body)` is the one
|
|
106
|
+
* implementation. Writing the list here by hand would be a second copy that
|
|
107
|
+
* drifts the first time a body is edited — and the failure mode is a published
|
|
108
|
+
* description containing a literal `{{series}}`.
|
|
109
|
+
*/
|
|
110
|
+
export const TEMPLATES = {
|
|
111
|
+
default: {
|
|
112
|
+
id: "tpl-default",
|
|
113
|
+
label: "Standard — manufacturer, condition, category",
|
|
114
|
+
variant: "default",
|
|
115
|
+
body: DEFAULT_BODY,
|
|
116
|
+
version: 1,
|
|
117
|
+
},
|
|
118
|
+
new_in_box: {
|
|
119
|
+
id: "tpl-new-in-box",
|
|
120
|
+
label: "New in box — sealed, with a specification table",
|
|
121
|
+
variant: "new_in_box",
|
|
122
|
+
body: NEW_IN_BOX_BODY,
|
|
123
|
+
version: 1,
|
|
124
|
+
},
|
|
125
|
+
new_in_packet: {
|
|
126
|
+
id: "tpl-new-in-packet",
|
|
127
|
+
label: "New in packet — carded or bagged, unopened",
|
|
128
|
+
variant: "new_in_packet",
|
|
129
|
+
body: NEW_IN_PACKET_BODY,
|
|
130
|
+
version: 1,
|
|
131
|
+
},
|
|
132
|
+
pre_owned: {
|
|
133
|
+
id: "tpl-pre-owned",
|
|
134
|
+
label: "Pre-owned — condition exactly as pictured",
|
|
135
|
+
variant: "pre_owned",
|
|
136
|
+
body: PRE_OWNED_BODY,
|
|
137
|
+
version: 1,
|
|
138
|
+
},
|
|
139
|
+
replica: {
|
|
140
|
+
id: "tpl-replica",
|
|
141
|
+
label: "Reproduction — disclosed, with a what-this-means panel",
|
|
142
|
+
variant: "replica",
|
|
143
|
+
body: REPLICA_BODY,
|
|
144
|
+
version: 1,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
/** The set a normal collectible category offers. */
|
|
148
|
+
export const COLLECTIBLE_TEMPLATES = [
|
|
149
|
+
TEMPLATES.new_in_box,
|
|
150
|
+
TEMPLATES.pre_owned,
|
|
151
|
+
TEMPLATES.replica,
|
|
152
|
+
TEMPLATES.default,
|
|
153
|
+
];
|
|
154
|
+
/** Parts and small accessories arrive carded or loose, not boxed. */
|
|
155
|
+
export const PART_TEMPLATES = [
|
|
156
|
+
TEMPLATES.new_in_packet,
|
|
157
|
+
TEMPLATES.pre_owned,
|
|
158
|
+
TEMPLATES.replica,
|
|
159
|
+
TEMPLATES.default,
|
|
160
|
+
];
|
|
161
|
+
/** Sealed-only verticals (TCG sealed product, model kits) — no repro tier. */
|
|
162
|
+
export const SEALED_TEMPLATES = [
|
|
163
|
+
TEMPLATES.new_in_box,
|
|
164
|
+
TEMPLATES.pre_owned,
|
|
165
|
+
TEMPLATES.default,
|
|
166
|
+
];
|
|
167
|
+
export function templateSet(...variants) {
|
|
168
|
+
return variants.map((v) => TEMPLATES[v]);
|
|
169
|
+
}
|
|
170
|
+
/*
|
|
171
|
+
* Day-1 bands, keyed by the leaf they belong to.
|
|
172
|
+
*
|
|
173
|
+
* 🛑 EVERY ROW HERE TRACES TO A LINE IN
|
|
174
|
+
* docs/research/competitor-crawl-2026-10/price-bands.md. Nothing is
|
|
175
|
+
* interpolated, averaged across sources, or extrapolated from a
|
|
176
|
+
* neighbouring leaf. A leaf the crawl has no evidence for is ABSENT from
|
|
177
|
+
* this map and renders no hint at all — ten of them are, and they are
|
|
178
|
+
* listed at the bottom so their absence is a recorded decision rather than
|
|
179
|
+
* an oversight.
|
|
180
|
+
*
|
|
181
|
+
* 🛑 An earlier draft of this map invented a sample size for all 21 Beyblade
|
|
182
|
+
* top lines, because the type required one. The crawl's own rule is the
|
|
183
|
+
* reason that matters: *"quote `n`, because a band with n=2 is not
|
|
184
|
+
* evidence"* — so a fabricated n is not a cosmetic liberty, it is the
|
|
185
|
+
* confidence signal replaced with a guess. `sampleSize` is optional now,
|
|
186
|
+
* and these 21 rows legitimately have none.
|
|
187
|
+
*
|
|
188
|
+
* SOURCE PRIORITY: worldhobbyshop first wherever it has data, which is every
|
|
189
|
+
* Beyblade band except HMS. HMS is beybladeshopindia at n=39 against WHS's
|
|
190
|
+
* n=2, and `src` records that so the decision is reviewable rather than
|
|
191
|
+
* buried. Parts pricing is raikages-only (the sole component-level source in
|
|
192
|
+
* the crawl), and TCG / figures / Hot Wheels come from the sites that sell
|
|
193
|
+
* them, since WHS carries none of those verticals.
|
|
194
|
+
*/
|
|
195
|
+
const MARKET_BANDS = {
|
|
196
|
+
// ── Beyblade tops, by line. WHS + beybladeshopindia; no per-leaf n ────────
|
|
197
|
+
"category-original-plastic-gen": { p25: 799, median: 899, p75: 1499, src: "worldhobbyshop (plastic-3-layer)" },
|
|
198
|
+
"category-original-spin-gear": { p25: 799, median: 1399, p75: 1799, src: "worldhobbyshop (spin-gear-sf)" },
|
|
199
|
+
"category-original-magnacore": { p25: 999, median: 1699, p75: 1999, src: "worldhobbyshop (magna-core-v-force)" },
|
|
200
|
+
// 🛑 The one row where a larger sample beat worldhobbyshop, and it is
|
|
201
|
+
// recorded because the gap is enormous: beybladeshopindia n=39 against WHS
|
|
202
|
+
// n=2. HMS is also the highest-value Beyblade tier on the site, so getting
|
|
203
|
+
// it from a coin-flip sample would mis-anchor every HMS listing.
|
|
204
|
+
"category-original-hms": { p25: 2999, median: 3999, p75: 9999, n: 39, src: "beybladeshopindia n=39 (worldhobbyshop had n=2)" },
|
|
205
|
+
"category-metal-phws": { p25: 799, median: 899, p75: 999, src: "worldhobbyshop (phws-metal-system)" },
|
|
206
|
+
"category-metal-fusion": { p25: 799, median: 999, p75: 1499, src: "worldhobbyshop (metal-fusion)" },
|
|
207
|
+
"category-metal-masters": { p25: 1099, median: 1499, p75: 1999, src: "worldhobbyshop (metal-masters)" },
|
|
208
|
+
"category-metal-fury-4d": { p25: 1799, median: 2399, p75: 2999, src: "worldhobbyshop (metal-fury-4d)" },
|
|
209
|
+
"category-metal-shogun-steel": { p25: 1599, median: 2499, p75: 2999, src: "worldhobbyshop (shogun-steel-zero-g)" },
|
|
210
|
+
// The crawl lists single-layer (299/299/399) and dual-layer (299/399/399)
|
|
211
|
+
// separately and the taxonomy collapses them to one leaf, so the band
|
|
212
|
+
// collapses with them — to the documented 299/399/399, not to a midpoint
|
|
213
|
+
// somebody computed.
|
|
214
|
+
"category-burst-classic": { p25: 299, median: 399, p75: 399, src: "worldhobbyshop (single-layer + dual-layer, collapsed)" },
|
|
215
|
+
"category-burst-god": { p25: 599, median: 599, p75: 999, src: "worldhobbyshop (burst-god-evolution)" },
|
|
216
|
+
"category-burst-cho-z": { p25: 599, median: 799, p75: 999, src: "worldhobbyshop (burst-cho-z-turbo)" },
|
|
217
|
+
"category-burst-gt": { p25: 799, median: 999, p75: 1099, src: "worldhobbyshop (burst-gt-rise)" },
|
|
218
|
+
"category-burst-superking": { p25: 999, median: 999, p75: 1499, src: "worldhobbyshop (burst-superking-surge)" },
|
|
219
|
+
"category-burst-db": { p25: 999, median: 1399, p75: 1799, src: "worldhobbyshop (burst-db-quaddrive)" },
|
|
220
|
+
"category-burst-bu": { p25: 1499, median: 1599, p75: 1799, src: "worldhobbyshop (burst-bu-quadstrike)" },
|
|
221
|
+
"category-x-basic": { p25: 799, median: 999, p75: 1499, src: "worldhobbyshop (x-basic-s1)" },
|
|
222
|
+
"category-x-unique-ux": { p25: 1199, median: 1399, p75: 1699, src: "worldhobbyshop (x-unique-ux)" },
|
|
223
|
+
"category-x-custom-cx": { p25: 1299, median: 1499, p75: 1799, src: "worldhobbyshop (x-custom-cx)" },
|
|
224
|
+
"category-x-over": { p25: 999, median: 1099, p75: 1499, src: "worldhobbyshop (x-over-remakes)" },
|
|
225
|
+
// ── Parts. raikages is the ONLY component-level source in the crawl ───────
|
|
226
|
+
"category-metal-energy-rings": { p25: 149, median: 199, p75: 249, n: 68, src: "raikages n=68" },
|
|
227
|
+
"category-metal-fusion-wheels": { p25: 149, median: 249, p75: 299, n: 52, src: "raikages n=52" },
|
|
228
|
+
"category-metal-spin-tracks": { p25: 149, median: 199, p75: 249, n: 53, src: "raikages n=53" },
|
|
229
|
+
"category-metal-performance-tips": { p25: 129, median: 139, p75: 149, n: 25, src: "raikages n=25" },
|
|
230
|
+
// WHS prices Burst parts as ONE pool (n=82) and X parts as one pool (n=14);
|
|
231
|
+
// it does not split layers from discs from drivers. The three leaves share
|
|
232
|
+
// the pooled band, and `src` says so rather than implying three measurements.
|
|
233
|
+
"category-burst-layers": { p25: 99, median: 149, p75: 249, n: 82, src: "worldhobbyshop n=82, POOLED across all Burst parts" },
|
|
234
|
+
"category-burst-discs": { p25: 99, median: 149, p75: 249, n: 82, src: "worldhobbyshop n=82, POOLED across all Burst parts" },
|
|
235
|
+
"category-burst-drivers": { p25: 99, median: 149, p75: 249, n: 82, src: "worldhobbyshop n=82, POOLED across all Burst parts" },
|
|
236
|
+
"category-x-blades": { p25: 199, median: 299, p75: 299, n: 14, src: "worldhobbyshop n=14, POOLED across all X parts" },
|
|
237
|
+
"category-x-ratchets": { p25: 199, median: 299, p75: 299, n: 14, src: "worldhobbyshop n=14, POOLED across all X parts" },
|
|
238
|
+
"category-x-bits": { p25: 199, median: 299, p75: 299, n: 14, src: "worldhobbyshop n=14, POOLED across all X parts" },
|
|
239
|
+
// ── Launchers. One pooled band per generation, again as WHS measured it ───
|
|
240
|
+
"category-original-ripcord-launchers": { p25: 199, median: 299, p75: 899, n: 32, src: "worldhobbyshop n=32, POOLED across original-era launchers" },
|
|
241
|
+
"category-original-winders": { p25: 199, median: 299, p75: 899, n: 32, src: "worldhobbyshop n=32, POOLED across original-era launchers" },
|
|
242
|
+
"category-metal-string-launchers": { p25: 399, median: 599, p75: 849, n: 37, src: "worldhobbyshop n=37, POOLED across Metal Fight launchers" },
|
|
243
|
+
"category-metal-ripcord-launchers": { p25: 399, median: 599, p75: 849, n: 37, src: "worldhobbyshop n=37, POOLED across Metal Fight launchers" },
|
|
244
|
+
"category-metal-lr-launchers": { p25: 399, median: 599, p75: 849, n: 37, src: "worldhobbyshop n=37, POOLED across Metal Fight launchers" },
|
|
245
|
+
"category-burst-string-launchers": { p25: 399, median: 599, p75: 699, n: 78, src: "worldhobbyshop n=78, POOLED across Burst launchers" },
|
|
246
|
+
"category-burst-lr-launchers": { p25: 399, median: 599, p75: 699, n: 78, src: "worldhobbyshop n=78, POOLED across Burst launchers" },
|
|
247
|
+
"category-burst-grips": { p25: 399, median: 599, p75: 699, n: 78, src: "worldhobbyshop n=78, POOLED across Burst launchers" },
|
|
248
|
+
"category-x-string-launchers": { p25: 299, median: 599, p75: 700, n: 34, src: "worldhobbyshop n=34, POOLED across X launchers" },
|
|
249
|
+
"category-x-winders": { p25: 299, median: 599, p75: 700, n: 34, src: "worldhobbyshop n=34, POOLED across X launchers" },
|
|
250
|
+
"category-x-grips": { p25: 299, median: 599, p75: 700, n: 34, src: "worldhobbyshop n=34, POOLED across X launchers" },
|
|
251
|
+
// ── Stadiums. ONE band, EIGHT leaves ──────────────────────────────────────
|
|
252
|
+
// WHS does not split stadiums by generation — its single "Stadiums & Arenas"
|
|
253
|
+
// category states the split in PROSE, because WooCommerce's flat taxonomy
|
|
254
|
+
// could not hold it. So there is no per-generation evidence, and inventing
|
|
255
|
+
// four bands from one sample of seven would be fabrication. The leaves stay
|
|
256
|
+
// split (a BX-10 Xtreme Stadium is a Beyblade X product); the band does not.
|
|
257
|
+
"category-original-bakuten-arena": { p25: 2000, median: 2999, p75: 3499, n: 7, src: "worldhobbyshop n=7, ALL generations pooled" },
|
|
258
|
+
"category-original-attack-stadium": { p25: 2000, median: 2999, p75: 3499, n: 7, src: "worldhobbyshop n=7, ALL generations pooled" },
|
|
259
|
+
"category-metal-standard-stadiums": { p25: 2000, median: 2999, p75: 3499, n: 7, src: "worldhobbyshop n=7, ALL generations pooled" },
|
|
260
|
+
"category-metal-zero-g-stadiums": { p25: 2000, median: 2999, p75: 3499, n: 7, src: "worldhobbyshop n=7, ALL generations pooled" },
|
|
261
|
+
"category-burst-standard-stadiums": { p25: 2000, median: 2999, p75: 3499, n: 7, src: "worldhobbyshop n=7, ALL generations pooled" },
|
|
262
|
+
"category-burst-stadium-variants": { p25: 2000, median: 2999, p75: 3499, n: 7, src: "worldhobbyshop n=7, ALL generations pooled" },
|
|
263
|
+
"category-x-xtreme-stadiums": { p25: 2000, median: 2999, p75: 3499, n: 7, src: "worldhobbyshop n=7, ALL generations pooled" },
|
|
264
|
+
"category-x-standard-stadiums": { p25: 2000, median: 2999, p75: 3499, n: 7, src: "worldhobbyshop n=7, ALL generations pooled" },
|
|
265
|
+
// ── TCG. tcgrepublic + hobbykart. WHS sells no cards ─────────────────────
|
|
266
|
+
"category-pokemon-singles": { p25: 350, median: 400, p75: 500, n: 545, src: "tcgrepublic n=545 (singles, all games)" },
|
|
267
|
+
"category-one-piece-singles": { p25: 350, median: 400, p75: 500, n: 545, src: "tcgrepublic n=545 (singles, all games)" },
|
|
268
|
+
"category-pokemon-graded": { p25: 4600, median: 8000, p75: 11000, n: 15, src: "hobbykart n=15 (graded slabs)" },
|
|
269
|
+
"category-one-piece-graded": { p25: 4600, median: 8000, p75: 11000, n: 15, src: "hobbykart n=15 (graded slabs)" },
|
|
270
|
+
"category-tcg-weiss-schwarz": { p25: 3500, median: 6500, p75: 7500, n: 14, src: "tcgrepublic n=14" },
|
|
271
|
+
"category-card-sleeves-toploaders": { p25: 250, median: 800, p75: 1800, n: 31, src: "tcgrepublic n=31 (supplies, pooled)" },
|
|
272
|
+
"category-card-binders-boxes": { p25: 250, median: 800, p75: 1800, n: 31, src: "tcgrepublic n=31 (supplies, pooled)" },
|
|
273
|
+
"category-card-slab-display": { p25: 250, median: 800, p75: 1800, n: 31, src: "tcgrepublic n=31 (supplies, pooled)" },
|
|
274
|
+
// ── Figures. hobson + redeyemerch ────────────────────────────────────────
|
|
275
|
+
"category-anime-scale-1-12": { p25: 3499, median: 5900, p75: 7999, n: 34, src: "hobson n=34 (1/12 scale)" },
|
|
276
|
+
// The only band the crawl has for mid-scale statues is its general "Anime
|
|
277
|
+
// Figures" tag, not a 1/7-or-1/8 measurement. Used, and labelled as general.
|
|
278
|
+
"category-anime-scale-1-7-1-8": { p25: 1199, median: 1699, p75: 2499, n: 94, src: "redeyemerch n=94, GENERAL anime-figure band (not scale-specific)" },
|
|
279
|
+
"category-anime-prize-figures": { p25: 1999, median: 2699, p75: 2999, n: 23, src: "hobson n=23 (prize figures)" },
|
|
280
|
+
"category-comic-marvel-legends": { p25: 2999, median: 3999, p75: 5499, n: 41, src: "hobson n=41" },
|
|
281
|
+
"category-comic-vintage-toybiz": { p25: 3499, median: 3999, p75: 4499, n: 22, src: "hobson n=22" },
|
|
282
|
+
// ── Hot Wheels. toycollectorsindia ───────────────────────────────────────
|
|
283
|
+
// 🛑 NO QUARTILES EXIST for either row. The crawl recorded a median and a
|
|
284
|
+
// RANGE (₹99–36,500 for the pooled 1:64 band; ₹130–999 for their own plain
|
|
285
|
+
// mainline collection), and a range is not a quartile. So these two carry a
|
|
286
|
+
// median only, which is why `p25`/`p75` are optional at all.
|
|
287
|
+
"category-hw-mainline-series": { median: 399, n: 27, src: "toycollectorsindia n=27 (their own plain mainline collection); range 130-999, no quartiles measured" },
|
|
288
|
+
"category-hw-treasure-hunt": { median: 850, n: 7418, src: "toycollectorsindia n=7418, 1:64 POOLED across mainline and premium; range 99-36500, no quartiles" },
|
|
289
|
+
"category-hw-car-culture": { median: 850, n: 7418, src: "toycollectorsindia n=7418, 1:64 POOLED across mainline and premium; range 99-36500, no quartiles" },
|
|
290
|
+
"category-hw-boulevard": { median: 850, n: 7418, src: "toycollectorsindia n=7418, 1:64 POOLED across mainline and premium; range 99-36500, no quartiles" },
|
|
291
|
+
"category-hw-team-transport": { median: 850, n: 7418, src: "toycollectorsindia n=7418, 1:64 POOLED across mainline and premium; range 99-36500, no quartiles" },
|
|
292
|
+
"category-hw-fast-and-furious": { median: 850, n: 7418, src: "toycollectorsindia n=7418, 1:64 POOLED across mainline and premium; range 99-36500, no quartiles" },
|
|
293
|
+
"category-hw-pop-culture": { median: 850, n: 7418, src: "toycollectorsindia n=7418, 1:64 POOLED across mainline and premium; range 99-36500, no quartiles" },
|
|
294
|
+
"category-hw-rlc": { median: 850, n: 7418, src: "toycollectorsindia n=7418, 1:64 POOLED across mainline and premium; range 99-36500, no quartiles" },
|
|
295
|
+
/*
|
|
296
|
+
* 🛑 DELIBERATELY ABSENT — the crawl measured no band for these, and a
|
|
297
|
+
* plausible-looking number would be worse than no hint, because a seller
|
|
298
|
+
* prices against whatever we show them:
|
|
299
|
+
*
|
|
300
|
+
* category-original-engine-gear WHS bands plastic / spin-gear / magnacore
|
|
301
|
+
* / HMS and NOT Engine Gear. An earlier
|
|
302
|
+
* draft of this map invented 899/1499/1899
|
|
303
|
+
* for it, interpolated from its neighbours.
|
|
304
|
+
* category-metal-face-bolts raikages prices rings, wheels, tracks and
|
|
305
|
+
* category-burst-chips-armour tips — not face bolts, chips or armour.
|
|
306
|
+
* category-anime-scale-1-6 No 1/6 measurement. S.H.Figuarts (n=25)
|
|
307
|
+
* is a 1/12 line and is not a substitute.
|
|
308
|
+
* category-anime-nendoroid-chibi No band. Good Smile's n=17 in the crawl
|
|
309
|
+
* is a TAG COUNT, not a price sample.
|
|
310
|
+
* category-comic-dc No DC band at all.
|
|
311
|
+
* category-game-* Three leaves, no franchise-level bands.
|
|
312
|
+
* the TCG expansion-set leaves Booster pack / box / ETB are a FORMAT
|
|
313
|
+
* axis (the `tcg-format` feature group),
|
|
314
|
+
* and a set leaf holds all of them mixed.
|
|
315
|
+
* No single band describes it honestly.
|
|
316
|
+
* the Gunpla grade leaves No model-kit pricing in any of the
|
|
317
|
+
* fifteen crawls.
|
|
318
|
+
* category-hw-multipacks children No multipack band.
|
|
319
|
+
* the accessory leaves No band for cases, tools or counters.
|
|
320
|
+
* the living-collectibles leaves Not a market we crawled, and would not
|
|
321
|
+
* want a competitor-derived band for.
|
|
322
|
+
*/
|
|
323
|
+
};
|
|
324
|
+
export function marketBand(leafId) {
|
|
325
|
+
const b = MARKET_BANDS[leafId];
|
|
326
|
+
if (!b)
|
|
327
|
+
return undefined;
|
|
328
|
+
return {
|
|
329
|
+
currency: "INR",
|
|
330
|
+
market: {
|
|
331
|
+
median: b.median,
|
|
332
|
+
...(b.p25 == null ? {} : { p25: b.p25 }),
|
|
333
|
+
...(b.p75 == null ? {} : { p75: b.p75 }),
|
|
334
|
+
...(b.n == null ? {} : { sampleSize: b.n }),
|
|
335
|
+
asOf: CRAWL_AS_OF,
|
|
336
|
+
/*
|
|
337
|
+
* Measured across the five largest competitors: 92.8 / 88.5 / 87.0 /
|
|
338
|
+
* 81.1 / 76.0% of their listings are sold out. Carried so the seller
|
|
339
|
+
* hint can say "this is a typical ASK, and most asks at this level do
|
|
340
|
+
* not convert" rather than presenting it as a clearing price.
|
|
341
|
+
*/
|
|
342
|
+
soldOutShare: 0.85,
|
|
343
|
+
source: b.src,
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/** Every leaf id this map claims a band for — the seed asserts they all exist. */
|
|
348
|
+
export const MARKET_BAND_LEAF_IDS = Object.keys(MARKET_BANDS);
|
|
349
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
350
|
+
// Per-vertical productDefaults
|
|
351
|
+
//
|
|
352
|
+
// 🛑 taxCodeId, not gstRate/hsnCode. The reference is for AUTHORING; the
|
|
353
|
+
// scalars are resolved onto the PRODUCT by `deriveTaxonomy` and snapshotted
|
|
354
|
+
// again onto the order item. Without that layering, editing a tax code
|
|
355
|
+
// would retroactively re-rate an invoice already issued.
|
|
356
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
357
|
+
const TAX_TOY = "tax-hsn-95030020"; // non-electronic toys incl. spinning tops — 5%
|
|
358
|
+
const TAX_TOY_OTHER = "tax-hsn-95030099"; // other toys — 5%
|
|
359
|
+
const TAX_EXEMPT = "tax-exempt-0";
|
|
360
|
+
/** Shared across every Beyblade-family node. */
|
|
361
|
+
const BEY_FAQ_TEMPLATES = [
|
|
362
|
+
{
|
|
363
|
+
question: "Is {{title}} an original {{brand}} release?",
|
|
364
|
+
answer: "The authenticity of every listing is stated on the listing itself — original, reproduction, or unverified. A reproduction is an unlicensed copy with no manufacturer's marque and is always labelled as one; it is not the same thing as a counterfeit sold as genuine.",
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
question: "What condition is {{title}} in?",
|
|
368
|
+
answer: "This listing is graded {{condition}}, and the photographs are of the actual item rather than a stock image. Structural damage — a chip, a crack, a stripped tooth — is called out in the description and photographed.",
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
question: "Does {{title}} include a launcher?",
|
|
372
|
+
answer: "Only if the description says so. Starter releases include a launcher and ripcord; booster releases are top-only. Where a used listing is involved, what is pictured is what ships.",
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
question: "Can I return {{title}} if it is not as described?",
|
|
376
|
+
answer: "Yes. A listing that misrepresents condition or authenticity is covered by buyer protection, and the return window opens from delivery. Report the listing as well as opening the return — a misdescribed reproduction is a listing problem, not just an order problem.",
|
|
377
|
+
},
|
|
378
|
+
];
|
|
379
|
+
const SPEC = (name, value, unit) => unit ? { name, value, unit } : { name, value };
|
|
380
|
+
/** A complete top. */
|
|
381
|
+
export function beyDefaults(leafId, series, system) {
|
|
382
|
+
return {
|
|
383
|
+
taxCodeId: TAX_TOY,
|
|
384
|
+
specifications: [SPEC("Series", series), SPEC("System", system)],
|
|
385
|
+
inTheBox: ["Beyblade top", "Manual (where supplied)"],
|
|
386
|
+
defaultAuthenticity: "unverified",
|
|
387
|
+
priceGuidance: marketBand(leafId),
|
|
388
|
+
seoTitleTemplate: "{{title}} — {{series}} | {{siteName}}",
|
|
389
|
+
seoDescriptionTemplate: "Buy {{title}}, a {{series}} {{category}}, from a verified seller on {{siteName}}. Condition {{condition}}, authenticity stated, buyer protection on every order.",
|
|
390
|
+
seoKeywords: ["beyblade", series.toLowerCase()],
|
|
391
|
+
weightG: 120,
|
|
392
|
+
dimensionsCm: { l: 12, w: 10, h: 6 },
|
|
393
|
+
fragile: false,
|
|
394
|
+
faqTemplates: BEY_FAQ_TEMPLATES,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/** A loose component. */
|
|
398
|
+
export function partDefaults(leafId, partKind) {
|
|
399
|
+
return {
|
|
400
|
+
taxCodeId: TAX_TOY,
|
|
401
|
+
specifications: [SPEC("Part type", partKind)],
|
|
402
|
+
inTheBox: [`${partKind} only — no complete top`],
|
|
403
|
+
defaultAuthenticity: "unverified",
|
|
404
|
+
priceGuidance: marketBand(leafId),
|
|
405
|
+
seoTitleTemplate: "{{title}} — {{category}} | {{siteName}}",
|
|
406
|
+
seoDescriptionTemplate: "Buy {{title}}, a loose {{category}} for custom builds, on {{siteName}}. Condition {{condition}}, sold as pictured.",
|
|
407
|
+
seoKeywords: ["beyblade parts", partKind.toLowerCase()],
|
|
408
|
+
weightG: 25,
|
|
409
|
+
dimensionsCm: { l: 8, w: 6, h: 3 },
|
|
410
|
+
fragile: false,
|
|
411
|
+
/*
|
|
412
|
+
* A part listing answers a different question from a complete top: the
|
|
413
|
+
* buyer already owns a bey and wants to know whether this fits it. So the
|
|
414
|
+
* first FAQ is compatibility, not authenticity — which is also the single
|
|
415
|
+
* most common pre-purchase question on used-part listings.
|
|
416
|
+
*/
|
|
417
|
+
faqTemplates: [
|
|
418
|
+
{
|
|
419
|
+
question: "Will {{title}} fit my Beyblade?",
|
|
420
|
+
answer: "Parts are compatible within a generation, not across them. A {{series}} component fits other {{series}} tops; it will not fit a different generation's system, because each generation uses its own locking geometry.",
|
|
421
|
+
},
|
|
422
|
+
BEY_FAQ_TEMPLATES[1],
|
|
423
|
+
BEY_FAQ_TEMPLATES[3],
|
|
424
|
+
],
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
export function launcherDefaults(leafId, series) {
|
|
428
|
+
return {
|
|
429
|
+
taxCodeId: TAX_TOY,
|
|
430
|
+
specifications: [SPEC("Series", series), SPEC("Accessory type", "Launcher")],
|
|
431
|
+
inTheBox: ["Launcher", "Ripcord or winder (where supplied)"],
|
|
432
|
+
defaultAuthenticity: "unverified",
|
|
433
|
+
priceGuidance: marketBand(leafId),
|
|
434
|
+
seoTitleTemplate: "{{title}} — {{series}} launcher | {{siteName}}",
|
|
435
|
+
seoDescriptionTemplate: "Buy {{title}}, a {{series}} launcher, on {{siteName}}. Condition {{condition}}, tested where the seller states so.",
|
|
436
|
+
seoKeywords: ["beyblade launcher", series.toLowerCase()],
|
|
437
|
+
weightG: 90,
|
|
438
|
+
dimensionsCm: { l: 16, w: 10, h: 4 },
|
|
439
|
+
fragile: false,
|
|
440
|
+
faqTemplates: [
|
|
441
|
+
{
|
|
442
|
+
question: "Which Beyblades does {{title}} launch?",
|
|
443
|
+
answer: "A {{series}} launcher drives {{series}} tops. Launchers are not interchangeable across generations — the ripcord, the prongs and the lock all changed between them.",
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
question: "Is the ripcord included with {{title}}?",
|
|
447
|
+
answer: "Check the description. A ripcord is a wear part and is frequently sold separately or replaced; where one is included the listing says so and it is in the photographs.",
|
|
448
|
+
},
|
|
449
|
+
BEY_FAQ_TEMPLATES[1],
|
|
450
|
+
],
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
export function stadiumDefaults(leafId, series) {
|
|
454
|
+
return {
|
|
455
|
+
taxCodeId: TAX_TOY,
|
|
456
|
+
specifications: [SPEC("Series", series), SPEC("Accessory type", "Stadium")],
|
|
457
|
+
inTheBox: ["Stadium", "Assembly parts where the design requires them"],
|
|
458
|
+
defaultAuthenticity: "unverified",
|
|
459
|
+
priceGuidance: marketBand(leafId),
|
|
460
|
+
seoTitleTemplate: "{{title}} — {{series}} stadium | {{siteName}}",
|
|
461
|
+
seoDescriptionTemplate: "Buy {{title}}, a {{series}} Beyblade stadium, on {{siteName}}. Condition {{condition}}, dimensions and compatibility in the description.",
|
|
462
|
+
seoKeywords: ["beyblade stadium", "beystadium", series.toLowerCase()],
|
|
463
|
+
/*
|
|
464
|
+
* 🛑 Stadiums are the one Beyblade node where shipping actually bites — a
|
|
465
|
+
* ~35 cm dish is volumetric freight, not a 120 g parcel. The weight and
|
|
466
|
+
* dimensions here are what a courier rate is computed from, so getting
|
|
467
|
+
* them wrong under-quotes every stadium listing.
|
|
468
|
+
*/
|
|
469
|
+
weightG: 900,
|
|
470
|
+
dimensionsCm: { l: 40, w: 40, h: 12 },
|
|
471
|
+
fragile: true,
|
|
472
|
+
faqTemplates: [
|
|
473
|
+
{
|
|
474
|
+
question: "Can I use {{title}} with every Beyblade generation?",
|
|
475
|
+
answer: "Mostly. Round stadiums handle Original, Metal Fight and Burst play. Beyblade X's Xtreme Line stadiums add a rail feature X tops are designed around, so serious X play wants an X-format stadium rather than a round one.",
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
question: "Does {{title}} ship assembled?",
|
|
479
|
+
answer: "A stadium ships flat or in its original packaging wherever the design allows it, because a rigid dish is volumetric freight. Any assembly required is noted in the description.",
|
|
480
|
+
},
|
|
481
|
+
BEY_FAQ_TEMPLATES[1],
|
|
482
|
+
],
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
export function tcgDefaults(leafId, game, format) {
|
|
486
|
+
return {
|
|
487
|
+
/*
|
|
488
|
+
* Chapter 9504 (playing cards) is the likelier correct HSN for a TCG
|
|
489
|
+
* single and it is deliberately NOT seeded: its current rate is
|
|
490
|
+
* UNVERIFIED, and a wrong HSN on a real invoice is worse than a
|
|
491
|
+
* conservative one. 9503 0099 ("other toys", 5%) is the rate the Indian
|
|
492
|
+
* hobby trade actually invoices sealed product at, and it is the one
|
|
493
|
+
* chapter here that was confirmed. Revisit when 9504 is verified.
|
|
494
|
+
*/
|
|
495
|
+
taxCodeId: TAX_TOY_OTHER,
|
|
496
|
+
specifications: [SPEC("Game", game), SPEC("Format", format)],
|
|
497
|
+
defaultAuthenticity: "unverified",
|
|
498
|
+
priceGuidance: marketBand(leafId),
|
|
499
|
+
seoTitleTemplate: "{{title}} — {{category}} | {{siteName}}",
|
|
500
|
+
seoDescriptionTemplate: "Buy {{title}}, a {{categoryPath}} listing on {{siteName}}. Condition {{condition}}, language and print run stated on the listing.",
|
|
501
|
+
seoKeywords: [game.toLowerCase(), "trading cards", format.toLowerCase()],
|
|
502
|
+
weightG: format === "Sealed" ? 450 : 15,
|
|
503
|
+
dimensionsCm: format === "Sealed" ? { l: 20, w: 15, h: 10 } : { l: 10, w: 7, h: 1 },
|
|
504
|
+
fragile: false,
|
|
505
|
+
/*
|
|
506
|
+
* The tcgindia model — five templated FAQs per product, three of which
|
|
507
|
+
* interpolate the name. Nobody else in the crawl of fifteen does it, and
|
|
508
|
+
* it is the cheapest structured-data win available to us.
|
|
509
|
+
*/
|
|
510
|
+
faqTemplates: [
|
|
511
|
+
{
|
|
512
|
+
question: "Is {{title}} authentic?",
|
|
513
|
+
answer: "Yes. Cards and sealed product listed here come from verified sellers, and a listing that misrepresents authenticity is covered by buyer protection. Language and print run are stated because a Japanese print and an English print of the same card are different products.",
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
question: "Is {{title}} sealed?",
|
|
517
|
+
answer: "The listing states it. Sealed product is unopened and factory-wrapped; a single is an individual card whose condition is graded on the listing.",
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
question: "Can I request {{title}} if it is out of stock?",
|
|
521
|
+
answer: "Yes — add it to your wishlist and you will be notified when a seller lists it again. Sold listings stay visible here on purpose, so you can see what has been available and at what price.",
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
question: "Are specific pulls guaranteed from {{title}}?",
|
|
525
|
+
answer: "No. Sealed product is random by design and no seller here guarantees a particular card from a pack or box. Any listing claiming a guaranteed pull from sealed product should be reported.",
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
question: "Can I return {{title}}?",
|
|
529
|
+
answer: "A misdescribed card or a damaged delivery is returnable within the stated window. Opened sealed product is not returnable on the basis of what was inside it, for the reason above.",
|
|
530
|
+
},
|
|
531
|
+
],
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
export function figureDefaults(leafId, scaleOrLine) {
|
|
535
|
+
return {
|
|
536
|
+
taxCodeId: TAX_TOY_OTHER,
|
|
537
|
+
specifications: [SPEC("Scale / line", scaleOrLine)],
|
|
538
|
+
inTheBox: ["Figure", "Stand and accessories as supplied by the maker"],
|
|
539
|
+
defaultAuthenticity: "unverified",
|
|
540
|
+
priceGuidance: marketBand(leafId),
|
|
541
|
+
seoTitleTemplate: "{{title}} — {{category}} | {{siteName}}",
|
|
542
|
+
seoDescriptionTemplate: "Buy {{title}}, a {{category}} collectible figure, on {{siteName}}. Condition {{condition}}, box state stated, buyer protection on every order.",
|
|
543
|
+
seoKeywords: ["collectible figure", "anime figure", scaleOrLine.toLowerCase()],
|
|
544
|
+
weightG: 700,
|
|
545
|
+
dimensionsCm: { l: 30, w: 20, h: 20 },
|
|
546
|
+
/*
|
|
547
|
+
* PVC at scale is genuinely fragile — a 1/7 figure has thin painted
|
|
548
|
+
* extremities and the box is part of the value. This is the one vertical
|
|
549
|
+
* where `fragile` drives packaging instructions rather than being advisory.
|
|
550
|
+
*/
|
|
551
|
+
fragile: true,
|
|
552
|
+
faqTemplates: [
|
|
553
|
+
{
|
|
554
|
+
question: "Is {{title}} an authentic {{brand}} figure?",
|
|
555
|
+
answer: "Authenticity is stated on the listing. Bootleg figures are common at scale and are identified by paint quality, seam finish and box printing — a seller listing a bootleg must label it as a reproduction.",
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
question: "What condition is the box of {{title}} in?",
|
|
559
|
+
answer: "Box condition is called out separately from figure condition, because for a collectible figure the two are priced differently. A \"without box\" listing says so in its title.",
|
|
560
|
+
},
|
|
561
|
+
BEY_FAQ_TEMPLATES[3],
|
|
562
|
+
],
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
export function diecastDefaults(leafId, line) {
|
|
566
|
+
return {
|
|
567
|
+
taxCodeId: TAX_TOY,
|
|
568
|
+
specifications: [SPEC("Line", line), SPEC("Scale", "1:64")],
|
|
569
|
+
inTheBox: ["Die-cast model on its original card where carded"],
|
|
570
|
+
defaultAuthenticity: "unverified",
|
|
571
|
+
priceGuidance: marketBand(leafId),
|
|
572
|
+
seoTitleTemplate: "{{title}} — Hot Wheels {{category}} | {{siteName}}",
|
|
573
|
+
seoDescriptionTemplate: "Buy {{title}}, a Hot Wheels {{category}} die-cast, on {{siteName}}. Card condition stated; chase variants identified on the listing.",
|
|
574
|
+
seoKeywords: ["hot wheels", "diecast", "1:64", line.toLowerCase()],
|
|
575
|
+
weightG: 60,
|
|
576
|
+
dimensionsCm: { l: 17, w: 9, h: 4 },
|
|
577
|
+
fragile: false,
|
|
578
|
+
faqTemplates: [
|
|
579
|
+
{
|
|
580
|
+
question: "Is {{title}} a Treasure Hunt?",
|
|
581
|
+
answer: "Chase variants are identified on the listing itself, not inferred. A Treasure Hunt carries the flame circle logo; a Super Treasure Hunt additionally has Real Riders rubber tyres and Spectraflame paint. A listing that claims a chase without showing it should be reported.",
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
question: "Is the card for {{title}} in good condition?",
|
|
585
|
+
answer: "Card condition is graded separately from the car, because carded collectors price it separately. Creases, bends and blister clouding are photographed where present.",
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
question: "Is {{title}} an Indian-card release?",
|
|
589
|
+
answer: "Where it is, the listing says so. The Indian-market blister card differs from the US one and collectors here price the two differently, so it is called out rather than left for the buyer to spot.",
|
|
590
|
+
},
|
|
591
|
+
],
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
export function accessoryDefaults(leafId, kind) {
|
|
595
|
+
return {
|
|
596
|
+
taxCodeId: TAX_TOY_OTHER,
|
|
597
|
+
specifications: [SPEC("Accessory type", kind)],
|
|
598
|
+
defaultAuthenticity: "unverified",
|
|
599
|
+
priceGuidance: marketBand(leafId),
|
|
600
|
+
seoTitleTemplate: "{{title}} — {{category}} | {{siteName}}",
|
|
601
|
+
seoDescriptionTemplate: "Buy {{title}}, a {{category}}, on {{siteName}}. Condition {{condition}}, capacity and dimensions in the description.",
|
|
602
|
+
seoKeywords: [kind.toLowerCase(), "collectible accessories"],
|
|
603
|
+
weightG: 400,
|
|
604
|
+
dimensionsCm: { l: 28, w: 20, h: 8 },
|
|
605
|
+
fragile: false,
|
|
606
|
+
faqTemplates: [
|
|
607
|
+
{
|
|
608
|
+
question: "What fits in {{title}}?",
|
|
609
|
+
answer: "Capacity is in the description. Storage designed for complete tops and storage designed for loose parts are different products, and the listing states which this is.",
|
|
610
|
+
},
|
|
611
|
+
BEY_FAQ_TEMPLATES[1],
|
|
612
|
+
],
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
/** Live animals and plants — exempt, and nothing about them is a toy. */
|
|
616
|
+
export function livingDefaults() {
|
|
617
|
+
return {
|
|
618
|
+
taxCodeId: TAX_EXEMPT,
|
|
619
|
+
defaultAuthenticity: "original",
|
|
620
|
+
seoTitleTemplate: "{{title}} — {{category}} | {{siteName}}",
|
|
621
|
+
seoDescriptionTemplate: "{{title}}, a {{category}} listing from a verified seller on {{siteName}}. Species, age and provenance disclosed; jurisdiction checked before payment.",
|
|
622
|
+
fragile: true,
|
|
623
|
+
faqTemplates: [
|
|
624
|
+
{
|
|
625
|
+
question: "Can {{title}} be shipped to me?",
|
|
626
|
+
answer: "The jurisdiction check runs at checkout, before payment rather than after. Most live listings are local-collection only and the listing states the collection area; where courier transport is lawful and appropriate the seller arranges a specialist live service.",
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
question: "What welfare information comes with {{title}}?",
|
|
630
|
+
answer: "Species, age, sex where known, and provenance are recorded on the listing, and a seller must be verified before any live listing goes public.",
|
|
631
|
+
},
|
|
632
|
+
],
|
|
633
|
+
};
|
|
634
|
+
}
|