@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
@@ -1,29 +1,6 @@
1
1
  "use client";
2
2
  import { useEffect, useState } from "react";
3
3
  import { CTA_ANCHOR_ATTR } from "./cta-anchor.js";
4
- /**
5
- * useCtaReveal — "has the user met the Buy button and scrolled past it?"
6
- *
7
- * Returns true when the floating action dock should be on screen:
8
- *
9
- * - the page marks NO CTA anchor → true immediately (nothing to wait
10
- * for; an admin bulk bar has no Buy button)
11
- * - the page marks one, not yet seen → false (the inline CTA is doing the
12
- * job; a second copy of it beside itself is noise)
13
- * - seen, and now out of view → true
14
- * - seen, and scrolled back into view → false again
15
- *
16
- * 🛑 `IntersectionObserver`, never a scroll-position threshold. The bar this
17
- * replaces used `window.scrollY > 400`, which is a guess about where a CTA sits
18
- * — wrong on a short page (the dock appears while the real button is still on
19
- * screen) and wrong on a long gallery (it appears before the CTA is reached).
20
- * Observing the element itself is the same rule stated exactly, and it costs no
21
- * scroll handler.
22
- *
23
- * The anchor is looked up on an interval-free `MutationObserver` because the
24
- * page that owns it mounts AFTER this dock — the dock lives in the app shell
25
- * and the CTA arrives with the route.
26
- */
27
4
  export function useCtaReveal(enabled = true, anchorId) {
28
5
  const [anchor, setAnchor] = useState(null);
29
6
  const [anchorExists, setAnchorExists] = useState(false);
@@ -74,9 +51,23 @@ export function useCtaReveal(enabled = true, anchorId) {
74
51
  io.observe(anchor);
75
52
  return () => io.disconnect();
76
53
  }, [anchor]);
54
+ /*
55
+ * Re-read at CLICK time rather than closing over `anchor`. A client-side
56
+ * navigation swaps the page under a dock that never unmounts, and the
57
+ * element captured when the effect last ran may already be detached — a
58
+ * `scrollIntoView` on a detached node silently does nothing.
59
+ */
60
+ const scrollToAnchor = () => {
61
+ if (typeof document === "undefined")
62
+ return;
63
+ const el = anchorId
64
+ ? document.getElementById(anchorId)
65
+ : document.querySelector(`[${CTA_ANCHOR_ATTR}]`);
66
+ el?.scrollIntoView({ behavior: "smooth", block: "center" });
67
+ };
77
68
  if (!enabled)
78
- return false;
69
+ return { revealed: false, hasAnchor: anchorExists, scrollToAnchor };
79
70
  if (!anchorExists)
80
- return true;
81
- return seen && !inView;
71
+ return { revealed: true, hasAnchor: false, scrollToAnchor };
72
+ return { revealed: seen && !inView, hasAnchor: true, scrollToAnchor };
82
73
  }
