@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,221 @@
1
+ /**
2
+ * What each business event posts.
3
+ *
4
+ * ## 🛑 THIS FILE IS THE WHOLE SAFETY ARGUMENT
5
+ *
6
+ * "Every journal entry is machine-generated from a business event" is the
7
+ * one constraint that makes a ledger maintained by a web application
8
+ * trustworthy. If a human has to remember that a gateway fee is
9
+ * `Dr Bank Charges / Cr Bank`, it will eventually be posted backwards — and
10
+ * a backwards entry still balances, so nothing downstream objects. These
11
+ * functions are where that memory lives instead.
12
+ *
13
+ * Pure, and returning a `PostingInput` rather than writing: the caller hands
14
+ * the result to `journalRepository.post`, which is the only thing that
15
+ * touches Firestore and the only thing that checks the period lock. That
16
+ * separation is also what lets `verify-posting-rules` exercise the rules
17
+ * against real order shapes without a database.
18
+ *
19
+ * ## 🛑 Codes are referenced through ACCOUNT, never as bare strings
20
+ *
21
+ * A literal `"2200"` scattered across six rules is six chances to type
22
+ * `"2000"`, and a posting to an account that does not exist is silently
23
+ * dropped from the P&L by `profitAndLoss`'s unknown-account skip. One map,
24
+ * named, so a wrong code is a compile error.
25
+ */
26
+ /** The accounts these rules post to. Codes match `chartOfAccountsSeedData`. */
27
+ export const ACCOUNT = {
28
+ BANK: "1100",
29
+ COURIER_FLOAT: "1150",
30
+ GATEWAY_RECEIVABLE: "1200",
31
+ INPUT_GST: "1300",
32
+ SELLER_PAYABLE: "2100",
33
+ ACCOUNTS_PAYABLE: "2300",
34
+ OUTPUT_GST: "2200",
35
+ BUYER_DEPOSITS: "2400",
36
+ REFUNDS_PAYABLE: "2500",
37
+ OPENING_EQUITY: "3900",
38
+ COMMISSION_INCOME: "4100",
39
+ SHIPPING_RECOVERED: "4200",
40
+ ADDON_INCOME: "4300",
41
+ GATEWAY_CHARGES: "5100",
42
+ FREIGHT_PAID: "5200",
43
+ };
44
+ /** Drop legs that rounded to nothing, so a zero fee does not fail `assertBalanced`. */
45
+ function compact(lines) {
46
+ return lines.filter((l) => (l.debit ?? 0) !== 0 || (l.credit ?? 0) !== 0);
47
+ }
48
+ const r2 = (n) => Math.round((n ?? 0) * 100) / 100;
49
+ /**
50
+ * A paid order.
51
+ *
52
+ * 🛑 **The seller payable is the RESIDUAL, not a computed share.** Everything
53
+ * the platform keeps is recognised explicitly — commission, its GST, the
54
+ * shipping the buyer paid, add-ons, and output GST — and what remains is what
55
+ * the seller is owed. Computing the seller's share independently and letting
56
+ * the two meet in the middle is how a rounding difference becomes an
57
+ * unbalanced entry that `assertBalanced` then refuses at checkout, turning a
58
+ * half-paisa into a failed order.
59
+ *
60
+ * 🛑 **Output GST is a LIABILITY, which is the entire point of the exercise.**
61
+ * `revenueRollup.totalRevenue` sums `order.totalPrice`, so today's "revenue"
62
+ * includes tax owed to the government and freight owed to a courier. Crediting
63
+ * 2200 here is what separates turnover from takings for the first time.
64
+ *
65
+ * 🛑 The debit is to GATEWAY RECEIVABLE, not Bank. Money captured is not
66
+ * money arrived — the settlement lands days later, net of MDR, and
67
+ * `settlementPosting` is what clears this account. Debiting Bank here would
68
+ * overstate cash by the whole float for the length of the settlement cycle.
69
+ */
70
+ export function orderPlacedPosting(o) {
71
+ const commission = r2(o.platformFee ?? 0);
72
+ const commissionGst = r2(o.platformFeeGst ?? 0);
73
+ const shipping = r2(o.shippingFee ?? 0);
74
+ const addons = r2(o.addonFees ?? 0);
75
+ const outputGst = r2((o.gstAmount ?? 0) + commissionGst);
76
+ const sellerPayable = r2(o.totalPrice - commission - commissionGst - shipping - addons - (o.gstAmount ?? 0));
77
+ return {
78
+ entryDate: o.placedAt,
79
+ sourceEvent: "order-placed",
80
+ sourceId: o.orderId,
81
+ narration: `Order ${o.orderId}`,
82
+ lines: compact([
83
+ { accountCode: ACCOUNT.GATEWAY_RECEIVABLE, debit: r2(o.totalPrice), credit: 0, memo: "Captured, awaiting settlement" },
84
+ { accountCode: ACCOUNT.SELLER_PAYABLE, debit: 0, credit: sellerPayable, counterpartyId: o.storeId, memo: "Owed to the seller" },
85
+ { accountCode: ACCOUNT.OUTPUT_GST, debit: 0, credit: outputGst, memo: "Tax collected, owed to the government" },
86
+ { accountCode: ACCOUNT.COMMISSION_INCOME, debit: 0, credit: commission },
87
+ { accountCode: ACCOUNT.SHIPPING_RECOVERED, debit: 0, credit: shipping },
88
+ { accountCode: ACCOUNT.ADDON_INCOME, debit: 0, credit: addons },
89
+ ]),
90
+ };
91
+ }
92
+ /**
93
+ * A gateway settlement arriving in the bank.
94
+ *
95
+ * 🛑 `fee` here is the gateway's REAL charge from the settlement report, and
96
+ * it is NOT `PayoutDocument.gatewayFee` — that field is our own withholding
97
+ * against a seller's gross, a different number with a different meaning.
98
+ * Posting one as the other overstates our costs and understates what we
99
+ * withheld, in opposite directions, so the error compounds.
100
+ *
101
+ * 🛑 The GST on the fee is an ASSET (input credit), not an expense. Nothing
102
+ * in this codebase could receive input tax before Part 4, so this was simply
103
+ * unrecoverable — which is money, every month, that was never claimable.
104
+ */
105
+ export function settlementPosting(s) {
106
+ const net = r2(s.gross - s.fee - s.feeGst);
107
+ return {
108
+ entryDate: s.settledAt,
109
+ sourceEvent: "settlement-ingest",
110
+ sourceId: s.settlementId,
111
+ narration: `Settlement ${s.settlementId}`,
112
+ lines: compact([
113
+ { accountCode: ACCOUNT.BANK, debit: net, credit: 0 },
114
+ { accountCode: ACCOUNT.GATEWAY_CHARGES, debit: r2(s.fee), credit: 0, memo: "MDR from the settlement report" },
115
+ { accountCode: ACCOUNT.INPUT_GST, debit: r2(s.feeGst), credit: 0, memo: "ITC on the gateway fee" },
116
+ { accountCode: ACCOUNT.GATEWAY_RECEIVABLE, debit: 0, credit: r2(s.gross) },
117
+ ]),
118
+ };
119
+ }
120
+ /**
121
+ * Paying a seller.
122
+ *
123
+ * Clears the payable raised at order time against real cash. Two legs and no
124
+ * income or expense — paying a debt is not a cost, and treating it as one
125
+ * double-counts every sale.
126
+ */
127
+ export function payoutPosting(p) {
128
+ return {
129
+ entryDate: p.paidAt,
130
+ sourceEvent: "payout-paid",
131
+ sourceId: p.payoutId,
132
+ narration: `Payout ${p.payoutId} to ${p.storeId}`,
133
+ lines: [
134
+ { accountCode: ACCOUNT.SELLER_PAYABLE, debit: r2(p.amount), credit: 0, counterpartyId: p.storeId },
135
+ { accountCode: ACCOUNT.BANK, debit: 0, credit: r2(p.amount) },
136
+ ],
137
+ };
138
+ }
139
+ /**
140
+ * Which expense account each category posts to.
141
+ *
142
+ * 🛑 A `Record<ExpenseCategory, …>`, so adding a category is a COMPILE
143
+ * ERROR until someone says where it lands. The alternative — a lookup with
144
+ * a default — silently files every new kind of spend into "other", and
145
+ * "other" growing is indistinguishable from nobody having noticed.
146
+ *
147
+ * 🛑 `asset` maps to a BALANCE-SHEET account, not an expense, and that is
148
+ * the one entry here that changes a P&L. A camera bought for ₹80,000 is not
149
+ * an ₹80,000 trading loss in the month it was bought; depreciation is the
150
+ * CA's and this system deliberately does not attempt it.
151
+ */
152
+ const EXPENSE_ACCOUNT = {
153
+ freight: ACCOUNT.FREIGHT_PAID,
154
+ "platform-software": "5500",
155
+ wages: "5400",
156
+ packaging: "5900",
157
+ asset: "1500",
158
+ other: "5900",
159
+ };
160
+ /**
161
+ * A bill.
162
+ *
163
+ * 🛑 The funding leg depends on whether it was PAID, and conflating the two
164
+ * is the commonest small-business bookkeeping error: crediting Bank for a
165
+ * bill not yet paid shows cash leaving that is still there, and crediting
166
+ * Accounts payable for one already settled leaves a debt that does not
167
+ * exist. Both balance; both are wrong; neither announces itself.
168
+ *
169
+ * 🛑 `inputGst` is DEBITED to an asset, never added to the cost. Expensing
170
+ * it is how input credit silently stops being claimable — the money is
171
+ * spent either way, but only one of the two gets it back.
172
+ */
173
+ export function expensePosting(e) {
174
+ const net = r2(e.amount);
175
+ const gst = r2(e.inputGst ?? 0);
176
+ return {
177
+ entryDate: e.billDate,
178
+ sourceEvent: "expense-recorded",
179
+ sourceId: e.expenseId,
180
+ narration: `${e.vendorName} — ${e.category}`,
181
+ lines: compact([
182
+ { accountCode: EXPENSE_ACCOUNT[e.category], debit: net, credit: 0, ...(e.vendorId ? { counterpartyId: e.vendorId } : {}) },
183
+ { accountCode: ACCOUNT.INPUT_GST, debit: gst, credit: 0, memo: "ITC on this bill" },
184
+ {
185
+ accountCode: e.paid ? ACCOUNT.BANK : ACCOUNT.ACCOUNTS_PAYABLE,
186
+ debit: 0,
187
+ credit: r2(net + gst),
188
+ ...(e.vendorId ? { counterpartyId: e.vendorId } : {}),
189
+ },
190
+ ]),
191
+ };
192
+ }
193
+ /**
194
+ * A refund.
195
+ *
196
+ * 🛑 **A refund is NOT a negative sale, and that distinction is why the top
197
+ * line is currently wrong.** `revenueRollup` removes a fully-refunded order
198
+ * from its sum entirely and does not reduce it at all for a partial — so
199
+ * revenue silently rewrites itself retroactively, and no two runs of the same
200
+ * report over the same period agree.
201
+ *
202
+ * Here it is its own event with its own date: the sale stays in the month it
203
+ * happened, the refund lands in the month it happened, and both are visible.
204
+ *
205
+ * 🛑 It DEBITS output GST, reclaiming tax already declared. Omitting that leg
206
+ * means paying GST on money that was given back.
207
+ */
208
+ export function refundPosting(r) {
209
+ const gst = r2(r.gstAmount ?? 0);
210
+ return {
211
+ entryDate: r.refundedAt,
212
+ sourceEvent: "order-refunded",
213
+ sourceId: r.refundId,
214
+ narration: `Refund ${r.refundId} on order ${r.orderId}`,
215
+ lines: compact([
216
+ { accountCode: ACCOUNT.COMMISSION_INCOME, debit: r2(r.amount - gst), credit: 0, memo: "Sale reversed" },
217
+ { accountCode: ACCOUNT.OUTPUT_GST, debit: gst, credit: 0, memo: "Tax reclaimed on the refunded amount" },
218
+ { accountCode: ACCOUNT.REFUNDS_PAYABLE, debit: 0, credit: r2(r.amount) },
219
+ ]),
220
+ };
221
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Coordinated purchase — several people chipping in on one listing.
3
+ *
4
+ * ## 🛑 It is a SPECIALISED OFFER. Almost nothing here is new.
5
+ *
6
+ * Owner, 2026-10-22: *"it is not a variant, it is similar to an offer, which
7
+ * the seller enables on the listing, then the customer can coordinate on it,
8
+ * it is a specialized offer."*
9
+ *
10
+ * What that framing DELETES is the valuable part, because every one of these
11
+ * is a thing somebody would otherwise build:
12
+ *
13
+ * | Looks required | Actually |
14
+ * |---|---|
15
+ * | a 7th `OrderSourceContext` variant | none — it is `offer-accepted`, and the provenance card renders that branch already |
16
+ * | an `orderType` tab | none — the Offer-wins tab covers it |
17
+ * | a commit / settlement path | the offer lifecycle: `made → countered → accepted → paid`, with its chain fields, `checkoutDeadline` and lapse sweep |
18
+ * | a new money term | none — `unitPriceFor` honours `lockedPrice`, so tax, payout base, refund ceiling and the invoice all follow |
19
+ * | a cart lane | none — the existing `offer` lane, not locked, lapsing at `checkoutDeadline` |
20
+ *
21
+ * So this module holds exactly two things the offer did not already have: WHO
22
+ * is chipping in and how much, and WHERE each of them wants their parcel.
23
+ *
24
+ * The arithmetic is Part 2's and is not re-derived here — `resolveSplit` and
25
+ * `lockedPriceWithSurplus` live in `checkout/split.ts`, already committed,
26
+ * already carrying the rounding-remainder discipline and the
27
+ * goods-not-landed-cost argument that makes the surplus a correct charge.
28
+ */
29
+ import type { SplitContribution } from "../../checkout/split";
30
+ /**
31
+ * 🛑 FIVE, and the ceiling is about carrier quotes rather than taste.
32
+ *
33
+ * Each distinct destination needs its own `Shipment` and its own real
34
+ * `checkServiceability` call, so members multiply outbound API calls on a
35
+ * path a buyer is waiting on — against Rule #6's ~3-round-trip budget and a
36
+ * 10s function ceiling. Five is what fits with room for the rest of checkout.
37
+ *
38
+ * It is NOT a statement that six people may not club together; it is the
39
+ * number of parcels one offer can quote for synchronously. Raising it means
40
+ * moving quoting off the request path first.
41
+ */
42
+ export declare const SHARED_PURCHASE_MAX_MEMBERS = 5;
43
+ /**
44
+ * One contributor.
45
+ *
46
+ * ## 🛑 There is deliberately NO member NAME field
47
+ *
48
+ * 🛑 **CORRECTED — the reason first given here was false.** It said
49
+ * `encryptPiiFields` *"is a FLAT top-level loop that never descends into
50
+ * arrays"*, taken from CLAUDE.md and wrong: the function handles dotted paths
51
+ * and `array[].key` via `encryptInArray`, and `CONVERSATION_PII_FIELDS` ships
52
+ * `"lastMessage.body"` in production.
53
+ *
54
+ * The real argument is narrower and still decisive. The encryptor does not
55
+ * INFER — a field is protected only where it is NAMED, in notation matching
56
+ * the object the list is applied to. So a `name` here would need
57
+ * `"coordination.members[].name"` in `OFFER_PII_FIELDS`, and the near-miss
58
+ * `"name"` would read as covered in review while storing plaintext: both are
59
+ * `string`, and `Encrypted<string>` is erased at runtime.
60
+ *
61
+ * A member is a uid; names resolve from it at render time. That costs one
62
+ * lookup the renderer was making anyway and removes the field from the
63
+ * document entirely, which is stronger than encrypting it — there is nothing
64
+ * to classify, nothing to retain, and nothing to get the notation wrong for.
65
+ */
66
+ export interface CoordinationMember extends SplitContribution {
67
+ /** Firebase Auth uid of the person chipping in. */
68
+ uid: string;
69
+ /**
70
+ * WHERE this member's parcel goes — an ID into `addresses`, never a copy.
71
+ *
72
+ * 🛑 **A reference, and this is not a style preference.** Copying the
73
+ * address inline would put a name, a phone number and a street into the
74
+ * offer document — a cart-adjacent record with no encryption story and no
75
+ * retention story, which is two new obligations to serve one picker.
76
+ *
77
+ * 🛑 **The id MUST be verified server-side to belong to the submitting
78
+ * buyer.** An id is guessable in a way an address is not: without the check,
79
+ * a member supplies any `addresses` document id and the parcel ships to a
80
+ * stranger's house at the coordinator's expense. See `assertOwnedAddresses`.
81
+ *
82
+ * Absent means "same as the coordinator's" — one parcel, which is the
83
+ * ordinary case and must not require every member to pick.
84
+ */
85
+ destinationAddressId?: string;
86
+ }
87
+ export interface OfferCoordination {
88
+ /**
89
+ * The coordinator is the offer's own `buyerUid` and is NOT repeated here.
90
+ *
91
+ * A second copy is a mirror, and a mirror drifts the first time a write
92
+ * path sets one and not the other (Root Cause #42). The coordinator is also
93
+ * a contributing member, so they appear in `members` like everyone else —
94
+ * what makes them the coordinator is owning the offer.
95
+ */
96
+ members: CoordinationMember[];
97
+ /**
98
+ * The landed cost the split was resolved against, snapshotted at the moment
99
+ * the coordinator committed.
100
+ *
101
+ * 🛑 Snapshotted, not recomputed. Shipping and fees move; a split agreed on
102
+ * Monday must still mean the same amounts on Friday, and recomputing would
103
+ * silently re-divide what people already agreed to pay.
104
+ */
105
+ landedCostAtSplit: number;
106
+ /** `contributed − landedCostAtSplit`, which rides on `lockedPrice`. */
107
+ surplus: number;
108
+ }
109
+ /**
110
+ * Is this set of members committable?
111
+ *
112
+ * Returns a MESSAGE rather than a boolean, for the same reason
113
+ * `rejectRateChange` does: "too many members" and "the split is short" need
114
+ * different corrections, and a bare `false` makes the form guess.
115
+ *
116
+ * 🛑 It does NOT check the split arithmetic — `resolveSplit` owns that, and
117
+ * duplicating the `Σ(contributions) ≥ landedCost` test here would be a second
118
+ * implementation of the one invariant Part 2 exists to hold.
119
+ */
120
+ export declare function rejectCoordination(members: readonly CoordinationMember[]): string | null;
121
+ /** The distinct destinations this offer will have to ship to. */
122
+ export declare function destinationIds(members: readonly CoordinationMember[]): string[];
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Coordinated purchase — several people chipping in on one listing.
3
+ *
4
+ * ## 🛑 It is a SPECIALISED OFFER. Almost nothing here is new.
5
+ *
6
+ * Owner, 2026-10-22: *"it is not a variant, it is similar to an offer, which
7
+ * the seller enables on the listing, then the customer can coordinate on it,
8
+ * it is a specialized offer."*
9
+ *
10
+ * What that framing DELETES is the valuable part, because every one of these
11
+ * is a thing somebody would otherwise build:
12
+ *
13
+ * | Looks required | Actually |
14
+ * |---|---|
15
+ * | a 7th `OrderSourceContext` variant | none — it is `offer-accepted`, and the provenance card renders that branch already |
16
+ * | an `orderType` tab | none — the Offer-wins tab covers it |
17
+ * | a commit / settlement path | the offer lifecycle: `made → countered → accepted → paid`, with its chain fields, `checkoutDeadline` and lapse sweep |
18
+ * | a new money term | none — `unitPriceFor` honours `lockedPrice`, so tax, payout base, refund ceiling and the invoice all follow |
19
+ * | a cart lane | none — the existing `offer` lane, not locked, lapsing at `checkoutDeadline` |
20
+ *
21
+ * So this module holds exactly two things the offer did not already have: WHO
22
+ * is chipping in and how much, and WHERE each of them wants their parcel.
23
+ *
24
+ * The arithmetic is Part 2's and is not re-derived here — `resolveSplit` and
25
+ * `lockedPriceWithSurplus` live in `checkout/split.ts`, already committed,
26
+ * already carrying the rounding-remainder discipline and the
27
+ * goods-not-landed-cost argument that makes the surplus a correct charge.
28
+ */
29
+ /**
30
+ * 🛑 FIVE, and the ceiling is about carrier quotes rather than taste.
31
+ *
32
+ * Each distinct destination needs its own `Shipment` and its own real
33
+ * `checkServiceability` call, so members multiply outbound API calls on a
34
+ * path a buyer is waiting on — against Rule #6's ~3-round-trip budget and a
35
+ * 10s function ceiling. Five is what fits with room for the rest of checkout.
36
+ *
37
+ * It is NOT a statement that six people may not club together; it is the
38
+ * number of parcels one offer can quote for synchronously. Raising it means
39
+ * moving quoting off the request path first.
40
+ */
41
+ export const SHARED_PURCHASE_MAX_MEMBERS = 5;
42
+ /**
43
+ * Is this set of members committable?
44
+ *
45
+ * Returns a MESSAGE rather than a boolean, for the same reason
46
+ * `rejectRateChange` does: "too many members" and "the split is short" need
47
+ * different corrections, and a bare `false` makes the form guess.
48
+ *
49
+ * 🛑 It does NOT check the split arithmetic — `resolveSplit` owns that, and
50
+ * duplicating the `Σ(contributions) ≥ landedCost` test here would be a second
51
+ * implementation of the one invariant Part 2 exists to hold.
52
+ */
53
+ export function rejectCoordination(members) {
54
+ if (members.length === 0)
55
+ return "A coordinated purchase needs at least one contributor.";
56
+ if (members.length > SHARED_PURCHASE_MAX_MEMBERS) {
57
+ return `A coordinated purchase is limited to ${SHARED_PURCHASE_MAX_MEMBERS} contributors.`;
58
+ }
59
+ const uids = new Set(members.map((m) => m.uid));
60
+ if (uids.size !== members.length) {
61
+ /*
62
+ * One person twice is almost always a UI slip, and letting it through
63
+ * means their two rows are summed into one charge they did not agree to.
64
+ */
65
+ return "The same person appears more than once.";
66
+ }
67
+ for (const m of members) {
68
+ if (m.splitMode === "percent" && (m.percent == null || m.percent <= 0 || m.percent > 100)) {
69
+ return "Every percentage share must be between 0 and 100.";
70
+ }
71
+ if (m.splitMode === "amount" && (m.amount == null || m.amount <= 0)) {
72
+ return "Every fixed share must be more than zero.";
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+ /** The distinct destinations this offer will have to ship to. */
78
+ export function destinationIds(members) {
79
+ return [...new Set(members.map((m) => m.destinationAddressId).filter((id) => !!id))];
80
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The delivery snapshot — what the buyer chose, frozen at checkout.
3
+ *
4
+ * ## 🛑 Why the fields are flat and ugly
5
+ *
6
+ * 🛑 **CORRECTED.** This read "`encryptPiiFields` is a flat top-level loop
7
+ * that never descends", which is FALSE — it handles `a.b` and `a[].b` via
8
+ * `encryptInArray`, and `CONVERSATION_PII_FIELDS` ships `lastMessage.body`.
9
+ *
10
+ * What it does not do is INFER. A nested `deliveryAddress: {fullName, …}`
11
+ * is covered only if somebody writes `deliveryAddress.fullName` into
12
+ * `ORDER_PII_FIELDS`, and the near-miss `"fullName"` reads as covering it
13
+ * while storing plaintext.
14
+ *
15
+ * The decisive argument is not tidiness: **an unencrypted nested field looks
16
+ * identical to an encrypted one in every inspection** — both are `string`,
17
+ * and `Encrypted<string>` is erased at runtime. Flat keys cannot
18
+ * silently half-apply, because `ORDER_PII_FIELDS` either names them or does
19
+ * not and the audit can see which.
20
+ *
21
+ * ## 🛑 Four fields are deliberately NOT encrypted
22
+ *
23
+ * `city` / `state` / `pincode` / `country` decide tax and serviceability and
24
+ * must stay QUERYABLE. An encrypted pincode cannot be filtered, grouped into
25
+ * a courier manifest, or read by `placeOfSupply` without a decrypt per row.
26
+ * They are not PII in any useful sense on their own.
27
+ */
28
+ import type { AddressPlain } from "../../../../features/addresses/schemas/firestore";
29
+ /** The plaintext shape — what a caller builds and what the repository encrypts. */
30
+ export interface DeliverySnapshot {
31
+ deliveryFullName: string;
32
+ deliveryPhone: string;
33
+ deliveryAddressLine1: string;
34
+ deliveryAddressLine2?: string;
35
+ deliveryLandmark?: string;
36
+ deliveryCity: string;
37
+ deliveryState: string;
38
+ deliveryPincode: string;
39
+ deliveryCountry: string;
40
+ deliveryAddressId: string;
41
+ }
42
+ /**
43
+ * Freeze a chosen address onto an order.
44
+ *
45
+ * 🛑 Takes the resolved DOCUMENT, never an id. Resolving here would mean a
46
+ * second read on the money path, and — worse — would invite a caller to pass
47
+ * an id it had not verified belongs to the buyer. The checkout already loads
48
+ * and ownership-checks the address; this just copies it.
49
+ *
50
+ * 🛑 Takes `AddressPlain`, not `AddressDocument`, and that is the point.
51
+ * `addressesRepository` encrypts `fullName` / `phone` / `addressLine1` on
52
+ * write, so a raw `findById` returns CIPHERTEXT — snapshotting it would put
53
+ * `enc:v1:…` on the invoice, and nothing would catch it because both are
54
+ * `string` at runtime and `Encrypted<string>` is erased.
55
+ *
56
+ * Demanding the revealed type makes the mistake a COMPILE error instead of a
57
+ * comment nobody reads. Call `addressesRepository.revealAddress(doc)` first.
58
+ */
59
+ export declare function snapshotDeliveryAddress(address: AddressPlain): DeliverySnapshot;
60
+ /** Everything a carrier booking needs, in `CreateShipmentInput`'s shape. */
61
+ export interface DeliveryParty {
62
+ name: string;
63
+ phone: string;
64
+ addressLine1: string;
65
+ addressLine2?: string;
66
+ city: string;
67
+ state: string;
68
+ pincode: string;
69
+ country: string;
70
+ }
71
+ /**
72
+ * What the PARCEL goes to — the latest amendment, or the snapshot.
73
+ *
74
+ * 🛑 This is the one read that must NOT use the snapshot, and the invoice is
75
+ * the one that must. A buyer who corrects their address before dispatch
76
+ * expects the parcel to follow; the invoice records what was agreed when they
77
+ * paid. Collapsing the two is how a parcel goes to the old address or an
78
+ * invoice silently restates itself.
79
+ *
80
+ * Returns `null` rather than a partial party when a required field is
81
+ * missing — a booking built from blanks is accepted by the carrier and
82
+ * delivered nowhere.
83
+ */
84
+ export declare function deliveryPartyFor(order: {
85
+ deliveryFullName?: string;
86
+ deliveryPhone?: string;
87
+ deliveryAddressLine1?: string;
88
+ deliveryAddressLine2?: string;
89
+ deliveryCity?: string;
90
+ deliveryState?: string;
91
+ deliveryPincode?: string;
92
+ deliveryCountry?: string;
93
+ deliveryAmendments?: Array<{
94
+ fullName: string;
95
+ phone: string;
96
+ addressLine1: string;
97
+ addressLine2?: string;
98
+ city: string;
99
+ state: string;
100
+ pincode: string;
101
+ country: string;
102
+ }>;
103
+ }): DeliveryParty | null;
104
+ /**
105
+ * The state the tax split was decided from.
106
+ *
107
+ * 🛑 Reads the SNAPSHOT, never an amendment. Place of supply is fixed at the
108
+ * time of supply; an address correction after the fact does not re-decide
109
+ * CGST/SGST versus IGST on an invoice that has been issued and may have been
110
+ * filed. That is the same forward-only rule `/admin/pricing` holds for rates.
111
+ */
112
+ export declare function placeOfSupplyFrom(order: {
113
+ deliveryState?: string;
114
+ }): string | undefined;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * The delivery snapshot — what the buyer chose, frozen at checkout.
3
+ *
4
+ * ## 🛑 Why the fields are flat and ugly
5
+ *
6
+ * 🛑 **CORRECTED.** This read "`encryptPiiFields` is a flat top-level loop
7
+ * that never descends", which is FALSE — it handles `a.b` and `a[].b` via
8
+ * `encryptInArray`, and `CONVERSATION_PII_FIELDS` ships `lastMessage.body`.
9
+ *
10
+ * What it does not do is INFER. A nested `deliveryAddress: {fullName, …}`
11
+ * is covered only if somebody writes `deliveryAddress.fullName` into
12
+ * `ORDER_PII_FIELDS`, and the near-miss `"fullName"` reads as covering it
13
+ * while storing plaintext.
14
+ *
15
+ * The decisive argument is not tidiness: **an unencrypted nested field looks
16
+ * identical to an encrypted one in every inspection** — both are `string`,
17
+ * and `Encrypted<string>` is erased at runtime. Flat keys cannot
18
+ * silently half-apply, because `ORDER_PII_FIELDS` either names them or does
19
+ * not and the audit can see which.
20
+ *
21
+ * ## 🛑 Four fields are deliberately NOT encrypted
22
+ *
23
+ * `city` / `state` / `pincode` / `country` decide tax and serviceability and
24
+ * must stay QUERYABLE. An encrypted pincode cannot be filtered, grouped into
25
+ * a courier manifest, or read by `placeOfSupply` without a decrypt per row.
26
+ * They are not PII in any useful sense on their own.
27
+ */
28
+ /**
29
+ * Freeze a chosen address onto an order.
30
+ *
31
+ * 🛑 Takes the resolved DOCUMENT, never an id. Resolving here would mean a
32
+ * second read on the money path, and — worse — would invite a caller to pass
33
+ * an id it had not verified belongs to the buyer. The checkout already loads
34
+ * and ownership-checks the address; this just copies it.
35
+ *
36
+ * 🛑 Takes `AddressPlain`, not `AddressDocument`, and that is the point.
37
+ * `addressesRepository` encrypts `fullName` / `phone` / `addressLine1` on
38
+ * write, so a raw `findById` returns CIPHERTEXT — snapshotting it would put
39
+ * `enc:v1:…` on the invoice, and nothing would catch it because both are
40
+ * `string` at runtime and `Encrypted<string>` is erased.
41
+ *
42
+ * Demanding the revealed type makes the mistake a COMPILE error instead of a
43
+ * comment nobody reads. Call `addressesRepository.revealAddress(doc)` first.
44
+ */
45
+ export function snapshotDeliveryAddress(address) {
46
+ return {
47
+ deliveryFullName: String(address.fullName ?? ""),
48
+ deliveryPhone: String(address.phone ?? ""),
49
+ deliveryAddressLine1: String(address.addressLine1 ?? ""),
50
+ ...(address.addressLine2 ? { deliveryAddressLine2: String(address.addressLine2) } : {}),
51
+ ...(address.landmark ? { deliveryLandmark: String(address.landmark) } : {}),
52
+ deliveryCity: address.city ?? "",
53
+ deliveryState: address.state ?? "",
54
+ deliveryPincode: address.postalCode ?? "",
55
+ deliveryCountry: address.country ?? "",
56
+ deliveryAddressId: address.id,
57
+ };
58
+ }
59
+ /**
60
+ * What the PARCEL goes to — the latest amendment, or the snapshot.
61
+ *
62
+ * 🛑 This is the one read that must NOT use the snapshot, and the invoice is
63
+ * the one that must. A buyer who corrects their address before dispatch
64
+ * expects the parcel to follow; the invoice records what was agreed when they
65
+ * paid. Collapsing the two is how a parcel goes to the old address or an
66
+ * invoice silently restates itself.
67
+ *
68
+ * Returns `null` rather than a partial party when a required field is
69
+ * missing — a booking built from blanks is accepted by the carrier and
70
+ * delivered nowhere.
71
+ */
72
+ export function deliveryPartyFor(order) {
73
+ const latest = order.deliveryAmendments?.[order.deliveryAmendments.length - 1];
74
+ const party = latest
75
+ ? {
76
+ name: latest.fullName,
77
+ phone: latest.phone,
78
+ addressLine1: latest.addressLine1,
79
+ ...(latest.addressLine2 ? { addressLine2: latest.addressLine2 } : {}),
80
+ city: latest.city,
81
+ state: latest.state,
82
+ pincode: latest.pincode,
83
+ country: latest.country,
84
+ }
85
+ : {
86
+ name: order.deliveryFullName ?? "",
87
+ phone: order.deliveryPhone ?? "",
88
+ addressLine1: order.deliveryAddressLine1 ?? "",
89
+ ...(order.deliveryAddressLine2 ? { addressLine2: order.deliveryAddressLine2 } : {}),
90
+ city: order.deliveryCity ?? "",
91
+ state: order.deliveryState ?? "",
92
+ pincode: order.deliveryPincode ?? "",
93
+ country: order.deliveryCountry ?? "",
94
+ };
95
+ const required = [
96
+ party.name,
97
+ party.phone,
98
+ party.addressLine1,
99
+ party.city,
100
+ party.state,
101
+ party.pincode,
102
+ party.country,
103
+ ];
104
+ return required.every((v) => v.trim().length > 0) ? party : null;
105
+ }
106
+ /**
107
+ * The state the tax split was decided from.
108
+ *
109
+ * 🛑 Reads the SNAPSHOT, never an amendment. Place of supply is fixed at the
110
+ * time of supply; an address correction after the fact does not re-decide
111
+ * CGST/SGST versus IGST on an invoice that has been issued and may have been
112
+ * filed. That is the same forward-only rule `/admin/pricing` holds for rates.
113
+ */
114
+ export function placeOfSupplyFrom(order) {
115
+ return order.deliveryState || undefined;
116
+ }