@mohasinac/appkit 4.69.1 → 4.71.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/PROVIDERS.md +190 -0
- package/dist/.build-stamp.json +3 -3
- package/dist/_internal/client/features/lottery/LotteryAdminSlotView.js +2 -1
- package/dist/_internal/server/features/checkout/actions.js +43 -8
- package/dist/_internal/server/features/orders/actions.js +2 -1
- package/dist/_internal/server/features/orders/adapters.js +18 -0
- package/dist/_internal/server/features/orders/balance.js +2 -1
- package/dist/_internal/server/features/orders/cancellation-refund.d.ts +0 -89
- package/dist/_internal/server/features/orders/cancellation-refund.js +3 -2
- package/dist/_internal/server/features/payouts/actions.js +2 -1
- package/dist/_internal/server/features/points/award.js +3 -2
- package/dist/_internal/server/features/protection-claims/actions.js +2 -1
- package/dist/_internal/server/features/refunds/actions.js +3 -2
- package/dist/_internal/server/features/shipping/courier-block.js +5 -2
- package/dist/_internal/server/features/shipping/reconcile.js +3 -2
- package/dist/_internal/server/features/shipping/reimbursement.js +3 -2
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +12 -2
- package/dist/_internal/server/jobs/core/dailyStatusDigest.js +7 -3
- package/dist/_internal/server/jobs/core/emiInstallmentReminder.js +2 -1
- package/dist/_internal/server/jobs/core/obligationSweep.js +5 -4
- package/dist/_internal/server/jobs/core/onScamReportCreate.js +2 -1
- package/dist/_internal/server/jobs/core/paymentReconcile.js +5 -4
- package/dist/_internal/shared/checkout/rules/auction.rule.js +25 -7
- package/dist/_internal/shared/checkout/rules/prize-draw.rule.js +26 -8
- package/dist/_internal/shared/config/schema.d.ts +15 -0
- package/dist/_internal/shared/content/published-values.d.ts +2 -2
- package/dist/_internal/shared/content/published-values.js +31 -12
- package/dist/_internal/shared/features/auctions/errors.js +2 -1
- package/dist/_internal/shared/features/orders/refund-copy.js +3 -2
- package/dist/_internal/shared/features/promotions/errors.js +2 -1
- package/dist/constants/field-names.d.ts +23 -0
- package/dist/constants/field-names.js +23 -0
- package/dist/constants/storage-keys.d.ts +90 -0
- package/dist/constants/storage-keys.js +119 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/payment.d.ts +42 -0
- package/dist/contracts/registry.d.ts +8 -0
- package/dist/contracts/tax.d.ts +118 -0
- package/dist/contracts/tax.js +3 -0
- package/dist/core/brand-resolver.d.ts +92 -0
- package/dist/core/brand-resolver.js +120 -0
- package/dist/core/brand-text.d.ts +15 -0
- package/dist/core/brand-text.js +73 -0
- package/dist/core/hooks/useNewsletterSubscribe.d.ts +30 -0
- package/dist/core/hooks/useNewsletterSubscribe.js +121 -0
- package/dist/features/about/components/DeveloperView.js +2 -1
- package/dist/features/about/index.d.ts +0 -2
- package/dist/features/about/index.js +0 -1
- package/dist/features/account/components/BuyerAccountGuideView.js +2 -1
- package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -3
- package/dist/features/account/components/BuyerOrdersGuideView.js +6 -5
- package/dist/features/account/components/BuyerShoppingGuideView.js +3 -2
- package/dist/features/account/components/UserOffersPanel.js +2 -1
- package/dist/features/account/hooks/useCollapsedSections.js +5 -3
- package/dist/features/account/schemas/index.js +1 -1
- package/dist/features/admin/actions/notification-actions.js +9 -2
- package/dist/features/admin/components/AdminAnalyticsGuideView.js +2 -1
- package/dist/features/admin/components/AdminBundlesView.js +2 -1
- package/dist/features/admin/components/AdminCatalogGuideView.js +2 -1
- package/dist/features/admin/components/AdminContentGuideView.js +2 -1
- package/dist/features/admin/components/AdminFaqEditorView.js +2 -1
- package/dist/features/admin/components/AdminGatewayEventsView.js +2 -1
- package/dist/features/admin/components/AdminGuideHubView.js +2 -1
- package/dist/features/admin/components/AdminMoneyCasesView.js +2 -1
- package/dist/features/admin/components/AdminOrdersGuideView.js +2 -1
- package/dist/features/admin/components/AdminProductsView.js +3 -2
- package/dist/features/admin/components/AdminSectionsView.js +3 -2
- package/dist/features/admin/components/AdminSiteSettingsView.js +8 -7
- package/dist/features/admin/components/AdminStoresGuideView.js +2 -1
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +2 -1
- package/dist/features/admin/components/AdminTeamGuideView.js +2 -1
- package/dist/features/admin/components/AdminTrustGuideView.js +3 -2
- package/dist/features/admin/components/AdminUsersGuideView.js +2 -1
- package/dist/features/admin/components/sections/adminSectionsTypes.js +1 -1
- package/dist/features/admin/schemas/firestore.js +0 -1
- package/dist/features/auth/actions/user-slug.js +21 -2
- package/dist/features/auth/hooks/useAuth.js +11 -4
- package/dist/features/cart/utils/pending-ops.js +16 -3
- package/dist/features/catalogue/utils/product-from-item.d.ts +18 -2
- package/dist/features/catalogue/utils/product-from-item.js +23 -4
- package/dist/features/categories/components/BundleItemsPicker.js +2 -1
- package/dist/features/categories/components/CategoryDetailPageView.js +2 -1
- package/dist/features/categories/components/ContextualTrail.d.ts +9 -2
- package/dist/features/categories/components/ContextualTrail.js +11 -3
- package/dist/features/categories/components/LastCategoryBeacon.js +2 -2
- package/dist/features/checkout/actions/checkout-value-otp-actions.js +9 -1
- package/dist/features/contact/email.js +11 -12
- package/dist/features/email/notification-templates.js +4 -5
- package/dist/features/email/render-static-markup.d.ts +34 -0
- package/dist/features/email/render-static-markup.js +4 -0
- package/dist/features/events/utils/event-charts.js +2 -1
- package/dist/features/homepage/components/AnnouncementBar.js +5 -3
- package/dist/features/homepage/components/FAQSection.js +1 -1
- package/dist/features/homepage/components/MarketplaceHomepageView.js +34 -0
- package/dist/features/homepage/components/WelcomeSection.d.ts +3 -1
- package/dist/features/homepage/components/WelcomeSection.js +2 -2
- package/dist/features/homepage/lib/section-renderer.d.ts +8 -0
- package/dist/features/homepage/lib/section-renderer.js +1 -1
- package/dist/features/layout/AppLayoutShell.d.ts +3 -1
- package/dist/features/layout/AppLayoutShell.js +2 -2
- package/dist/features/layout/TitleBarLayout.d.ts +8 -1
- package/dist/features/layout/TitleBarLayout.js +2 -2
- package/dist/features/media/MediaVideo.js +2 -1
- package/dist/features/orders/actions/refund-actions.js +3 -2
- package/dist/features/orders/components/RefundRequestView.js +15 -2
- package/dist/features/orders/repository/orders.repository.js +2 -1
- package/dist/features/orders/types/index.d.ts +14 -0
- package/dist/features/orders/utils/payment-proof-message.js +5 -2
- package/dist/features/price-index/components/PriceIndexToolView.js +6 -5
- package/dist/features/pricing/components/AdminPricingView.js +2 -1
- package/dist/features/print/components/labels.js +2 -1
- package/dist/features/products/components/CityCollectiblesView.js +2 -1
- package/dist/features/products/components/MakeOfferButton.js +2 -1
- package/dist/features/products/components/PrizeDrawCollage.js +3 -2
- package/dist/features/products/components/PrizeRevealModal.js +2 -1
- package/dist/features/products/repository/products.repository.js +2 -1
- package/dist/features/promotions/components/CouponCard.js +3 -2
- package/dist/features/promotions/components/CouponHelpDetails.js +2 -1
- package/dist/features/promotions/constants/coupon-help.d.ts +1 -1
- package/dist/features/promotions/constants/coupon-help.js +8 -1
- package/dist/features/scams/components/ScamAwarenessModal.js +2 -1
- package/dist/features/scams/components/ScamProfileView.js +5 -3
- package/dist/features/scams/components/ScamRegistryView.js +2 -1
- package/dist/features/scams/constants/scam-types.d.ts +13 -1
- package/dist/features/scams/constants/scam-types.js +67 -23
- package/dist/features/scams/schemas/firestore.js +1 -1
- package/dist/features/search/components/Search.js +5 -1
- package/dist/features/seller/actions/offer-actions.js +47 -9
- package/dist/features/seller/components/DispatchForm.js +3 -2
- package/dist/features/seller/components/PrintCenterView.js +4 -2
- package/dist/features/seller/components/SellerBundlesView.js +2 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +3 -2
- package/dist/features/seller/components/SellerProductShell.js +3 -2
- package/dist/features/seller/components/SellerProductsView.js +4 -3
- package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +30 -4
- package/dist/features/store-extensions/repository/store-extensions.repositories.js +46 -7
- package/dist/features/store-extensions/schemas/firestore.d.ts +22 -3
- package/dist/features/store-extensions/schemas/firestore.js +20 -0
- package/dist/features/stores/components/StoreCapabilitiesGuideView.js +7 -6
- package/dist/features/stores/components/StoreFinanceGuideView.js +6 -5
- package/dist/features/stores/components/StoreGuideHubView.js +2 -1
- package/dist/features/stores/components/StoreListingsGuideView.js +2 -1
- package/dist/features/stores/components/StoreOrdersGuideView.js +5 -4
- package/dist/features/stores/components/StoreSettingsGuideView.js +2 -1
- package/dist/features/support/schemas/index.d.ts +2 -2
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +1 -1
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +3 -2
- package/dist/features/whatsapp-bot/helpers/whatsapp.js +5 -2
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -6
- package/dist/providers/payment-manual/index.d.ts +11 -0
- package/dist/providers/payment-manual/index.js +13 -0
- package/dist/providers/payment-phonepe/index.d.ts +23 -7
- package/dist/providers/payment-phonepe/index.js +15 -0
- package/dist/providers/payment-razorpay/index.d.ts +13 -0
- package/dist/providers/payment-razorpay/index.js +21 -1
- package/dist/providers/tax-india-gst/index.d.ts +45 -0
- package/dist/providers/tax-india-gst/index.js +78 -0
- package/dist/react/hooks/useScrollToTop.d.ts +5 -0
- package/dist/react/hooks/useScrollToTop.js +70 -0
- package/dist/schemas/registry.d.ts +2 -2
- package/dist/seo/json-ld.js +25 -4
- package/dist/styles.css +20 -0
- package/dist/ui/components/SiteLogo.d.ts +20 -2
- package/dist/ui/components/SiteLogo.js +28 -6
- package/dist/ui/components/SiteMark.d.ts +1 -1
- package/dist/ui/components/SiteMark.js +21 -6
- package/dist/ui/components/Tabs.d.ts +9 -1
- package/dist/ui/components/Tabs.js +20 -3
- package/dist/ui/components/Tabs.style.css +20 -0
- package/dist/ui/rich-text/RichText.d.ts +0 -12
- package/dist/ui/rich-text/RichText.js +52 -1
- package/dist/utils/date.formatter.d.ts +20 -0
- package/dist/utils/date.formatter.js +25 -0
- package/dist/utils/number.formatter.d.ts +47 -0
- package/dist/utils/number.formatter.js +79 -1
- package/dist/validation/phone.validator.d.ts +0 -1
- package/dist/validation/phone.validator.js +17 -3
- package/package.json +17 -3
- package/dist/features/about/components/AboutView.d.ts +0 -30
- package/dist/features/about/components/AboutView.js +0 -14
package/PROVIDERS.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Provider registry — what you can replace, and what you cannot
|
|
2
|
+
|
|
3
|
+
Written against the tree on **2026-10-29**. Every count here was measured, not
|
|
4
|
+
estimated; re-measure before quoting any of it elsewhere.
|
|
5
|
+
|
|
6
|
+
appkit reaches the outside world through one registry. A consumer calls
|
|
7
|
+
`registerProviders()` once at boot and appkit code asks for what it needs:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { registerProviders } from "@mohasinac/appkit/contracts/registry";
|
|
11
|
+
|
|
12
|
+
registerProviders({
|
|
13
|
+
db: myDbProvider,
|
|
14
|
+
auth: myAuthProvider,
|
|
15
|
+
session: mySessionProvider,
|
|
16
|
+
email: myEmailProvider,
|
|
17
|
+
storage: myStorageProvider,
|
|
18
|
+
style: tailwindAdapter,
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The five without `?` in `ProviderRegistry` are required by the type. The rest
|
|
23
|
+
are optional, and an absent optional provider means "appkit keeps its existing
|
|
24
|
+
behaviour" — never a throw.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 🛑 Read this before planning a replacement: Firebase is a hard dependency today
|
|
29
|
+
|
|
30
|
+
**appkit is "Firebase ecommerce from scratch", not "ecommerce from scratch".**
|
|
31
|
+
|
|
32
|
+
| Measured 2026-10-29 | |
|
|
33
|
+
|---|---|
|
|
34
|
+
| appkit files importing `firebase-admin` / `firebase/*` **outside `providers/`** | **59** |
|
|
35
|
+
| appkit files calling `getAdminDb` / `getAdminAuth` / `getAdminStorage` directly | **64** |
|
|
36
|
+
|
|
37
|
+
`IDbProvider` exists and is registered, but 64 files bypass it and talk to the
|
|
38
|
+
Admin SDK. So swapping `db` for Postgres does **not** work today, whatever the
|
|
39
|
+
registry's shape suggests. The seams that are real are listed below; the rest
|
|
40
|
+
is a direction of travel.
|
|
41
|
+
|
|
42
|
+
This is stated plainly because the alternative is discovering it in a sales
|
|
43
|
+
conversation. Closing it is a tracked tier (AK3-dip-direct-repo: *"every appkit
|
|
44
|
+
component/hook importing a concrete repository instead of a provider"*), not a
|
|
45
|
+
afternoon's work.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## The slots
|
|
50
|
+
|
|
51
|
+
🛑 **"Read?" is the column that matters.** A slot nothing reads is
|
|
52
|
+
indistinguishable from one that works — the consumer passes a provider, appkit
|
|
53
|
+
accepts it, and the value is never fetched. Six slots are in that state right
|
|
54
|
+
now. `scripts/audit-provider-registry-readers.mjs` blocks a seventh.
|
|
55
|
+
|
|
56
|
+
| Slot | Contract | letitrip registers | Read? | Replaceable today? |
|
|
57
|
+
|---|---|---|---|---|
|
|
58
|
+
| `db` | `IDbProvider` | `firebaseDbProvider` | ✅ 46 files | ❌ — 64 files bypass it for the Admin SDK |
|
|
59
|
+
| `auth` | `IAuthProvider` | `firebaseAuthProvider` | ✅ 4 | partly |
|
|
60
|
+
| `session` | `ISessionProvider` | `firebaseSessionProvider` | ✅ 3 | partly |
|
|
61
|
+
| `email` | `IEmailProvider` | `createResendProvider(...)` | ✅ 1 | **yes** — the cleanest seam in the package |
|
|
62
|
+
| `payment` | `IPaymentProvider` | manual \| Razorpay \| PhonePe | ✅ 11 | **yes** — three implementations prove it |
|
|
63
|
+
| `shipping` | `IShippingProvider` | manual \| Shiprocket | ✅ 6 | **yes** — two implementations |
|
|
64
|
+
| `rbac` | `IRbacProvider` | inline `{getPermissions, isAdmin}` | ✅ 1 | yes |
|
|
65
|
+
| `eventBus` | `IEventBus` | — | ✅ 1 | yes |
|
|
66
|
+
| `storage` | `IStorageProvider` | `firebaseStorageProvider` | 🛑 **0** | no — media code imports the provider module directly |
|
|
67
|
+
| `style` | `IStyleAdapter` | `tailwindAdapter` | 🛑 **0** | no — reached by direct import |
|
|
68
|
+
| `tax` | `ITaxProvider` | `indiaGstTaxProvider` | 🛑 **0** | **blocked — see below** |
|
|
69
|
+
| `search` | `ISearchProvider` | — | 🛑 **0** | no — listing queries call repositories |
|
|
70
|
+
| `cache` | `ICacheProvider` | — | 🛑 **0** | declared only |
|
|
71
|
+
| `queue` | `IQueueProvider` | — | 🛑 **0** | declared only; the job primitive is `enqueueJob` + `JOB_RUNNERS` |
|
|
72
|
+
|
|
73
|
+
`payment` and `shipping` are the two to copy when writing a new provider: each
|
|
74
|
+
has more than one real implementation, selected at boot from site settings, and
|
|
75
|
+
a fall-through to a complete manual flow. A half-configured gateway fails
|
|
76
|
+
loudly at boot rather than when a buyer tries to pay.
|
|
77
|
+
|
|
78
|
+
### 🛑 `tax` — a declared seam that cannot be read yet
|
|
79
|
+
|
|
80
|
+
Two obstacles, and the second is a design problem rather than effort:
|
|
81
|
+
|
|
82
|
+
1. **Runtime.** Its call sites are in `_internal/shared/checkout/order-math.ts`,
|
|
83
|
+
which is isomorphic — `features/cart/utils/line-total.ts` reaches it from the
|
|
84
|
+
*client* graph for per-store subtotals. `getProviders()` throws where nothing
|
|
85
|
+
registered, and a consumer registers on the server only. (Same trap
|
|
86
|
+
`features/contact/email.tsx` documents for Firebase Functions, where it made
|
|
87
|
+
the daily digest silently never deliver.)
|
|
88
|
+
2. **Shape.** `TaxBreakdown` carries `components[]` keyed by provider-specific
|
|
89
|
+
codes. `order-math.ts` needs named `cgst`/`sgst`/`igst` scalars — 27
|
|
90
|
+
references — because India's Rule 46 requires tax *per rate* on the invoice.
|
|
91
|
+
Converting means generic code doing `components.find(c => c.code === "cgst")`,
|
|
92
|
+
re-importing the exact vocabulary the interface exists to abstract.
|
|
93
|
+
|
|
94
|
+
The honest reading: **`order-math.ts` is not generic code that uses GST, it is
|
|
95
|
+
India invoice code.** Fixing it is a decision about where GST invoice assembly
|
|
96
|
+
lives, on a money path.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Two injection mechanisms, and which to use
|
|
101
|
+
|
|
102
|
+
The registry is not the only seam. Some values are needed on **every runtime**,
|
|
103
|
+
including the browser bundle and Firebase Functions, neither of which calls
|
|
104
|
+
`registerProviders()`.
|
|
105
|
+
|
|
106
|
+
| Need | Mechanism | Call it in |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| A service appkit calls (db, email, payment) | `registerProviders({...})` | `providers.config.ts` (server) |
|
|
109
|
+
| A value isomorphic code reads (brand, currency, storage namespace) | a `configure*` function | **both** `providers.config.ts` *and* `client-providers-init.ts` |
|
|
110
|
+
|
|
111
|
+
The `configure*` family — all merge rather than replace, all have neutral
|
|
112
|
+
defaults:
|
|
113
|
+
|
|
114
|
+
| Function | Module | Sets |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| `configureBrand` | `core/brand-resolver` | name, shortName, domain, support/noreply email |
|
|
117
|
+
| `configureMarketDefaults` | `core/baseline-resolver` | currency, locale, country, phone prefix, timezone, symbol |
|
|
118
|
+
| `setStorageNamespace` | `constants/storage-keys` | the prefix on every browser storage key |
|
|
119
|
+
| `configurePlatformSupplier` | `_internal/.../supplier-of-record` | statutory identity for invoices (server only) |
|
|
120
|
+
|
|
121
|
+
🛑 **Module state does not cross a module graph.** A `configure*` call in
|
|
122
|
+
`providers.config.ts` runs from the instrumentation hook and is invisible to the
|
|
123
|
+
browser bundle. An isomorphic value needs the call in both places, reading the
|
|
124
|
+
same owner (`appkit.config.js`) so the two cannot disagree about the value —
|
|
125
|
+
only about whether they ran.
|
|
126
|
+
|
|
127
|
+
🛑 **Brand defaults are NEUTRAL (`"Site"`), market defaults are not.**
|
|
128
|
+
`baseline-resolver`'s baseline is India's (INR / en-IN / +91). That asymmetry is
|
|
129
|
+
deliberate: a wrong currency is visible in the first rendered price, while a
|
|
130
|
+
wrong brand name renders plausibly and is someone else's trademark.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## The override hierarchy — least invasive first
|
|
135
|
+
|
|
136
|
+
Replacing a provider is the *fifth* of six options. Try them in order:
|
|
137
|
+
|
|
138
|
+
1. **Config / tokens** — `siteSettings.theme`, a partial `LabelsProvider` map,
|
|
139
|
+
`appkit.config.js`.
|
|
140
|
+
2. **Options object** — every public function takes an optional final `opts?`.
|
|
141
|
+
`renderXOgImage(doc, { theme, headline, layout: "compact" })`.
|
|
142
|
+
3. **Render-prop slots** — `<XDetailView renderActions={(ctx) => <MyActions/>}>`.
|
|
143
|
+
4. **Adapter wrap** — `const myToClient = (doc) => decorate(toClientX(doc))`.
|
|
144
|
+
5. **Replace the call** — register your own provider, or call the repository
|
|
145
|
+
directly and skip the helper.
|
|
146
|
+
6. **Fork via patch** — only when the seam does not exist. File an issue; the
|
|
147
|
+
seam should ship in appkit rather than live in your fork.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Writing a provider
|
|
152
|
+
|
|
153
|
+
Contracts are **abstract classes**, not interfaces, so every implementation
|
|
154
|
+
shares one base:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { IPaymentProvider } from "@mohasinac/appkit/contracts/payment";
|
|
158
|
+
|
|
159
|
+
export class MyGateway extends IPaymentProvider {
|
|
160
|
+
readonly id = "my-gateway";
|
|
161
|
+
// implement the abstract members; `tsc` enumerates them for you
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Three rules learned the hard way:
|
|
166
|
+
|
|
167
|
+
- **Fail at boot, not at point of use.** A provider constructed with empty
|
|
168
|
+
credentials should throw while the app is starting. The alternative is a buyer
|
|
169
|
+
discovering it at checkout.
|
|
170
|
+
- **Treat a placeholder as absent.** Seeded credentials like
|
|
171
|
+
`re_PLACEHOLDER` are truthy. `resolveKeys()` maps them to absent so a flag
|
|
172
|
+
switched on with no real key does not look configured.
|
|
173
|
+
- **Never name your gateway in generic code.** `audit-provider-name-leak`
|
|
174
|
+
blocks it: a provider name belongs inside its own `providers/<name>/`
|
|
175
|
+
directory and nowhere else.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Verifying a standalone install
|
|
180
|
+
|
|
181
|
+
🛑 **There is no proof app yet, so "standalone" remains a claim.**
|
|
182
|
+
|
|
183
|
+
The acceptance test is a scratch Next.js app that installs appkit from the
|
|
184
|
+
registry, registers a **non-Firebase** provider for at least one contract, and
|
|
185
|
+
renders a catalogue. Given the 64 files above, the honest expectation is that it
|
|
186
|
+
fails on `db` and succeeds on `email` — which is exactly why the test is worth
|
|
187
|
+
running rather than reasoned about.
|
|
188
|
+
|
|
189
|
+
It cannot be written against this tree: it must install a *published* version,
|
|
190
|
+
and the work described in this document is not published yet.
|
package/dist/.build-stamp.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"stampVersion": 1,
|
|
3
|
-
"version": "4.
|
|
4
|
-
"head": "
|
|
3
|
+
"version": "4.71.0",
|
|
4
|
+
"head": "260f012ee352221df711fee74a7d61d8825ae3c5",
|
|
5
5
|
"branch": "main",
|
|
6
6
|
"dirty": false,
|
|
7
|
-
"builtAt": "2026-10-
|
|
7
|
+
"builtAt": "2026-10-29T02:57:55.180Z"
|
|
8
8
|
}
|
|
@@ -5,6 +5,7 @@ import { Text, Heading } from "../../../../ui/components/Typography.js";
|
|
|
5
5
|
import { Badge } from "../../../../ui/components/Badge.js";
|
|
6
6
|
import { Section } from "../../../../ui/components/Semantic.js";
|
|
7
7
|
import { DataTable } from "../../../../ui/DataTable.js";
|
|
8
|
+
import { formatMoney } from "../../../../utils/number.formatter.js";
|
|
8
9
|
/**
|
|
9
10
|
* Admin-only view showing slots with price + weight + % chance.
|
|
10
11
|
* Separate from LotterySlotGrid (public) which never shows price/weight.
|
|
@@ -25,7 +26,7 @@ export function LotteryAdminSlotView({ slots }) {
|
|
|
25
26
|
{
|
|
26
27
|
key: "price",
|
|
27
28
|
header: "Price (₹)",
|
|
28
|
-
render: (s) => (
|
|
29
|
+
render: (s) => (_jsx(Text, { size: "sm", family: "mono", children: formatMoney(s.price) })),
|
|
29
30
|
},
|
|
30
31
|
{
|
|
31
32
|
key: "weight",
|
|
@@ -45,6 +45,7 @@ import { computeProtectionFees } from "../../../shared/features/shipping/protect
|
|
|
45
45
|
/* S5a — one payer, N destinations. Pure: the rates come in snapshotted, and
|
|
46
46
|
* an UNPRICED arrangement is refused rather than quoted at ₹0 (D44's lesson). */
|
|
47
47
|
import { countDestinations, quoteSharedPurchase, } from "../../../shared/features/shipping/shared-purchase.js";
|
|
48
|
+
import { formatMoney } from "../../../../utils/number.formatter.js";
|
|
48
49
|
import { resolveCouponCategorySlugs } from "../../../../features/promotions/actions/coupon-actions.js";
|
|
49
50
|
import { assertCheckoutLane, assertLockedLinesStillValid, finalizeLockedLines, } from "./locked-lines.js";
|
|
50
51
|
import { activeLane, defaultCheckoutItems, resolveCheckoutItems, laneOf, } from "../../../shared/checkout/lanes.js";
|
|
@@ -65,7 +66,6 @@ import { getProviders } from "../../../../contracts/index.js";
|
|
|
65
66
|
* index would pull a static `import Razorpay from "razorpay"` into this
|
|
66
67
|
* action's import graph for one HMAC comparison — see that file's header.
|
|
67
68
|
*/
|
|
68
|
-
import { verifyPaymentSignature as verifyRazorpayPaymentSignature } from "../../../../providers/payment-razorpay/signature.js";
|
|
69
69
|
import { resolveKeys } from "../../../../core/integration-keys.js";
|
|
70
70
|
import { paiseToRupees } from "../../../../core/money.js";
|
|
71
71
|
import { CHECKOUT_DEFAULT_COMMISSIONS, CHECKOUT_DEFAULT_EMI_SETTINGS } from "../../../shared/features/checkout/config.js";
|
|
@@ -868,7 +868,7 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
868
868
|
*/
|
|
869
869
|
const emiGate = groupEmiGate(emiGroupInput, emiSettings);
|
|
870
870
|
if (emiOptIn && emiTenureMonths && !emiGate.applies) {
|
|
871
|
-
throw new ValidationError(`EMI is not available for this order (${emiGate.reason}). Minimum order value per seller is
|
|
871
|
+
throw new ValidationError(`EMI is not available for this order (${emiGate.reason}). Minimum order value per seller is ${formatMoney(emiSettings.minOrderValue)}.`);
|
|
872
872
|
}
|
|
873
873
|
const emiSchedule = groupEmiSchedule(emiGroupInput, emiSettings) ?? undefined;
|
|
874
874
|
// Handling fee charged to the buyer for choosing COD, on top of the order
|
|
@@ -2897,8 +2897,43 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
2897
2897
|
*/
|
|
2898
2898
|
if (input.razorpaySignature) {
|
|
2899
2899
|
const { razorpayKeySecret } = await resolveKeys();
|
|
2900
|
+
/*
|
|
2901
|
+
* 🛑 A4b — through the CONTRACT, not a direct Razorpay import.
|
|
2902
|
+
*
|
|
2903
|
+
* This file used to `import { verifyPaymentSignature } from
|
|
2904
|
+
* "providers/payment-razorpay/signature"`, which named one gateway in the
|
|
2905
|
+
* one place that has to work for every gateway. `verifyCheckoutSignature`
|
|
2906
|
+
* is on `IPaymentProvider` now; Razorpay's implementation calls the SAME
|
|
2907
|
+
* function with the SAME HMAC, so no arithmetic moved.
|
|
2908
|
+
*
|
|
2909
|
+
* Everything that made this fail closed is unchanged: the branch still only
|
|
2910
|
+
* runs when a signature is present, the secret is still resolved here, and
|
|
2911
|
+
* `!!razorpayKeySecret` still short-circuits. A provider with no such
|
|
2912
|
+
* handshake (manual, PhonePe) returns `false` — never `true`.
|
|
2913
|
+
*
|
|
2914
|
+
* Proven rather than argued: `.build-memory/checkout-sig-parity.mjs` ran
|
|
2915
|
+
* both paths over 12 cases — valid, tampered order id, tampered payment id,
|
|
2916
|
+
* wrong secret, empty secret, non-hex, short, and every missing-field
|
|
2917
|
+
* permutation. The provider never accepts what the direct call rejected.
|
|
2918
|
+
*
|
|
2919
|
+
* One case differs, in the safe direction, and it fixes a latent crash:
|
|
2920
|
+
* on a callback MISSING `razorpay_signature` the old call THREW
|
|
2921
|
+
* (`hexEqual` reads `.length` of `undefined`), so a malformed callback
|
|
2922
|
+
* escaped as an unhandled TypeError instead of taking the rejection path
|
|
2923
|
+
* below. The provider checks its three fields first and returns false, so
|
|
2924
|
+
* that request now gets logged as `signature_mismatch` like any other
|
|
2925
|
+
* forgery.
|
|
2926
|
+
*/
|
|
2927
|
+
/*
|
|
2928
|
+
* 🛑 No `payment!`. `payment` is OPTIONAL on the registry, and a missing
|
|
2929
|
+
* provider must reject rather than throw — the same fail-closed direction
|
|
2930
|
+
* as the absent secret beside it. An assertion here would turn an
|
|
2931
|
+
* unconfigured install into a 500 on the payment path.
|
|
2932
|
+
*/
|
|
2933
|
+
const paymentProvider = getProviders().payment;
|
|
2900
2934
|
const signatureValid = !!razorpayKeySecret &&
|
|
2901
|
-
|
|
2935
|
+
!!paymentProvider &&
|
|
2936
|
+
paymentProvider.verifyCheckoutSignature(input.razorpaySignature, razorpayKeySecret);
|
|
2902
2937
|
if (!signatureValid) {
|
|
2903
2938
|
serverLogger.warn(`Razorpay signature rejected: order=${input.razorpaySignature.razorpay_order_id} payment=${input.razorpaySignature.razorpay_payment_id}`);
|
|
2904
2939
|
failedCheckoutRepository
|
|
@@ -3022,7 +3057,7 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
3022
3057
|
// (no session) reconstructs them from the gateway's own passthrough channel
|
|
3023
3058
|
// — PhonePe's `metaInfo`, Razorpay's `notes` — both packed at create-order
|
|
3024
3059
|
// time and both surfaced as `PaymentOrder.metadata`. See
|
|
3025
|
-
// `
|
|
3060
|
+
// `CheckoutIntentMetadata`, which is the shape either one carries.
|
|
3026
3061
|
const intent = gatewayOrder.metadata;
|
|
3027
3062
|
const uid = input.userId ?? intent?.uid;
|
|
3028
3063
|
if (!uid) {
|
|
@@ -3428,9 +3463,9 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
3428
3463
|
const amountDelta = paidAmountRs - expectedPaymentAmountRs;
|
|
3429
3464
|
const AMOUNT_TOLERANCE_RS = 1;
|
|
3430
3465
|
if (amountDelta < -AMOUNT_TOLERANCE_RS) {
|
|
3431
|
-
serverLogger.warn(`Payment amount mismatch for user ${uid}: paid
|
|
3466
|
+
serverLogger.warn(`Payment amount mismatch for user ${uid}: paid ${formatMoney(paidAmountRs)}, expected ≥ ${formatMoney(expectedPaymentAmountRs)}`);
|
|
3432
3467
|
failedCheckoutRepository
|
|
3433
|
-
.logPayment(uid, "amount_mismatch", `Paid
|
|
3468
|
+
.logPayment(uid, "amount_mismatch", `Paid ${formatMoney(paidAmountRs)}, expected ≥ ${formatMoney(expectedPaymentAmountRs)}`, {
|
|
3434
3469
|
gatewayOrderId: gatewayOrder.gatewayOrderId,
|
|
3435
3470
|
gatewayPaymentId: merchantOrderId,
|
|
3436
3471
|
amountRs: paidAmountRs,
|
|
@@ -3440,9 +3475,9 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
3440
3475
|
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.PAYMENT_FAILED);
|
|
3441
3476
|
}
|
|
3442
3477
|
if (amountDelta > AMOUNT_TOLERANCE_RS) {
|
|
3443
|
-
serverLogger.error(`OVER-CAPTURE for user ${uid}: paid
|
|
3478
|
+
serverLogger.error(`OVER-CAPTURE for user ${uid}: paid ${formatMoney(paidAmountRs)}, expected ${formatMoney(expectedPaymentAmountRs)} (surplus ${formatMoney(amountDelta)}) — order will still be placed`, { merchantOrderId, expectedCouponDiscount });
|
|
3444
3479
|
failedCheckoutRepository
|
|
3445
|
-
.logPayment(uid, "amount_mismatch", `OVER-CAPTURE: paid
|
|
3480
|
+
.logPayment(uid, "amount_mismatch", `OVER-CAPTURE: paid ${formatMoney(paidAmountRs)}, expected ${formatMoney(expectedPaymentAmountRs)}, surplus ${formatMoney(amountDelta)} — refund owed`, {
|
|
3446
3481
|
gatewayOrderId: gatewayOrder.gatewayOrderId,
|
|
3447
3482
|
gatewayPaymentId: merchantOrderId,
|
|
3448
3483
|
amountRs: paidAmountRs,
|
|
@@ -37,6 +37,7 @@ import { PAYMENT_WINDOW_MS, PAYMENT_FRAUD_REJECTED_REASON, isManualPaymentMethod
|
|
|
37
37
|
import { deadlinesFor } from "../../../shared/obligations/deadlines.js";
|
|
38
38
|
import { normalizeError } from "../../../../errors/normalize.js";
|
|
39
39
|
import { serverLogger } from "../../../../monitoring/index.js";
|
|
40
|
+
import { formatMoney } from "../../../../utils/number.formatter.js";
|
|
40
41
|
export async function createOrderAction(input) {
|
|
41
42
|
return wrapAction(async () => {
|
|
42
43
|
const user = await requireRoleUser(["user", "buyer", "seller", "admin"]);
|
|
@@ -181,7 +182,7 @@ function notifyAdminsOfPaymentProof(order) {
|
|
|
181
182
|
await adminNotificationsRepository.create({
|
|
182
183
|
category: "payouts",
|
|
183
184
|
title: "Payment proof awaiting review",
|
|
184
|
-
body: `${order.userName} uploaded proof for "${order.productTitle}" (
|
|
185
|
+
body: `${order.userName} uploaded proof for "${order.productTitle}" (${formatMoney(order.totalPrice)}). Review before the 2-hour auto-approve window closes.`,
|
|
185
186
|
severity: "warning",
|
|
186
187
|
isRead: false,
|
|
187
188
|
entityType: "order",
|
|
@@ -269,6 +269,24 @@ export function orderDocumentToOrder(doc) {
|
|
|
269
269
|
supplierGstin: doc.supplierGstin,
|
|
270
270
|
supplierLegalName: doc.supplierLegalName,
|
|
271
271
|
supplierAddress: doc.supplierAddress,
|
|
272
|
+
/*
|
|
273
|
+
* 🛑 Root Cause #57, in the one place it is most visible: this adapter
|
|
274
|
+
* mapped the CGST/SGST/IGST split above and dropped the field that
|
|
275
|
+
* EXPLAINS it.
|
|
276
|
+
*
|
|
277
|
+
* Place of supply is what decides whether a supply is intra-State
|
|
278
|
+
* (CGST+SGST) or inter-State (IGST). Without it the HTML invoice showed a
|
|
279
|
+
* split it could not justify — and the PDF, which reads the raw
|
|
280
|
+
* `OrderDocument` rather than going through here, printed the line
|
|
281
|
+
* correctly. So the two invoices for ONE order disagreed on a Rule 46(n)
|
|
282
|
+
* field, and the buyer-facing one was the deficient half.
|
|
283
|
+
*
|
|
284
|
+
* Nothing was wrong with the data: `createOrderForGroup` has always
|
|
285
|
+
* written it. It simply never crossed the client boundary, which is why
|
|
286
|
+
* the gap reads as "this order has no place of supply" rather than as a
|
|
287
|
+
* dropped field.
|
|
288
|
+
*/
|
|
289
|
+
placeOfSupply: doc.placeOfSupply,
|
|
272
290
|
codHandlingFee: doc.codHandlingFee,
|
|
273
291
|
trackingNumber: doc.trackingNumber,
|
|
274
292
|
shippingCarrier: doc.shippingCarrier,
|
|
@@ -35,6 +35,7 @@ import { AuthorizationError } from "../../../../errors/authorization-error.js";
|
|
|
35
35
|
import { storeRepository } from "../../../../features/stores/repository/store.repository.js";
|
|
36
36
|
import { sendNotification } from "../../../../features/admin/actions/notification-actions.js";
|
|
37
37
|
import { serverLogger } from "../../../../monitoring/server-logger.js";
|
|
38
|
+
import { formatMoney } from "../../../../utils/number.formatter.js";
|
|
38
39
|
/*
|
|
39
40
|
* 🛑 ONE message, for the same reason the ads collector names its own: an
|
|
40
41
|
* order that is not the caller's answers 404 rather than 403, so "no such
|
|
@@ -128,7 +129,7 @@ export async function openBalanceCollection(orderId, actor, now = new Date()) {
|
|
|
128
129
|
type: "order_confirmed",
|
|
129
130
|
priority: "high",
|
|
130
131
|
title: "Your pre-order balance is due",
|
|
131
|
-
message: `The balance of
|
|
132
|
+
message: `The balance of ${formatMoney(due)} on order ${orderId} is now payable. Your item is ready to ship once it clears.`,
|
|
132
133
|
relatedId: orderId,
|
|
133
134
|
relatedType: "order",
|
|
134
135
|
});
|
|
@@ -1,92 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cancel an order AND give the money back — one owner, three callers.
|
|
3
|
-
*
|
|
4
|
-
* ## D14, and why passing the amount was only half of it
|
|
5
|
-
*
|
|
6
|
-
* `cancelOrder(orderId, reason, refundAmount?)` has carried an optional
|
|
7
|
-
* `refundAmount` since it was written, and **all four callers omitted it**. So
|
|
8
|
-
* cancelling a *paid* order restored stock and left the money with us, while
|
|
9
|
-
* `previewCancellationRefund` showed the buyer a figure nothing honoured.
|
|
10
|
-
*
|
|
11
|
-
* 🛑 **But passing the amount alone would have made it WORSE.** `cancelOrder`'s
|
|
12
|
-
* body writes `{ refundAmount, refundStatus: "pending" }` and nothing else —
|
|
13
|
-
* there is no provider call anywhere in it. So the obvious fix creates a *new*
|
|
14
|
-
* dead state: a recorded refund intent that nothing ever settles, which is
|
|
15
|
-
* strictly worse than today's honest silence because it reads, in the database,
|
|
16
|
-
* as though a refund is on its way. The amount and the movement have to land
|
|
17
|
-
* together, which is why this module exists rather than a one-line edit at each
|
|
18
|
-
* call site.
|
|
19
|
-
*
|
|
20
|
-
* ## The decision table
|
|
21
|
-
*
|
|
22
|
-
* Every row cancels the order and **restores its stock** — see `releaseStock`,
|
|
23
|
-
* which closes a second half of D14 the plan had recorded backwards. The table
|
|
24
|
-
* is only about the money.
|
|
25
|
-
*
|
|
26
|
-
* | condition | what happens to the money |
|
|
27
|
-
* |---|---|
|
|
28
|
-
* | `paymentStatus !== "paid"` | **nothing** — nothing was ever captured |
|
|
29
|
-
* | nothing left refundable | nothing — already refunded in full |
|
|
30
|
-
* | goods have left the seller | intent recorded, **case opened** — a human decides |
|
|
31
|
-
* | paid by cash / UPI-manual / EMI | intent recorded, **case opened** — there is no automatic rail |
|
|
32
|
-
* | paid through the gateway | **refunded automatically**, in full, once |
|
|
33
|
-
*
|
|
34
|
-
* The last row is the only automatic money movement, and it satisfies the
|
|
35
|
-
* plan's guardrail for one: the ceiling is an amount we captured, the recipient
|
|
36
|
-
* is the payer, and no counterparty is debited.
|
|
37
|
-
*
|
|
38
|
-
* ## Three things that are load-bearing
|
|
39
|
-
*
|
|
40
|
-
* **It goes through `processRefundAction`, never `payment.refund()` directly.**
|
|
41
|
-
* That action owns the idempotency claim, the cumulative cap against
|
|
42
|
-
* `refundableBase`, and the two-phase reserve/settle write around the provider
|
|
43
|
-
* call. A direct provider call here would reintroduce every defect step 8
|
|
44
|
-
* closed — D1, D2, D6 and D7 at once.
|
|
45
|
-
*
|
|
46
|
-
* **The key is `cancel_{orderId}`** — a NATURAL key, so a double-clicked Cancel
|
|
47
|
-
* button, a retried server action and a sweep all collapse to one refund. A
|
|
48
|
-
* `randomUUID()` here would be D2 wearing a new name.
|
|
49
|
-
*
|
|
50
|
-
* ## Points ARE reversed here now (Phase P)
|
|
51
|
-
*
|
|
52
|
-
* `pointsLedger`'s own header states the rule as a legal boundary: *"points
|
|
53
|
-
* spent on a cancelled order are returned as points"* — never as money, because
|
|
54
|
-
* paying them out in rupees is what would make this stored value, which in
|
|
55
|
-
* India is PPI territory with RBI authorisation attached.
|
|
56
|
-
*
|
|
57
|
-
* `reverseRedemptionForOrder` is that rule's first writer; the kind had been
|
|
58
|
-
* declared with none since it was written (D48). It is idempotent on the order
|
|
59
|
-
* — one reversal per order, checked by query before the write, because an
|
|
60
|
-
* append-only ledger cannot net off a double reversal.
|
|
61
|
-
*
|
|
62
|
-
* 🛑 **It stopped being a no-op on 2026-10-22.** This paragraph read "It is a
|
|
63
|
-
* NO-OP today and still correct to call … when redemption becomes a real
|
|
64
|
-
* tender, cancellation already gives the points back" — and the tender is now
|
|
65
|
-
* built (`resolvePointsTender` + `settlePointsTender` in `checkout/actions.ts`),
|
|
66
|
-
* so orders carry `pointsApplied` and this query finds real redemptions. The
|
|
67
|
-
* bet paid off: no cancellation path had to be remembered, because the reversal
|
|
68
|
-
* was already wired and keyed.
|
|
69
|
-
*
|
|
70
|
-
* 🛑 **The MONEY half had to change in the same commit, and it is the sharper
|
|
71
|
-
* of the two.** `refundableBase` now subtracts `pointsApplied`, because a
|
|
72
|
-
* refund can only return what was collected: on a ₹1,000 order settled with
|
|
73
|
-
* 250 points the card paid ₹750, so refunding the full base would return
|
|
74
|
-
* ₹1,000 in cash AND 250 points — ₹250 of real money conjured from a discount.
|
|
75
|
-
* Two halves of one refund, in two units, and only their sum is what the buyer
|
|
76
|
-
* gave up.
|
|
77
|
-
*
|
|
78
|
-
* **Cancel, then stock, then money.** The cancellation is the urgent half and
|
|
79
|
-
* must not be held hostage to a gateway round trip; the restock is a local
|
|
80
|
-
* write another buyer is actively waiting on; the refund is the one step that
|
|
81
|
-
* can hang for a full network timeout. The cost of that ordering is a window
|
|
82
|
-
* where the order is `cancelled` with the money still held — which is exactly
|
|
83
|
-
* why `refundAmount` is written *at cancellation time*, before the provider is
|
|
84
|
-
* called: a crash in the window leaves a detectable artefact rather than
|
|
85
|
-
* nothing, and R5 in `paymentReconcile` is the thing that finds it. The reverse
|
|
86
|
-
* ordering has no such consolation, because a refund posted against a live
|
|
87
|
-
* order that then fails to cancel is money gone from an order the buyer still
|
|
88
|
-
* expects to receive.
|
|
89
|
-
*/
|
|
90
1
|
import type { OrderDocument } from "../../../../features/orders/schemas/firestore";
|
|
91
2
|
import type { CancellationRefundOutcome } from "../../../../features/orders/types/index";
|
|
92
3
|
import type { HistoryActor } from "../../../shared/history/types";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatMoney } from "../../../../utils/number.formatter.js";
|
|
1
2
|
/**
|
|
2
3
|
* Cancel an order AND give the money back — one owner, three callers.
|
|
3
4
|
*
|
|
@@ -212,7 +213,7 @@ async function openOwedCase(order, amount, why, detailLines) {
|
|
|
212
213
|
*/
|
|
213
214
|
subjectId: order.id,
|
|
214
215
|
severity: "error",
|
|
215
|
-
title: `Refund of
|
|
216
|
+
title: `Refund of ${formatMoney(amount)} owed on cancelled order ${order.id}`,
|
|
216
217
|
detail: [
|
|
217
218
|
...detailLines,
|
|
218
219
|
"",
|
|
@@ -221,7 +222,7 @@ async function openOwedCase(order, amount, why, detailLines) {
|
|
|
221
222
|
`Store: ${order.storeId ?? "—"}`,
|
|
222
223
|
`Payment method: ${order.paymentMethod ?? "unknown"}`,
|
|
223
224
|
`Gateway reference: ${order.paymentId ?? "—"}`,
|
|
224
|
-
`Amount owed:
|
|
225
|
+
`Amount owed: ${formatMoney(amount)}`,
|
|
225
226
|
"",
|
|
226
227
|
"The order is cancelled and its stock is restored. `refundAmount` is recorded on the order with `refundStatus: \"pending\"`, so this case and the order agree about what is owed.",
|
|
227
228
|
].join("\n"),
|
|
@@ -64,6 +64,7 @@ import { payoutRepository } from "../../../../features/payments/repository/payou
|
|
|
64
64
|
import { serverErrorsRepository } from "../../../../features/server-errors/repository/server-errors.repository.js";
|
|
65
65
|
import { ValidationError } from "../../../../errors/index.js";
|
|
66
66
|
import { roundRupees } from "../../../../utils/number.formatter.js";
|
|
67
|
+
import { formatMoney } from "../../../../utils/number.formatter.js";
|
|
67
68
|
export async function applyRefundDeductionAction(input) {
|
|
68
69
|
return wrapAction(async () => {
|
|
69
70
|
if (input.refundedAmount <= 0) {
|
|
@@ -95,7 +96,7 @@ export async function applyRefundDeductionAction(input) {
|
|
|
95
96
|
source: "vercel",
|
|
96
97
|
route: "applyRefundDeductionAction",
|
|
97
98
|
code,
|
|
98
|
-
message: `Clawback of
|
|
99
|
+
message: `Clawback of ${formatMoney(deductedAmount)} for order ${input.orderId} (store ${input.storeId}) could not be applied: ${unrecoverable}. ` +
|
|
99
100
|
`The seller retains this amount for returned goods; there is no automatic recovery until a seller ledger exists.`,
|
|
100
101
|
requestId: input.refundId,
|
|
101
102
|
});
|
|
@@ -56,6 +56,7 @@ import { POINTS_EARN_PERCENT, pointsExpiryFrom, } from "../../../../features/poi
|
|
|
56
56
|
import { orderTaxableValue } from "../../../shared/checkout/order-math.js";
|
|
57
57
|
import { normalizeError } from "../../../../errors/normalize.js";
|
|
58
58
|
import { serverLogger } from "../../../../monitoring/index.js";
|
|
59
|
+
import { formatMoney } from "../../../../utils/number.formatter.js";
|
|
59
60
|
function roundPoints(value) {
|
|
60
61
|
return Math.round(value * 100) / 100;
|
|
61
62
|
}
|
|
@@ -160,13 +161,13 @@ export async function awardPurchasePoints(order) {
|
|
|
160
161
|
* later — and written once rather than re-derived, so it stays true of this
|
|
161
162
|
* moment even if the order is edited afterwards.
|
|
162
163
|
*/
|
|
163
|
-
description: `${POINTS_EARN_PERCENT}% back on ${order.id}:
|
|
164
|
+
description: `${POINTS_EARN_PERCENT}% back on ${order.id}: ${formatMoney(moneyPaidForGoods)} of items` +
|
|
164
165
|
// Named explicitly, because an unexplained gap between the order's item
|
|
165
166
|
// value and the earn base is exactly the kind of figure a buyer writes
|
|
166
167
|
// in to ask about — and the answer ("you paid part of it with points")
|
|
167
168
|
// is a fact about their own order that they can check.
|
|
168
169
|
((order.pointsApplied ?? 0) > 0
|
|
169
|
-
? ` (
|
|
170
|
+
? ` (${formatMoney(goods)} of items less ${order.pointsApplied} points).`
|
|
170
171
|
: "."),
|
|
171
172
|
expiresAt: pointsExpiryFrom(now),
|
|
172
173
|
});
|
|
@@ -35,6 +35,7 @@ import { adjudicateClaim, SALE_PROTECTION_MAX_VALUE, } from "../../../shared/fea
|
|
|
35
35
|
import { deadlinesFor } from "../../../shared/obligations/deadlines.js";
|
|
36
36
|
import { paymentCasesRepository } from "../../../../features/payment-cases/repository/payment-cases.repository.js";
|
|
37
37
|
import { orderGoodsValue } from "../../../shared/checkout/order-math.js";
|
|
38
|
+
import { formatMoney } from "../../../../utils/number.formatter.js";
|
|
38
39
|
/**
|
|
39
40
|
* File a claim as the buyer.
|
|
40
41
|
*
|
|
@@ -100,7 +101,7 @@ export async function fileProtectionClaimAction(input) {
|
|
|
100
101
|
*/
|
|
101
102
|
const claimedAmount = orderGoodsValue(order);
|
|
102
103
|
if (kind === "sale" && claimedAmount > SALE_PROTECTION_MAX_VALUE) {
|
|
103
|
-
throw new ValidationError(`Sale protection covers orders up to
|
|
104
|
+
throw new ValidationError(`Sale protection covers orders up to ${formatMoney(SALE_PROTECTION_MAX_VALUE)}. This order is above that, so the claim needs a human — please contact support.`);
|
|
104
105
|
}
|
|
105
106
|
const claim = await protectionClaimsRepository().file({
|
|
106
107
|
kind,
|
|
@@ -49,6 +49,7 @@ import { applyRefundDeductionAction } from "../payouts/actions.js";
|
|
|
49
49
|
import { issueCreditNoteForRefund } from "../invoicing/credit-note.js";
|
|
50
50
|
import { getAdminDb } from "../../../../providers/db-firebase/index.js";
|
|
51
51
|
import { PRODUCT_COLLECTION, PRODUCT_CODES_SUBCOLLECTION, } from "../../../../features/products/schemas/firestore.js";
|
|
52
|
+
import { formatMoney } from "../../../../utils/number.formatter.js";
|
|
52
53
|
/**
|
|
53
54
|
* SB-UNI-N — block the refund if any of the order's digital codes has already
|
|
54
55
|
* been claimed (revealed). Throws ValidationError on the first claimed code.
|
|
@@ -146,9 +147,9 @@ export async function processRefundAction(input) {
|
|
|
146
147
|
*/
|
|
147
148
|
const capacity = refundCapacity(order, siteSettings?.commissions?.gstPercent);
|
|
148
149
|
if (input.amount > capacity.remaining + MONEY_EPSILON) {
|
|
149
|
-
throw new ValidationError(`Refund of
|
|
150
|
+
throw new ValidationError(`Refund of ${formatMoney(input.amount)} exceeds the ${formatMoney(capacity.remaining)} still refundable on this order` +
|
|
150
151
|
(capacity.withheld > 0
|
|
151
|
-
? ` (
|
|
152
|
+
? ` (${formatMoney(capacity.withheld)} platform fee is non-refundable).`
|
|
152
153
|
: "."));
|
|
153
154
|
}
|
|
154
155
|
// Guard: non-refundable orders (prize draws, bundles). A property of the
|
|
@@ -23,7 +23,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
23
23
|
* operator blind to everything else.
|
|
24
24
|
*/
|
|
25
25
|
import { Fragment } from "react";
|
|
26
|
-
|
|
26
|
+
// 🛑 Lazy require, not a top-level `react-dom/server` import — this module is
|
|
27
|
+
// reachable from a Server Component via appkit/server, and Next's static tracer
|
|
28
|
+
// fails the build on it. See render-static-markup.ts.
|
|
29
|
+
import { renderStaticMarkup } from "../../../../features/email/render-static-markup.js";
|
|
27
30
|
import { EmailContainer, EmailDoc, EmailRow, } from "../../../../features/email/primitives.js";
|
|
28
31
|
import { paymentCasesRepository } from "../../../../features/payment-cases/repository/payment-cases.repository.js";
|
|
29
32
|
// `adminNotificationsRepository` lives behind the repositories barrel — the
|
|
@@ -260,5 +263,5 @@ async function emailStaff(input) {
|
|
|
260
263
|
* our own constants and the provider/reason enums.
|
|
261
264
|
*/
|
|
262
265
|
function renderStaffAlertEmail(title, body) {
|
|
263
|
-
return `<!DOCTYPE html>${
|
|
266
|
+
return `<!DOCTYPE html>${renderStaticMarkup(_jsx(EmailDoc, { title: title, children: _jsx(EmailContainer, { children: _jsx(EmailRow, { children: body.split("\n\n").map((para, i) => (_jsxs(Fragment, { children: [i > 0 ? _jsx("br", {}) : null, i > 0 ? _jsx("br", {}) : null, para.split("\n").map((line, j) => (_jsxs(Fragment, { children: [j > 0 ? _jsx("br", {}) : null, line] }, j)))] }, i))) }) }) }))}`;
|
|
264
267
|
}
|
|
@@ -8,6 +8,7 @@ import { orderRepository } from "../../../../features/orders/repository/orders.r
|
|
|
8
8
|
import { pointsRepository } from "../../../../features/points/repository/points.repository.js";
|
|
9
9
|
import { reconcileShipping, } from "../../../shared/shipping/quote.js";
|
|
10
10
|
import { serverLogger } from "../../../../monitoring/index.js";
|
|
11
|
+
import { formatMoney } from "../../../../utils/number.formatter.js";
|
|
11
12
|
/**
|
|
12
13
|
* 🛑 IDEMPOTENT, and the guard is `order.shippingReconciliation` being unset.
|
|
13
14
|
*
|
|
@@ -106,8 +107,8 @@ export async function reconcileOrderShipping(orderId, actual) {
|
|
|
106
107
|
* never re-derived, so it stays true of the moment it describes even if
|
|
107
108
|
* the order is edited afterwards.
|
|
108
109
|
*/
|
|
109
|
-
description: `Shipping saving on ${orderId}: charged
|
|
110
|
-
`for ${order.selectedShippingQuote.courierName}, shipped
|
|
110
|
+
description: `Shipping saving on ${orderId}: charged ${formatMoney(order.selectedShippingQuote.chargedRate)} ` +
|
|
111
|
+
`for ${order.selectedShippingQuote.courierName}, shipped ${formatMoney(actual.actualRate)} ` +
|
|
111
112
|
`via ${actual.courierName}.`,
|
|
112
113
|
});
|
|
113
114
|
}
|