@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
@@ -0,0 +1,183 @@
1
+ /**
2
+ * `chartOfAccounts` + `journalEntries` โ€” Part 4's double-entry books.
3
+ *
4
+ * ## ๐Ÿ›‘ THE LEDGER IS A PROJECTION. Orders, payouts and expenses are truth.
5
+ *
6
+ * The same split `obligations` takes, and for the same reason. An order says
7
+ * what was sold; a journal entry is an accounting *statement about* that
8
+ * order. So: **never read a balance to make an operational decision.** Do not
9
+ * gate a payout on a ledger figure, do not refuse a refund because an account
10
+ * looks short โ€” read the payout and the order. A projection that starts
11
+ * making decisions becomes a second source of truth, and then the two
12
+ * disagree with no way to say which is wrong.
13
+ *
14
+ * ## ๐Ÿ›‘ Every entry is MACHINE-GENERATED from a business event
15
+ *
16
+ * This is the one constraint that makes the whole thing safe. If a human has
17
+ * to remember that a gateway fee is `Dr Bank Charges / Cr Bank`, it will
18
+ * eventually be posted the other way round; if the settlement ingest posts
19
+ * it, it cannot be. `sourceEvent` is REQUIRED and names the event, not the
20
+ * person โ€” a hand-posted adjustment uses `"manual-journal"`, which is a
21
+ * distinguishable value precisely so the exception stays countable.
22
+ *
23
+ * ## ๐Ÿ›‘ A correction is a REVERSING ENTRY, never an edit
24
+ *
25
+ * The invoice/credit-note rule, applied to the books. `journalEntries` rows
26
+ * are append-only: `reverse()` writes a NEW entry with the legs swapped and
27
+ * the two pointed at each other. An edit would silently rewrite a period a
28
+ * GST return has already been filed for, and nothing downstream could tell.
29
+ *
30
+ * ## ๐Ÿ›‘ Money is DECIMAL RUPEES, matching every other money field here
31
+ *
32
+ * Rupees with two decimal places, like every other money field here. The
33
+ * 2026-05 migration moved the whole codebase off minor units and
34
+ * `audit-money-units` enforces it; a ledger in minor units would be the one
35
+ * place a reader has to remember a different scale, which is how a 100ร—
36
+ * error gets written (Root Cause #31).
37
+ */
38
+ import type { BaseDocument } from "../../../_internal/shared/types/base-document";
39
+ export declare const CHART_OF_ACCOUNTS_COLLECTION = "chartOfAccounts";
40
+ export declare const JOURNAL_ENTRIES_COLLECTION = "journalEntries";
41
+ export declare const ACCOUNTING_PERIODS_COLLECTION = "accountingPeriods";
42
+ /**
43
+ * The five classical account types.
44
+ *
45
+ * Carried on the ACCOUNT rather than inferred from its code, because the
46
+ * normal-balance side and every report's sign convention derive from it โ€”
47
+ * and a report that guesses the type from a code prefix breaks the first time
48
+ * somebody adds an account out of range.
49
+ */
50
+ export type AccountType = "asset" | "liability" | "equity" | "income" | "expense";
51
+ /**
52
+ * Which side INCREASES this account.
53
+ *
54
+ * Derived from `AccountType` by `normalBalanceFor`, never stored โ€” a stored
55
+ * copy is a mirror that drifts the first time somebody types the wrong one
56
+ * (Root Cause #42), and the mapping is a fact about accounting rather than
57
+ * about this business.
58
+ */
59
+ export type NormalBalance = "debit" | "credit";
60
+ export declare function normalBalanceFor(type: AccountType): NormalBalance;
61
+ export interface ChartAccountDocument extends BaseDocument {
62
+ /**
63
+ * Stable numeric code, e.g. `"1100"`. The id is `account-{code}`.
64
+ *
65
+ * ๐Ÿ›‘ Codes are grouped by type in the conventional Indian small-business
66
+ * ranges (1xxx asset, 2xxx liability, 3xxx equity, 4xxx income, 5xxx
67
+ * expense) **so a CA can map them to Tally at a glance** โ€” that is the
68
+ * entire reason for a numeric code rather than a slug. It is a convention
69
+ * for humans; nothing in the code derives `type` from the range, because a
70
+ * derivation would make adding an account outside it silently wrong.
71
+ */
72
+ code: string;
73
+ name: string;
74
+ type: AccountType;
75
+ /** A one-line note for whoever has to reconcile this against a bank statement. */
76
+ description?: string;
77
+ /**
78
+ * A closed account refuses new postings but keeps its history.
79
+ *
80
+ * Deliberately not a delete: an account with entries against it cannot be
81
+ * removed without orphaning them, and a closed row still has to appear in
82
+ * last year's trial balance.
83
+ */
84
+ isActive: boolean;
85
+ }
86
+ /**
87
+ * One side of one entry.
88
+ *
89
+ * ๐Ÿ›‘ `debit` and `credit` are SEPARATE non-negative fields rather than one
90
+ * signed amount. A signed amount needs a sign convention, the convention
91
+ * differs per account type, and a reader then has to know the account's type
92
+ * to interpret the number โ€” which is how a credit gets read as a debit. Two
93
+ * fields are unambiguous at the point of reading, which is where it matters.
94
+ *
95
+ * Exactly one of them is non-zero on any line; `assertBalanced` refuses both.
96
+ */
97
+ export interface JournalLine {
98
+ accountCode: string;
99
+ debit: number;
100
+ credit: number;
101
+ /** Why this leg exists, for a human reading the entry a year later. */
102
+ memo?: string;
103
+ /**
104
+ * Who this leg is ABOUT, when that is a specific counterparty โ€” a store for
105
+ * a seller payable, a vendor for a bill.
106
+ *
107
+ * Optional because most legs (bank, GST, sales) have no counterparty. Where
108
+ * it exists it is what makes a per-seller payable ageing possible without a
109
+ * separate ledger per store.
110
+ */
111
+ counterpartyId?: string;
112
+ }
113
+ /**
114
+ * What caused this entry. REQUIRED, and the closed list is the point.
115
+ *
116
+ * ๐Ÿ›‘ `manual-journal` is a real member and must stay one. The alternative โ€”
117
+ * letting a hand-posted entry claim an automated source โ€” is how the "every
118
+ * entry is machine-generated" constraint stops being checkable. With it
119
+ * declared, "how many of our entries were typed by a person" is one query.
120
+ */
121
+ export type JournalSourceEvent = "order-placed" | "order-delivered" | "order-refunded" | "payout-paid" | "settlement-ingest" | "expense-recorded" | "asset-purchased" | "opening-balance" | "manual-journal" | "reversal";
122
+ export interface JournalEntryDocument extends BaseDocument {
123
+ /**
124
+ * The date the entry BELONGS TO, which is not when it was written.
125
+ *
126
+ * A settlement ingested on the 3rd for transactions on the 31st posts to
127
+ * the 31st. `createdAt` records when we learned; this records when it
128
+ * happened, and every report and the period lock read this one.
129
+ */
130
+ entryDate: Date;
131
+ /** `YYYY-MM`, derived from `entryDate` โ€” the lock and the rollups key on it. */
132
+ period: string;
133
+ sourceEvent: JournalSourceEvent;
134
+ /**
135
+ * The record that caused this โ€” an order id, a payout id, an expense id.
136
+ *
137
+ * Absent only for `opening-balance` and `manual-journal`, which have no
138
+ * causing record by definition. `verify-posting-rules` checks that pairing
139
+ * rather than trusting it.
140
+ */
141
+ sourceId?: string;
142
+ narration: string;
143
+ lines: JournalLine[];
144
+ /**
145
+ * Both totals, stored.
146
+ *
147
+ * ๐Ÿ›‘ Redundant with `lines` ON PURPOSE, and this is the one place a mirror
148
+ * is right: a trial balance that had to sum every line of every entry is an
149
+ * unbounded read, and these two are written inside the same transaction
150
+ * that validated them. They cannot drift, because nothing can write an
151
+ * entry without going through `assertBalanced`.
152
+ */
153
+ totalDebit: number;
154
+ totalCredit: number;
155
+ /** Set on the entry this one reverses, and on the reversal, pointing both ways. */
156
+ reversedByEntryId?: string;
157
+ reversesEntryId?: string;
158
+ }
159
+ /**
160
+ * A period, and whether it is closed.
161
+ *
162
+ * Id is the period itself (`period-2026-10`), so the lock check is a single
163
+ * `get` by a key the caller already has โ€” not a query.
164
+ */
165
+ export interface AccountingPeriodDocument extends BaseDocument {
166
+ /** `YYYY-MM`. */
167
+ period: string;
168
+ status: "open" | "locked";
169
+ /** Set when locked โ€” which GST return this period was filed under. */
170
+ lockedReason?: string;
171
+ lockedAt?: Date;
172
+ lockedByUid?: string;
173
+ }
174
+ export declare const LEDGER_FIELDS: {
175
+ readonly CODE: "code";
176
+ readonly TYPE: "type";
177
+ readonly IS_ACTIVE: "isActive";
178
+ readonly ENTRY_DATE: "entryDate";
179
+ readonly PERIOD: "period";
180
+ readonly SOURCE_EVENT: "sourceEvent";
181
+ readonly SOURCE_ID: "sourceId";
182
+ readonly STATUS: "status";
183
+ };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * `chartOfAccounts` + `journalEntries` โ€” Part 4's double-entry books.
3
+ *
4
+ * ## ๐Ÿ›‘ THE LEDGER IS A PROJECTION. Orders, payouts and expenses are truth.
5
+ *
6
+ * The same split `obligations` takes, and for the same reason. An order says
7
+ * what was sold; a journal entry is an accounting *statement about* that
8
+ * order. So: **never read a balance to make an operational decision.** Do not
9
+ * gate a payout on a ledger figure, do not refuse a refund because an account
10
+ * looks short โ€” read the payout and the order. A projection that starts
11
+ * making decisions becomes a second source of truth, and then the two
12
+ * disagree with no way to say which is wrong.
13
+ *
14
+ * ## ๐Ÿ›‘ Every entry is MACHINE-GENERATED from a business event
15
+ *
16
+ * This is the one constraint that makes the whole thing safe. If a human has
17
+ * to remember that a gateway fee is `Dr Bank Charges / Cr Bank`, it will
18
+ * eventually be posted the other way round; if the settlement ingest posts
19
+ * it, it cannot be. `sourceEvent` is REQUIRED and names the event, not the
20
+ * person โ€” a hand-posted adjustment uses `"manual-journal"`, which is a
21
+ * distinguishable value precisely so the exception stays countable.
22
+ *
23
+ * ## ๐Ÿ›‘ A correction is a REVERSING ENTRY, never an edit
24
+ *
25
+ * The invoice/credit-note rule, applied to the books. `journalEntries` rows
26
+ * are append-only: `reverse()` writes a NEW entry with the legs swapped and
27
+ * the two pointed at each other. An edit would silently rewrite a period a
28
+ * GST return has already been filed for, and nothing downstream could tell.
29
+ *
30
+ * ## ๐Ÿ›‘ Money is DECIMAL RUPEES, matching every other money field here
31
+ *
32
+ * Rupees with two decimal places, like every other money field here. The
33
+ * 2026-05 migration moved the whole codebase off minor units and
34
+ * `audit-money-units` enforces it; a ledger in minor units would be the one
35
+ * place a reader has to remember a different scale, which is how a 100ร—
36
+ * error gets written (Root Cause #31).
37
+ */
38
+ export const CHART_OF_ACCOUNTS_COLLECTION = "chartOfAccounts";
39
+ export const JOURNAL_ENTRIES_COLLECTION = "journalEntries";
40
+ export const ACCOUNTING_PERIODS_COLLECTION = "accountingPeriods";
41
+ export function normalBalanceFor(type) {
42
+ return type === "asset" || type === "expense" ? "debit" : "credit";
43
+ }
44
+ export const LEDGER_FIELDS = {
45
+ CODE: "code",
46
+ TYPE: "type",
47
+ IS_ACTIVE: "isActive",
48
+ ENTRY_DATE: "entryDate",
49
+ PERIOD: "period",
50
+ SOURCE_EVENT: "sourceEvent",
51
+ SOURCE_ID: "sourceId",
52
+ STATUS: "status",
53
+ };
54
+ /*
55
+ * ๐Ÿ›‘ `periodOf` LIVES IN `_internal/shared/features/ledger/posting.ts`, not
56
+ * here, and the reason is mechanical rather than taxonomic.
57
+ *
58
+ * `verify-posting-rules.mjs` imports the posting rules directly via Node's
59
+ * `--experimental-strip-types` so it exercises the shipped source rather
60
+ * than a build artefact (Root Cause #105). Type stripping erases
61
+ * `import type` entirely but cannot RESOLVE a runtime import โ€” appkit source
62
+ * is written extensionless and the build adds `.js` โ€” so a single runtime
63
+ * import in that file would make it unloadable and the audit would be a
64
+ * load-failure instead of a check.
65
+ *
66
+ * Deciding which period an entry belongs to is a posting rule anyway, and it
67
+ * is the one the audit most needs to exercise: a UTC derivation files every
68
+ * IST evening sale in the previous GST return.
69
+ */
@@ -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();