@mohasinac/appkit 4.56.0 → 4.58.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 (172) hide show
  1. package/dist/.build-stamp.json +4 -4
  2. package/dist/_internal/server/features/beys/data.d.ts +44 -0
  3. package/dist/_internal/server/features/beys/data.js +107 -0
  4. package/dist/_internal/server/features/beys/og.d.ts +42 -0
  5. package/dist/_internal/server/features/beys/og.js +89 -0
  6. package/dist/_internal/server/features/checkout/actions.d.ts +31 -0
  7. package/dist/_internal/server/features/checkout/actions.js +117 -5
  8. package/dist/_internal/server/features/orders/actions.d.ts +30 -0
  9. package/dist/_internal/server/features/orders/actions.js +103 -0
  10. package/dist/_internal/server/features/orders/adapters.js +7 -0
  11. package/dist/_internal/server/features/parts/data.js +3 -2
  12. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  13. package/dist/_internal/shared/actions/action-registry.js +139 -0
  14. package/dist/_internal/shared/shipping/quote.js +5 -6
  15. package/dist/client.d.ts +23 -0
  16. package/dist/client.js +3 -0
  17. package/dist/constants/api-endpoints.d.ts +12 -0
  18. package/dist/constants/api-endpoints.js +10 -0
  19. package/dist/constants/field-names.d.ts +46 -0
  20. package/dist/constants/field-names.js +46 -0
  21. package/dist/features/about/components/PublicProfileView.js +2 -1
  22. package/dist/features/account/components/UserOrdersView.js +25 -3
  23. package/dist/features/admin/components/AdminBeysView.d.ts +27 -0
  24. package/dist/features/admin/components/AdminBeysView.js +178 -0
  25. package/dist/features/admin/components/AdminPartsView.d.ts +27 -0
  26. package/dist/features/admin/components/AdminPartsView.js +160 -0
  27. package/dist/features/admin/components/OwnerQuickActions.d.ts +19 -2
  28. package/dist/features/admin/components/OwnerQuickActions.js +83 -8
  29. package/dist/features/admin/components/RecordOwnerQuickActions.d.ts +31 -0
  30. package/dist/features/admin/components/RecordOwnerQuickActions.js +128 -0
  31. package/dist/features/admin/components/ReferenceRecordEditor.d.ts +45 -0
  32. package/dist/features/admin/components/ReferenceRecordEditor.js +153 -0
  33. package/dist/features/admin/schemas/firestore.d.ts +1 -1
  34. package/dist/features/auctions/components/MarketplaceAuctionCard.js +15 -2
  35. package/dist/features/auth/permissions/constants.d.ts +1 -1
  36. package/dist/features/auth/permissions/constants.js +12 -1
  37. package/dist/features/beys/components/BeyDetailPageView.d.ts +50 -0
  38. package/dist/features/beys/components/BeyDetailPageView.js +98 -0
  39. package/dist/features/beys/schemas/firestore.d.ts +417 -0
  40. package/dist/features/beys/schemas/firestore.js +65 -0
  41. package/dist/features/blog/components/BlogFeaturedCard.js +15 -2
  42. package/dist/features/blog/components/BlogPostView.js +2 -1
  43. package/dist/features/cart/components/CartPriceBreakdown.d.ts +2 -0
  44. package/dist/features/cart/components/CartPriceBreakdown.js +1 -1
  45. package/dist/features/categories/components/BrandDetailPageView.js +4 -2
  46. package/dist/features/categories/components/BundleDetailView.js +2 -2
  47. package/dist/features/categories/components/CategoryDetailPageView.js +36 -3
  48. package/dist/features/categories/components/CategoryGrid.js +24 -2
  49. package/dist/features/categories/components/FranchiseDetailPageView.js +2 -1
  50. package/dist/features/events/components/EventCard.js +15 -2
  51. package/dist/features/faq/components/FAQPageContent.js +17 -4
  52. package/dist/features/faq/constants/help-guides.d.ts +36 -0
  53. package/dist/features/faq/constants/help-guides.js +64 -0
  54. package/dist/features/grouped/components/GroupedListingDetailView.js +2 -3
  55. package/dist/features/homepage/components/CustomCardsSection.js +67 -50
  56. package/dist/features/homepage/components/HeroCarousel.js +3 -1
  57. package/dist/features/homepage/components/ShopByCategorySection.js +23 -10
  58. package/dist/features/homepage/lib/section-renderer.js +2 -1
  59. package/dist/features/homepage/schemas/index.d.ts +10 -10
  60. package/dist/features/orders/components/EmiInstallmentsPanel.d.ts +19 -0
  61. package/dist/features/orders/components/EmiInstallmentsPanel.js +57 -0
  62. package/dist/features/orders/components/index.d.ts +1 -0
  63. package/dist/features/orders/components/index.js +1 -0
  64. package/dist/features/orders/schemas/index.d.ts +2 -2
  65. package/dist/features/orders/types/index.d.ts +15 -0
  66. package/dist/features/parts/components/PartDetailPageView.js +5 -3
  67. package/dist/features/parts/components/PartsSearchBox.js +79 -4
  68. package/dist/features/parts/schemas/admin-edit.d.ts +81 -0
  69. package/dist/features/parts/schemas/admin-edit.js +89 -0
  70. package/dist/features/parts/schemas/firestore.d.ts +2 -1
  71. package/dist/features/points/repository/points.repository.js +5 -5
  72. package/dist/features/points/schemas/firestore.d.ts +16 -0
  73. package/dist/features/points/schemas/firestore.js +17 -0
  74. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +15 -2
  75. package/dist/features/products/components/MarketplaceBundleCard.js +15 -2
  76. package/dist/features/products/components/MarketplacePrizeDrawCard.js +15 -2
  77. package/dist/features/products/components/ProductGrid.js +18 -2
  78. package/dist/features/products/schemas/firestore.d.ts +18 -0
  79. package/dist/features/reviews/components/ReviewDetailPageView.js +3 -2
  80. package/dist/features/seller/components/CategoryInlineSelect.d.ts +10 -1
  81. package/dist/features/seller/components/CategoryInlineSelect.js +53 -5
  82. package/dist/features/seller/components/SellerProductShell.d.ts +39 -0
  83. package/dist/features/seller/components/SellerProductShell.js +31 -3
  84. package/dist/features/stores/components/StoreHeader.js +3 -1
  85. package/dist/index.d.ts +8 -0
  86. package/dist/index.js +13 -0
  87. package/dist/next/routing/route-map.d.ts +17 -1
  88. package/dist/next/routing/route-map.js +39 -0
  89. package/dist/schemas/registry.d.ts +8 -8
  90. package/dist/seed/_helpers/category-models/chunk0.d.ts +2 -0
  91. package/dist/seed/_helpers/category-models/chunk0.js +2648 -0
  92. package/dist/seed/_helpers/category-models/chunk1.d.ts +2 -0
  93. package/dist/seed/_helpers/category-models/chunk1.js +2318 -0
  94. package/dist/seed/_helpers/category-models/chunk2.d.ts +2 -0
  95. package/dist/seed/_helpers/category-models/chunk2.js +2560 -0
  96. package/dist/seed/_helpers/category-models/chunk3.d.ts +2 -0
  97. package/dist/seed/_helpers/category-models/chunk3.js +2196 -0
  98. package/dist/seed/_helpers/category-models/chunk4.d.ts +2 -0
  99. package/dist/seed/_helpers/category-models/chunk4.js +1732 -0
  100. package/dist/seed/_helpers/category-models.d.ts +9 -2
  101. package/dist/seed/_helpers/category-models.js +19 -1368
  102. package/dist/seed/_helpers/category-parts-glossary-map.js +38 -38
  103. package/dist/seed/_helpers/category-parts.js +39 -39
  104. package/dist/seed/bey-products-seed-data.d.ts +18 -0
  105. package/dist/seed/bey-products-seed-data.js +18011 -0
  106. package/dist/seed/beys/chunk0.d.ts +17 -0
  107. package/dist/seed/beys/chunk0.js +36733 -0
  108. package/dist/seed/beys/chunk1.d.ts +17 -0
  109. package/dist/seed/beys/chunk1.js +35728 -0
  110. package/dist/seed/beys/chunk2.d.ts +17 -0
  111. package/dist/seed/beys/chunk2.js +35857 -0
  112. package/dist/seed/beys/chunk3.d.ts +17 -0
  113. package/dist/seed/beys/chunk3.js +34476 -0
  114. package/dist/seed/beys-seed-data.d.ts +18 -0
  115. package/dist/seed/beys-seed-data.js +11 -0
  116. package/dist/seed/homepage-sections-seed-data.js +16 -0
  117. package/dist/seed/index.d.ts +2 -0
  118. package/dist/seed/index.js +3 -0
  119. package/dist/seed/media-assets-seed-data.js +4 -4
  120. package/dist/seed/products-catalogue/chunk0.d.ts +12 -0
  121. package/dist/seed/products-catalogue/chunk0.js +7262 -0
  122. package/dist/seed/products-catalogue/chunk1.d.ts +12 -0
  123. package/dist/seed/products-catalogue/chunk1.js +7290 -0
  124. package/dist/seed/products-catalogue/chunk10.d.ts +12 -0
  125. package/dist/seed/products-catalogue/chunk10.js +7290 -0
  126. package/dist/seed/products-catalogue/chunk11.d.ts +12 -0
  127. package/dist/seed/products-catalogue/chunk11.js +7005 -0
  128. package/dist/seed/products-catalogue/chunk12.d.ts +12 -0
  129. package/dist/seed/products-catalogue/chunk12.js +7002 -0
  130. package/dist/seed/products-catalogue/chunk13.d.ts +12 -0
  131. package/dist/seed/products-catalogue/chunk13.js +7002 -0
  132. package/dist/seed/products-catalogue/chunk14.d.ts +12 -0
  133. package/dist/seed/products-catalogue/chunk14.js +7002 -0
  134. package/dist/seed/products-catalogue/chunk15.d.ts +12 -0
  135. package/dist/seed/products-catalogue/chunk15.js +7002 -0
  136. package/dist/seed/products-catalogue/chunk16.d.ts +12 -0
  137. package/dist/seed/products-catalogue/chunk16.js +7002 -0
  138. package/dist/seed/products-catalogue/chunk17.d.ts +12 -0
  139. package/dist/seed/products-catalogue/chunk17.js +7002 -0
  140. package/dist/seed/products-catalogue/chunk18.d.ts +12 -0
  141. package/dist/seed/products-catalogue/chunk18.js +4398 -0
  142. package/dist/seed/products-catalogue/chunk2.d.ts +12 -0
  143. package/dist/seed/products-catalogue/chunk2.js +7268 -0
  144. package/dist/seed/products-catalogue/chunk3.d.ts +12 -0
  145. package/dist/seed/products-catalogue/chunk3.js +7264 -0
  146. package/dist/seed/products-catalogue/chunk4.d.ts +12 -0
  147. package/dist/seed/products-catalogue/chunk4.js +7268 -0
  148. package/dist/seed/products-catalogue/chunk5.d.ts +12 -0
  149. package/dist/seed/products-catalogue/chunk5.js +7258 -0
  150. package/dist/seed/products-catalogue/chunk6.d.ts +12 -0
  151. package/dist/seed/products-catalogue/chunk6.js +7268 -0
  152. package/dist/seed/products-catalogue/chunk7.d.ts +12 -0
  153. package/dist/seed/products-catalogue/chunk7.js +7311 -0
  154. package/dist/seed/products-catalogue/chunk8.d.ts +12 -0
  155. package/dist/seed/products-catalogue/chunk8.js +7304 -0
  156. package/dist/seed/products-catalogue/chunk9.d.ts +12 -0
  157. package/dist/seed/products-catalogue/chunk9.js +7309 -0
  158. package/dist/seed/products-catalogue-seed-data.d.ts +2 -22
  159. package/dist/seed/products-catalogue-seed-data.js +58 -86382
  160. package/dist/seed/seed-purpose.js +9 -0
  161. package/dist/server.d.ts +5 -1
  162. package/dist/server.js +19 -0
  163. package/dist/styles.css +468 -1
  164. package/dist/tailwind-utilities.css +1 -1
  165. package/dist/ui/components/PosterCard.d.ts +99 -0
  166. package/dist/ui/components/PosterCard.js +109 -0
  167. package/dist/ui/components/PosterCard.style.css +466 -0
  168. package/dist/ui/components/index.style.css +1 -0
  169. package/dist/ui/index.d.ts +2 -0
  170. package/dist/ui/index.js +2 -0
  171. package/package.json +1 -1
  172. package/scripts/seed-cli.mjs +108 -1
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "stampVersion": 1,
3
- "version": "4.56.0",
4
- "head": "a2d85e72b5cac1f1a55ae7386f661b293eccd704",
3
+ "version": "4.58.0",
4
+ "head": "978d9ef892ec5d3ef08a87c7f040877faf3c5daa",
5
5
  "branch": "main",
