@mohasinac/appkit 4.62.1 → 4.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) 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 +120 -0
  112. package/dist/features/ads/schemas/booking-write.js +75 -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.d.ts +27 -1
  232. package/dist/server.js +144 -0
  233. package/dist/styles.css +349 -1
  234. package/dist/tailwind-utilities.css +1 -1
  235. package/dist/ui/components/Button.d.ts +10 -1
  236. package/dist/ui/components/Button.js +25 -3
  237. package/dist/ui/components/Chart.d.ts +63 -0
  238. package/dist/ui/components/Chart.js +169 -0
  239. package/dist/ui/components/Chart.style.css +214 -0
  240. package/dist/ui/components/Disclaimer.js +10 -1
  241. package/dist/ui/components/OverflowRow.js +54 -2
  242. package/dist/ui/components/OverflowRow.style.css +38 -0
  243. package/dist/ui/components/RichTextEditor.js +99 -8
  244. package/dist/ui/components/chart-spec.d.ts +68 -0
  245. package/dist/ui/components/chart-spec.js +140 -0
  246. package/dist/ui/components/index.style.css +1 -0
  247. package/dist/ui/components/surface-tokens.d.ts +1 -1
  248. package/dist/ui/components/surface-tokens.js +9 -0
  249. package/dist/ui/components/toolbar-collapse.d.ts +13 -0
  250. package/dist/ui/components/toolbar-collapse.js +10 -1
  251. package/dist/ui/index.d.ts +6 -0
  252. package/dist/ui/index.js +3 -0
  253. package/dist/ui/rich-text/RichText.d.ts +10 -1
  254. package/dist/ui/rich-text/RichText.js +33 -2
  255. package/dist/ui/rich-text/RichText.style.css +35 -0
  256. package/dist/ui/rich-text/RichTextRenderer.d.ts +30 -1
  257. package/dist/ui/rich-text/RichTextRenderer.js +34 -2
  258. package/dist/ui/rich-text/chart-blocks.d.ts +62 -0
  259. package/dist/ui/rich-text/chart-blocks.js +87 -0
  260. package/package.json +1 -1
  261. package/scripts/seed-cli.mjs +3 -0
