@mohasinac/appkit 4.62.1 → 4.63.1

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 (263) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +8 -29
  3. package/dist/_internal/client/features/layout/filterNavItems.d.ts +28 -0
  4. package/dist/_internal/client/features/layout/filterNavItems.js +35 -0
  5. package/dist/_internal/server/features/ads/booking.d.ts +118 -0
  6. package/dist/_internal/server/features/ads/booking.js +326 -0
  7. package/dist/_internal/server/features/checkout/actions.d.ts +51 -11
  8. package/dist/_internal/server/features/checkout/actions.js +335 -80
  9. package/dist/_internal/server/features/checkout/expand-order-items.d.ts +1 -2
  10. package/dist/_internal/server/features/checkout/expand-order-items.js +32 -0
  11. package/dist/_internal/server/features/checkout/freeze-parts.d.ts +50 -0
  12. package/dist/_internal/server/features/checkout/freeze-parts.js +94 -0
  13. package/dist/_internal/server/features/checkout/value-otp-gate.d.ts +55 -0
  14. package/dist/_internal/server/features/checkout/value-otp-gate.js +60 -0
  15. package/dist/_internal/server/features/dashboard/attention.d.ts +128 -0
  16. package/dist/_internal/server/features/dashboard/attention.js +183 -0
  17. package/dist/_internal/server/features/dashboard/data.d.ts +47 -0
  18. package/dist/_internal/server/features/dashboard/data.js +82 -0
  19. package/dist/_internal/server/features/inventory/reservations.d.ts +99 -0
  20. package/dist/_internal/server/features/inventory/reservations.js +273 -0
  21. package/dist/_internal/server/features/offers/destinations.d.ts +40 -0
  22. package/dist/_internal/server/features/offers/destinations.js +65 -0
  23. package/dist/_internal/server/features/orders/adapters.js +21 -0
  24. package/dist/_internal/server/features/orders/amend-delivery.d.ts +56 -0
  25. package/dist/_internal/server/features/orders/amend-delivery.js +124 -0
  26. package/dist/_internal/server/features/orders/balance.d.ts +89 -0
  27. package/dist/_internal/server/features/orders/balance.js +256 -0
  28. package/dist/_internal/server/features/pricing/actions.d.ts +58 -0
  29. package/dist/_internal/server/features/pricing/actions.js +144 -0
  30. package/dist/_internal/server/features/shipping/book-split.d.ts +86 -0
  31. package/dist/_internal/server/features/shipping/book-split.js +130 -0
  32. package/dist/_internal/server/features/shipping/book.d.ts +61 -0
  33. package/dist/_internal/server/features/shipping/book.js +176 -0
  34. package/dist/_internal/server/features/shipping/courier-block.js +26 -1
  35. package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
  36. package/dist/_internal/server/features/site-settings/adapters.js +14 -0
  37. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  38. package/dist/_internal/server/features/stores/adapters.js +14 -0
  39. package/dist/_internal/server/functions/scheduled.d.ts +2 -1
  40. package/dist/_internal/server/functions/scheduled.js +57 -1
  41. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +2 -0
  42. package/dist/_internal/server/jobs/core/jobRunners.js +45 -0
  43. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +4 -1
  44. package/dist/_internal/server/jobs/core/reservationExpiry.d.ts +17 -0
  45. package/dist/_internal/server/jobs/core/reservationExpiry.js +40 -0
  46. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +15 -0
  47. package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
  48. package/dist/_internal/server/jobs/handlers/index.js +1 -0
  49. package/dist/_internal/server/jobs/handlers/reservationExpiry.d.ts +2 -0
  50. package/dist/_internal/server/jobs/handlers/reservationExpiry.js +2 -0
  51. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  52. package/dist/_internal/shared/actions/action-registry.js +268 -0
  53. package/dist/_internal/shared/checkout/order-math.d.ts +31 -2
  54. package/dist/_internal/shared/checkout/order-math.js +22 -36
  55. package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
  56. package/dist/_internal/shared/checkout/rules/index.js +1 -1
  57. package/dist/_internal/shared/checkout/rules/payment-gate.d.ts +18 -0
  58. package/dist/_internal/shared/checkout/rules/payment-gate.js +25 -0
  59. package/dist/_internal/shared/checkout/split.d.ts +0 -12
  60. package/dist/_internal/shared/checkout/split.js +22 -2
  61. package/dist/_internal/shared/content/disclaimers.js +7 -7
  62. package/dist/_internal/shared/content/published-values.d.ts +6 -1
  63. package/dist/_internal/shared/content/published-values.js +8 -0
  64. package/dist/_internal/shared/features/admin/integrations.d.ts +32 -2
  65. package/dist/_internal/shared/features/admin/integrations.js +19 -9
  66. package/dist/_internal/shared/features/dashboard/hub-sections.d.ts +72 -0
  67. package/dist/_internal/shared/features/dashboard/hub-sections.js +75 -0
  68. package/dist/_internal/shared/features/emi/choice.d.ts +98 -0
  69. package/dist/_internal/shared/features/emi/choice.js +69 -0
  70. package/dist/_internal/shared/features/emi/group.d.ts +84 -0
  71. package/dist/_internal/shared/features/emi/group.js +126 -0
  72. package/dist/_internal/shared/features/ledger/posting.d.ts +107 -0
  73. package/dist/_internal/shared/features/ledger/posting.js +165 -0
  74. package/dist/_internal/shared/features/ledger/reports.d.ts +76 -0
  75. package/dist/_internal/shared/features/ledger/reports.js +127 -0
  76. package/dist/_internal/shared/features/ledger/rules.d.ts +170 -0
  77. package/dist/_internal/shared/features/ledger/rules.js +221 -0
  78. package/dist/_internal/shared/features/offers/coordination.d.ts +122 -0
  79. package/dist/_internal/shared/features/offers/coordination.js +80 -0
  80. package/dist/_internal/shared/features/orders/delivery-snapshot.d.ts +114 -0
  81. package/dist/_internal/shared/features/orders/delivery-snapshot.js +116 -0
  82. package/dist/_internal/shared/features/payouts/pay-period.d.ts +91 -0
  83. package/dist/_internal/shared/features/payouts/pay-period.js +104 -0
  84. package/dist/_internal/shared/features/points/accepts.d.ts +1 -8
  85. package/dist/_internal/shared/features/points/accepts.js +12 -6
  86. package/dist/_internal/shared/features/pricing/rate-registry.d.ts +150 -0
  87. package/dist/_internal/shared/features/pricing/rate-registry.js +246 -0
  88. package/dist/_internal/shared/features/shipping/parcel.d.ts +123 -0
  89. package/dist/_internal/shared/features/shipping/parcel.js +139 -0
  90. package/dist/_internal/shared/features/site-settings/fees.d.ts +16 -8
  91. package/dist/_internal/shared/features/site-settings/fees.js +12 -8
  92. package/dist/_internal/shared/fees/explain.d.ts +81 -0
  93. package/dist/_internal/shared/fees/explain.js +101 -0
  94. package/dist/_internal/shared/history/index.d.ts +1 -1
  95. package/dist/_internal/shared/history/index.js +1 -1
  96. package/dist/_internal/shared/history/types.d.ts +7 -1
  97. package/dist/_internal/shared/history/types.js +25 -0
  98. package/dist/client.d.ts +11 -1
  99. package/dist/client.js +13 -0
  100. package/dist/constants/api-endpoints.d.ts +72 -0
  101. package/dist/constants/api-endpoints.js +41 -0
  102. package/dist/constants/field-names.d.ts +41 -0
  103. package/dist/constants/field-names.js +47 -0
  104. package/dist/features/account/components/UserOrdersView.js +28 -2
  105. package/dist/features/admin/components/AdminIntegrationsView.d.ts +55 -0
  106. package/dist/features/admin/components/AdminIntegrationsView.js +74 -0
  107. package/dist/features/admin/components/AdminOrderEditorView.d.ts +16 -1
  108. package/dist/features/admin/components/AdminOrderEditorView.js +72 -3
  109. package/dist/features/admin/components/AdminOrdersView.js +20 -1
  110. package/dist/features/admin/schemas/firestore.d.ts +32 -0
  111. package/dist/features/ads/schemas/booking-write.d.ts +157 -0
  112. package/dist/features/ads/schemas/booking-write.js +88 -0
  113. package/dist/features/ads/schemas/creative-form.d.ts +18 -0
  114. package/dist/features/ads/schemas/creative-form.js +51 -0
  115. package/dist/features/ads/schemas/firestore.d.ts +140 -0
  116. package/dist/features/ads/schemas/firestore.js +109 -0
  117. package/dist/features/auctions/repository/bid.repository.js +3 -1
  118. package/dist/features/audit-log/schemas/firestore.d.ts +110 -0
  119. package/dist/features/audit-log/schemas/firestore.js +74 -0
  120. package/dist/features/auth/actions/profile-actions.d.ts +69 -1
  121. package/dist/features/auth/actions/profile-actions.js +62 -1
  122. package/dist/features/auth/schemas/firestore.d.ts +16 -2
  123. package/dist/features/auth/schemas/firestore.js +2 -1
  124. package/dist/features/blog/actions/blog-actions.d.ts +6 -0
  125. package/dist/features/blog/actions/blog-actions.js +17 -2
  126. package/dist/features/blog/api/route.js +4 -1
  127. package/dist/features/blog/columns/index.js +5 -0
  128. package/dist/features/blog/components/BlogIndexPageView.js +2 -1
  129. package/dist/features/blog/repository/blog.repository.d.ts +4 -0
  130. package/dist/features/blog/repository/blog.repository.js +5 -0
  131. package/dist/features/blog/schemas/blog-form.d.ts +3 -0
  132. package/dist/features/blog/schemas/blog-form.js +1 -0
  133. package/dist/features/blog/schemas/firestore.d.ts +38 -1
  134. package/dist/features/blog/schemas/firestore.js +23 -0
  135. package/dist/features/blog/schemas/index.d.ts +6 -0
  136. package/dist/features/blog/schemas/index.js +2 -0
  137. package/dist/features/blog/types/index.d.ts +2 -0
  138. package/dist/features/cart/components/CartPriceBreakdown.d.ts +12 -1
  139. package/dist/features/cart/components/CartPriceBreakdown.js +24 -5
  140. package/dist/features/cart/repository/cart.repository.d.ts +14 -0
  141. package/dist/features/cart/repository/cart.repository.js +20 -0
  142. package/dist/features/cart/schemas/firestore.d.ts +24 -0
  143. package/dist/features/dashboard/components/DashboardHubView.d.ts +83 -0
  144. package/dist/features/dashboard/components/DashboardHubView.js +56 -0
  145. package/dist/features/dashboard/components/DashboardSectionsClient.d.ts +23 -0
  146. package/dist/features/dashboard/components/DashboardSectionsClient.js +66 -0
  147. package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.d.ts +55 -0
  148. package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.js +175 -0
  149. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +24 -0
  150. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +28 -0
  151. package/dist/features/dispute-reasons/schemas/firestore.d.ts +3 -5
  152. package/dist/features/events/components/EventSaleBanner.js +1 -1
  153. package/dist/features/faq/components/FAQPageContent.js +2 -2
  154. package/dist/features/gateway-events/schemas/index.d.ts +2 -2
  155. package/dist/features/homepage/schemas/index.d.ts +8 -8
  156. package/dist/features/inventory/schemas/stock-reservation.d.ts +114 -0
  157. package/dist/features/inventory/schemas/stock-reservation.js +103 -0
  158. package/dist/features/layout/AppLayoutShell.d.ts +9 -1
  159. package/dist/features/layout/AppLayoutShell.js +12 -4
  160. package/dist/features/layout/BottomActions.js +2 -1
  161. package/dist/features/layout/BottomNavbar.d.ts +10 -2
  162. package/dist/features/layout/SideActionDock.js +52 -2
  163. package/dist/features/layout/TitleBarLayout.js +13 -4
  164. package/dist/features/layout/TitleBarLayout.style.css +59 -0
  165. package/dist/features/layout/useCtaReveal.d.ts +16 -1
  166. package/dist/features/layout/useCtaReveal.js +17 -26
  167. package/dist/features/ledger/repository/journal.repository.d.ts +110 -0
  168. package/dist/features/ledger/repository/journal.repository.js +312 -0
  169. package/dist/features/ledger/schemas/expenses.d.ts +87 -0
  170. package/dist/features/ledger/schemas/expenses.js +27 -0
  171. package/dist/features/ledger/schemas/firestore.d.ts +183 -0
  172. package/dist/features/ledger/schemas/firestore.js +69 -0
  173. package/dist/features/orders/components/BalancePaymentPanel.d.ts +12 -0
  174. package/dist/features/orders/components/BalancePaymentPanel.js +94 -0
  175. package/dist/features/orders/components/RaiseDisputeForm.js +19 -11
  176. package/dist/features/orders/repository/orders.repository.d.ts +28 -1
  177. package/dist/features/orders/repository/orders.repository.js +60 -6
  178. package/dist/features/orders/schemas/balance.d.ts +40 -0
  179. package/dist/features/orders/schemas/balance.js +47 -0
  180. package/dist/features/orders/schemas/firestore.d.ts +202 -0
  181. package/dist/features/orders/schemas/firestore.js +43 -0
  182. package/dist/features/orders/schemas/index.d.ts +2 -2
  183. package/dist/features/orders/types/index.d.ts +21 -0
  184. package/dist/features/payments/repository/payout.repository.js +3 -2
  185. package/dist/features/payments/schemas/firestore.d.ts +92 -1
  186. package/dist/features/payments/schemas/firestore.js +23 -0
  187. package/dist/features/pricing/components/AdminPricingView.d.ts +13 -0
  188. package/dist/features/pricing/components/AdminPricingView.js +157 -0
  189. package/dist/features/products/schemas/firestore.d.ts +26 -1
  190. package/dist/features/products/schemas/firestore.js +2 -0
  191. package/dist/features/products/schemas/index.d.ts +2 -2
  192. package/dist/features/reviews/repository/reviews.repository.d.ts +14 -0
  193. package/dist/features/reviews/repository/reviews.repository.js +16 -0
  194. package/dist/features/scams/repository/scammer.repository.js +2 -1
  195. package/dist/features/scams/schemas/firestore.d.ts +2 -1
  196. package/dist/features/scams/schemas/firestore.js +2 -1
  197. package/dist/features/seller/actions/offer-actions.js +56 -0
  198. package/dist/features/seller/actions/seller-actions.d.ts +1 -1
  199. package/dist/features/seller/actions/seller-actions.js +4 -0
  200. package/dist/features/seller/components/DispatchForm.d.ts +11 -0
  201. package/dist/features/seller/components/DispatchForm.js +190 -0
  202. package/dist/features/seller/components/SellerOrdersView.js +94 -2
  203. package/dist/features/seller/components/index.d.ts +1 -0
  204. package/dist/features/seller/components/index.js +1 -0
  205. package/dist/features/seller/repository/offer.repository.js +4 -3
  206. package/dist/features/seller/schemas/firestore.d.ts +23 -2
  207. package/dist/features/seller/schemas/firestore.js +3 -2
  208. package/dist/features/seller/schemas/index.d.ts +4 -4
  209. package/dist/features/seller/schemas/order-forms.d.ts +61 -4
  210. package/dist/features/seller/schemas/order-forms.js +41 -8
  211. package/dist/features/shipments/schemas/firestore.d.ts +25 -0
  212. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +39 -0
  213. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +55 -0
  214. package/dist/features/shipping-boxes/schemas/firestore.d.ts +2 -4
  215. package/dist/features/stores/repository/store.repository.d.ts +18 -0
  216. package/dist/features/stores/repository/store.repository.js +27 -1
  217. package/dist/features/stores/schemas/firestore.d.ts +41 -0
  218. package/dist/features/stores/schemas/firestore.js +20 -0
  219. package/dist/features/stores/schemas/index.d.ts +6 -6
  220. package/dist/features/support/repository/support.repository.js +72 -6
  221. package/dist/features/support/schemas/firestore.d.ts +17 -0
  222. package/dist/features/support/schemas/index.d.ts +2 -2
  223. package/dist/index.d.ts +5 -0
  224. package/dist/index.js +19 -0
  225. package/dist/next/routing/route-map.d.ts +92 -0
  226. package/dist/next/routing/route-map.js +53 -0
  227. package/dist/react/contexts/SessionContext.d.ts +17 -1
  228. package/dist/schemas/registry.d.ts +22 -16
  229. package/dist/security/pii-schemas.d.ts +1 -1
  230. package/dist/security/pii-schemas.js +45 -0
  231. package/dist/server-entry.d.ts +1 -1
  232. package/dist/server-entry.js +10 -1
  233. package/dist/server.d.ts +29 -1
  234. package/dist/server.js +158 -0
  235. package/dist/styles.css +349 -1
  236. package/dist/tailwind-utilities.css +1 -1
  237. package/dist/ui/components/Button.d.ts +10 -1
  238. package/dist/ui/components/Button.js +25 -3
  239. package/dist/ui/components/Chart.d.ts +63 -0
  240. package/dist/ui/components/Chart.js +169 -0
  241. package/dist/ui/components/Chart.style.css +214 -0
  242. package/dist/ui/components/Disclaimer.js +10 -1
  243. package/dist/ui/components/OverflowRow.js +54 -2
  244. package/dist/ui/components/OverflowRow.style.css +38 -0
  245. package/dist/ui/components/RichTextEditor.js +99 -8
  246. package/dist/ui/components/chart-spec.d.ts +68 -0
  247. package/dist/ui/components/chart-spec.js +140 -0
  248. package/dist/ui/components/index.style.css +1 -0
  249. package/dist/ui/components/surface-tokens.d.ts +1 -1
  250. package/dist/ui/components/surface-tokens.js +9 -0
  251. package/dist/ui/components/toolbar-collapse.d.ts +13 -0
  252. package/dist/ui/components/toolbar-collapse.js +10 -1
  253. package/dist/ui/index.d.ts +6 -0
  254. package/dist/ui/index.js +3 -0
  255. package/dist/ui/rich-text/RichText.d.ts +10 -1
  256. package/dist/ui/rich-text/RichText.js +33 -2
  257. package/dist/ui/rich-text/RichText.style.css +35 -0
  258. package/dist/ui/rich-text/RichTextRenderer.d.ts +30 -1
  259. package/dist/ui/rich-text/RichTextRenderer.js +34 -2
  260. package/dist/ui/rich-text/chart-blocks.d.ts +62 -0
  261. package/dist/ui/rich-text/chart-blocks.js +87 -0
  262. package/package.json +1 -1
  263. package/scripts/seed-cli.mjs +3 -0
