@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
@@ -104,6 +104,36 @@ export declare function readIntegrationStates(settings: Partial<SiteSettingsDocu
104
104
  * Returned as a nested partial rather than applied here, so the caller does
105
105
  * one write and the audit log records one action.
106
106
  */
107
- export declare function integrationPatch(id: IntegrationId, enabled: boolean): Record<string, unknown>;
107
+ /**
108
+ * The settings groups an integration switch may write, and nothing else.
109
+ *
110
+ * 🛑 Deliberately a LOCAL shape rather than `Partial<SiteSettingsDocument>`.
111
+ * This module sits in `_internal/shared` and the settings schema pulls in the
112
+ * whole admin feature; importing it here to describe four booleans would add
113
+ * an edge `audit-import-cycles` exists to keep out. The four groups below are
114
+ * the entire surface these two functions write, so a local type is both
115
+ * narrower and honest.
116
+ *
117
+ * It replaces `Record<string, unknown>`, which said "some object" and
118
+ * therefore type-checked a patch that wrote a misspelled group or the wrong
119
+ * value shape — on a function whose whole job is flipping production kill
120
+ * switches.
121
+ */
122
+ export interface IntegrationPatch {
123
+ payment?: {
124
+ razorpayEnabled?: boolean;
125
+ phonepeEnabled?: boolean;
126
+ };
127
+ messaging?: {
128
+ emailEnabled: boolean;
129
+ };
130
+ shipping?: {
131
+ provider: "manual" | "shiprocket";
132
+ };
133
+ support?: {
134
+ enabled: boolean;
135
+ };
136
+ }
137
+ export declare function integrationPatch(id: IntegrationId, enabled: boolean): IntegrationPatch;
108
138
  /** Every patch for a bulk enable/disable, merged into one object. */
109
- export declare function allIntegrationsPatch(enabled: boolean): Record<string, unknown>;
139
+ export declare function allIntegrationsPatch(enabled: boolean): IntegrationPatch;
@@ -190,12 +190,6 @@ export function readIntegrationStates(settings) {
190
190
  support: { enabled: settings?.support?.enabled !== false },
191
191
  };
192
192
  }