@@ -0,0 +1,12 @@
1
+ import type { Order } from "../types";
2
+ export interface BalancePaymentPanelProps {
3
+ order: Pick<Order, "id" | "balance" | "currency">;
4
+ /** Uploads one file and resolves to the stored `/media/…` slug. */
5
+ onUpload: (file: File) => Promise<string>;
6
+ /** POSTs to `/api/orders/{id}/balance-proof`. Throws on failure. */
7
+ onSubmit: (input: {
8
+ proofUrl: string;
9
+ transactionId: string;
10
+ }) => Promise<void>;
11
+ }
12
+ export declare function BalancePaymentPanel({ order, onUpload, onSubmit }: BalancePaymentPanelProps): import("react").JSX.Element | null;
@@ -0,0 +1,94 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * D55 — the buyer's panel for paying a pre-order balance.
5
+ *
6
+ * ## 🛑 This exists because the figure was already on screen
7
+ *
8
+ * `OrderProvenanceCard` has printed "Balance due ₹X" on every pre-order since
9
+ * Part 2, and nothing in the product could accept that money. A buyer who
10
+ * reads an amount owed and finds no control does not conclude "the feature is
11
+ * coming" — they conclude the site is broken, and the seller waiting on the
12
+ * money has no way to chase it.
13
+ *
14
+ * ## Why it is NOT a second page
15
+ *
16
+ * The deposit's proof upload is its own route (`/user/orders/[id]/payment`)
17
+ * because it runs against a 15-minute window with a countdown, a UPI address
18
+ * to copy and a fraud declaration — a task with its own screen. The balance
19
+ * has a 7-day window and no countdown, so sending the buyer somewhere else to
20
+ * fill in two fields would add a navigation and a back button for nothing.
21
+ *
22
+ * ## The four states
23
+ *
24
+ * | `balance.status` | What the buyer sees |
25
+ * |---|---|
26
+ * | absent | NOTHING — the seller has not opened collection; the goods do not exist yet |
27
+ * | `awaiting` / `rejected` | the form, with the rejection note when there is one |
28
+ * | `submitted` | "under review", no form |
29
+ * | `paid` | a confirmation line and the reference |
30
+ *
31
+ * 🛑 The absent case rendering nothing is the load-bearing one. A pre-order's
32
+ * premise is that the item has not been made yet, so a payable-looking balance
33
+ * before the seller opens it would be asking the buyer to fund inventory.
34
+ */
35
+ import { useState } from "react";
36
+ import { Button } from "../../../ui/components/Button.js";
37
+ import { Row, Stack } from "../../../ui/components/Layout.js";
38
+ import { Div } from "../../../ui/components/Div.js";
39
+ import { Text, Span } from "../../../ui/components/Typography.js";
40
+ import { Form } from "../../../ui/components/Form.js";
41
+ import { FieldInput } from "../../../ui/forms/FieldInput.js";
42
+ import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary.js";
43
+ import { MediaUploadField } from "../../media/upload/MediaUploadField.js";
44
+ import { balanceProofSchema } from "../schemas/balance.js";
45
+ import { formatCurrency } from "../../../utils/number.formatter.js";
46
+ function dueText(dueBy) {
47
+ if (!dueBy)
48
+ return "";
49
+ const d = new Date(dueBy);
50
+ if (Number.isNaN(d.getTime()))
51
+ return "";
52
+ return ` Due by ${d.toLocaleDateString("en-IN", { day: "numeric", month: "short", year: "numeric" })}.`;
53
+ }
54
+ export function BalancePaymentPanel({ order, onUpload, onSubmit }) {
55
+ const [proofUrl, setProofUrl] = useState("");
56
+ const [transactionId, setTransactionId] = useState("");
57
+ const [isPending, setIsPending] = useState(false);
58
+ const balance = order.balance;
59
+ // Nothing to collect yet — see the header. Not an empty card, no card.
60
+ if (!balance)
61
+ return null;
62
+ const amount = formatCurrency(balance.amount, order.currency);
63
+ if (balance.status === "paid") {
64
+ return (_jsxs(Div, { surface: "card", padding: "md", className: "mt-4", children: [_jsx(Span, { layout: "inline-flex", color: "success", surface: "success-surface", weight: "semibold", className: "text-[10px]", rounded: "full", padding: "pill-xs", children: "Balance paid" }), _jsxs(Text, { className: "mt-1", color: "muted", size: "xs", children: [amount, " received. This order is ready to ship.", balance.transactionId ? ` Reference: ${balance.transactionId}` : ""] })] }));
65
+ }
66
+ if (balance.status === "submitted") {
67
+ return (_jsxs(Div, { surface: "card", padding: "md", className: "mt-4", children: [_jsx(Span, { layout: "inline-flex", color: "info", surface: "info-surface", weight: "semibold", className: "text-[10px]", rounded: "full", padding: "pill-xs", children: "Balance under review" }), _jsxs(Text, { className: "mt-1", color: "muted", size: "xs", children: ["We have your proof for ", amount, ". Our team checks it against the reference you gave, and the order ships once it clears."] })] }));
68
+ }
69
+ const rejected = balance.status === "rejected";
70
+ const handleSubmit = async () => {
71
+ setIsPending(true);
72
+ try {
73
+ await onSubmit({ proofUrl, transactionId: transactionId.trim() });
74
+ }
75
+ finally {
76
+ /*
77
+ * Cleared even on failure. The caller surfaces the error; leaving the
78
+ * button spinning forever is how a recoverable 400 reads as a hang.
79
+ */
80
+ setIsPending(false);
81
+ }
82
+ };
83
+ return (_jsx(Div, { surface: "card", padding: "md", className: "mt-4", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsx(Span, { layout: "inline-flex", color: rejected ? "error" : "warning", surface: rejected ? "danger-surface" : "warning-surface", weight: "semibold", className: "text-[10px]", rounded: "full", padding: "pill-xs", children: rejected ? "Balance proof needs correction" : "Balance due" }), _jsx(Text, { className: "mt-1", color: "muted", size: "xs", children: rejected
84
+ ? /*
85
+ * The note is REQUIRED on a rejection server-side, so the
86
+ * fallback here should effectively never render — it exists
87
+ * because a row written before that rule would otherwise say
88
+ * nothing at all, which is the one thing a rejection must not do.
89
+ */
90
+ balance.reviewNote
91
+ ? `Our team could not accept the last proof: ${balance.reviewNote}`
92
+ : "Our team could not accept the last proof. Please send a clearer screenshot."
93
+ : `Your item is ready. Pay the remaining ${amount} and upload the receipt.${dueText(balance.dueBy)}` })] }), _jsx(Form, { schema: balanceProofSchema, onSubmit: (e) => e.preventDefault(), children: () => (_jsxs(Stack, { gap: "md", children: [_jsx(FormErrorSummary, {}), _jsx(MediaUploadField, { label: "Payment screenshot (JPG/PNG/PDF)", value: proofUrl, onChange: setProofUrl, onUpload: onUpload, kind: "image", accept: "image/*,application/pdf", showYoutube: false, showExternal: false, helperText: "The confirmation screen from your bank or UPI app." }), _jsx(FieldInput, { name: "transactionId", label: "UTR / Transaction reference", required: true, value: transactionId, onChange: (value) => setTransactionId(value), placeholder: "e.g. 123456789012", hint: "Required \u2014 it is the only thing our team can check the payment against. A screenshot on its own is a picture." }), _jsx(Row, { gap: "3", children: _jsx(Button, { type: "button", variant: "primary", disabled: isPending || !proofUrl || transactionId.trim().length < 4, onClick: handleSubmit, paddingX: "md", paddingY: "sm", textSize: "sm", weight: "semibold", children: isPending ? "Sending…" : rejected ? "Re-send proof" : "Submit payment proof" }) })] })) })] }) }));
94
+ }
@@ -28,6 +28,12 @@ import { Alert, Button, Div, FieldTextarea, Form, Stack, Text, useToast } from "
28
28
  import { PaginatedSelect } from "../../../ui/components/PaginatedSelect.js";
