@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
|
@@ -40,6 +40,7 @@ import { orderRepository } from "../../../../features/orders/repository/orders.r
|
|
|
40
40
|
import { pointsRepository } from "../../../../features/points/repository/points.repository.js";
|
|
41
41
|
import { reconcileShipping, isReimbursableShipment, } from "../../../shared/shipping/quote.js";
|
|
42
42
|
import { serverLogger } from "../../../../monitoring/index.js";
|
|
43
|
+
import { formatMoney } from "../../../../utils/number.formatter.js";
|
|
43
44
|
/**
|
|
44
45
|
* The seller submits postage evidence on a self-shipped order.
|
|
45
46
|
*
|
|
@@ -191,8 +192,8 @@ export async function approveShippingReimbursement(input) {
|
|
|
191
192
|
* and this string is written once and never re-derived, so it stays true
|
|
192
193
|
* of the moment it describes.
|
|
193
194
|
*/
|
|
194
|
-
description: `Shipping saving on ${input.orderId}: you paid
|
|
195
|
-
`the approved cost was
|
|
195
|
+
description: `Shipping saving on ${input.orderId}: you paid ${formatMoney(chargedToBuyer)} for postage, ` +
|
|
196
|
+
`the approved cost was ${formatMoney(spent)}.`,
|
|
196
197
|
});
|
|
197
198
|
}
|
|
198
199
|
await orderRepository.update(input.orderId, {
|
|
@@ -27,7 +27,7 @@ import type { StoreDetail, StoreListItem } from "../../../../features/stores/typ
|
|
|
27
27
|
*/
|
|
28
28
|
export declare const PUBLIC_STORE_FIELDS: readonly ["id", "storeSlug", "ownerId", "storeName", "storeDescription", "storeCategory", "storeLogoURL", "storeBannerURL", "status", "isPublic", "stats", "bio", "location", "website", "socialLinks", "returnPolicy", "shippingPolicy", "isVacationMode", "vacationMessage", "supportsShippingProtection", "supportsSaleProtection", "allowSharedPurchase", "sharedPurchaseHandlingFee", "sharedPurchaseExtraShippingPerDestination", "isVerified", "sellerTier", "standing", "createdAt"];
|
|
29
29
|
/** Everything else, with the reason it stays server-side. */
|
|
30
|
-
export declare const PRIVATE_STORE_FIELDS: readonly ["whatsappNotifySubscribed", "whatsappFeedEnabled", "trustedSeller", "acceptsPoints", "searchTxt", "
|
|
30
|
+
export declare const PRIVATE_STORE_FIELDS: readonly ["whatsappNotifySubscribed", "whatsappFeedEnabled", "trustedSeller", "acceptsPoints", "searchTxt", "updatedAt", "adminNotes", "statusHistory", "statusHistoryTruncated", "tierLockedByAdmin", "suspensionReason", "capabilities", "customCommissionRate", "googleReviews", "shippingConfig", "emiEnabled", "pricesIncludeGst", "shippingPricing", "returnsDirectToSeller", "isFeatured", "vacationReturnDate", "isTestData", "testDataExpiresAt", "gstin", "legalName", "registeredAddress"];
|
|
31
31
|
/**
|
|
32
32
|
* Accepted input: a `StoreDocument` (the common case) or a loosely-typed
|
|
33
33
|
* Firestore row. The legacy top-level counters below predate `stats` and are
|
|
@@ -168,9 +168,19 @@ export const PRIVATE_STORE_FIELDS = [
|
|
|
168
168
|
* what, which is commercial terms (same reason `customCommissionRate` is
|
|
169
169
|
* below), and it has no reader on any public store surface.
|
|
170
170
|
*/
|
|
171
|
-
|
|
171
|
+
/*
|
|
172
|
+
* 🛑 `whatsappNotifySubscribed` was listed TWICE in this array, and
|
|
173
|
+
* `whatsappConfig` was listed although `StoreDocument` says of it
|
|
174
|
+
* "🛑 whatsappConfig IS GONE AND MUST NOT COME BACK" — so this list named a
|
|
175
|
+
* field that does not exist and double-counted one that does.
|
|
176
|
+
*
|
|
177
|
+
* Both were invisible to `audit-public-projection-parity`, which checks that
|
|
178
|
+
* every SCHEMA field appears in exactly one list but not the reverse: it
|
|
179
|
+
* never asks whether a listed name is real, or listed once. An exemption
|
|
180
|
+
* list is a claim nobody checks unless something checks it (Root Cause
|
|
181
|
+
* #106) — the duplicate half is enforced now; see that audit's R5.
|
|
182
|
+
*/
|
|
172
183
|
"updatedAt", // bookkeeping; no public reader
|
|
173
|
-
"whatsappConfig", // Meta access token + WABA/catalog ids — secret
|
|
174
184
|
"adminNotes", // schema comment: never shown to the owner or the public
|
|
175
185
|
// Admin decisions with actor UIDs and suspension reasons. The store OWNER
|
|
176
186
|
// sees this on their own admin surfaces; an anonymous visitor to /stores has
|
|
@@ -14,7 +14,10 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
14
14
|
* sync ceiling — it has the Function's much larger budget.
|
|
15
15
|
*/
|
|
16
16
|
import { Fragment } from "react";
|
|
17
|
-
|
|
17
|
+
// 🛑 Lazy require, not a top-level `react-dom/server` import — this module is
|
|
18
|
+
// reachable from a Server Component via appkit/server, and Next's static tracer
|
|
19
|
+
// fails the build on it. See render-static-markup.ts.
|
|
20
|
+
import { renderStaticMarkup } from "../../../../features/email/render-static-markup.js";
|
|
18
21
|
import { getAdminDb } from "../../../../providers/db-firebase/index.js";
|
|
19
22
|
import { getProviders } from "../../../../contracts/registry.js";
|
|
20
23
|
import { checkCourierWalletAndAlert } from "../../features/shipping/courier-block.js";
|
|
@@ -40,6 +43,7 @@ import { PAYMENT_CASES_COLLECTION, PAYMENT_CASE_ACTIVE_STATUSES, PAYMENT_CASE_FI
|
|
|
40
43
|
import { GATEWAY_EVENTS_COLLECTION, GATEWAY_EVENT_FIELDS, } from "../../../../features/gateway-events/schemas/firestore.js";
|
|
41
44
|
import { EmailBold, EmailContainer, EmailDivider, EmailDoc, EmailFooter, EmailHeader, EmailLink, EmailRow, } from "../../../../features/email/primitives.js";
|
|
42
45
|
import { normalizeError } from "../../../../errors/normalize.js";
|
|
46
|
+
import { formatMoney } from "../../../../utils/number.formatter.js";
|
|
43
47
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
44
48
|
/*
|
|
45
49
|
* `createdAt` and `isRead` on collections with no feature-specific field-name
|
|
@@ -62,7 +66,7 @@ const NON_REVENUE_STATUSES = new Set([
|
|
|
62
66
|
/** How many contact messages the digest lists in full before summarising the rest. */
|
|
63
67
|
const CONTACT_PREVIEW_LIMIT = 20;
|
|
64
68
|
function currency(amount) {
|
|
65
|
-
return
|
|
69
|
+
return formatMoney(amount);
|
|
66
70
|
}
|
|
67
71
|
/** One "Label: value" line. Uses only email primitives — email clients need
|
|
68
72
|
* inline-styled, table-based markup, which the Email* components provide. */
|
|
@@ -263,7 +267,7 @@ export async function collectDailyStatusSummary() {
|
|
|
263
267
|
function renderDigestHtml(summary, siteName, version) {
|
|
264
268
|
const statusRows = Object.entries(summary.statusBreakdown).sort(([a], [b]) => a.localeCompare(b));
|
|
265
269
|
const title = siteName ? `${siteName} — Daily Status` : "Daily Status";
|
|
266
|
-
return `<!DOCTYPE html>${
|
|
270
|
+
return `<!DOCTYPE html>${renderStaticMarkup(_jsx(EmailDoc, { title: title, children: _jsxs(EmailContainer, { children: [_jsx(EmailHeader, { brandName: siteName }), _jsxs(EmailRow, { children: [version
|
|
267
271
|
? `Deployment ${version} is live. Activity for the last 24 hours.`
|
|
268
272
|
: "Activity for the last 24 hours.", _jsx("br", {}), _jsx("br", {}), _jsx(StatLine, { label: "Orders placed", value: summary.orderCount }), _jsx(StatLine, { label: "Revenue (excl. cancelled/refunded)", value: currency(summary.revenue) }), _jsx(StatLine, { label: "Active listings", value: summary.activeListingCount }), _jsx(StatLine, { label: "Pending > 24h (needs attention)", value: summary.stuckPendingCount })] }), _jsx(EmailDivider, {}), _jsxs(EmailRow, { children: [_jsx(EmailBold, { children: "Orders by status" }), _jsx("br", {}), _jsx("br", {}), statusRows.length === 0 ? ("No orders in this window.") : (statusRows.map(([status, count]) => (_jsx(StatLine, { label: status, value: count }, status))))] }), _jsx(EmailDivider, {}), _jsxs(EmailRow, { children: [_jsx(EmailBold, { children: "Inbox" }), _jsx("br", {}), _jsx("br", {}), _jsx(StatLine, { label: "New contact messages", value: summary.contactTotal }), _jsx(StatLine, { label: "New support tickets", value: summary.newTicketCount }), _jsx(StatLine, { label: "Tickets open / in progress / waiting on user", value: `${summary.ticketsOpen} / ${summary.ticketsInProgress} / ${summary.ticketsWaitingOnUser}` }), _jsx(StatLine, { label: "Unread admin notifications", value: summary.unreadAdminNotificationCount })] }), summary.contactMessages.length > 0 ? (_jsxs(EmailRow, { children: [_jsx(EmailBold, { children: "Contact messages" }), _jsx("br", {}), _jsx("br", {}), summary.contactMessages.map((m) => (_jsxs(Fragment, { children: [_jsx(EmailBold, { children: m.subject }), _jsx("br", {}), m.name, " \u2014", " ", _jsxs(EmailLink, { href: `mailto:${m.email}?subject=${encodeURIComponent(`Re: ${m.subject}`)}`, children: ["Reply to ", m.email] }), _jsx("br", {}), m.excerpt, _jsx("br", {}), _jsx("br", {})] }, m.id))), summary.contactTotal > summary.contactMessages.length
|
|
269
273
|
? `…and ${summary.contactTotal - summary.contactMessages.length} more in /admin/contact.`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { normalizeError } from "../../../../errors/normalize.js";
|
|
2
2
|
import { sendNotification } from "../../../../features/admin/actions/notification-actions.js";
|
|
3
3
|
import { orderRepository } from "../../../../repositories/index.js";
|
|
4
|
+
import { formatMoney } from "../../../../utils/number.formatter.js";
|
|
4
5
|
const REMINDER_WINDOW_MS = 3 * 86400000;
|
|
5
6
|
function toDate(value) {
|
|
6
7
|
return value instanceof Date ? value : new Date(value);
|
|
@@ -46,7 +47,7 @@ export async function runEmiInstallmentReminder(ctx) {
|
|
|
46
47
|
title: isOverdue ? "EMI installment overdue" : "EMI installment due soon",
|
|
47
48
|
message: isOverdue
|
|
48
49
|
? `Installment ${inst.index} for order ${id} is overdue. Pay now to avoid delaying your shipment.`
|
|
49
|
-
: `Installment ${inst.index} for order ${id} (
|
|
50
|
+
: `Installment ${inst.index} for order ${id} (${formatMoney(inst.amount)}) is due on ${dueDate.toDateString()}.`,
|
|
50
51
|
relatedId: id,
|
|
51
52
|
relatedType: "order",
|
|
52
53
|
actionUrl: `/user/orders/view/${id}`,
|
|
@@ -34,6 +34,7 @@ import { awardPurchasePoints } from "../../features/points/award.js";
|
|
|
34
34
|
import { processRefundAction } from "../../features/refunds/actions.js";
|
|
35
35
|
import { resolveRefundCapacity } from "../../features/refunds/capacity.js";
|
|
36
36
|
import { MONEY_EPSILON } from "../../../shared/checkout/refundable-base.js";
|
|
37
|
+
import { formatMoney } from "../../../../utils/number.formatter.js";
|
|
37
38
|
/**
|
|
38
39
|
* Perform the policy's action.
|
|
39
40
|
*
|
|
@@ -304,14 +305,14 @@ async function performAction(ctx, obligation, order) {
|
|
|
304
305
|
kind: "manual_refund_due",
|
|
305
306
|
subjectId: order.id,
|
|
306
307
|
severity: "error",
|
|
307
|
-
title: `Uninspected return — automatic refund of
|
|
308
|
+
title: `Uninspected return — automatic refund of ${formatMoney(capacity.remaining)} FAILED on ${order.id}`,
|
|
308
309
|
detail: [
|
|
309
310
|
"A received return was never inspected, so it auto-resolved in the buyer's favour — and the refund itself failed.",
|
|
310
311
|
"",
|
|
311
312
|
`Gateway error: ${result.error ?? "unknown"}`,
|
|
312
313
|
"",
|
|
313
314
|
`Order: ${order.id}`,
|
|
314
|
-
`Owed:
|
|
315
|
+
`Owed: ${formatMoney(capacity.remaining)}`,
|
|
315
316
|
"",
|
|
316
317
|
"Retrying from the admin order page is idempotent on this order.",
|
|
317
318
|
].join("\n"),
|
|
@@ -332,7 +333,7 @@ async function performAction(ctx, obligation, order) {
|
|
|
332
333
|
kind: "refund_mismatch",
|
|
333
334
|
subjectId: `seller-charge:${order.id}`,
|
|
334
335
|
severity: "warning",
|
|
335
|
-
title: `Decide whether the seller bears
|
|
336
|
+
title: `Decide whether the seller bears ${formatMoney(capacity.remaining)} — uninspected return on ${order.id}`,
|
|
336
337
|
detail: [
|
|
337
338
|
"A returned parcel reached the seller and was never inspected, so the buyer has been refunded automatically — the cover is unconditional and the seller had the goods and the full window.",
|
|
338
339
|
"",
|
|
@@ -340,7 +341,7 @@ async function performAction(ctx, obligation, order) {
|
|
|
340
341
|
"",
|
|
341
342
|
`Order: ${order.id}`,
|
|
342
343
|
`Store: ${order.storeId ?? "—"}`,
|
|
343
|
-
`Refunded to the buyer:
|
|
344
|
+
`Refunded to the buyer: ${formatMoney(capacity.remaining)}`,
|
|
344
345
|
`Return received: ${order.returnReceivedAt?.toISOString() ?? "—"}`,
|
|
345
346
|
`Buyer's stated reason: ${order.returnReasonCode ?? "—"}`,
|
|
346
347
|
"",
|
|
@@ -3,6 +3,7 @@ import { adminNotificationsRepository } from "../../../../features/store-extensi
|
|
|
3
3
|
import { userRepository } from "../../../../features/auth/repository/user.repository.js";
|
|
4
4
|
import { sendNotification } from "../../../../features/admin/actions/notification-actions.js";
|
|
5
5
|
import { SCAM_TYPE_LABELS } from "../../../../features/scams/constants/scam-types.js";
|
|
6
|
+
import { formatMoney } from "../../../../utils/number.formatter.js";
|
|
6
7
|
export async function handleScamReportCreate(input, ctx) {
|
|
7
8
|
const { scammerId, report } = input;
|
|
8
9
|
const { reportedBy, displayNames, scamType, scamPlatform, amountLost } = report;
|
|
@@ -59,7 +60,7 @@ export async function handleScamReportCreate(input, ctx) {
|
|
|
59
60
|
*/
|
|
60
61
|
try {
|
|
61
62
|
const scamTypeLabel = scamType ? (SCAM_TYPE_LABELS[scamType] ?? scamType) : "Unknown";
|
|
62
|
-
const amountStr = amountLost ? `
|
|
63
|
+
const amountStr = amountLost ? ` ${formatMoney(amountLost)}` : "";
|
|
63
64
|
const platformStr = scamPlatform ? ` via ${scamPlatform}` : "";
|
|
64
65
|
await adminNotificationsRepository.create({
|
|
65
66
|
category: "fraud",
|
|
@@ -40,6 +40,7 @@ import { REFUND_CLAIMS_COLLECTION, REFUND_CLAIM_FIELDS, ORDER_COLLECTION, } from
|
|
|
40
40
|
import { ORDER_FIELDS } from "../../../../constants/field-names.js";
|
|
41
41
|
import { reconcileDisputes } from "../../features/payment-cases/disputes.js";
|
|
42
42
|
import { refundableBase, committedRefundTotal, MONEY_EPSILON, } from "../../../shared/checkout/refundable-base.js";
|
|
43
|
+
import { formatMoney } from "../../../../utils/number.formatter.js";
|
|
43
44
|
/** How far back R2 looks. Two days of overlap, so a run that fails is covered by the next. */
|
|
44
45
|
const PAYMENT_WINDOW_HOURS = 48;
|
|
45
46
|
/** A refund claim unsettled for longer than this is a crash between call and record. */
|
|
@@ -478,7 +479,7 @@ async function reconcileUnpaidCancellations(ctx) {
|
|
|
478
479
|
kind: "manual_refund_due",
|
|
479
480
|
subjectId: doc.id,
|
|
480
481
|
severity: "error",
|
|
481
|
-
title: `Cancelled order still owes
|
|
482
|
+
title: `Cancelled order still owes ${formatMoney(shortfall)} — ${doc.id}`,
|
|
482
483
|
detail: [
|
|
483
484
|
"This order is cancelled and records a refund it has not paid.",
|
|
484
485
|
"",
|
|
@@ -488,9 +489,9 @@ async function reconcileUnpaidCancellations(ctx) {
|
|
|
488
489
|
"cover.",
|
|
489
490
|
"",
|
|
490
491
|
`Order: ${doc.id}`,
|
|
491
|
-
`Recorded as owed:
|
|
492
|
-
`Actually refunded:
|
|
493
|
-
`Shortfall:
|
|
492
|
+
`Recorded as owed: ${formatMoney(intended)}`,
|
|
493
|
+
`Actually refunded: ${formatMoney(committed)}`,
|
|
494
|
+
`Shortfall: ${formatMoney(shortfall)}`,
|
|
494
495
|
data.paymentMethod ? `Payment method: ${data.paymentMethod}` : null,
|
|
495
496
|
data.paymentId ? `Gateway reference: ${data.paymentId}` : null,
|
|
496
497
|
data.userId ? `Buyer: ${data.userId}` : null,
|
|
@@ -5,14 +5,32 @@ export const auctionRule = {
|
|
|
5
5
|
cartEligible: false, // bid-only; add-to-cart is blocked at the action layer
|
|
6
6
|
cartIneligibleHint: "Place a bid on the auction page instead.",
|
|
7
7
|
/*
|
|
8
|
-
* 🛑
|
|
9
|
-
*
|
|
10
|
-
* auction
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* 🛑 THE GATEWAY IS PROHIBITED HERE, and this list was INVERTED until
|
|
9
|
+
* 2026-10-28 — it blocked `cod` and `cash` while leaving `online` open, so
|
|
10
|
+
* every auction win was payable by card.
|
|
11
|
+
*
|
|
12
|
+
* The reason is the acquirer's, not ours. Auction and bidding businesses sit
|
|
13
|
+
* on the prohibited-merchant lists Indian PGs inherit from the card networks,
|
|
14
|
+
* alongside lotteries and games of chance. Settling auction wins through a
|
|
15
|
+
* standard e-commerce MID is a terms breach, and the penalty is not a
|
|
16
|
+
* declined transaction — it is the whole merchant account frozen with the
|
|
17
|
+
* settlement balance held. One category of order can cost every other
|
|
18
|
+
* category its ability to take money at all.
|
|
19
|
+
*
|
|
20
|
+
* So: manual bank/UPI transfer, or cash on pickup. Both land in the owner's
|
|
21
|
+
* own account with no acquirer in the path.
|
|
22
|
+
*
|
|
23
|
+
* 🛑 `cash` is now ALLOWED, reversing the rule below it. The old comment is
|
|
24
|
+
* kept because its reasoning is still sound — a pay-later method on a unique
|
|
25
|
+
* lot has no second-highest bidder to fall back on — but it was weighing
|
|
26
|
+
* seller risk against buyer convenience, and that trade is not what decides
|
|
27
|
+
* this. The storefront is single-vendor and the owner is the seller: they
|
|
28
|
+
* accept the non-payment risk on their own lots, and they do not get to
|
|
29
|
+
* accept the risk of losing the merchant account. `emi` goes for the same
|
|
30
|
+
* reason as `online` — it settles through the same acquirer.
|
|
13
31
|
*/
|
|
14
|
-
blockedPaymentMethods: ["cod", "
|
|
15
|
-
blockedPaymentMessage: "An auction win
|
|
32
|
+
blockedPaymentMethods: ["online", "cod", "emi"],
|
|
33
|
+
blockedPaymentMessage: "An auction win is settled by direct bank/UPI transfer or cash on pickup — card and online payment are not available for auctions.",
|
|
16
34
|
canMergeWithSameProduct: false,
|
|
17
35
|
refundPolicy: { full: true, partial: false, partialGranularity: "none" },
|
|
18
36
|
splitKey: (item) => `auction:${item.itemId}`,
|
|
@@ -2,14 +2,32 @@ import { ValidationError } from "../../../../errors/index.js";
|
|
|
2
2
|
import { DEFAULT_LISTING_RULE } from "./_defaults.js";
|
|
3
3
|
import { PRIZE_DRAW_MAX_REVEALS_PER_ORDER } from "./_limits.js";
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* 🛑 THE GATEWAY IS PROHIBITED HERE — `online` joined this list 2026-10-28,
|
|
6
|
+
* and `cash` left it.
|
|
7
|
+
*
|
|
8
|
+
* A prize draw is a game of chance, which is on the prohibited-merchant lists
|
|
9
|
+
* Indian payment gateways inherit from the card networks — the same lists that
|
|
10
|
+
* carry lotteries and auctions. Routing entry payments through a standard
|
|
11
|
+
* e-commerce MID is a terms breach, and the penalty is the merchant account
|
|
12
|
+
* frozen with the settlement balance held, not a declined charge. One category
|
|
13
|
+
* can cost every other category its ability to take money.
|
|
14
|
+
*
|
|
15
|
+
* 🛑 This REPLACES the original reasoning rather than extending it, and the
|
|
16
|
+
* original is worth keeping in view because it is still true and no longer
|
|
17
|
+
* decisive: deferred methods were blocked because instant-mode winner
|
|
18
|
+
* assignment triggers on confirmed payment, which `cod`/`cash`/`emi` do not
|
|
19
|
+
* guarantee happens promptly. By that logic `online` was the SAFEST method
|
|
20
|
+
* available and `cash` the worst.
|
|
21
|
+
*
|
|
22
|
+
* The acquirer's rules invert it. `online` and `emi` both settle through the
|
|
23
|
+
* gateway and are out; `cash` is in, because the owner is the seller and
|
|
24
|
+
* confirms the payment themselves before the draw. The timing concern becomes
|
|
25
|
+
* an operational one — do not run the draw until payment is confirmed — which
|
|
26
|
+
* `upi_manual` already required and the reveal flow already handles.
|
|
27
|
+
*
|
|
28
|
+
* `admin_bypass` stays usable for QA regardless.
|
|
11
29
|
*/
|
|
12
|
-
const PRIZE_DRAW_BLOCKED_PAYMENT_METHODS = ["
|
|
30
|
+
const PRIZE_DRAW_BLOCKED_PAYMENT_METHODS = ["online", "cod", "emi"];
|
|
13
31
|
/**
|
|
14
32
|
* The refusal, kept VERBATIM when this check moved out of `preflightChecks`
|
|
15
33
|
* into `assertPaymentMethodAllowed`.
|
|
@@ -20,7 +38,7 @@ const PRIZE_DRAW_BLOCKED_PAYMENT_METHODS = ["cod", "cash", "emi"];
|
|
|
20
38
|
* reason is the thing a buyer needs in order to pick a different method rather
|
|
21
39
|
* than abandon the entry.
|
|
22
40
|
*/
|
|
23
|
-
const PRIZE_DRAW_BLOCKED_PAYMENT_MESSAGE = "Prize-draw entries
|
|
41
|
+
const PRIZE_DRAW_BLOCKED_PAYMENT_MESSAGE = "Prize-draw entries are paid by direct bank/UPI transfer or cash on pickup — card and online payment are not available for prize draws.";
|
|
24
42
|
export const prizeDrawRule = {
|
|
25
43
|
...DEFAULT_LISTING_RULE,
|
|
26
44
|
orderType: "prize-draw",
|
|
@@ -95,6 +95,21 @@ export interface AppkitBrandConfig {
|
|
|
95
95
|
* {name}") and this one stands alone, so they are spelled differently.
|
|
96
96
|
*/
|
|
97
97
|
heroCaption?: string;
|
|
98
|
+
/**
|
|
99
|
+
* TLD badge drawn in a pill after the SVG wordmark, e.g. `"in"`. Absent
|
|
100
|
+
* renders no badge.
|
|
101
|
+
*
|
|
102
|
+
* 🛑 Optional, and appkit renders nothing without it — a library must not
|
|
103
|
+
* assume its consumer has a TLD worth showing, let alone which one. The
|
|
104
|
+
* badge used to be a hardcoded `.in` inside `SiteLogo`, where no prop could
|
|
105
|
+
* reach it (A5).
|
|
106
|
+
*
|
|
107
|
+
* 🛑 NOT a source for the site's domain. `brand.name + "." + tld` happens to
|
|
108
|
+
* give `letitrip.in` here, which is what makes it a trap: any multi-word
|
|
109
|
+
* brand yields `my shop.com`. The host has one owner, `seo.siteUrl`
|
|
110
|
+
* (Root Cause #81), and `BRAND.DOMAIN` derives from that.
|
|
111
|
+
*/
|
|
112
|
+
tld?: string;
|
|
98
113
|
/** One-line brand description used in meta tags and footers */
|
|
99
114
|
description?: string;
|
|
100
115
|
/** Tagline shown in footer / about sections */
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* sentence with a word missing, which nobody reports, while `{{wrongKey}}` on
|
|
45
45
|
* a live page gets reported in minutes.
|
|
46
46
|
*/
|
|
47
|
-
export type PublishedValueKey = "returnWindowDays" | "sellerAcceptDays" | "sellerDispatchBusinessDays" | "returnRespondHours" | "returnPostDays" | "offerCheckoutHours" | "paymentWindowMinutes" | "deliveryConfirmDays" | "orderCompleteDays" | "shipmentClaimHours" | "saleClaimHours" | "disputeTransitWindowDays" | "disputeNotReachedGraceDays" | "platformFeeMax" | "platformFeePercent" | "shipmentProtectionPercent" | "shipmentProtectionMin" | "saleProtectionPercent" | "saleProtectionMin" | "saleProtectionMaxValue" | "codHandlingPercent" | "codDepositPercent" | "giftWrapFee" | "pointsEarnPercent" | "pointsMaxRedemptionPercent" | "pointsExpiryMonths" | "refundBusinessDaysMin" | "refundBusinessDaysMax" | "siteName";
|
|
47
|
+
export type PublishedValueKey = "returnWindowDays" | "sellerAcceptDays" | "sellerDispatchBusinessDays" | "returnRespondHours" | "returnPostDays" | "offerCheckoutHours" | "paymentWindowMinutes" | "deliveryConfirmDays" | "orderCompleteDays" | "shipmentClaimHours" | "saleClaimHours" | "disputeTransitWindowDays" | "disputeNotReachedGraceDays" | "platformFeeMax" | "platformFeePercent" | "shipmentProtectionPercent" | "shipmentProtectionMin" | "saleProtectionPercent" | "saleProtectionMin" | "saleProtectionMaxValue" | "codHandlingPercent" | "codDepositPercent" | "giftWrapFee" | "pointsEarnPercent" | "pointsMaxRedemptionPercent" | "pointsExpiryMonths" | "refundBusinessDaysMin" | "refundBusinessDaysMax" | "siteName" | "siteDomain";
|
|
48
48
|
export type PublishedValues = Record<PublishedValueKey, string>;
|
|
49
49
|
/**
|
|
50
50
|
* The settings this registry reads, where a value genuinely is configurable.
|
|
@@ -58,7 +58,7 @@ export type PublishedValues = Record<PublishedValueKey, string>;
|
|
|
58
58
|
export interface PublishedValueSettings {
|
|
59
59
|
/**
|
|
60
60
|
* The operator's display name, for the disclaimers that name who is making
|
|
61
|
-
* the statement. Falls back to `
|
|
61
|
+
* the statement. Falls back to the injected brand (`getBrandName()`).
|
|
62
62
|
*/
|
|
63
63
|
siteName?: string;
|
|
64
64
|
platformFeeMax?: number;
|
|
@@ -40,13 +40,14 @@
|
|
|
40
40
|
* somebody has to remember — and it is why there is no literal for the offer
|
|
41
41
|
* deadline or the claim windows even though both appear in published copy.
|
|
42
42
|
*/
|
|
43
|
-
import {
|
|
43
|
+
import { getBrandName, getBrandDomain } from "../../../core/brand-resolver.js";
|
|
44
44
|
import { OBLIGATION_POLICIES } from "../../../features/obligations/constants/policies.js";
|
|
45
45
|
import { POINTS_EARN_PERCENT, POINTS_EXPIRY_MONTHS, POINTS_MAX_REDEMPTION_PERCENT, } from "../../../features/points/schemas/firestore.js";
|
|
46
46
|
import { PAYMENT_WINDOW_MINUTES } from "../../../features/orders/constants/payment-window.js";
|
|
47
47
|
import { SALE_PROTECTION_MAX_VALUE } from "../features/shipping/protections.js";
|
|
48
48
|
import { LANE_CHECKOUT_WINDOW_MS } from "../checkout/lanes.js";
|
|
49
49
|
import { DISPUTE_TRANSIT_WINDOW_DAYS, DISPUTE_NOT_REACHED_GRACE_DAYS, } from "../features/orders/dispute-reasons.js";
|
|
50
|
+
import { formatMoney } from "../../../utils/number.formatter.js";
|
|
50
51
|
/**
|
|
51
52
|
* The offer checkout deadline, in hours, derived from the constant the lapse
|
|
52
53
|
* sweep actually enforces.
|
|
@@ -173,12 +174,23 @@ export function resolvePublishedValues(settings) {
|
|
|
173
174
|
refundBusinessDaysMin: "5",
|
|
174
175
|
refundBusinessDaysMax: "7",
|
|
175
176
|
/*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
177
|
+
* Admin setting first, then the brand the consumer INJECTED at boot.
|
|
178
|
+
*
|
|
179
|
+
* 🛑 Was `SITE_CONFIG.brand.name`, and the reasoning given for that was
|
|
180
|
+
* right about the failure mode — "a fork that forgets to configure its
|
|
181
|
+
* brand publishes a visibly generic disclaimer rather than silently
|
|
182
|
+
* attributing its legal statements to us" — but wrong about the mechanism:
|
|
183
|
+
* `SITE_CONFIG` is a FROZEN const, and the only thing that can override it
|
|
184
|
+
* (`createSiteConfig`) has **zero callers**. So the override it described
|
|
185
|
+
* did not exist, and a consumer with a configured brand still published
|
|
186
|
+
* "App" in its own legal text whenever `siteSettings.siteName` was unset.
|
|
187
|
+
*
|
|
188
|
+
* `getBrandName()` keeps the same safe default ("Site") and adds the
|
|
189
|
+
* injection path that was missing.
|
|
180
190
|
*/
|
|
181
|
-
siteName: settings?.siteName ??
|
|
191
|
+
siteName: settings?.siteName ?? getBrandName(),
|
|
192
|
+
/** Bare host, for addresses quoted in published copy. */
|
|
193
|
+
siteDomain: getBrandDomain(),
|
|
182
194
|
};
|
|
183
195
|
}
|
|
184
196
|
/**
|
|
@@ -196,11 +208,18 @@ export function resolvePublishedCopy(text, values) {
|
|
|
196
208
|
return resolved ?? whole;
|
|
197
209
|
});
|
|
198
210
|
}
|
|
199
|
-
/**
|
|
211
|
+
/**
|
|
212
|
+
* `₹10`, `₹1,499.50` — no trailing `.00` on a whole figure.
|
|
213
|
+
*
|
|
214
|
+
* 🛑 A `const whole = Number.isInteger(amount)` sat here unused after the A2
|
|
215
|
+
* conversion, and an unused local is worth opening rather than deleting: in
|
|
216
|
+
* this codebase a discarded value has three times turned out to be a dropped
|
|
217
|
+
* one (a lost delivery snapshot, two blank seller tiles, a full page reload).
|
|
218
|
+
*
|
|
219
|
+
* Checked — this one is genuinely dead. `formatMoney` runs exactly that test
|
|
220
|
+
* internally (`Number.isInteger(amount) ? 0 : 2`), which is what this function
|
|
221
|
+
* used to do by hand before delegating. Nothing is lost by removing it.
|
|
222
|
+
*/
|
|
200
223
|
function formatRupees(amount) {
|
|
201
|
-
|
|
202
|
-
return `₹${amount.toLocaleString("en-IN", {
|
|
203
|
-
minimumFractionDigits: whole ? 0 : 2,
|
|
204
|
-
maximumFractionDigits: 2,
|
|
205
|
-
})}`;
|
|
224
|
+
return formatMoney(amount);
|
|
206
225
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NotFoundError, ValidationError, ConflictError, ExpiredError } from "../../errors/index.js";
|
|
2
|
+
import { formatMoney } from "../../../../utils/number.formatter.js";
|
|
2
3
|
export class AuctionNotFoundError extends NotFoundError {
|
|
3
4
|
constructor(id) {
|
|
4
5
|
super("Auction", id);
|
|
@@ -13,7 +14,7 @@ export class AuctionEndedError extends ExpiredError {
|
|
|
13
14
|
}
|
|
14
15
|
export class BidTooLowError extends ValidationError {
|
|
15
16
|
constructor(minBid) {
|
|
16
|
-
super(`Bid must be at least
|
|
17
|
+
super(`Bid must be at least ${formatMoney(minBid)}`, "amount");
|
|
17
18
|
this.name = "BidTooLowError";
|
|
18
19
|
}
|
|
19
20
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatMoney } from "../../../../utils/number.formatter.js";
|
|
1
2
|
/**
|
|
2
3
|
* REFUND_COPY — all user-visible strings for refund + order-sibling UI.
|
|
3
4
|
*
|
|
@@ -79,10 +80,10 @@ export const REFUND_COPY = {
|
|
|
79
80
|
cancellation: {
|
|
80
81
|
message: (outcome) => {
|
|
81
82
|
if (outcome.moved === "gateway") {
|
|
82
|
-
return `Order cancelled. A refund of
|
|
83
|
+
return `Order cancelled. A refund of ${formatMoney(outcome.amount)} has been sent back to your original payment method.`;
|
|
83
84
|
}
|
|
84
85
|
if (outcome.moved === "owed") {
|
|
85
|
-
return `Order cancelled. We owe you
|
|
86
|
+
return `Order cancelled. We owe you ${formatMoney(outcome.amount)} — it needs to be sent manually, and our team has been notified.`;
|
|
86
87
|
}
|
|
87
88
|
/*
|
|
88
89
|
* `none` splits two very different situations and must not blur them: an
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NotFoundError, ConflictError, ValidationError, ExpiredError } from "../../errors/index.js";
|
|
2
|
+
import { formatMoney } from "../../../../utils/number.formatter.js";
|
|
2
3
|
export class CouponNotFoundError extends NotFoundError {
|
|
3
4
|
constructor(code) {
|
|
4
5
|
super("Coupon", code);
|
|
@@ -25,7 +26,7 @@ export class CouponPerUserLimitError extends ConflictError {
|
|
|
25
26
|
}
|
|
26
27
|
export class CouponMinPurchaseError extends ValidationError {
|
|
27
28
|
constructor(minAmount) {
|
|
28
|
-
super(`Minimum purchase of
|
|
29
|
+
super(`Minimum purchase of ${formatMoney(minAmount)} required for this coupon`, "total");
|
|
29
30
|
this.name = "CouponMinPurchaseError";
|
|
30
31
|
}
|
|
31
32
|
}
|
|
@@ -1058,5 +1058,28 @@ export declare const SCHEMA_DEFAULTS: {
|
|
|
1058
1058
|
readonly UNKNOWN_USER: "Unknown User";
|
|
1059
1059
|
readonly ANONYMOUS_USER: "Anonymous";
|
|
1060
1060
|
readonly DEFAULT_DISPLAY_NAME: "User";
|
|
1061
|
+
/**
|
|
1062
|
+
* 🛑 NOT A DEFAULT — THIS CONSTANT GRANTS THE ADMIN ROLE, and it is the one
|
|
1063
|
+
* brand literal in appkit that was DELIBERATELY left in place by A5d.
|
|
1064
|
+
*
|
|
1065
|
+
* Three consumer auth routes read it as an authorisation decision:
|
|
1066
|
+
* `email === SCHEMA_DEFAULTS.ADMIN_EMAIL ? "admin" : "user"`
|
|
1067
|
+
* in `api/auth/register`, `api/auth/session` and `api/auth/google/callback`.
|
|
1068
|
+
* So whoever controls this address is auto-promoted to admin at signup or at
|
|
1069
|
+
* first session — in EVERY install of appkit, because the address is baked
|
|
1070
|
+
* into the library rather than configured by the consumer.
|
|
1071
|
+
*
|
|
1072
|
+
* It was not changed with the other 131 brand literals because changing it
|
|
1073
|
+
* changes who becomes an administrator. CLAUDE.md Rule #1 puts "security
|
|
1074
|
+
* boundaries: auth, RBAC" on the escalate-before-acting list, and rewiring an
|
|
1075
|
+
* admin-promotion check as a drive-by inside a branding sweep is exactly the
|
|
1076
|
+
* shape that rule forbids — a wrong move here either locks the owner out or
|
|
1077
|
+
* hands the role to someone else.
|
|
1078
|
+
*
|
|
1079
|
+
* The fix needs an owner decision, and the options differ in blast radius:
|
|
1080
|
+
* move it to `appkit.config.js` and inject it like the brand; replace
|
|
1081
|
+
* email-matching with an explicit role grant; or drop auto-promotion and seed
|
|
1082
|
+
* the first admin. Tracked, not silently chosen.
|
|
1083
|
+
*/
|
|
1061
1084
|
readonly ADMIN_EMAIL: "admin@letitrip.in";
|
|
1062
1085
|
};
|
|
@@ -1285,5 +1285,28 @@ export const SCHEMA_DEFAULTS = {
|
|
|
1285
1285
|
UNKNOWN_USER: "Unknown User",
|
|
1286
1286
|
ANONYMOUS_USER: "Anonymous",
|
|
1287
1287
|
DEFAULT_DISPLAY_NAME: "User",
|
|
1288
|
+
/**
|
|
1289
|
+
* 🛑 NOT A DEFAULT — THIS CONSTANT GRANTS THE ADMIN ROLE, and it is the one
|
|
1290
|
+
* brand literal in appkit that was DELIBERATELY left in place by A5d.
|
|
1291
|
+
*
|
|
1292
|
+
* Three consumer auth routes read it as an authorisation decision:
|
|
1293
|
+
* `email === SCHEMA_DEFAULTS.ADMIN_EMAIL ? "admin" : "user"`
|
|
1294
|
+
* in `api/auth/register`, `api/auth/session` and `api/auth/google/callback`.
|
|
1295
|
+
* So whoever controls this address is auto-promoted to admin at signup or at
|
|
1296
|
+
* first session — in EVERY install of appkit, because the address is baked
|
|
1297
|
+
* into the library rather than configured by the consumer.
|
|
1298
|
+
*
|
|
1299
|
+
* It was not changed with the other 131 brand literals because changing it
|
|
1300
|
+
* changes who becomes an administrator. CLAUDE.md Rule #1 puts "security
|
|
1301
|
+
* boundaries: auth, RBAC" on the escalate-before-acting list, and rewiring an
|
|
1302
|
+
* admin-promotion check as a drive-by inside a branding sweep is exactly the
|
|
1303
|
+
* shape that rule forbids — a wrong move here either locks the owner out or
|
|
1304
|
+
* hands the role to someone else.
|
|
1305
|
+
*
|
|
1306
|
+
* The fix needs an owner decision, and the options differ in blast radius:
|
|
1307
|
+
* move it to `appkit.config.js` and inject it like the brand; replace
|
|
1308
|
+
* email-matching with an explicit role grant; or drop auto-promotion and seed
|
|
1309
|
+
* the first admin. Tracked, not silently chosen.
|
|
1310
|
+
*/
|
|
1288
1311
|
ADMIN_EMAIL: "admin@letitrip.in",
|
|
1289
1312
|
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-storage and postMessage keys — one namespace, configurable.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* Seven call sites hardcoded a `letitrip`-prefixed key:
|
|
7
|
+
*
|
|
8
|
+
* localStorage "letitrip:section-state" · "letitrip:last-category"
|
|
9
|
+
* "letitrip:announcement-dismissed" · "letitrip_oauth_redirect"
|
|
10
|
+
* "letitrip_search_type"
|
|
11
|
+
* postMessage "letitrip_auth_close"
|
|
12
|
+
*
|
|
13
|
+
* For the app that shipped them that is harmless. For a LIBRARY it is not:
|
|
14
|
+
* every consumer inherits one marketplace's namespace, two appkit apps on the
|
|
15
|
+
* same origin collide, and the brand is baked into data the user's browser
|
|
16
|
+
* keeps. Tier AK's whole premise is that appkit should not know whose app it
|
|
17
|
+
* is in.
|
|
18
|
+
*
|
|
19
|
+
* ## 🛑 The prefix is read at CALL TIME, not at module load
|
|
20
|
+
*
|
|
21
|
+
* `storageKey()` is a function and the keys below are getters. A consumer sets
|
|
22
|
+
* the namespace from its own bootstrap, which runs after this module is
|
|
23
|
+
* evaluated — so a `const KEY = PREFIX + ":x"` captured at import time would
|
|
24
|
+
* freeze the default and silently ignore the configuration. That is the same
|
|
25
|
+
* trap as a mirror field (Root Cause #42): two things that must agree, where
|
|
26
|
+
* only one is ever written.
|
|
27
|
+
*
|
|
28
|
+
* ## 🛑 Changing the namespace ORPHANS existing values
|
|
29
|
+
*
|
|
30
|
+
* These keys are how a returning visitor's dismissed banner and collapsed
|
|
31
|
+
* sections are remembered. Renaming the prefix does not migrate them — the old
|
|
32
|
+
* entries simply stop being read. That is acceptable for UI state and would NOT
|
|
33
|
+
* be for anything load-bearing, which is why nothing load-bearing belongs here:
|
|
34
|
+
* the auth session is an HTTP-only cookie, not a storage key.
|
|
35
|
+
*
|
|
36
|
+
* ## The separator is deliberately NOT normalised
|
|
37
|
+
*
|
|
38
|
+
* Five keys use `:` and three use `_`. They are kept exactly as they were,
|
|
39
|
+
* because normalising them is a silent data migration for every existing
|
|
40
|
+
* visitor in exchange for tidiness. New keys should use `:`.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Set the prefix every appkit storage key carries. Call once, from the
|
|
44
|
+
* consumer's client bootstrap, before any UI mounts.
|
|
45
|
+
*
|
|
46
|
+
* Idempotent and last-call-wins; there is no unset, because a namespace that
|
|
47
|
+
* changes mid-session would strand whatever was written under the previous one.
|
|
48
|
+
*/
|
|
49
|
+
export declare function setStorageNamespace(value: string): void;
|
|
50
|
+
/** The active namespace. Exported for diagnostics, not for building keys. */
|
|
51
|
+
export declare function getStorageNamespace(): string;
|
|
52
|
+
/**
|
|
53
|
+
* Build a namespaced key.
|
|
54
|
+
*
|
|
55
|
+
* @param suffix the part after the separator, e.g. `"last-category"`
|
|
56
|
+
* @param separator `":"` for new keys; `"_"` only where an existing key used it
|
|
57
|
+
*/
|
|
58
|
+
export declare function storageKey(suffix: string, separator?: ":" | "_"): string;
|
|
59
|
+
/**
|
|
60
|
+
* The keys appkit itself owns.
|
|
61
|
+
*
|
|
62
|
+
* Getters, not constants — see the call-time note above. Each records the
|
|
63
|
+
* literal it replaced so the migration is auditable and a reader can tell at a
|
|
64
|
+
* glance which separator is historical rather than chosen.
|
|
65
|
+
*/
|
|
66
|
+
export declare const STORAGE_KEYS: {
|
|
67
|
+
/** Collapsed account-page sections. Was `"letitrip:section-state"`. */
|
|
68
|
+
readonly sectionState: string;
|
|
69
|
+
/** Last category visited, for the contextual trail. Was `"letitrip:last-category"`. */
|
|
70
|
+
readonly lastCategory: string;
|
|
71
|
+
/** Announcement bar dismissal. Was `"letitrip:announcement-dismissed"`. */
|
|
72
|
+
readonly announcementDismissed: string;
|
|
73
|
+
/** OAuth return URL across the popup round-trip. Was `"letitrip_oauth_redirect"`. */
|
|
74
|
+
readonly oauthRedirect: string;
|
|
75
|
+
/** Remembered search-type tab. Was `"letitrip_search_type"`. */
|
|
76
|
+
readonly searchType: string;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* postMessage `type` values.
|
|
80
|
+
*
|
|
81
|
+
* 🛑 Namespaced for the same reason, and with a sharper edge: a `message`
|
|
82
|
+
* listener receives events from ANY window that can reach it, so an
|
|
83
|
+
* unnamespaced type is both a collision and an input-validation question. The
|
|
84
|
+
* listeners that read this must keep checking `event.origin` regardless — a
|
|
85
|
+
* namespace is not an authentication mechanism.
|
|
86
|
+
*/
|
|
87
|
+
export declare const MESSAGE_TYPES: {
|
|
88
|
+
/** Auth popup signalling it is done. Was `"letitrip_auth_close"`. */
|
|
89
|
+
readonly authClose: string;
|
|
90
|
+
};
|