@@ -0,0 +1,110 @@
1
+ import { BaseRepository } from "../../../providers/db-firebase";
2
+ import { type JournalEntryDocument } from "../schemas/firestore";
3
+ import { type PostingInput } from "../../../_internal/shared/features/ledger/posting";
4
+ /**
5
+ * How many entries one period read may return.
6
+ *
7
+ * Sized against a plausible month rather than a round number: a few hundred
8
+ * orders, each posting two or three entries, plus settlements and expenses.
9
+ * 2,000 leaves real headroom and still fits comfortably inside a function's
10
+ * memory and the ~3-round-trip budget.
11
+ */
12
+ export declare const JOURNAL_PERIOD_SCAN_LIMIT = 2000;
13
+ export declare class JournalRepository extends BaseRepository<JournalEntryDocument> {
14
+ constructor();
15
+ /**
16
+ * 🛑 APPEND-ONLY, and the method bodies refuse rather than a comment
17
+ * asking nicely.
18
+ *
19
+ * `inventoryMovements` takes the same stance for the same reason: a
20
+ * Firestore update succeeds silently, so a caller who edits history never
21
+ * finds out, and a ledger whose rows can be edited cannot be used as
22
+ * evidence of anything. The correction mechanism is `reverse()`.
23
+ */
24
+ update(): Promise<never>;
25
+ delete(): Promise<never>;
26
+ /**
27
+ * Post one entry.
28
+ *
29
+ * 🛑 **The balance check and the lock check are INSIDE the transaction.**
30
+ * Reading the period's state first and writing after is a TOCTOU window: an
31
+ * admin filing a return between the two gets an entry in the period they
32
+ * just closed, which is the one outcome the lock exists to prevent. It is a
33
+ * narrow window and it is exactly the kind that shows up once, in the
34
+ * quarter it matters.
35
+ */
36
+ post(input: PostingInput): Promise<JournalEntryDocument>;
37
+ /**
38
+ * Reverse an entry.
39
+ *
40
+ * 🛑 **Refuses a SECOND reversal of the same entry.** Without the check a
41
+ * double-click posts the opposite legs twice, and the books then show the
42
+ * original plus two reversals — a net position that is the mirror image of
43
+ * the truth, balanced, and wrong. `reversedByEntryId` is set inside the
44
+ * same transaction that reads it, so the second attempt loses the race
45
+ * rather than both succeeding.
46
+ */
47
+ reverse(entryId: string, reason: string, at?: Date): Promise<JournalEntryDocument>;
48
+ /**
49
+ * Close a period.
50
+ *
51
+ * 🛑 Takes the reason and the actor, both required. "Who closed October
52
+ * and under which return" is the first question anyone asks when a figure
53
+ * disagrees with a filing, and a lock with no answer to it is a lock
54
+ * somebody will simply reopen.
55
+ */
56
+ lockPeriod(period: string, lockedByUid: string, lockedReason: string): Promise<void>;
57
+ /**
58
+ * 🛑 There is deliberately NO `unlockPeriod`.
59
+ *
60
+ * Reopening a period a return was filed from is not an operation the
61
+ * system should make easy — the correct response to a late correction is a
62
+ * reversing entry in the OPEN period, which is what `resolvePostingPeriod`
63
+ * computes and what every accountant expects. If a period genuinely must
64
+ * be reopened (a return withdrawn, say), that is a deliberate act with a
65
+ * paper trail outside this system, and a console write is the right amount
66
+ * of friction for it.
67
+ *
68
+ * This method exists only to say so. Deleting it would leave the absence
69
+ * looking like an oversight somebody helpfully fills in.
70
+ */
71
+ /**
72
+ * Every entry in a period, for a trial balance or a return.
73
+ *
74
+ * 🛑 **BOUNDED, and truncation is REPORTED rather than silent.** An
75
+ * unbounded read is the Rule #6 violation, but the reason it matters here
76
+ * is sharper than cost: a trial balance computed from a silently truncated
77
+ * page is a trial balance that BALANCES — both sides lose the same
78
+ * entries — while being wrong. It would look perfect and reconcile to
79
+ * nothing.
80
+ *
81
+ * So `truncated` is part of the return type and callers must handle it.
82
+ * The same stance `listPublicProducts` takes with its own `truncated`
83
+ * flag: a floor rendered as "50+", never a figure presented as a total.
84
+ *
85
+ * The real answer at volume is a period rollup written by a scheduled job
86
+ * — the `revenueRollup` pattern — and this is deliberately NOT that, so
87
+ * the flag is what forces the question rather than hiding it.
88
+ */
89
+ listByPeriod(period: string, limit?: number): Promise<{
90
+ entries: JournalEntryDocument[];
91
+ truncated: boolean;
92
+ }>;
93
+ /** Whether a period is closed. Absence means open — see `post`. */
94
+ isPeriodLocked(period: string): Promise<boolean>;
95
+ /**
96
+ * The cutover entry.
97
+ *
98
+ * 🛑 **NOT a backfill.** Pre-cutover data here is seeded and test, so
99
+ * replaying it would post fixtures as trade — the first financial year is
100
+ * therefore split across the old rollups and this ledger, and the
101
+ * "pre-ledger estimate" label on the old reports is what keeps that
102
+ * honest rather than hiding it.
103
+ *
104
+ * 🛑 Refuses to run twice. An opening balance posted a second time doubles
105
+ * every starting figure, and because both entries balance nothing
106
+ * downstream would object.
107
+ */
108
+ postOpeningBalances(at: Date, lines: PostingInput["lines"]): Promise<JournalEntryDocument>;
109
+ }
110
+ export declare const journalRepository: JournalRepository;
@@ -0,0 +1,312 @@
1
+ /*
2
+ * WHY: The only write path into the books. Every rule that makes a ledger
3
+ * trustworthy — balanced, append-only, period-locked — is enforced here
4
+ * rather than asked of callers, because a caller who gets one wrong
5
+ * produces a successful write and finds out at the next GST return.
6
+ *
7
+ * WHAT: JournalRepository — post, reverse, lock, read.
8
+ *
9
+ * @tag domain:ledger,finance
10
+ * @tag layer:repository
11
+ * @tag access:server
12
+ * @tag sideEffects:firestore
13
+ */
14
+ /* The DIRECTORY barrel, which is where every other repository gets this
15
+ * from — `providers/db-firebase/base` exports only `FirebaseRepository`,
16
+ * and `BaseRepository` (the one with `this.db` and `mapDoc`) is composed
17
+ * one level up. */
18
+ import { BaseRepository } from "../../../providers/db-firebase/index.js";
19
+ import { unitOfWork } from "../../../core/unit-of-work.js";
20
+ import { JOURNAL_ENTRIES_COLLECTION, ACCOUNTING_PERIODS_COLLECTION, LEDGER_FIELDS, } from "../schemas/firestore.js";
21
+ import { assertBalanced, buildReversal, periodOf, UnbalancedEntryError, } from "../../../_internal/shared/features/ledger/posting.js";
22
+ import { NotFoundError } from "../../../errors/not-found-error.js";
23
+ import { ValidationError } from "../../../errors/validation-error.js";
24
+ /**
25
+ * How many entries one period read may return.
26
+ *
27
+ * Sized against a plausible month rather than a round number: a few hundred
28
+ * orders, each posting two or three entries, plus settlements and expenses.
29
+ * 2,000 leaves real headroom and still fits comfortably inside a function's
30
+ * memory and the ~3-round-trip budget.
31
+ */
32
+ export const JOURNAL_PERIOD_SCAN_LIMIT = 2000;
33
+ export class JournalRepository extends BaseRepository {
34
+ /*
35
+ * 🛑 NO `SIEVE_FIELDS`, same reasoning as `obligations`. The reports read
36
+ * by period and by account code — two fixed shapes, both served by the
37
+ * automatic index — and an admin looking at a figure opens the ORDER it
38
+ * came from, not a filtered journal list. A Sieve surface nothing calls is
39
+ * the `route-schema-registry` pattern: 576 keys into a layer with no
40
+ * readers.
41
+ */
42
+ constructor() {
43
+ super(JOURNAL_ENTRIES_COLLECTION);
44
+ }
45
+ /**
46
+ * 🛑 APPEND-ONLY, and the method bodies refuse rather than a comment
47
+ * asking nicely.
48
+ *
49
+ * `inventoryMovements` takes the same stance for the same reason: a
50
+ * Firestore update succeeds silently, so a caller who edits history never
51
+ * finds out, and a ledger whose rows can be edited cannot be used as
52
+ * evidence of anything. The correction mechanism is `reverse()`.
53
+ */
54
+ async update() {
55
+ throw new Error("journalEntries is append-only. Correct an entry with reverse(), which posts the opposite legs and links the two — an edit would silently rewrite a period a GST return may already have been filed from.");
56
+ }
57
+ async delete() {
58
+ throw new Error("journalEntries is append-only. An entry that should not have been posted is reversed, not removed; the reversal is the record that it was wrong.");
59
+ }
60
+ /**
61
+ * Post one entry.
62
+ *
63
+ * 🛑 **The balance check and the lock check are INSIDE the transaction.**
64
+ * Reading the period's state first and writing after is a TOCTOU window: an
65
+ * admin filing a return between the two gets an entry in the period they
66
+ * just closed, which is the one outcome the lock exists to prevent. It is a
67
+ * narrow window and it is exactly the kind that shows up once, in the
68
+ * quarter it matters.
69
+ */
70
+ async post(input) {
71
+ const balanced = assertBalanced(input);
72
+ return unitOfWork.runTransaction(async (tx) => {
73
+ const db = this.db;
74
+ const periodRef = db
75
+ .collection(ACCOUNTING_PERIODS_COLLECTION)
76
+ .doc(`period-${balanced.period}`);
77
+ const snap = await tx.get(periodRef);
78
+ const period = snap.exists ? snap.data() : null;
79
+ /*
80
+ * 🛑 A period with NO row is OPEN, not closed.
81
+ *
82
+ * Fail-open here and fail-closed everywhere else is deliberate: a
83
+ * period is closed by someone filing a return, which is an action that
84
+ * writes a row. Treating absence as closed would mean the books refuse
85
+ * every posting until an admin opens each month by hand — and the
86
+ * first missed month silently stops the entire ingest, which looks
87
+ * like a broken job rather than a missing row.
88
+ */
89
+ if (period?.status === "locked") {
90
+ throw new ValidationError(`Period ${balanced.period} is closed and cannot accept new entries.`, {
91
+ period: period.lockedReason ??
92
+ "A return has been filed for this period. Post the correction to the open period instead.",
93
+ });
94
+ }
95
+ const ref = db.collection(JOURNAL_ENTRIES_COLLECTION).doc();
96
+ const now = new Date();
97
+ const doc = {
98
+ ...balanced,
99
+ id: ref.id,
100
+ createdAt: now,
101
+ updatedAt: now,
102
+ };
103
+ tx.set(ref, doc);
104
+ return doc;
105
+ });
106
+ }
107
+ /**
108
+ * Reverse an entry.
109
+ *
110
+ * 🛑 **Refuses a SECOND reversal of the same entry.** Without the check a
111
+ * double-click posts the opposite legs twice, and the books then show the
112
+ * original plus two reversals — a net position that is the mirror image of
113
+ * the truth, balanced, and wrong. `reversedByEntryId` is set inside the
114
+ * same transaction that reads it, so the second attempt loses the race
115
+ * rather than both succeeding.
116
+ */
117
+ async reverse(entryId, reason, at = new Date()) {
118
+ if (!reason.trim()) {
119
+ /*
120
+ * A reversal with no stated reason is the one thing a reader of the
121
+ * books cannot reconstruct later. The original entry says what was
122
+ * posted; only this says why it was wrong.
123
+ */
124
+ throw new ValidationError("A reversal needs a reason.", {
125
+ reason: "Say what was wrong with the original entry.",
126
+ });
127
+ }
128
+ return unitOfWork.runTransaction(async (tx) => {
129
+ const db = this.db;
130
+ const originalRef = db.collection(JOURNAL_ENTRIES_COLLECTION).doc(entryId);
131
+ const snap = await tx.get(originalRef);
132
+ if (!snap.exists)
133
+ throw new NotFoundError(`Journal entry ${entryId} not found.`);
134
+ const original = snap.data();
135
+ if (original.reversedByEntryId) {
136
+ throw new ValidationError(`Entry ${entryId} has already been reversed by ${original.reversedByEntryId}.`, { entryId: "Reversing it again would double the correction." });
137
+ }
138
+ if (original.sourceEvent === "reversal") {
139
+ /*
140
+ * Reversing a reversal is re-instating the original, and saying so
141
+ * is clearer than letting someone build a chain nobody can read. If
142
+ * the original was right after all, post it again with a narration
143
+ * that says why.
144
+ */
145
+ throw new ValidationError(`Entry ${entryId} is itself a reversal.`, { entryId: "To re-instate the original, post it again with a narration explaining why." });
146
+ }
147
+ const reversalInput = buildReversal({ id: entryId, lines: original.lines, narration: original.narration }, at, reason.trim());
148
+ const balanced = assertBalanced(reversalInput);
149
+ /*
150
+ * The reversal's OWN period must be open. It is `at`'s period, not the
151
+ * original's — see `buildReversal`, which deliberately does not
152
+ * back-date. A correction found after a return was filed posts to the
153
+ * open month and references the closed one.
154
+ */
155
+ const periodRef = db
156
+ .collection(ACCOUNTING_PERIODS_COLLECTION)
157
+ .doc(`period-${balanced.period}`);
158
+ const periodSnap = await tx.get(periodRef);
159
+ const period = periodSnap.exists
160
+ ? periodSnap.data()
161
+ : null;
162
+ if (period?.status === "locked") {
163
+ throw new ValidationError(`Period ${balanced.period} is closed, so the correction has nowhere to post.`, { period: "Open the current period before reversing." });
164
+ }
165
+ const ref = db.collection(JOURNAL_ENTRIES_COLLECTION).doc();
166
+ const now = new Date();
167
+ const reversal = {
168
+ ...balanced,
169
+ id: ref.id,
170
+ reversesEntryId: entryId,
171
+ createdAt: now,
172
+ updatedAt: now,
173
+ };
174
+ tx.set(ref, reversal);
175
+ /*
176
+ * 🛑 The back-link is written with `tx.update` on the ORIGINAL, which
177
+ * is the one edit to a posted entry this class permits — and it adds a
178
+ * pointer rather than changing a figure. Without it `reverse()` has no
179
+ * way to refuse a second attempt, and nothing reading the original can
180
+ * tell it has been corrected.
181
+ */
182
+ tx.update(originalRef, { reversedByEntryId: ref.id, updatedAt: now });
183
+ return reversal;
184
+ });
185
+ }
186
+ /**
187
+ * Close a period.
188
+ *
189
+ * 🛑 Takes the reason and the actor, both required. "Who closed October
190
+ * and under which return" is the first question anyone asks when a figure
191
+ * disagrees with a filing, and a lock with no answer to it is a lock
192
+ * somebody will simply reopen.
193
+ */
194
+ async lockPeriod(period, lockedByUid, lockedReason) {
195
+ if (!/^\d{4}-\d{2}$/.test(period)) {
196
+ throw new ValidationError(`"${period}" is not a YYYY-MM period.`, {
197
+ period: "Expected a month such as 2026-10.",
198
+ });
199
+ }
200
+ if (!lockedReason.trim()) {
201
+ throw new ValidationError("Closing a period needs a reason.", {
202
+ lockedReason: "Name the return this period was filed under.",
203
+ });
204
+ }
205
+ await unitOfWork.runTransaction(async (tx) => {
206
+ const db = this.db;
207
+ const ref = db.collection(ACCOUNTING_PERIODS_COLLECTION).doc(`period-${period}`);
208
+ const snap = await tx.get(ref);
209
+ if (snap.data()?.status === "locked") {
210
+ /*
211
+ * Idempotent rather than an error: a retried request must not read
212
+ * as a failure, and re-closing a closed period changes nothing.
213
+ */
214
+ return;
215
+ }
216
+ const now = new Date();
217
+ tx.set(ref, {
218
+ id: `period-${period}`,
219
+ period,
220
+ status: "locked",
221
+ lockedReason: lockedReason.trim(),
222
+ lockedAt: now,
223
+ lockedByUid,
224
+ createdAt: now,
225
+ updatedAt: now,
226
+ });
227
+ });
228
+ }
229
+ /**
230
+ * 🛑 There is deliberately NO `unlockPeriod`.
231
+ *
232
+ * Reopening a period a return was filed from is not an operation the
233
+ * system should make easy — the correct response to a late correction is a
234
+ * reversing entry in the OPEN period, which is what `resolvePostingPeriod`
235
+ * computes and what every accountant expects. If a period genuinely must
236
+ * be reopened (a return withdrawn, say), that is a deliberate act with a
237
+ * paper trail outside this system, and a console write is the right amount
238
+ * of friction for it.
239
+ *
240
+ * This method exists only to say so. Deleting it would leave the absence
241
+ * looking like an oversight somebody helpfully fills in.
242
+ */
243
+ /**
244
+ * Every entry in a period, for a trial balance or a return.
245
+ *
246
+ * 🛑 **BOUNDED, and truncation is REPORTED rather than silent.** An
247
+ * unbounded read is the Rule #6 violation, but the reason it matters here
248
+ * is sharper than cost: a trial balance computed from a silently truncated
249
+ * page is a trial balance that BALANCES — both sides lose the same
250
+ * entries — while being wrong. It would look perfect and reconcile to
251
+ * nothing.
252
+ *
253
+ * So `truncated` is part of the return type and callers must handle it.
254
+ * The same stance `listPublicProducts` takes with its own `truncated`
255
+ * flag: a floor rendered as "50+", never a figure presented as a total.
256
+ *
257
+ * The real answer at volume is a period rollup written by a scheduled job
258
+ * — the `revenueRollup` pattern — and this is deliberately NOT that, so
259
+ * the flag is what forces the question rather than hiding it.
260
+ */
261
+ async listByPeriod(period, limit = JOURNAL_PERIOD_SCAN_LIMIT) {
262
+ const snap = await this.db
263
+ .collection(JOURNAL_ENTRIES_COLLECTION)
264
+ .where(LEDGER_FIELDS.PERIOD, "==", period)
265
+ .orderBy(LEDGER_FIELDS.ENTRY_DATE)
266
+ .limit(limit + 1)
267
+ .get();
268
+ const docs = snap.docs.slice(0, limit);
269
+ return {
270
+ entries: docs.map((d) => this.mapDoc(d)),
271
+ truncated: snap.docs.length > limit,
272
+ };
273
+ }
274
+ /** Whether a period is closed. Absence means open — see `post`. */
275
+ async isPeriodLocked(period) {
276
+ const snap = await this.db
277
+ .collection(ACCOUNTING_PERIODS_COLLECTION)
278
+ .doc(`period-${period}`)
279
+ .get();
280
+ return snap.data()?.status === "locked";
281
+ }
282
+ /**
283
+ * The cutover entry.
284
+ *
285
+ * 🛑 **NOT a backfill.** Pre-cutover data here is seeded and test, so
286
+ * replaying it would post fixtures as trade — the first financial year is
287
+ * therefore split across the old rollups and this ledger, and the
288
+ * "pre-ledger estimate" label on the old reports is what keeps that
289
+ * honest rather than hiding it.
290
+ *
291
+ * 🛑 Refuses to run twice. An opening balance posted a second time doubles
292
+ * every starting figure, and because both entries balance nothing
293
+ * downstream would object.
294
+ */
295
+ async postOpeningBalances(at, lines) {
296
+ const existing = await this.db
297
+ .collection(JOURNAL_ENTRIES_COLLECTION)
298
+ .where(LEDGER_FIELDS.SOURCE_EVENT, "==", "opening-balance")
299
+ .limit(1)
300
+ .get();
301
+ if (!existing.empty) {
302
+ throw new UnbalancedEntryError("Opening balances have already been posted. Correct them with a reversing entry rather than posting a second opening.");
303
+ }
304
+ return this.post({
305
+ entryDate: at,
306
+ sourceEvent: "opening-balance",
307
+ narration: `Opening balances at cutover, ${periodOf(at)}`,
308
+ lines,
309
+ });
310
+ }
311
+ }
312
+ export const journalRepository = new JournalRepository();
@@ -0,0 +1,87 @@
1
+ /**
2
+ * `vendors` + `expenses` — money that leaves the business for something
3
+ * other than a payout or a refund.
4
+ *
5
+ * ## 🛑 A VENDOR IS AN IDENTITY, which `supplierName` never was
6
+ *
7
+ * `ShipmentDocument.supplierName` is free text, so four shipments from one
8
+ * supplier are four unjoined strings and "what have we spent with Acme" is
9
+ * unanswerable. That is not a reporting nicety: it is also the join that
10
+ * makes input GST claimable per supplier, which is what a GSTR-2B
11
+ * reconciliation needs.
12
+ *
13
+ * ## 🛑 An expense is TRUTH; its journal entry is the projection
14
+ *
15
+ * Same split as orders and payouts. The expense row is what somebody
16
+ * actually paid for, with the bill attached; the ledger says how it was
17
+ * classified. Re-classifying an expense posts a reversal and a new entry —
18
+ * it never edits the old one — so the two stay reconcilable.
19
+ */
20
+ import type { BaseDocument } from "../../../_internal/shared/types/base-document";
21
+ export declare const VENDORS_COLLECTION = "vendors";
22
+ export declare const EXPENSES_COLLECTION = "expenses";
23
+ export interface VendorDocument extends BaseDocument {
24
+ /** `vendor-{slug}` — a pure slug, like every other named entity here. */
25
+ name: string;
26
+ /**
27
+ * 🛑 The GSTIN is what makes input tax claimable, and it is NOT PII.
28
+ *
29
+ * It is a business registration number, printed on every invoice the
30
+ * vendor issues and published in the GST portal's public search. Treating
31
+ * it as PII would mean encrypting the one field a GSTR-2B reconciliation
32
+ * has to match on — which would make the reconciliation impossible for no
33
+ * privacy gain.
34
+ */
35
+ gstin?: string;
36
+ /**
37
+ * Where the vendor is registered, for the place-of-supply rule.
38
+ *
39
+ * An intra-state purchase splits input tax into CGST + SGST and an
40
+ * inter-state one is IGST; they are different ledger accounts and a wrong
41
+ * one is a wrong return. Optional because a vendor with no GSTIN charges
42
+ * no GST and the question does not arise.
43
+ */
44
+ stateCode?: string;
45
+ notes?: string;
46
+ isActive: boolean;
47
+ }
48
+ /**
49
+ * What the money was for. Drives the expense account the posting hits.
50
+ *
51
+ * 🛑 A closed union rather than free text, and `other` is a real member
52
+ * precisely so the exception stays countable — "how much of our spend is
53
+ * unclassified" is one query, and a growing answer is the signal to add a
54
+ * category rather than a reason to allow typing one.
55
+ */
56
+ export type ExpenseCategory = "freight" | "platform-software" | "wages" | "packaging" | "asset" | "other";
57
+ export interface ExpenseDocument extends BaseDocument {
58
+ vendorId?: string;
59
+ /** As written on the bill, for the same reason `supplierName` survives. */
60
+ vendorName: string;
61
+ category: ExpenseCategory;
62
+ /** The date on the BILL, not when it was entered — the period follows this. */
63
+ billDate: Date;
64
+ billNumber?: string;
65
+ /** Excluding tax. */
66
+ amount: number;
67
+ /**
68
+ * Input GST on this bill, claimable as ITC.
69
+ *
70
+ * 🛑 Stored SEPARATELY from `amount` rather than as a rate, because the
71
+ * figure on the bill is authoritative and re-deriving it from a
72
+ * percentage reproduces the supplier's rounding wrongly about as often as
73
+ * it reproduces it rightly — and the difference is what a GSTR-2B
74
+ * mismatch is made of.
75
+ */
76
+ inputGst?: number;
77
+ /** Set once posted, so a re-run cannot double-post. */
78
+ journalEntryId?: string;
79
+ attachmentUrl?: string;
80
+ recordedByUid: string;
81
+ }
82
+ export declare const EXPENSE_FIELDS: {
83
+ readonly VENDOR_ID: "vendorId";
84
+ readonly CATEGORY: "category";
85
+ readonly BILL_DATE: "billDate";
86
+ readonly JOURNAL_ENTRY_ID: "journalEntryId";
87
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `vendors` + `expenses` — money that leaves the business for something
3
+ * other than a payout or a refund.
4
+ *
5
+ * ## 🛑 A VENDOR IS AN IDENTITY, which `supplierName` never was
6
+ *
7
+ * `ShipmentDocument.supplierName` is free text, so four shipments from one
8
+ * supplier are four unjoined strings and "what have we spent with Acme" is
9
+ * unanswerable. That is not a reporting nicety: it is also the join that
10
+ * makes input GST claimable per supplier, which is what a GSTR-2B
11
+ * reconciliation needs.
12
+ *
13
+ * ## 🛑 An expense is TRUTH; its journal entry is the projection
14
+ *
15
+ * Same split as orders and payouts. The expense row is what somebody
16
+ * actually paid for, with the bill attached; the ledger says how it was
17
+ * classified. Re-classifying an expense posts a reversal and a new entry —
18
+ * it never edits the old one — so the two stay reconcilable.
19
+ */
20
+ export const VENDORS_COLLECTION = "vendors";
21
+ export const EXPENSES_COLLECTION = "expenses";
22
+ export const EXPENSE_FIELDS = {
23
+ VENDOR_ID: "vendorId",
24
+ CATEGORY: "category",
25
+ BILL_DATE: "billDate",
26
+ JOURNAL_ENTRY_ID: "journalEntryId",
27
+ };