29
29
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
30
30
  import { normalizeError } from "../../../errors/normalize.js";
31
+ /*
32
+ * 🛑 From the DEFINING module, not the `@mohasinac/appkit/client` barrel. This
33
+ * is a `"use client"` component so either resolves, but a barrel import drags
34
+ * the whole client entry's graph in for one symbol (Root Cause #18).
35
+ */
36
+ import { apiClient } from "../../../http/ApiClient.js";
31
37
  export function RaiseDisputeForm({ order, reasonsEndpoint, onSubmit, onDone, }) {
32
38
  const { showToast } = useToast();
33
39
  const [kind, setKind] = useState(null);
@@ -55,17 +61,19 @@ export function RaiseDisputeForm({ order, reasonsEndpoint, onSubmit, onDone, })
55
61
  const loadReasons = useCallback(async (query, page) => {
56
62
  if (!kind)
57
63
  return { items: [], hasMore: false };
58
- const res = await fetch(`${reasonsEndpoint}?kind=${encodeURIComponent(kind)}`);
59
- if (!res.ok) {
60
- /*
61
- * Surfaced, not swallowed into an empty list. An empty picker is
62
- * indistinguishable from "there are no reasons", and the buyer would
63
- * sit on a required field with nothing in it (Root Cause #59).
64
- */
65
- throw new Error("Could not load the list of reasons. Please try again.");
66
- }
67
- const json = (await res.json());
68
- const rows = json.data?.items ?? [];
64
+ /*
65
+ * `apiClient`, not a bare `fetch` — it carries the credentials and the
66
+ * error shape every other call in this feature uses, and the bare form
67
+ * is what `audit-form-mutation-hook` blocks inside a <Form>-rendering
68
+ * file. Same shape as `YourPurchaseStrip`'s read, one folder over.
69
+ *
70
+ * It THROWS on a non-2xx, which is the behaviour this call site already
71
+ * wanted: surfaced, not swallowed into an empty list. An empty picker is
72
+ * indistinguishable from "there are no reasons", and the buyer would sit
73
+ * on a required field with nothing in it (Root Cause #59).
74
+ */
75
+ const json = await apiClient.get(`${reasonsEndpoint}?kind=${encodeURIComponent(kind)}`);
76
+ const rows = json?.data?.items ?? [];
69
77
  const q = query.trim().toLowerCase();
70
78
  const filtered = q ? rows.filter((r) => r.label.toLowerCase().includes(q)) : rows;
71
79
  return {
@@ -5,6 +5,7 @@ import { BaseRepository, parseSieveDateValue } from "../../../providers/db-fireb
5
5
  import { type OrderCreateInput, type OrderDocument, type OrderPlain, type OrderRefundEvent } from "../schemas";
6
6
  import { type HistoryActor, type FieldChange } from "../../../_internal/shared/history/index";
7
7
  import type { OrderStatus, PaymentStatus } from "../types";
8
+ import type { OrderType } from "../utils/order-splitter";
8
9
  import { type PaymentReviewQueueMode } from "../constants/payment-window";
9
10
  /**
10
11
  * Who/why for a write that lands in `statusHistory`.
@@ -64,7 +65,7 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
64
65
  * encryption lived in a private method only three call sites remembered to
65
66
  * call.
66
67
  */
67
- protected piiFields: readonly ["userName", "userEmail", "sellerEmail"];
68
+ protected piiFields: readonly ["userName", "userEmail", "sellerEmail", "deliveryFullName", "deliveryPhone", "deliveryAddressLine1", "deliveryAddressLine2", "deliveryLandmark", "deliveryAmendments[].fullName", "deliveryAmendments[].phone", "deliveryAmendments[].addressLine1", "deliveryAmendments[].addressLine2", "deliveryAmendments[].landmark"];
68
69
  /** Plaintext PII. **Opt-in and named** — shows up in review and in a grep. */
69
70
  revealOrder(doc: OrderDocument): OrderPlain;
70
71
  /** As `revealOrder`, for a list read. */
@@ -144,6 +145,20 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
144
145
  * `order.bundleId` at creation time (SB3 / SB6-C).
145
146
  */
146
147
  countByUserAndBundle(userId: string, bundleId: string): Promise<number>;
148
+ /**
149
+ * How many orders this buyer has placed, optionally scoped to one lane.
150
+ *
151
+ * A Firestore aggregation — billed at one read per 1000 index entries
152
+ * matched, not one per order — so it stays flat as an account's history
153
+ * grows. That matters because the only caller is the session profile
154
+ * endpoint, which every page load hits.
155
+ *
156
+ * 🛑 `orderType` may only name a NON-standard lane. The field was added
157
+ * after orders already existed, so an equality on `"standard"` would return
158
+ * zero for every legacy order while looking perfectly correct — see the
159
+ * constant's own note.
160
+ */
161
+ countForUser(userId: string, orderType?: Exclude<OrderType, "standard">): Promise<number>;
147
162
  deleteByUser(userId: string): Promise<number>;
148
163
  static readonly SELLER_SIEVE_FIELDS: {
149
164
  id: {
@@ -564,6 +579,18 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
564
579
  */
565
580
  getEligibleAutomatic(cutoff: Date, opts?: {
566
581
  onWarn?: (msg: string, meta?: Record<string, JsonValue>) => void;
582
+ /**
583
+ * Stores paid at SHIPPING rather than at delivery.
584
+ *
585
+ * 🛑 A faster path through the SAME query, not a second one. A `shipped`
586
+ * row is admitted only for a store in this set, and then skips the hold
587
+ * anchor — being paid before delivery is the entire grant, so applying a
588
+ * post-delivery hold to it would make the flag do nothing.
589
+ *
590
+ * Omitted or empty means nobody is trusted, which is the correct default
591
+ * and reproduces the pre-existing behaviour exactly.
592
+ */
593
+ trustedStoreIds?: ReadonlySet<string>;
567
594
  }): Promise<Array<{
568
595
  id: string;
569
596
  ref: DocumentReference;
@@ -9,7 +9,7 @@ import { normalizeError } from "../../../errors/normalize.js";
9
9
  * that mix-up swallowing ~100 call sites' validation messages.
10
10
  */
11
11
  import { DatabaseError, NotFoundError, ValidationError } from "../../../errors/index.js";
12
- import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase/index.js";
12
+ import { BaseRepository, prepareForFirestore, parseSieveDateValue, getFirestoreCount, } from "../../../providers/db-firebase/index.js";
13
13
  import { maskPii, revealPii, ORDER_PII_FIELDS, } from "../../../security/index.js";
14
14
  import { serverTimestamp, arrayUnion } from "../../../contracts/field-ops.js";
15
15
  import { createOrderId, ORDER_COLLECTION, ORDER_TRACKED_FIELDS, OrderStatusValues, } from "../schemas/index.js";
@@ -304,10 +304,11 @@ class OrderRepository extends BaseRepository {
304
304
  reason: ctx?.reason,
305
305
  note: ctx?.note,
306
306
  extraChanges: ctx?.extraChanges,
307
- // `encryptPiiFields` is a flat top-level loop and never descends into
308
- // arrays, so anything PII-named reaching `changes` would persist in
309
- // PLAINTEXT inside statusHistory. None of ORDER_TRACKED_FIELDS is PII
310
- // today — this is defence for the next field somebody adds.
307
+ // `encryptPiiFields` covers a field only where it is NAMED, so a
308
+ // PII-named value reaching `changes` is plaintext unless somebody
309
+ // declared `statusHistory[].changes.<that key>` — which nobody will.
310
+ // The scrub removes the guesswork. None of ORDER_TRACKED_FIELDS is PII
311
+ // today; this is defence for the next field somebody adds.
311
312
  piiFields: ORDER_PII_FIELDS,
312
313
  historyField: "statusHistory",
313
314
  truncatedField: "statusHistoryTruncated",
@@ -516,6 +517,26 @@ class OrderRepository extends BaseRepository {
516
517
  .get();
517
518
  return snapshot.size;
518
519
  }
520
+ /**
521
+ * How many orders this buyer has placed, optionally scoped to one lane.
522
+ *
523
+ * A Firestore aggregation — billed at one read per 1000 index entries
524
+ * matched, not one per order — so it stays flat as an account's history
525
+ * grows. That matters because the only caller is the session profile
526
+ * endpoint, which every page load hits.
527
+ *
528
+ * 🛑 `orderType` may only name a NON-standard lane. The field was added
529
+ * after orders already existed, so an equality on `"standard"` would return
530
+ * zero for every legacy order while looking perfectly correct — see the
531
+ * constant's own note.
532
+ */
533
+ async countForUser(userId, orderType) {
534
+ let query = this.getCollection().where(ORDER_FIELDS.USER_ID, "==", userId);
535
+ if (orderType) {
536
+ query = query.where(ORDER_FIELDS.ORDER_TYPE, "==", orderType);
537
+ }
538
+ return getFirestoreCount(query);
539
+ }
519
540
  async deleteByUser(userId) {
520
541
  try {
521
542
  const snapshot = await this.getCollection()
@@ -1139,10 +1160,23 @@ class OrderRepository extends BaseRepository {
1139
1160
  * and the anchor test is a per-row predicate below. One query, no new
1140
1161
  * composite index, and no way to silently return nothing.
1141
1162
  */
1163
+ const trusted = opts?.trustedStoreIds ?? new Set();
1142
1164
  const snap = await this.db
1143
1165
  .collection(this.collection)
1144
1166
  .where(ORDER_FIELDS.PAYOUT_STATUS, "==", "eligible")
1145
- .where(ORDER_FIELDS.STATUS, "==", OrderStatusValues.DELIVERED)
1167
+ /*
1168
+ * 🛑 Widened to an `in` ONLY when some store is trusted.
1169
+ *
1170
+ * Firestore's `in` is an OR of equalities, so the existing
1171
+ * `(payoutStatus, status)` composite serves both forms and no new index
1172
+ * is needed. Keeping the plain equality when the set is empty matters
1173
+ * anyway: a deployment with no trusted sellers then runs byte-identically
1174
+ * to before, so the flag cannot change behaviour for anyone who has not
1175
+ * been granted it.
1176
+ */
1177
+ .where(ORDER_FIELDS.STATUS, ...(trusted.size > 0
1178
+ ? ["in", [OrderStatusValues.DELIVERED, OrderStatusValues.SHIPPED]]
1179
+ : ["==", OrderStatusValues.DELIVERED]))
1146
1180
  .limit(500)
1147
1181
  .get();
1148
1182
  let missingAnchor = 0;
@@ -1153,6 +1187,26 @@ class OrderRepository extends BaseRepository {
1153
1187
  data: { id: d.id, ...d.data() },
1154
1188
  }))
1155
1189
  .filter((o) => !o.data.emiEnabled || o.data.emiComplete)
1190
+ .filter((o) => {
1191
+ /*
1192
+ * 🛑 A SHIPPED row is admitted only for a TRUSTED store, and then
1193
+ * bypasses the hold entirely — paying at shipping is the grant.
1194
+ *
1195
+ * Tested before the anchor rather than inside it, because a shipped
1196
+ * order has no `deliveryDate` and would fall through to the
1197
+ * `updatedAt` fallback — which is always LATER than delivery would
1198
+ * have been. A trusted seller would be paid more SLOWLY than an
1199
+ * untrusted one: the flag would read as working and do the opposite.
1200
+ *
1201
+ * The `in` above cannot express "shipped, but only for these stores",
1202
+ * so the store test has to live here — otherwise one trusted seller
1203
+ * anywhere would widen the sweep for every seller.
1204
+ */
1205
+ if (o.data.status === OrderStatusValues.SHIPPED) {
1206
+ return typeof o.data.storeId === "string" && trusted.has(o.data.storeId);
1207
+ }
1208
+ return true;
1209
+ })
1156
1210
  .filter((o) => {
1157
1211
  /*
1158
1212
  * The anchor, with a conservative fallback. `deliveryDate` is the
@@ -0,0 +1,40 @@
1
+ /**
2
+ * D55 — write shapes for the pre-order balance collector.
3
+ *
4
+ * 🛑 Both are `.strict()`, and that is the Root Cause #76 lesson rather than
5
+ * tidiness: the balance block carries `status`, `reviewedBy`, `reviewedAt`
6
+ * and `amount`, and a write shape that can NAME a field is a write shape that
7
+ * can destroy it. A buyer must not be able to send `status: "paid"` or
8
+ * `amount: 1`, and `z.object()` merely STRIPS unknown keys — which is the
9
+ * same silent-success failure as the grouped-listings PATCH. `.strict()`
10
+ * makes it a 400 instead.
11
+ *
12
+ * Neither schema is where `amount` comes from. It is snapshotted from
13
+ * `sourceContext.balanceDue` when the seller opens collection, months before
14
+ * this, and is never re-read from the live product (D61).
15
+ */
16
+ import { z } from "zod";
17
+ /** The buyer's proof of payment. */
18
+ export declare const balanceProofSchema: z.ZodObject<{
19
+ proofUrl: z.ZodString;
20
+ transactionId: z.ZodString;
21
+ }, "strict", z.ZodTypeAny, {
22
+ transactionId: string;
23
+ proofUrl: string;
24
+ }, {
25
+ transactionId: string;
26
+ proofUrl: string;
27
+ }>;
28
+ export type BalanceProofInput = z.infer<typeof balanceProofSchema>;
29
+ /** The admin's verdict on a submitted proof. */
30
+ export declare const balanceReviewSchema: z.ZodObject<{
31
+ approve: z.ZodBoolean;
32
+ note: z.ZodOptional<z.ZodString>;
33
+ }, "strict", z.ZodTypeAny, {
34
+ approve: boolean;
35
+ note?: string | undefined;
36
+ }, {
37
+ approve: boolean;
38
+ note?: string | undefined;
39
+ }>;
40
+ export type BalanceReviewInput = z.infer<typeof balanceReviewSchema>;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * D55 — write shapes for the pre-order balance collector.
3
+ *
4
+ * 🛑 Both are `.strict()`, and that is the Root Cause #76 lesson rather than
5
+ * tidiness: the balance block carries `status`, `reviewedBy`, `reviewedAt`
6
+ * and `amount`, and a write shape that can NAME a field is a write shape that
7
+ * can destroy it. A buyer must not be able to send `status: "paid"` or
8
+ * `amount: 1`, and `z.object()` merely STRIPS unknown keys — which is the
9
+ * same silent-success failure as the grouped-listings PATCH. `.strict()`
10
+ * makes it a 400 instead.
11
+ *
12
+ * Neither schema is where `amount` comes from. It is snapshotted from
13
+ * `sourceContext.balanceDue` when the seller opens collection, months before
14
+ * this, and is never re-read from the live product (D61).
15
+ */
16
+ import { z } from "zod";
17
+ /** The buyer's proof of payment. */
18
+ export const balanceProofSchema = z
19
+ .object({
20
+ /*
21
+ * A `/media/…` slug from the signed-URL upload flow — bytes never reach
22
+ * this route (Rule #6's 4.5 MB payload cap, and § Media Architecture).
23
+ */
24
+ proofUrl: z.string().min(1, "Upload a screenshot of the payment."),
25
+ /*
26
+ * The UTR or UPI reference. Required, because it is the only thing an
27
+ * admin can check the payment against — a screenshot alone is a picture.
28
+ */
29
+ transactionId: z
30
+ .string()
31
+ .min(4, "Enter the transaction reference from your bank or UPI app.")
32
+ .max(64),
33
+ })
34
+ .strict();
35
+ /** The admin's verdict on a submitted proof. */
36
+ export const balanceReviewSchema = z
37
+ .object({
38
+ approve: z.boolean(),
39
+ /*
40
+ * Optional HERE and required in `reviewBalanceProof` when `approve` is
41
+ * false. Deliberately not a `.refine()`: the rule belongs with the
42
+ * transition it guards, and duplicating it would give two places to
43
+ * disagree. An approval genuinely needs no note.
44
+ */
45
+ note: z.string().max(300).optional(),
46
+ })
47
+ .strict();
@@ -174,6 +174,68 @@ export declare const RefundStatusValues: {
174
174
  readonly REJECTED: "rejected";
175
175
  };
176
176
  /** Firestore storage shape for an order line item â€" distinct from the API display model OrderItem */
177
+ /**
178
+ * The PII inside one amendment.
179
+ *
180
+ * 🛑 BARE keys, and `ORDER_PII_FIELDS` uses the `deliveryAmendments[].…`
181
+ * form for the same five fields. Two notations, two scopes, both correct:
182
+ * the `[]` form is for the whole ORDER document, where the hook has to walk
183
+ * into the array; these bare names are for ONE amendment object, where they
184
+ * are genuinely top-level.
185
+ *
186
+ * 🛑 This list was first justified as "`encryptPiiFields` CANNOT cover an
187
+ * array" — CLAUDE.md's wording, and FALSE. `encryptInArray` has always
188
+ * handled `a[].b`, in both directions. The real rule is that the encryptor
189
+ * does not infer: a field is covered only where it is NAMED, in the notation
190
+ * matching the object the list is applied to.
191
+ */
192
+ export declare const AMENDMENT_PII_FIELDS: readonly ["fullName", "phone", "addressLine1", "addressLine2", "landmark"];
193
+ /**
194
+ * A correction to the delivery address after the order was placed.
195
+ *
196
+ * 🛑 Carries the FULL new address, not a diff. A diff would make the current
197
+ * address reconstructable only by replaying every entry in order, so one
198
+ * malformed entry in the middle makes everything after it unreadable — and
199
+ * the parcel label is produced from the current address.
200
+ *
201
+ * PII in here is the same problem as everywhere else: `encryptPiiFields` does
202
+ * not descend into arrays, so these values are NOT encrypted by that
203
+ * mechanism. `scrubAmendmentPii` is what handles them; see its header.
204
+ */
205
+ export interface DeliveryAmendment {
206
+ at: Date;
207
+ /** Who asked. `actorRole` distinguishes a buyer correction from a support fix. */
208
+ actorUid?: string;
209
+ actorRole: "buyer" | "seller" | "admin" | "system";
210
+ reason?: string;
211
+ fullName: string;
212
+ phone: string;
213
+ addressLine1: string;
214
+ addressLine2?: string;
215
+ landmark?: string;
216
+ city: string;
217
+ state: string;
218
+ pincode: string;
219
+ country: string;
220
+ }
221
+ /**
222
+ * How much seller prose a single order line carries.
223
+ *
224
+ * 🛑 A Firestore document caps at 1 MiB and `items[]` is inline on the order.
225
+ * 2,000 characters × 20 lines is ~40 KB of description — comfortable. The
226
+ * same field unbounded is a rejected write at checkout, and the checkout is
227
+ * the one operation in the system that must never fail for a reason the buyer
228
+ * cannot act on.
229
+ */
230
+ export declare const ORDER_ITEM_DESCRIPTION_MAX = 2000;
231
+ /**
232
+ * How many spec rows one order line keeps.
233
+ *
234
+ * Capped by COUNT rather than characters: a spec table is rows, and half a
235
+ * row is worse than a missing one — "Weight: 4" with the unit cut off reads
236
+ * as a fact rather than as truncation.
237
+ */
238
+ export declare const ORDER_ITEM_SPECIFICATIONS_MAX = 30;
177
239
  export interface OrderDocumentItem {
178
240
  productId: string;
179
241
  productTitle: string;
@@ -261,6 +323,37 @@ export interface OrderDocumentItem {
261
323
  /** Which kind of multi-member line produced these rows. */
262
324
  groupSource?: "bundle" | "product-group" | "grouped-listing";
263
325
  /** P-8 GST — snapshotted from the product at order time so the invoice stays accurate even if the product's HSN/rate later changes. */
326
+ /**
327
+ * The listing description as it read at purchase.
328
+ *
329
+ * 🛑 BOUNDED at `ORDER_ITEM_DESCRIPTION_MAX`, with `descriptionTruncated`
330
+ * recording that it was cut. A Firestore document caps at 1 MiB and this is
331
+ * free-form seller prose on every line of every order — an unbounded copy
332
+ * on a 20-line order is a rejected write AT CHECKOUT, which fails the one
333
+ * operation that must not fail. Truncation is recorded rather than silent
334
+ * for the same reason `statusHistoryTruncated` exists: a short description
335
+ * and a cut one must not read alike.
336
+ */
337
+ description?: string;
338
+ descriptionTruncated?: boolean;
339
+ /** The spec table as it read at purchase. */
340
+ specifications?: Array<{
341
+ name: string;
342
+ value: string;
343
+ unit?: string;
344
+ }>;
345
+ /**
346
+ * For a composed listing, the parts it was sold as containing.
347
+ *
348
+ * Ids and names only — the part CATALOGUE is a live reference work and
349
+ * copying its entries onto every order line would duplicate a research
350
+ * corpus per purchase. What matters for a dispute is which parts were
351
+ * claimed, not their current catalogue entry.
352
+ */
353
+ parts?: Array<{
354
+ code: string;
355
+ name: string;
356
+ }>;
264
357
  hsnCode?: string;
265
358
  gstRate?: 0 | 5 | 12 | 18 | 28;
266
359
  /**
@@ -425,7 +518,64 @@ export interface OrderDocument extends BaseDocument {
425
518
  paymentStatus: PaymentStatus;
426
519
  paymentId?: string;
427
520
  paymentMethod?: string;
521
+ /**
522
+ * 🛑 LEGACY. Never written for an order created after Part 14.
523
+ *
524
+ * Flat free text with no pincode, no phone and no address id — which is why
525
+ * `placeOfSupply` had to be written separately rather than derived, and why
526
+ * parcel booking could not be built at all (`CreateShipmentInput` needs a
527
+ * structured `{name, phone, addressLine1, city, state, pincode, country}`).
528
+ *
529
+ * It is ALSO in `ORDER_UPDATABLE_FIELDS`, so it is editable after the order
530
+ * — meaning it never recorded what the buyer actually chose. And it was
531
+ * never encrypted: `ORDER_PII_FIELDS` is `userName` / `userEmail` /
532
+ * `sellerEmail`, so every delivery address on every order up to now sits in
533
+ * Firestore in plaintext, despite the docs saying otherwise.
534
+ *
535
+ * Read it only as a fallback for orders that predate `deliveryAddress*`,
536
+ * and render it labelled as legacy. Forward-only; no backfill — parsing a
537
+ * state out of free text to file a tax return is the thing the
538
+ * `placeOfSupply` comment below already refuses.
539
+ */
428
540
  shippingAddress?: string;
541
+ /** PII — encrypted. See `ORDER_PII_FIELDS`. */
542
+ deliveryFullName?: Encrypted<string>;
543
+ /** PII — encrypted. */
544
+ deliveryPhone?: Encrypted<string>;
545
+ /** PII — encrypted. */
546
+ deliveryAddressLine1?: Encrypted<string>;
547
+ /** PII — encrypted. Optional second line / apartment. */
548
+ deliveryAddressLine2?: Encrypted<string>;
549
+ /** PII — encrypted. */
550
+ deliveryLandmark?: Encrypted<string>;
551
+ deliveryCity?: string;
552
+ deliveryState?: string;
553
+ deliveryPincode?: string;
554
+ deliveryCountry?: string;
555
+ /**
556
+ * The `addresses` row this was taken from, for provenance only.
557
+ *
558
+ * 🛑 Never resolved at read time. The row can be edited or deleted after the
559
+ * order, and following it would make the invoice disagree with what the
560
+ * buyer chose — which is the entire failure a snapshot exists to prevent.
561
+ */
562
+ deliveryAddressId?: string;
563
+ /**
564
+ * Corrections after the fact, newest LAST. Append-only.
565
+ *
566
+ * 🛑 Its OWN array, not a passenger on `statusHistory`.
567
+ *
568
+ * `statusHistory` caps at 50 with FIFO eviction. Address edits riding that
569
+ * log would compete with status transitions for the same slots and evict
570
+ * the thing the timeline exists to show — and on a chatty order the
571
+ * creation entry goes first, which is precisely the value reconstruction
572
+ * depends on. The creation snapshot is a FIELD above for the same reason: a
573
+ * field cannot be truncated.
574
+ *
575
+ * Dispatch reads the latest amendment (or the snapshot when there is none).
576
+ * The invoice always reads the snapshot.
577
+ */
578
+ deliveryAmendments?: DeliveryAmendment[];
429
579
  trackingNumber?: string;
430
580
  notes?: string;
431
581
  orderDate: Date;
@@ -1130,6 +1280,58 @@ export interface OrderDocument extends BaseDocument {
1130
1280
  /** Buyer's required "this payment is genuine, no fraudulent tricks/chargebacks" agreement — server-validated, not just a client-side disable. */
1131
1281
  buyerFraudAgreementAccepted?: boolean;
1132
1282
  buyerFraudAgreementAcceptedAt?: Date;
1283
+ /**
1284
+ * ── D55: the pre-order BALANCE, which is a second payment ──────────────
1285
+ *
1286
+ * 🛑 Its own block rather than reusing the fields above, and that is the
1287
+ * decision this whole feature turns on. Every field above describes ONE
1288
+ * payment; a pre-order has two, months apart. Reusing `paymentProofUrl`
1289
+ * for the balance overwrites the deposit's proof — the evidence for the
1290
+ * payment that created the order — and `paymentReviewOutcome` then means
1291
+ * two different things depending on when you read it.
1292
+ *
1293
+ * Worse, `paymentWindowTimeout` sweeps orders that are `pending`/`pending`
1294
+ * with no `paymentProofUrl`, so clearing that field to collect a balance
1295
+ * would make the sweep cancel and restock a PAID order.
1296
+ *
1297
+ * The balance is therefore its own small state machine beside the
1298
+ * deposit's, sharing the review vocabulary but none of the storage.
1299
+ */
1300
+ balance?: {
1301
+ /**
1302
+ * 🛑 SNAPSHOTTED, not recomputed — D61, and the worst case in the
1303
+ * codebase for it: a deposit-to-balance gap is months. It is copied from
1304
+ * `sourceContext.balanceDue` at the moment the collection is opened, so
1305
+ * a price change, a tax-rate change or a re-quote in between cannot move
1306
+ * what the buyer was told they owe. Recomputing at payment time is how a
1307
+ * buyer who agreed to ₹4,000 is charged ₹4,400.
1308
+ */
1309
+ amount: number;
1310
+ /**
1311
+ * `manual` only, today, and the field exists so the absence is explicit.
1312
+ *
1313
+ * 🛑 The AUTO path is not merely unbuilt — it is blocked on things
1314
+ * outside this repo. A months-long gap cannot use authorise-then-capture
1315
+ * (capture windows are days), so it needs a real RBI-compliant mandate:
1316
+ * AFA at creation, a pre-debit notice at least 24 HOURS before every
1317
+ * debit, and a max amount fixed at registration. Razorpay must also have
1318
+ * recurring enabled, and production runs TEST keys with
1319
+ * `razorpayEnabled` defaulting false. Declaring the union now means
1320
+ * adding `auto` later is a compile-checked change rather than a new
1321
+ * field nobody wires.
1322
+ */
1323
+ method: "manual";
1324
+ status: "awaiting" | "submitted" | "paid" | "rejected";
1325
+ /** Set when the collection is opened, so the buyer has a stated window. */
1326
+ dueBy?: Date;
1327
+ proofUrl?: string;
1328
+ transactionId?: string;
1329
+ submittedAt?: Date;
1330
+ reviewedBy?: string;
1331
+ reviewedAt?: Date;
1332
+ /** Required on `rejected`, for the same reason a refund reason is. */
1333
+ reviewNote?: string;
1334
+ };
1133
1335
  /** Which of the three admin review actions was taken on this order's payment proof. */
1134
1336
  paymentReviewOutcome?: "approved" | "reupload_requested" | "rejected_fraud";
1135
1337
  /** Admin's reason text — required for both `reupload_requested` and `rejected_fraud`. */