@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,91 @@
1
+ /**
2
+ * Pay periods for employee payouts — ONE resolver, two cadences.
3
+ *
4
+ * ## 🛑 Why this is a module and not a ternary at each call site
5
+ *
6
+ * Two cadences means two period-boundary rules, and a reopened ticket can
7
+ * straddle a boundary differently per employee: the same reopen is inside the
8
+ * period for a monthly payee and across it for a weekly one. A branch written
9
+ * at each call site is two chances per site to disagree about where a week
10
+ * starts — and the symptom is a payslip that is quietly short, which nobody
11
+ * reports as a bug because nobody has the other number to compare it to.
12
+ *
13
+ * So the period is resolved once, from a date and a cadence, and every caller
14
+ * takes the pair it returns.
15
+ *
16
+ * ## 🛑 Go-forward, no clawback
17
+ *
18
+ * A payslip is final once issued. If a ticket is reopened after the period it
19
+ * was paid in has closed, the money is not recovered and the period is not
20
+ * recomputed — the ticket simply does not count again in the period where it
21
+ * is next resolved. Recomputing a closed period would mean an employee's pay
22
+ * changing after they were told what it was, which is a worse failure than
23
+ * occasionally paying for work that was later reopened.
24
+ */
25
+ export type PayCadence = "weekly" | "monthly";
26
+ export declare const PAY_CADENCE_VALUES: readonly ["weekly", "monthly"];
27
+ export interface PayPeriod {
28
+ start: Date;
29
+ /** EXCLUSIVE. See the note below — this is the half-open convention. */
30
+ end: Date;
31
+ cadence: PayCadence;
32
+ }
33
+ /**
34
+ * The period containing `when`.
35
+ *
36
+ * ## 🛑 Half-open: `start <= t < end`
37
+ *
38
+ * An inclusive end means the last millisecond of a period is also the first of
39
+ * the next, so a ticket resolved exactly at midnight is counted twice — paid
40
+ * in two periods — or, if one side uses `<` and the other `<=`, in neither.
41
+ * Half-open makes both impossible by construction, and every consumer must
42
+ * compare with `< end`, never `<= end`.
43
+ *
44
+ * ## 🛑 Weeks start MONDAY
45
+ *
46
+ * `Date.getDay()` returns 0 for Sunday, so the naive `d - d.getDay()` produces
47
+ * a Sunday-start week. India's working week is Monday–Saturday and a support
48
+ * rota is read that way, so a Sunday boundary would split every weekend across
49
+ * two payslips. The `(day + 6) % 7` shift is what makes Monday day 0.
50
+ *
51
+ * ## 🛑 Local midnight, not UTC
52
+ *
53
+ * Wages are a human-calendar concept: an employee's Monday is their Monday.
54
+ * `setHours(0,0,0,0)` is local, and the server runs in the deployment's zone —
55
+ * which is why `periodLabel` renders the same boundary a payslip prints.
56
+ */
57
+ export declare function payPeriodFor(when: Date, cadence: PayCadence): PayPeriod;
58
+ /** The period immediately before the one containing `when`. */
59
+ export declare function previousPayPeriod(when: Date, cadence: PayCadence): PayPeriod;
60
+ /** Half-open membership. The ONLY correct way to ask; see `payPeriodFor`. */
61
+ export declare function isWithinPeriod(t: Date, period: PayPeriod): boolean;
62
+ /**
63
+ * What a payslip prints.
64
+ *
65
+ * 🛑 Renders the end as the LAST DAY INSIDE the period, not the exclusive
66
+ * boundary. "1 Jan – 1 Feb" on a January payslip reads as an overlap with
67
+ * February's and invites someone to "fix" the half-open convention that makes
68
+ * the arithmetic correct.
69
+ */
70
+ export declare function periodLabel(period: PayPeriod, locale?: string): string;
71
+ /**
72
+ * The tickets a period actually pays for.
73
+ *
74
+ * 🛑 **Tickets STILL RESOLVED at period end — not tickets closed during it.**
75
+ * Paying per ticket closed rewards closing rather than resolving: close it,
76
+ * let the buyer reopen it, close it again, and it pays twice. So a ticket
77
+ * counts only when it is resolved, was resolved inside the period, and is
78
+ * still resolved when the period ends.
79
+ *
80
+ * A reopen therefore removes the ticket from that period even if the reopen
81
+ * happened after the resolution — which is why this takes the whole ticket
82
+ * rather than a timestamp, and why `resolvedBy` must be CLEARED on reopen.
83
+ */
84
+ export interface PayableTicket {
85
+ id: string;
86
+ resolvedAt?: Date | null;
87
+ resolvedBy?: string | null;
88
+ /** The live status, read at the moment the period is computed. */
89
+ isResolved: boolean;
90
+ }
91
+ export declare function resolvedTicketsForPeriod(tickets: readonly PayableTicket[], employeeUid: string, period: PayPeriod): string[];
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Pay periods for employee payouts — ONE resolver, two cadences.
3
+ *
4
+ * ## 🛑 Why this is a module and not a ternary at each call site
5
+ *
6
+ * Two cadences means two period-boundary rules, and a reopened ticket can
7
+ * straddle a boundary differently per employee: the same reopen is inside the
8
+ * period for a monthly payee and across it for a weekly one. A branch written
9
+ * at each call site is two chances per site to disagree about where a week
10
+ * starts — and the symptom is a payslip that is quietly short, which nobody
11
+ * reports as a bug because nobody has the other number to compare it to.
12
+ *
13
+ * So the period is resolved once, from a date and a cadence, and every caller
14
+ * takes the pair it returns.
15
+ *
16
+ * ## 🛑 Go-forward, no clawback
17
+ *
18
+ * A payslip is final once issued. If a ticket is reopened after the period it
19
+ * was paid in has closed, the money is not recovered and the period is not
20
+ * recomputed — the ticket simply does not count again in the period where it
21
+ * is next resolved. Recomputing a closed period would mean an employee's pay
22
+ * changing after they were told what it was, which is a worse failure than
23
+ * occasionally paying for work that was later reopened.
24
+ */
25
+ export const PAY_CADENCE_VALUES = ["weekly", "monthly"];
26
+ /** A cadence absent here is a compile error, not a silently unpayable employee. */
27
+ const _CADENCE_COVERS_UNION = { weekly: true, monthly: true };
28
+ void _CADENCE_COVERS_UNION;
29
+ /**
30
+ * The period containing `when`.
31
+ *
32
+ * ## 🛑 Half-open: `start <= t < end`
33
+ *
34
+ * An inclusive end means the last millisecond of a period is also the first of
35
+ * the next, so a ticket resolved exactly at midnight is counted twice — paid
36
+ * in two periods — or, if one side uses `<` and the other `<=`, in neither.
37
+ * Half-open makes both impossible by construction, and every consumer must
38
+ * compare with `< end`, never `<= end`.
39
+ *
40
+ * ## 🛑 Weeks start MONDAY
41
+ *
42
+ * `Date.getDay()` returns 0 for Sunday, so the naive `d - d.getDay()` produces
43
+ * a Sunday-start week. India's working week is Monday–Saturday and a support
44
+ * rota is read that way, so a Sunday boundary would split every weekend across
45
+ * two payslips. The `(day + 6) % 7` shift is what makes Monday day 0.
46
+ *
47
+ * ## 🛑 Local midnight, not UTC
48
+ *
49
+ * Wages are a human-calendar concept: an employee's Monday is their Monday.
50
+ * `setHours(0,0,0,0)` is local, and the server runs in the deployment's zone —
51
+ * which is why `periodLabel` renders the same boundary a payslip prints.
52
+ */
53
+ export function payPeriodFor(when, cadence) {
54
+ const start = new Date(when.getTime());
55
+ start.setHours(0, 0, 0, 0);
56
+ if (cadence === "weekly") {
57
+ const mondayIndex = (start.getDay() + 6) % 7;
58
+ start.setDate(start.getDate() - mondayIndex);
59
+ const end = new Date(start.getTime());
60
+ end.setDate(end.getDate() + 7);
61
+ return { start, end, cadence };
62
+ }
63
+ start.setDate(1);
64
+ const end = new Date(start.getTime());
65
+ /*
66
+ * `setMonth(+1)` on the 1st is safe; it is only unsafe on day 29–31, which
67
+ * `setDate(1)` above has already removed. Doing it the other way round is
68
+ * the classic bug — 31 January + 1 month is 3 March.
69
+ */
70
+ end.setMonth(end.getMonth() + 1);
71
+ return { start, end, cadence };
72
+ }
73
+ /** The period immediately before the one containing `when`. */
74
+ export function previousPayPeriod(when, cadence) {
75
+ const current = payPeriodFor(when, cadence);
76
+ const justBefore = new Date(current.start.getTime() - 1);
77
+ return payPeriodFor(justBefore, cadence);
78
+ }
79
+ /** Half-open membership. The ONLY correct way to ask; see `payPeriodFor`. */
80
+ export function isWithinPeriod(t, period) {
81
+ const ms = t.getTime();
82
+ return ms >= period.start.getTime() && ms < period.end.getTime();
83
+ }
84
+ /**
85
+ * What a payslip prints.
86
+ *
87
+ * 🛑 Renders the end as the LAST DAY INSIDE the period, not the exclusive
88
+ * boundary. "1 Jan – 1 Feb" on a January payslip reads as an overlap with
89
+ * February's and invites someone to "fix" the half-open convention that makes
90
+ * the arithmetic correct.
91
+ */
92
+ export function periodLabel(period, locale = "en-IN") {
93
+ const lastDay = new Date(period.end.getTime() - 1);
94
+ const fmt = (d) => d.toLocaleDateString(locale, { dateStyle: "medium" });
95
+ return `${fmt(period.start)} – ${fmt(lastDay)}`;
96
+ }
97
+ export function resolvedTicketsForPeriod(tickets, employeeUid, period) {
98
+ return tickets
99
+ .filter((t) => t.isResolved &&
100
+ t.resolvedBy === employeeUid &&
101
+ t.resolvedAt != null &&
102
+ isWithinPeriod(t.resolvedAt, period))
103
+ .map((t) => t.id);
104
+ }
@@ -22,14 +22,7 @@
22
22
  * discounts they never agreed to. A tri-state would be worse still — a buyer
