@mohasinac/appkit 4.62.1 → 4.63.0
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 +120 -0
- package/dist/features/ads/schemas/booking-write.js +75 -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.d.ts +27 -1
- package/dist/server.js +144 -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,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The posting rules — pure, so they can be tested by an audit rather than by
|
|
3
|
+
* a `*.test.ts` the project forbids.
|
|
4
|
+
*
|
|
5
|
+
* Nothing here touches Firestore. `assertBalanced` and `buildReversal` are
|
|
6
|
+
* the two decisions that must never be wrong, and keeping them pure is what
|
|
7
|
+
* lets `verify-posting-rules.mjs` exercise them directly — the same shape as
|
|
8
|
+
* `verify-shipment-allocation-guard.mjs`, which reproduces a real failure
|
|
9
|
+
* against the real data shape and carries negative controls.
|
|
10
|
+
*/
|
|
11
|
+
import type { JournalLine, JournalSourceEvent } from "../../../../features/ledger/schemas/firestore";
|
|
12
|
+
/**
|
|
13
|
+
* `YYYY-MM` for a date, in the ONE place that derives it.
|
|
14
|
+
*
|
|
15
|
+
* 🛑 **LOCAL, not UTC, and the difference is a GST return.** A sale at 02:00
|
|
16
|
+
* IST on 1 November is 20:30 UTC on 31 October, so `getUTCMonth()` files it
|
|
17
|
+
* under the wrong month — and the month is exactly what a GSTR-1 period is.
|
|
18
|
+
* Every evening sale in India would land in the previous return.
|
|
19
|
+
*
|
|
20
|
+
* Local is correct because `vercel.json` pins functions to `bom1` and the
|
|
21
|
+
* Firebase region is `asia-south1`, so "local" IS IST on every server that
|
|
22
|
+
* runs this. It also matches `financialYearLabel`, which uses `getMonth()`
|
|
23
|
+
* for the same reason and whose boundary error would be unrecoverable —
|
|
24
|
+
* these two must agree or a March entry could sit in one FY and one period
|
|
25
|
+
* that disagree about which year it belongs to.
|
|
26
|
+
*
|
|
27
|
+
* 🛑 It therefore DISAGREES with nothing by accident but with
|
|
28
|
+
* `payPeriodFor`'s UTC-free local midnight by design — both are local; if
|
|
29
|
+
* either is ever moved to UTC, move both, and expect the invoice series to
|
|
30
|
+
* need the same treatment.
|
|
31
|
+
*/
|
|
32
|
+
export declare function periodOf(date: Date): string;
|
|
33
|
+
/**
|
|
34
|
+
* 🛑 Money comparison needs a TOLERANCE, and omitting one is the same bug
|
|
35
|
+
* Root Cause #92 cost a million invocations a day.
|
|
36
|
+
*
|
|
37
|
+
* Every leg here is a rupee amount with at most two decimals, and the
|
|
38
|
+
* arithmetic that produces them (a GST split, a pro-rata allocation) is
|
|
39
|
+
* floating point — `0.1 + 0.2 !== 0.3`. An exact `===` on the two totals
|
|
40
|
+
* would refuse a correct entry whose halves differ in the last bit, and the
|
|
41
|
+
* refusal would look like a posting-rule bug rather than a float.
|
|
42
|
+
*
|
|
43
|
+
* Half of the smallest representable unit (0.005): tight enough that a real
|
|
44
|
+
* 0.01 imbalance still fails, loose enough that no accumulation of
|
|
45
|
+
* two-decimal values can reach it.
|
|
46
|
+
*/
|
|
47
|
+
export declare const BALANCE_TOLERANCE = 0.005;
|
|
48
|
+
export interface PostingInput {
|
|
49
|
+
entryDate: Date;
|
|
50
|
+
sourceEvent: JournalSourceEvent;
|
|
51
|
+
sourceId?: string;
|
|
52
|
+
narration: string;
|
|
53
|
+
lines: JournalLine[];
|
|
54
|
+
}
|
|
55
|
+
export interface BalancedPosting extends PostingInput {
|
|
56
|
+
period: string;
|
|
57
|
+
totalDebit: number;
|
|
58
|
+
totalCredit: number;
|
|
59
|
+
}
|
|
60
|
+
export declare class UnbalancedEntryError extends Error {
|
|
61
|
+
constructor(message: string);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Validate a posting and compute its totals, or refuse it.
|
|
65
|
+
*
|
|
66
|
+
* 🛑 **Refuses rather than correcting.** A plausible repair — dropping a
|
|
67
|
+
* zero line, inferring the missing side from the difference — is how a wrong
|
|
68
|
+
* posting rule gets a correct-looking entry written for it, and the books
|
|
69
|
+
* then balance while saying something false. An unbalanced entry is always a
|
|
70
|
+
* bug in the rule that built it, and the only safe response is to not write.
|
|
71
|
+
*
|
|
72
|
+
* The five checks, each of which has a distinct failure it prevents:
|
|
73
|
+
*/
|
|
74
|
+
export declare function assertBalanced(input: PostingInput): BalancedPosting;
|
|
75
|
+
/**
|
|
76
|
+
* The reversing entry for a posting — every leg's sides swapped.
|
|
77
|
+
*
|
|
78
|
+
* 🛑 **Swapped, never negated.** `debit: -100` would balance arithmetically
|
|
79
|
+
* and is wrong in every other way: it breaks the non-negative rule above, it
|
|
80
|
+
* makes a trial balance sum to the right number through cancelling errors
|
|
81
|
+
* rather than through correct postings, and a ledger export to Tally would
|
|
82
|
+
* reject it. A credit of 100 against a debit of 100 is what a reversal IS.
|
|
83
|
+
*
|
|
84
|
+
* 🛑 **The reversal's `entryDate` is the CALLER's, not the original's.** A
|
|
85
|
+
* correction discovered in November for an October entry posts in November
|
|
86
|
+
* when October is locked — that is the whole point of the lock, and
|
|
87
|
+
* back-dating the reversal to the original would silently reopen a filed
|
|
88
|
+
* period. The link between them is `reversesEntryId`, not a shared date.
|
|
89
|
+
*/
|
|
90
|
+
export declare function buildReversal(original: {
|
|
91
|
+
id: string;
|
|
92
|
+
lines: JournalLine[];
|
|
93
|
+
narration: string;
|
|
94
|
+
}, at: Date, reason: string): PostingInput;
|
|
95
|
+
/**
|
|
96
|
+
* Where a late correction must post.
|
|
97
|
+
*
|
|
98
|
+
* 🛑 Returns the entry's OWN period when it is open, and the caller's
|
|
99
|
+
* fallback when it is not — it never silently moves a posting. A rule that
|
|
100
|
+
* quietly re-dated anything landing in a closed period would make the lock
|
|
101
|
+
* invisible, and the first anyone knew of it would be a GST return that no
|
|
102
|
+
* longer reconciles to the books it was filed from.
|
|
103
|
+
*/
|
|
104
|
+
export declare function resolvePostingPeriod(wanted: string, isLocked: (period: string) => boolean, openFallback: string): {
|
|
105
|
+
period: string;
|
|
106
|
+
deferred: boolean;
|
|
107
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The posting rules — pure, so they can be tested by an audit rather than by
|
|
3
|
+
* a `*.test.ts` the project forbids.
|
|
4
|
+
*
|
|
5
|
+
* Nothing here touches Firestore. `assertBalanced` and `buildReversal` are
|
|
6
|
+
* the two decisions that must never be wrong, and keeping them pure is what
|
|
7
|
+
* lets `verify-posting-rules.mjs` exercise them directly — the same shape as
|
|
8
|
+
* `verify-shipment-allocation-guard.mjs`, which reproduces a real failure
|
|
9
|
+
* against the real data shape and carries negative controls.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* `YYYY-MM` for a date, in the ONE place that derives it.
|
|
13
|
+
*
|
|
14
|
+
* 🛑 **LOCAL, not UTC, and the difference is a GST return.** A sale at 02:00
|
|
15
|
+
* IST on 1 November is 20:30 UTC on 31 October, so `getUTCMonth()` files it
|
|
16
|
+
* under the wrong month — and the month is exactly what a GSTR-1 period is.
|
|
17
|
+
* Every evening sale in India would land in the previous return.
|
|
18
|
+
*
|
|
19
|
+
* Local is correct because `vercel.json` pins functions to `bom1` and the
|
|
20
|
+
* Firebase region is `asia-south1`, so "local" IS IST on every server that
|
|
21
|
+
* runs this. It also matches `financialYearLabel`, which uses `getMonth()`
|
|
22
|
+
* for the same reason and whose boundary error would be unrecoverable —
|
|
23
|
+
* these two must agree or a March entry could sit in one FY and one period
|
|
24
|
+
* that disagree about which year it belongs to.
|
|
25
|
+
*
|
|
26
|
+
* 🛑 It therefore DISAGREES with nothing by accident but with
|
|
27
|
+
* `payPeriodFor`'s UTC-free local midnight by design — both are local; if
|
|
28
|
+
* either is ever moved to UTC, move both, and expect the invoice series to
|
|
29
|
+
* need the same treatment.
|
|
30
|
+
*/
|
|
31
|
+
export function periodOf(date) {
|
|
32
|
+
const y = date.getFullYear();
|
|
33
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
34
|
+
return `${y}-${m}`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 🛑 Money comparison needs a TOLERANCE, and omitting one is the same bug
|
|
38
|
+
* Root Cause #92 cost a million invocations a day.
|
|
39
|
+
*
|
|
40
|
+
* Every leg here is a rupee amount with at most two decimals, and the
|
|
41
|
+
* arithmetic that produces them (a GST split, a pro-rata allocation) is
|
|
42
|
+
* floating point — `0.1 + 0.2 !== 0.3`. An exact `===` on the two totals
|
|
43
|
+
* would refuse a correct entry whose halves differ in the last bit, and the
|
|
44
|
+
* refusal would look like a posting-rule bug rather than a float.
|
|
45
|
+
*
|
|
46
|
+
* Half of the smallest representable unit (0.005): tight enough that a real
|
|
47
|
+
* 0.01 imbalance still fails, loose enough that no accumulation of
|
|
48
|
+
* two-decimal values can reach it.
|
|
49
|
+
*/
|
|
50
|
+
export const BALANCE_TOLERANCE = 0.005;
|
|
51
|
+
export class UnbalancedEntryError extends Error {
|
|
52
|
+
constructor(message) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "UnbalancedEntryError";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Validate a posting and compute its totals, or refuse it.
|
|
59
|
+
*
|
|
60
|
+
* 🛑 **Refuses rather than correcting.** A plausible repair — dropping a
|
|
61
|
+
* zero line, inferring the missing side from the difference — is how a wrong
|
|
62
|
+
* posting rule gets a correct-looking entry written for it, and the books
|
|
63
|
+
* then balance while saying something false. An unbalanced entry is always a
|
|
64
|
+
* bug in the rule that built it, and the only safe response is to not write.
|
|
65
|
+
*
|
|
66
|
+
* The five checks, each of which has a distinct failure it prevents:
|
|
67
|
+
*/
|
|
68
|
+
export function assertBalanced(input) {
|
|
69
|
+
const { lines } = input;
|
|
70
|
+
// 1. A single-legged entry is not double entry. One line can never balance,
|
|
71
|
+
// and the commonest way to get one is a rule that forgot its credit.
|
|
72
|
+
if (lines.length < 2) {
|
|
73
|
+
throw new UnbalancedEntryError(`A journal entry needs at least two lines; got ${lines.length}.`);
|
|
74
|
+
}
|
|
75
|
+
let totalDebit = 0;
|
|
76
|
+
let totalCredit = 0;
|
|
77
|
+
for (const [i, line] of lines.entries()) {
|
|
78
|
+
const d = line.debit ?? 0;
|
|
79
|
+
const c = line.credit ?? 0;
|
|
80
|
+
// 2. NEGATIVE amounts are refused outright. A negative debit is a credit
|
|
81
|
+
// written the wrong way round, and allowing it means every report has
|
|
82
|
+
// to handle a sign it should never see — plus the two totals could
|
|
83
|
+
// then balance while both sides are nonsense.
|
|
84
|
+
if (d < 0 || c < 0) {
|
|
85
|
+
throw new UnbalancedEntryError(`Line ${i} (${line.accountCode}) has a negative amount. A reversal swaps the sides; it never negates them.`);
|
|
86
|
+
}
|
|
87
|
+
// 3. EXACTLY ONE SIDE per line. A line carrying both is ambiguous about
|
|
88
|
+
// what it asserts, and its net effect depends on a subtraction the
|
|
89
|
+
// reader has to perform — which is precisely what two separate fields
|
|
90
|
+
// exist to avoid.
|
|
91
|
+
if (d > 0 && c > 0) {
|
|
92
|
+
throw new UnbalancedEntryError(`Line ${i} (${line.accountCode}) carries both a debit and a credit. Split it into two lines.`);
|
|
93
|
+
}
|
|
94
|
+
// 4. A ZERO line asserts nothing and is almost always a rule that
|
|
95
|
+
// computed an amount it then failed to use.
|
|
96
|
+
if (d === 0 && c === 0) {
|
|
97
|
+
throw new UnbalancedEntryError(`Line ${i} (${line.accountCode}) is zero on both sides.`);
|
|
98
|
+
}
|
|
99
|
+
totalDebit += d;
|
|
100
|
+
totalCredit += c;
|
|
101
|
+
}
|
|
102
|
+
// 5. The accounting identity itself.
|
|
103
|
+
if (Math.abs(totalDebit - totalCredit) > BALANCE_TOLERANCE) {
|
|
104
|
+
throw new UnbalancedEntryError(`Entry does not balance: debits ${totalDebit.toFixed(2)} vs credits ${totalCredit.toFixed(2)}.`);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
...input,
|
|
108
|
+
period: periodOf(input.entryDate),
|
|
109
|
+
/*
|
|
110
|
+
* Rounded to two decimals AFTER the comparison, not before. Rounding
|
|
111
|
+
* first would mask a genuine sub-unit imbalance by making both sides
|
|
112
|
+
* equal; rounding after records a figure a human can reconcile against
|
|
113
|
+
* a bank statement.
|
|
114
|
+
*/
|
|
115
|
+
totalDebit: Math.round(totalDebit * 100) / 100,
|
|
116
|
+
totalCredit: Math.round(totalCredit * 100) / 100,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The reversing entry for a posting — every leg's sides swapped.
|
|
121
|
+
*
|
|
122
|
+
* 🛑 **Swapped, never negated.** `debit: -100` would balance arithmetically
|
|
123
|
+
* and is wrong in every other way: it breaks the non-negative rule above, it
|
|
124
|
+
* makes a trial balance sum to the right number through cancelling errors
|
|
125
|
+
* rather than through correct postings, and a ledger export to Tally would
|
|
126
|
+
* reject it. A credit of 100 against a debit of 100 is what a reversal IS.
|
|
127
|
+
*
|
|
128
|
+
* 🛑 **The reversal's `entryDate` is the CALLER's, not the original's.** A
|
|
129
|
+
* correction discovered in November for an October entry posts in November
|
|
130
|
+
* when October is locked — that is the whole point of the lock, and
|
|
131
|
+
* back-dating the reversal to the original would silently reopen a filed
|
|
132
|
+
* period. The link between them is `reversesEntryId`, not a shared date.
|
|
133
|
+
*/
|
|
134
|
+
export function buildReversal(original, at, reason) {
|
|
135
|
+
return {
|
|
136
|
+
entryDate: at,
|
|
137
|
+
sourceEvent: "reversal",
|
|
138
|
+
sourceId: original.id,
|
|
139
|
+
narration: `Reversal of ${original.id}: ${reason}`,
|
|
140
|
+
lines: original.lines.map((l) => ({
|
|
141
|
+
accountCode: l.accountCode,
|
|
142
|
+
debit: l.credit,
|
|
143
|
+
credit: l.debit,
|
|
144
|
+
...(l.memo ? { memo: l.memo } : {}),
|
|
145
|
+
...(l.counterpartyId ? { counterpartyId: l.counterpartyId } : {}),
|
|
146
|
+
})),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Where a late correction must post.
|
|
151
|
+
*
|
|
152
|
+
* 🛑 Returns the entry's OWN period when it is open, and the caller's
|
|
153
|
+
* fallback when it is not — it never silently moves a posting. A rule that
|
|
154
|
+
* quietly re-dated anything landing in a closed period would make the lock
|
|
155
|
+
* invisible, and the first anyone knew of it would be a GST return that no
|
|
156
|
+
* longer reconciles to the books it was filed from.
|
|
157
|
+
*/
|
|
158
|
+
export function resolvePostingPeriod(wanted, isLocked, openFallback) {
|
|
159
|
+
if (!isLocked(wanted))
|
|
160
|
+
return { period: wanted, deferred: false };
|
|
161
|
+
if (isLocked(openFallback)) {
|
|
162
|
+
throw new UnbalancedEntryError(`Both ${wanted} and the fallback period ${openFallback} are locked; there is nowhere to post.`);
|
|
163
|
+
}
|
|
164
|
+
return { period: openFallback, deferred: true };
|
|
165
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trial balance and P&L — derived views, pure, so the audit can exercise them.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 These are REPORTS, not truth, and they carry the disclaimer
|
|
5
|
+
*
|
|
6
|
+
* Phase 12's `accounting-output` label applies to every figure produced
|
|
7
|
+
* here. The ledger is already a projection of orders and payouts; a report
|
|
8
|
+
* is a projection of the ledger. Nobody files a return from this without a
|
|
9
|
+
* CA reading it, and the label is what stops the number being treated as
|
|
10
|
+
* though they had.
|
|
11
|
+
*
|
|
12
|
+
* ## 🛑 Signs are applied ONCE, at the boundary
|
|
13
|
+
*
|
|
14
|
+
* A trial balance lists raw debit and credit totals per account, which is
|
|
15
|
+
* what makes it a check: the two columns must agree, and that is only
|
|
16
|
+
* meaningful if nothing has been netted on the way. The P&L is where a
|
|
17
|
+
* normal-balance sign is applied, and it is applied in exactly one place
|
|
18
|
+
* (`signedBalance`) so an income account can never be summed as though it
|
|
19
|
+
* were an expense.
|
|
20
|
+
*/
|
|
21
|
+
import type { AccountType, JournalEntryDocument, JournalLine } from "../../../../features/ledger/schemas/firestore";
|
|
22
|
+
/** Which side increases this type. Duplicated from the schema's own helper? No — see below. */
|
|
23
|
+
type Side = "debit" | "credit";
|
|
24
|
+
export interface AccountTotals {
|
|
25
|
+
accountCode: string;
|
|
26
|
+
debit: number;
|
|
27
|
+
credit: number;
|
|
28
|
+
}
|
|
29
|
+
export interface TrialBalance {
|
|
30
|
+
period: string;
|
|
31
|
+
rows: AccountTotals[];
|
|
32
|
+
totalDebit: number;
|
|
33
|
+
totalCredit: number;
|
|
34
|
+
/** The identity. False means an entry was written outside `post()`. */
|
|
35
|
+
balanced: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Propagated from the read that produced it.
|
|
38
|
+
*
|
|
39
|
+
* 🛑 A truncated trial balance BALANCES — both columns lose the same
|
|
40
|
+
* entries — so `balanced: true` with `truncated: true` says nothing at
|
|
41
|
+
* all. A caller that renders the first and ignores the second is showing
|
|
42
|
+
* a reconciliation that did not happen.
|
|
43
|
+
*/
|
|
44
|
+
truncated: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** Sum every line by account. Raw columns, nothing netted. */
|
|
47
|
+
export declare function trialBalance(period: string, entries: readonly JournalEntryDocument[], truncated?: boolean): TrialBalance;
|
|
48
|
+
/**
|
|
49
|
+
* The net movement on an account, signed by its normal balance.
|
|
50
|
+
*
|
|
51
|
+
* 🛑 The ONE place a sign is applied. An income account with ₹1,000 credited
|
|
52
|
+
* and ₹100 debited has earned ₹900; the same raw figures on an expense
|
|
53
|
+
* account are a ₹900 loss in the other direction. Applying this per call
|
|
54
|
+
* site is how one of the two gets inverted and a P&L reads profitable.
|
|
55
|
+
*/
|
|
56
|
+
export declare function signedBalance(type: AccountType, totals: AccountTotals): number;
|
|
57
|
+
export interface ProfitAndLoss {
|
|
58
|
+
period: string;
|
|
59
|
+
income: number;
|
|
60
|
+
expenses: number;
|
|
61
|
+
net: number;
|
|
62
|
+
truncated: boolean;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Income less expenses for a period.
|
|
66
|
+
*
|
|
67
|
+
* 🛑 Needs the account TYPES, which it takes as a map rather than inferring
|
|
68
|
+
* from the code's leading digit. The range convention is for humans (see the
|
|
69
|
+
* chart's own header); inferring from it makes the first out-of-range
|
|
70
|
+
* account silently land in the wrong half of the P&L, and the total would
|
|
71
|
+
* still look plausible.
|
|
72
|
+
*/
|
|
73
|
+
export declare function profitAndLoss(tb: TrialBalance, typeOf: (accountCode: string) => AccountType | undefined): ProfitAndLoss;
|
|
74
|
+
/** Re-exported for the audit, which checks the two mappings agree. */
|
|
75
|
+
export declare const NORMAL_SIDE_TABLE: Readonly<Record<AccountType, Side>>;
|
|
76
|
+
export type { JournalLine };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trial balance and P&L — derived views, pure, so the audit can exercise them.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 These are REPORTS, not truth, and they carry the disclaimer
|
|
5
|
+
*
|
|
6
|
+
* Phase 12's `accounting-output` label applies to every figure produced
|
|
7
|
+
* here. The ledger is already a projection of orders and payouts; a report
|
|
8
|
+
* is a projection of the ledger. Nobody files a return from this without a
|
|
9
|
+
* CA reading it, and the label is what stops the number being treated as
|
|
10
|
+
* though they had.
|
|
11
|
+
*
|
|
12
|
+
* ## 🛑 Signs are applied ONCE, at the boundary
|
|
13
|
+
*
|
|
14
|
+
* A trial balance lists raw debit and credit totals per account, which is
|
|
15
|
+
* what makes it a check: the two columns must agree, and that is only
|
|
16
|
+
* meaningful if nothing has been netted on the way. The P&L is where a
|
|
17
|
+
* normal-balance sign is applied, and it is applied in exactly one place
|
|
18
|
+
* (`signedBalance`) so an income account can never be summed as though it
|
|
19
|
+
* were an expense.
|
|
20
|
+
*/
|
|
21
|
+
/*
|
|
22
|
+
* 🛑 `normalBalanceFor` lives on the SCHEMA and is not imported here, for the
|
|
23
|
+
* same mechanical reason `periodOf` moved out of it: `verify-posting-rules`
|
|
24
|
+
* loads these modules through Node's type stripping, which erases
|
|
25
|
+
* `import type` but cannot resolve a runtime import against extensionless
|
|
26
|
+
* appkit source.
|
|
27
|
+
*
|
|
28
|
+
* So this is NOT a second definition — the table below is the same mapping,
|
|
29
|
+
* and the schema's function is what application code calls. Two copies of a
|
|
30
|
+
* rule is Root Cause #42, and the honest mitigation is that `R6` in the
|
|
31
|
+
* audit asserts they agree for all five types rather than trusting a
|
|
32
|
+
* comment to keep them in step.
|
|
33
|
+
*/
|
|
34
|
+
const NORMAL_SIDE = {
|
|
35
|
+
asset: "debit",
|
|
36
|
+
expense: "debit",
|
|
37
|
+
liability: "credit",
|
|
38
|
+
equity: "credit",
|
|
39
|
+
income: "credit",
|
|
40
|
+
};
|
|
41
|
+
/** Sum every line by account. Raw columns, nothing netted. */
|
|
42
|
+
export function trialBalance(period, entries, truncated = false) {
|
|
43
|
+
const byAccount = new Map();
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
for (const line of entry.lines) {
|
|
46
|
+
const row = byAccount.get(line.accountCode) ??
|
|
47
|
+
{ accountCode: line.accountCode, debit: 0, credit: 0 };
|
|
48
|
+
row.debit += line.debit ?? 0;
|
|
49
|
+
row.credit += line.credit ?? 0;
|
|
50
|
+
byAccount.set(line.accountCode, row);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const rows = [...byAccount.values()]
|
|
54
|
+
.map((r) => ({
|
|
55
|
+
accountCode: r.accountCode,
|
|
56
|
+
debit: round2(r.debit),
|
|
57
|
+
credit: round2(r.credit),
|
|
58
|
+
}))
|
|
59
|
+
.sort((a, b) => a.accountCode.localeCompare(b.accountCode));
|
|
60
|
+
const totalDebit = round2(rows.reduce((s, r) => s + r.debit, 0));
|
|
61
|
+
const totalCredit = round2(rows.reduce((s, r) => s + r.credit, 0));
|
|
62
|
+
return {
|
|
63
|
+
period,
|
|
64
|
+
rows,
|
|
65
|
+
totalDebit,
|
|
66
|
+
totalCredit,
|
|
67
|
+
/*
|
|
68
|
+
* 🛑 Compared with the SAME tolerance the posting rules use. A trial
|
|
69
|
+
* balance stricter than the gate that wrote the entries would report an
|
|
70
|
+
* imbalance nobody can act on — every entry in it passed `assertBalanced`.
|
|
71
|
+
*/
|
|
72
|
+
balanced: Math.abs(totalDebit - totalCredit) <= 0.005,
|
|
73
|
+
truncated,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The net movement on an account, signed by its normal balance.
|
|
78
|
+
*
|
|
79
|
+
* 🛑 The ONE place a sign is applied. An income account with ₹1,000 credited
|
|
80
|
+
* and ₹100 debited has earned ₹900; the same raw figures on an expense
|
|
81
|
+
* account are a ₹900 loss in the other direction. Applying this per call
|
|
82
|
+
* site is how one of the two gets inverted and a P&L reads profitable.
|
|
83
|
+
*/
|
|
84
|
+
export function signedBalance(type, totals) {
|
|
85
|
+
return NORMAL_SIDE[type] === "debit"
|
|
86
|
+
? round2(totals.debit - totals.credit)
|
|
87
|
+
: round2(totals.credit - totals.debit);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Income less expenses for a period.
|
|
91
|
+
*
|
|
92
|
+
* 🛑 Needs the account TYPES, which it takes as a map rather than inferring
|
|
93
|
+
* from the code's leading digit. The range convention is for humans (see the
|
|
94
|
+
* chart's own header); inferring from it makes the first out-of-range
|
|
95
|
+
* account silently land in the wrong half of the P&L, and the total would
|
|
96
|
+
* still look plausible.
|
|
97
|
+
*/
|
|
98
|
+
export function profitAndLoss(tb, typeOf) {
|
|
99
|
+
let income = 0;
|
|
100
|
+
let expenses = 0;
|
|
101
|
+
for (const row of tb.rows) {
|
|
102
|
+
const type = typeOf(row.accountCode);
|
|
103
|
+
/*
|
|
104
|
+
* 🛑 An UNKNOWN account is skipped and that is why `truncated` is not
|
|
105
|
+
* the only honesty flag this needs — but it is the behaviour with the
|
|
106
|
+
* least bad failure: including it would need a guessed type, and a
|
|
107
|
+
* guessed type puts real money on the wrong side. An account code with
|
|
108
|
+
* no account is a `chartOfAccounts` wipe (the orphan case its
|
|
109
|
+
* `seedPurpose: "permanent"` exists to prevent), and the trial balance
|
|
110
|
+
* above still shows the row, so the discrepancy is visible there.
|
|
111
|
+
*/
|
|
112
|
+
if (!type)
|
|
113
|
+
continue;
|
|
114
|
+
if (type === "income")
|
|
115
|
+
income += signedBalance(type, row);
|
|
116
|
+
else if (type === "expense")
|
|
117
|
+
expenses += signedBalance(type, row);
|
|
118
|
+
}
|
|
119
|
+
income = round2(income);
|
|
120
|
+
expenses = round2(expenses);
|
|
121
|
+
return { period: tb.period, income, expenses, net: round2(income - expenses), truncated: tb.truncated };
|
|
122
|
+
}
|
|
123
|
+
function round2(n) {
|
|
124
|
+
return Math.round(n * 100) / 100;
|
|
125
|
+
}
|
|
126
|
+
/** Re-exported for the audit, which checks the two mappings agree. */
|
|
127
|
+
export const NORMAL_SIDE_TABLE = NORMAL_SIDE;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each business event posts.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 THIS FILE IS THE WHOLE SAFETY ARGUMENT
|
|
5
|
+
*
|
|
6
|
+
* "Every journal entry is machine-generated from a business event" is the
|
|
7
|
+
* one constraint that makes a ledger maintained by a web application
|
|
8
|
+
* trustworthy. If a human has to remember that a gateway fee is
|
|
9
|
+
* `Dr Bank Charges / Cr Bank`, it will eventually be posted backwards — and
|
|
10
|
+
* a backwards entry still balances, so nothing downstream objects. These
|
|
11
|
+
* functions are where that memory lives instead.
|
|
12
|
+
*
|
|
13
|
+
* Pure, and returning a `PostingInput` rather than writing: the caller hands
|
|
14
|
+
* the result to `journalRepository.post`, which is the only thing that
|
|
15
|
+
* touches Firestore and the only thing that checks the period lock. That
|
|
16
|
+
* separation is also what lets `verify-posting-rules` exercise the rules
|
|
17
|
+
* against real order shapes without a database.
|
|
18
|
+
*
|
|
19
|
+
* ## 🛑 Codes are referenced through ACCOUNT, never as bare strings
|
|
20
|
+
*
|
|
21
|
+
* A literal `"2200"` scattered across six rules is six chances to type
|
|
22
|
+
* `"2000"`, and a posting to an account that does not exist is silently
|
|
23
|
+
* dropped from the P&L by `profitAndLoss`'s unknown-account skip. One map,
|
|
24
|
+
* named, so a wrong code is a compile error.
|
|
25
|
+
*/
|
|
26
|
+
import type { PostingInput } from "./posting";
|
|
27
|
+
import type { ExpenseCategory } from "../../../../features/ledger/schemas/expenses";
|
|
28
|
+
/** The accounts these rules post to. Codes match `chartOfAccountsSeedData`. */
|
|
29
|
+
export declare const ACCOUNT: {
|
|
30
|
+
readonly BANK: "1100";
|
|
31
|
+
readonly COURIER_FLOAT: "1150";
|
|
32
|
+
readonly GATEWAY_RECEIVABLE: "1200";
|
|
33
|
+
readonly INPUT_GST: "1300";
|
|
34
|
+
readonly SELLER_PAYABLE: "2100";
|
|
35
|
+
readonly ACCOUNTS_PAYABLE: "2300";
|
|
36
|
+
readonly OUTPUT_GST: "2200";
|
|
37
|
+
readonly BUYER_DEPOSITS: "2400";
|
|
38
|
+
readonly REFUNDS_PAYABLE: "2500";
|
|
39
|
+
readonly OPENING_EQUITY: "3900";
|
|
40
|
+
readonly COMMISSION_INCOME: "4100";
|
|
41
|
+
readonly SHIPPING_RECOVERED: "4200";
|
|
42
|
+
readonly ADDON_INCOME: "4300";
|
|
43
|
+
readonly GATEWAY_CHARGES: "5100";
|
|
44
|
+
readonly FREIGHT_PAID: "5200";
|
|
45
|
+
};
|
|
46
|
+
export interface OrderPostingInput {
|
|
47
|
+
orderId: string;
|
|
48
|
+
storeId: string;
|
|
49
|
+
placedAt: Date;
|
|
50
|
+
/** What the buyer paid in total, GST included. */
|
|
51
|
+
totalPrice: number;
|
|
52
|
+
/** Product GST inside `totalPrice`. */
|
|
53
|
+
gstAmount?: number;
|
|
54
|
+
/** Our commission, and the GST we charge on it. */
|
|
55
|
+
platformFee?: number;
|
|
56
|
+
platformFeeGst?: number;
|
|
57
|
+
/** What the buyer paid for delivery, inside `totalPrice`. */
|
|
58
|
+
shippingFee?: number;
|
|
59
|
+
/** Gift wrap, protection, WhatsApp — inside `totalPrice`. */
|
|
60
|
+
addonFees?: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A paid order.
|
|
64
|
+
*
|
|
65
|
+
* 🛑 **The seller payable is the RESIDUAL, not a computed share.** Everything
|
|
66
|
+
* the platform keeps is recognised explicitly — commission, its GST, the
|
|
67
|
+
* shipping the buyer paid, add-ons, and output GST — and what remains is what
|
|
68
|
+
* the seller is owed. Computing the seller's share independently and letting
|
|
69
|
+
* the two meet in the middle is how a rounding difference becomes an
|
|
70
|
+
* unbalanced entry that `assertBalanced` then refuses at checkout, turning a
|
|
71
|
+
* half-paisa into a failed order.
|
|
72
|
+
*
|
|
73
|
+
* 🛑 **Output GST is a LIABILITY, which is the entire point of the exercise.**
|
|
74
|
+
* `revenueRollup.totalRevenue` sums `order.totalPrice`, so today's "revenue"
|
|
75
|
+
* includes tax owed to the government and freight owed to a courier. Crediting
|
|
76
|
+
* 2200 here is what separates turnover from takings for the first time.
|
|
77
|
+
*
|
|
78
|
+
* 🛑 The debit is to GATEWAY RECEIVABLE, not Bank. Money captured is not
|
|
79
|
+
* money arrived — the settlement lands days later, net of MDR, and
|
|
80
|
+
* `settlementPosting` is what clears this account. Debiting Bank here would
|
|
81
|
+
* overstate cash by the whole float for the length of the settlement cycle.
|
|
82
|
+
*/
|
|
83
|
+
export declare function orderPlacedPosting(o: OrderPostingInput): PostingInput;
|
|
84
|
+
export interface SettlementPostingInput {
|
|
85
|
+
settlementId: string;
|
|
86
|
+
settledAt: Date;
|
|
87
|
+
/** Gross captured in this settlement, matching what was debited to 1200. */
|
|
88
|
+
gross: number;
|
|
89
|
+
/** The gateway's real MDR for it. */
|
|
90
|
+
fee: number;
|
|
91
|
+
/** GST on that fee — an INPUT credit, not a cost. */
|
|
92
|
+
feeGst: number;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* A gateway settlement arriving in the bank.
|
|
96
|
+
*
|
|
97
|
+
* 🛑 `fee` here is the gateway's REAL charge from the settlement report, and
|
|
98
|
+
* it is NOT `PayoutDocument.gatewayFee` — that field is our own withholding
|
|
99
|
+
* against a seller's gross, a different number with a different meaning.
|
|
100
|
+
* Posting one as the other overstates our costs and understates what we
|
|
101
|
+
* withheld, in opposite directions, so the error compounds.
|
|
102
|
+
*
|
|
103
|
+
* 🛑 The GST on the fee is an ASSET (input credit), not an expense. Nothing
|
|
104
|
+
* in this codebase could receive input tax before Part 4, so this was simply
|
|
105
|
+
* unrecoverable — which is money, every month, that was never claimable.
|
|
106
|
+
*/
|
|
107
|
+
export declare function settlementPosting(s: SettlementPostingInput): PostingInput;
|
|
108
|
+
export interface PayoutPostingInput {
|
|
109
|
+
payoutId: string;
|
|
110
|
+
storeId: string;
|
|
111
|
+
paidAt: Date;
|
|
112
|
+
amount: number;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Paying a seller.
|
|
116
|
+
*
|
|
117
|
+
* Clears the payable raised at order time against real cash. Two legs and no
|
|
118
|
+
* income or expense — paying a debt is not a cost, and treating it as one
|
|
119
|
+
* double-counts every sale.
|
|
120
|
+
*/
|
|
121
|
+
export declare function payoutPosting(p: PayoutPostingInput): PostingInput;
|
|
122
|
+
export interface ExpensePostingInput {
|
|
123
|
+
expenseId: string;
|
|
124
|
+
vendorId?: string;
|
|
125
|
+
vendorName: string;
|
|
126
|
+
category: ExpenseCategory;
|
|
127
|
+
billDate: Date;
|
|
128
|
+
amount: number;
|
|
129
|
+
inputGst?: number;
|
|
130
|
+
/** Paid outright, or on credit? Decides the funding leg. */
|
|
131
|
+
paid: boolean;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* A bill.
|
|
135
|
+
*
|
|
136
|
+
* 🛑 The funding leg depends on whether it was PAID, and conflating the two
|
|
137
|
+
* is the commonest small-business bookkeeping error: crediting Bank for a
|
|
138
|
+
* bill not yet paid shows cash leaving that is still there, and crediting
|
|
139
|
+
* Accounts payable for one already settled leaves a debt that does not
|
|
140
|
+
* exist. Both balance; both are wrong; neither announces itself.
|
|
141
|
+
*
|
|
142
|
+
* 🛑 `inputGst` is DEBITED to an asset, never added to the cost. Expensing
|
|
143
|
+
* it is how input credit silently stops being claimable — the money is
|
|
144
|
+
* spent either way, but only one of the two gets it back.
|
|
145
|
+
*/
|
|
146
|
+
export declare function expensePosting(e: ExpensePostingInput): PostingInput;
|
|
147
|
+
export interface RefundPostingInput {
|
|
148
|
+
orderId: string;
|
|
149
|
+
refundId: string;
|
|
150
|
+
refundedAt: Date;
|
|
151
|
+
amount: number;
|
|
152
|
+
/** The product GST inside the refunded amount, which is reclaimable. */
|
|
153
|
+
gstAmount?: number;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* A refund.
|
|
157
|
+
*
|
|
158
|
+
* 🛑 **A refund is NOT a negative sale, and that distinction is why the top
|
|
159
|
+
* line is currently wrong.** `revenueRollup` removes a fully-refunded order
|
|
160
|
+
* from its sum entirely and does not reduce it at all for a partial — so
|
|
161
|
+
* revenue silently rewrites itself retroactively, and no two runs of the same
|
|
162
|
+
* report over the same period agree.
|
|
163
|
+
*
|
|
164
|
+
* Here it is its own event with its own date: the sale stays in the month it
|
|
165
|
+
* happened, the refund lands in the month it happened, and both are visible.
|
|
166
|
+
*
|
|
167
|
+
* 🛑 It DEBITS output GST, reclaiming tax already declared. Omitting that leg
|
|
168
|
+
* means paying GST on money that was given back.
|
|
169
|
+
*/
|
|
170
|
+
export declare function refundPosting(r: RefundPostingInput): PostingInput;
|