@mohasinac/appkit 4.62.1 โ 4.63.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.build-stamp.json +3 -3
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +8 -29
- package/dist/_internal/client/features/layout/filterNavItems.d.ts +28 -0
- package/dist/_internal/client/features/layout/filterNavItems.js +35 -0
- package/dist/_internal/server/features/ads/booking.d.ts +118 -0
- package/dist/_internal/server/features/ads/booking.js +326 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +51 -11
- package/dist/_internal/server/features/checkout/actions.js +335 -80
- package/dist/_internal/server/features/checkout/expand-order-items.d.ts +1 -2
- package/dist/_internal/server/features/checkout/expand-order-items.js +32 -0
- package/dist/_internal/server/features/checkout/freeze-parts.d.ts +50 -0
- package/dist/_internal/server/features/checkout/freeze-parts.js +94 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.d.ts +55 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.js +60 -0
- package/dist/_internal/server/features/dashboard/attention.d.ts +128 -0
- package/dist/_internal/server/features/dashboard/attention.js +183 -0
- package/dist/_internal/server/features/dashboard/data.d.ts +47 -0
- package/dist/_internal/server/features/dashboard/data.js +82 -0
- package/dist/_internal/server/features/inventory/reservations.d.ts +99 -0
- package/dist/_internal/server/features/inventory/reservations.js +273 -0
- package/dist/_internal/server/features/offers/destinations.d.ts +40 -0
- package/dist/_internal/server/features/offers/destinations.js +65 -0
- package/dist/_internal/server/features/orders/adapters.js +21 -0
- package/dist/_internal/server/features/orders/amend-delivery.d.ts +56 -0
- package/dist/_internal/server/features/orders/amend-delivery.js +124 -0
- package/dist/_internal/server/features/orders/balance.d.ts +89 -0
- package/dist/_internal/server/features/orders/balance.js +256 -0
- package/dist/_internal/server/features/pricing/actions.d.ts +58 -0
- package/dist/_internal/server/features/pricing/actions.js +144 -0
- package/dist/_internal/server/features/shipping/book-split.d.ts +86 -0
- package/dist/_internal/server/features/shipping/book-split.js +130 -0
- package/dist/_internal/server/features/shipping/book.d.ts +61 -0
- package/dist/_internal/server/features/shipping/book.js +176 -0
- package/dist/_internal/server/features/shipping/courier-block.js +26 -1
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +14 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +14 -0
- package/dist/_internal/server/functions/scheduled.d.ts +2 -1
- package/dist/_internal/server/functions/scheduled.js +57 -1
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +2 -0
- package/dist/_internal/server/jobs/core/jobRunners.js +45 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +4 -1
- package/dist/_internal/server/jobs/core/reservationExpiry.d.ts +17 -0
- package/dist/_internal/server/jobs/core/reservationExpiry.js +40 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +15 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
- package/dist/_internal/server/jobs/handlers/index.js +1 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.js +2 -0
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +268 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +31 -2
- package/dist/_internal/shared/checkout/order-math.js +22 -36
- package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
- package/dist/_internal/shared/checkout/rules/index.js +1 -1
- package/dist/_internal/shared/checkout/rules/payment-gate.d.ts +18 -0
- package/dist/_internal/shared/checkout/rules/payment-gate.js +25 -0
- package/dist/_internal/shared/checkout/split.d.ts +0 -12
- package/dist/_internal/shared/checkout/split.js +22 -2
- package/dist/_internal/shared/content/disclaimers.js +7 -7
- package/dist/_internal/shared/content/published-values.d.ts +6 -1
- package/dist/_internal/shared/content/published-values.js +8 -0
- package/dist/_internal/shared/features/admin/integrations.d.ts +32 -2
- package/dist/_internal/shared/features/admin/integrations.js +19 -9
- package/dist/_internal/shared/features/dashboard/hub-sections.d.ts +72 -0
- package/dist/_internal/shared/features/dashboard/hub-sections.js +75 -0
- package/dist/_internal/shared/features/emi/choice.d.ts +98 -0
- package/dist/_internal/shared/features/emi/choice.js +69 -0
- package/dist/_internal/shared/features/emi/group.d.ts +84 -0
- package/dist/_internal/shared/features/emi/group.js +126 -0
- package/dist/_internal/shared/features/ledger/posting.d.ts +107 -0
- package/dist/_internal/shared/features/ledger/posting.js +165 -0
- package/dist/_internal/shared/features/ledger/reports.d.ts +76 -0
- package/dist/_internal/shared/features/ledger/reports.js +127 -0
- package/dist/_internal/shared/features/ledger/rules.d.ts +170 -0
- package/dist/_internal/shared/features/ledger/rules.js +221 -0
- package/dist/_internal/shared/features/offers/coordination.d.ts +122 -0
- package/dist/_internal/shared/features/offers/coordination.js +80 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.d.ts +114 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.js +116 -0
- package/dist/_internal/shared/features/payouts/pay-period.d.ts +91 -0
- package/dist/_internal/shared/features/payouts/pay-period.js +104 -0
- package/dist/_internal/shared/features/points/accepts.d.ts +1 -8
- package/dist/_internal/shared/features/points/accepts.js +12 -6
- package/dist/_internal/shared/features/pricing/rate-registry.d.ts +150 -0
- package/dist/_internal/shared/features/pricing/rate-registry.js +246 -0
- package/dist/_internal/shared/features/shipping/parcel.d.ts +123 -0
- package/dist/_internal/shared/features/shipping/parcel.js +139 -0
- package/dist/_internal/shared/features/site-settings/fees.d.ts +16 -8
- package/dist/_internal/shared/features/site-settings/fees.js +12 -8
- package/dist/_internal/shared/fees/explain.d.ts +81 -0
- package/dist/_internal/shared/fees/explain.js +101 -0
- package/dist/_internal/shared/history/index.d.ts +1 -1
- package/dist/_internal/shared/history/index.js +1 -1
- package/dist/_internal/shared/history/types.d.ts +7 -1
- package/dist/_internal/shared/history/types.js +25 -0
- package/dist/client.d.ts +11 -1
- package/dist/client.js +13 -0
- package/dist/constants/api-endpoints.d.ts +72 -0
- package/dist/constants/api-endpoints.js +41 -0
- package/dist/constants/field-names.d.ts +41 -0
- package/dist/constants/field-names.js +47 -0
- package/dist/features/account/components/UserOrdersView.js +28 -2
- package/dist/features/admin/components/AdminIntegrationsView.d.ts +55 -0
- package/dist/features/admin/components/AdminIntegrationsView.js +74 -0
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +16 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +72 -3
- package/dist/features/admin/components/AdminOrdersView.js +20 -1
- package/dist/features/admin/schemas/firestore.d.ts +32 -0
- package/dist/features/ads/schemas/booking-write.d.ts +157 -0
- package/dist/features/ads/schemas/booking-write.js +88 -0
- package/dist/features/ads/schemas/creative-form.d.ts +18 -0
- package/dist/features/ads/schemas/creative-form.js +51 -0
- package/dist/features/ads/schemas/firestore.d.ts +140 -0
- package/dist/features/ads/schemas/firestore.js +109 -0
- package/dist/features/auctions/repository/bid.repository.js +3 -1
- package/dist/features/audit-log/schemas/firestore.d.ts +110 -0
- package/dist/features/audit-log/schemas/firestore.js +74 -0
- package/dist/features/auth/actions/profile-actions.d.ts +69 -1
- package/dist/features/auth/actions/profile-actions.js +62 -1
- package/dist/features/auth/schemas/firestore.d.ts +16 -2
- package/dist/features/auth/schemas/firestore.js +2 -1
- package/dist/features/blog/actions/blog-actions.d.ts +6 -0
- package/dist/features/blog/actions/blog-actions.js +17 -2
- package/dist/features/blog/api/route.js +4 -1
- package/dist/features/blog/columns/index.js +5 -0
- package/dist/features/blog/components/BlogIndexPageView.js +2 -1
- package/dist/features/blog/repository/blog.repository.d.ts +4 -0
- package/dist/features/blog/repository/blog.repository.js +5 -0
- package/dist/features/blog/schemas/blog-form.d.ts +3 -0
- package/dist/features/blog/schemas/blog-form.js +1 -0
- package/dist/features/blog/schemas/firestore.d.ts +38 -1
- package/dist/features/blog/schemas/firestore.js +23 -0
- package/dist/features/blog/schemas/index.d.ts +6 -0
- package/dist/features/blog/schemas/index.js +2 -0
- package/dist/features/blog/types/index.d.ts +2 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +12 -1
- package/dist/features/cart/components/CartPriceBreakdown.js +24 -5
- package/dist/features/cart/repository/cart.repository.d.ts +14 -0
- package/dist/features/cart/repository/cart.repository.js +20 -0
- package/dist/features/cart/schemas/firestore.d.ts +24 -0
- package/dist/features/dashboard/components/DashboardHubView.d.ts +83 -0
- package/dist/features/dashboard/components/DashboardHubView.js +56 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.d.ts +23 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.js +66 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.d.ts +55 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.js +175 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +24 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +28 -0
- package/dist/features/dispute-reasons/schemas/firestore.d.ts +3 -5
- package/dist/features/events/components/EventSaleBanner.js +1 -1
- package/dist/features/faq/components/FAQPageContent.js +2 -2
- package/dist/features/gateway-events/schemas/index.d.ts +2 -2
- package/dist/features/homepage/schemas/index.d.ts +8 -8
- package/dist/features/inventory/schemas/stock-reservation.d.ts +114 -0
- package/dist/features/inventory/schemas/stock-reservation.js +103 -0
- package/dist/features/layout/AppLayoutShell.d.ts +9 -1
- package/dist/features/layout/AppLayoutShell.js +12 -4
- package/dist/features/layout/BottomActions.js +2 -1
- package/dist/features/layout/BottomNavbar.d.ts +10 -2
- package/dist/features/layout/SideActionDock.js +52 -2
- package/dist/features/layout/TitleBarLayout.js +13 -4
- package/dist/features/layout/TitleBarLayout.style.css +59 -0
- package/dist/features/layout/useCtaReveal.d.ts +16 -1
- package/dist/features/layout/useCtaReveal.js +17 -26
- package/dist/features/ledger/repository/journal.repository.d.ts +110 -0
- package/dist/features/ledger/repository/journal.repository.js +312 -0
- package/dist/features/ledger/schemas/expenses.d.ts +87 -0
- package/dist/features/ledger/schemas/expenses.js +27 -0
- package/dist/features/ledger/schemas/firestore.d.ts +183 -0
- package/dist/features/ledger/schemas/firestore.js +69 -0
- package/dist/features/orders/components/BalancePaymentPanel.d.ts +12 -0
- package/dist/features/orders/components/BalancePaymentPanel.js +94 -0
- package/dist/features/orders/components/RaiseDisputeForm.js +19 -11
- package/dist/features/orders/repository/orders.repository.d.ts +28 -1
- package/dist/features/orders/repository/orders.repository.js +60 -6
- package/dist/features/orders/schemas/balance.d.ts +40 -0
- package/dist/features/orders/schemas/balance.js +47 -0
- package/dist/features/orders/schemas/firestore.d.ts +202 -0
- package/dist/features/orders/schemas/firestore.js +43 -0
- package/dist/features/orders/schemas/index.d.ts +2 -2
- package/dist/features/orders/types/index.d.ts +21 -0
- package/dist/features/payments/repository/payout.repository.js +3 -2
- package/dist/features/payments/schemas/firestore.d.ts +92 -1
- package/dist/features/payments/schemas/firestore.js +23 -0
- package/dist/features/pricing/components/AdminPricingView.d.ts +13 -0
- package/dist/features/pricing/components/AdminPricingView.js +157 -0
- package/dist/features/products/schemas/firestore.d.ts +26 -1
- package/dist/features/products/schemas/firestore.js +2 -0
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/reviews/repository/reviews.repository.d.ts +14 -0
- package/dist/features/reviews/repository/reviews.repository.js +16 -0
- package/dist/features/scams/repository/scammer.repository.js +2 -1
- package/dist/features/scams/schemas/firestore.d.ts +2 -1
- package/dist/features/scams/schemas/firestore.js +2 -1
- package/dist/features/seller/actions/offer-actions.js +56 -0
- package/dist/features/seller/actions/seller-actions.d.ts +1 -1
- package/dist/features/seller/actions/seller-actions.js +4 -0
- package/dist/features/seller/components/DispatchForm.d.ts +11 -0
- package/dist/features/seller/components/DispatchForm.js +190 -0
- package/dist/features/seller/components/SellerOrdersView.js +94 -2
- package/dist/features/seller/components/index.d.ts +1 -0
- package/dist/features/seller/components/index.js +1 -0
- package/dist/features/seller/repository/offer.repository.js +4 -3
- package/dist/features/seller/schemas/firestore.d.ts +23 -2
- package/dist/features/seller/schemas/firestore.js +3 -2
- package/dist/features/seller/schemas/index.d.ts +4 -4
- package/dist/features/seller/schemas/order-forms.d.ts +61 -4
- package/dist/features/seller/schemas/order-forms.js +41 -8
- package/dist/features/shipments/schemas/firestore.d.ts +25 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +39 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +55 -0
- package/dist/features/shipping-boxes/schemas/firestore.d.ts +2 -4
- package/dist/features/stores/repository/store.repository.d.ts +18 -0
- package/dist/features/stores/repository/store.repository.js +27 -1
- package/dist/features/stores/schemas/firestore.d.ts +41 -0
- package/dist/features/stores/schemas/firestore.js +20 -0
- package/dist/features/stores/schemas/index.d.ts +6 -6
- package/dist/features/support/repository/support.repository.js +72 -6
- package/dist/features/support/schemas/firestore.d.ts +17 -0
- package/dist/features/support/schemas/index.d.ts +2 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +19 -0
- package/dist/next/routing/route-map.d.ts +92 -0
- package/dist/next/routing/route-map.js +53 -0
- package/dist/react/contexts/SessionContext.d.ts +17 -1
- package/dist/schemas/registry.d.ts +22 -16
- package/dist/security/pii-schemas.d.ts +1 -1
- package/dist/security/pii-schemas.js +45 -0
- package/dist/server-entry.d.ts +1 -1
- package/dist/server-entry.js +10 -1
- package/dist/server.d.ts +29 -1
- package/dist/server.js +158 -0
- package/dist/styles.css +349 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Button.d.ts +10 -1
- package/dist/ui/components/Button.js +25 -3
- package/dist/ui/components/Chart.d.ts +63 -0
- package/dist/ui/components/Chart.js +169 -0
- package/dist/ui/components/Chart.style.css +214 -0
- package/dist/ui/components/Disclaimer.js +10 -1
- package/dist/ui/components/OverflowRow.js +54 -2
- package/dist/ui/components/OverflowRow.style.css +38 -0
- package/dist/ui/components/RichTextEditor.js +99 -8
- package/dist/ui/components/chart-spec.d.ts +68 -0
- package/dist/ui/components/chart-spec.js +140 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/components/surface-tokens.d.ts +1 -1
- package/dist/ui/components/surface-tokens.js +9 -0
- package/dist/ui/components/toolbar-collapse.d.ts +13 -0
- package/dist/ui/components/toolbar-collapse.js +10 -1
- package/dist/ui/index.d.ts +6 -0
- package/dist/ui/index.js +3 -0
- package/dist/ui/rich-text/RichText.d.ts +10 -1
- package/dist/ui/rich-text/RichText.js +33 -2
- package/dist/ui/rich-text/RichText.style.css +35 -0
- package/dist/ui/rich-text/RichTextRenderer.d.ts +30 -1
- package/dist/ui/rich-text/RichTextRenderer.js +34 -2
- package/dist/ui/rich-text/chart-blocks.d.ts +62 -0
- package/dist/ui/rich-text/chart-blocks.js +87 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +3 -0
|
@@ -0,0 +1,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
|
+
*/
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Order } from "../types";
|
|
2
|
+
export interface BalancePaymentPanelProps {
|
|
3
|
+
order: Pick<Order, "id" | "balance" | "currency">;
|
|
4
|
+
/** Uploads one file and resolves to the stored `/media/โฆ` slug. */
|
|
5
|
+
onUpload: (file: File) => Promise<string>;
|
|
6
|
+
/** POSTs to `/api/orders/{id}/balance-proof`. Throws on failure. */
|
|
7
|
+
onSubmit: (input: {
|
|
8
|
+
proofUrl: string;
|
|
9
|
+
transactionId: string;
|
|
10
|
+
}) => Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
export declare function BalancePaymentPanel({ order, onUpload, onSubmit }: BalancePaymentPanelProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* D55 โ the buyer's panel for paying a pre-order balance.
|
|
5
|
+
*
|
|
6
|
+
* ## ๐ This exists because the figure was already on screen
|
|
7
|
+
*
|
|
8
|
+
* `OrderProvenanceCard` has printed "Balance due โนX" on every pre-order since
|
|
9
|
+
* Part 2, and nothing in the product could accept that money. A buyer who
|
|
10
|
+
* reads an amount owed and finds no control does not conclude "the feature is
|
|
11
|
+
* coming" โ they conclude the site is broken, and the seller waiting on the
|
|
12
|
+
* money has no way to chase it.
|
|
13
|
+
*
|
|
14
|
+
* ## Why it is NOT a second page
|
|
15
|
+
*
|
|
16
|
+
* The deposit's proof upload is its own route (`/user/orders/[id]/payment`)
|
|
17
|
+
* because it runs against a 15-minute window with a countdown, a UPI address
|
|
18
|
+
* to copy and a fraud declaration โ a task with its own screen. The balance
|
|
19
|
+
* has a 7-day window and no countdown, so sending the buyer somewhere else to
|
|
20
|
+
* fill in two fields would add a navigation and a back button for nothing.
|
|
21
|
+
*
|
|
22
|
+
* ## The four states
|
|
23
|
+
*
|
|
24
|
+
* | `balance.status` | What the buyer sees |
|
|
25
|
+
* |---|---|
|
|
26
|
+
* | absent | NOTHING โ the seller has not opened collection; the goods do not exist yet |
|
|
27
|
+
* | `awaiting` / `rejected` | the form, with the rejection note when there is one |
|
|
28
|
+
* | `submitted` | "under review", no form |
|
|
29
|
+
* | `paid` | a confirmation line and the reference |
|
|
30
|
+
*
|
|
31
|
+
* ๐ The absent case rendering nothing is the load-bearing one. A pre-order's
|
|
32
|
+
* premise is that the item has not been made yet, so a payable-looking balance
|
|
33
|
+
* before the seller opens it would be asking the buyer to fund inventory.
|
|
34
|
+
*/
|
|
35
|
+
import { useState } from "react";
|
|
36
|
+
import { Button } from "../../../ui/components/Button.js";
|
|
37
|
+
import { Row, Stack } from "../../../ui/components/Layout.js";
|
|
38
|
+
import { Div } from "../../../ui/components/Div.js";
|
|
39
|
+
import { Text, Span } from "../../../ui/components/Typography.js";
|
|
40
|
+
import { Form } from "../../../ui/components/Form.js";
|
|
41
|
+
import { FieldInput } from "../../../ui/forms/FieldInput.js";
|
|
42
|
+
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary.js";
|
|
43
|
+
import { MediaUploadField } from "../../media/upload/MediaUploadField.js";
|
|
44
|
+
import { balanceProofSchema } from "../schemas/balance.js";
|
|
45
|
+
import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
46
|
+
function dueText(dueBy) {
|
|
47
|
+
if (!dueBy)
|
|
48
|
+
return "";
|
|
49
|
+
const d = new Date(dueBy);
|
|
50
|
+
if (Number.isNaN(d.getTime()))
|
|
51
|
+
return "";
|
|
52
|
+
return ` Due by ${d.toLocaleDateString("en-IN", { day: "numeric", month: "short", year: "numeric" })}.`;
|
|
53
|
+
}
|
|
54
|
+
export function BalancePaymentPanel({ order, onUpload, onSubmit }) {
|
|
55
|
+
const [proofUrl, setProofUrl] = useState("");
|
|
56
|
+
const [transactionId, setTransactionId] = useState("");
|
|
57
|
+
const [isPending, setIsPending] = useState(false);
|
|
58
|
+
const balance = order.balance;
|
|
59
|
+
// Nothing to collect yet โ see the header. Not an empty card, no card.
|
|
60
|
+
if (!balance)
|
|
61
|
+
return null;
|
|
62
|
+
const amount = formatCurrency(balance.amount, order.currency);
|
|
63
|
+
if (balance.status === "paid") {
|
|
64
|
+
return (_jsxs(Div, { surface: "card", padding: "md", className: "mt-4", children: [_jsx(Span, { layout: "inline-flex", color: "success", surface: "success-surface", weight: "semibold", className: "text-[10px]", rounded: "full", padding: "pill-xs", children: "Balance paid" }), _jsxs(Text, { className: "mt-1", color: "muted", size: "xs", children: [amount, " received. This order is ready to ship.", balance.transactionId ? ` Reference: ${balance.transactionId}` : ""] })] }));
|
|
65
|
+
}
|
|
66
|
+
if (balance.status === "submitted") {
|
|
67
|
+
return (_jsxs(Div, { surface: "card", padding: "md", className: "mt-4", children: [_jsx(Span, { layout: "inline-flex", color: "info", surface: "info-surface", weight: "semibold", className: "text-[10px]", rounded: "full", padding: "pill-xs", children: "Balance under review" }), _jsxs(Text, { className: "mt-1", color: "muted", size: "xs", children: ["We have your proof for ", amount, ". Our team checks it against the reference you gave, and the order ships once it clears."] })] }));
|
|
68
|
+
}
|
|
69
|
+
const rejected = balance.status === "rejected";
|
|
70
|
+
const handleSubmit = async () => {
|
|
71
|
+
setIsPending(true);
|
|
72
|
+
try {
|
|
73
|
+
await onSubmit({ proofUrl, transactionId: transactionId.trim() });
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
/*
|
|
77
|
+
* Cleared even on failure. The caller surfaces the error; leaving the
|
|
78
|
+
* button spinning forever is how a recoverable 400 reads as a hang.
|
|
79
|
+
*/
|
|
80
|
+
setIsPending(false);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
return (_jsx(Div, { surface: "card", padding: "md", className: "mt-4", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsx(Span, { layout: "inline-flex", color: rejected ? "error" : "warning", surface: rejected ? "danger-surface" : "warning-surface", weight: "semibold", className: "text-[10px]", rounded: "full", padding: "pill-xs", children: rejected ? "Balance proof needs correction" : "Balance due" }), _jsx(Text, { className: "mt-1", color: "muted", size: "xs", children: rejected
|
|
84
|
+
? /*
|
|
85
|
+
* The note is REQUIRED on a rejection server-side, so the
|
|
86
|
+
* fallback here should effectively never render โ it exists
|
|
87
|
+
* because a row written before that rule would otherwise say
|
|
88
|
+
* nothing at all, which is the one thing a rejection must not do.
|
|
89
|
+
*/
|
|
90
|
+
balance.reviewNote
|
|
91
|
+
? `Our team could not accept the last proof: ${balance.reviewNote}`
|
|
92
|
+
: "Our team could not accept the last proof. Please send a clearer screenshot."
|
|
93
|
+
: `Your item is ready. Pay the remaining ${amount} and upload the receipt.${dueText(balance.dueBy)}` })] }), _jsx(Form, { schema: balanceProofSchema, onSubmit: (e) => e.preventDefault(), children: () => (_jsxs(Stack, { gap: "md", children: [_jsx(FormErrorSummary, {}), _jsx(MediaUploadField, { label: "Payment screenshot (JPG/PNG/PDF)", value: proofUrl, onChange: setProofUrl, onUpload: onUpload, kind: "image", accept: "image/*,application/pdf", showYoutube: false, showExternal: false, helperText: "The confirmation screen from your bank or UPI app." }), _jsx(FieldInput, { name: "transactionId", label: "UTR / Transaction reference", required: true, value: transactionId, onChange: (value) => setTransactionId(value), placeholder: "e.g. 123456789012", hint: "Required \u2014 it is the only thing our team can check the payment against. A screenshot on its own is a picture." }), _jsx(Row, { gap: "3", children: _jsx(Button, { type: "button", variant: "primary", disabled: isPending || !proofUrl || transactionId.trim().length < 4, onClick: handleSubmit, paddingX: "md", paddingY: "sm", textSize: "sm", weight: "semibold", children: isPending ? "Sendingโฆ" : rejected ? "Re-send proof" : "Submit payment proof" }) })] })) })] }) }));
|
|
94
|
+
}
|
|
@@ -28,6 +28,12 @@ import { Alert, Button, Div, FieldTextarea, Form, Stack, Text, useToast } from "
|
|
|
28
28
|
import { PaginatedSelect } from "../../../ui/components/PaginatedSelect.js";
|
|
29
29
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
30
30
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
31
|
+
/*
|
|
32
|
+
* ๐ From the DEFINING module, not the `@mohasinac/appkit/client` barrel. This
|
|
33
|
+
* is a `"use client"` component so either resolves, but a barrel import drags
|
|
34
|
+
* the whole client entry's graph in for one symbol (Root Cause #18).
|
|
35
|
+
*/
|
|
36
|
+
import { apiClient } from "../../../http/ApiClient.js";
|
|
31
37
|
export function RaiseDisputeForm({ order, reasonsEndpoint, onSubmit, onDone, }) {
|
|
32
38
|
const { showToast } = useToast();
|
|
33
39
|
const [kind, setKind] = useState(null);
|
|
@@ -55,17 +61,19 @@ export function RaiseDisputeForm({ order, reasonsEndpoint, onSubmit, onDone, })
|
|
|
55
61
|
const loadReasons = useCallback(async (query, page) => {
|
|
56
62
|
if (!kind)
|
|
57
63
|
return { items: [], hasMore: false };
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
/*
|
|
65
|
+
* `apiClient`, not a bare `fetch` โ it carries the credentials and the
|
|
66
|
+
* error shape every other call in this feature uses, and the bare form
|
|
67
|
+
* is what `audit-form-mutation-hook` blocks inside a <Form>-rendering
|
|
68
|
+
* file. Same shape as `YourPurchaseStrip`'s read, one folder over.
|
|
69
|
+
*
|
|
70
|
+
* It THROWS on a non-2xx, which is the behaviour this call site already
|
|
71
|
+
* wanted: surfaced, not swallowed into an empty list. An empty picker is
|
|
72
|
+
* indistinguishable from "there are no reasons", and the buyer would sit
|
|
73
|
+
* on a required field with nothing in it (Root Cause #59).
|
|
74
|
+
*/
|
|
75
|
+
const json = await apiClient.get(`${reasonsEndpoint}?kind=${encodeURIComponent(kind)}`);
|
|
76
|
+
const rows = json?.data?.items ?? [];
|
|
69
77
|
const q = query.trim().toLowerCase();
|
|
70
78
|
const filtered = q ? rows.filter((r) => r.label.toLowerCase().includes(q)) : rows;
|
|
71
79
|
return {
|
|
@@ -5,6 +5,7 @@ import { BaseRepository, parseSieveDateValue } from "../../../providers/db-fireb
|
|
|
5
5
|
import { type OrderCreateInput, type OrderDocument, type OrderPlain, type OrderRefundEvent } from "../schemas";
|
|
6
6
|
import { type HistoryActor, type FieldChange } from "../../../_internal/shared/history/index";
|
|
7
7
|
import type { OrderStatus, PaymentStatus } from "../types";
|
|
8
|
+
import type { OrderType } from "../utils/order-splitter";
|
|
8
9
|
import { type PaymentReviewQueueMode } from "../constants/payment-window";
|
|
9
10
|
/**
|
|
10
11
|
* Who/why for a write that lands in `statusHistory`.
|
|
@@ -64,7 +65,7 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
|
|
|
64
65
|
* encryption lived in a private method only three call sites remembered to
|
|
65
66
|
* call.
|
|
66
67
|
*/
|
|
67
|
-
protected piiFields: readonly ["userName", "userEmail", "sellerEmail"];
|
|
68
|
+
protected piiFields: readonly ["userName", "userEmail", "sellerEmail", "deliveryFullName", "deliveryPhone", "deliveryAddressLine1", "deliveryAddressLine2", "deliveryLandmark", "deliveryAmendments[].fullName", "deliveryAmendments[].phone", "deliveryAmendments[].addressLine1", "deliveryAmendments[].addressLine2", "deliveryAmendments[].landmark"];
|
|
68
69
|
/** Plaintext PII. **Opt-in and named** โ shows up in review and in a grep. */
|
|
69
70
|
revealOrder(doc: OrderDocument): OrderPlain;
|
|
70
71
|
/** As `revealOrder`, for a list read. */
|
|
@@ -144,6 +145,20 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
|
|
|
144
145
|
* `order.bundleId` at creation time (SB3 / SB6-C).
|
|
145
146
|
*/
|
|
146
147
|
countByUserAndBundle(userId: string, bundleId: string): Promise<number>;
|
|
148
|
+
/**
|
|
149
|
+
* How many orders this buyer has placed, optionally scoped to one lane.
|
|
150
|
+
*
|
|
151
|
+
* A Firestore aggregation โ billed at one read per 1000 index entries
|
|
152
|
+
* matched, not one per order โ so it stays flat as an account's history
|
|
153
|
+
* grows. That matters because the only caller is the session profile
|
|
154
|
+
* endpoint, which every page load hits.
|
|
155
|
+
*
|
|
156
|
+
* ๐ `orderType` may only name a NON-standard lane. The field was added
|
|
157
|
+
* after orders already existed, so an equality on `"standard"` would return
|
|
158
|
+
* zero for every legacy order while looking perfectly correct โ see the
|
|
159
|
+
* constant's own note.
|
|
160
|
+
*/
|
|
161
|
+
countForUser(userId: string, orderType?: Exclude<OrderType, "standard">): Promise<number>;
|
|
147
162
|
deleteByUser(userId: string): Promise<number>;
|
|
148
163
|
static readonly SELLER_SIEVE_FIELDS: {
|
|
149
164
|
id: {
|
|
@@ -564,6 +579,18 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
|
|
|
564
579
|
*/
|
|
565
580
|
getEligibleAutomatic(cutoff: Date, opts?: {
|
|
566
581
|
onWarn?: (msg: string, meta?: Record<string, JsonValue>) => void;
|
|
582
|
+
/**
|
|
583
|
+
* Stores paid at SHIPPING rather than at delivery.
|
|
584
|
+
*
|
|
585
|
+
* ๐ A faster path through the SAME query, not a second one. A `shipped`
|
|
586
|
+
* row is admitted only for a store in this set, and then skips the hold
|
|
587
|
+
* anchor โ being paid before delivery is the entire grant, so applying a
|
|
588
|
+
* post-delivery hold to it would make the flag do nothing.
|
|
589
|
+
*
|
|
590
|
+
* Omitted or empty means nobody is trusted, which is the correct default
|
|
591
|
+
* and reproduces the pre-existing behaviour exactly.
|
|
592
|
+
*/
|
|
593
|
+
trustedStoreIds?: ReadonlySet<string>;
|
|
567
594
|
}): Promise<Array<{
|
|
568
595
|
id: string;
|
|
569
596
|
ref: DocumentReference;
|
|
@@ -9,7 +9,7 @@ import { normalizeError } from "../../../errors/normalize.js";
|
|
|
9
9
|
* that mix-up swallowing ~100 call sites' validation messages.
|
|
10
10
|
*/
|
|
11
11
|
import { DatabaseError, NotFoundError, ValidationError } from "../../../errors/index.js";
|
|
12
|
-
import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase/index.js";
|
|
12
|
+
import { BaseRepository, prepareForFirestore, parseSieveDateValue, getFirestoreCount, } from "../../../providers/db-firebase/index.js";
|
|
13
13
|
import { maskPii, revealPii, ORDER_PII_FIELDS, } from "../../../security/index.js";
|
|
14
14
|
import { serverTimestamp, arrayUnion } from "../../../contracts/field-ops.js";
|
|
15
15
|
import { createOrderId, ORDER_COLLECTION, ORDER_TRACKED_FIELDS, OrderStatusValues, } from "../schemas/index.js";
|
|
@@ -304,10 +304,11 @@ class OrderRepository extends BaseRepository {
|
|
|
304
304
|
reason: ctx?.reason,
|
|
305
305
|
note: ctx?.note,
|
|
306
306
|
extraChanges: ctx?.extraChanges,
|
|
307
|
-
// `encryptPiiFields`
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
307
|
+
// `encryptPiiFields` covers a field only where it is NAMED, so a
|
|
308
|
+
// PII-named value reaching `changes` is plaintext unless somebody
|
|
309
|
+
// declared `statusHistory[].changes.<that key>` โ which nobody will.
|
|
310
|
+
// The scrub removes the guesswork. None of ORDER_TRACKED_FIELDS is PII
|
|
311
|
+
// today; this is defence for the next field somebody adds.
|
|
311
312
|
piiFields: ORDER_PII_FIELDS,
|
|
312
313
|
historyField: "statusHistory",
|
|
313
314
|
truncatedField: "statusHistoryTruncated",
|
|
@@ -516,6 +517,26 @@ class OrderRepository extends BaseRepository {
|
|
|
516
517
|
.get();
|
|
517
518
|
return snapshot.size;
|
|
518
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* How many orders this buyer has placed, optionally scoped to one lane.
|
|
522
|
+
*
|
|
523
|
+
* A Firestore aggregation โ billed at one read per 1000 index entries
|
|
524
|
+
* matched, not one per order โ so it stays flat as an account's history
|
|
525
|
+
* grows. That matters because the only caller is the session profile
|
|
526
|
+
* endpoint, which every page load hits.
|
|
527
|
+
*
|
|
528
|
+
* ๐ `orderType` may only name a NON-standard lane. The field was added
|
|
529
|
+
* after orders already existed, so an equality on `"standard"` would return
|
|
530
|
+
* zero for every legacy order while looking perfectly correct โ see the
|
|
531
|
+
* constant's own note.
|
|
532
|
+
*/
|
|
533
|
+
async countForUser(userId, orderType) {
|
|
534
|
+
let query = this.getCollection().where(ORDER_FIELDS.USER_ID, "==", userId);
|
|
535
|
+
if (orderType) {
|
|
536
|
+
query = query.where(ORDER_FIELDS.ORDER_TYPE, "==", orderType);
|
|
537
|
+
}
|
|
538
|
+
return getFirestoreCount(query);
|
|
539
|
+
}
|
|
519
540
|
async deleteByUser(userId) {
|
|
520
541
|
try {
|
|
521
542
|
const snapshot = await this.getCollection()
|
|
@@ -1139,10 +1160,23 @@ class OrderRepository extends BaseRepository {
|
|
|
1139
1160
|
* and the anchor test is a per-row predicate below. One query, no new
|
|
1140
1161
|
* composite index, and no way to silently return nothing.
|
|
1141
1162
|
*/
|
|
1163
|
+
const trusted = opts?.trustedStoreIds ?? new Set();
|
|
1142
1164
|
const snap = await this.db
|
|
1143
1165
|
.collection(this.collection)
|
|
1144
1166
|
.where(ORDER_FIELDS.PAYOUT_STATUS, "==", "eligible")
|
|
1145
|
-
|
|
1167
|
+
/*
|
|
1168
|
+
* ๐ Widened to an `in` ONLY when some store is trusted.
|
|
1169
|
+
*
|
|
1170
|
+
* Firestore's `in` is an OR of equalities, so the existing
|
|
1171
|
+
* `(payoutStatus, status)` composite serves both forms and no new index
|
|
1172
|
+
* is needed. Keeping the plain equality when the set is empty matters
|
|
1173
|
+
* anyway: a deployment with no trusted sellers then runs byte-identically
|
|
1174
|
+
* to before, so the flag cannot change behaviour for anyone who has not
|
|
1175
|
+
* been granted it.
|
|
1176
|
+
*/
|
|
1177
|
+
.where(ORDER_FIELDS.STATUS, ...(trusted.size > 0
|
|
1178
|
+
? ["in", [OrderStatusValues.DELIVERED, OrderStatusValues.SHIPPED]]
|
|
1179
|
+
: ["==", OrderStatusValues.DELIVERED]))
|
|
1146
1180
|
.limit(500)
|
|
1147
1181
|
.get();
|
|
1148
1182
|
let missingAnchor = 0;
|
|
@@ -1153,6 +1187,26 @@ class OrderRepository extends BaseRepository {
|
|
|
1153
1187
|
data: { id: d.id, ...d.data() },
|
|
1154
1188
|
}))
|
|
1155
1189
|
.filter((o) => !o.data.emiEnabled || o.data.emiComplete)
|
|
1190
|
+
.filter((o) => {
|
|
1191
|
+
/*
|
|
1192
|
+
* ๐ A SHIPPED row is admitted only for a TRUSTED store, and then
|
|
1193
|
+
* bypasses the hold entirely โ paying at shipping is the grant.
|
|
1194
|
+
*
|
|
1195
|
+
* Tested before the anchor rather than inside it, because a shipped
|
|
1196
|
+
* order has no `deliveryDate` and would fall through to the
|
|
1197
|
+
* `updatedAt` fallback โ which is always LATER than delivery would
|
|
1198
|
+
* have been. A trusted seller would be paid more SLOWLY than an
|
|
1199
|
+
* untrusted one: the flag would read as working and do the opposite.
|
|
1200
|
+
*
|
|
1201
|
+
* The `in` above cannot express "shipped, but only for these stores",
|
|
1202
|
+
* so the store test has to live here โ otherwise one trusted seller
|
|
1203
|
+
* anywhere would widen the sweep for every seller.
|
|
1204
|
+
*/
|
|
1205
|
+
if (o.data.status === OrderStatusValues.SHIPPED) {
|
|
1206
|
+
return typeof o.data.storeId === "string" && trusted.has(o.data.storeId);
|
|
1207
|
+
}
|
|
1208
|
+
return true;
|
|
1209
|
+
})
|
|
1156
1210
|
.filter((o) => {
|
|
1157
1211
|
/*
|
|
1158
1212
|
* The anchor, with a conservative fallback. `deliveryDate` is the
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D55 โ write shapes for the pre-order balance collector.
|
|
3
|
+
*
|
|
4
|
+
* ๐ Both are `.strict()`, and that is the Root Cause #76 lesson rather than
|
|
5
|
+
* tidiness: the balance block carries `status`, `reviewedBy`, `reviewedAt`
|
|
6
|
+
* and `amount`, and a write shape that can NAME a field is a write shape that
|
|
7
|
+
* can destroy it. A buyer must not be able to send `status: "paid"` or
|
|
8
|
+
* `amount: 1`, and `z.object()` merely STRIPS unknown keys โ which is the
|
|
9
|
+
* same silent-success failure as the grouped-listings PATCH. `.strict()`
|
|
10
|
+
* makes it a 400 instead.
|
|
11
|
+
*
|
|
12
|
+
* Neither schema is where `amount` comes from. It is snapshotted from
|
|
13
|
+
* `sourceContext.balanceDue` when the seller opens collection, months before
|
|
14
|
+
* this, and is never re-read from the live product (D61).
|
|
15
|
+
*/
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
/** The buyer's proof of payment. */
|
|
18
|
+
export declare const balanceProofSchema: z.ZodObject<{
|
|
19
|
+
proofUrl: z.ZodString;
|
|
20
|
+
transactionId: z.ZodString;
|
|
21
|
+
}, "strict", z.ZodTypeAny, {
|
|
22
|
+
transactionId: string;
|
|
23
|
+
proofUrl: string;
|
|
24
|
+
}, {
|
|
25
|
+
transactionId: string;
|
|
26
|
+
proofUrl: string;
|
|
27
|
+
}>;
|
|
28
|
+
export type BalanceProofInput = z.infer<typeof balanceProofSchema>;
|
|
29
|
+
/** The admin's verdict on a submitted proof. */
|
|
30
|
+
export declare const balanceReviewSchema: z.ZodObject<{
|
|
31
|
+
approve: z.ZodBoolean;
|
|
32
|
+
note: z.ZodOptional<z.ZodString>;
|
|
33
|
+
}, "strict", z.ZodTypeAny, {
|
|
34
|
+
approve: boolean;
|
|
35
|
+
note?: string | undefined;
|
|
36
|
+
}, {
|
|
37
|
+
approve: boolean;
|
|
38
|
+
note?: string | undefined;
|
|
39
|
+
}>;
|
|
40
|
+
export type BalanceReviewInput = z.infer<typeof balanceReviewSchema>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D55 โ write shapes for the pre-order balance collector.
|
|
3
|
+
*
|
|
4
|
+
* ๐ Both are `.strict()`, and that is the Root Cause #76 lesson rather than
|
|
5
|
+
* tidiness: the balance block carries `status`, `reviewedBy`, `reviewedAt`
|
|
6
|
+
* and `amount`, and a write shape that can NAME a field is a write shape that
|
|
7
|
+
* can destroy it. A buyer must not be able to send `status: "paid"` or
|
|
8
|
+
* `amount: 1`, and `z.object()` merely STRIPS unknown keys โ which is the
|
|
9
|
+
* same silent-success failure as the grouped-listings PATCH. `.strict()`
|
|
10
|
+
* makes it a 400 instead.
|
|
11
|
+
*
|
|
12
|
+
* Neither schema is where `amount` comes from. It is snapshotted from
|
|
13
|
+
* `sourceContext.balanceDue` when the seller opens collection, months before
|
|
14
|
+
* this, and is never re-read from the live product (D61).
|
|
15
|
+
*/
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
/** The buyer's proof of payment. */
|
|
18
|
+
export const balanceProofSchema = z
|
|
19
|
+
.object({
|
|
20
|
+
/*
|
|
21
|
+
* A `/media/โฆ` slug from the signed-URL upload flow โ bytes never reach
|
|
22
|
+
* this route (Rule #6's 4.5 MB payload cap, and ยง Media Architecture).
|
|
23
|
+
*/
|
|
24
|
+
proofUrl: z.string().min(1, "Upload a screenshot of the payment."),
|
|
25
|
+
/*
|
|
26
|
+
* The UTR or UPI reference. Required, because it is the only thing an
|
|
27
|
+
* admin can check the payment against โ a screenshot alone is a picture.
|
|
28
|
+
*/
|
|
29
|
+
transactionId: z
|
|
30
|
+
.string()
|
|
31
|
+
.min(4, "Enter the transaction reference from your bank or UPI app.")
|
|
32
|
+
.max(64),
|
|
33
|
+
})
|
|
34
|
+
.strict();
|
|
35
|
+
/** The admin's verdict on a submitted proof. */
|
|
36
|
+
export const balanceReviewSchema = z
|
|
37
|
+
.object({
|
|
38
|
+
approve: z.boolean(),
|
|
39
|
+
/*
|
|
40
|
+
* Optional HERE and required in `reviewBalanceProof` when `approve` is
|
|
41
|
+
* false. Deliberately not a `.refine()`: the rule belongs with the
|
|
42
|
+
* transition it guards, and duplicating it would give two places to
|
|
43
|
+
* disagree. An approval genuinely needs no note.
|
|
44
|
+
*/
|
|
45
|
+
note: z.string().max(300).optional(),
|
|
46
|
+
})
|
|
47
|
+
.strict();
|