@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,312 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: The only write path into the books. Every rule that makes a ledger
|
|
3
|
+
* trustworthy — balanced, append-only, period-locked — is enforced here
|
|
4
|
+
* rather than asked of callers, because a caller who gets one wrong
|
|
5
|
+
* produces a successful write and finds out at the next GST return.
|
|
6
|
+
*
|
|
7
|
+
* WHAT: JournalRepository — post, reverse, lock, read.
|
|
8
|
+
*
|
|
9
|
+
* @tag domain:ledger,finance
|
|
10
|
+
* @tag layer:repository
|
|
11
|
+
* @tag access:server
|
|
12
|
+
* @tag sideEffects:firestore
|
|
13
|
+
*/
|
|
14
|
+
/* The DIRECTORY barrel, which is where every other repository gets this
|
|
15
|
+
* from — `providers/db-firebase/base` exports only `FirebaseRepository`,
|
|
16
|
+
* and `BaseRepository` (the one with `this.db` and `mapDoc`) is composed
|
|
17
|
+
* one level up. */
|
|
18
|
+
import { BaseRepository } from "../../../providers/db-firebase/index.js";
|
|
19
|
+
import { unitOfWork } from "../../../core/unit-of-work.js";
|
|
20
|
+
import { JOURNAL_ENTRIES_COLLECTION, ACCOUNTING_PERIODS_COLLECTION, LEDGER_FIELDS, } from "../schemas/firestore.js";
|
|
21
|
+
import { assertBalanced, buildReversal, periodOf, UnbalancedEntryError, } from "../../../_internal/shared/features/ledger/posting.js";
|
|
22
|
+
import { NotFoundError } from "../../../errors/not-found-error.js";
|
|
23
|
+
import { ValidationError } from "../../../errors/validation-error.js";
|
|
24
|
+
/**
|
|
25
|
+
* How many entries one period read may return.
|
|
26
|
+
*
|
|
27
|
+
* Sized against a plausible month rather than a round number: a few hundred
|
|
28
|
+
* orders, each posting two or three entries, plus settlements and expenses.
|
|
29
|
+
* 2,000 leaves real headroom and still fits comfortably inside a function's
|
|
30
|
+
* memory and the ~3-round-trip budget.
|
|
31
|
+
*/
|
|
32
|
+
export const JOURNAL_PERIOD_SCAN_LIMIT = 2000;
|
|
33
|
+
export class JournalRepository extends BaseRepository {
|
|
34
|
+
/*
|
|
35
|
+
* 🛑 NO `SIEVE_FIELDS`, same reasoning as `obligations`. The reports read
|
|
36
|
+
* by period and by account code — two fixed shapes, both served by the
|
|
37
|
+
* automatic index — and an admin looking at a figure opens the ORDER it
|
|
38
|
+
* came from, not a filtered journal list. A Sieve surface nothing calls is
|
|
39
|
+
* the `route-schema-registry` pattern: 576 keys into a layer with no
|
|
40
|
+
* readers.
|
|
41
|
+
*/
|
|
42
|
+
constructor() {
|
|
43
|
+
super(JOURNAL_ENTRIES_COLLECTION);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 🛑 APPEND-ONLY, and the method bodies refuse rather than a comment
|
|
47
|
+
* asking nicely.
|
|
48
|
+
*
|
|
49
|
+
* `inventoryMovements` takes the same stance for the same reason: a
|
|
50
|
+
* Firestore update succeeds silently, so a caller who edits history never
|
|
51
|
+
* finds out, and a ledger whose rows can be edited cannot be used as
|
|
52
|
+
* evidence of anything. The correction mechanism is `reverse()`.
|
|
53
|
+
*/
|
|
54
|
+
async update() {
|
|
55
|
+
throw new Error("journalEntries is append-only. Correct an entry with reverse(), which posts the opposite legs and links the two — an edit would silently rewrite a period a GST return may already have been filed from.");
|
|
56
|
+
}
|
|
57
|
+
async delete() {
|
|
58
|
+
throw new Error("journalEntries is append-only. An entry that should not have been posted is reversed, not removed; the reversal is the record that it was wrong.");
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Post one entry.
|
|
62
|
+
*
|
|
63
|
+
* 🛑 **The balance check and the lock check are INSIDE the transaction.**
|
|
64
|
+
* Reading the period's state first and writing after is a TOCTOU window: an
|
|
65
|
+
* admin filing a return between the two gets an entry in the period they
|
|
66
|
+
* just closed, which is the one outcome the lock exists to prevent. It is a
|
|
67
|
+
* narrow window and it is exactly the kind that shows up once, in the
|
|
68
|
+
* quarter it matters.
|
|
69
|
+
*/
|
|
70
|
+
async post(input) {
|
|
71
|
+
const balanced = assertBalanced(input);
|
|
72
|
+
return unitOfWork.runTransaction(async (tx) => {
|
|
73
|
+
const db = this.db;
|
|
74
|
+
const periodRef = db
|
|
75
|
+
.collection(ACCOUNTING_PERIODS_COLLECTION)
|
|
76
|
+
.doc(`period-${balanced.period}`);
|
|
77
|
+
const snap = await tx.get(periodRef);
|
|
78
|
+
const period = snap.exists ? snap.data() : null;
|
|
79
|
+
/*
|
|
80
|
+
* 🛑 A period with NO row is OPEN, not closed.
|
|
81
|
+
*
|
|
82
|
+
* Fail-open here and fail-closed everywhere else is deliberate: a
|
|
83
|
+
* period is closed by someone filing a return, which is an action that
|
|
84
|
+
* writes a row. Treating absence as closed would mean the books refuse
|
|
85
|
+
* every posting until an admin opens each month by hand — and the
|
|
86
|
+
* first missed month silently stops the entire ingest, which looks
|
|
87
|
+
* like a broken job rather than a missing row.
|
|
88
|
+
*/
|
|
89
|
+
if (period?.status === "locked") {
|
|
90
|
+
throw new ValidationError(`Period ${balanced.period} is closed and cannot accept new entries.`, {
|
|
91
|
+
period: period.lockedReason ??
|
|
92
|
+
"A return has been filed for this period. Post the correction to the open period instead.",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const ref = db.collection(JOURNAL_ENTRIES_COLLECTION).doc();
|
|
96
|
+
const now = new Date();
|
|
97
|
+
const doc = {
|
|
98
|
+
...balanced,
|
|
99
|
+
id: ref.id,
|
|
100
|
+
createdAt: now,
|
|
101
|
+
updatedAt: now,
|
|
102
|
+
};
|
|
103
|
+
tx.set(ref, doc);
|
|
104
|
+
return doc;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Reverse an entry.
|
|
109
|
+
*
|
|
110
|
+
* 🛑 **Refuses a SECOND reversal of the same entry.** Without the check a
|
|
111
|
+
* double-click posts the opposite legs twice, and the books then show the
|
|
112
|
+
* original plus two reversals — a net position that is the mirror image of
|
|
113
|
+
* the truth, balanced, and wrong. `reversedByEntryId` is set inside the
|
|
114
|
+
* same transaction that reads it, so the second attempt loses the race
|
|
115
|
+
* rather than both succeeding.
|
|
116
|
+
*/
|
|
117
|
+
async reverse(entryId, reason, at = new Date()) {
|
|
118
|
+
if (!reason.trim()) {
|
|
119
|
+
/*
|
|
120
|
+
* A reversal with no stated reason is the one thing a reader of the
|
|
121
|
+
* books cannot reconstruct later. The original entry says what was
|
|
122
|
+
* posted; only this says why it was wrong.
|
|
123
|
+
*/
|
|
124
|
+
throw new ValidationError("A reversal needs a reason.", {
|
|
125
|
+
reason: "Say what was wrong with the original entry.",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return unitOfWork.runTransaction(async (tx) => {
|
|
129
|
+
const db = this.db;
|
|
130
|
+
const originalRef = db.collection(JOURNAL_ENTRIES_COLLECTION).doc(entryId);
|
|
131
|
+
const snap = await tx.get(originalRef);
|
|
132
|
+
if (!snap.exists)
|
|
133
|
+
throw new NotFoundError(`Journal entry ${entryId} not found.`);
|
|
134
|
+
const original = snap.data();
|
|
135
|
+
if (original.reversedByEntryId) {
|
|
136
|
+
throw new ValidationError(`Entry ${entryId} has already been reversed by ${original.reversedByEntryId}.`, { entryId: "Reversing it again would double the correction." });
|
|
137
|
+
}
|
|
138
|
+
if (original.sourceEvent === "reversal") {
|
|
139
|
+
/*
|
|
140
|
+
* Reversing a reversal is re-instating the original, and saying so
|
|
141
|
+
* is clearer than letting someone build a chain nobody can read. If
|
|
142
|
+
* the original was right after all, post it again with a narration
|
|
143
|
+
* that says why.
|
|
144
|
+
*/
|
|
145
|
+
throw new ValidationError(`Entry ${entryId} is itself a reversal.`, { entryId: "To re-instate the original, post it again with a narration explaining why." });
|
|
146
|
+
}
|
|
147
|
+
const reversalInput = buildReversal({ id: entryId, lines: original.lines, narration: original.narration }, at, reason.trim());
|
|
148
|
+
const balanced = assertBalanced(reversalInput);
|
|
149
|
+
/*
|
|
150
|
+
* The reversal's OWN period must be open. It is `at`'s period, not the
|
|
151
|
+
* original's — see `buildReversal`, which deliberately does not
|
|
152
|
+
* back-date. A correction found after a return was filed posts to the
|
|
153
|
+
* open month and references the closed one.
|
|
154
|
+
*/
|
|
155
|
+
const periodRef = db
|
|
156
|
+
.collection(ACCOUNTING_PERIODS_COLLECTION)
|
|
157
|
+
.doc(`period-${balanced.period}`);
|
|
158
|
+
const periodSnap = await tx.get(periodRef);
|
|
159
|
+
const period = periodSnap.exists
|
|
160
|
+
? periodSnap.data()
|
|
161
|
+
: null;
|
|
162
|
+
if (period?.status === "locked") {
|
|
163
|
+
throw new ValidationError(`Period ${balanced.period} is closed, so the correction has nowhere to post.`, { period: "Open the current period before reversing." });
|
|
164
|
+
}
|
|
165
|
+
const ref = db.collection(JOURNAL_ENTRIES_COLLECTION).doc();
|
|
166
|
+
const now = new Date();
|
|
167
|
+
const reversal = {
|
|
168
|
+
...balanced,
|
|
169
|
+
id: ref.id,
|
|
170
|
+
reversesEntryId: entryId,
|
|
171
|
+
createdAt: now,
|
|
172
|
+
updatedAt: now,
|
|
173
|
+
};
|
|
174
|
+
tx.set(ref, reversal);
|
|
175
|
+
/*
|
|
176
|
+
* 🛑 The back-link is written with `tx.update` on the ORIGINAL, which
|
|
177
|
+
* is the one edit to a posted entry this class permits — and it adds a
|
|
178
|
+
* pointer rather than changing a figure. Without it `reverse()` has no
|
|
179
|
+
* way to refuse a second attempt, and nothing reading the original can
|
|
180
|
+
* tell it has been corrected.
|
|
181
|
+
*/
|
|
182
|
+
tx.update(originalRef, { reversedByEntryId: ref.id, updatedAt: now });
|
|
183
|
+
return reversal;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Close a period.
|
|
188
|
+
*
|
|
189
|
+
* 🛑 Takes the reason and the actor, both required. "Who closed October
|
|
190
|
+
* and under which return" is the first question anyone asks when a figure
|
|
191
|
+
* disagrees with a filing, and a lock with no answer to it is a lock
|
|
192
|
+
* somebody will simply reopen.
|
|
193
|
+
*/
|
|
194
|
+
async lockPeriod(period, lockedByUid, lockedReason) {
|
|
195
|
+
if (!/^\d{4}-\d{2}$/.test(period)) {
|
|
196
|
+
throw new ValidationError(`"${period}" is not a YYYY-MM period.`, {
|
|
197
|
+
period: "Expected a month such as 2026-10.",
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (!lockedReason.trim()) {
|
|
201
|
+
throw new ValidationError("Closing a period needs a reason.", {
|
|
202
|
+
lockedReason: "Name the return this period was filed under.",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
await unitOfWork.runTransaction(async (tx) => {
|
|
206
|
+
const db = this.db;
|
|
207
|
+
const ref = db.collection(ACCOUNTING_PERIODS_COLLECTION).doc(`period-${period}`);
|
|
208
|
+
const snap = await tx.get(ref);
|
|
209
|
+
if (snap.data()?.status === "locked") {
|
|
210
|
+
/*
|
|
211
|
+
* Idempotent rather than an error: a retried request must not read
|
|
212
|
+
* as a failure, and re-closing a closed period changes nothing.
|
|
213
|
+
*/
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const now = new Date();
|
|
217
|
+
tx.set(ref, {
|
|
218
|
+
id: `period-${period}`,
|
|
219
|
+
period,
|
|
220
|
+
status: "locked",
|
|
221
|
+
lockedReason: lockedReason.trim(),
|
|
222
|
+
lockedAt: now,
|
|
223
|
+
lockedByUid,
|
|
224
|
+
createdAt: now,
|
|
225
|
+
updatedAt: now,
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* 🛑 There is deliberately NO `unlockPeriod`.
|
|
231
|
+
*
|
|
232
|
+
* Reopening a period a return was filed from is not an operation the
|
|
233
|
+
* system should make easy — the correct response to a late correction is a
|
|
234
|
+
* reversing entry in the OPEN period, which is what `resolvePostingPeriod`
|
|
235
|
+
* computes and what every accountant expects. If a period genuinely must
|
|
236
|
+
* be reopened (a return withdrawn, say), that is a deliberate act with a
|
|
237
|
+
* paper trail outside this system, and a console write is the right amount
|
|
238
|
+
* of friction for it.
|
|
239
|
+
*
|
|
240
|
+
* This method exists only to say so. Deleting it would leave the absence
|
|
241
|
+
* looking like an oversight somebody helpfully fills in.
|
|
242
|
+
*/
|
|
243
|
+
/**
|
|
244
|
+
* Every entry in a period, for a trial balance or a return.
|
|
245
|
+
*
|
|
246
|
+
* 🛑 **BOUNDED, and truncation is REPORTED rather than silent.** An
|
|
247
|
+
* unbounded read is the Rule #6 violation, but the reason it matters here
|
|
248
|
+
* is sharper than cost: a trial balance computed from a silently truncated
|
|
249
|
+
* page is a trial balance that BALANCES — both sides lose the same
|
|
250
|
+
* entries — while being wrong. It would look perfect and reconcile to
|
|
251
|
+
* nothing.
|
|
252
|
+
*
|
|
253
|
+
* So `truncated` is part of the return type and callers must handle it.
|
|
254
|
+
* The same stance `listPublicProducts` takes with its own `truncated`
|
|
255
|
+
* flag: a floor rendered as "50+", never a figure presented as a total.
|
|
256
|
+
*
|
|
257
|
+
* The real answer at volume is a period rollup written by a scheduled job
|
|
258
|
+
* — the `revenueRollup` pattern — and this is deliberately NOT that, so
|
|
259
|
+
* the flag is what forces the question rather than hiding it.
|
|
260
|
+
*/
|
|
261
|
+
async listByPeriod(period, limit = JOURNAL_PERIOD_SCAN_LIMIT) {
|
|
262
|
+
const snap = await this.db
|
|
263
|
+
.collection(JOURNAL_ENTRIES_COLLECTION)
|
|
264
|
+
.where(LEDGER_FIELDS.PERIOD, "==", period)
|
|
265
|
+
.orderBy(LEDGER_FIELDS.ENTRY_DATE)
|
|
266
|
+
.limit(limit + 1)
|
|
267
|
+
.get();
|
|
268
|
+
const docs = snap.docs.slice(0, limit);
|
|
269
|
+
return {
|
|
270
|
+
entries: docs.map((d) => this.mapDoc(d)),
|
|
271
|
+
truncated: snap.docs.length > limit,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/** Whether a period is closed. Absence means open — see `post`. */
|
|
275
|
+
async isPeriodLocked(period) {
|
|
276
|
+
const snap = await this.db
|
|
277
|
+
.collection(ACCOUNTING_PERIODS_COLLECTION)
|
|
278
|
+
.doc(`period-${period}`)
|
|
279
|
+
.get();
|
|
280
|
+
return snap.data()?.status === "locked";
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* The cutover entry.
|
|
284
|
+
*
|
|
285
|
+
* 🛑 **NOT a backfill.** Pre-cutover data here is seeded and test, so
|
|
286
|
+
* replaying it would post fixtures as trade — the first financial year is
|
|
287
|
+
* therefore split across the old rollups and this ledger, and the
|
|
288
|
+
* "pre-ledger estimate" label on the old reports is what keeps that
|
|
289
|
+
* honest rather than hiding it.
|
|
290
|
+
*
|
|
291
|
+
* 🛑 Refuses to run twice. An opening balance posted a second time doubles
|
|
292
|
+
* every starting figure, and because both entries balance nothing
|
|
293
|
+
* downstream would object.
|
|
294
|
+
*/
|
|
295
|
+
async postOpeningBalances(at, lines) {
|
|
296
|
+
const existing = await this.db
|
|
297
|
+
.collection(JOURNAL_ENTRIES_COLLECTION)
|
|
298
|
+
.where(LEDGER_FIELDS.SOURCE_EVENT, "==", "opening-balance")
|
|
299
|
+
.limit(1)
|
|
300
|
+
.get();
|
|
301
|
+
if (!existing.empty) {
|
|
302
|
+
throw new UnbalancedEntryError("Opening balances have already been posted. Correct them with a reversing entry rather than posting a second opening.");
|
|
303
|
+
}
|
|
304
|
+
return this.post({
|
|
305
|
+
entryDate: at,
|
|
306
|
+
sourceEvent: "opening-balance",
|
|
307
|
+
narration: `Opening balances at cutover, ${periodOf(at)}`,
|
|
308
|
+
lines,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
export const journalRepository = new JournalRepository();
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vendors` + `expenses` — money that leaves the business for something
|
|
3
|
+
* other than a payout or a refund.
|
|
4
|
+
*
|
|
5
|
+
* ## 🛑 A VENDOR IS AN IDENTITY, which `supplierName` never was
|
|
6
|
+
*
|
|
7
|
+
* `ShipmentDocument.supplierName` is free text, so four shipments from one
|
|
8
|
+
* supplier are four unjoined strings and "what have we spent with Acme" is
|
|
9
|
+
* unanswerable. That is not a reporting nicety: it is also the join that
|
|
10
|
+
* makes input GST claimable per supplier, which is what a GSTR-2B
|
|
11
|
+
* reconciliation needs.
|
|
12
|
+
*
|
|
13
|
+
* ## 🛑 An expense is TRUTH; its journal entry is the projection
|
|
14
|
+
*
|
|
15
|
+
* Same split as orders and payouts. The expense row is what somebody
|
|
16
|
+
* actually paid for, with the bill attached; the ledger says how it was
|
|
17
|
+
* classified. Re-classifying an expense posts a reversal and a new entry —
|
|
18
|
+
* it never edits the old one — so the two stay reconcilable.
|
|
19
|
+
*/
|
|
20
|
+
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
21
|
+
export declare const VENDORS_COLLECTION = "vendors";
|
|
22
|
+
export declare const EXPENSES_COLLECTION = "expenses";
|
|
23
|
+
export interface VendorDocument extends BaseDocument {
|
|
24
|
+
/** `vendor-{slug}` — a pure slug, like every other named entity here. */
|
|
25
|
+
name: string;
|
|
26
|
+
/**
|
|
27
|
+
* 🛑 The GSTIN is what makes input tax claimable, and it is NOT PII.
|
|
28
|
+
*
|
|
29
|
+
* It is a business registration number, printed on every invoice the
|
|
30
|
+
* vendor issues and published in the GST portal's public search. Treating
|
|
31
|
+
* it as PII would mean encrypting the one field a GSTR-2B reconciliation
|
|
32
|
+
* has to match on — which would make the reconciliation impossible for no
|
|
33
|
+
* privacy gain.
|
|
34
|
+
*/
|
|
35
|
+
gstin?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Where the vendor is registered, for the place-of-supply rule.
|
|
38
|
+
*
|
|
39
|
+
* An intra-state purchase splits input tax into CGST + SGST and an
|
|
40
|
+
* inter-state one is IGST; they are different ledger accounts and a wrong
|
|
41
|
+
* one is a wrong return. Optional because a vendor with no GSTIN charges
|
|
42
|
+
* no GST and the question does not arise.
|
|
43
|
+
*/
|
|
44
|
+
stateCode?: string;
|
|
45
|
+
notes?: string;
|
|
46
|
+
isActive: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* What the money was for. Drives the expense account the posting hits.
|
|
50
|
+
*
|
|
51
|
+
* 🛑 A closed union rather than free text, and `other` is a real member
|
|
52
|
+
* precisely so the exception stays countable — "how much of our spend is
|
|
53
|
+
* unclassified" is one query, and a growing answer is the signal to add a
|
|
54
|
+
* category rather than a reason to allow typing one.
|
|
55
|
+
*/
|
|
56
|
+
export type ExpenseCategory = "freight" | "platform-software" | "wages" | "packaging" | "asset" | "other";
|
|
57
|
+
export interface ExpenseDocument extends BaseDocument {
|
|
58
|
+
vendorId?: string;
|
|
59
|
+
/** As written on the bill, for the same reason `supplierName` survives. */
|
|
60
|
+
vendorName: string;
|
|
61
|
+
category: ExpenseCategory;
|
|
62
|
+
/** The date on the BILL, not when it was entered — the period follows this. */
|
|
63
|
+
billDate: Date;
|
|
64
|
+
billNumber?: string;
|
|
65
|
+
/** Excluding tax. */
|
|
66
|
+
amount: number;
|
|
67
|
+
/**
|
|
68
|
+
* Input GST on this bill, claimable as ITC.
|
|
69
|
+
*
|
|
70
|
+
* 🛑 Stored SEPARATELY from `amount` rather than as a rate, because the
|
|
71
|
+
* figure on the bill is authoritative and re-deriving it from a
|
|
72
|
+
* percentage reproduces the supplier's rounding wrongly about as often as
|
|
73
|
+
* it reproduces it rightly — and the difference is what a GSTR-2B
|
|
74
|
+
* mismatch is made of.
|
|
75
|
+
*/
|
|
76
|
+
inputGst?: number;
|
|
77
|
+
/** Set once posted, so a re-run cannot double-post. */
|
|
78
|
+
journalEntryId?: string;
|
|
79
|
+
attachmentUrl?: string;
|
|
80
|
+
recordedByUid: string;
|
|
81
|
+
}
|
|
82
|
+
export declare const EXPENSE_FIELDS: {
|
|
83
|
+
readonly VENDOR_ID: "vendorId";
|
|
84
|
+
readonly CATEGORY: "category";
|
|
85
|
+
readonly BILL_DATE: "billDate";
|
|
86
|
+
readonly JOURNAL_ENTRY_ID: "journalEntryId";
|
|
87
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vendors` + `expenses` — money that leaves the business for something
|
|
3
|
+
* other than a payout or a refund.
|
|
4
|
+
*
|
|
5
|
+
* ## 🛑 A VENDOR IS AN IDENTITY, which `supplierName` never was
|
|
6
|
+
*
|
|
7
|
+
* `ShipmentDocument.supplierName` is free text, so four shipments from one
|
|
8
|
+
* supplier are four unjoined strings and "what have we spent with Acme" is
|
|
9
|
+
* unanswerable. That is not a reporting nicety: it is also the join that
|
|
10
|
+
* makes input GST claimable per supplier, which is what a GSTR-2B
|
|
11
|
+
* reconciliation needs.
|
|
12
|
+
*
|
|
13
|
+
* ## 🛑 An expense is TRUTH; its journal entry is the projection
|
|
14
|
+
*
|
|
15
|
+
* Same split as orders and payouts. The expense row is what somebody
|
|
16
|
+
* actually paid for, with the bill attached; the ledger says how it was
|
|
17
|
+
* classified. Re-classifying an expense posts a reversal and a new entry —
|
|
18
|
+
* it never edits the old one — so the two stay reconcilable.
|
|
19
|
+
*/
|
|
20
|
+
export const VENDORS_COLLECTION = "vendors";
|
|
21
|
+
export const EXPENSES_COLLECTION = "expenses";
|
|
22
|
+
export const EXPENSE_FIELDS = {
|
|
23
|
+
VENDOR_ID: "vendorId",
|
|
24
|
+
CATEGORY: "category",
|
|
25
|
+
BILL_DATE: "billDate",
|
|
26
|
+
JOURNAL_ENTRY_ID: "journalEntryId",
|
|
27
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chartOfAccounts` + `journalEntries` — Part 4's double-entry books.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 THE LEDGER IS A PROJECTION. Orders, payouts and expenses are truth.
|
|
5
|
+
*
|
|
6
|
+
* The same split `obligations` takes, and for the same reason. An order says
|
|
7
|
+
* what was sold; a journal entry is an accounting *statement about* that
|
|
8
|
+
* order. So: **never read a balance to make an operational decision.** Do not
|
|
9
|
+
* gate a payout on a ledger figure, do not refuse a refund because an account
|
|
10
|
+
* looks short — read the payout and the order. A projection that starts
|
|
11
|
+
* making decisions becomes a second source of truth, and then the two
|
|
12
|
+
* disagree with no way to say which is wrong.
|
|
13
|
+
*
|
|
14
|
+
* ## 🛑 Every entry is MACHINE-GENERATED from a business event
|
|
15
|
+
*
|
|
16
|
+
* This is the one constraint that makes the whole thing safe. If a human has
|
|
17
|
+
* to remember that a gateway fee is `Dr Bank Charges / Cr Bank`, it will
|
|
18
|
+
* eventually be posted the other way round; if the settlement ingest posts
|
|
19
|
+
* it, it cannot be. `sourceEvent` is REQUIRED and names the event, not the
|
|
20
|
+
* person — a hand-posted adjustment uses `"manual-journal"`, which is a
|
|
21
|
+
* distinguishable value precisely so the exception stays countable.
|
|
22
|
+
*
|
|
23
|
+
* ## 🛑 A correction is a REVERSING ENTRY, never an edit
|
|
24
|
+
*
|
|
25
|
+
* The invoice/credit-note rule, applied to the books. `journalEntries` rows
|
|
26
|
+
* are append-only: `reverse()` writes a NEW entry with the legs swapped and
|
|
27
|
+
* the two pointed at each other. An edit would silently rewrite a period a
|
|
28
|
+
* GST return has already been filed for, and nothing downstream could tell.
|
|
29
|
+
*
|
|
30
|
+
* ## 🛑 Money is DECIMAL RUPEES, matching every other money field here
|
|
31
|
+
*
|
|
32
|
+
* Rupees with two decimal places, like every other money field here. The
|
|
33
|
+
* 2026-05 migration moved the whole codebase off minor units and
|
|
34
|
+
* `audit-money-units` enforces it; a ledger in minor units would be the one
|
|
35
|
+
* place a reader has to remember a different scale, which is how a 100×
|
|
36
|
+
* error gets written (Root Cause #31).
|
|
37
|
+
*/
|
|
38
|
+
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
39
|
+
export declare const CHART_OF_ACCOUNTS_COLLECTION = "chartOfAccounts";
|
|
40
|
+
export declare const JOURNAL_ENTRIES_COLLECTION = "journalEntries";
|
|
41
|
+
export declare const ACCOUNTING_PERIODS_COLLECTION = "accountingPeriods";
|
|
42
|
+
/**
|
|
43
|
+
* The five classical account types.
|
|
44
|
+
*
|
|
45
|
+
* Carried on the ACCOUNT rather than inferred from its code, because the
|
|
46
|
+
* normal-balance side and every report's sign convention derive from it —
|
|
47
|
+
* and a report that guesses the type from a code prefix breaks the first time
|
|
48
|
+
* somebody adds an account out of range.
|
|
49
|
+
*/
|
|
50
|
+
export type AccountType = "asset" | "liability" | "equity" | "income" | "expense";
|
|
51
|
+
/**
|
|
52
|
+
* Which side INCREASES this account.
|
|
53
|
+
*
|
|
54
|
+
* Derived from `AccountType` by `normalBalanceFor`, never stored — a stored
|
|
55
|
+
* copy is a mirror that drifts the first time somebody types the wrong one
|
|
56
|
+
* (Root Cause #42), and the mapping is a fact about accounting rather than
|
|
57
|
+
* about this business.
|
|
58
|
+
*/
|
|
59
|
+
export type NormalBalance = "debit" | "credit";
|
|
60
|
+
export declare function normalBalanceFor(type: AccountType): NormalBalance;
|
|
61
|
+
export interface ChartAccountDocument extends BaseDocument {
|
|
62
|
+
/**
|
|
63
|
+
* Stable numeric code, e.g. `"1100"`. The id is `account-{code}`.
|
|
64
|
+
*
|
|
65
|
+
* 🛑 Codes are grouped by type in the conventional Indian small-business
|
|
66
|
+
* ranges (1xxx asset, 2xxx liability, 3xxx equity, 4xxx income, 5xxx
|
|
67
|
+
* expense) **so a CA can map them to Tally at a glance** — that is the
|
|
68
|
+
* entire reason for a numeric code rather than a slug. It is a convention
|
|
69
|
+
* for humans; nothing in the code derives `type` from the range, because a
|
|
70
|
+
* derivation would make adding an account outside it silently wrong.
|
|
71
|
+
*/
|
|
72
|
+
code: string;
|
|
73
|
+
name: string;
|
|
74
|
+
type: AccountType;
|
|
75
|
+
/** A one-line note for whoever has to reconcile this against a bank statement. */
|
|
76
|
+
description?: string;
|
|
77
|
+
/**
|
|
78
|
+
* A closed account refuses new postings but keeps its history.
|
|
79
|
+
*
|
|
80
|
+
* Deliberately not a delete: an account with entries against it cannot be
|
|
81
|
+
* removed without orphaning them, and a closed row still has to appear in
|
|
82
|
+
* last year's trial balance.
|
|
83
|
+
*/
|
|
84
|
+
isActive: boolean;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* One side of one entry.
|
|
88
|
+
*
|
|
89
|
+
* 🛑 `debit` and `credit` are SEPARATE non-negative fields rather than one
|
|
90
|
+
* signed amount. A signed amount needs a sign convention, the convention
|
|
91
|
+
* differs per account type, and a reader then has to know the account's type
|
|
92
|
+
* to interpret the number — which is how a credit gets read as a debit. Two
|
|
93
|
+
* fields are unambiguous at the point of reading, which is where it matters.
|
|
94
|
+
*
|
|
95
|
+
* Exactly one of them is non-zero on any line; `assertBalanced` refuses both.
|
|
96
|
+
*/
|
|
97
|
+
export interface JournalLine {
|
|
98
|
+
accountCode: string;
|
|
99
|
+
debit: number;
|
|
100
|
+
credit: number;
|
|
101
|
+
/** Why this leg exists, for a human reading the entry a year later. */
|
|
102
|
+
memo?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Who this leg is ABOUT, when that is a specific counterparty — a store for
|
|
105
|
+
* a seller payable, a vendor for a bill.
|
|
106
|
+
*
|
|
107
|
+
* Optional because most legs (bank, GST, sales) have no counterparty. Where
|
|
108
|
+
* it exists it is what makes a per-seller payable ageing possible without a
|
|
109
|
+
* separate ledger per store.
|
|
110
|
+
*/
|
|
111
|
+
counterpartyId?: string;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* What caused this entry. REQUIRED, and the closed list is the point.
|
|
115
|
+
*
|
|
116
|
+
* 🛑 `manual-journal` is a real member and must stay one. The alternative —
|
|
117
|
+
* letting a hand-posted entry claim an automated source — is how the "every
|
|
118
|
+
* entry is machine-generated" constraint stops being checkable. With it
|
|
119
|
+
* declared, "how many of our entries were typed by a person" is one query.
|
|
120
|
+
*/
|
|
121
|
+
export type JournalSourceEvent = "order-placed" | "order-delivered" | "order-refunded" | "payout-paid" | "settlement-ingest" | "expense-recorded" | "asset-purchased" | "opening-balance" | "manual-journal" | "reversal";
|
|
122
|
+
export interface JournalEntryDocument extends BaseDocument {
|
|
123
|
+
/**
|
|
124
|
+
* The date the entry BELONGS TO, which is not when it was written.
|
|
125
|
+
*
|
|
126
|
+
* A settlement ingested on the 3rd for transactions on the 31st posts to
|
|
127
|
+
* the 31st. `createdAt` records when we learned; this records when it
|
|
128
|
+
* happened, and every report and the period lock read this one.
|
|
129
|
+
*/
|
|
130
|
+
entryDate: Date;
|
|
131
|
+
/** `YYYY-MM`, derived from `entryDate` — the lock and the rollups key on it. */
|
|
132
|
+
period: string;
|
|
133
|
+
sourceEvent: JournalSourceEvent;
|
|
134
|
+
/**
|
|
135
|
+
* The record that caused this — an order id, a payout id, an expense id.
|
|
136
|
+
*
|
|
137
|
+
* Absent only for `opening-balance` and `manual-journal`, which have no
|
|
138
|
+
* causing record by definition. `verify-posting-rules` checks that pairing
|
|
139
|
+
* rather than trusting it.
|
|
140
|
+
*/
|
|
141
|
+
sourceId?: string;
|
|
142
|
+
narration: string;
|
|
143
|
+
lines: JournalLine[];
|
|
144
|
+
/**
|
|
145
|
+
* Both totals, stored.
|
|
146
|
+
*
|
|
147
|
+
* 🛑 Redundant with `lines` ON PURPOSE, and this is the one place a mirror
|
|
148
|
+
* is right: a trial balance that had to sum every line of every entry is an
|
|
149
|
+
* unbounded read, and these two are written inside the same transaction
|
|
150
|
+
* that validated them. They cannot drift, because nothing can write an
|
|
151
|
+
* entry without going through `assertBalanced`.
|
|
152
|
+
*/
|
|
153
|
+
totalDebit: number;
|
|
154
|
+
totalCredit: number;
|
|
155
|
+
/** Set on the entry this one reverses, and on the reversal, pointing both ways. */
|
|
156
|
+
reversedByEntryId?: string;
|
|
157
|
+
reversesEntryId?: string;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* A period, and whether it is closed.
|
|
161
|
+
*
|
|
162
|
+
* Id is the period itself (`period-2026-10`), so the lock check is a single
|
|
163
|
+
* `get` by a key the caller already has — not a query.
|
|
164
|
+
*/
|
|
165
|
+
export interface AccountingPeriodDocument extends BaseDocument {
|
|
166
|
+
/** `YYYY-MM`. */
|
|
167
|
+
period: string;
|
|
168
|
+
status: "open" | "locked";
|
|
169
|
+
/** Set when locked — which GST return this period was filed under. */
|
|
170
|
+
lockedReason?: string;
|
|
171
|
+
lockedAt?: Date;
|
|
172
|
+
lockedByUid?: string;
|
|
173
|
+
}
|
|
174
|
+
export declare const LEDGER_FIELDS: {
|
|
175
|
+
readonly CODE: "code";
|
|
176
|
+
readonly TYPE: "type";
|
|
177
|
+
readonly IS_ACTIVE: "isActive";
|
|
178
|
+
readonly ENTRY_DATE: "entryDate";
|
|
179
|
+
readonly PERIOD: "period";
|
|
180
|
+
readonly SOURCE_EVENT: "sourceEvent";
|
|
181
|
+
readonly SOURCE_ID: "sourceId";
|
|
182
|
+
readonly STATUS: "status";
|
|
183
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chartOfAccounts` + `journalEntries` — Part 4's double-entry books.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 THE LEDGER IS A PROJECTION. Orders, payouts and expenses are truth.
|
|
5
|
+
*
|
|
6
|
+
* The same split `obligations` takes, and for the same reason. An order says
|
|
7
|
+
* what was sold; a journal entry is an accounting *statement about* that
|
|
8
|
+
* order. So: **never read a balance to make an operational decision.** Do not
|
|
9
|
+
* gate a payout on a ledger figure, do not refuse a refund because an account
|
|
10
|
+
* looks short — read the payout and the order. A projection that starts
|
|
11
|
+
* making decisions becomes a second source of truth, and then the two
|
|
12
|
+
* disagree with no way to say which is wrong.
|
|
13
|
+
*
|
|
14
|
+
* ## 🛑 Every entry is MACHINE-GENERATED from a business event
|
|
15
|
+
*
|
|
16
|
+
* This is the one constraint that makes the whole thing safe. If a human has
|
|
17
|
+
* to remember that a gateway fee is `Dr Bank Charges / Cr Bank`, it will
|
|
18
|
+
* eventually be posted the other way round; if the settlement ingest posts
|
|
19
|
+
* it, it cannot be. `sourceEvent` is REQUIRED and names the event, not the
|
|
20
|
+
* person — a hand-posted adjustment uses `"manual-journal"`, which is a
|
|
21
|
+
* distinguishable value precisely so the exception stays countable.
|
|
22
|
+
*
|
|
23
|
+
* ## 🛑 A correction is a REVERSING ENTRY, never an edit
|
|
24
|
+
*
|
|
25
|
+
* The invoice/credit-note rule, applied to the books. `journalEntries` rows
|
|
26
|
+
* are append-only: `reverse()` writes a NEW entry with the legs swapped and
|
|
27
|
+
* the two pointed at each other. An edit would silently rewrite a period a
|
|
28
|
+
* GST return has already been filed for, and nothing downstream could tell.
|
|
29
|
+
*
|
|
30
|
+
* ## 🛑 Money is DECIMAL RUPEES, matching every other money field here
|
|
31
|
+
*
|
|
32
|
+
* Rupees with two decimal places, like every other money field here. The
|
|
33
|
+
* 2026-05 migration moved the whole codebase off minor units and
|
|
34
|
+
* `audit-money-units` enforces it; a ledger in minor units would be the one
|
|
35
|
+
* place a reader has to remember a different scale, which is how a 100×
|
|
36
|
+
* error gets written (Root Cause #31).
|
|
37
|
+
*/
|
|
38
|
+
export const CHART_OF_ACCOUNTS_COLLECTION = "chartOfAccounts";
|
|
39
|
+
export const JOURNAL_ENTRIES_COLLECTION = "journalEntries";
|
|
40
|
+
export const ACCOUNTING_PERIODS_COLLECTION = "accountingPeriods";
|
|
41
|
+
export function normalBalanceFor(type) {
|
|
42
|
+
return type === "asset" || type === "expense" ? "debit" : "credit";
|
|
43
|
+
}
|
|
44
|
+
export const LEDGER_FIELDS = {
|
|
45
|
+
CODE: "code",
|
|
46
|
+
TYPE: "type",
|
|
47
|
+
IS_ACTIVE: "isActive",
|
|
48
|
+
ENTRY_DATE: "entryDate",
|
|
49
|
+
PERIOD: "period",
|
|
50
|
+
SOURCE_EVENT: "sourceEvent",
|
|
51
|
+
SOURCE_ID: "sourceId",
|
|
52
|
+
STATUS: "status",
|
|
53
|
+
};
|
|
54
|
+
/*
|
|
55
|
+
* 🛑 `periodOf` LIVES IN `_internal/shared/features/ledger/posting.ts`, not
|
|
56
|
+
* here, and the reason is mechanical rather than taxonomic.
|
|
57
|
+
*
|
|
58
|
+
* `verify-posting-rules.mjs` imports the posting rules directly via Node's
|
|
59
|
+
* `--experimental-strip-types` so it exercises the shipped source rather
|
|
60
|
+
* than a build artefact (Root Cause #105). Type stripping erases
|
|
61
|
+
* `import type` entirely but cannot RESOLVE a runtime import — appkit source
|
|
62
|
+
* is written extensionless and the build adds `.js` — so a single runtime
|
|
63
|
+
* import in that file would make it unloadable and the audit would be a
|
|
64
|
+
* load-failure instead of a check.
|
|
65
|
+
*
|
|
66
|
+
* Deciding which period an entry belongs to is a posting rule anyway, and it
|
|
67
|
+
* is the one the audit most needs to exercise: a UTC derivation files every
|
|
68
|
+
* IST evening sale in the previous GST return.
|
|
69
|
+
*/
|