6
- "dirty": true,
7
- "builtAt": "2026-10-20T16:58:51.945Z"
6
+ "dirty": false,
7
+ "builtAt": "2026-10-20T20:29:00.050Z"
8
8
  }
@@ -0,0 +1,44 @@
1
+ import { type BeyDocument, type BeyProductDocument } from "../../../../features/beys/schemas/firestore";
2
+ /**
3
+ * One bey, by slug.
4
+ *
5
+ * 🛑 A DIRECT DOC READ — `id === slug`, so this needs no query, no index and
6
+ * no scan. That is the whole reason the id is the slug.
7
+ */
8
+ export declare const getBeyBySlug: (slug: string) => Promise<BeyDocument | null>;
9
+ /**
10
+ * The bey(s) paired with a category leaf — the inverse of `BeyDocument.categoryId`.
11
+ *
12
+ * 🛑 ONE INDEXED EQUALITY, no composite index, which is the same shape
13
+ * `getCompositionsForCategory` uses for the parts side of this exact job. A
14
+ * single-field `==` is served by Firestore's automatic index, so this adds no
15
+ * index to declare and no deploy to wait on.
16
+ *
17
+ * 🛑 NOT `.limit(1)`, and that is measured rather than cautious. 1,880 beys
18
+ * carry a `categoryId` across 1,858 distinct leaves — so **22 leaves pair with
19
+ * TWO beys**, which are the vendor pairs (a Takara-Tomy release and its Hasbro
20
+ * equivalent merged onto one taxonomy leaf). `limit(1)` would render one of
21
+ * them and silently drop the other on 22 pages, with nothing to indicate a
22
+ * second existed. The cap is small and the extra rows are the point.
23
+ *
24
+ * Fails OPEN to an empty list for the same reason as below: a category page
25
+ * missing its reference link is a worse page, a category page that throws is no
26
+ * page at all.
27
+ */
28
+ export declare const getBeysForCategory: (categoryId: string) => Promise<BeyDocument[]>;
29
+ /**
30
+ * The products that ship a bey.
31
+ *
32
+ * 🛑 A QUERY, unlike `getModelsUsingPart`'s `getAll`, and the difference is a
33
+ * real asymmetry in the data rather than an inconsistency: a part row carries
34
+ * `usedBy` so its models are ids already in hand, whereas the product→bey edge
35
+ * lives on the PRODUCT (`assortment[].beyId`). A bey has no stored list of the
36
+ * products containing it.
37
+ *
38
+ * So this is one `array-contains` over a denormalised id array, bounded at 12.
39
+ * 🛑 It needs `assortmentBeyIds` on the product — a flat array of the ids
40
+ * inside `assortment[]`, because Firestore cannot `array-contains` on a field
41
+ * nested inside objects in an array. Until the seed carries that field this
42
+ * returns [] and says so, rather than running a scan.
43
+ */
44
+ export declare const getProductsContainingBey: (beyId: string) => Promise<BeyProductDocument[]>;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Reads for the public bey surfaces.
3
+ *
4
+ * 🛑 Wrapped in `React.cache` because a page and its `generateMetadata` share
5
+ * the call — and the NARROWEST shared call is what gets cached, not whatever
6
+ * is convenient to export. `React.cache` memoises per argument list, so two
7
+ * call sites passing different arguments get two entries and dedupe nothing
8
+ * (the `getStoreBySlug(slug)` vs `getStoreBySlug(slug, viewer)` trap).
9
+ */
10
+ import { cache } from "react";
11
+ import { BEYS_COLLECTION, BEY_PRODUCTS_COLLECTION, } from "../../../../features/beys/schemas/firestore.js";
12
+ import { getAdminDb } from "../../../../providers/db-firebase/index.js";
13
+ import { BEY_FIELDS } from "../../../../constants/field-names.js";
14
+ import { serverLogger } from "../../../../monitoring/server-logger.js";
15
+ import { normalizeError } from "../../../../errors/normalize.js";
16
+ /**
17
+ * One bey, by slug.
18
+ *
19
+ * 🛑 A DIRECT DOC READ — `id === slug`, so this needs no query, no index and
20
+ * no scan. That is the whole reason the id is the slug.
21
+ */
22
+ export const getBeyBySlug = cache(async (slug) => {
23
+ try {
24
+ const snap = await getAdminDb().collection(BEYS_COLLECTION).doc(slug).get();
25
+ return snap.exists ? { id: snap.id, ...snap.data() } : null;
26
+ }
27
+ catch (err) {
28
+ const e = normalizeError(err);
29
+ serverLogger.warn("getBeyBySlug failed", { slug, error: e.message });
30
+ return null;
31
+ }
32
+ });
33
+ /**
34
+ * The bey(s) paired with a category leaf — the inverse of `BeyDocument.categoryId`.
35
+ *
36
+ * 🛑 ONE INDEXED EQUALITY, no composite index, which is the same shape
37
+ * `getCompositionsForCategory` uses for the parts side of this exact job. A
38
+ * single-field `==` is served by Firestore's automatic index, so this adds no
39
+ * index to declare and no deploy to wait on.
40
+ *
41
+ * 🛑 NOT `.limit(1)`, and that is measured rather than cautious. 1,880 beys
42
+ * carry a `categoryId` across 1,858 distinct leaves — so **22 leaves pair with
43
+ * TWO beys**, which are the vendor pairs (a Takara-Tomy release and its Hasbro
44
+ * equivalent merged onto one taxonomy leaf). `limit(1)` would render one of
45
+ * them and silently drop the other on 22 pages, with nothing to indicate a
46
+ * second existed. The cap is small and the extra rows are the point.
47
+ *
48
+ * Fails OPEN to an empty list for the same reason as below: a category page
49
+ * missing its reference link is a worse page, a category page that throws is no
50
+ * page at all.
51
+ */
52
+ export const getBeysForCategory = cache(async (categoryId) => {
53
+ try {
54
+ const snap = await getAdminDb()
55
+ .collection(BEYS_COLLECTION)
56
+ .where(BEY_FIELDS.CATEGORY_ID, "==", categoryId)
57
+ .limit(4)
58
+ .get();
59
+ return snap.docs.map((d) => ({ id: d.id, ...d.data() }));
60
+ }
61
+ catch (err) {
62
+ const e = normalizeError(err);
63
+ serverLogger.warn("getBeysForCategory failed", { categoryId, error: e.message });
64
+ return [];
65
+ }
66
+ });
67
+ /**
68
+ * The products that ship a bey.
69
+ *
70
+ * 🛑 A QUERY, unlike `getModelsUsingPart`'s `getAll`, and the difference is a
71
+ * real asymmetry in the data rather than an inconsistency: a part row carries
72
+ * `usedBy` so its models are ids already in hand, whereas the product→bey edge
73
+ * lives on the PRODUCT (`assortment[].beyId`). A bey has no stored list of the
74
+ * products containing it.
75
+ *
76
+ * So this is one `array-contains` over a denormalised id array, bounded at 12.
77
+ * 🛑 It needs `assortmentBeyIds` on the product — a flat array of the ids
78
+ * inside `assortment[]`, because Firestore cannot `array-contains` on a field
79
+ * nested inside objects in an array. Until the seed carries that field this
80
+ * returns [] and says so, rather than running a scan.
81
+ */
82
+ export const getProductsContainingBey = cache(async (beyId) => {
83
+ try {
84
+ const snap = await getAdminDb()
85
+ .collection(BEY_PRODUCTS_COLLECTION)
86
+ .where("assortmentBeyIds", "array-contains", beyId)
87
+ .limit(12)
88
+ .get();
89
+ return snap.docs.map((d) => ({ id: d.id, ...d.data() }));
90
+ }
91
+ catch (err) {
92
+ const e = normalizeError(err);
93
+ /*
94
+ * Fails OPEN to an empty list. A missing composite index throws
95
+ * `FAILED_PRECONDITION`, and the right outcome is a bey page without its
96
+ * "found in" section rather than a broken page — the caller wraps this in
97
+ * `safeRead` as well, so the degradation is recorded rather than silent.
98
+ *
99
+ * 🛑 `normalizeError` is the FIRST statement on purpose. It was below
100
+ * this comment, which put it outside audit-catch-normalize's proximity
101
+ * window — so the audit correctly reported it as missing even though the
102
+ * call was right there. Comment after the call, never before it.
103
+ */
104
+ serverLogger.warn("getProductsContainingBey failed", { beyId, error: e.message });
105
+ return [];
106
+ }
107
+ });
@@ -0,0 +1,42 @@
1
+ /**
2
+ * OG card for `/beys/{slug}`.
3
+ *
4
+ * ── 🛑 NO IMAGE FETCHES, FOR THE REASON THE PARTS CARDS DO NOT EITHER ─────
5
+ *
6
+ * Beys DO have photographs — 1,903 of 1,988 — so this is a deliberate refusal
7
+ * rather than a gap. The project was suspended for **Fast Origin Transfer at
8
+ * 5.25× its allowance** (52.52 GB against 10 GB), traced to render-time image
9
+ * proxying (Root Cause #104). 1,988 bey routes with one remote fetch each, on
10
+ * a crawler sweep, is precisely the amplification that was just removed. Type
11
+ * on a gradient costs one lambda and nothing else.
12
+ *
13
+ * ── 🛑 NO MONEY ON THE CANVAS ─────────────────────────────────────────────
14
+ *
15
+ * All eight money-bearing OG renderers were stripped, and the rule is stated
16
+ * in CLAUDE.md: an OG image is a public image URL with the amount burned into
17
+ * the pixels, so no sign-in gate reaches it and no paywall markup describes
18
+ * it. 1,309 beys carry a price in the research data and none of it appears
19
+ * here.
20
+ *
21
+ * `audit-guest-price-leak`'s R1 blocks a `₹` in any `og.tsx`, so this is
22
+ * mechanically enforced rather than left to discipline.
23
+ */
24
+ import type { ReactElement } from "react";
25
+ import type { BeyDocument } from "../../../../features/beys/schemas/firestore";
26
+ /**
27
+ * One bey's card.
28
+ *
29
+ * 🛑 EVERY CLAUSE IS CONDITIONAL, because the coverage genuinely varies: 93%
30
+ * of beys have a mass but only 248 state it directly, and `system` is on 94%
31
+ * while `type` and `spinDirection` are lower. A card reading "0 g" or an empty
32
+ * separator would be a claim the data does not support — the same reason the
33
+ * parts card builds its detail line by filtering.
34
+ *
35
+ * 🛑 The mass is marked `≈` when it was SUMMED from the parts rather than
36
+ * stated, which is the same honesty the detail page applies. A derived figure
37
+ * printed as an exact one is the single easiest way for this dataset to start
38
+ * lying.
39
+ */
40
+ export declare function renderBeyOg(bey: BeyDocument | null | undefined, opts: {
41
+ siteName: string;
42
+ }): ReactElement;
@@ -0,0 +1,89 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ const BG = "#0f172a";
3
+ const GRADIENT = "linear-gradient(135deg, #0f172a 0%, #1e293b 100%)";
4
+ const MUTED = "#94a3b8";
5
+ const ACCENT = "#5eead4";
6
+ /*
7
+ * 🛑 A COPY of the parts shell, not an import, and that is a considered
8
+ * choice rather than laziness.
9
+ *
10
+ * The Duplication Decision Framework's Rule of Three says extract on the
11
+ * THIRD copy, not the second — "two copies is observation; three is a
12
+ * pattern". There are now exactly two OG shells of this shape. Lifting it to
13
+ * a shared module today would also put a third consumer between every OG
14
+ * renderer and its layout for no behavioural gain.
15
+ *
16
+ * When a third appears, extract then. The constants above are duplicated for
17
+ * the same reason and in the same breath.
18
+ */
19
+ function shell(eyebrow, title, detail, siteName) {
20
+ return (_jsxs("div", { style: {
21
+ display: "flex",
22
+ width: "100%",
23
+ height: "100%",
24
+ background: BG,
25
+ fontFamily: "sans-serif",
26
+ position: "relative",
27
+ overflow: "hidden",
28
+ alignItems: "center",
29
+ justifyContent: "center",
30
+ }, children: [_jsx("div", { style: { position: "absolute", inset: 0, background: GRADIENT } }), _jsxs("div", { style: {
31
+ position: "relative",
32
+ display: "flex",
33
+ flexDirection: "column",
34
+ alignItems: "center",
35
+ gap: 24,
36
+ padding: "60px",
37
+ textAlign: "center",
38
+ }, children: [_jsx("div", { style: {
39
+ fontSize: 20,
40
+ color: ACCENT,
41
+ fontWeight: 600,
42
+ letterSpacing: 3,
43
+ textTransform: "uppercase",
44
+ }, children: eyebrow }), _jsx("div", { style: {
45
+ fontSize: title.length > 34 ? 56 : 76,
46
+ color: "#ffffff",
47
+ fontWeight: 700,
48
+ lineHeight: 1.1,
49
+ }, children: title }), detail && _jsx("div", { style: { fontSize: 28, color: MUTED }, children: detail }), _jsx("div", { style: { fontSize: 20, color: MUTED, marginTop: 8 }, children: siteName })] })] }));
50
+ }
51
+ /**
52
+ * One bey's card.
53
+ *
54
+ * 🛑 EVERY CLAUSE IS CONDITIONAL, because the coverage genuinely varies: 93%
55
+ * of beys have a mass but only 248 state it directly, and `system` is on 94%
56
+ * while `type` and `spinDirection` are lower. A card reading "0 g" or an empty
57
+ * separator would be a claim the data does not support — the same reason the
58
+ * parts card builds its detail line by filtering.
59
+ *
60
+ * 🛑 The mass is marked `≈` when it was SUMMED from the parts rather than
61
+ * stated, which is the same honesty the detail page applies. A derived figure
62
+ * printed as an exact one is the single easiest way for this dataset to start
63
+ * lying.
64
+ */
65
+ export function renderBeyOg(bey, opts) {
66
+ if (!bey)
67
+ return shell("Beyblades", "Beyblade database", "", opts.siteName);
68
+ const statedMass = typeof bey.weightG === "number" ? bey.weightG : null;
69
+ const summedMass = typeof bey.weightGFromParts === "number" ? bey.weightGFromParts : null;
70
+ const mass = statedMass !== null
71
+ ? `${statedMass} g`
72
+ : summedMass !== null
73
+ ? `≈ ${Math.round(summedMass * 10) / 10} g`
74
+ : null;
75
+ const bits = [
76
+ bey.type ? `${bey.type} type` : null,
77
+ bey.spinDirection ?? null,
78
+ mass,
79
+ bey.composition?.length ? `${bey.composition.length} parts` : null,
80
+ ].filter((b) => b !== null);
81
+ /*
82
+ * The eyebrow is the SYSTEM (Hybrid Wheel System, Burst Ultimate…) because
83
+ * that is the one line a collector uses to place a bey in time, and it is
84
+ * present on 94% of them. `series` is the fallback and "Beyblade" the floor
85
+ * — never an empty eyebrow, which reads as a broken template.
86
+ */
87
+ const eyebrow = bey.system ?? bey.series ?? "Beyblade";
88
+ return shell(eyebrow, bey.name, bits.join(" · "), opts.siteName);
89
+ }
@@ -1,3 +1,4 @@
1
+ import { type SelectedShippingQuote } from "../../../shared/shipping/quote";
1
2
  import { type StoreShippingPolicy } from "../../../shared/features/shipping/mode";