193
- /**
194
- * Turn a desired state into the settings patch that expresses it.
195
- *
196
- * Returned as a nested partial rather than applied here, so the caller does
197
- * one write and the audit log records one action.
198
- */
199
193
  export function integrationPatch(id, enabled) {
200
194
  switch (id) {
201
195
  case "payments":
@@ -245,9 +239,25 @@ export function allIntegrationsPatch(enabled) {
245
239
  const merged = {};
246
240
  for (const id of INTEGRATION_ORDER) {
247
241
  const patch = integrationPatch(id, enabled);
248
- for (const [group, value] of Object.entries(patch)) {
249
- merged[group] = { ...merged[group], ...value };
250
- }
242
+ /*
243
+ * Merged per GROUP, so two integrations writing different fields of the
244
+ * same group (none do today, but `payment` already carries two flags)
245
+ * cannot clobber each other.
246
+ *
247
+ * Spelled out per key rather than via `Object.entries`, which erased the
248
+ * types this patch now carries — and listing the groups is also a forcing
249
+ * function: a fifth integration writing a NEW group does not silently
250
+ * vanish from the bulk switch, it fails to merge until someone adds the
251
+ * line. Same argument as the exhaustive `never` in `integrationPatch`.
252
+ */
253
+ if (patch.payment)
254
+ merged.payment = { ...merged.payment, ...patch.payment };
255
+ if (patch.messaging)
256
+ merged.messaging = { ...merged.messaging, ...patch.messaging };
257
+ if (patch.shipping)
258
+ merged.shipping = { ...merged.shipping, ...patch.shipping };
259
+ if (patch.support)
260
+ merged.support = { ...merged.support, ...patch.support };
251
261
  }
252
262
  return merged;
253
263
  }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Dashboard hub sections — one hub per nav group, derived from the nav itself.
3
+ *
4
+ * ## What a hub is
5
+ *
6
+ * Each dashboard (`/admin`, `/store`, `/user`) has a landing page listing its
7
+ * SECTIONS, and each section has a hub listing the SCREENS in it. Twelve admin
8
+ * sections, six store, four user — 22 hubs, served by three dynamic routes
9
+ * (`/{portal}/sections/[section]`), because a hub has no content of its own
10
+ * beyond the group it names.
11
+ *
12
+ * ## 🛑 Everything here is DERIVED. There is no second list.
13
+ *
14
+ * `ADMIN_NAV_GROUPS` / `STORE_NAV_GROUPS` / `USER_NAV_GROUPS` already carry a
15
+ * title per group and, per item, an href, a label, a `description` ("what this
16
+ * screen is FOR, in the user's words") and `keywords`. That is the whole card:
17
+ * a hub needs no copy of its own, and must not have any.
18
+ *
19
+ * The alternative was a hand-written array of 22 section descriptors, which is
20
+ * Root Cause #61 pre-written — ten independent enumerations of one union, nine
21
+ * of which silently did not grow. `AdminGuideHubView` is the live warning: it
22
+ * is already a hub, it is the visual pattern this follows, and it hand-writes
23
+ * its card list, so a guide added to the nav does not appear on it.
24
+ *
25
+ * ## The slug
26
+ *
27
+ * `hubSectionSlug` is the only mapping between a group title and its URL. It is
28
+ * lossy by construction ("Trust & Safety" → `trust-safety`), so two titles
29
+ * CAN collide — and a collision makes one hub permanently unreachable while
30
+ * the other answers for both, with nothing erroring.
31
+ *
32
+ * That is checked statically rather than defended at runtime:
33
+ * `audit-dashboard-hub-coverage` fails on a duplicate slug within a portal. A
34
+ * runtime throw would turn a naming choice into a 500 on a live dashboard, and
35
+ * a silent de-dupe would hide exactly the state the audit exists to name.
36
+ */
37
+ import type { SidebarNavGroup, SidebarNavItem } from "../layout/types";
38
+ /** One section of a dashboard: a nav group plus the slug its hub answers on. */
39
+ export interface DashboardHubSection {
40
+ /** URL segment — derived from `title`, never authored. */
41
+ slug: string;
42
+ /** The group's own title, rendered verbatim as the section heading. */
43
+ title: string;
44
+ /** The screens in this section, already permission-filtered by the caller. */
45
+ items: SidebarNavItem[];
46
+ }
47
+ /**
48
+ * A group title as a URL segment.
49
+ *
50
+ * Deliberately the same shape as `navItemId`'s path normalisation — lower-case,
51
+ * non-alphanumerics collapsed to a single hyphen, no leading or trailing
52
+ * hyphen. `&` and `/` carry no meaning in a slug, so "Trust & Safety" and
53
+ * "Orders & Reviews" become `trust-safety` and `orders-reviews`.
54
+ */
55
+ export declare function hubSectionSlug(title: string): string;
56
+ /**
57
+ * Every section of a dashboard, in nav order.
58
+ *
59
+ * 🛑 Pass groups that are ALREADY permission-filtered (`filterNavGroups`). This
60
+ * function deliberately applies no filter of its own: two places deciding
61
+ * visibility is how a hub ends up offering a card the sidebar hides, and the
62
+ * sidebar's answer is the one the route guards agree with.
63
+ */
64
+ export declare function hubSections(groups: readonly SidebarNavGroup[]): DashboardHubSection[];
65
+ /**
66
+ * The section a `[section]` route segment names, or `undefined`.
67
+ *
68
+ * `undefined` is the caller's cue to `notFound()`. It must not fall back to the
69
+ * first section: a mistyped or retired slug would then render a real page under
70
+ * the wrong heading, which is indistinguishable from a working link.
71
+ */
72
+ export declare function findHubSection(groups: readonly SidebarNavGroup[], slug: string): DashboardHubSection | undefined;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Dashboard hub sections — one hub per nav group, derived from the nav itself.
3
+ *
4
+ * ## What a hub is
5
+ *
6
+ * Each dashboard (`/admin`, `/store`, `/user`) has a landing page listing its
7
+ * SECTIONS, and each section has a hub listing the SCREENS in it. Twelve admin
8
+ * sections, six store, four user — 22 hubs, served by three dynamic routes
9
+ * (`/{portal}/sections/[section]`), because a hub has no content of its own
10
+ * beyond the group it names.
11
+ *
12
+ * ## 🛑 Everything here is DERIVED. There is no second list.
13
+ *
14
+ * `ADMIN_NAV_GROUPS` / `STORE_NAV_GROUPS` / `USER_NAV_GROUPS` already carry a
15
+ * title per group and, per item, an href, a label, a `description` ("what this
16
+ * screen is FOR, in the user's words") and `keywords`. That is the whole card:
17
+ * a hub needs no copy of its own, and must not have any.
18
+ *
19
+ * The alternative was a hand-written array of 22 section descriptors, which is
20
+ * Root Cause #61 pre-written — ten independent enumerations of one union, nine
21
+ * of which silently did not grow. `AdminGuideHubView` is the live warning: it
22
+ * is already a hub, it is the visual pattern this follows, and it hand-writes
23
+ * its card list, so a guide added to the nav does not appear on it.
24
+ *
25
+ * ## The slug
26
+ *
27
+ * `hubSectionSlug` is the only mapping between a group title and its URL. It is
28
+ * lossy by construction ("Trust & Safety" → `trust-safety`), so two titles
29
+ * CAN collide — and a collision makes one hub permanently unreachable while
30
+ * the other answers for both, with nothing erroring.
31
+ *
32
+ * That is checked statically rather than defended at runtime:
33
+ * `audit-dashboard-hub-coverage` fails on a duplicate slug within a portal. A
34
+ * runtime throw would turn a naming choice into a 500 on a live dashboard, and
35
+ * a silent de-dupe would hide exactly the state the audit exists to name.
36
+ */
37
+ /**
38
+ * A group title as a URL segment.
39
+ *
40
+ * Deliberately the same shape as `navItemId`'s path normalisation — lower-case,
41
+ * non-alphanumerics collapsed to a single hyphen, no leading or trailing
42
+ * hyphen. `&` and `/` carry no meaning in a slug, so "Trust & Safety" and
43
+ * "Orders & Reviews" become `trust-safety` and `orders-reviews`.
44
+ */
45
+ export function hubSectionSlug(title) {
46
+ return title
47
+ .toLowerCase()
48
+ .replace(/[^a-z0-9]+/g, "-")
49
+ .replace(/^-+|-+$/g, "");
50
+ }
51
+ /**
52
+ * Every section of a dashboard, in nav order.
53
+ *
54
+ * 🛑 Pass groups that are ALREADY permission-filtered (`filterNavGroups`). This
55
+ * function deliberately applies no filter of its own: two places deciding
56
+ * visibility is how a hub ends up offering a card the sidebar hides, and the
57
+ * sidebar's answer is the one the route guards agree with.
58
+ */
59
+ export function hubSections(groups) {
60
+ return groups.map((group) => ({
61
+ slug: hubSectionSlug(group.title),
62
+ title: group.title,
63
+ items: group.items,
64
+ }));
65
+ }
66
+ /**
67
+ * The section a `[section]` route segment names, or `undefined`.
68
+ *
69
+ * `undefined` is the caller's cue to `notFound()`. It must not fall back to the
70
+ * first section: a mistyped or retired slug would then render a real page under
71
+ * the wrong heading, which is indistinguishable from a working link.
72
+ */
73
+ export function findHubSection(groups, slug) {
74
+ return hubSections(groups).find((section) => section.slug === slug);
75
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * EMI is an OPT-IN ON THE ORDER, not a way of paying.
3
+ *
4
+ * 🛑 Until 2026-10-22 it was a sixth `paymentMethod`, and that conflated two
5
+ * independent decisions: *do I want to spread this over months* and *how does
6
+ * the money reach you*. A buyer who wanted instalments had, by picking "Pay in
7
+ * EMI", also implicitly chosen the tender — and the tender they got was the
8
+ * manual bank/UPI proof flow, because `MANUAL_PAYMENT_METHODS` is
9
+ * `["cash","upi_manual","emi"]` and `hasPaymentWindow` tested for `EMI`
10
+ * alongside the other two. Nothing in the UI said so.
11
+ *
12
+ * The two axes are now separate: `emiOptIn` + `emiTenureMonths` describe the
13
+ * arrangement, `paymentMethod` describes the tender for the token. Everything
14
+ * downstream — the schedule, the surcharge, the order fields — keys on
15
+ * `emiOptIn`, so an EMI order can be paid online or by hand.
16
+ *
17
+ * ── Why the legacy value is MAPPED rather than rejected ────────────────────
18
+ *
19
+ * A browser tab opened before this change still posts `paymentMethod: "emi"`.
20
+ * Mapping it to `{ tender: "upi_manual", emiOptIn: true }` is not a guess: it
21
+ * is exactly what that request did before, measured at
22
+ * `actions.ts`'s `hasPaymentWindow` (a 15-minute window plus a displayed UPI
23
+ * id — i.e. the manual-proof flow) and at `MANUAL_PAYMENT_METHODS`. So the
24
+ * stale tab keeps behaving identically instead of 400ing at the one moment a
25
+ * buyer is trying to pay.
26
+ *
27
+ * ── Why this returns a RESULT rather than throwing ─────────────────────────
28
+ *
29
+ * The same answer has to serve two callers with different jobs: the order
30
+ * action, which must refuse, and the pricing preview, which must still price
31
+ * *something* rather than 500 while a buyer fiddles with a radio. A plain
32
+ * value lets each decide.
33
+ */
34
+ import type { CheckoutPaymentMethod } from "../checkout/config";
35
+ /** The tender — `emi` is excluded by construction, which is the whole point. */
36
+ export type EmiTender = Exclude<CheckoutPaymentMethod, "emi">;
37
+ export interface EmiChoiceRequest {
38
+ paymentMethod: CheckoutPaymentMethod;
39
+ /**
40
+ * The DURABLE selection, read off `CartDocument.emiPlan`.
41
+ *
42
+ * 🛑 Not a request-body field, and that is the point: the online path
43
+ * decides the capture amount in one request and writes the order in
44
+ * another, so a body-carried choice cannot be read by both. See the schema
45
+ * note on `CartDocument.emiPlan`.
46
+ */
47
+ cartEmiPlan?: {
48
+ tenureMonths: number;
49
+ } | null;
50
+ /**
51
+ * Legacy only: a tab opened before 2026-10-22 posts
52
+ * `paymentMethod: "emi"` plus a tenure in the body, with nothing on the
53
+ * cart. Honoured so that buyer is not 400'd mid-payment, and ignored
54
+ * whenever the cart carries a plan.
55
+ */
56
+ legacyTenureMonths?: number;
57
+ }
58
+ export interface EmiChoice {
59
+ paymentMethod: EmiTender;
60
+ emiOptIn: boolean;
61
+ /**
62
+ * 🛑 Set ONLY when `emiOptIn` is true. A tenure left over from a radio the
63
+ * buyer un-ticked must not reach `computeBuyerEmiQuote`, or they are
64
+ * surcharged for an arrangement they declined.
65
+ */
66
+ emiTenureMonths?: number;
67
+ }
68
+ export type EmiChoiceResult = {
69
+ ok: true;
70
+ choice: EmiChoice;
71
+ } | {
72
+ ok: false;
73
+ reason: string;
74
+ };
75
+ /**
76
+ * Normalise a checkout request's payment axes. **Every** caller that reads
77
+ * `paymentMethod` or decides whether to build an EMI schedule must resolve
78
+ * through this — two copies of "is this an EMI order" is the drift that this
79
+ * function exists to end (Root Cause #75).
80
+ */
81
+ /**
82
+ * The TENDER alone — a pure function of the request, needing no cart.
83
+ *
84
+ * Split out because the early checkout guards (is COD enabled, is manual UPI
85
+ * accepted) run *before* the cart is read and must already be talking about
86
+ * the real tender. `resolveEmiChoice` calls this, so the legacy mapping has
87
+ * one definition rather than two that agree today.
88
+ */
89
+ export declare function emiTenderFor(method: CheckoutPaymentMethod): EmiTender;
90
+ export declare function resolveEmiChoice(request: EmiChoiceRequest): EmiChoiceResult;
91
+ /**
92
+ * The tenders a buyer may pick while opted in, for disabling a control rather
93
+ * than letting them pick it and be refused at submit. Derived from the same
94
+ * map the refusal reads, so the affordance and the rule cannot disagree.
95
+ */
96
+ export declare function isTenderCompatibleWithEmi(method: EmiTender): boolean;
97
+ /** Why a tender is unavailable while instalments are on. `null` when it is available. */
98
+ export declare function emiTenderBlockReason(method: EmiTender): string | null;
@@ -0,0 +1,69 @@
1
+ /** The tender a pre-2026-10-22 `paymentMethod: "emi"` request actually got. */
2
+ const EMI_LEGACY_TENDER = "upi_manual";
3
+ /**
4
+ * Tenders that cannot carry an instalment plan, with the reason stated rather
5
+ * than the combination silently dropped.
6
+ *
7
+ * - `cod` collects the balance at the door; "the balance" is precisely what an
8
+ * instalment plan is for. The two describe opposite settlements of the same
9
+ * money.
10
+ * - `admin_bypass` marks an order paid without a payment. The existing order
11
+ * writer already nulls every `emi*` field under `adminBypass`, so the
12
+ * combination has never produced an EMI order — this refuses it out loud
13
+ * instead of producing a plain order that looks like the EMI one the tester
14
+ * asked for.
15
+ */
16
+ const EMI_INCOMPATIBLE = {
17
+ cod: "Cash on delivery collects the balance when the parcel arrives, so it cannot also be paid in monthly instalments. Pay the token online or by bank/UPI transfer instead.",
18
+ admin_bypass: "The admin bypass marks an order paid without taking a payment, so there is no token to collect and no instalment plan to start.",
19
+ };
20
+ /**
21
+ * Normalise a checkout request's payment axes. **Every** caller that reads
22
+ * `paymentMethod` or decides whether to build an EMI schedule must resolve
23
+ * through this — two copies of "is this an EMI order" is the drift that this
24
+ * function exists to end (Root Cause #75).
25
+ */
26
+ /**
27
+ * The TENDER alone — a pure function of the request, needing no cart.
28
+ *
29
+ * Split out because the early checkout guards (is COD enabled, is manual UPI
30
+ * accepted) run *before* the cart is read and must already be talking about
31
+ * the real tender. `resolveEmiChoice` calls this, so the legacy mapping has
32
+ * one definition rather than two that agree today.
33
+ */
34
+ export function emiTenderFor(method) {
35
+ return method === "emi" ? EMI_LEGACY_TENDER : method;
36
+ }
37
+ export function resolveEmiChoice(request) {
38
+ const legacy = request.paymentMethod === "emi";
39
+ const paymentMethod = emiTenderFor(request.paymentMethod);
40
+ /*
41
+ * The cart WINS. A stale tab's body tenure is a fallback for the one case
42
+ * where there is nothing on the cart to read — never an override, or a
43
+ * client could re-price an order the server has already quoted.
44
+ */
45
+ const tenureMonths = request.cartEmiPlan?.tenureMonths ?? (legacy ? request.legacyTenureMonths : undefined);
46
+ const emiOptIn = tenureMonths !== undefined;
47
+ if (!emiOptIn) {
48
+ return { ok: true, choice: { paymentMethod, emiOptIn: false } };
49
+ }
50
+ const incompatible = EMI_INCOMPATIBLE[paymentMethod];
51
+ if (incompatible)
52
+ return { ok: false, reason: incompatible };
53
+ return {
54
+ ok: true,
55
+ choice: { paymentMethod, emiOptIn: true, emiTenureMonths: tenureMonths },
56
+ };
57
+ }
58
+ /**
59
+ * The tenders a buyer may pick while opted in, for disabling a control rather
60
+ * than letting them pick it and be refused at submit. Derived from the same
61
+ * map the refusal reads, so the affordance and the rule cannot disagree.
62
+ */
63
+ export function isTenderCompatibleWithEmi(method) {
64
+ return EMI_INCOMPATIBLE[method] === undefined;
65
+ }
66
+ /** Why a tender is unavailable while instalments are on. `null` when it is available. */
67
+ export function emiTenderBlockReason(method) {
68
+ return EMI_INCOMPATIBLE[method] ?? null;
69
+ }
@@ -0,0 +1,84 @@
1
+ import { type BuyerEmiQuote, type EmiScheduleResult, type EmiSettings } from "./schedule";
2
+ /** What every caller knows about one seller-group at the moment EMI is priced. */
3
+ export interface GroupEmiInput {
4
+ /** Already normalised by `resolveEmiChoice` — never derived from a tender. */
5
+ emiOptIn: boolean;
6
+ emiTenureMonths?: number;
7
+ /** The group's GOODS subtotal, before coupon and before any fee. */
8
+ groupSubtotal: number;
9
+ /** `StoreDocument.emiEnabled` for this group's seller. */
10
+ sellerEmiEnabled: boolean;
11
+ }
12
+ /**
13
+ * The gate, shared so "does EMI apply to this group" is answered identically
14
+ * everywhere.
15
+ *
16
+ * 🛑 Returns a REASON rather than a boolean, because the two callers do
17
+ * opposite things with it: order creation must refuse (an ineligible EMI order
18
+ * that silently becomes a full-price order is money the buyer did not agree
19
+ * to), while the preview must carry on and simply price no surcharge (a
20
+ * preview that throws blanks the Order Summary while the buyer is still
21
+ * choosing). A boolean would have forced each to re-derive the reason.
22
+ */
23
+ export type GroupEmiGate = {
24
+ applies: true;
25
+ } | {
26
+ applies: false;
27
+ reason: "not_opted_in" | "no_tenure" | "no_settings" | string;
28
+ };
29
+ export declare function groupEmiGate(input: GroupEmiInput, settings: Pick<EmiSettings, "enabled" | "minOrderValue"> | null | undefined): GroupEmiGate;
30
+ /**
31
+ * The buyer-visible quote for one group, or `null` when EMI does not apply.
32
+ *
33
+ * For the preview and for the gateway CAPTURE amount — neither needs the
34
+ * platform/seller surcharge split, and neither should be able to print it.
35
+ */
36
+ export declare function groupEmiQuote(input: GroupEmiInput, settings: (Pick<EmiSettings, "enabled" | "minOrderValue" | "tokenPercent" | "billingDay" | "surchargePercentPerMonth">) | null | undefined, purchaseDate?: Date): BuyerEmiQuote | null;
37
+ /**
38
+ * The server-side schedule for one group, including the surcharge split that
39
+ * is persisted onto the order. For the two order WRITERS only.
40
+ */
41
+ export declare function groupEmiSchedule(input: GroupEmiInput, settings: EmiSettings | null | undefined, purchaseDate?: Date): EmiScheduleResult | null;
42
+ /**
43
+ * What is actually collected at checkout: the order total, less any points
44
+ * tendered, less whatever has been deferred into instalments.
45
+ *
46
+ * 🛑 Composes with the points tender rather than replacing it. Both reduce
47
+ * what is collected and they are independent, so an EMI order part-funded by
48
+ * points must subtract both — writing `total − remainingBalance` would hand
49
+ * back the points discount, and `total − pointsApplied` alone is the bug this
50
+ * function exists to fix. Floored at zero: points can cover at most half an
51
+ * order, so the sum cannot genuinely go negative, and clamping means a future
52
+ * change to that cap degrades to "collect nothing" rather than to a refund.
53
+ */
54
+ export declare function amountDueNow(args: {
55
+ orderTotal: number;
56
+ pointsApplied: number;
57
+ /** `EmiScheduleResult.remainingBalance`, or 0 on a non-EMI order. */
58
+ emiRemainingBalance: number;
59
+ }): number;
60
+ /**
61
+ * The `emi*` fields written onto an order document.
62
+ *
63
+ * 🛑 One builder for both creation paths. These are eleven fields that have to
64
+ * be written identically by `createOrderForGroup` and
65
+ * `createPhonePeGroupOrder`, and the second one carried **none of them** —
66
+ * every order paid online lost its instalment plan entirely. Two hand-written
67
+ * copies is the create-vs-create axis of Root Cause #39, and these two
68
+ * functions have already drifted once before over add-on fees (#65).
69
+ */
70
+ export declare function emiOrderFields(schedule: EmiScheduleResult | null, emiTenureMonths: number | undefined): {
71
+ emiEnabled?: true;
72
+ emiTenureMonths?: number;
73
+ emiTokenAmount?: number;
74
+ emiSurchargeAmount?: number;
75
+ emiSurchargeSellerShare?: number;
76
+ emiInstallments?: Array<{
77
+ index: number;
78
+ dueDate: Date;
79
+ amount: number;
80
+ status: "pending";
81
+ }>;
82
+ emiRemainingBalance?: number;
83
+ emiComplete?: false;
84
+ };
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Per-order-group EMI terms, and the one definition of what is COLLECTED NOW
3
+ * on an instalment order.
4
+ *
5
+ * ── Why this file exists ──────────────────────────────────────────────────
6
+ *
7
+ * Four money paths have to agree about an EMI order: the pricing preview (what
8
+ * the buyer is shown), `createOrderForGroup` (COD/manual), the gateway capture
9
+ * in `/api/payment/create-order`, and `createPhonePeGroupOrder` (the order
10
+ * written after that capture). Three of them had no EMI code at all, and the
11
+ * fourth computed its schedule inline — so adding the online tender by copying
12
+ * that block would have produced the exact shape Root Cause #75 is about: a
13
+ * rule reachable by copy-paste, copied, and the copy never learning about the
14
+ * branch added later. `lineTotalFor` was the last instance; this is the same
15
+ * fix applied before the divergence rather than after it.
16
+ *
17
+ * ── 🛑 THE DEFECT THIS CLOSES, which is larger than the refactor ──────────
18
+ *
19
+ * `emiTokenAmount` was written onto every EMI order and had **ZERO readers**,
20
+ * measured across `src` and `appkit/src`. The buyer's manual-payment page asks
21
+ * for `order.amountDue ?? order.total`, and `amountDue` was set only when
22
+ * points applied — so an EMI buyer was asked to transfer the **entire order
23
+ * total up front** *and* handed a schedule of monthly instalments. The token
24
+ * percentage was computed, stored, scheduled around, and never charged.
25
+ *
26
+ * `amountDueNow` below is that fix, and it is deliberately a SUBTRACTION
27
+ * rather than a re-derivation:
28
+ *
29
+ * orderTotal − remainingBalance
30
+ * = (subtotal + surcharge + fees) − (principalRemaining + surcharge)
31
+ * = (subtotal − principalRemaining) + fees
32
+ * = token + fees
33
+ *
34
+ * Provably the token plus every additive charge, with no second arithmetic to
35
+ * drift from `computeBuyerEmiQuote`'s own. A function that recomputed
36
+ * `tokenPercent × subtotal + …` would be a fifth place rounding money.
37
+ */
38
+ import { roundRupees } from "../../../../utils/number.formatter.js";
39
+ import { checkEmiEligibility, computeBuyerEmiQuote, computeEmiSchedule, } from "./schedule.js";
40
+ export function groupEmiGate(input, settings) {
41
+ if (!input.emiOptIn)
42
+ return { applies: false, reason: "not_opted_in" };
43
+ if (!input.emiTenureMonths)
44
+ return { applies: false, reason: "no_tenure" };
45
+ if (!settings)
46
+ return { applies: false, reason: "no_settings" };
47
+ const eligibility = checkEmiEligibility(input.groupSubtotal, input.sellerEmiEnabled, settings);
48
+ if (!eligibility.eligible)
49
+ return { applies: false, reason: eligibility.reason };
50
+ return { applies: true };
51
+ }
52
+ /**
53
+ * The buyer-visible quote for one group, or `null` when EMI does not apply.
54
+ *
55
+ * For the preview and for the gateway CAPTURE amount — neither needs the
56
+ * platform/seller surcharge split, and neither should be able to print it.
57
+ */
58
+ export function groupEmiQuote(input, settings, purchaseDate) {
59
+ if (!settings)
60
+ return null;
61
+ if (!groupEmiGate(input, settings).applies)
62
+ return null;
63
+ return computeBuyerEmiQuote(input.groupSubtotal, input.emiTenureMonths, settings, purchaseDate);
64
+ }
65
+ /**
66
+ * The server-side schedule for one group, including the surcharge split that
67
+ * is persisted onto the order. For the two order WRITERS only.
68
+ */
69
+ export function groupEmiSchedule(input, settings, purchaseDate) {
70
+ if (!settings)
71
+ return null;
72
+ if (!groupEmiGate(input, settings).applies)
73
+ return null;
74
+ return computeEmiSchedule(input.groupSubtotal, input.emiTenureMonths, settings, purchaseDate);
75
+ }
76
+ /**
77
+ * What is actually collected at checkout: the order total, less any points
78
+ * tendered, less whatever has been deferred into instalments.
79
+ *
80
+ * 🛑 Composes with the points tender rather than replacing it. Both reduce
81
+ * what is collected and they are independent, so an EMI order part-funded by
82
+ * points must subtract both — writing `total − remainingBalance` would hand
83
+ * back the points discount, and `total − pointsApplied` alone is the bug this
84
+ * function exists to fix. Floored at zero: points can cover at most half an
85
+ * order, so the sum cannot genuinely go negative, and clamping means a future
86
+ * change to that cap degrades to "collect nothing" rather than to a refund.
87
+ */
88
+ export function amountDueNow(args) {
89
+ return roundRupees(Math.max(0, args.orderTotal - args.pointsApplied - args.emiRemainingBalance));
90
+ }
91
+ /**
92
+ * The `emi*` fields written onto an order document.
93
+ *
94
+ * 🛑 One builder for both creation paths. These are eleven fields that have to
95
+ * be written identically by `createOrderForGroup` and
96
+ * `createPhonePeGroupOrder`, and the second one carried **none of them** —
97
+ * every order paid online lost its instalment plan entirely. Two hand-written
98
+ * copies is the create-vs-create axis of Root Cause #39, and these two
99
+ * functions have already drifted once before over add-on fees (#65).
100
+ */
101
+ export function emiOrderFields(schedule, emiTenureMonths) {
102
+ if (!schedule)
103
+ return {};
104
+ return {
105
+ emiEnabled: true,
106
+ emiTenureMonths,
107
+ emiTokenAmount: schedule.tokenAmount,
108
+ emiSurchargeAmount: schedule.surchargeAmount,
109
+ /*
110
+ * Persisted even though it moves no money at checkout: it is the
111
+ * platform/seller split of the surcharge, and the payout run needs it to
112
+ * credit the seller their share. It was discarded on every EMI order until
113
+ * it was added here. 🛑 It is `PRIVATE_*_FIELDS` — it belongs on the
114
+ * document and must never reach a buyer-facing projection.
115
+ */
116
+ emiSurchargeSellerShare: schedule.surchargeSellerShare,
117
+ emiInstallments: schedule.installments.map((i) => ({
118
+ index: i.index,
119
+ dueDate: i.dueDate,
120
+ amount: i.amount,
121
+ status: "pending",
122
+ })),
123
+ emiRemainingBalance: schedule.remainingBalance,
124
+ emiComplete: false,
125
+ };
126
+ }