23
23
  * cannot tell a seller who declined from one who has not been asked.
24
24
  */
25
- /**
26
- * The platform's own storefront.
27
- *
28
- * Hard-coded deliberately: this is an identity, not a setting. It is the same
29
- * id `realListingsOnly` and the seed's `REAL_STORE_IDS` already treat as the
30
- * platform's own.
31
- */
32
- export declare const PLATFORM_STORE_ID = "store-letitrip-official";
25
+ export { PLATFORM_STORE_ID } from "../../../../features/stores/schemas/firestore";
33
26
  export declare function storeAcceptsPoints(store: {
34
27
  id?: string;
35
28
  acceptsPoints?: boolean;
@@ -22,14 +22,20 @@
22
22
  * discounts they never agreed to. A tri-state would be worse still — a buyer
23
23
  * cannot tell a seller who declined from one who has not been asked.
24
24
  */
25
- /**
26
- * The platform's own storefront.
25
+ /*
26
+ * 🛑 `PLATFORM_STORE_ID` is DECLARED in `features/stores/schemas/firestore.ts`
27
+ * and re-exported here, not defined here.
27
28
  *
28
- * Hard-coded deliberately: this is an identity, not a setting. It is the same
29
- * id `realListingsOnly` and the seed's `REAL_STORE_IDS` already treat as the
30
- * platform's own.
29
+ * It is still an identity rather than a setting — that reasoning is unchanged
30
+ * and is recorded at the declaration. What moved, and why: a brand-bearing
31
+ * literal inside `_internal/` is what `audit-ssr-in-appkit` Rule 3 blocks,
32
+ * because a consumer of appkit would inherit OUR store id with no seam to
33
+ * override it. The stores schema is also where the matching `acceptsPoints`
34
+ * docstring already explains the always-accepts rule, so the constant and its
35
+ * justification now sit together.
31
36
  */
32
- export const PLATFORM_STORE_ID = "store-letitrip-official";
37
+ export { PLATFORM_STORE_ID } from "../../../../features/stores/schemas/firestore.js";
38
+ import { PLATFORM_STORE_ID } from "../../../../features/stores/schemas/firestore.js";
33
39
  export function storeAcceptsPoints(store, storeId) {
34
40
  /*
35
41
  * The id is taken from either source so a caller holding only an id (the
@@ -0,0 +1,150 @@
1
+ /**
2
+ * The pricing registry — every rate the platform charges, in one place, with
3
+ * its tier and who may change it.
4
+ *
5
+ * ## Why this exists
6
+ *
7
+ * Before it, a rate was a field on `siteSettings` and nothing said what KIND
8
+ * of thing it was. A GST percentage, a policy ceiling the owner sets, and an
9
+ * operational commission a back-office agent tunes all looked identical in the
10
+ * schema and in the admin form — so the only thing standing between an agent
11
+ * and editing a statutory tax rate was that nobody had tried.
12
+ *
13
+ * It is also the destination `audit-money-paths`' R6 needs. That rule is
14
+ * specified as "a percentage or fee literal in logic, not in the pricing
15
+ * registry", and was deliberately NOT implemented in 9a because the registry
16
+ * did not exist: a rule whose remedy is "move it somewhere that isn't there"
17
+ * can only produce noise.
18
+ *
19
+ * ## 🛑 Three tiers, and the top one is NOT editable here at all
20
+ *
21
+ * | Tier | Who | Why |
22
+ * |---|---|---|
23
+ * | `statutory` | **nobody** | A GST rate is not ours to choose. It is read from `/admin/tax-codes`, which is sourced per HSN, and a writable copy here would be a second place for a filed tax figure to disagree with itself |
24
+ * | `policy` | admin only, reason required | A ceiling the owner decides — `platformFeeMax`, the OTP threshold, `minPayoutAmount`. Changing one changes what the business promises, not what it charges today |
25
+ * | `operational` | back-office | Commission, COD handling, the add-on fees. Tuned, audited, and reversible |
26
+ *
27
+ * ## 🛑 EVERY CHANGE IS FORWARD-ONLY. This is the load-bearing rule.
28
+ *
29
+ * A rate answers *"what do we charge from now on"* and never *"what did we
30
+ * charge"*. Historical paths read the record's own snapshot — `order.gst`,
31
+ * `order.platformFee`, `appliedDiscounts` — which is why those fields are
32
+ * snapshotted at all, and `audit-money-paths`' R7 blocks a live read in a
33
+ * payout, invoice or GSTR-1 path.
34
+ *
35
+ * Without that, lowering a commission would silently restate every historical
36
+ * payout, and an invoice reprinted after a GST change would state a tax that
37
+ * was never charged. **Invoices are FILED.** The failure is not a wrong screen;
38
+ * it is a wrong return.
39
+ *
40
+ * ## 🛑 A rate appearing here does NOT make it public
41
+ *
42
+ * `gatewayFeePercent`, `payoutHoldDays`, `minPayoutAmount` and
43
+ * `surchargeSellerSharePercent` are seller-side economics and stay in
44
+ * `PRIVATE_*_FIELDS` (Root Cause #70 — a deny-list projection once shipped all
45
+ * 25 commission fields to anonymous callers). This registry describes an ADMIN
46
+ * surface. Buyer-facing display goes through `toBuyerFacingFees()` /
47
+ * `toBuyerEmiSettings()`, and `audit-guest-price-leak`'s R6 blocks a seller
48
+ * rate reaching a buyer path.
49
+ */
50
+ /** Who may change a rate, and what the change costs in ceremony. */
51
+ export type RateTier = "statutory" | "policy" | "operational";
52
+ /**
53
+ * Render order: most constrained first.
54
+ *
55
+ * 🛑 A runtime array with the type DERIVED from it would be the usual pattern
56
+ * here, but it is inverted on purpose — `RateTier` is the union the registry
57
+ * and `canEditRate` switch on, and this array is only a presentation order.
58
+ * Typing it `readonly RateTier[]` makes a missing tier a silent omission from
59
+ * the page rather than a compile error, so it is `Record`-checked below.
60
+ */
61
+ export declare const RATE_TIER_ORDER: readonly ["statutory", "policy", "operational"];
62
+ /** How to read and render the number. */
63
+ export type RateUnit = "percent" | "rupees" | "days" | "minutes" | "count";
64
+ export interface RateDescriptor {
65
+ /**
66
+ * Dotted path inside `siteSettings`, e.g. `commissions.platformFeePercent`.
67
+ *
68
+ * 🛑 The path IS the identity. A registry keyed on a prettier id would be a
69
+ * second name for one field, and the two would drift the first time a field
70
+ * moved — the mirror problem (Root Cause #42) applied to money.
71
+ */
72
+ path: string;
73
+ label: string;
74
+ /** What this rate actually does, in the operator's words rather than the field's. */
75
+ description: string;
76
+ tier: RateTier;
77
+ unit: RateUnit;
78
+ /**
79
+ * `true` when the value is seller-side economics.
80
+ *
81
+ * Not a display hint — it is what keeps `audit-guest-price-leak` honest
82
+ * about which rates may never reach a buyer-facing path.
83
+ */
84
+ sellerOnly?: boolean;
85
+ }
86
+ /**
87
+ * Every rate, in the order an operator would look for them.
88
+ *
89
+ * 🛑 **Statutory rates are deliberately ABSENT rather than listed read-only.**
90
+ *
91
+ * The first draft included GST as `tier: "statutory", editable: false`, and
92
+ * that is the worse design: a read-only row on this page is a SECOND place GST
93
+ * appears, and the day the two disagree the page is the one an operator
94
+ * believes. `/admin/tax-codes` owns per-HSN rates, sourced and effective-dated
95
+ * already. This page links there instead of mirroring it.
96
+ *
97
+ * The tier remains in the union because the DISTINCTION is what the page has
98
+ * to teach — and because a future statutory value that genuinely has no other
99
+ * home needs somewhere honest to go.
100
+ */
101
+ export declare const RATE_REGISTRY: readonly RateDescriptor[];
102
+ /** Lookup by path. `undefined` for a field nobody registered. */
103
+ export declare function rateByPath(path: string): RateDescriptor | undefined;
104
+ /**
105
+ * Who may change this rate.
106
+ *
107
+ * 🛑 `statutory` returns `false` for EVERYONE, admins included. That is not a
108
+ * permission check that happens to exclude admins — it is the statement that
109
+ * this value is not ours to choose. An admin who needs a different GST rate
110
+ * needs a different HSN code, which is a different screen and a different
111
+ * kind of decision.
112
+ */
113
+ export declare function canEditRate(tier: RateTier, isAdmin: boolean): boolean;
114
+ /**
115
+ * One recorded rate change. Appended, never overwritten.
116
+ *
117
+ * ## 🛑 `effectiveFrom` is forward-only and the WRITE must enforce it
118
+ *
119
+ * A backdated rate is indistinguishable from a correction in the data and
120
+ * catastrophic in effect: every historical read that honours effective dates
121
+ * would restate figures already invoiced and already paid out. The field exists
122
+ * so a change can be SCHEDULED, not so history can be edited.
123
+ *
124
+ * `reason` is required on a `policy` change and optional otherwise, which is
125
+ * the whole difference in ceremony between the two tiers.
126
+ */
127
+ export interface RateChange {
128
+ path: string;
129
+ previousValue: number | null;
130
+ newValue: number;
131
+ /** Never earlier than the moment of the write. */
132
+ effectiveFrom: Date;
133
+ changedByUid: string;
134
+ changedByName: string;
135
+ reason?: string;
136
+ changedAt: Date;
137
+ }
138
+ /**
139
+ * Is this `effectiveFrom` acceptable?
140
+ *
141
+ * Returns a MESSAGE rather than a boolean so the caller can say which rule was
142
+ * broken — "that date is in the past" and "nothing to change" are different
143
+ * corrections, and a bare `false` makes the form guess.
144
+ */
145
+ export declare function rejectRateChange(input: {
146
+ effectiveFrom: Date;
147
+ previousValue: number | null;
148
+ newValue: number;
149
+ reason?: string;
150
+ }, tier: RateTier, now: Date): string | null;
@@ -0,0 +1,246 @@
1
+ /**
2
+ * The pricing registry — every rate the platform charges, in one place, with
3
+ * its tier and who may change it.
4
+ *
5
+ * ## Why this exists
6
+ *
7
+ * Before it, a rate was a field on `siteSettings` and nothing said what KIND
8
+ * of thing it was. A GST percentage, a policy ceiling the owner sets, and an
9
+ * operational commission a back-office agent tunes all looked identical in the
10
+ * schema and in the admin form — so the only thing standing between an agent
11
+ * and editing a statutory tax rate was that nobody had tried.
12
+ *
13
+ * It is also the destination `audit-money-paths`' R6 needs. That rule is
14
+ * specified as "a percentage or fee literal in logic, not in the pricing
15
+ * registry", and was deliberately NOT implemented in 9a because the registry
16
+ * did not exist: a rule whose remedy is "move it somewhere that isn't there"
17
+ * can only produce noise.
18
+ *
19
+ * ## 🛑 Three tiers, and the top one is NOT editable here at all
20
+ *
21
+ * | Tier | Who | Why |
22
+ * |---|---|---|
23
+ * | `statutory` | **nobody** | A GST rate is not ours to choose. It is read from `/admin/tax-codes`, which is sourced per HSN, and a writable copy here would be a second place for a filed tax figure to disagree with itself |
24
+ * | `policy` | admin only, reason required | A ceiling the owner decides — `platformFeeMax`, the OTP threshold, `minPayoutAmount`. Changing one changes what the business promises, not what it charges today |
25
+ * | `operational` | back-office | Commission, COD handling, the add-on fees. Tuned, audited, and reversible |
26
+ *
27
+ * ## 🛑 EVERY CHANGE IS FORWARD-ONLY. This is the load-bearing rule.
28
+ *
29
+ * A rate answers *"what do we charge from now on"* and never *"what did we
30
+ * charge"*. Historical paths read the record's own snapshot — `order.gst`,
31
+ * `order.platformFee`, `appliedDiscounts` — which is why those fields are
32
+ * snapshotted at all, and `audit-money-paths`' R7 blocks a live read in a
33
+ * payout, invoice or GSTR-1 path.
34
+ *
35
+ * Without that, lowering a commission would silently restate every historical
36
+ * payout, and an invoice reprinted after a GST change would state a tax that
37
+ * was never charged. **Invoices are FILED.** The failure is not a wrong screen;
38
+ * it is a wrong return.
39
+ *
40
+ * ## 🛑 A rate appearing here does NOT make it public
41
+ *
42
+ * `gatewayFeePercent`, `payoutHoldDays`, `minPayoutAmount` and
43
+ * `surchargeSellerSharePercent` are seller-side economics and stay in
44
+ * `PRIVATE_*_FIELDS` (Root Cause #70 — a deny-list projection once shipped all
45
+ * 25 commission fields to anonymous callers). This registry describes an ADMIN
46
+ * surface. Buyer-facing display goes through `toBuyerFacingFees()` /
47
+ * `toBuyerEmiSettings()`, and `audit-guest-price-leak`'s R6 blocks a seller
48
+ * rate reaching a buyer path.
49
+ */
50
+ /**
51
+ * Render order: most constrained first.
52
+ *
53
+ * 🛑 A runtime array with the type DERIVED from it would be the usual pattern
54
+ * here, but it is inverted on purpose — `RateTier` is the union the registry
55
+ * and `canEditRate` switch on, and this array is only a presentation order.
56
+ * Typing it `readonly RateTier[]` makes a missing tier a silent omission from
57
+ * the page rather than a compile error, so it is `Record`-checked below.
58
+ */
59
+ export const RATE_TIER_ORDER = ["statutory", "policy", "operational"];
60
+ /** A tier absent from `RATE_TIER_ORDER` is a compile error, not a missing section. */
61
+ const _TIER_ORDER_COVERS_UNION = {
62
+ statutory: true,
63
+ policy: true,
64
+ operational: true,
65
+ };
66
+ void _TIER_ORDER_COVERS_UNION;
67
+ /**
68
+ * Every rate, in the order an operator would look for them.
69
+ *
70
+ * 🛑 **Statutory rates are deliberately ABSENT rather than listed read-only.**
71
+ *
72
+ * The first draft included GST as `tier: "statutory", editable: false`, and
73
+ * that is the worse design: a read-only row on this page is a SECOND place GST
74
+ * appears, and the day the two disagree the page is the one an operator
75
+ * believes. `/admin/tax-codes` owns per-HSN rates, sourced and effective-dated
76
+ * already. This page links there instead of mirroring it.
77
+ *
78
+ * The tier remains in the union because the DISTINCTION is what the page has
79
+ * to teach — and because a future statutory value that genuinely has no other
80
+ * home needs somewhere honest to go.
81
+ */
82
+ export const RATE_REGISTRY = [
83
+ // ── Policy ceilings: the owner's decisions ──────────────────────────────
84
+ {
85
+ path: "commissions.platformFeeMax",
86
+ label: "Platform fee cap",
87
+ description: "The most we take from one checkout, before GST on it. Applied before the tax, because GST is levied on the commission actually charged.",
88
+ tier: "policy",
89
+ unit: "rupees",
90
+ },
91
+ {
92
+ path: "payment.otpCheckoutThreshold",
93
+ label: "OTP threshold",
94
+ description: "Order value above which a buyer must confirm by one-time code. Measured on the WHOLE checkout, not each resulting per-seller order — otherwise a big cart dodges it by spanning sellers.",
95
+ tier: "policy",
96
+ unit: "rupees",
97
+ },
98
+ {
99
+ path: "payment.codMaxOrderValue",
100
+ label: "COD ceiling",
101
+ description: "The largest order we will hand over against cash on delivery.",
102
+ tier: "policy",
103
+ unit: "rupees",
104
+ },
105
+ {
106
+ path: "commissions.minPayoutAmount",
107
+ label: "Minimum payout",
108
+ description: "A seller's balance must reach this before a payout runs.",
109
+ tier: "policy",
110
+ unit: "rupees",
111
+ sellerOnly: true,
112
+ },
113
+ {
114
+ path: "commissions.payoutHoldDays",
115
+ label: "Payout hold",
116
+ description: "How long a seller's money waits after delivery, so a dispute can still be raised against it.",
117
+ tier: "policy",
118
+ unit: "days",
119
+ sellerOnly: true,
120
+ },
121
+ // ── Operational: tuned, audited, reversible ─────────────────────────────
122
+ {
123
+ path: "commissions.platformFeePercent",
124
+ label: "Platform commission",
125
+ description: "Our cut of a checkout, before the cap above is applied.",
126
+ tier: "operational",
127
+ unit: "percent",
128
+ },
129
+ {
130
+ path: "commissions.minimumTransactionFee",
131
+ label: "Minimum transaction fee",
132
+ description: "The floor under the commission, so a very small order still covers its own cost.",
133
+ tier: "operational",
134
+ unit: "rupees",
135
+ },
136
+ {
137
+ path: "commissions.gatewayFeePercent",
138
+ label: "Gateway fee",
139
+ description: "What the payment provider takes. Recorded so a payout can account for it.",
140
+ tier: "operational",
141
+ unit: "percent",
142
+ sellerOnly: true,
143
+ },
144
+ {
145
+ path: "commissions.codHandlingFeePercent",
146
+ label: "COD handling fee",
147
+ description: "Charged to the buyer for paying cash on delivery.",
148
+ tier: "operational",
149
+ unit: "percent",
150
+ },
151
+ {
152
+ path: "commissions.codHandlingFeeCap",
153
+ label: "COD handling cap",
154
+ description: "The most the COD handling fee can reach on one order.",
155
+ tier: "operational",
156
+ unit: "rupees",
157
+ },
158
+ {
159
+ path: "commissions.codDepositPercent",
160
+ label: "COD token",
161
+ description: "Paid up front on a cash-on-delivery order; the rest is collected at the door.",
162
+ tier: "operational",
163
+ unit: "percent",
164
+ },
165
+ {
166
+ path: "commissions.giftWrapFee",
167
+ label: "Gift wrap",
168
+ description: "Per-store add-on. Charged once per seller in the cart, never once per cart.",
169
+ tier: "operational",
170
+ unit: "rupees",
171
+ },
172
+ {
173
+ path: "commissions.shipmentProtectionFeePercent",
174
+ label: "Shipment protection",
175
+ description: "Transit cover, charged per store. Claimable within 48 hours of delivery.",
176
+ tier: "operational",
177
+ unit: "percent",
178
+ },
179
+ {
180
+ path: "commissions.shipmentProtectionFeeMin",
181
+ label: "Shipment protection floor",
182
+ description: "The minimum transit-cover premium on any one order.",
183
+ tier: "operational",
184
+ unit: "rupees",
185
+ },
186
+ {
187
+ path: "commissions.saleProtectionFeePercent",
188
+ label: "Sale protection",
189
+ description: "Cover against the sale itself going wrong. A different window from transit cover.",
190
+ tier: "operational",
191
+ unit: "percent",
192
+ },
193
+ {
194
+ path: "commissions.saleProtectionFeeMin",
195
+ label: "Sale protection floor",
196
+ description: "The minimum sale-cover premium on any one order.",
197
+ tier: "operational",
198
+ unit: "rupees",
199
+ },
200
+ ];
201
+ /** Lookup by path. `undefined` for a field nobody registered. */
202
+ export function rateByPath(path) {
203
+ return RATE_REGISTRY.find((r) => r.path === path);
204
+ }
205
+ /**
206
+ * Who may change this rate.
207
+ *
208
+ * 🛑 `statutory` returns `false` for EVERYONE, admins included. That is not a
209
+ * permission check that happens to exclude admins — it is the statement that
210
+ * this value is not ours to choose. An admin who needs a different GST rate
211
+ * needs a different HSN code, which is a different screen and a different
212
+ * kind of decision.
213
+ */
214
+ export function canEditRate(tier, isAdmin) {
215
+ if (tier === "statutory")
216
+ return false;
217
+ if (tier === "policy")
218
+ return isAdmin;
219
+ return true;
220
+ }
221
+ /**
222
+ * Is this `effectiveFrom` acceptable?
223
+ *
224
+ * Returns a MESSAGE rather than a boolean so the caller can say which rule was
225
+ * broken — "that date is in the past" and "nothing to change" are different
226
+ * corrections, and a bare `false` makes the form guess.
227
+ */
228
+ export function rejectRateChange(input, tier, now) {
229
+ if (!Number.isFinite(input.newValue))
230
+ return "That is not a number.";
231
+ if (input.newValue < 0)
232
+ return "A rate cannot be negative.";
233
+ if (input.previousValue === input.newValue)
234
+ return "That is already the current value.";
235
+ /*
236
+ * A minute of slack, because a form submitted at 14:59:59 against a server
237
+ * clock at 15:00:00 is not an attempt to backdate anything.
238
+ */
239
+ if (input.effectiveFrom.getTime() < now.getTime() - 60000) {
240
+ return "A rate change cannot be backdated — it would restate invoices already filed.";
241
+ }
242
+ if (tier === "policy" && !input.reason?.trim()) {
243
+ return "A policy ceiling needs a reason.";
244
+ }
245
+ return null;
246
+ }