2
3
  import { type CartLane } from "../../../shared/checkout/lanes";
3
4
  import type { OutOfStockPolicy } from "../../../../features/orders/schemas/index";
@@ -12,6 +13,36 @@ export interface CreateCheckoutOrderInput {
12
13
  paymentMethod: CheckoutPaymentMethod;
13
14
  /** Required when paymentMethod === "emi" — the buyer's chosen tenure (2–6 months). */
14
15
  emiTenureMonths?: number;
16
+ /**
17
+ * The courier the buyer picked at checkout, with the ₹50-rounded rate they
18
+ * were shown.
19
+ *
20
+ * 🛑 OPTIONAL, and its absence is meaningful: a cart whose carrier quote
21
+ * failed, a digital-only cart, or any caller written before the picker
22
+ * existed falls back to `resolveShippingCost` exactly as before. Absence is
23
+ * what makes reconciliation skip the order as "no-quote" rather than
24
+ * measuring a seller's spend against a figure nobody was charged.
25
+ *
26
+ * 🛑 `chargedRate` is TRUSTED from the client, and that is a deliberate,
27
+ * bounded decision rather than an oversight. It is re-derived server-side
28
+ * through `roundShippingUp` before it is billed, so a tampered value can
29
+ * only ever be a different carrier rate rounded the same way — the gap
30
+ * worth closing is re-quoting the carrier at order time, which costs an
31
+ * outbound call on the payment path and is tracked separately.
32
+ */
33
+ selectedShippingQuote?: SelectedShippingQuote;
34
+ /**
35
+ * Points the buyer chose to spend on this checkout. 1 point = ₹1.
36
+ *
37
+ * 🛑 Requested, never trusted. The server re-reads the balance, re-applies
38
+ * the 50% cap against the real cart total, and spends the LESSER of the two
39
+ * — a client that asks for more than it holds gets its request trimmed, not
40
+ * an error, because the honest reading of "spend 500 when you have 300" is a
41
+ * stale page rather than an attack. `pointsRepository.redeem` then re-checks
42
+ * the balance a third time inside its own transaction, which is the check
43
+ * that actually prevents a negative balance under concurrency.
44
+ */
45
+ pointsToRedeem?: number;
15
46
  notes?: string;
16
47
  excludedProductIds?: string[];
17
48
  /** When true this is an admin test order — OTP consent is pre-granted and the order is created paid/processing. */
@@ -12,6 +12,9 @@ import { roundRupees } from "../../../../utils/number.formatter.js";
12
12
  import { ApiError, ValidationError, NotFoundError, ERROR_MESSAGES } from "../../../../errors/index.js";
13
13
  import { ORDER_FIELDS } from "../../../../constants/field-names.js";
14
14
  import { serverLogger } from "../../../../monitoring/index.js";
15
+ import { roundShippingUp, } from "../../../shared/shipping/quote.js";
16
+ import { pointsRepository } from "../../../../features/points/repository/points.repository.js";
17
+ import { maxRedeemablePoints } from "../../../../features/points/schemas/firestore.js";
15
18
  import { unitOfWork, adminNotificationsRepository, siteSettingsRepository, userRepository, storeRepository, couponsRepository, claimedCouponsRepository, addressesRepository, productRepository, } from "../../../../repositories/index.js";
16
19
  import { isIntraStateSupply } from "../../../shared/fees/calculator.js";
17
20
  import { computePreOrderDepositAmount, lineTotalFor, sumGroupGst, unitPriceFor, } from "../../../shared/checkout/order-math.js";
@@ -490,7 +493,7 @@ function dispatchOrderConfirmationEmails(emailsToSend) {
490
493
  * so the caller can accumulate the checkout-wide `total`.
491
494
  */
492
495
  async function createOrderForGroup(group, orderType, ctx) {
493
- const { paymentMethod, emiTenureMonths, emiSettings, commissions, appliedCoupons, cartSubtotal, couponUsageAccumulator, uid, userName, userEmail, shippingAddress, notes, adminBypass, adminBypassBy, adminBatchId, orderIds, emailsToSend, outOfStockPolicy, droppedItems, buyerState, gstSettings, siteContactUpiVpa, storeAddons, platformFeeByStore, } = ctx;
496
+ const { paymentMethod, emiTenureMonths, emiSettings, commissions, appliedCoupons, cartSubtotal, couponUsageAccumulator, uid, userName, userEmail, shippingAddress, notes, adminBypass, adminBypassBy, adminBatchId, orderIds, emailsToSend, outOfStockPolicy, droppedItems, buyerState, gstSettings, siteContactUpiVpa, storeAddons, platformFeeByStore, selectedShippingQuote, } = ctx;
494
497
  const firstItem = group[0].item;
495
498
  const firstProduct = group[0].product;
496
499
  // Add-ons are this store's choice, not the cart's — see OrderGroupContext.
@@ -507,7 +510,25 @@ async function createOrderForGroup(group, orderType, ctx) {
507
510
  return expandCartLineToOrderRows(item, product, ctx.productById).map((baseLine) => itemRule.decorateOrderItem(baseLine, product));
508
511
  });
509
512
  const totalQuantity = group.reduce((sum, { item }) => sum + item.quantity, 0);
510
- const { shippingFee, storeOwnerId, storeEmiEnabled, storeState, storeProtections, storeTaxIdentity } = await resolveShippingCost(firstItem.storeId);
513
+ const { shippingFee: resolvedShippingFee, storeOwnerId, storeEmiEnabled, storeState, storeProtections, storeTaxIdentity } = await resolveShippingCost(firstItem.storeId);
514
+ /*
515
+ * 🛑 The buyer's quote WINS over the seller's configured rate, and it is
516
+ * re-rounded here rather than trusted as sent.
517
+ *
518
+ * `roundShippingUp` is idempotent, so re-applying it to an honest value is a
519
+ * no-op; applied to a tampered one it forces the result back onto a ₹50 step.
520
+ * That bounds what a forged `chargedRate` can be to "some carrier rate,
521
+ * correctly rounded" rather than an arbitrary number.
522
+ *
523
+ * Applied to the FIRST group only — see the field's note on OrderGroupContext.
524
+ * A cart spanning sellers must not be charged the same parcel rate twice.
525
+ */
526
+ const isFirstGroup = ctx.orderIds.length === 0;
527
+ const shippingFee = selectedShippingQuote && isFirstGroup
528
+ ? roundShippingUp(selectedShippingQuote.chargedRate)
529
+ : selectedShippingQuote
530
+ ? 0
531
+ : resolvedShippingFee;
511
532
  /*
512
533
  * Who issues this invoice — the seller if they have their own GSTIN, the
513
534
  * platform otherwise. ONE function decides, and it is the only place that
@@ -678,6 +699,16 @@ async function createOrderForGroup(group, orderType, ctx) {
678
699
  // are "auction". Written ONCE here and never recomputed: the auction and
679
700
  // offer documents it summarises are mutable and eventually archived.
680
701
  sourceContext,
702
+ /*
703
+ * Frozen onto the order so reconciliation has a charged figure to measure
704
+ * the seller's real postage against. Only on the group that actually
705
+ * carries the shipping fee — a later group in a multi-seller cart is
706
+ * charged ₹0 shipping, so recording a quote there would invite a credit
707
+ * against money that group never collected.
708
+ */
709
+ selectedShippingQuote: selectedShippingQuote && isFirstGroup
710
+ ? { ...selectedShippingQuote, chargedRate: shippingFee }
711
+ : undefined,
681
712
  offerId: firstItem.offerId ?? undefined,
682
713
  // Distinguishes a Buy Now purchase from a settled auction win. Both are
683
714
  // orderType "auction"; only this one gets the 24h admin-review window.
@@ -1139,12 +1170,53 @@ export async function createCheckoutOrderAction(input) {
1139
1170
  siteContactUpiVpa: siteSettings?.contact?.upiVpa,
1140
1171
  storeAddons: cart.storeAddons,
1141
1172
  platformFeeByStore,
1173
+ selectedShippingQuote: input.selectedShippingQuote,
1142
1174
  productById: checkoutProductById,
1143
1175
  };
1144
1176
  for (const { items: group, orderType } of orderGroups) {
1145
1177
  total += await createOrderForGroup(group, orderType, groupCtx);
1146
1178
  }
1147
1179
  await flushCouponUsageAccumulator(couponUsageAccumulator, uid);
1180
+ /*
1181
+ * Spend points AFTER the orders exist.
1182
+ *
1183
+ * 🛑 This ordering is the whole safety argument. Redeeming first and then
1184
+ * failing to create an order would debit a buyer for something they never
1185
+ * received, and points have no refund path that does not need a human. This
1186
+ * way the worst case is an order placed at full price with the balance
1187
+ * intact — recoverable, and visible to the buyer, who still has their
1188
+ * points.
1189
+ *
1190
+ * The amount is re-derived server-side against the real total rather than
1191
+ * taken from the request; `maxRedeemablePoints` applies the 50% cap, and
1192
+ * `redeem` re-checks the balance inside its own transaction.
1193
+ */
1194
+ if (input.pointsToRedeem && input.pointsToRedeem > 0 && orderIds.length > 0) {
1195
+ try {
1196
+ const balance = await pointsRepository.getBalance(uid);
1197
+ const spend = maxRedeemablePoints(total, Math.min(balance, input.pointsToRedeem));
1198
+ if (spend > 0) {
1199
+ await pointsRepository.redeem({
1200
+ userId: uid,
1201
+ points: spend,
1202
+ orderId: orderIds[0],
1203
+ description: `Redeemed on ${orderIds[0]}`,
1204
+ });
1205
+ }
1206
+ }
1207
+ catch (err) {
1208
+ /*
1209
+ * A failed redemption must not fail a placed order. The buyer keeps the
1210
+ * points and paid full price — the strictly safer side of this trade.
1211
+ */
1212
+ const normalized = normalizeError(err);
1213
+ serverLogger.error("points redemption failed after order creation", {
1214
+ uid,
1215
+ orderId: orderIds[0],
1216
+ message: normalized.message,
1217
+ });
1218
+ }
1219
+ }
1148
1220
  dispatchOrderConfirmationEmails(emailsToSend);
1149
1221
  serverLogger.info(`createCheckoutOrderAction: ${orderIds.length} order(s) placed for uid=${uid}` +
1150
1222
  (unavailable.length > 0 ? ` (${unavailable.length} item(s) unavailable)` : ""));
@@ -1572,7 +1644,7 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
1572
1644
  // Reuses the same store/seller lookup as the COD/UPI path above instead
1573
1645
  // of re-implementing it inline — was two sequential findById calls per
1574
1646
  // seller group here, duplicated from resolveShippingCost.
1575
- const { shippingFee, storeOwnerId, storeState, storeTaxIdentity } = await resolveShippingCost(firstItem.storeId);
1647
+ const { shippingFee, storeOwnerId, storeState, storeTaxIdentity, storeProtections } = await resolveShippingCost(firstItem.storeId);
1576
1648
  /*
1577
1649
  * Same supplier-of-record rule as `createOrderForGroup`, through the same
1578
1650
  * function. This path (PhonePe verify / webhook) is the SECOND place an
@@ -1608,7 +1680,44 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
1608
1680
  const addons = storeAddons?.[firstItem.storeId] ?? {};
1609
1681
  const giftWrapFee = computeGiftWrapFee(addons.giftWrapAddon ?? false, commissionRates);
1610
1682
  const giftWrapMessage = addons.giftWrapMessage;
1611
- const shipmentProtectionFee = computeShipmentProtectionFee(groupTotal, addons.shipmentProtectionAddon ?? false, commissionRates);
1683
+ /*
1684
+ * 🛑 `computeProtectionFees`, not a bare `computeShipmentProtectionFee`.
1685
+ *
1686
+ * This path hand-rolled the shipment fee and computed NO sale-protection fee
1687
+ * at all, which was wrong twice over:
1688
+ *
1689
+ * 1. **Sale protection was never charged here.** Razorpay's create-order
1690
+ * route DOES charge it (it calls `computeProtectionFees`), so the amount
1691
+ * captured and the amount this function records on the order disagreed
1692
+ * by the fee — and the platform absorbs the unconditional refund that
1693
+ * fee exists to pay for. See `computeSaleProtectionFee`.
1694
+ * 2. **The C5e tiers were skipped.** The bare helper tests only "did the
1695
+ * buyer tick it" and the site-wide master switch, so shipment protection
1696
+ * could be billed on a seller who never agreed to back it, or on a
1697
+ * final-sale item where it cannot apply.
1698
+ *
1699
+ * Latent rather than live when written — `payment.phonepeEnabled` is false
1700
+ * and the one live store has `supportsSaleProtection` unset — but both of
1701
+ * those are a settings toggle away, and an under-charge that appears the
1702
+ * moment a flag flips is worse than one that is visible now.
1703
+ */
1704
+ const groupHasFinalSale = group.some(({ product }) => product?.finalSale === true);
1705
+ const { shippingProtectionFee: shipmentProtectionFee, saleProtectionFee } = computeProtectionFees({
1706
+ subtotal: groupTotal,
1707
+ selected: {
1708
+ shipping: addons.shipmentProtectionAddon ?? false,
1709
+ sale: addons.saleProtectionAddon ?? false,
1710
+ },
1711
+ tiers: {
1712
+ platformShipping: commissionRates.shipmentProtectionFeeEnabled === true,
1713
+ platformSale: commissionRates.saleProtectionFeeEnabled === true,
1714
+ sellerShipping: storeProtections.shipping,
1715
+ sellerSale: storeProtections.sale,
1716
+ finalSale: groupHasFinalSale,
1717
+ },
1718
+ shippingFee: (s, sel) => computeShipmentProtectionFee(s, sel, commissionRates),
1719
+ saleFee: (s, sel) => computeSaleProtectionFee(s, sel, commissionRates),
1720
+ });
1612
1721
  /*
1613
1722
  * P-8 GST — now wired through this path. C0c defect 1.
1614
1723
  *
@@ -1641,7 +1750,7 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
1641
1750
  /* Product GST is not a term — prices are tax-inclusive, so it is already
1642
1751
  * inside `groupTotal`. Matches the COD/UPI path exactly; `platformFeeGst`
1643
1752
  * stays because that is GST on our fee, charged on top. */
1644
- const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee + giftWrapFee + shipmentProtectionFee + platformFee + platformFeeGst;
1753
+ const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee + giftWrapFee + shipmentProtectionFee + saleProtectionFee + platformFee + platformFeeGst;
1645
1754
  // S-SBUNI-RULES 2026-05-13 — order-item decoration via rule registry.
1646
1755
  // Same expansion as the COD/UPI path. This was a THIRD hand-rolled copy of
1647
1756
  // the price rule (`isBundle ? item.price : product.price`) that — like the
@@ -1730,6 +1839,9 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
1730
1839
  giftWrapMessage: giftWrapFee > 0 ? giftWrapMessage?.slice(0, 500) : undefined,
1731
1840
  shipmentProtectionAddon: shipmentProtectionFee > 0 ? true : undefined,
1732
1841
  shipmentProtectionFee: shipmentProtectionFee > 0 ? shipmentProtectionFee : undefined,
1842
+ // Recorded as well as charged — an order that collects a protection fee
1843
+ // without a record of it cannot be reconciled against a later claim.
1844
+ saleProtectionFee: saleProtectionFee > 0 ? saleProtectionFee : undefined,
1733
1845
  /*
1734
1846
  * The tax breakdown, matching `createOrderForGroup` field for field.
1735
1847
  * This path recorded NONE of it before C0c defect 1 — so an online order
@@ -46,3 +46,33 @@ export declare function adminRejectPaymentAsFraudAction(orderId: string, note: s
46
46
  * payment/order status — surfaces to the admin queue for investigation.
47
47
  */
48
48
  export declare function raiseOrderDisputeAction(orderId: string, reason: string): Promise<ActionResult<void>>;
49
+ /**
50
+ * A buyer submits payment for ONE of their own EMI instalments.
51
+ *
52
+ * 🛑 Why this exists at all: there was no buyer-side route. The only writer
53
+ * was the seller's `markEmiInstallmentPaid`, so a buyer on an EMI plan could
54
+ * not pay an instalment — they waited for the seller to record it. The
55
+ * schema had anticipated them all along (`EmiInstallment.transactionId` and
56
+ * `.proofUrl` are documented as "buyer-uploaded"), which is the tell that the
57
+ * surface, not the model, was missing.
58
+ *
59
+ * 🛑 **ANY unpaid instalment, at ANY time — deliberately unbounded.** There is
60
+ * no due-date check and none should be added. A buyer who wants to clear the
61
+ * plan early is reducing our exposure, not breaking a rule, and the dueDate is
62
+ * the date we will CHASE them on, never the first date they may pay. Paying
63
+ * ahead also keeps `emiComplete` honest: that flag is what
64
+ * `assertEmiShippable` reads, so early settlement should be able to release a
65
+ * shipment early rather than waiting out a calendar.
66
+ *
67
+ * It mirrors the manual-payment model rather than inventing a second one: the
68
+ * buyer supplies a UTR and a proof, the seller or an admin confirms. The
69
+ * status therefore moves to `pending` review here, NOT to `paid` — a buyer
70
+ * must never be able to mark their own money received. `markEmiInstallmentPaid`
71
+ * stays the only writer of `paid`, and so stays the only way `emiComplete`
72
+ * can flip.
73
+ */
74
+ export declare function submitEmiInstallmentPaymentAction(orderId: string, input: {
75
+ installmentIndex: number;
76
+ transactionId: string;
77
+ proofUrl?: string;
78
+ }): Promise<ActionResult<void>>;