@@ -126,12 +126,27 @@ export interface UserDocument extends BaseDocument {
126
126
  isVacationMode?: boolean;
127
127
  vacationMessage?: string;
128
128
  };
129
+ /**
130
+ * 🛑 LEGACY AND UNMAINTAINED — do not read these to answer "how many X does
131
+ * this account have". Nothing in either repo writes them: every occurrence
132
+ * across the codebase is a read, and the only `stats.itemsSold` write is
133
+ * `StoreRepository`'s, on the STORE document. The values present in Firestore
134
+ * today are seed literals.
135
+ *
136
+ * `getBuyerActivityStats` / `getProfileStats` count the real collections, and
137
+ * every surface that showed a number now goes through them.
138
+ *
139
+ * Kept on the schema because the documents still carry the field and a
140
+ * migration to strip it would buy nothing. `rating` was REMOVED outright: it
141
+ * had no writer AND no buyer-rating concept behind it, so its one reader
142
+ * (`/admin/users/[id]`) rendered a tile guarded on a value that could never
143
+ * be set.
144
+ */
129
145
  stats?: {
130
146
  totalOrders: number;
131
147
  auctionsWon: number;
132
148
  itemsSold: number;
133
149
  reviewsCount: number;
134
- rating?: number;
135
150
  };
