@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.
- package/dist/.build-stamp.json +3 -3
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +8 -29
- package/dist/_internal/client/features/layout/filterNavItems.d.ts +28 -0
- package/dist/_internal/client/features/layout/filterNavItems.js +35 -0
- package/dist/_internal/server/features/ads/booking.d.ts +118 -0
- package/dist/_internal/server/features/ads/booking.js +326 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +51 -11
- package/dist/_internal/server/features/checkout/actions.js +335 -80
- package/dist/_internal/server/features/checkout/expand-order-items.d.ts +1 -2
- package/dist/_internal/server/features/checkout/expand-order-items.js +32 -0
- package/dist/_internal/server/features/checkout/freeze-parts.d.ts +50 -0
- package/dist/_internal/server/features/checkout/freeze-parts.js +94 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.d.ts +55 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.js +60 -0
- package/dist/_internal/server/features/dashboard/attention.d.ts +128 -0
- package/dist/_internal/server/features/dashboard/attention.js +183 -0
- package/dist/_internal/server/features/dashboard/data.d.ts +47 -0
- package/dist/_internal/server/features/dashboard/data.js +82 -0
- package/dist/_internal/server/features/inventory/reservations.d.ts +99 -0
- package/dist/_internal/server/features/inventory/reservations.js +273 -0
- package/dist/_internal/server/features/offers/destinations.d.ts +40 -0
- package/dist/_internal/server/features/offers/destinations.js +65 -0
- package/dist/_internal/server/features/orders/adapters.js +21 -0
- package/dist/_internal/server/features/orders/amend-delivery.d.ts +56 -0
- package/dist/_internal/server/features/orders/amend-delivery.js +124 -0
- package/dist/_internal/server/features/orders/balance.d.ts +89 -0
- package/dist/_internal/server/features/orders/balance.js +256 -0
- package/dist/_internal/server/features/pricing/actions.d.ts +58 -0
- package/dist/_internal/server/features/pricing/actions.js +144 -0
- package/dist/_internal/server/features/shipping/book-split.d.ts +86 -0
- package/dist/_internal/server/features/shipping/book-split.js +130 -0
- package/dist/_internal/server/features/shipping/book.d.ts +61 -0
- package/dist/_internal/server/features/shipping/book.js +176 -0
- package/dist/_internal/server/features/shipping/courier-block.js +26 -1
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +14 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +14 -0
- package/dist/_internal/server/functions/scheduled.d.ts +2 -1
- package/dist/_internal/server/functions/scheduled.js +57 -1
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +2 -0
- package/dist/_internal/server/jobs/core/jobRunners.js +45 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +4 -1
- package/dist/_internal/server/jobs/core/reservationExpiry.d.ts +17 -0
- package/dist/_internal/server/jobs/core/reservationExpiry.js +40 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +15 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
- package/dist/_internal/server/jobs/handlers/index.js +1 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.js +2 -0
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +268 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +31 -2
- package/dist/_internal/shared/checkout/order-math.js +22 -36
- package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
- package/dist/_internal/shared/checkout/rules/index.js +1 -1
- package/dist/_internal/shared/checkout/rules/payment-gate.d.ts +18 -0
- package/dist/_internal/shared/checkout/rules/payment-gate.js +25 -0
- package/dist/_internal/shared/checkout/split.d.ts +0 -12
- package/dist/_internal/shared/checkout/split.js +22 -2
- package/dist/_internal/shared/content/disclaimers.js +7 -7
- package/dist/_internal/shared/content/published-values.d.ts +6 -1
- package/dist/_internal/shared/content/published-values.js +8 -0
- package/dist/_internal/shared/features/admin/integrations.d.ts +32 -2
- package/dist/_internal/shared/features/admin/integrations.js +19 -9
- package/dist/_internal/shared/features/dashboard/hub-sections.d.ts +72 -0
- package/dist/_internal/shared/features/dashboard/hub-sections.js +75 -0
- package/dist/_internal/shared/features/emi/choice.d.ts +98 -0
- package/dist/_internal/shared/features/emi/choice.js +69 -0
- package/dist/_internal/shared/features/emi/group.d.ts +84 -0
- package/dist/_internal/shared/features/emi/group.js +126 -0
- package/dist/_internal/shared/features/ledger/posting.d.ts +107 -0
- package/dist/_internal/shared/features/ledger/posting.js +165 -0
- package/dist/_internal/shared/features/ledger/reports.d.ts +76 -0
- package/dist/_internal/shared/features/ledger/reports.js +127 -0
- package/dist/_internal/shared/features/ledger/rules.d.ts +170 -0
- package/dist/_internal/shared/features/ledger/rules.js +221 -0
- package/dist/_internal/shared/features/offers/coordination.d.ts +122 -0
- package/dist/_internal/shared/features/offers/coordination.js +80 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.d.ts +114 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.js +116 -0
- package/dist/_internal/shared/features/payouts/pay-period.d.ts +91 -0
- package/dist/_internal/shared/features/payouts/pay-period.js +104 -0
- package/dist/_internal/shared/features/points/accepts.d.ts +1 -8
- package/dist/_internal/shared/features/points/accepts.js +12 -6
- package/dist/_internal/shared/features/pricing/rate-registry.d.ts +150 -0
- package/dist/_internal/shared/features/pricing/rate-registry.js +246 -0
- package/dist/_internal/shared/features/shipping/parcel.d.ts +123 -0
- package/dist/_internal/shared/features/shipping/parcel.js +139 -0
- package/dist/_internal/shared/features/site-settings/fees.d.ts +16 -8
- package/dist/_internal/shared/features/site-settings/fees.js +12 -8
- package/dist/_internal/shared/fees/explain.d.ts +81 -0
- package/dist/_internal/shared/fees/explain.js +101 -0
- package/dist/_internal/shared/history/index.d.ts +1 -1
- package/dist/_internal/shared/history/index.js +1 -1
- package/dist/_internal/shared/history/types.d.ts +7 -1
- package/dist/_internal/shared/history/types.js +25 -0
- package/dist/client.d.ts +11 -1
- package/dist/client.js +13 -0
- package/dist/constants/api-endpoints.d.ts +72 -0
- package/dist/constants/api-endpoints.js +41 -0
- package/dist/constants/field-names.d.ts +41 -0
- package/dist/constants/field-names.js +47 -0
- package/dist/features/account/components/UserOrdersView.js +28 -2
- package/dist/features/admin/components/AdminIntegrationsView.d.ts +55 -0
- package/dist/features/admin/components/AdminIntegrationsView.js +74 -0
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +16 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +72 -3
- package/dist/features/admin/components/AdminOrdersView.js +20 -1
- package/dist/features/admin/schemas/firestore.d.ts +32 -0
- package/dist/features/ads/schemas/booking-write.d.ts +157 -0
- package/dist/features/ads/schemas/booking-write.js +88 -0
- package/dist/features/ads/schemas/creative-form.d.ts +18 -0
- package/dist/features/ads/schemas/creative-form.js +51 -0
- package/dist/features/ads/schemas/firestore.d.ts +140 -0
- package/dist/features/ads/schemas/firestore.js +109 -0
- package/dist/features/auctions/repository/bid.repository.js +3 -1
- package/dist/features/audit-log/schemas/firestore.d.ts +110 -0
- package/dist/features/audit-log/schemas/firestore.js +74 -0
- package/dist/features/auth/actions/profile-actions.d.ts +69 -1
- package/dist/features/auth/actions/profile-actions.js +62 -1
- package/dist/features/auth/schemas/firestore.d.ts +16 -2
- package/dist/features/auth/schemas/firestore.js +2 -1
- package/dist/features/blog/actions/blog-actions.d.ts +6 -0
- package/dist/features/blog/actions/blog-actions.js +17 -2
- package/dist/features/blog/api/route.js +4 -1
- package/dist/features/blog/columns/index.js +5 -0
- package/dist/features/blog/components/BlogIndexPageView.js +2 -1
- package/dist/features/blog/repository/blog.repository.d.ts +4 -0
- package/dist/features/blog/repository/blog.repository.js +5 -0
- package/dist/features/blog/schemas/blog-form.d.ts +3 -0
- package/dist/features/blog/schemas/blog-form.js +1 -0
- package/dist/features/blog/schemas/firestore.d.ts +38 -1
- package/dist/features/blog/schemas/firestore.js +23 -0
- package/dist/features/blog/schemas/index.d.ts +6 -0
- package/dist/features/blog/schemas/index.js +2 -0
- package/dist/features/blog/types/index.d.ts +2 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +12 -1
- package/dist/features/cart/components/CartPriceBreakdown.js +24 -5
- package/dist/features/cart/repository/cart.repository.d.ts +14 -0
- package/dist/features/cart/repository/cart.repository.js +20 -0
- package/dist/features/cart/schemas/firestore.d.ts +24 -0
- package/dist/features/dashboard/components/DashboardHubView.d.ts +83 -0
- package/dist/features/dashboard/components/DashboardHubView.js +56 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.d.ts +23 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.js +66 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.d.ts +55 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.js +175 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +24 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +28 -0
- package/dist/features/dispute-reasons/schemas/firestore.d.ts +3 -5
- package/dist/features/events/components/EventSaleBanner.js +1 -1
- package/dist/features/faq/components/FAQPageContent.js +2 -2
- package/dist/features/gateway-events/schemas/index.d.ts +2 -2
- package/dist/features/homepage/schemas/index.d.ts +8 -8
- package/dist/features/inventory/schemas/stock-reservation.d.ts +114 -0
- package/dist/features/inventory/schemas/stock-reservation.js +103 -0
- package/dist/features/layout/AppLayoutShell.d.ts +9 -1
- package/dist/features/layout/AppLayoutShell.js +12 -4
- package/dist/features/layout/BottomActions.js +2 -1
- package/dist/features/layout/BottomNavbar.d.ts +10 -2
- package/dist/features/layout/SideActionDock.js +52 -2
- package/dist/features/layout/TitleBarLayout.js +13 -4
- package/dist/features/layout/TitleBarLayout.style.css +59 -0
- package/dist/features/layout/useCtaReveal.d.ts +16 -1
- package/dist/features/layout/useCtaReveal.js +17 -26
- package/dist/features/ledger/repository/journal.repository.d.ts +110 -0
- package/dist/features/ledger/repository/journal.repository.js +312 -0
- package/dist/features/ledger/schemas/expenses.d.ts +87 -0
- package/dist/features/ledger/schemas/expenses.js +27 -0
- package/dist/features/ledger/schemas/firestore.d.ts +183 -0
- package/dist/features/ledger/schemas/firestore.js +69 -0
- package/dist/features/orders/components/BalancePaymentPanel.d.ts +12 -0
- package/dist/features/orders/components/BalancePaymentPanel.js +94 -0
- package/dist/features/orders/components/RaiseDisputeForm.js +19 -11
- package/dist/features/orders/repository/orders.repository.d.ts +28 -1
- package/dist/features/orders/repository/orders.repository.js +60 -6
- package/dist/features/orders/schemas/balance.d.ts +40 -0
- package/dist/features/orders/schemas/balance.js +47 -0
- package/dist/features/orders/schemas/firestore.d.ts +202 -0
- package/dist/features/orders/schemas/firestore.js +43 -0
- package/dist/features/orders/schemas/index.d.ts +2 -2
- package/dist/features/orders/types/index.d.ts +21 -0
- package/dist/features/payments/repository/payout.repository.js +3 -2
- package/dist/features/payments/schemas/firestore.d.ts +92 -1
- package/dist/features/payments/schemas/firestore.js +23 -0
- package/dist/features/pricing/components/AdminPricingView.d.ts +13 -0
- package/dist/features/pricing/components/AdminPricingView.js +157 -0
- package/dist/features/products/schemas/firestore.d.ts +26 -1
- package/dist/features/products/schemas/firestore.js +2 -0
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/reviews/repository/reviews.repository.d.ts +14 -0
- package/dist/features/reviews/repository/reviews.repository.js +16 -0
- package/dist/features/scams/repository/scammer.repository.js +2 -1
- package/dist/features/scams/schemas/firestore.d.ts +2 -1
- package/dist/features/scams/schemas/firestore.js +2 -1
- package/dist/features/seller/actions/offer-actions.js +56 -0
- package/dist/features/seller/actions/seller-actions.d.ts +1 -1
- package/dist/features/seller/actions/seller-actions.js +4 -0
- package/dist/features/seller/components/DispatchForm.d.ts +11 -0
- package/dist/features/seller/components/DispatchForm.js +190 -0
- package/dist/features/seller/components/SellerOrdersView.js +94 -2
- package/dist/features/seller/components/index.d.ts +1 -0
- package/dist/features/seller/components/index.js +1 -0
- package/dist/features/seller/repository/offer.repository.js +4 -3
- package/dist/features/seller/schemas/firestore.d.ts +23 -2
- package/dist/features/seller/schemas/firestore.js +3 -2
- package/dist/features/seller/schemas/index.d.ts +4 -4
- package/dist/features/seller/schemas/order-forms.d.ts +61 -4
- package/dist/features/seller/schemas/order-forms.js +41 -8
- package/dist/features/shipments/schemas/firestore.d.ts +25 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +39 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +55 -0
- package/dist/features/shipping-boxes/schemas/firestore.d.ts +2 -4
- package/dist/features/stores/repository/store.repository.d.ts +18 -0
- package/dist/features/stores/repository/store.repository.js +27 -1
- package/dist/features/stores/schemas/firestore.d.ts +41 -0
- package/dist/features/stores/schemas/firestore.js +20 -0
- package/dist/features/stores/schemas/index.d.ts +6 -6
- package/dist/features/support/repository/support.repository.js +72 -6
- package/dist/features/support/schemas/firestore.d.ts +17 -0
- package/dist/features/support/schemas/index.d.ts +2 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +19 -0
- package/dist/next/routing/route-map.d.ts +92 -0
- package/dist/next/routing/route-map.js +53 -0
- package/dist/react/contexts/SessionContext.d.ts +17 -1
- package/dist/schemas/registry.d.ts +22 -16
- package/dist/security/pii-schemas.d.ts +1 -1
- package/dist/security/pii-schemas.js +45 -0
- package/dist/server-entry.d.ts +1 -1
- package/dist/server-entry.js +10 -1
- package/dist/server.d.ts +29 -1
- package/dist/server.js +158 -0
- package/dist/styles.css +349 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Button.d.ts +10 -1
- package/dist/ui/components/Button.js +25 -3
- package/dist/ui/components/Chart.d.ts +63 -0
- package/dist/ui/components/Chart.js +169 -0
- package/dist/ui/components/Chart.style.css +214 -0
- package/dist/ui/components/Disclaimer.js +10 -1
- package/dist/ui/components/OverflowRow.js +54 -2
- package/dist/ui/components/OverflowRow.style.css +38 -0
- package/dist/ui/components/RichTextEditor.js +99 -8
- package/dist/ui/components/chart-spec.d.ts +68 -0
- package/dist/ui/components/chart-spec.js +140 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/components/surface-tokens.d.ts +1 -1
- package/dist/ui/components/surface-tokens.js +9 -0
- package/dist/ui/components/toolbar-collapse.d.ts +13 -0
- package/dist/ui/components/toolbar-collapse.js +10 -1
- package/dist/ui/index.d.ts +6 -0
- package/dist/ui/index.js +3 -0
- package/dist/ui/rich-text/RichText.d.ts +10 -1
- package/dist/ui/rich-text/RichText.js +33 -2
- package/dist/ui/rich-text/RichText.style.css +35 -0
- package/dist/ui/rich-text/RichTextRenderer.d.ts +30 -1
- package/dist/ui/rich-text/RichTextRenderer.js +34 -2
- package/dist/ui/rich-text/chart-blocks.d.ts +62 -0
- package/dist/ui/rich-text/chart-blocks.js +87 -0
- package/package.json +1 -1
- 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
|
-
*
|
|
25
|
+
/*
|
|
26
|
+
* 🛑 `PLATFORM_STORE_ID` is DECLARED in `features/stores/schemas/firestore.ts`
|
|
27
|
+
* and re-exported here, not defined here.
|
|
27
28
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
|
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
|
+
}
|