@mohasinac/appkit 4.55.0-next.0 → 4.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.build-stamp.json +4 -4
- package/dist/_internal/server/features/checkout/actions.d.ts +22 -0
- package/dist/_internal/server/features/checkout/actions.js +100 -5
- package/dist/_internal/server/features/shipping/reconcile.d.ts +28 -0
- package/dist/_internal/server/features/shipping/reconcile.js +93 -0
- package/dist/_internal/shared/config/schema.d.ts +6 -0
- package/dist/_internal/shared/features/checkout/config.d.ts +1 -1
- package/dist/_internal/shared/features/checkout/config.js +1 -1
- package/dist/_internal/shared/features/site-settings/fees.d.ts +1 -1
- package/dist/_internal/shared/features/site-settings/fees.js +1 -1
- package/dist/_internal/shared/fees/calculator.d.ts +35 -3
- package/dist/_internal/shared/fees/calculator.js +34 -4
- package/dist/_internal/shared/listing-types/route-gating.d.ts +56 -0
- package/dist/_internal/shared/listing-types/route-gating.js +114 -0
- package/dist/_internal/shared/shipping/quote.d.ts +153 -0
- package/dist/_internal/shared/shipping/quote.js +124 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +8 -0
- package/dist/errors/error-codes.d.ts +15 -0
- package/dist/errors/error-codes.js +15 -0
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/features/account/components/UserBidsView.js +4 -2
- package/dist/features/admin/components/AdminSiteSettingsView.js +18 -2
- package/dist/features/admin/schemas/firestore.d.ts +37 -2
- package/dist/features/admin/schemas/firestore.js +12 -1
- package/dist/features/auctions/components/AuctionDetailPageView.js +2 -1
- package/dist/features/auth/schemas/firestore.d.ts +15 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +1 -1
- package/dist/features/checkout/schemas/firestore.d.ts +12 -1
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +2 -1
- package/dist/features/homepage/components/HeroCarousel.js +4 -2
- package/dist/features/homepage/components/MarketplaceHomepageView.js +16 -1
- package/dist/features/homepage/components/WelcomeSection.d.ts +16 -3
- package/dist/features/homepage/components/WelcomeSection.js +3 -3
- package/dist/features/homepage/lib/section-renderer.d.ts +9 -0
- package/dist/features/homepage/lib/section-renderer.js +22 -5
- package/dist/features/homepage/lib/slide-gating.d.ts +52 -0
- package/dist/features/homepage/lib/slide-gating.js +98 -0
- package/dist/features/layout/NavbarLayout.js +139 -32
- package/dist/features/live/components/LiveItemDetailPageView.js +2 -1
- package/dist/features/media/MediaAvatar.js +1 -1
- package/dist/features/media/MediaImage.d.ts +18 -1
- package/dist/features/media/MediaImage.js +1 -1
- package/dist/features/media/messages/en.json +67 -67
- package/dist/features/orders/schemas/firestore.d.ts +39 -1
- package/dist/features/points/repository/points.repository.d.ts +81 -0
- package/dist/features/points/repository/points.repository.js +155 -0
- package/dist/features/points/schemas/firestore.d.ts +102 -0
- package/dist/features/points/schemas/firestore.js +99 -0
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -1
- package/dist/features/products/components/ProductDetailPageView.js +5 -26
- package/dist/features/products/components/ProductOwnerQuickActions.d.ts +11 -0
- package/dist/features/products/components/ProductOwnerQuickActions.js +120 -0
- package/dist/features/products/components/ProductsIndexListing.js +35 -5
- package/dist/features/products/components/ProductsIndexPageView.js +40 -7
- package/dist/features/seller/actions/seller-actions.d.ts +17 -0
- package/dist/features/seller/actions/seller-actions.js +33 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +8 -0
- package/dist/providers/shipping-shiprocket/index.d.ts +13 -4
- package/dist/providers/shipping-shiprocket/index.js +34 -10
- package/dist/security/csp.d.ts +1 -16
- package/dist/security/csp.js +3 -5
- package/dist/seed/carousel-slides-seed-data.js +48 -7
- package/dist/seed/faq-seed-data.js +1 -1
- package/dist/seed/site-settings-seed-data.js +1 -1
- package/dist/seo/json-ld.d.ts +8 -1
- package/dist/seo/json-ld.js +39 -2
- package/dist/styles.css +419 -542
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/index.d.ts +1 -3
- package/dist/tokens/themes/index.js +1 -11
- package/dist/tokens/tokens.css +0 -155
- package/dist/ui/components/GatedPrice.style.css +43 -43
- package/dist/ui/components/ImageMosaic.d.ts +17 -1
- package/dist/ui/components/ImageMosaic.js +28 -5
- package/dist/ui/components/ImageMosaic.style.css +37 -5
- package/dist/ui/components/QuickCreateModal.style.css +78 -78
- package/dist/ui/forms/FormShell.style.css +260 -260
- package/package.json +1 -1
- package/dist/tokens/themes/technical-dark.d.ts +0 -30
- package/dist/tokens/themes/technical-dark.js +0 -103
- package/dist/tokens/themes/technical-light.d.ts +0 -22
- package/dist/tokens/themes/technical-light.js +0 -89
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Does this href lead somewhere the site has switched off?"
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* `listings.listingTypes` already hides a disabled type everywhere the type is
|
|
7
|
+
* named — the nav, the `/products` chips, the admin tabs, the homepage
|
|
8
|
+
* SECTIONS (`HOMEPAGE_SECTION_LISTING_TYPE` in `MarketplaceHomepageView`). But
|
|
9
|
+
* a lot of this site's merchandising is free-text content that names a
|
|
10
|
+
* destination by URL and never mentions a listing type at all: a carousel
|
|
11
|
+
* slide's overlay button, a card's CTA, a promo-grid tile.
|
|
12
|
+
*
|
|
13
|
+
* Measured 2026-10-20 with `auction: false` live: the homepage hero carousel
|
|
14
|
+
* still shipped a "Live auctions / Place a bid before the timer runs out"
|
|
15
|
+
* CARD linking to `/auctions`, and a whole `slide-beyblade-auction` slide
|
|
16
|
+
* whose overlay read "Win it at your price". Both are authored content, so no
|
|
17
|
+
* type-keyed gate could see them — the only thing that identifies them is
|
|
18
|
+
* where they point.
|
|
19
|
+
*
|
|
20
|
+
* ## The rule, and the one case that makes it non-obvious
|
|
21
|
+
*
|
|
22
|
+
* A href is disabled when EVERY listing type that claims its route is
|
|
23
|
+
* disabled. Not "any" — `art` and `stickers` share `/art`, so a site that
|
|
24
|
+
* sells art but not stickers must keep `/art` reachable. Treating one disabled
|
|
25
|
+
* claimant as enough would silently delete a working destination.
|
|
26
|
+
*
|
|
27
|
+
* ## What it deliberately does NOT match
|
|
28
|
+
*
|
|
29
|
+
* Editorial pages that merely have a type's name in the path — `/help/auctions`,
|
|
30
|
+
* `/how-auctions-work`, `/seller-guide/prize-draws`. None of them starts with
|
|
31
|
+
* `/auctions`, so prefix matching excludes them for free, and that is correct:
|
|
32
|
+
* an explainer about auctions is still honest reading when auctions are off.
|
|
33
|
+
* A substring test would have eaten all three.
|
|
34
|
+
*
|
|
35
|
+
* Store tabs (`/stores/{slug}/auctions`) are also out of scope — those are
|
|
36
|
+
* resolved by `resolveStoreTabs`, which owns its own visibility, and a second
|
|
37
|
+
* owner is how a mirror drifts (Root Cause #42).
|
|
38
|
+
*/
|
|
39
|
+
import { LISTING_TYPE_REGISTRY } from "./_registry.js";
|
|
40
|
+
import { ALL_LISTING_TYPES, isListingTypeEnabled } from "./feature-flags.js";
|
|
41
|
+
/**
|
|
42
|
+
* browse route → every listing type that browses there.
|
|
43
|
+
*
|
|
44
|
+
* Built from the registry rather than hand-listed, so a tenth type joins it by
|
|
45
|
+
* existing. `art` and `stickers` both resolve to `/art`, which is the entire
|
|
46
|
+
* reason the value is an array.
|
|
47
|
+
*/
|
|
48
|
+
const TYPES_BY_BROWSE_ROUTE = (() => {
|
|
49
|
+
const map = new Map();
|
|
50
|
+
for (const type of ALL_LISTING_TYPES) {
|
|
51
|
+
const route = normalisePath(LISTING_TYPE_REGISTRY[type].browseRoute);
|
|
52
|
+
if (!route)
|
|
53
|
+
continue;
|
|
54
|
+
const existing = map.get(route);
|
|
55
|
+
if (existing)
|
|
56
|
+
existing.push(type);
|
|
57
|
+
else
|
|
58
|
+
map.set(route, [type]);
|
|
59
|
+
}
|
|
60
|
+
return map;
|
|
61
|
+
})();
|
|
62
|
+
/**
|
|
63
|
+
* Reduce an authored href to a comparable path.
|
|
64
|
+
*
|
|
65
|
+
* Returns `""` for anything that is not an in-app path — an absolute URL to
|
|
66
|
+
* another origin, a `mailto:`, an anchor. Those can never be a listing browse
|
|
67
|
+
* route, and a caller must not filter them out.
|
|
68
|
+
*
|
|
69
|
+
* 🛑 The locale prefix is stripped. `ROUTES` values are unprefixed
|
|
70
|
+
* (`/auctions`) and `next-intl` adds `/en` at navigation time, but an admin
|
|
71
|
+
* pasting a URL out of the address bar stores the prefixed form — so a
|
|
72
|
+
* comparison against the raw value matches the seed and misses the hand-typed
|
|
73
|
+
* one, which is the half most likely to be wrong.
|
|
74
|
+
*/
|
|
75
|
+
function normalisePath(href) {
|
|
76
|
+
if (typeof href !== "string")
|
|
77
|
+
return "";
|
|
78
|
+
const trimmed = href.trim();
|
|
79
|
+
if (!trimmed || !trimmed.startsWith("/"))
|
|
80
|
+
return "";
|
|
81
|
+
// Drop query + hash: `/auctions?sort=ending` is still /auctions.
|
|
82
|
+
const path = trimmed.split(/[?#]/)[0] ?? "";
|
|
83
|
+
// `/en/auctions` → `/auctions`. Two-letter segment only, so a real route
|
|
84
|
+
// whose first segment is short (there are none today, but `/ar` would be a
|
|
85
|
+
// plausible future one) is the only thing this could over-match — and it
|
|
86
|
+
// would have to collide with a locale code to do it.
|
|
87
|
+
const delocalised = path.replace(/^\/[a-z]{2}(?=\/)/, "");
|
|
88
|
+
// Trailing slash off, but never reduce "/" to "".
|
|
89
|
+
return delocalised.length > 1 ? delocalised.replace(/\/+$/, "") : delocalised;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* `true` when `href` points at a listing type's browse page (or any page
|
|
93
|
+
* beneath it) and every type that browses there is disabled.
|
|
94
|
+
*
|
|
95
|
+
* Fails OPEN on anything it cannot classify: an unknown path, an external URL
|
|
96
|
+
* or a missing settings document all return `false`. A settings read that
|
|
97
|
+
* errored must not strip working destinations out of the homepage — the same
|
|
98
|
+
* reasoning as the suggestions endpoint's TTL cache and `src/proxy.ts`'s
|
|
99
|
+
* disabled-routes gate.
|
|
100
|
+
*/
|
|
101
|
+
export function isDisabledListingHref(href, settings) {
|
|
102
|
+
const path = normalisePath(href);
|
|
103
|
+
if (!path)
|
|
104
|
+
return false;
|
|
105
|
+
for (const [route, types] of TYPES_BY_BROWSE_ROUTE) {
|
|
106
|
+
// The browse page itself, or a detail page under it. `startsWith(route)`
|
|
107
|
+
// alone would match `/live` against `/livestream`, so the boundary is
|
|
108
|
+
// explicit.
|
|
109
|
+
if (path !== route && !path.startsWith(`${route}/`))
|
|
110
|
+
continue;
|
|
111
|
+
return types.every((type) => !isListingTypeEnabled(type, settings));
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the buyer is quoted for shipping, what the seller actually spent, and
|
|
3
|
+
* who wears the difference.
|
|
4
|
+
*
|
|
5
|
+
* Pure and synchronous — no I/O, no Firestore, no provider. Every surface that
|
|
6
|
+
* touches a shipping figure resolves through here so the quote shown at
|
|
7
|
+
* checkout, the amount charged, the order record and the reconciliation can
|
|
8
|
+
* never disagree (Root Cause #75: a rule reachable by copy-paste WILL be
|
|
9
|
+
* copied, and the copy never learns about the branch added later).
|
|
10
|
+
*
|
|
11
|
+
* ## The model
|
|
12
|
+
*
|
|
13
|
+
* 1. Shiprocket returns per-courier rates for the buyer's pincode.
|
|
14
|
+
* 2. Each rate is ROUNDED UP to the nearest ₹50 and that is what the buyer
|
|
15
|
+
* sees and is charged. The raw rate is never shown and never billed.
|
|
16
|
+
* 3. The seller ships. If their courier is dearer than the charged figure they
|
|
17
|
+
* absorb the difference; if it is cheaper the buyer is credited the
|
|
18
|
+
* difference in points, 1 point = ₹1.
|
|
19
|
+
*
|
|
20
|
+
* ## 🛑 Reconciliation measures against the CHARGED figure, not the raw rate
|
|
21
|
+
*
|
|
22
|
+
* This is the decision that makes the whole thing coherent, and it is worth
|
|
23
|
+
* stating because the obvious alternative is defensible and wrong.
|
|
24
|
+
*
|
|
25
|
+
* Measuring against the raw rate would let the platform keep the round-up
|
|
26
|
+
* (₹70 raw → ₹100 charged → a silent ₹30 margin on every parcel) while still
|
|
27
|
+
* calling the ₹50 step a "rounding". Measuring against the charged figure
|
|
28
|
+
* means the buyer is made whole for every rupee they handed over that did not
|
|
29
|
+
* become postage: ₹100 charged, shipped at ₹47, 53 points back. The round-up
|
|
30
|
+
* stops being a margin and becomes a BUFFER that absorbs ordinary courier
|
|
31
|
+
* variance before the seller has to.
|
|
32
|
+
*
|
|
33
|
+
* It also makes the rule explainable in one sentence to both sides — "you pay
|
|
34
|
+
* the rounded quote and get back whatever we did not spend", "you keep the
|
|
35
|
+
* quote and cover any overage" — and a rule a seller can predict is a rule
|
|
36
|
+
* they do not dispute.
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Shipping is always billed in whole ₹50 steps.
|
|
40
|
+
*
|
|
41
|
+
* 🛑 Not a formatting choice. A courier rate is a volatile per-parcel number
|
|
42
|
+
* (fuel, zone, weight slab, surcharge) and quoting it to the rupee invites a
|
|
43
|
+
* dispute every time the actual differs by ₹3. A coarse step makes the quote
|
|
44
|
+
* stable, makes it look like a price rather than a machine reading, and buys
|
|
45
|
+
* the headroom that keeps most orders off the reconciliation path entirely.
|
|
46
|
+
*/
|
|
47
|
+
export declare const SHIPPING_ROUND_UP_TO = 50;
|
|
48
|
+
/**
|
|
49
|
+
* Round a raw carrier rate up to the next ₹50.
|
|
50
|
+
*
|
|
51
|
+
* `Math.ceil`, so ₹70 → ₹100 and ₹100 → ₹100 — a rate exactly on a step is NOT
|
|
52
|
+
* pushed to the next one. ₹0 stays ₹0: free shipping must survive the rounding
|
|
53
|
+
* or every free-shipping order silently acquires a ₹50 charge.
|
|
54
|
+
*
|
|
55
|
+
* Negative input is clamped to 0 rather than rounded "up" toward zero, which
|
|
56
|
+
* would otherwise turn a bad provider response into a credit.
|
|
57
|
+
*/
|
|
58
|
+
export declare function roundShippingUp(rawRate: number): number;
|
|
59
|
+
/** One courier the buyer may pick, as offered at checkout. */
|
|
60
|
+
export interface ShippingQuoteOption {
|
|
61
|
+
/** Stable id from the carrier, so a pick survives a re-quote. */
|
|
62
|
+
courierId: string;
|
|
63
|
+
/** Display name — "Ekart", "Delhivery", "India Post". */
|
|
64
|
+
courierName: string;
|
|
65
|
+
/**
|
|
66
|
+
* The carrier's own rate, kept for the audit trail and NEVER rendered.
|
|
67
|
+
*
|
|
68
|
+
* 🛑 Showing both numbers would invite "why am I paying ₹100 for a ₹70
|
|
69
|
+
* courier" — a question whose honest answer (the difference comes back to
|
|
70
|
+
* you as points if it is not spent) is not something a buyer should have to
|
|
71
|
+
* reconstruct from two figures on a checkout page.
|
|
72
|
+
*/
|
|
73
|
+
rawRate: number;
|
|
74
|
+
/** What the buyer sees and is charged. `roundShippingUp(rawRate)`. */
|
|
75
|
+
chargedRate: number;
|
|
76
|
+
estimatedDays: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The option the buyer committed to, frozen onto the order.
|
|
80
|
+
*
|
|
81
|
+
* 🛑 Stored, never recomputed. Carrier rates move hourly; re-quoting at
|
|
82
|
+
* reconciliation time would compare the seller's actual spend against a price
|
|
83
|
+
* nobody was ever charged, and the buyer's points would change depending on
|
|
84
|
+
* when the job happened to run. Same reasoning as `OrderSourceContext` being
|
|
85
|
+
* written once at creation.
|
|
86
|
+
*/
|
|
87
|
+
export interface SelectedShippingQuote {
|
|
88
|
+
courierId: string;
|
|
89
|
+
courierName: string;
|
|
90
|
+
rawRate: number;
|
|
91
|
+
/** The amount actually added to the order total. */
|
|
92
|
+
chargedRate: number;
|
|
93
|
+
estimatedDays: number;
|
|
94
|
+
/** When the quote was taken, so a stale pick is recognisable. */
|
|
95
|
+
quotedAt: Date;
|
|
96
|
+
}
|
|
97
|
+
/** What the seller really shipped with, recorded when they dispatch. */
|
|
98
|
+
export interface ActualShipmentCost {
|
|
99
|
+
courierId: string;
|
|
100
|
+
courierName: string;
|
|
101
|
+
/** The seller's real postage, to the rupee. Not rounded. */
|
|
102
|
+
actualRate: number;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The outcome of comparing what was charged with what was spent.
|
|
106
|
+
*
|
|
107
|
+
* Exactly one of `sellerAbsorbs` / `buyerPoints` is non-zero, and both are
|
|
108
|
+
* non-negative — a signed "difference" field would make every consumer decide
|
|
109
|
+
* what its sign meant, and the two directions have completely different
|
|
110
|
+
* consequences (one is a cost to a seller, one is a credit to a buyer).
|
|
111
|
+
*/
|
|
112
|
+
export interface ShippingReconciliation {
|
|
113
|
+
/** Rupees the seller eats because their courier cost more than quoted. */
|
|
114
|
+
sellerAbsorbs: number;
|
|
115
|
+
/** Points credited to the buyer, 1 point = ₹1. */
|
|
116
|
+
buyerPoints: number;
|
|
117
|
+
/** True when the seller shipped with a courier the buyer did not choose. */
|
|
118
|
+
courierChanged: boolean;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Compare the charged quote against the real spend.
|
|
122
|
+
*
|
|
123
|
+
* 🛑 Works on the CHARGED figure (see the header). The worked examples from the
|
|
124
|
+
* spec, with the ₹70 Ekart quote billed at ₹100:
|
|
125
|
+
*
|
|
126
|
+
* shipped Delhivery ₹100 → sellerAbsorbs 0, buyerPoints 0
|
|
127
|
+
* shipped Delhivery ₹120 → sellerAbsorbs 20, buyerPoints 0
|
|
128
|
+
* shipped India Post ₹47 → sellerAbsorbs 0, buyerPoints 53
|
|
129
|
+
*
|
|
130
|
+
* Points are the EXACT rupee difference and are deliberately not rounded to a
|
|
131
|
+
* ₹50 step. The round-up exists to make a *quote* stable; applying it to a
|
|
132
|
+
* credit would either invent money (rounding up) or quietly keep some of the
|
|
133
|
+
* buyer's (rounding down).
|
|
134
|
+
*/
|
|
135
|
+
export declare function reconcileShipping(quote: Pick<SelectedShippingQuote, "chargedRate" | "courierId">, actual: ActualShipmentCost): ShippingReconciliation;
|
|
136
|
+
/**
|
|
137
|
+
* Turn carrier serviceability results into offerable options.
|
|
138
|
+
*
|
|
139
|
+
* 🛑 Drops any courier whose rate is not a usable number instead of charging
|
|
140
|
+
* ₹0 for it. `resolveShippingCost` already carries this lesson in its
|
|
141
|
+
* `shippingUnavailable` flag: a zero there "would ship every parcel free at
|
|
142
|
+
* our expense, silently, until somebody noticed". A malformed row from a
|
|
143
|
+
* provider is missing data, never free postage.
|
|
144
|
+
*
|
|
145
|
+
* Sorted cheapest-first, then fastest — the buyer is choosing, so the default
|
|
146
|
+
* reading order should be the one most of them want.
|
|
147
|
+
*/
|
|
148
|
+
export declare function toQuoteOptions(couriers: readonly {
|
|
149
|
+
courierId?: string;
|
|
150
|
+
name: string;
|
|
151
|
+
rate: number;
|
|
152
|
+
estimatedDays: number;
|
|
153
|
+
}[]): ShippingQuoteOption[];
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the buyer is quoted for shipping, what the seller actually spent, and
|
|
3
|
+
* who wears the difference.
|
|
4
|
+
*
|
|
5
|
+
* Pure and synchronous — no I/O, no Firestore, no provider. Every surface that
|
|
6
|
+
* touches a shipping figure resolves through here so the quote shown at
|
|
7
|
+
* checkout, the amount charged, the order record and the reconciliation can
|
|
8
|
+
* never disagree (Root Cause #75: a rule reachable by copy-paste WILL be
|
|
9
|
+
* copied, and the copy never learns about the branch added later).
|
|
10
|
+
*
|
|
11
|
+
* ## The model
|
|
12
|
+
*
|
|
13
|
+
* 1. Shiprocket returns per-courier rates for the buyer's pincode.
|
|
14
|
+
* 2. Each rate is ROUNDED UP to the nearest ₹50 and that is what the buyer
|
|
15
|
+
* sees and is charged. The raw rate is never shown and never billed.
|
|
16
|
+
* 3. The seller ships. If their courier is dearer than the charged figure they
|
|
17
|
+
* absorb the difference; if it is cheaper the buyer is credited the
|
|
18
|
+
* difference in points, 1 point = ₹1.
|
|
19
|
+
*
|
|
20
|
+
* ## 🛑 Reconciliation measures against the CHARGED figure, not the raw rate
|
|
21
|
+
*
|
|
22
|
+
* This is the decision that makes the whole thing coherent, and it is worth
|
|
23
|
+
* stating because the obvious alternative is defensible and wrong.
|
|
24
|
+
*
|
|
25
|
+
* Measuring against the raw rate would let the platform keep the round-up
|
|
26
|
+
* (₹70 raw → ₹100 charged → a silent ₹30 margin on every parcel) while still
|
|
27
|
+
* calling the ₹50 step a "rounding". Measuring against the charged figure
|
|
28
|
+
* means the buyer is made whole for every rupee they handed over that did not
|
|
29
|
+
* become postage: ₹100 charged, shipped at ₹47, 53 points back. The round-up
|
|
30
|
+
* stops being a margin and becomes a BUFFER that absorbs ordinary courier
|
|
31
|
+
* variance before the seller has to.
|
|
32
|
+
*
|
|
33
|
+
* It also makes the rule explainable in one sentence to both sides — "you pay
|
|
34
|
+
* the rounded quote and get back whatever we did not spend", "you keep the
|
|
35
|
+
* quote and cover any overage" — and a rule a seller can predict is a rule
|
|
36
|
+
* they do not dispute.
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Shipping is always billed in whole ₹50 steps.
|
|
40
|
+
*
|
|
41
|
+
* 🛑 Not a formatting choice. A courier rate is a volatile per-parcel number
|
|
42
|
+
* (fuel, zone, weight slab, surcharge) and quoting it to the rupee invites a
|
|
43
|
+
* dispute every time the actual differs by ₹3. A coarse step makes the quote
|
|
44
|
+
* stable, makes it look like a price rather than a machine reading, and buys
|
|
45
|
+
* the headroom that keeps most orders off the reconciliation path entirely.
|
|
46
|
+
*/
|
|
47
|
+
export const SHIPPING_ROUND_UP_TO = 50;
|
|
48
|
+
/**
|
|
49
|
+
* Round a raw carrier rate up to the next ₹50.
|
|
50
|
+
*
|
|
51
|
+
* `Math.ceil`, so ₹70 → ₹100 and ₹100 → ₹100 — a rate exactly on a step is NOT
|
|
52
|
+
* pushed to the next one. ₹0 stays ₹0: free shipping must survive the rounding
|
|
53
|
+
* or every free-shipping order silently acquires a ₹50 charge.
|
|
54
|
+
*
|
|
55
|
+
* Negative input is clamped to 0 rather than rounded "up" toward zero, which
|
|
56
|
+
* would otherwise turn a bad provider response into a credit.
|
|
57
|
+
*/
|
|
58
|
+
export function roundShippingUp(rawRate) {
|
|
59
|
+
if (!Number.isFinite(rawRate) || rawRate <= 0)
|
|
60
|
+
return 0;
|
|
61
|
+
return Math.ceil(rawRate / SHIPPING_ROUND_UP_TO) * SHIPPING_ROUND_UP_TO;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Compare the charged quote against the real spend.
|
|
65
|
+
*
|
|
66
|
+
* 🛑 Works on the CHARGED figure (see the header). The worked examples from the
|
|
67
|
+
* spec, with the ₹70 Ekart quote billed at ₹100:
|
|
68
|
+
*
|
|
69
|
+
* shipped Delhivery ₹100 → sellerAbsorbs 0, buyerPoints 0
|
|
70
|
+
* shipped Delhivery ₹120 → sellerAbsorbs 20, buyerPoints 0
|
|
71
|
+
* shipped India Post ₹47 → sellerAbsorbs 0, buyerPoints 53
|
|
72
|
+
*
|
|
73
|
+
* Points are the EXACT rupee difference and are deliberately not rounded to a
|
|
74
|
+
* ₹50 step. The round-up exists to make a *quote* stable; applying it to a
|
|
75
|
+
* credit would either invent money (rounding up) or quietly keep some of the
|
|
76
|
+
* buyer's (rounding down).
|
|
77
|
+
*/
|
|
78
|
+
export function reconcileShipping(quote, actual) {
|
|
79
|
+
const charged = Number.isFinite(quote.chargedRate) ? Math.max(0, quote.chargedRate) : 0;
|
|
80
|
+
const spent = Number.isFinite(actual.actualRate) ? Math.max(0, actual.actualRate) : 0;
|
|
81
|
+
const delta = roundPaise(charged - spent);
|
|
82
|
+
return {
|
|
83
|
+
sellerAbsorbs: delta < 0 ? Math.abs(delta) : 0,
|
|
84
|
+
buyerPoints: delta > 0 ? delta : 0,
|
|
85
|
+
courierChanged: quote.courierId !== actual.courierId,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Two decimal places.
|
|
90
|
+
*
|
|
91
|
+
* Money here is decimal rupees (the paise→rupees migration, Root Cause #31), so
|
|
92
|
+
* a subtraction can leave a float artefact — `100 - 47.1` is `52.900000000000006`,
|
|
93
|
+
* and that would become a points credit with eleven decimal places on a user's
|
|
94
|
+
* balance.
|
|
95
|
+
*/
|
|
96
|
+
function roundPaise(value) {
|
|
97
|
+
return Math.round(value * 100) / 100;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Turn carrier serviceability results into offerable options.
|
|
101
|
+
*
|
|
102
|
+
* 🛑 Drops any courier whose rate is not a usable number instead of charging
|
|
103
|
+
* ₹0 for it. `resolveShippingCost` already carries this lesson in its
|
|
104
|
+
* `shippingUnavailable` flag: a zero there "would ship every parcel free at
|
|
105
|
+
* our expense, silently, until somebody noticed". A malformed row from a
|
|
106
|
+
* provider is missing data, never free postage.
|
|
107
|
+
*
|
|
108
|
+
* Sorted cheapest-first, then fastest — the buyer is choosing, so the default
|
|
109
|
+
* reading order should be the one most of them want.
|
|
110
|
+
*/
|
|
111
|
+
export function toQuoteOptions(couriers) {
|
|
112
|
+
return couriers
|
|
113
|
+
.filter((c) => Number.isFinite(c.rate) && c.rate > 0 && Boolean(c.name))
|
|
114
|
+
.map((c) => ({
|
|
115
|
+
// Providers do not all return an id. Falling back to the name keeps the
|
|
116
|
+
// pick round-trippable; it is only ever compared against itself.
|
|
117
|
+
courierId: c.courierId ?? c.name,
|
|
118
|
+
courierName: c.name,
|
|
119
|
+
rawRate: c.rate,
|
|
120
|
+
chargedRate: roundShippingUp(c.rate),
|
|
121
|
+
estimatedDays: c.estimatedDays,
|
|
122
|
+
}))
|
|
123
|
+
.sort((a, b) => a.chargedRate - b.chargedRate || a.estimatedDays - b.estimatedDays);
|
|
124
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -534,6 +534,7 @@ export { SELLER_LISTING_TABS, type SellerListingTabId, } from "./features/produc
|
|
|
534
534
|
export { ALL_TAB, EMPTY_TAB, ADMIN_PRODUCT_STATUS_TABS, ADMIN_PRODUCT_LISTING_TYPE_TABS, ADMIN_BLOG_STATUS_TABS, ADMIN_USER_STATUS_TABS, ADMIN_USER_ROLE_TABS, ADMIN_STORE_STATUS_TABS, ADMIN_PAYOUT_STATUS_TABS, ADMIN_ORDER_STATUS_TABS, ADMIN_REVIEW_STATUS_TABS, ADMIN_REVIEW_RATING_TABS, ADMIN_BID_STATUS_TABS, ADMIN_CONTACT_STATUS_TABS, ADMIN_NEWSLETTER_STATUS_TABS, ADMIN_EVENT_ENTRY_STATUS_TABS, ADMIN_EVENT_STATUS_TABS, ADMIN_CART_OWNERSHIP_TABS, ADMIN_COUPON_TYPE_TABS, SELLER_PRODUCT_STATUS_TABS, SELLER_AUCTION_STATUS_TABS, SELLER_ORDER_STATUS_TABS, SELLER_OFFER_STATUS_TABS, SELLER_BID_STATUS_TABS, } from "./features/admin/constants/filter-tabs";
|
|
535
535
|
export { PRODUCT_FIELDS, PRODUCT_STATUS_TRANSITIONS, ORDER_FIELDS, REVIEW_FIELDS, BID_FIELDS, AD_FIELDS, EVENT_FIELDS, EVENT_ENTRY_FIELDS, PAYOUT_FIELDS, STORE_FIELDS, CATEGORY_FIELDS, BLOG_FIELDS, USER_FIELDS, ADDRESS_FIELDS, BRAND_FIELDS, CART_FIELDS, WISHLIST_FIELDS, HISTORY_FIELDS, NOTIFICATION_FIELDS, SESSION_FIELDS, COUPON_USAGE_FIELDS, SCAMMER_FIELDS, SUPPORT_TICKET_FIELDS, CAROUSEL_FIELDS, COUPON_FIELDS, FAQ_FIELDS, HOMEPAGE_SECTION_FIELDS, SITE_SETTINGS_FIELDS, COMMON_FIELDS, OAUTH_STATE_VALUES, SCHEMA_DEFAULTS, } from "./constants/field-names";
|
|
536
536
|
export { CART_LANE, CART_LANE_PRIORITY, CART_LANE_LABELS, laneOf, activeLane, laneItems, laneCounts, isLaneCheckoutable, laneBlockReason, isLockedLane, canAddNewItems, type CartLane, type LaneAssignable, } from "./_internal/shared/checkout/lanes";
|
|
537
|
+
export { roundShippingUp, toQuoteOptions, reconcileShipping, SHIPPING_ROUND_UP_TO, type ShippingQuoteOption, type SelectedShippingQuote, type ActualShipmentCost, type ShippingReconciliation, } from "./_internal/shared/shipping/quote";
|
|
537
538
|
export { getCartLineMembers, isMultiMemberLine, } from "./_internal/shared/checkout/line-members";
|
|
538
539
|
export { newsletterSubscribeSchema, quickCreateTaxonomySchema, payoutMarkPaidSchema, } from "./features/admin/schemas/small-forms";
|
|
539
540
|
export { raffleEntrySchema } from "./features/contact/schemas/inquiry-forms";
|
package/dist/client.js
CHANGED
|
@@ -546,6 +546,14 @@ export { PRODUCT_FIELDS, PRODUCT_STATUS_TRANSITIONS, ORDER_FIELDS, REVIEW_FIELDS
|
|
|
546
546
|
// Checkout lanes — the derived auction > offer > standard partition of the
|
|
547
547
|
// cart, and the priority rule that decides which one may be checked out.
|
|
548
548
|
export { CART_LANE, CART_LANE_PRIORITY, CART_LANE_LABELS, laneOf, activeLane, laneItems, laneCounts, isLaneCheckoutable, laneBlockReason, isLockedLane, canAddNewItems, } from "./_internal/shared/checkout/lanes.js";
|
|
549
|
+
/*
|
|
550
|
+
* Shipping quote model. Public because the CONSUMER owns the route that asks
|
|
551
|
+
* the carrier for rates (`/api/shipping/quote`) and the checkout UI that
|
|
552
|
+
* renders them — the rounding rule has to be reachable from both, and it must
|
|
553
|
+
* be the same rule the server-side reconciliation uses, or the buyer is
|
|
554
|
+
* credited against a figure they were never charged.
|
|
555
|
+
*/
|
|
556
|
+
export { roundShippingUp, toQuoteOptions, reconcileShipping, SHIPPING_ROUND_UP_TO, } from "./_internal/shared/shipping/quote.js";
|
|
549
557
|
/*
|
|
550
558
|
* The cart page needs these to tell a multi-member line from an ordinary one: a
|
|
551
559
|
* group/bundle line's `productId` is a group or category id, not a product id,
|
|
@@ -22,6 +22,21 @@ export declare const BID_ERROR_CODES: {
|
|
|
22
22
|
/** The auction's clock ran out mid-checkout, so it settled to a real bidder. */
|
|
23
23
|
readonly AUCTION_ENDED_BEFORE_CHECKOUT: "BID_AUCTION_ENDED_BEFORE_CHECKOUT";
|
|
24
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* Checkout refusals that are BUSINESS rules rather than malformed requests.
|
|
27
|
+
*
|
|
28
|
+
* 🛑 Registered here, not left as a bare string at the throw site. The code is
|
|
29
|
+
* what `toUserMessage` resolves through `errors.codes.*`, and an unregistered
|
|
30
|
+
* one terminates at `errors.codes.UNKNOWN` — so a buyer refused for a reason
|
|
31
|
+
* the site can explain in one sentence would instead read "Something went
|
|
32
|
+
* wrong. Please try again." and retry the same cart forever.
|
|
33
|
+
* `audit-error-display-i18n` holds both ends: an enum value with no
|
|
34
|
+
* `messages/en.json` key fails, and so does a key with no enum value.
|
|
35
|
+
*/
|
|
36
|
+
export declare const CHECKOUT_ERROR_CODES: {
|
|
37
|
+
/** Cart total exceeds `siteSettings.payment.codMaxOrderValue`. */
|
|
38
|
+
readonly COD_LIMIT_EXCEEDED: "COD_LIMIT_EXCEEDED";
|
|
39
|
+
};
|
|
25
40
|
export declare const OFFER_ERROR_CODES: {
|
|
26
41
|
readonly DISABLED: "OFFER_DISABLED";
|
|
27
42
|
readonly PRICE_TOO_LOW: "OFFER_PRICE_TOO_LOW";
|
|
@@ -22,6 +22,21 @@ export const BID_ERROR_CODES = {
|
|
|
22
22
|
/** The auction's clock ran out mid-checkout, so it settled to a real bidder. */
|
|
23
23
|
AUCTION_ENDED_BEFORE_CHECKOUT: "BID_AUCTION_ENDED_BEFORE_CHECKOUT",
|
|
24
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* Checkout refusals that are BUSINESS rules rather than malformed requests.
|
|
27
|
+
*
|
|
28
|
+
* 🛑 Registered here, not left as a bare string at the throw site. The code is
|
|
29
|
+
* what `toUserMessage` resolves through `errors.codes.*`, and an unregistered
|
|
30
|
+
* one terminates at `errors.codes.UNKNOWN` — so a buyer refused for a reason
|
|
31
|
+
* the site can explain in one sentence would instead read "Something went
|
|
32
|
+
* wrong. Please try again." and retry the same cart forever.
|
|
33
|
+
* `audit-error-display-i18n` holds both ends: an enum value with no
|
|
34
|
+
* `messages/en.json` key fails, and so does a key with no enum value.
|
|
35
|
+
*/
|
|
36
|
+
export const CHECKOUT_ERROR_CODES = {
|
|
37
|
+
/** Cart total exceeds `siteSettings.payment.codMaxOrderValue`. */
|
|
38
|
+
COD_LIMIT_EXCEEDED: "COD_LIMIT_EXCEEDED",
|
|
39
|
+
};
|
|
25
40
|
export const OFFER_ERROR_CODES = {
|
|
26
41
|
DISABLED: "OFFER_DISABLED",
|
|
27
42
|
PRICE_TOO_LOW: "OFFER_PRICE_TOO_LOW",
|
package/dist/errors/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { AppError } from "./base-error";
|
|
2
|
-
export { ERROR_CODES, BID_ERROR_CODES, OFFER_ERROR_CODES } from "./error-codes";
|
|
2
|
+
export { ERROR_CODES, BID_ERROR_CODES, OFFER_ERROR_CODES, CHECKOUT_ERROR_CODES } from "./error-codes";
|
|
3
3
|
export type { ErrorCode } from "./error-codes";
|
|
4
4
|
export { ERROR_MESSAGES } from "./messages";
|
|
5
5
|
export { ApiError } from "./api-error";
|
package/dist/errors/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { AppError } from "./base-error.js";
|
|
2
|
-
export { ERROR_CODES, BID_ERROR_CODES, OFFER_ERROR_CODES } from "./error-codes.js";
|
|
2
|
+
export { ERROR_CODES, BID_ERROR_CODES, OFFER_ERROR_CODES, CHECKOUT_ERROR_CODES } from "./error-codes.js";
|
|
3
3
|
export { ERROR_MESSAGES } from "./messages.js";
|
|
4
4
|
// Note: appkit/src/errors has two ERROR_MESSAGE patterns:
|
|
5
5
|
// - ERROR_CODES.AUTH_001 → "Invalid email or password" (code-based)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx,
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { sortBy, sieveFilter, SIEVE_OP } from "../../../utils/sieve-builder.js";
|
|
4
4
|
import { ACCOUNT_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
5
5
|
import { ROUTES } from "../../../constants/index.js";
|
|
@@ -9,6 +9,7 @@ import { buildBidDetailFields, bidStatusBadge } from "../../auctions/components/
|
|
|
9
9
|
import { FieldSelect } from "../../../ui/forms/index.js";
|
|
10
10
|
import { AuctionBidsTable } from "../../auctions/components/AuctionBidsTable.js";
|
|
11
11
|
import { DataListingView } from "../../admin/components/DataListingView.js";
|
|
12
|
+
import { useListingTypeFlags } from "../../../react/hooks/useListingTypeFlags.js";
|
|
12
13
|
const SORT_OPTIONS = [
|
|
13
14
|
{ value: sortBy("bidDate", "DESC"), label: "Newest first" },
|
|
14
15
|
{ value: "bidDate", label: "Oldest first" },
|
|
@@ -28,6 +29,7 @@ export function UserBidsView() {
|
|
|
28
29
|
// not an affordance: it is not row-scoped, and it only shows when there are
|
|
29
30
|
// no bids at all.
|
|
30
31
|
const [detail, setDetail] = useState(null);
|
|
32
|
+
const auctionsEnabled = useListingTypeFlags().auction;
|
|
31
33
|
const config = {
|
|
32
34
|
portal: "user",
|
|
33
35
|
title: "My Bids",
|
|
@@ -52,7 +54,7 @@ export function UserBidsView() {
|
|
|
52
54
|
if (isLoading) {
|
|
53
55
|
return (_jsx(Stack, { gap: "md", children: Array.from({ length: 3 }).map((_, i) => (_jsx(Div, { className: "h-16 animate-pulse border border-[var(--appkit-color-border)]", rounded: "xl" }, i))) }));
|
|
54
56
|
}
|
|
55
|
-
return (_jsx(AuctionBidsTable, { bids: rows, portal: "buyer", onRowClick: (bid) => setDetail(bid), totalPages: 1, currentPage: 1, onPageChange: () => { }, emptyLabel: _jsxs(Span, { children: ["You haven't placed any bids yet.", " ", _jsx(TextLink, { href: String(ROUTES.PUBLIC.AUCTIONS), children: "Browse auctions" })] }) }));
|
|
57
|
+
return (_jsx(AuctionBidsTable, { bids: rows, portal: "buyer", onRowClick: (bid) => setDetail(bid), totalPages: 1, currentPage: 1, onPageChange: () => { }, emptyLabel: _jsxs(Span, { children: ["You haven't placed any bids yet.", auctionsEnabled && (_jsxs(_Fragment, { children: [" ", _jsx(TextLink, { href: String(ROUTES.PUBLIC.AUCTIONS), children: "Browse auctions" })] }))] }) }));
|
|
56
58
|
},
|
|
57
59
|
};
|
|
58
60
|
const badge = detail ? bidStatusBadge(detail.status) : null;
|
|
@@ -18,6 +18,12 @@ import { ThemeManagerView, } from "../../site-settings/components/ThemeManagerVi
|
|
|
18
18
|
import { DEFAULT_LIGHT_THEME, DEFAULT_DARK_THEME } from "../../../tokens/themes/index.js";
|
|
19
19
|
import { DEFAULT_AUCTION_BID_INCREMENT_TIERS, } from "../../../_internal/shared/features/auctions/config.js";
|
|
20
20
|
import { validateGstEnablement } from "../../../_internal/shared/features/site-settings/gst-registration.js";
|
|
21
|
+
/*
|
|
22
|
+
* 🛑 The SAME constant the checkout page and the server-side gate read. An
|
|
23
|
+
* admin screen that defaulted its own input to a different number would show
|
|
24
|
+
* a cap that is not the cap being enforced until somebody pressed Save.
|
|
25
|
+
*/
|
|
26
|
+
import { COD_MAX_ORDER_VALUE_FALLBACK } from "../schemas/firestore.js";
|
|
21
27
|
import { siteSettingsFormSchema } from "../schemas/site-settings-form.js";
|
|
22
28
|
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary.js";
|
|
23
29
|
import { SectionForm } from "../../shell/index.js";
|
|
@@ -279,6 +285,7 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
279
285
|
const [upiManualEnabled, setUpiManualEnabled] = React.useState(true);
|
|
280
286
|
const [codEnabled, setCodEnabled] = React.useState(true);
|
|
281
287
|
const [otpCheckoutThreshold, setOtpCheckoutThreshold] = React.useState(5000);
|
|
288
|
+
const [codMaxOrderValue, setCodMaxOrderValue] = React.useState(COD_MAX_ORDER_VALUE_FALLBACK);
|
|
282
289
|
// Per-listing-type / per-category-type visibility flags.
|
|
283
290
|
const [listingTypeFlags, setListingTypeFlags] = React.useState({});
|
|
284
291
|
const [categoryTypeFlags, setCategoryTypeFlags] = React.useState({});
|
|
@@ -472,6 +479,7 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
472
479
|
setUpiManualEnabled(s.payment?.upiManualEnabled ?? true);
|
|
473
480
|
setCodEnabled(s.payment?.codEnabled ?? true);
|
|
474
481
|
setOtpCheckoutThreshold(s.payment?.otpCheckoutThreshold ?? 5000);
|
|
482
|
+
setCodMaxOrderValue(s.payment?.codMaxOrderValue ?? COD_MAX_ORDER_VALUE_FALLBACK);
|
|
475
483
|
setListingTypeFlags(s.listings?.listingTypes ?? {});
|
|
476
484
|
setCategoryTypeFlags(s.listings?.categoryTypes ?? {});
|
|
477
485
|
setNotifEmailTypes(s.notificationChannels?.email?.types ?? []);
|
|
@@ -667,7 +675,7 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
667
675
|
},
|
|
668
676
|
integrations: { googleAnalyticsId: gaMeasurementId, facebookPixelId: fbPixelId, gtmContainerId, clarityProjectId },
|
|
669
677
|
shipping: { freeShippingThreshold, defaultCarrier, maxDeliveryRadius },
|
|
670
|
-
payment: { razorpayEnabled, phonepeEnabled, upiManualEnabled, codEnabled, otpCheckoutThreshold },
|
|
678
|
+
payment: { razorpayEnabled, phonepeEnabled, upiManualEnabled, codEnabled, codMaxOrderValue, otpCheckoutThreshold },
|
|
671
679
|
auctionConfig: { bidIncrementTiers: bidIncrementTiers, autoExtendWindowMinutes: autoExtendWindow, settlementGracePeriodHours: settlementGrace },
|
|
672
680
|
platformLimits: { maxProductsPerStore, maxCustomFieldsPerProduct: maxCustomFields, orderCancellationWindowHours: orderCancelWindow },
|
|
673
681
|
legalPages: { terms: termsHtml, privacy: privacyHtml, refundPolicy: refundHtml, shipping: shippingPolicyHtml, cookies: cookieHtml, ethics: ethicsHtml, codeOfConduct: conductHtml },
|
|
@@ -791,7 +799,15 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
791
799
|
// The plugin's own display label, not the raw key — the
|
|
792
800
|
// literal version rendered "pre-order" and
|
|
793
801
|
// "digital-code" at an admin.
|
|
794
|
-
label: pluginFor(key).typeLabel, checked: listingTypeFlags[key] ?? true, onChange: (v) => setListingTypeFlags((prev) => ({ ...prev, [key]: v })) }, key))) })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Category types" }), _jsx(Grid, { cols: 2, gap: "sm", children: CATEGORY_TYPE_KEYS.map((key) => (_jsx(Toggle, { label: key, checked: categoryTypeFlags[key] ?? true, onChange: (v) => setCategoryTypeFlags((prev) => ({ ...prev, [key]: v })) }, key))) })] })] }) }), _jsx(TabsContent, { value: "integrations", children: _jsxs(SettingsTabForm, { id: "integrations", label: "Integrations", children: [_jsx(Text, { size: "xs", color: "muted", children: "Keys are masked in transit and stored encrypted. Click Reveal to view." }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Razorpay" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Razorpay Key ID", value: razorpayKeyId, onChange: setRazorpayKeyId, placeholder: "rzp_test_\u2026 / rzp_live_\u2026" }), _jsx(MaskedInput, { label: "Razorpay Key Secret", value: razorpayKeySecret, onChange: setRazorpayKeySecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(MaskedInput, { label: "Razorpay Webhook Secret", value: razorpayWebhookSecret, onChange: setRazorpayWebhookSecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "PhonePe" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "PhonePe Client ID", value: phonepeClientId, onChange: setPhonepeClientId, placeholder: "MERCHANTID_\u2026" }), _jsx(MaskedInput, { label: "PhonePe Client Secret", value: phonepeClientSecret, onChange: setPhonepeClientSecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(Input, { label: "PhonePe Client Version", value: phonepeClientVersion, onChange: (e) => setPhonepeClientVersion(e.target.value), placeholder: "1" }), _jsx(Select, { label: "PhonePe Environment", options: PHONEPE_ENVIRONMENT_OPTIONS, value: phonepeEnvironment, onValueChange: setPhonepeEnvironment }), _jsx(MaskedInput, { label: "PhonePe Webhook Username", value: phonepeWebhookUsername, onChange: setPhonepeWebhookUsername, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(MaskedInput, { label: "PhonePe Webhook Password", value: phonepeWebhookPassword, onChange: setPhonepeWebhookPassword, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "SMTP / Email" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "SMTP host", value: smtpHost, onChange: (e) => setSmtpHost(e.target.value), placeholder: "smtp.sendgrid.net" }), _jsx(Input, { label: "SMTP port", value: smtpPort, onChange: (e) => setSmtpPort(e.target.value), type: "number", placeholder: "587" }), _jsx(Input, { label: "SMTP user", value: smtpUser, onChange: (e) => setSmtpUser(e.target.value), placeholder: "apikey" })] }), _jsx(Input, { label: "From address", value: smtpFrom, onChange: (e) => setSmtpFrom(e.target.value), placeholder: "noreply@letitrip.in", type: "email" }), _jsx(Text, { size: "xs", color: "muted", children: "Transactional email is delivered through Resend \u2014 set the Resend API key below. These SMTP fields are stored but not currently used for delivery." })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Google Places (store reviews)" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Google Maps API key", value: googleMapsApiKey, onChange: setGoogleMapsApiKey, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(Input, { label: "Default Google Place ID", value: googlePlaceId, onChange: (e) => setGooglePlaceId(e.target.value), placeholder: "ChIJ\u2026" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Analytics & Tracking" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Google Analytics ID", value: gaMeasurementId, onChange: (e) => setGaMeasurementId(e.target.value), placeholder: "G-XXXXXXXXXX" }), _jsx(Input, { label: "Facebook Pixel ID", value: fbPixelId, onChange: (e) => setFbPixelId(e.target.value), placeholder: "XXXXXXXXXXXXXXXX" }), _jsx(Input, { label: "GTM Container ID", value: gtmContainerId, onChange: (e) => setGtmContainerId(e.target.value), placeholder: "GTM-XXXXXXX" }), _jsx(Input, { label: "Microsoft Clarity Project ID", value: clarityProjectId, onChange: (e) => setClarityProjectId(e.target.value), placeholder: "abcdefghij", helperText: "Heatmaps and session replay. Recording is strictly masked \u2014 no text or input values are captured. Leave blank to keep Clarity off." })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Meta (Instagram & Facebook Social Feed)" }), _jsx(Text, { size: "xs", color: "muted", children: "Used by the Social Feed section to fetch Instagram and Facebook posts via Meta Graph API v19." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Page Access Token", value: metaPageAccessToken, onChange: setMetaPageAccessToken, placeholder: "EAAxxxxxxx\u2026" }), _jsx(Input, { label: "Page ID (or handle)", value: metaPageId, onChange: (e) => setMetaPageId(e.target.value), placeholder: "letitrip" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "TikTok for Developers (Social Feed)" }), _jsx(Text, { size: "xs", color: "muted", children: "Client credentials + long-lived access token from TikTok for Developers. Used to list your account's public videos." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Client Key", value: tiktokClientKey, onChange: setTiktokClientKey, placeholder: "aw\u2026" }), _jsx(MaskedInput, { label: "Client Secret", value: tiktokClientSecret, onChange: setTiktokClientSecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(MaskedInput, { label: "Access Token (long-lived)", value: tiktokAccessToken, onChange: setTiktokAccessToken, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "DeviantArt OAuth2 (Social Feed)" }), _jsx(Text, { size: "xs", color: "muted", children: "Client credentials for DeviantArt gallery fetching (client-credentials OAuth2 flow \u2014 no user login required)." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Client ID", value: deviantartClientId, onChange: setDeviantartClientId, placeholder: "1234" }), _jsx(MaskedInput, { label: "Client Secret", value: deviantartClientSecret, onChange: setDeviantartClientSecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] })] })] }) }), _jsx(TabsContent, { value: "shipping", children: _jsxs(SettingsTabForm, { id: "shipping", label: "Shipping", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Payment methods" }), _jsx(Div, { id: "setting-razorpay-enabled", children: _jsx(Toggle, { label: "Razorpay (online card/UPI/netbanking) enabled \u2014 needs Key ID + Key Secret under Credentials", checked: razorpayEnabled, onChange: setRazorpayEnabled }) }), _jsx(Div, { id: "setting-phonepe-enabled", children: _jsx(Toggle, { label: "PhonePe (online card/UPI) enabled \u2014 ignored while Razorpay is on; only one online gateway runs at a time", checked: phonepeEnabled, onChange: setPhonepeEnabled }) }), _jsx(Div, { id: "setting-manual-payment-enabled", children: _jsx(Toggle, { label: "Manual UPI/bank transfer enabled", checked: upiManualEnabled, onChange: setUpiManualEnabled }) }), _jsx(Div, { id: "setting-cod-enabled", children: _jsx(Toggle, { label: "Cash on delivery (COD) enabled", checked: codEnabled, onChange: setCodEnabled }) }), _jsx(Input, { label: "Free shipping threshold (\u20B9)", value: String(freeShippingThreshold), onChange: (e) => setFreeShippingThreshold(parseInt(e.target.value) || 0), type: "number", min: 0, helperText: "Orders above this amount get free shipping." }), _jsx(Select, { label: "Default carrier", options: CARRIER_OPTIONS, value: defaultCarrier, onValueChange: setDefaultCarrier }), _jsx(Input, { label: "Max delivery radius (km, 0 = no limit)", value: String(maxDeliveryRadius), onChange: (e) => setMaxDeliveryRadius(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "High-value checkout OTP threshold (\u20B9)", helperText: "Carts at or above this subtotal require an extra OTP verification step before payment (0 = disabled).", value: String(otpCheckoutThreshold), onChange: (e) => setOtpCheckoutThreshold(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Text, { size: "sm", weight: "medium", color: "muted", className: "pt-[var(--appkit-space-2)]", children: "Seller shipping charge defaults" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Seller shipping fixed (\u20B9)", helperText: "Flat shipping charge collected on the seller's behalf.", value: String(sellerShippingFixed), onChange: (e) => setSellerShippingFixed(parseFloat(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Platform shipping (%)", helperText: "Platform's own shipping markup, as a % of order value.", value: String(platformShippingPercent), onChange: (e) => setPlatformShippingPercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1 }), _jsx(Input, { label: "Platform shipping minimum (\u20B9)", helperText: "Floor for the platform shipping markup.", value: String(platformShippingFixedMin), onChange: (e) => setPlatformShippingFixedMin(parseFloat(e.target.value) || 0), type: "number", min: 0 })] })] }) }), _jsx(TabsContent, { value: "auction", children: _jsxs(SettingsTabForm, { id: "auction", label: "Auction", children: [_jsxs(Stack, { gap: "sm", rounded: "lg", border: "default", padding: "sm", children: [_jsx(Text, { size: "xs", weight: "medium", color: "muted", children: "Bid increment tiers" }), _jsx(Text, { size: "xs", color: "muted", children: "The minimum jump a new bid must clear, based on the current bid amount. A seller's per-listing override can require more than the matching tier, but can never undercut it." }), bidIncrementTiers.map((tier, i) => {
|
|
802
|
+
label: pluginFor(key).typeLabel, checked: listingTypeFlags[key] ?? true, onChange: (v) => setListingTypeFlags((prev) => ({ ...prev, [key]: v })) }, key))) })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Category types" }), _jsx(Grid, { cols: 2, gap: "sm", children: CATEGORY_TYPE_KEYS.map((key) => (_jsx(Toggle, { label: key, checked: categoryTypeFlags[key] ?? true, onChange: (v) => setCategoryTypeFlags((prev) => ({ ...prev, [key]: v })) }, key))) })] })] }) }), _jsx(TabsContent, { value: "integrations", children: _jsxs(SettingsTabForm, { id: "integrations", label: "Integrations", children: [_jsx(Text, { size: "xs", color: "muted", children: "Keys are masked in transit and stored encrypted. Click Reveal to view." }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Razorpay" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Razorpay Key ID", value: razorpayKeyId, onChange: setRazorpayKeyId, placeholder: "rzp_test_\u2026 / rzp_live_\u2026" }), _jsx(MaskedInput, { label: "Razorpay Key Secret", value: razorpayKeySecret, onChange: setRazorpayKeySecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(MaskedInput, { label: "Razorpay Webhook Secret", value: razorpayWebhookSecret, onChange: setRazorpayWebhookSecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "PhonePe" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "PhonePe Client ID", value: phonepeClientId, onChange: setPhonepeClientId, placeholder: "MERCHANTID_\u2026" }), _jsx(MaskedInput, { label: "PhonePe Client Secret", value: phonepeClientSecret, onChange: setPhonepeClientSecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(Input, { label: "PhonePe Client Version", value: phonepeClientVersion, onChange: (e) => setPhonepeClientVersion(e.target.value), placeholder: "1" }), _jsx(Select, { label: "PhonePe Environment", options: PHONEPE_ENVIRONMENT_OPTIONS, value: phonepeEnvironment, onValueChange: setPhonepeEnvironment }), _jsx(MaskedInput, { label: "PhonePe Webhook Username", value: phonepeWebhookUsername, onChange: setPhonepeWebhookUsername, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(MaskedInput, { label: "PhonePe Webhook Password", value: phonepeWebhookPassword, onChange: setPhonepeWebhookPassword, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "SMTP / Email" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "SMTP host", value: smtpHost, onChange: (e) => setSmtpHost(e.target.value), placeholder: "smtp.sendgrid.net" }), _jsx(Input, { label: "SMTP port", value: smtpPort, onChange: (e) => setSmtpPort(e.target.value), type: "number", placeholder: "587" }), _jsx(Input, { label: "SMTP user", value: smtpUser, onChange: (e) => setSmtpUser(e.target.value), placeholder: "apikey" })] }), _jsx(Input, { label: "From address", value: smtpFrom, onChange: (e) => setSmtpFrom(e.target.value), placeholder: "noreply@letitrip.in", type: "email" }), _jsx(Text, { size: "xs", color: "muted", children: "Transactional email is delivered through Resend \u2014 set the Resend API key below. These SMTP fields are stored but not currently used for delivery." })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Google Places (store reviews)" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Google Maps API key", value: googleMapsApiKey, onChange: setGoogleMapsApiKey, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(Input, { label: "Default Google Place ID", value: googlePlaceId, onChange: (e) => setGooglePlaceId(e.target.value), placeholder: "ChIJ\u2026" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Analytics & Tracking" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Google Analytics ID", value: gaMeasurementId, onChange: (e) => setGaMeasurementId(e.target.value), placeholder: "G-XXXXXXXXXX" }), _jsx(Input, { label: "Facebook Pixel ID", value: fbPixelId, onChange: (e) => setFbPixelId(e.target.value), placeholder: "XXXXXXXXXXXXXXXX" }), _jsx(Input, { label: "GTM Container ID", value: gtmContainerId, onChange: (e) => setGtmContainerId(e.target.value), placeholder: "GTM-XXXXXXX" }), _jsx(Input, { label: "Microsoft Clarity Project ID", value: clarityProjectId, onChange: (e) => setClarityProjectId(e.target.value), placeholder: "abcdefghij", helperText: "Heatmaps and session replay. Recording is strictly masked \u2014 no text or input values are captured. Leave blank to keep Clarity off." })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Meta (Instagram & Facebook Social Feed)" }), _jsx(Text, { size: "xs", color: "muted", children: "Used by the Social Feed section to fetch Instagram and Facebook posts via Meta Graph API v19." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Page Access Token", value: metaPageAccessToken, onChange: setMetaPageAccessToken, placeholder: "EAAxxxxxxx\u2026" }), _jsx(Input, { label: "Page ID (or handle)", value: metaPageId, onChange: (e) => setMetaPageId(e.target.value), placeholder: "letitrip" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "TikTok for Developers (Social Feed)" }), _jsx(Text, { size: "xs", color: "muted", children: "Client credentials + long-lived access token from TikTok for Developers. Used to list your account's public videos." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Client Key", value: tiktokClientKey, onChange: setTiktokClientKey, placeholder: "aw\u2026" }), _jsx(MaskedInput, { label: "Client Secret", value: tiktokClientSecret, onChange: setTiktokClientSecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(MaskedInput, { label: "Access Token (long-lived)", value: tiktokAccessToken, onChange: setTiktokAccessToken, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "DeviantArt OAuth2 (Social Feed)" }), _jsx(Text, { size: "xs", color: "muted", children: "Client credentials for DeviantArt gallery fetching (client-credentials OAuth2 flow \u2014 no user login required)." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Client ID", value: deviantartClientId, onChange: setDeviantartClientId, placeholder: "1234" }), _jsx(MaskedInput, { label: "Client Secret", value: deviantartClientSecret, onChange: setDeviantartClientSecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] })] })] }) }), _jsx(TabsContent, { value: "shipping", children: _jsxs(SettingsTabForm, { id: "shipping", label: "Shipping", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Payment methods" }), _jsx(Div, { id: "setting-razorpay-enabled", children: _jsx(Toggle, { label: "Razorpay (online card/UPI/netbanking) enabled \u2014 needs Key ID + Key Secret under Credentials", checked: razorpayEnabled, onChange: setRazorpayEnabled }) }), _jsx(Div, { id: "setting-phonepe-enabled", children: _jsx(Toggle, { label: "PhonePe (online card/UPI) enabled \u2014 ignored while Razorpay is on; only one online gateway runs at a time", checked: phonepeEnabled, onChange: setPhonepeEnabled }) }), _jsx(Div, { id: "setting-manual-payment-enabled", children: _jsx(Toggle, { label: "Manual UPI/bank transfer enabled", checked: upiManualEnabled, onChange: setUpiManualEnabled }) }), _jsx(Div, { id: "setting-cod-enabled", children: _jsx(Toggle, { label: "Cash on delivery (COD) enabled", checked: codEnabled, onChange: setCodEnabled }) }), _jsx(Input, { label: "Free shipping threshold (\u20B9)", value: String(freeShippingThreshold), onChange: (e) => setFreeShippingThreshold(parseInt(e.target.value) || 0), type: "number", min: 0, helperText: "Orders above this amount get free shipping." }), _jsx(Select, { label: "Default carrier", options: CARRIER_OPTIONS, value: defaultCarrier, onValueChange: setDefaultCarrier }), _jsx(Input, { label: "Max delivery radius (km, 0 = no limit)", value: String(maxDeliveryRadius), onChange: (e) => setMaxDeliveryRadius(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "COD maximum order value (\u20B9)",
|
|
803
|
+
/*
|
|
804
|
+
🛑 A MAXIMUM, sitting directly above a THRESHOLD that is a
|
|
805
|
+
minimum. The helper text says which way each one runs,
|
|
806
|
+
because the two fields look identical and mean opposite
|
|
807
|
+
things — and getting it backwards here either refuses every
|
|
808
|
+
COD order or refuses none.
|
|
809
|
+
*/
|
|
810
|
+
helperText: "Carts ABOVE this subtotal cannot pay by cash on delivery; the option is shown disabled with the reason. 0 = COD off for every cart.", value: String(codMaxOrderValue), onChange: (e) => setCodMaxOrderValue(parseInt(e.target.value) || 0), type: "number", min: 0, disabled: !codEnabled }), _jsx(Input, { label: "High-value checkout OTP threshold (\u20B9)", helperText: "Carts at or above this subtotal require an extra OTP verification step before payment (0 = disabled).", value: String(otpCheckoutThreshold), onChange: (e) => setOtpCheckoutThreshold(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Text, { size: "sm", weight: "medium", color: "muted", className: "pt-[var(--appkit-space-2)]", children: "Seller shipping charge defaults" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Seller shipping fixed (\u20B9)", helperText: "Flat shipping charge collected on the seller's behalf.", value: String(sellerShippingFixed), onChange: (e) => setSellerShippingFixed(parseFloat(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Platform shipping (%)", helperText: "Platform's own shipping markup, as a % of order value.", value: String(platformShippingPercent), onChange: (e) => setPlatformShippingPercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1 }), _jsx(Input, { label: "Platform shipping minimum (\u20B9)", helperText: "Floor for the platform shipping markup.", value: String(platformShippingFixedMin), onChange: (e) => setPlatformShippingFixedMin(parseFloat(e.target.value) || 0), type: "number", min: 0 })] })] }) }), _jsx(TabsContent, { value: "auction", children: _jsxs(SettingsTabForm, { id: "auction", label: "Auction", children: [_jsxs(Stack, { gap: "sm", rounded: "lg", border: "default", padding: "sm", children: [_jsx(Text, { size: "xs", weight: "medium", color: "muted", children: "Bid increment tiers" }), _jsx(Text, { size: "xs", color: "muted", children: "The minimum jump a new bid must clear, based on the current bid amount. A seller's per-listing override can require more than the matching tier, but can never undercut it." }), bidIncrementTiers.map((tier, i) => {
|
|
795
811
|
const isLast = i === bidIncrementTiers.length - 1;
|
|
796
812
|
return (_jsxs(Grid, { align: "end", gap: "xs", className: "grid-cols-12", children: [_jsx(Div, { className: "col-span-5", children: isLast ? (_jsxs(_Fragment, { children: [_jsx(Text, { size: "xs", weight: "medium", className: "mb-1", children: "Up to (\u20B9)" }), _jsx(Text, { size: "sm", color: "muted", children: "and above" })] })) : (_jsx(Input, { label: "Up to (\u20B9)", type: "number", min: 1, value: String(tier.upTo ?? ""), onChange: (e) => setBidIncrementTiers(bidIncrementTiers.map((t, j) => j === i ? { ...t, upTo: parseInt(e.target.value) || 0 } : t)) })) }), _jsx(Div, { className: "col-span-5", children: _jsx(Input, { label: "Increment (\u20B9)", type: "number", min: 1, value: String(tier.increment), onChange: (e) => setBidIncrementTiers(bidIncrementTiers.map((t, j) => j === i ? { ...t, increment: parseInt(e.target.value) || 0 } : t)) }) }), _jsx(Row, { centered: true, className: "col-span-2", padding: "b-xs", children: _jsx(Button, { variant: "ghost", type: "button", disabled: bidIncrementTiers.length <= 1, onClick: () => setBidIncrementTiers(bidIncrementTiers.filter((_, j) => j !== i)), "aria-label": "Remove tier", children: "\u00D7" }) })] }, i));
|
|
797
813
|
}), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setBidIncrementTiers([
|