136
151
  metadata?: {
137
152
  lastSignInTime?: Date;
@@ -334,7 +349,6 @@ export declare const USER_FIELDS: {
334
349
  readonly AUCTIONS_WON: "stats.auctionsWon";
335
350
  readonly ITEMS_SOLD: "stats.itemsSold";
336
351
  readonly REVIEWS_COUNT: "stats.reviewsCount";
337
- readonly RATING: "stats.rating";
338
352
  };
339
353
  readonly METADATA: "metadata";
340
354
  readonly META: {
@@ -150,7 +150,8 @@ export const USER_FIELDS = {
150
150
  AUCTIONS_WON: "stats.auctionsWon",
151
151
  ITEMS_SOLD: "stats.itemsSold",
152
152
  REVIEWS_COUNT: "stats.reviewsCount",
153
- RATING: "stats.rating",
153
+ // `RATING` removed — `stats.rating` had no writer and no buyer-rating
154
+ // concept behind it anywhere in the codebase.
154
155
  },
155
156
  METADATA: "metadata",
156
157
  META: {
@@ -9,6 +9,7 @@ export declare const createBlogPostSchema: z.ZodObject<{
9
9
  category: z.ZodEnum<["news", "tips", "guides", "updates", "community"]>;
10
10
  tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
11
11
  isFeatured: z.ZodDefault<z.ZodBoolean>;
12
+ isPinned: z.ZodDefault<z.ZodBoolean>;
12
13
  status: z.ZodDefault<z.ZodEnum<["draft", "published", "archived"]>>;
13
14
  coverImage: z.ZodOptional<z.ZodNullable<z.ZodUnion<[z.ZodObject<{
14
15
  url: z.ZodEffects<z.ZodString, string, string>;
@@ -78,6 +79,7 @@ export declare const createBlogPostSchema: z.ZodObject<{
78
79
  isFeatured: boolean;
79
80
  excerpt: string;
80
81
  readTimeMinutes: number;
82
+ isPinned: boolean;
81
83
  additionalImages: {
82
84
  type: "video" | "image" | "file";
83
85
  url: string;
@@ -124,6 +126,7 @@ export declare const createBlogPostSchema: z.ZodObject<{
124
126
  authorId?: string | undefined;
125
127
  authorName?: string | undefined;
126
128
  readTimeMinutes?: number | undefined;
129
+ isPinned?: boolean | undefined;
127
130
  additionalImages?: {
128
131
  type: "video" | "image" | "file";
129
132
  url: string;
@@ -148,6 +151,7 @@ export declare const updateBlogPostSchema: z.ZodObject<{
148
151
  category: z.ZodOptional<z.ZodEnum<["news", "tips", "guides", "updates", "community"]>>;
149
152
  tags: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString, "many">>>;
150
153
  isFeatured: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
154
+ isPinned: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
151
155
  status: z.ZodOptional<z.ZodDefault<z.ZodEnum<["draft", "published", "archived"]>>>;
152
156
  coverImage: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodUnion<[z.ZodObject<{
153
157
  url: z.ZodEffects<z.ZodString, string, string>;
@@ -229,6 +233,7 @@ export declare const updateBlogPostSchema: z.ZodObject<{
229
233
  authorId?: string | undefined;
230
234
  authorName?: string | undefined;
231
235
  readTimeMinutes?: number | undefined;
236
+ isPinned?: boolean | undefined;
232
237
  additionalImages?: {
233
238
  type: "video" | "image" | "file";
234
239
  url: string;
@@ -263,6 +268,7 @@ export declare const updateBlogPostSchema: z.ZodObject<{
263
268
  authorId?: string | undefined;
264
269
  authorName?: string | undefined;
265
270
  readTimeMinutes?: number | undefined;
271
+ isPinned?: boolean | undefined;
266
272
  additionalImages?: {
267
273
  type: "video" | "image" | "file";
268
274
  url: string;
@@ -6,6 +6,8 @@ import { blogRepository } from "../repository/blog.repository.js";
6
6
  import { coerceMediaField, getMediaUrl } from "../../../utils/index.js";
7
7
  import { hidePublicTestData } from "../../../_internal/server/features/tester/visibility.js";
8
8
  import { finalizeStagedMediaField, finalizeStagedMediaObject, finalizeStagedMediaObjectArray, } from "../../media/finalize.js";
9
+ // Defining module, not the `../schemas` barrel — Root Cause #18.
10
+ import { BLOG_DEFAULT_PUBLIC_SORT } from "../schemas/firestore.js";
9
11
  // --- Schemas --------------------------------------------------------------
10
12
  const singleImageMediaSchema = z
11
13
  .union([
@@ -33,6 +35,11 @@ export const createBlogPostSchema = z.object({
33
35
  category: z.enum(["news", "tips", "guides", "updates", "community"]),
34
36
  tags: z.array(z.string()).default([]),
35
37
  isFeatured: z.boolean().default(false),
38
+ // `.default(false)` is required, not cosmetic: `isPinned` is the first key of
39
+ // the public default sort, and Firestore excludes a document that lacks an
40
+ // `orderBy` field (Root Cause #100). A create that omitted it would write a
41
+ // post that cannot appear on its own index.
42
+ isPinned: z.boolean().default(false),
36
43
  status: z.enum(["draft", "published", "archived"]).default("draft"),
37
44
  coverImage: singleImageMediaSchema,
38
45
  contentImages: z.array(mediaFieldSchema).max(10).optional().default([]),
@@ -108,8 +115,16 @@ export async function getFeaturedBlogPosts(count = 3) {
108
115
  return hidePublicTestData(result.items);
109
116
  }
110
117
  export async function getLatestBlogPosts(count = 5) {
111
- const result = await blogRepository.listPublished({}, { sorts: sortBy("publishedAt", "DESC"), page: 1, pageSize: count });
112
- return result.items;
118
+ const result = await blogRepository.listPublished({}, { sorts: BLOG_DEFAULT_PUBLIC_SORT, page: 1, pageSize: count });
119
+ /*
120
+ * 🛑 `hidePublicTestData` was MISSING here while its sibling
121
+ * `getFeaturedBlogPosts` has always had it. This function feeds the homepage
122
+ * `blog-articles` section, so the seeded sandbox post was reaching the
123
+ * busiest public page on the site — the exact shape
124
+ * `audit-public-test-data-leak` exists to block, in the one blog path it
125
+ * does not scan.
126
+ */
127
+ return hidePublicTestData(result.items);
113
128
  }
114
129
  export async function getBlogPostById(id) {
115
130
  return blogRepository.findById(id);
@@ -9,6 +9,7 @@
9
9
  * ```
10
10
  */
11
11
  import { blogRepository } from "../repository/blog.repository.js";
12
+ import { BLOG_DEFAULT_PUBLIC_SORT } from "../schemas/firestore.js";
12
13
  import { NextResponse } from "next/server.js";
13
14
  import { getProviders } from "../../../contracts/index.js";
14
15
  import { normalizeError } from "../../../errors/normalize.js";
@@ -34,7 +35,9 @@ export async function GET(request) {
34
35
  const pageSize = url.searchParams.has("perPage")
35
36
  ? numParam(url, "perPage", 20)
36
37
  : numParam(url, "pageSize", 20);
37
- const sort = param(url, "sort") ?? "-publishedAt";
38
+ // Must be the SAME constant the SSR view uses — the view's result becomes
39
+ // this route's `initialData` under `staleTime: Infinity` (Root Cause #30).
40
+ const sort = param(url, "sort") ?? BLOG_DEFAULT_PUBLIC_SORT;
38
41
  const parts = [];
39
42
  const category = param(url, "category");
40
43
  if (category)
@@ -25,6 +25,11 @@ export const blogAdminColumns = [
25
25
  header: "Featured",
26
26
  render: (p) => (p.isFeatured ? "Yes" : "No"),
27
27
  },
28
+ {
29
+ key: "isPinned",
30
+ header: "Pinned",
31
+ render: (p) => (p.isPinned ? "Yes" : "No"),
32
+ },
28
33
  {
29
34
  key: "readTimeMinutes",
30
35
  header: "Read Time",
@@ -3,6 +3,7 @@ import { blogRepository } from "../../../repositories/index.js";
3
3
  import { Container, Heading, Main, Section } from "../../../ui/index.js";
4
4
  import { AdSlot } from "../../homepage/components/AdSlot.js";
5
5
  import { BlogIndexListing } from "./BlogIndexListing.js";
6
+ import { BLOG_DEFAULT_PUBLIC_SORT } from "../schemas/firestore.js";
6
7
  import { safeRead } from "../../../errors/safe-read.js";
7
8
  import { hidePublicTestData } from "../../../_internal/server/features/tester/visibility.js";
8
9
  function sp(params, key) {
@@ -10,7 +11,7 @@ function sp(params, key) {
10
11
  return Array.isArray(v) ? v[0] ?? "" : v ?? "";
11
12
  }
12
13
  export async function BlogIndexPageView({ searchParams = {} }) {
13
- const sort = sp(searchParams, "sort") || "-publishedAt";
14
+ const sort = sp(searchParams, "sort") || BLOG_DEFAULT_PUBLIC_SORT;
14
15
  const page = Number(sp(searchParams, "page")) || 1;
15
16
  const pageSize = Number(sp(searchParams, "pageSize")) || 24;
16
17
  const category = sp(searchParams, "category");
@@ -55,6 +55,10 @@ declare class BlogRepository extends BaseRepository<BlogPostDocument> {
55
55
  canFilter: boolean;
56
56
  canSort: boolean;
57
57
  };
58
+ isPinned: {
59
+ canFilter: boolean;
60
+ canSort: boolean;
61
+ };
58
62
  readTimeMinutes: {
59
63
  canFilter: boolean;
60
64
  canSort: boolean;
@@ -223,6 +223,11 @@ BlogRepository.SIEVE_FIELDS = {
223
223
  authorName: { canFilter: true, canSort: true },
224
224
  authorId: { canFilter: true, canSort: false },
225
225
  isFeatured: { canFilter: true, canSort: false },
226
+ // canSort because it is the FIRST key of the default sort
227
+ // `-isPinned,-publishedAt`. A `canSort: false` field is silently dropped by
228
+ // sievejs (Root Cause #63), which would leave the index ordered by date
229
+ // alone and the pin doing nothing, with no error anywhere.
230
+ isPinned: { canFilter: true, canSort: true },
226
231
  readTimeMinutes: { canFilter: true, canSort: true },
227
232
  views: { canFilter: true, canSort: true },
228
233
  publishedAt: { canFilter: true, canSort: true, parseValue: parseSieveDateValue },
@@ -47,6 +47,7 @@ export declare const blogDraftSchema: z.ZodObject<{
47
47
  publishedAt: z.ZodString;
48
48
  authorName: z.ZodString;
49
49
  isFeatured: z.ZodBoolean;
50
+ isPinned: z.ZodBoolean;
50
51
  }, "strip", z.ZodTypeAny, {
51
52
  status: "published" | "draft" | "archived";
52
53
  title: string;
@@ -60,6 +61,7 @@ export declare const blogDraftSchema: z.ZodObject<{
60
61
  excerpt: string;
61
62
  coverImage: string;
62
63
  authorName: string;
64
+ isPinned: boolean;
63
65
  metaTitle: string;
64
66
  metaDescription: string;
65
67
  }, {
@@ -75,6 +77,7 @@ export declare const blogDraftSchema: z.ZodObject<{
75
77
  excerpt: string;
76
78
  coverImage: string;
77
79
  authorName: string;
80
+ isPinned: boolean;
78
81
  metaTitle: string;
79
82
  metaDescription: string;
80
83
  }>;
@@ -97,4 +97,5 @@ export const blogDraftSchema = z.object({
97
97
  }),
98
98
  authorName: annotate(z.string(), { section: "publish", order: 3, row: "pair" }),
99
99
  isFeatured: annotate(z.boolean(), { section: "publish", order: 4, row: "quarter" }),
100
+ isPinned: annotate(z.boolean(), { section: "publish", order: 5, row: "quarter" }),
100
101
  });
@@ -29,6 +29,23 @@ export interface BlogPostDocument extends BaseDocument {
29
29
  category: BlogPostCategory;
30
30
  tags: string[];
31
31
  isFeatured: boolean;
32
+ /**
33
+ * Keeps the post at the top of the blog index and the homepage
34
+ * `blog-articles` section, above newer posts.
35
+ *
36
+ * 🛑 REQUIRED, not optional, and that is load-bearing. The default sort is
37
+ * `-isPinned,-publishedAt`, and Firestore EXCLUDES a document that lacks an
38
+ * `orderBy` field (Root Cause #100) — so an optional `isPinned` would drop
39
+ * every pre-existing post out of its own index, silently, with a 200.
40
+ * A `mapDoc` default cannot rescue it either: that is a read-side fixup and
41
+ * a query never sees it (Root Cause #101). Every row needs a real `false`
42
+ * written, which is why the seed carries it explicitly.
43
+ *
44
+ * Distinct from `isFeatured` on purpose: featured drives the badge and the
45
+ * featured-card treatment, and there are several featured posts. Overloading
46
+ * it would make "featured" and "first" the same decision.
47
+ */
48
+ isPinned: boolean;
32
49
  status: BlogPostStatus;
33
50
  publishedAt?: Date;
34
51
  authorId: string;
@@ -40,7 +57,25 @@ export interface BlogPostDocument extends BaseDocument {
40
57
  metaDescription?: string;
41
58
  }
42
59
  export declare const BLOG_POSTS_COLLECTION: "blogPosts";
43
- export declare const BLOG_POSTS_INDEXED_FIELDS: readonly ["searchTxt", "status", "publishedAt", "isFeatured", "category", "slug"];
60
+ export declare const BLOG_POSTS_INDEXED_FIELDS: readonly ["searchTxt", "status", "publishedAt", "isFeatured", "isPinned", "category", "slug"];
61
+ /**
62
+ * The default ordering for every PUBLIC blog surface — the `/blog` index, the
63
+ * `/api/blog` route that refetches it, and the homepage `blog-articles` feed.
64
+ *
65
+ * 🛑 ONE constant, three consumers, deliberately. The SSR view hands its result
66
+ * to the listing hook as `initialData`, and that hook sets `staleTime: Infinity`
67
+ * when given SSR data — so if the route's default sort differed from the view's
68
+ * by even a character, the first paint would be frozen in one order while the
69
+ * query key described another (Root Cause #30). Three literals is how that
70
+ * drift happens; one import is how it cannot.
71
+ *
72
+ * `isPinned` leads so pinned posts sit above newer ones. Needs the composite
73
+ * indexes on (status, isPinned DESC, publishedAt DESC) and its category /
74
+ * isFeatured / searchTxt variants — a multi-field sieve sort becomes two
75
+ * `orderBy` calls, and a missing index is a silent FAILED_PRECONDITION that
76
+ * every caller swallows into an empty page (Root Cause #2 / #59).
77
+ */
78
+ export declare const BLOG_DEFAULT_PUBLIC_SORT: "-isPinned,-publishedAt";
44
79
  export declare const BLOG_POST_FIELDS: {
45
80
  /** Word-prefix search tokens. Derived on write by `buildSearchTxtFor`. */
46
81
  readonly SEARCH_TXT: "searchTxt";
@@ -56,6 +91,7 @@ export declare const BLOG_POST_FIELDS: {
56
91
  readonly CATEGORY: "category";
57
92
  readonly TAGS: "tags";
58
93
  readonly IS_FEATURED: "isFeatured";
94
+ readonly IS_PINNED: "isPinned";
59
95
  readonly STATUS: "status";
60
96
  readonly PUBLISHED_AT: "publishedAt";
61
97
  readonly AUTHOR_ID: "authorId";
@@ -89,6 +125,7 @@ export type BlogPostUpdateInput = Partial<Omit<BlogPostDocument, "id" | "created
89
125
  export declare const blogPostQueryHelpers: {
90
126
  readonly published: () => readonly ["status", "==", BlogPostStatus];
91
127
  readonly featured: () => readonly ["isFeatured", "==", true];
128
+ readonly pinned: () => readonly ["isPinned", "==", true];
92
129
  readonly byCategory: (category: BlogPostCategory) => readonly ["category", "==", BlogPostCategory];
93
130
  readonly bySlug: (slug: string) => readonly ["slug", "==", string];
94
131
  readonly byAuthor: (authorId: string) => readonly ["authorId", "==", string];
@@ -14,9 +14,28 @@ export const BLOG_POSTS_INDEXED_FIELDS = [
14
14
  "status",
15
15
  "publishedAt",
16
16
  "isFeatured",
17
+ "isPinned",
17
18
  "category",
18
19
  "slug",
19
20
  ];
21
+ /**
22
+ * The default ordering for every PUBLIC blog surface — the `/blog` index, the
23
+ * `/api/blog` route that refetches it, and the homepage `blog-articles` feed.
24
+ *
25
+ * 🛑 ONE constant, three consumers, deliberately. The SSR view hands its result
26
+ * to the listing hook as `initialData`, and that hook sets `staleTime: Infinity`
27
+ * when given SSR data — so if the route's default sort differed from the view's
28
+ * by even a character, the first paint would be frozen in one order while the
29
+ * query key described another (Root Cause #30). Three literals is how that
30
+ * drift happens; one import is how it cannot.
31
+ *
32
+ * `isPinned` leads so pinned posts sit above newer ones. Needs the composite
33
+ * indexes on (status, isPinned DESC, publishedAt DESC) and its category /
34
+ * isFeatured / searchTxt variants — a multi-field sieve sort becomes two
35
+ * `orderBy` calls, and a missing index is a silent FAILED_PRECONDITION that
36
+ * every caller swallows into an empty page (Root Cause #2 / #59).
37
+ */
38
+ export const BLOG_DEFAULT_PUBLIC_SORT = "-isPinned,-publishedAt";
20
39
  export const BLOG_POST_FIELDS = {
21
40
  /** Word-prefix search tokens. Derived on write by `buildSearchTxtFor`. */
22
41
  SEARCH_TXT: "searchTxt",
@@ -32,6 +51,7 @@ export const BLOG_POST_FIELDS = {
32
51
  CATEGORY: "category",
33
52
  TAGS: "tags",
34
53
  IS_FEATURED: "isFeatured",
54
+ IS_PINNED: "isPinned",
35
55
  STATUS: "status",
36
56
  PUBLISHED_AT: "publishedAt",
37
57
  AUTHOR_ID: "authorId",
@@ -64,6 +84,7 @@ export const DEFAULT_BLOG_POST_DATA = {
64
84
  category: "news",
65
85
  tags: [],
66
86
  isFeatured: false,
87
+ isPinned: false,
67
88
  status: "draft",
68
89
  authorId: "",
69
90
  authorName: "",
@@ -80,6 +101,7 @@ export const BLOG_POST_PUBLIC_FIELDS = [
80
101
  "category",
81
102
  "tags",
82
103
  "isFeatured",
104
+ "isPinned",
83
105
  "status",
84
106
  "publishedAt",
85
107
  "authorName",
@@ -94,6 +116,7 @@ export const BLOG_POST_PUBLIC_FIELDS = [
94
116
  export const blogPostQueryHelpers = {
95
117
  published: () => ["status", "==", "published"],
96
118
  featured: () => ["isFeatured", "==", true],
119
+ pinned: () => ["isPinned", "==", true],
97
120
  byCategory: (category) => ["category", "==", category],
98
121
  bySlug: (slug) => ["slug", "==", slug],
99
122
  byAuthor: (authorId) => ["authorId", "==", authorId],
@@ -99,6 +99,7 @@ export declare const blogPostFirestoreSchema: z.ZodObject<{
99
99
  category: z.ZodString;
100
100
  tags: z.ZodArray<z.ZodString, "many">;
101
101
  isFeatured: z.ZodBoolean;
102
+ isPinned: z.ZodBoolean;
102
103
  status: z.ZodEnum<["draft", "published", "archived"]>;
103
104
  publishedAt: z.ZodOptional<z.ZodUnion<[z.ZodDate, z.ZodString, z.ZodObject<{
104
105
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
@@ -134,6 +135,7 @@ export declare const blogPostFirestoreSchema: z.ZodObject<{
134
135
  authorId: string;
135
136
  authorName: string;
136
137
  readTimeMinutes: number;
138
+ isPinned: boolean;
137
139
  publishedAt?: string | Date | z.objectOutputType<{
138
140
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
139
141
  }, z.ZodTypeAny, "passthrough"> | undefined;
@@ -187,6 +189,7 @@ export declare const blogPostFirestoreSchema: z.ZodObject<{
187
189
  authorId: string;
188
190
  authorName: string;
189
191
  readTimeMinutes: number;
192
+ isPinned: boolean;
190
193
  publishedAt?: string | Date | z.objectInputType<{
191
194
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
192
195
  }, z.ZodTypeAny, "passthrough"> | undefined;
@@ -343,6 +346,7 @@ export declare const blogPostSchema: z.ZodObject<{
343
346
  category: z.ZodEnum<["news", "tips", "guides", "updates", "community"]>;
344
347
  tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
345
348
  isFeatured: z.ZodOptional<z.ZodBoolean>;
349
+ isPinned: z.ZodOptional<z.ZodBoolean>;
346
350
  status: z.ZodEnum<["draft", "published", "archived"]>;
347
351
  publishedAt: z.ZodOptional<z.ZodString>;
348
352
  authorId: z.ZodOptional<z.ZodString>;
@@ -401,6 +405,7 @@ export declare const blogPostSchema: z.ZodObject<{
401
405
  authorId?: string | undefined;
402
406
  authorName?: string | undefined;
403
407
  readTimeMinutes?: number | undefined;
408
+ isPinned?: boolean | undefined;
404
409
  authorAvatar?: string | undefined;
405
410
  metaTitle?: string | undefined;
406
411
  metaDescription?: string | undefined;
@@ -430,6 +435,7 @@ export declare const blogPostSchema: z.ZodObject<{
430
435
  authorId?: string | undefined;
431
436
  authorName?: string | undefined;
432
437
  readTimeMinutes?: number | undefined;
438
+ isPinned?: boolean | undefined;
433
439
  additionalImages?: {
434
440
  type: "video" | "image" | "file";
435
441
  url: string;
@@ -18,6 +18,7 @@ export const blogPostFirestoreSchema = z.object({
18
18
  category: z.string(),
19
19
  tags: z.array(z.string()),
20
20
  isFeatured: z.boolean(),
21
+ isPinned: z.boolean(),
21
22
  status: blogPostStatusEnumSchema,
22
23
  publishedAt: firestoreDateSchema.optional(),
23
24
  authorId: z.string(),
@@ -72,6 +73,7 @@ export const blogPostSchema = z.object({
72
73
  category: blogPostCategorySchema,
73
74
  tags: z.array(z.string()).optional(),
74
75
  isFeatured: z.boolean().optional(),
76
+ isPinned: z.boolean().optional(),
75
77
  status: blogPostStatusSchema,
76
78
  publishedAt: z.string().optional(),
77
79
  authorId: z.string().optional(),
@@ -15,6 +15,8 @@ export interface BlogPost {
15
15
  category: BlogPostCategory;
16
16
  tags?: string[];
17
17
  isFeatured?: boolean;
18
+ /** Keeps the post above newer ones on the blog index and homepage. */
19
+ isPinned?: boolean;
18
20
  status: BlogPostStatus;
19
21
  publishedAt?: string;
20
22
  authorId?: string;
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import type { FeeDerivations } from "../../../_internal/shared/fees/explain";
2
3
  /**
3
4
  * The aggregate price breakdown shown in the cart's expandable preview and in
4
5
  * the checkout Order Summary.
@@ -86,5 +87,15 @@ export interface CartPriceBreakdownProps {
86
87
  errorNote?: React.ReactNode;
87
88
  isLoading?: boolean;
88
89
  className?: string;
90
+ /**
91
+ * Per-line "why am I paying this" disclosures, built by the
92
+ * `explain*` helpers in `_internal/shared/fees/explain`.
93
+ *
94
+ * 🛑 Optional, and an absent entry renders no disclosure rather than a
95
+ * placeholder. The cart renders this breakdown for a signed-out guest with
96
+ * no server preview at all — a line that cannot be explained must say
97
+ * nothing, never "we could not work this out", which reads as a fault.
98
+ */
99
+ derivations?: FeeDerivations;
89
100
  }
90
- export declare function CartPriceBreakdown({ preview, itemCount, currency, unavailableNote, fallbackSubtotal, errorNote, isLoading, className, }: CartPriceBreakdownProps): React.JSX.Element;
101
+ export declare function CartPriceBreakdown({ preview, itemCount, currency, unavailableNote, fallbackSubtotal, errorNote, isLoading, className, derivations, }: CartPriceBreakdownProps): React.JSX.Element;
@@ -1,15 +1,34 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { Div, Row, Stack, Text } from "../../../ui/index.js";
3
+ import { Details, Div, Row, Stack, Summary, Text } from "../../../ui/index.js";
4
4
  import { formatCurrency } from "../../../utils/number.formatter.js";
5
- function Line({ label, amount, tone = "muted", strong = false, }) {
6
- return (_jsxs(Row, { align: "center", justify: "between", gap: "sm", className: "min-w-0", children: [_jsx(Text, { size: "sm", color: tone, weight: strong ? "semibold" : undefined, className: "min-w-0", truncate: 1, children: label }), _jsx(Text, { size: "sm", color: tone, weight: strong ? "semibold" : undefined, className: "flex-shrink-0 tabular-nums", children: amount })] }));
5
+ /**
6
+ * The "how is this calculated?" disclosure under a fee line.
7
+ *
8
+ * 🛑 Collapsed by default, and that is the whole design. An unexplained charge
9
+ * is untrustworthy, but five expanded derivations turn the Order Summary into
10
+ * a wall of arithmetic at the moment a buyer is deciding to pay. The
11
+ * obligation is that the answer is *available at the point of the charge* —
12
+ * not that it is shouted.
13
+ *
14
+ * A native `<details>` rather than a tooltip or a modal: it works before
15
+ * hydration, it is keyboard-reachable for free, and it does not cover the
16
+ * number it is explaining.
17
+ */
18
+ function Derivation({ derivation }) {
19
+ return (_jsxs(Details, { tone: "muted", padding: "sm", rounded: "md", children: [_jsx(Summary, { size: "xs", color: "muted", children: "How is this calculated?" }), _jsxs(Stack, { gap: "xs", className: "pt-[var(--appkit-space-2)]", children: [_jsx(Text, { size: "xs", color: "muted", children: derivation.summary }), derivation.steps.map((step, i) => (_jsx(Text, { size: "xs", color: "faint", className: "tabular-nums", children: step }, i)))] })] }));
20
+ }
21
+ function Line({ label, amount, tone = "muted", strong = false, derivation, }) {
22
+ const row = (_jsxs(Row, { align: "center", justify: "between", gap: "sm", className: "min-w-0", children: [_jsx(Text, { size: "sm", color: tone, weight: strong ? "semibold" : undefined, className: "min-w-0", truncate: 1, children: label }), _jsx(Text, { size: "sm", color: tone, weight: strong ? "semibold" : undefined, className: "flex-shrink-0 tabular-nums", children: amount })] }));
23
+ if (!derivation)
24
+ return row;
25
+ return (_jsxs(Stack, { gap: "xs", children: [row, _jsx(Derivation, { derivation: derivation })] }));
7
26
  }
8
27
  /** "Shipping (2 stores)" — only qualifies when more than one store contributes. */
9
28
  function withStoreCount(label, count) {
10
29
  return count > 1 ? `${label} (${count} stores)` : label;
11
30
  }
12
- export function CartPriceBreakdown({ preview, itemCount, currency = "INR", unavailableNote, fallbackSubtotal = 0, errorNote, isLoading = false, className = "", }) {
31
+ export function CartPriceBreakdown({ preview, itemCount, currency = "INR", unavailableNote, fallbackSubtotal = 0, errorNote, isLoading = false, className = "", derivations, }) {
13
32
  const money = (value) => formatCurrency(value, currency);
14
33
  if (!preview) {
15
34
  return (_jsxs(Stack, { gap: "xs", className: className, children: [_jsx(Line, { label: itemCount !== undefined ? `Subtotal (${itemCount} item${itemCount !== 1 ? "s" : ""})` : "Subtotal", amount: money(fallbackSubtotal) }), (errorNote ?? unavailableNote) && (_jsx(Text, { size: "xs", color: errorNote ? "error" : "muted", children: errorNote ?? unavailableNote }))] }));
@@ -21,7 +40,7 @@ export function CartPriceBreakdown({ preview, itemCount, currency = "INR", unava
21
40
  const subtotalLabel = itemCount !== undefined
22
41
  ? `Subtotal (${itemCount} item${itemCount !== 1 ? "s" : ""}${storeCount > 1 ? `, ${storeCount} stores` : ""})`
23
42
  : "Subtotal";
24
- return (_jsxs(Stack, { gap: "xs", className: className, children: [_jsx(Line, { label: subtotalLabel, amount: money(preview.subtotal) }), preview.shippingFee > 0 && (_jsx(Line, { label: withStoreCount("Shipping", storeCount), amount: money(preview.shippingFee) })), preview.codHandlingFee > 0 && (_jsx(Line, { label: "COD handling fee", amount: money(preview.codHandlingFee) })), preview.giftWrapFee > 0 && (_jsx(Line, { label: withStoreCount("Gift wrap", countCharged((s) => s.giftWrapFee)), amount: money(preview.giftWrapFee) })), preview.shipmentProtectionFee > 0 && (_jsx(Line, { label: withStoreCount("Shipment protection", countCharged((s) => s.shipmentProtectionFee)), amount: money(preview.shipmentProtectionFee) })), preview.saleProtectionFee > 0 && (_jsx(Line, { label: withStoreCount("Sale protection", countCharged((s) => s.saleProtectionFee)), amount: money(preview.saleProtectionFee) })), preview.emiSurcharge > 0 && (_jsx(Line, { label: "EMI surcharge", amount: money(preview.emiSurcharge) })), preview.sharedPurchaseFee > 0 && (_jsx(Line, { label: withStoreCount("Split delivery", countCharged((s) => s.sharedPurchaseFee)), amount: money(preview.sharedPurchaseFee) })), preview.platformFee > 0 && _jsx(Line, { label: "Platform fee", amount: money(preview.platformFee) }), preview.gstOnFee > 0 && (_jsx(Line, { label: "GST on platform fee", amount: money(preview.gstOnFee) })), preview.couponDiscount > 0 && (_jsx(Line, { label: "Coupon discount", amount: `−${money(preview.couponDiscount)}`, tone: "success" })), _jsx(Div, { border: "top-subtle", paddingY: "t-sm", children: _jsx(Line, { label: "Total", amount: money(preview.total), tone: "primary", strong: preview.pointsApplied <= 0 }) }), preview.pointsApplied > 0 && (_jsxs(_Fragment, { children: [_jsx(Line, { label: `Points applied (${preview.pointsApplied.toLocaleString("en-IN")} pts)`, amount: `−${money(preview.pointsApplied)}`, tone: "success" }), _jsx(Div, { border: "top-subtle", paddingY: "t-sm", children: _jsx(Line, { label: "Amount due", amount: money(preview.amountDue), tone: "primary", strong: true }) })] })), preview.pointsApplied > 0 && preview.pointsBalance > preview.pointsApplied && (_jsxs(Text, { size: "xs", color: "muted", children: [(preview.pointsBalance - preview.pointsApplied).toLocaleString("en-IN"), " points stay in your balance \u2014 up to half of an order can be paid with points."] })), preview.pointsApplied === 0 && preview.pointsBalance > 0 && (_jsx(Text, { size: "xs", color: "muted", children: preview.stores.some((s) => s.acceptsPoints)
43
+ return (_jsxs(Stack, { gap: "xs", className: className, children: [_jsx(Line, { label: subtotalLabel, amount: money(preview.subtotal) }), preview.shippingFee > 0 && (_jsx(Line, { label: withStoreCount("Shipping", storeCount), amount: money(preview.shippingFee) })), preview.codHandlingFee > 0 && (_jsx(Line, { label: "COD handling fee", amount: money(preview.codHandlingFee), derivation: derivations?.codHandlingFee })), preview.giftWrapFee > 0 && (_jsx(Line, { label: withStoreCount("Gift wrap", countCharged((s) => s.giftWrapFee)), amount: money(preview.giftWrapFee) })), preview.shipmentProtectionFee > 0 && (_jsx(Line, { label: withStoreCount("Shipment protection", countCharged((s) => s.shipmentProtectionFee)), amount: money(preview.shipmentProtectionFee) })), preview.saleProtectionFee > 0 && (_jsx(Line, { label: withStoreCount("Sale protection", countCharged((s) => s.saleProtectionFee)), amount: money(preview.saleProtectionFee) })), preview.emiSurcharge > 0 && (_jsx(Line, { label: "EMI surcharge", amount: money(preview.emiSurcharge), derivation: derivations?.emiSurcharge })), preview.sharedPurchaseFee > 0 && (_jsx(Line, { label: withStoreCount("Split delivery", countCharged((s) => s.sharedPurchaseFee)), amount: money(preview.sharedPurchaseFee) })), preview.platformFee > 0 && (_jsx(Line, { label: "Platform fee", amount: money(preview.platformFee), derivation: derivations?.platformFee })), preview.gstOnFee > 0 && (_jsx(Line, { label: "GST on platform fee", amount: money(preview.gstOnFee), derivation: derivations?.gstOnFee })), preview.couponDiscount > 0 && (_jsx(Line, { label: "Coupon discount", amount: `−${money(preview.couponDiscount)}`, tone: "success" })), _jsx(Div, { border: "top-subtle", paddingY: "t-sm", children: _jsx(Line, { label: "Total", amount: money(preview.total), tone: "primary", strong: preview.pointsApplied <= 0 }) }), preview.pointsApplied > 0 && (_jsxs(_Fragment, { children: [_jsx(Line, { label: `Points applied (${preview.pointsApplied.toLocaleString("en-IN")} pts)`, amount: `−${money(preview.pointsApplied)}`, tone: "success" }), _jsx(Div, { border: "top-subtle", paddingY: "t-sm", children: _jsx(Line, { label: "Amount due", amount: money(preview.amountDue), tone: "primary", strong: true }) })] })), preview.pointsApplied > 0 && preview.pointsBalance > preview.pointsApplied && (_jsxs(Text, { size: "xs", color: "muted", children: [(preview.pointsBalance - preview.pointsApplied).toLocaleString("en-IN"), " points stay in your balance \u2014 up to half of an order can be paid with points."] })), preview.pointsApplied === 0 && preview.pointsBalance > 0 && (_jsx(Text, { size: "xs", color: "muted", children: preview.stores.some((s) => s.acceptsPoints)
25
44
  ? "Your points could not be applied to this order."
26
45
  : storeCount > 1
27
46
  ? "None of these sellers accepts points."
@@ -147,6 +147,20 @@ export declare class CartRepository extends BaseRepository<CartDocument> {
147
147
  * and keeping both shapes invites a reader that handles only one. Same
148
148
  * reasoning as `setStoreAddons` directly above.
149
149
  */
150
+ /**
151
+ * Set or clear the instalment plan.
152
+ *
153
+ * 🛑 `null` DELETES the field rather than storing `{ optIn: false }`. The
154
+ * schema's rule is that absent means "no instalments", so a second
155
+ * representation of the same state is one more thing every reader has to
156
+ * get right (Root Cause #42's shape). `tenureMonths` is stored verbatim and
157
+ * validated against the live `emi.tenureOptions` at order time — the admin
158
+ * can change those options after a cart was saved, so validating here would
159
+ * freeze a tenure that is no longer offered.
160
+ */
161
+ setEmiPlan(userId: string, plan: {
162
+ tenureMonths: number;
163
+ } | null): Promise<void>;
150
164
  setUseAllPoints(userId: string, useAllPoints: boolean): Promise<void>;
151
165
  /**
152
166
  * Cloud Functions: return refs of stale carts not updated within TTL.
@@ -608,6 +608,26 @@ export class CartRepository extends BaseRepository {
608
608
  * and keeping both shapes invites a reader that handles only one. Same
609
609
  * reasoning as `setStoreAddons` directly above.
610
610
  */
611
+ /**
612
+ * Set or clear the instalment plan.
613
+ *
614
+ * 🛑 `null` DELETES the field rather than storing `{ optIn: false }`. The
615
+ * schema's rule is that absent means "no instalments", so a second
616
+ * representation of the same state is one more thing every reader has to
617
+ * get right (Root Cause #42's shape). `tenureMonths` is stored verbatim and
618
+ * validated against the live `emi.tenureOptions` at order time — the admin
619
+ * can change those options after a cart was saved, so validating here would
620
+ * freeze a tenure that is no longer offered.
621
+ */
622
+ async setEmiPlan(userId, plan) {
623
+ await this.db
624
+ .collection(this.collection)
625
+ .doc(userId)
626
+ .set({
627
+ emiPlan: plan ? { tenureMonths: plan.tenureMonths } : FieldValue.delete(),
628
+ updatedAt: new Date(),
629
+ }, { merge: true });
630
+ }
611
631
  async setUseAllPoints(userId, useAllPoints) {
612
632
  await this.db
613
633
  .collection(this.collection)
@@ -331,6 +331,30 @@ export interface CartDocument extends BaseDocument {
331
331
  * (D46). A number was the wrong shape as well as the wrong place.
332
332
  */
333
333
  useAllPoints?: boolean;
334
+ /**
335
+ * "Pay in instalments", with the chosen tenure — an opt-in ON THE ORDER,
336
+ * orthogonal to how the token is tendered.
337
+ *
338
+ * 🛑 It lives HERE, on the cart, for exactly the reason `storeAddons` does:
339
+ * the online path creates the gateway order in one request
340
+ * (`/api/payment/create-order`, which decides the CAPTURE amount) and the
341
+ * order document in a later one (`/api/payment/verify`). A choice carried in
342
+ * a request body cannot be read by both, so the two would be free to
343
+ * disagree about how much of the total is deferred — and the difference is
344
+ * money captured that the order does not record (the D37/D38/D40 class).
345
+ *
346
+ * `useAllPoints` is the precedent and the shape is deliberately the same: a
347
+ * durable selection the server re-reads, never a client-supplied amount.
348
+ *
349
+ * Absent means a plain full-price order, which is why it is written with
350
+ * `FieldValue.delete()` rather than `{ optIn: false }` — a stored false and
351
+ * an absent field must not be two ways of saying the same thing.
352
+ */
353
+ emiPlan?: CartEmiPlan;
354
+ }
355
+ /** The buyer's instalment selection. `tenureMonths` is validated against the live `emi.tenureOptions` at order time, never here. */
356
+ export interface CartEmiPlan {
357
+ tenureMonths: number;
334
358
  }
335
359
  export declare const CART_COLLECTION: "carts";
336
360
  export declare const CART_INDEXED_FIELDS: string[];