@mohasinac/appkit 4.70.0 → 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/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/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/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 -1
- package/dist/index.js +0 -2
- 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/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/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
|
@@ -9,6 +9,7 @@ const CLS_PILL_LINK = "inline-flex items-center gap-[var(--appkit-space-1)] roun
|
|
|
9
9
|
import { Shield, Phone, Wallet, Mail, ChevronRight, Search } from "lucide-react";
|
|
10
10
|
import { Button, Container, Disclaimer, Div, Form, Heading, Main, Section, Select, Text, Grid, Stack, Row, Card, Badge, Alert, EmptyState, Input, } from "../../../ui/index.js";
|
|
11
11
|
import { listVerifiedScammers } from "../actions/scam-actions.js";
|
|
12
|
+
import { withBrand } from "../../../core/brand-text.js";
|
|
12
13
|
import { SCAM_PLATFORM_LABELS } from "../schemas/firestore.js";
|
|
13
14
|
import { SCAM_TYPES, SCAM_TYPE_LABELS } from "../constants/scam-types.js";
|
|
14
15
|
const SORT_OPTIONS = [
|
|
@@ -25,7 +26,7 @@ function ScammerCard({ scammer }) {
|
|
|
25
26
|
const href = String(ROUTES.PUBLIC.SCAM_DETAIL(scammer.id));
|
|
26
27
|
const name = scammer.displayNames[0] ?? "Unknown";
|
|
27
28
|
const aliases = scammer.displayNames.slice(1);
|
|
28
|
-
return (_jsx(Link, { href: href, className: "group block", children: _jsx(Card, { variant: "outlined", hover: true, padding: "md", children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", gap: "sm", align: "start", children: [_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsx(Text, { className: "truncate", weight: "semibold", children: name }), aliases.length > 0 && (_jsxs(Text, { variant: "secondary", size: "xs", children: ["Also known as: ", aliases.join(", ")] }))] }), _jsx(Badge, { variant: "danger", children: "Verified" })] }), _jsxs(Row, { gap: "xs", wrap: true, children: [_jsx(Badge, { variant: "warning", children: SCAM_TYPE_LABELS[scammer.scamType] ?? scammer.scamType }), _jsx(Badge, { variant: "default", children: SCAM_PLATFORM_LABELS[scammer.scamPlatform] ?? scammer.scamPlatform })] }), _jsxs(Stack, { gap: "xs", children: [scammer.phones.slice(0, 2).map((p) => (_jsxs(Text, { variant: "secondary", layout: "flex", gap: "1", size: "xs", children: [_jsx(Phone, { className: CLS_ICON_SM }), " ", p] }, p))), scammer.upiIds.slice(0, 1).map((u) => (_jsxs(Text, { variant: "secondary", layout: "flex", gap: "1", size: "xs", children: [_jsx(Wallet, { className: CLS_ICON_SM }), " ", u] }, u))), scammer.emails.slice(0, 1).map((e) => (_jsxs(Text, { variant: "secondary", layout: "flex", gap: "1", size: "xs", children: [_jsx(Mail, { className: CLS_ICON_SM }), " ", e] }, e)))] }), _jsxs(Row, { justify: "between", gap: "sm", children: [_jsxs(Text, { variant: "secondary", size: "xs", children: [scammer.incidentCount > 0
|
|
29
|
+
return (_jsx(Link, { href: href, className: "group block", children: _jsx(Card, { variant: "outlined", hover: true, padding: "md", children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", gap: "sm", align: "start", children: [_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsx(Text, { className: "truncate", weight: "semibold", children: name }), aliases.length > 0 && (_jsxs(Text, { variant: "secondary", size: "xs", children: ["Also known as: ", aliases.join(", ")] }))] }), _jsx(Badge, { variant: "danger", children: "Verified" })] }), _jsxs(Row, { gap: "xs", wrap: true, children: [_jsx(Badge, { variant: "warning", children: SCAM_TYPE_LABELS[scammer.scamType] ?? scammer.scamType }), _jsx(Badge, { variant: "default", children: withBrand(SCAM_PLATFORM_LABELS[scammer.scamPlatform] ?? scammer.scamPlatform) })] }), _jsxs(Stack, { gap: "xs", children: [scammer.phones.slice(0, 2).map((p) => (_jsxs(Text, { variant: "secondary", layout: "flex", gap: "1", size: "xs", children: [_jsx(Phone, { className: CLS_ICON_SM }), " ", p] }, p))), scammer.upiIds.slice(0, 1).map((u) => (_jsxs(Text, { variant: "secondary", layout: "flex", gap: "1", size: "xs", children: [_jsx(Wallet, { className: CLS_ICON_SM }), " ", u] }, u))), scammer.emails.slice(0, 1).map((e) => (_jsxs(Text, { variant: "secondary", layout: "flex", gap: "1", size: "xs", children: [_jsx(Mail, { className: CLS_ICON_SM }), " ", e] }, e)))] }), _jsxs(Row, { justify: "between", gap: "sm", children: [_jsxs(Text, { variant: "secondary", size: "xs", children: [scammer.incidentCount > 0
|
|
29
30
|
? `${scammer.incidentCount + 1} victims reported`
|
|
30
31
|
: "1 victim reported", scammer.amountLost ? ` · ${formatCurrency(scammer.amountLost)} lost` : ""] }), _jsx(ChevronRight, { className: CLS_ROW_CHEVRON })] })] }) }) }));
|
|
31
32
|
}
|
|
@@ -27,7 +27,17 @@ export interface ScamCategoryDefinition {
|
|
|
27
27
|
}
|
|
28
28
|
export declare const SCAM_CATEGORIES: ScamCategoryDefinition[];
|
|
29
29
|
export declare const SCAM_TYPES: ScamTypeDefinition[];
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Every scam type, with brand tokens resolved — use this for anything that
|
|
32
|
+
* DISPLAYS or PUBLISHES the prose.
|
|
33
|
+
*
|
|
34
|
+
* 🛑 The raw `SCAM_TYPES` export is for `id` / `label` / `length` only. The
|
|
35
|
+
* consumer's `/scams/types` page feeds `howToAvoid` into `faqJsonLd`, so taking
|
|
36
|
+
* it from the raw array publishes a literal `{{siteName}}` into FAQ structured data
|
|
37
|
+
* that Google reads — a leak with no symptom on the rendered page.
|
|
38
|
+
*/
|
|
39
|
+
export declare function getScamTypes(): ScamTypeDefinition[];
|
|
40
|
+
/** Get a scam type definition by its ID, with brand tokens resolved. */
|
|
31
41
|
export declare function getScamType(id: ScamType): ScamTypeDefinition | undefined;
|
|
32
42
|
/** Is `value` one of the 27 documented scam-type ids? */
|
|
33
43
|
export declare function isScamType(value: string): value is ScamType;
|
|
@@ -35,6 +45,8 @@ export declare function isScamType(value: string): value is ScamType;
|
|
|
35
45
|
export declare function getScamTypesByCategory(category: ScamCategory): ScamTypeDefinition[];
|
|
36
46
|
/** Get the category definition for a given scam type ID. */
|
|
37
47
|
export declare function getCategoryForScamType(scamTypeId: ScamType): ScamCategoryDefinition | undefined;
|
|
48
|
+
/** Every category, with brand tokens resolved. For display. */
|
|
49
|
+
export declare function getScamCategories(): ScamCategoryDefinition[];
|
|
38
50
|
/** Human-readable label map for quick lookups. */
|
|
39
51
|
export declare const SCAM_TYPE_LABELS: Record<ScamType, string>;
|
|
40
52
|
export declare const SCAM_CATEGORY_LABELS: Record<ScamCategory, string>;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* howToAvoid[] (actionable advice). Used on public scam-type pages,
|
|
10
10
|
* registration acknowledgement modal, and structured SEO data.
|
|
11
11
|
*/
|
|
12
|
+
import { withBrand, withBrandAll } from "../../../core/brand-text.js";
|
|
12
13
|
// ============================================================================
|
|
13
14
|
// CATEGORY DEFINITIONS
|
|
14
15
|
// ============================================================================
|
|
@@ -40,7 +41,7 @@ export const SCAM_CATEGORIES = [
|
|
|
40
41
|
{
|
|
41
42
|
id: "identity_impersonation",
|
|
42
43
|
label: "Identity & Impersonation",
|
|
43
|
-
description: "Scammer pretends to be a reputable seller,
|
|
44
|
+
description: "Scammer pretends to be a reputable seller, {{siteName}} support, or a trusted community member to steal your money or credentials.",
|
|
44
45
|
icon: "UserCheck",
|
|
45
46
|
},
|
|
46
47
|
{
|
|
@@ -68,7 +69,7 @@ export const SCAM_TYPES = [
|
|
|
68
69
|
shortDescription: "Scammer convinces seller the item is worth far less than market value to buy cheap and resell at profit.",
|
|
69
70
|
howItHappens: "A buyer approaches — often someone who appears knowledgeable — and insists your item is common, damaged, or worth only a fraction of the real price. They may say 'I saw this for ₹300 online' or 'this series is not popular anymore'. Once you sell at a deep discount, they immediately resell at true market value. This is especially common with vintage items whose original owners don't actively follow current pricing.",
|
|
70
71
|
howToAvoid: [
|
|
71
|
-
"Check current sold listings on
|
|
72
|
+
"Check current sold listings on {{siteName}}, eBay, and local Facebook groups before agreeing to any price.",
|
|
72
73
|
"Never accept a buyer's stated 'market price' at face value — do your own 5-minute research first.",
|
|
73
74
|
"If someone seems unusually eager to buy at a specific low price, that is a red flag.",
|
|
74
75
|
"PSA-graded and sealed vintage items especially hold hidden value — get independent opinions.",
|
|
@@ -97,7 +98,7 @@ export const SCAM_TYPES = [
|
|
|
97
98
|
howToAvoid: [
|
|
98
99
|
"Confirm all item specifics (set number, condition, edition) in writing before payment.",
|
|
99
100
|
"Request detailed photos of the actual item being sold — not stock images.",
|
|
100
|
-
"Use
|
|
101
|
+
"Use {{siteName}}'s on-platform messaging so all communications are recorded.",
|
|
101
102
|
"Report listings that change their item description after you express interest.",
|
|
102
103
|
],
|
|
103
104
|
},
|
|
@@ -110,7 +111,7 @@ export const SCAM_TYPES = [
|
|
|
110
111
|
howToAvoid: [
|
|
111
112
|
"Always ask for photos from multiple angles, including the corners, back, and any seal/hologram.",
|
|
112
113
|
"Ask for proof of grading: PSA/BGS certificate number you can verify on their website.",
|
|
113
|
-
"For sealed products, ask for a photo of the original shrink wrap and any
|
|
114
|
+
"For sealed products, ask for a photo of the original shrink wrap and any {{siteName}} hologram sticker.",
|
|
114
115
|
"Video unboxing on delivery is your strongest evidence for dispute claims.",
|
|
115
116
|
],
|
|
116
117
|
},
|
|
@@ -175,7 +176,7 @@ export const SCAM_TYPES = [
|
|
|
175
176
|
howItHappens: "Buyer sends money upfront via UPI, bank transfer, or wallet before receiving the item. Seller then stops responding: messages go unread, calls unanswered, account disappears or blocks the buyer. The money is gone and there is no enforceable recourse without platform protection.",
|
|
176
177
|
howToAvoid: [
|
|
177
178
|
"Never send full advance payment to an unverified individual seller outside a trusted platform.",
|
|
178
|
-
"Use
|
|
179
|
+
"Use {{siteName}}'s order flow — payment is held by the platform until delivery is confirmed.",
|
|
179
180
|
"For off-platform trades, insist on COD (cash on delivery) or use a mutually trusted physical middleman.",
|
|
180
181
|
"Partial advance (≤20%) for legitimate preorders is more reasonable — never 100% upfront to an unknown.",
|
|
181
182
|
"If a seller insists on full advance before sharing shipping details, do not proceed.",
|
|
@@ -215,8 +216,8 @@ export const SCAM_TYPES = [
|
|
|
215
216
|
howToAvoid: [
|
|
216
217
|
"Photograph and video the item before packing, and the packed parcel with tracking label visible.",
|
|
217
218
|
"Use tracked shipping with proof of delivery signatures for high-value orders.",
|
|
218
|
-
"Collect buyer's full address, phone, and
|
|
219
|
-
"On
|
|
219
|
+
"Collect buyer's full address, phone, and {{siteName}} account before shipping.",
|
|
220
|
+
"On {{siteName}}'s platform, all transactions are logged — dispute resolution can use platform records.",
|
|
220
221
|
"Be cautious of buyers who insist on credit card payment specifically for peer-to-peer deals.",
|
|
221
222
|
],
|
|
222
223
|
},
|
|
@@ -245,7 +246,7 @@ export const SCAM_TYPES = [
|
|
|
245
246
|
"Ask for proof of supplier relationship: purchase order, supplier invoice, or import record.",
|
|
246
247
|
"Be suspicious of preorders with no clear shipping date or supplier name.",
|
|
247
248
|
"A seller who cannot provide any evidence of an actual supplier order likely doesn't have one.",
|
|
248
|
-
"
|
|
249
|
+
"{{siteName}} preorders require store verification — prefer platform transactions over off-platform deals.",
|
|
249
250
|
],
|
|
250
251
|
},
|
|
251
252
|
{
|
|
@@ -256,7 +257,7 @@ export const SCAM_TYPES = [
|
|
|
256
257
|
howItHappens: "Scammer identifies a sold-out or unreleased product generating demand (e.g., a new Pokémon TCG set, limited Gundam model). They create a listing offering preorders at slightly below expected retail. After collecting enough deposits, the account disappears or is suspended.",
|
|
257
258
|
howToAvoid: [
|
|
258
259
|
"Verify that the seller has previously delivered similar preorders — check their store history.",
|
|
259
|
-
"For brand-new unreleased items, prefer official distributors or
|
|
260
|
+
"For brand-new unreleased items, prefer official distributors or {{siteName}}-verified stores.",
|
|
260
261
|
"If the deal is significantly below expected retail for a sold-out item, it is very likely fraudulent.",
|
|
261
262
|
],
|
|
262
263
|
},
|
|
@@ -270,7 +271,7 @@ export const SCAM_TYPES = [
|
|
|
270
271
|
"Set a personal deadline when placing any order with advance payment — if not shipped by date X, dispute immediately.",
|
|
271
272
|
"Vague updates without tracking numbers or evidence of actual shipment are a warning sign.",
|
|
272
273
|
"File a payment dispute with your UPI bank before the dispute window closes, even if the seller is still communicating.",
|
|
273
|
-
"On
|
|
274
|
+
"On {{siteName}}, orders past their expected date can be escalated to admin — do not wait.",
|
|
274
275
|
],
|
|
275
276
|
},
|
|
276
277
|
// ── Identity & Impersonation ─────────────────────────────────────────────────
|
|
@@ -279,23 +280,23 @@ export const SCAM_TYPES = [
|
|
|
279
280
|
category: "identity_impersonation",
|
|
280
281
|
label: "Seller Impersonation",
|
|
281
282
|
shortDescription: "Scammer steals product photos from a reputable seller's listings and creates identical listings to collect payment for nothing.",
|
|
282
|
-
howItHappens: "Scammer finds a legitimate seller's high-quality listing photos (from
|
|
283
|
+
howItHappens: "Scammer finds a legitimate seller's high-quality listing photos (from {{siteName}}, OLX, Instagram, or YouTube). They create a near-identical listing — same photos, similar description, slightly lower price. Buyers who search the seller's name or item find the fake listing first. After payment, nothing is shipped.",
|
|
283
284
|
howToAvoid: [
|
|
284
|
-
"Verify the seller profile carefully — check their store on
|
|
285
|
+
"Verify the seller profile carefully — check their store on {{siteName}}, their review history, and how long they've been active.",
|
|
285
286
|
"Do a reverse image search on any listing photos to see if they appear elsewhere.",
|
|
286
287
|
"If a price is significantly lower than the original seller's known price, it may be an impersonation.",
|
|
287
|
-
"Transact only through
|
|
288
|
+
"Transact only through {{siteName}}'s order flow — off-platform payments have no protection.",
|
|
288
289
|
],
|
|
289
290
|
},
|
|
290
291
|
{
|
|
291
292
|
id: "platform_impersonation",
|
|
292
293
|
category: "identity_impersonation",
|
|
293
294
|
label: "Platform Impersonation (Fake Support)",
|
|
294
|
-
shortDescription: "Scammer pretends to be
|
|
295
|
-
howItHappens: "You receive a message from '
|
|
295
|
+
shortDescription: "Scammer pretends to be {{siteName}} support via WhatsApp/DM and requests OTP, bank details, or login credentials.",
|
|
296
|
+
howItHappens: "You receive a message from '{{siteName}} Support' on WhatsApp, Telegram, or Instagram DMs. The message claims there's a 'KYC issue', 'payment hold', or 'account verification'. They ask for your OTP, password, bank account number, or ask you to send a 'refund' to their UPI to receive a 'blocked payout'.",
|
|
296
297
|
howToAvoid: [
|
|
297
|
-
"
|
|
298
|
-
"Official
|
|
298
|
+
"{{siteName}} support will NEVER ask for your password, OTP, or bank account details via WhatsApp.",
|
|
299
|
+
"Official {{siteName}} communication comes only from @{{siteDomain}} email addresses.",
|
|
299
300
|
"If you receive a suspicious support message, verify by logging into your account and checking the official support section.",
|
|
300
301
|
"Never send money to 'unblock' a payout — this is always a scam.",
|
|
301
302
|
],
|
|
@@ -320,7 +321,7 @@ export const SCAM_TYPES = [
|
|
|
320
321
|
shortDescription: "Scammer creates a fake third-party escrow or middleman service that is themselves or an accomplice.",
|
|
321
322
|
howItHappens: "Scammer proposes using an 'escrow service' or 'trusted community middleman' to reassure the other party. The escrow account, website, or WhatsApp contact is created or controlled by the scammer. Both buyer and seller are told to send money or item to the 'neutral' party — who then disappears.",
|
|
322
323
|
howToAvoid: [
|
|
323
|
-
"Use only
|
|
324
|
+
"Use only {{siteName}}'s platform for escrow protection — never third-party escrow services you haven't independently verified.",
|
|
324
325
|
"A genuine middleman will not be introduced by one party in the transaction.",
|
|
325
326
|
"Verify any escrow service independently before using it — search their name online, look for reviews.",
|
|
326
327
|
],
|
|
@@ -415,7 +416,7 @@ export const SCAM_TYPES = [
|
|
|
415
416
|
howToAvoid: [
|
|
416
417
|
"Photo and video every item before packing, and photograph the parcel with tracking label visible.",
|
|
417
418
|
"On receiving a return, photograph the unopened return package before opening.",
|
|
418
|
-
"If return contents do not match what was sent, document immediately and escalate to
|
|
419
|
+
"If return contents do not match what was sent, document immediately and escalate to {{siteName}} support.",
|
|
419
420
|
"For high-value items, add unique identifying marks (invisible ink, specific photo angles) that prove the returned item is yours.",
|
|
420
421
|
],
|
|
421
422
|
},
|
|
@@ -423,9 +424,42 @@ export const SCAM_TYPES = [
|
|
|
423
424
|
// ============================================================================
|
|
424
425
|
// LOOKUP HELPERS
|
|
425
426
|
// ============================================================================
|
|
426
|
-
/**
|
|
427
|
+
/**
|
|
428
|
+
* Resolve the brand tokens in one definition's prose.
|
|
429
|
+
*
|
|
430
|
+
* 🛑 `SCAM_TYPES` stores `{{siteName}}` / `{{siteDomain}}` rather than a literal, because
|
|
431
|
+
* it is module-level data and a template literal there would capture appkit's
|
|
432
|
+
* neutral default at import time — before the consumer's `configureBrand()`
|
|
433
|
+
* runs. See `core/brand-text.ts`.
|
|
434
|
+
*
|
|
435
|
+
* `id`, `category` and `label` are passed through untouched: they are keys and
|
|
436
|
+
* short names, carry no brand, and `label` is what the registry filter's
|
|
437
|
+
* `<PaginatedSelect>` options and the scam-report route match on.
|
|
438
|
+
*/
|
|
439
|
+
function resolveScamTypeBrand(t) {
|
|
440
|
+
return {
|
|
441
|
+
...t,
|
|
442
|
+
shortDescription: withBrand(t.shortDescription),
|
|
443
|
+
howItHappens: withBrand(t.howItHappens),
|
|
444
|
+
howToAvoid: withBrandAll(t.howToAvoid),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Every scam type, with brand tokens resolved — use this for anything that
|
|
449
|
+
* DISPLAYS or PUBLISHES the prose.
|
|
450
|
+
*
|
|
451
|
+
* 🛑 The raw `SCAM_TYPES` export is for `id` / `label` / `length` only. The
|
|
452
|
+
* consumer's `/scams/types` page feeds `howToAvoid` into `faqJsonLd`, so taking
|
|
453
|
+
* it from the raw array publishes a literal `{{siteName}}` into FAQ structured data
|
|
454
|
+
* that Google reads — a leak with no symptom on the rendered page.
|
|
455
|
+
*/
|
|
456
|
+
export function getScamTypes() {
|
|
457
|
+
return SCAM_TYPES.map(resolveScamTypeBrand);
|
|
458
|
+
}
|
|
459
|
+
/** Get a scam type definition by its ID, with brand tokens resolved. */
|
|
427
460
|
export function getScamType(id) {
|
|
428
|
-
|
|
461
|
+
const found = SCAM_TYPES.find((t) => t.id === id);
|
|
462
|
+
return found ? resolveScamTypeBrand(found) : undefined;
|
|
429
463
|
}
|
|
430
464
|
/**
|
|
431
465
|
* The set of real ids, for narrowing an arbitrary string.
|
|
@@ -447,14 +481,24 @@ export function isScamType(value) {
|
|
|
447
481
|
}
|
|
448
482
|
/** Get all scam types in a given category. */
|
|
449
483
|
export function getScamTypesByCategory(category) {
|
|
450
|
-
return SCAM_TYPES.filter((t) => t.category === category);
|
|
484
|
+
return SCAM_TYPES.filter((t) => t.category === category).map(resolveScamTypeBrand);
|
|
451
485
|
}
|
|
452
486
|
/** Get the category definition for a given scam type ID. */
|
|
453
487
|
export function getCategoryForScamType(scamTypeId) {
|
|
454
488
|
const type = getScamType(scamTypeId);
|
|
455
489
|
if (!type)
|
|
456
490
|
return undefined;
|
|
457
|
-
|
|
491
|
+
const category = SCAM_CATEGORIES.find((c) => c.id === type.category);
|
|
492
|
+
return category
|
|
493
|
+
? { ...category, description: withBrand(category.description) }
|
|
494
|
+
: undefined;
|
|
495
|
+
}
|
|
496
|
+
/** Every category, with brand tokens resolved. For display. */
|
|
497
|
+
export function getScamCategories() {
|
|
498
|
+
return SCAM_CATEGORIES.map((c) => ({
|
|
499
|
+
...c,
|
|
500
|
+
description: withBrand(c.description),
|
|
501
|
+
}));
|
|
458
502
|
}
|
|
459
503
|
/** Human-readable label map for quick lookups. */
|
|
460
504
|
export const SCAM_TYPE_LABELS = Object.fromEntries(SCAM_TYPES.map((t) => [t.id, t.label]));
|
|
@@ -210,7 +210,7 @@ export const SCAM_PLATFORM_LABELS = {
|
|
|
210
210
|
twitter: "X (Twitter)",
|
|
211
211
|
olx: "OLX",
|
|
212
212
|
facebook_marketplace: "Facebook Marketplace",
|
|
213
|
-
letitrip: "
|
|
213
|
+
letitrip: "{{siteName}}",
|
|
214
214
|
in_person: "In Person",
|
|
215
215
|
phone_call: "Phone Call",
|
|
216
216
|
other: "Other",
|
|
@@ -8,6 +8,7 @@ const SVG_PATH_SEARCH = "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z";
|
|
|
8
8
|
const CLS_HOVER_ROW = "hover:bg-surface-hover";
|
|
9
9
|
const CLS_ICON_ROW = "w-4 h-4 flex-shrink-0 text-[var(--appkit-color-text-muted)]";
|
|
10
10
|
const CLS_TRUNCATE = "font-medium truncate";
|
|
11
|
+
import { STORAGE_KEYS } from "../../../constants/storage-keys.js";
|
|
11
12
|
import { Button } from "../../../ui/components/Button.js";
|
|
12
13
|
import { Div } from "../../../ui/components/Div.js";
|
|
13
14
|
import { Input } from "../../../ui/components/Input.js";
|
|
@@ -44,7 +45,10 @@ function SuggestionItem({ suggestion, itemIndex, activeIndex, onSelect, onHover,
|
|
|
44
45
|
const isActive = activeIndex === itemIndex;
|
|
45
46
|
return (_jsx(Li, { children: _jsxs(Button, { type: "button", variant: "ghost", onClick: () => onSelect(suggestion), onMouseEnter: () => onHover(itemIndex), className: `w-full flex items-center gap-[var(--appkit-space-3)] px-[var(--appkit-space-4)] py-[var(--appkit-space-3)] text-left transition-colors border-b border-[var(--appkit-color-border)] last:border-b-0 ${isActive ? "bg-[var(--appkit-color-surface-elevated)]" : CLS_HOVER_ROW}`, children: [_jsx(Span, { size: "sm", children: typeIcons[suggestion.type] }), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Text, { size: "sm", className: CLS_TRUNCATE, children: suggestion.title }), suggestion.subtitle && (_jsx(Text, { variant: "secondary", size: "xs", className: "truncate", children: suggestion.subtitle }))] }), _jsx(Span, { size: "xs", className: `${typeBadges[suggestion.type]}`, rounded: "full", padding: "pill-xs", children: suggestion.type })] }) }));
|
|
46
47
|
}
|
|
47
|
-
export function Search({ isOpen, onClose, onSearch, onOpen, value, onChange, placeholder, deferred = true, debounceMs = 300, onClear, className, router, quickLinks = [], labels, suggestionTypeIcons, suggestionTypeBadges, resourceTypes, defaultResourceType = "products",
|
|
48
|
+
export function Search({ isOpen, onClose, onSearch, onOpen, value, onChange, placeholder, deferred = true, debounceMs = 300, onClear, className, router, quickLinks = [], labels, suggestionTypeIcons, suggestionTypeBadges, resourceTypes, defaultResourceType = "products",
|
|
49
|
+
// Default resolved at call time from the configured namespace, not a brand
|
|
50
|
+
// literal — a library default must not name one consumer.
|
|
51
|
+
storageKey = STORAGE_KEYS.searchType, searchIndexEnabled, }) {
|
|
48
52
|
const isInlineMode = value !== undefined;
|
|
49
53
|
const [query, setQuery] = useState(isInlineMode ? value : "");
|
|
50
54
|
const [activeIndex, setActiveIndex] = useState(-1);
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* Auth, rate-limiting, and input validation are handled by the calling server action.
|
|
6
6
|
*/
|
|
7
7
|
import { serverLogger } from "../../../monitoring/index.js";
|
|
8
|
+
// Money in prose reads from the configured market profile — symbol, grouping
|
|
9
|
+
// and locale. These strings used `₹${n}`, which also rendered ungrouped.
|
|
10
|
+
import { formatMoney } from "../../../utils/number.formatter.js";
|
|
8
11
|
import { offerRepository } from "../repository/offer.repository.js";
|
|
9
12
|
import { productRepository } from "../../products/repository/products.repository.js";
|
|
10
13
|
import { categoriesRepository } from "../../categories/repository/categories.repository.js";
|
|
@@ -103,18 +106,18 @@ export async function makeOffer(userId, userEmail, input) {
|
|
|
103
106
|
// asking price is the only legal amount. Naming that plainly beats a
|
|
104
107
|
// "minimum offer is ₹X" message where X happens to equal the price.
|
|
105
108
|
if (offerAmount !== bounds.max)
|
|
106
|
-
throw new ValidationError(`This seller is not taking lower offers — send a request to buy at
|
|
109
|
+
throw new ValidationError(`This seller is not taking lower offers — send a request to buy at ${formatMoney(bounds.max)}.`);
|
|
107
110
|
}
|
|
108
111
|
else {
|
|
109
112
|
if (offerAmount < bounds.min)
|
|
110
|
-
throw new ValidationError(`Minimum offer is
|
|
113
|
+
throw new ValidationError(`Minimum offer is ${formatMoney(bounds.min)} (${product.minOfferPercent ?? DEFAULT_MIN_OFFER_PERCENT}% of listing price).`);
|
|
111
114
|
if (offerAmount > bounds.max)
|
|
112
115
|
throw new ValidationError(
|
|
113
116
|
// Deliberately NOT "use Add to Cart instead" unconditionally: on a
|
|
114
117
|
// listing whose only purchase path is an offer there is no cart button,
|
|
115
118
|
// so that advice named a control the page does not have.
|
|
116
119
|
offerIsOnlyPurchasePath(listingType)
|
|
117
|
-
? `Your offer cannot exceed the asking price of
|
|
120
|
+
? `Your offer cannot exceed the asking price of ${formatMoney(product.price)}.`
|
|
118
121
|
: "Your offer must be below the listed price. Use Add to Cart instead.");
|
|
119
122
|
}
|
|
120
123
|
const offerCount = await offerRepository.countByBuyerAndProduct(userId, productId, product.updatedAt);
|
|
@@ -159,7 +162,7 @@ export async function makeOffer(userId, userEmail, input) {
|
|
|
159
162
|
type: "offer_received",
|
|
160
163
|
priority: "normal",
|
|
161
164
|
title: "New offer received",
|
|
162
|
-
message: `${buyerDisplayName === "Buyer" ? "A buyer" : buyerDisplayName} offered
|
|
165
|
+
message: `${buyerDisplayName === "Buyer" ? "A buyer" : buyerDisplayName} offered ${formatMoney(offerAmount)} on "${product.title}"`,
|
|
163
166
|
relatedId: offer.id,
|
|
164
167
|
relatedType: "offer",
|
|
165
168
|
});
|
|
@@ -186,6 +189,12 @@ export async function respondToOffer(userId, input) {
|
|
|
186
189
|
if (new Date() > offer.expiresAt)
|
|
187
190
|
throw new ValidationError(ERROR_MESSAGES.OFFER.EXPIRED);
|
|
188
191
|
let updated;
|
|
192
|
+
/**
|
|
193
|
+
* The buyer-facing sentence for a COUNTER, assembled in the branch that owns
|
|
194
|
+
* the amount. `""` is unreachable — the ternary below only selects it when
|
|
195
|
+
* `action === "counter"`, which is exactly the branch that assigns it.
|
|
196
|
+
*/
|
|
197
|
+
let counterMessage = "";
|
|
189
198
|
if (action === "accept") {
|
|
190
199
|
/*
|
|
191
200
|
* ── S5a: a coordinated offer must be SHIPPABLE before it is binding ──
|
|
@@ -253,12 +262,28 @@ export async function respondToOffer(userId, input) {
|
|
|
253
262
|
if (counterAmount === offer.offerAmount)
|
|
254
263
|
throw new ValidationError("Counter amount must differ from the buyer's offer. Accept it instead.");
|
|
255
264
|
updated = await offerRepository.counter(offerId, counterAmount, sellerNote);
|
|
265
|
+
/*
|
|
266
|
+
* 🛑 Built HERE, where the guards above have proven `counterAmount` is a
|
|
267
|
+
* number, rather than in the ternary below.
|
|
268
|
+
*
|
|
269
|
+
* That ternary used to read `₹${counterAmount}` against the
|
|
270
|
+
* possibly-undefined parameter — so a counter reaching it without an amount
|
|
271
|
+
* told the buyer the seller "countered with ₹undefined". `tsc` only
|
|
272
|
+
* surfaced it once the literal became `formatMoney(...)`, which is the
|
|
273
|
+
* argument for the conversion beyond consistency.
|
|
274
|
+
*
|
|
275
|
+
* Neither `!` nor `?? 0` is the fix: the first re-asserts what narrowing
|
|
276
|
+
* already knows and the second would quote a real-looking "₹0" to a buyer.
|
|
277
|
+
* Assigning the finished sentence in the branch that owns the value makes
|
|
278
|
+
* the absent case unrepresentable instead.
|
|
279
|
+
*/
|
|
280
|
+
counterMessage = `${offer.storeName} countered with ${formatMoney(counterAmount)} on "${offer.productTitle}".`;
|
|
256
281
|
}
|
|
257
282
|
const notifMessage = action === "accept"
|
|
258
|
-
? `Your offer of
|
|
283
|
+
? `Your offer of ${formatMoney(offer.offerAmount)} on "${offer.productTitle}" was accepted! Complete checkout now.`
|
|
259
284
|
: action === "decline"
|
|
260
285
|
? `Your offer on "${offer.productTitle}" was declined.`
|
|
261
|
-
:
|
|
286
|
+
: counterMessage;
|
|
262
287
|
await sendNotification({
|
|
263
288
|
userId: offer.buyerUid,
|
|
264
289
|
type: "offer_responded",
|
|
@@ -288,6 +313,19 @@ export async function acceptCounterOffer(userId, offerId) {
|
|
|
288
313
|
throw new AuthorizationError(ERR_NOT_AUTHORISED);
|
|
289
314
|
if (offer.status !== OfferStatusValues.COUNTERED)
|
|
290
315
|
throw new ValidationError("No counter to accept.");
|
|
316
|
+
/*
|
|
317
|
+
* 🛑 A COUNTERED offer with no amount is a data-integrity violation, not a
|
|
318
|
+
* case to paper over. The seller notification below quotes this figure, and
|
|
319
|
+
* it previously interpolated the optional field directly — so such a row
|
|
320
|
+
* would have told the seller the buyer "accepted your counter of ₹undefined",
|
|
321
|
+
* and accepting it would have locked a cart line at a price nobody set.
|
|
322
|
+
*
|
|
323
|
+
* Refusing is the only honest answer: `counterAmount` is what the buyer is
|
|
324
|
+
* agreeing to, so if it is missing there is nothing to agree to. Narrowing it
|
|
325
|
+
* here is a side effect of saying so, not the reason.
|
|
326
|
+
*/
|
|
327
|
+
if (typeof offer.counterAmount !== "number")
|
|
328
|
+
throw new ValidationError("This counter has no amount recorded — ask the seller to send it again.");
|
|
291
329
|
if (new Date() > offer.expiresAt)
|
|
292
330
|
throw new ValidationError(ERROR_MESSAGES.OFFER.EXPIRED);
|
|
293
331
|
const updated = await offerRepository.acceptCounter(offerId, new Date(Date.now() + OFFER_CHECKOUT_WINDOW_MS));
|
|
@@ -298,7 +336,7 @@ export async function acceptCounterOffer(userId, offerId) {
|
|
|
298
336
|
type: "offer_counter_accepted",
|
|
299
337
|
priority: "high",
|
|
300
338
|
title: "Counter offer accepted",
|
|
301
|
-
message: `Buyer accepted your counter of
|
|
339
|
+
message: `Buyer accepted your counter of ${formatMoney(offer.counterAmount)} on "${offer.productTitle}". They can now checkout.`,
|
|
302
340
|
relatedId: offerId,
|
|
303
341
|
relatedType: "offer",
|
|
304
342
|
});
|
|
@@ -322,7 +360,7 @@ export async function counterOfferByBuyer(userId, userEmail, input) {
|
|
|
322
360
|
const minAllowed = Math.floor(sellerCounter * 0.8);
|
|
323
361
|
const maxAllowed = Math.ceil(sellerCounter * 1.2);
|
|
324
362
|
if (counterAmount < minAllowed || counterAmount > maxAllowed)
|
|
325
|
-
throw new ValidationError(`${ERROR_MESSAGES.OFFER.COUNTER_RANGE} Allowed:
|
|
363
|
+
throw new ValidationError(`${ERROR_MESSAGES.OFFER.COUNTER_RANGE} Allowed: ${formatMoney(minAllowed)}–${formatMoney(maxAllowed)}.`);
|
|
326
364
|
if (counterAmount >= offer.listedPrice)
|
|
327
365
|
throw new ValidationError("Counter offer cannot reach or exceed the listed price. Accept the seller's counter instead.");
|
|
328
366
|
const product = await productRepository.findById(offer.productId);
|
|
@@ -407,7 +445,7 @@ export async function counterOfferByBuyer(userId, userEmail, input) {
|
|
|
407
445
|
* already in scope 41 lines up — `offerPlain` at :431, used for the
|
|
408
446
|
* `createOffer` call at :439.
|
|
409
447
|
*/
|
|
410
|
-
message: `${offerPlain.buyerName} countered with
|
|
448
|
+
message: `${offerPlain.buyerName} countered with ${formatMoney(counterAmount)} on "${offer.productTitle}".`,
|
|
411
449
|
relatedId: newOffer.id,
|
|
412
450
|
relatedType: "offer",
|
|
413
451
|
});
|
|
@@ -39,6 +39,7 @@ import { FieldInput } from "../../../ui/forms/FieldInput.js";
|
|
|
39
39
|
import { FieldSelect } from "../../../ui/forms/FieldSelect.js";
|
|
40
40
|
import { shipOrderSchema } from "../schemas/order-forms.js";
|
|
41
41
|
import { applyZodIssues, rootLevelMessages } from "../../../ui/forms/apply-zod-issues.js";
|
|
42
|
+
import { getBrandName } from "../../../core/brand-resolver.js";
|
|
42
43
|
/**
|
|
43
44
|
* Box presets, in centimetres and kilograms.
|
|
44
45
|
*
|
|
@@ -135,7 +136,7 @@ platformBooked, onDispatched, }) {
|
|
|
135
136
|
}, [pickups, pickupId]);
|
|
136
137
|
if (trackingNumber) {
|
|
137
138
|
return (_jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", children: "Dispatched" }), _jsx(Text, { size: "xs", color: "muted", children: platformBooked
|
|
138
|
-
? `Booked through
|
|
139
|
+
? `Booked through ${getBrandName()} · ${trackingNumber}`
|
|
139
140
|
: `Posted by you · ${trackingNumber}` })] }));
|
|
140
141
|
}
|
|
141
142
|
const submit = async (
|
|
@@ -178,7 +179,7 @@ platformBooked, onDispatched, }) {
|
|
|
178
179
|
}
|
|
179
180
|
shipMutation.mutate(parsed.data);
|
|
180
181
|
};
|
|
181
|
-
return (_jsxs(Stack, { gap: "3", children: [_jsx(Text, { size: "sm", weight: "medium", children: "Dispatch" }), _jsxs(Row, { gap: "sm", children: [_jsx(Button, { size: "sm", variant: method === "custom" ? "primary" : "outline", "aria-pressed": method === "custom", onClick: () => setMethod("custom"), children: "I posted it" }),
|
|
182
|
+
return (_jsxs(Stack, { gap: "3", children: [_jsx(Text, { size: "sm", weight: "medium", children: "Dispatch" }), _jsxs(Row, { gap: "sm", children: [_jsx(Button, { size: "sm", variant: method === "custom" ? "primary" : "outline", "aria-pressed": method === "custom", onClick: () => setMethod("custom"), children: "I posted it" }), _jsxs(Button, { size: "sm", variant: method === "platform" ? "primary" : "outline", "aria-pressed": method === "platform", onClick: () => setMethod("platform"), children: ["Book through ", getBrandName()] })] }), _jsx(Form, { onSubmit: (e) => e.preventDefault(), children: ({ setFieldError, clearErrors }) => {
|
|
182
183
|
fieldErrorSink.current = setFieldError;
|
|
183
184
|
return (_jsxs(Stack, { gap: "3", children: [method === "custom" ? (_jsxs(_Fragment, { children: [_jsx(FieldInput, { name: "shippingCarrier", label: "Carrier", required: true, value: carrier, onChange: setCarrier }), _jsx(FieldInput, { name: "trackingNumber", label: "Tracking number", required: true, value: tracking, onChange: setTracking }), _jsx(FieldInput, { name: "trackingUrl", label: "Tracking URL", required: true, type: "url", value: trackingUrl, onChange: setTrackingUrl })] })) : (_jsxs(_Fragment, { children: [pickupsFailed ? (_jsx(Text, { size: "xs", color: "error", role: "alert", children: "Could not load your pickup addresses. Reopen this panel to try again." })) : pickups.length === 0 ? (_jsx(Text, { size: "xs", color: "muted", children: "Loading your pickup addresses\u2026" })) : (_jsx(FieldSelect, { name: "pickupAddressId", label: "Collect from", required: true, value: pickupId, onChange: setPickupId, options: pickups.map((p) => ({
|
|
184
185
|
value: p.id,
|
|
@@ -9,6 +9,8 @@ import { formatPhysicalLocation, } from "../../../utils/physical-location.js";
|
|
|
9
9
|
import { LabelPageStyle, LabelSheet, ProductLabel, PrintScaleWarning, } from "../../print/components/labels.js";
|
|
10
10
|
import { LabelProfilePicker } from "../../print/components/LabelProfilePicker.js";
|
|
11
11
|
import { LABEL_PROFILES, documentPageCss, labelPageCss, } from "../../../_internal/shared/features/print/label-profiles.js";
|
|
12
|
+
import { formatMoney } from "../../../utils/number.formatter.js";
|
|
13
|
+
import { getBrandName } from "../../../core/brand-resolver.js";
|
|
12
14
|
/*
|
|
13
15
|
* 🛑 THE LOCAL `ProductLabel` IS GONE. It was `w-[200px]`, rendered a barcode
|
|
14
16
|
* at `width: 2` (a 0.53 mm module — twice the density needed) with a fixed
|
|
@@ -22,9 +24,9 @@ import { LABEL_PROFILES, documentPageCss, labelPageCss, } from "../../../_intern
|
|
|
22
24
|
* and `label-profiles.ts` for why the fit check refuses rather than scales.
|
|
23
25
|
*/
|
|
24
26
|
function PackingSlip({ order, brandName }) {
|
|
25
|
-
return (_jsxs(Stack, { surface: "card", padding: "md", rounded: "sm", className: "break-inside-avoid mb-4 max-w-[480px]", children: [_jsxs(Row, { justify: "between", className: "mb-2", children: [_jsx(Heading, { level: 5, children: brandName }), _jsxs(Text, { size: "xs", color: "muted", children: ["#", order.id] })] }), _jsx(Text, { size: "xs", color: "muted", children: new Date(order.createdAt).toLocaleDateString("en-IN") }), order.buyerDisplayName && _jsx(Text, { size: "sm", weight: "medium", children: order.buyerDisplayName }), order.buyerCity && _jsx(Text, { size: "xs", color: "muted", children: order.buyerCity }), formatPhysicalLocation(order.physicalLocation) && (_jsxs(Text, { size: "xs", color: "muted", children: ["Staging: ", formatPhysicalLocation(order.physicalLocation)] })), _jsx(Stack, { border: "top", padding: "t-xs", className: "mt-3", children: order.items.map((item, i) => (_jsxs(Row, { justify: "between", padding: "y-2xs", className: "border-b last:border-b-0", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", weight: "medium", children: item.productName }), item.barcodeId && _jsx(Text, { size: "xs", color: "muted", children: item.barcodeId })] }), _jsxs(Stack, { className: "text-right", children: [_jsxs(Text, { size: "xs", children: ["\u00D7", item.quantity] }),
|
|
27
|
+
return (_jsxs(Stack, { surface: "card", padding: "md", rounded: "sm", className: "break-inside-avoid mb-4 max-w-[480px]", children: [_jsxs(Row, { justify: "between", className: "mb-2", children: [_jsx(Heading, { level: 5, children: brandName }), _jsxs(Text, { size: "xs", color: "muted", children: ["#", order.id] })] }), _jsx(Text, { size: "xs", color: "muted", children: new Date(order.createdAt).toLocaleDateString("en-IN") }), order.buyerDisplayName && _jsx(Text, { size: "sm", weight: "medium", children: order.buyerDisplayName }), order.buyerCity && _jsx(Text, { size: "xs", color: "muted", children: order.buyerCity }), formatPhysicalLocation(order.physicalLocation) && (_jsxs(Text, { size: "xs", color: "muted", children: ["Staging: ", formatPhysicalLocation(order.physicalLocation)] })), _jsx(Stack, { border: "top", padding: "t-xs", className: "mt-3", children: order.items.map((item, i) => (_jsxs(Row, { justify: "between", padding: "y-2xs", className: "border-b last:border-b-0", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", weight: "medium", children: item.productName }), item.barcodeId && _jsx(Text, { size: "xs", color: "muted", children: item.barcodeId })] }), _jsxs(Stack, { className: "text-right", children: [_jsxs(Text, { size: "xs", children: ["\u00D7", item.quantity] }), _jsx(Text, { size: "xs", color: "muted", children: formatMoney(item.price) })] })] }, i))) })] }));
|
|
26
28
|
}
|
|
27
|
-
export function PrintCenterView({ store, brandName =
|
|
29
|
+
export function PrintCenterView({ store, brandName = `${getBrandName()}`, initialProducts = [], initialOrders = [], }) {
|
|
28
30
|
const [tab, setTab] = useState("labels");
|
|
29
31
|
const [selectedProducts, setSelectedProducts] = useState(new Set());
|
|
30
32
|
const [selectedOrders, setSelectedOrders] = useState(new Set());
|
|
@@ -16,10 +16,11 @@ import { toRecordArray, toRelativeDate, toStringValue, } from "../../admin/hooks
|
|
|
16
16
|
import { useOptimisticRowFlag } from "../../admin/hooks/useOptimisticRowFlag.js";
|
|
17
17
|
import { apiClient } from "../../../http/index.js";
|
|
18
18
|
import { DataListingView } from "../../admin/components/DataListingView.js";
|
|
19
|
+
import { formatMoney } from "../../../utils/number.formatter.js";
|
|
19
20
|
function formatPrice(value) {
|
|
20
21
|
if (typeof value !== "number" || value <= 0)
|
|
21
22
|
return "—";
|
|
22
|
-
return
|
|
23
|
+
return formatMoney(value);
|
|
23
24
|
}
|
|
24
25
|
const COLUMNS = [
|
|
25
26
|
{
|
|
@@ -10,6 +10,7 @@ import { annotate } from "../../shell/field-ui-meta.js";
|
|
|
10
10
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
11
11
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
12
12
|
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
13
|
+
import { getBrandName } from "../../../core/brand-resolver.js";
|
|
13
14
|
const __P = {
|
|
14
15
|
p3: "p-[var(--appkit-space-3)]",
|
|
15
16
|
};
|
|
@@ -50,7 +51,7 @@ const payoutSettingsDraftSchema = z.object({
|
|
|
50
51
|
accountType: annotate(z.enum(["savings", "current"]), { section: "method", quick: true, order: 7, row: "pair" }),
|
|
51
52
|
// The three fields that identify the SUPPLIER OF RECORD on this store's
|
|
52
53
|
// invoices. A seller with a GSTIN invoices under it; one without
|
|
53
|
-
// invoices under
|
|
54
|
+
// invoices under {getBrandName()}'s (resolveSupplierIdentity).
|
|
54
55
|
//
|
|
55
56
|
// `pan` and `businessType` were removed 2026-10-19: neither exists on
|
|
56
57
|
// StoreDocument or UserDocument and the route stripped them, so a
|
|
@@ -334,7 +335,7 @@ export function SellerPayoutSettingsView({ apiBase = SELLER_ENDPOINTS.PAYOUT_SET
|
|
|
334
335
|
id: "tax",
|
|
335
336
|
label: "Tax Info",
|
|
336
337
|
fields: ["gstin", "legalName", "registeredAddress"],
|
|
337
|
-
render: ({ values, onChange }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Tax Information" }),
|
|
338
|
+
render: ({ values, onChange }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Tax Information" }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: ["Optional. Enter a GSTIN and your orders are invoiced under your own registration; leave it blank and they are invoiced under ", getBrandName(), "'s."] }), _jsx(FormField, { name: "gstin", label: "GSTIN (optional)", type: "text", value: values.gstin, onChange: (v) => onChange({ gstin: v.toUpperCase() }), placeholder: "e.g. 22AAAAA0000A1Z5", helpText: "15-character Goods and Services Tax Identification Number.", disabled: busy }), _jsx(FormField, { name: "legalName", label: "Registered legal name (optional)", type: "text", value: values.legalName, onChange: (v) => onChange({ legalName: v }), placeholder: "e.g. Arena Collectibles Pvt Ltd", helpText: "The entity name that appears on the invoice. Leave blank to use your store name.", disabled: busy }), _jsx(FormField, { name: "registeredAddress", label: "Registered address (optional)", type: "text", value: values.registeredAddress, onChange: (v) => onChange({ registeredAddress: v }), placeholder: "Address as registered against your GSTIN", helpText: "Printed on invoices issued under your GSTIN.", disabled: busy })] })),
|
|
338
339
|
},
|
|
339
340
|
{
|
|
340
341
|
id: "preferences",
|
|
@@ -20,6 +20,7 @@ import { isFinalSale } from "../../products/constants/final-sale.js";
|
|
|
20
20
|
import { productImageUrls } from "../../products/schemas/firestore.js";
|
|
21
21
|
import { ProductFeaturesSelector } from "../../products/components/ProductFeaturesSelector.js";
|
|
22
22
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
23
|
+
import { getBrandName } from "../../../core/brand-resolver.js";
|
|
23
24
|
/**
|
|
24
25
|
* "bundle" is a categoryType, not a ListingType, so it can't live in
|
|
25
26
|
* LISTING_TYPE_REGISTRY — this is the single, explicit escape hatch that
|
|
@@ -132,7 +133,7 @@ function StepBasic({ values, onChange, renderCategorySelector, renderBrandSelect
|
|
|
132
133
|
title: values.title ?? "",
|
|
133
134
|
brand: values.brand ?? "",
|
|
134
135
|
condition: CONDITION_OPTIONS.find((o) => o.value === (values.condition ?? "new"))?.label ?? "",
|
|
135
|
-
siteName:
|
|
136
|
+
siteName: `${getBrandName()}`,
|
|
136
137
|
};
|
|
137
138
|
return (_jsxs(Stack, { gap: "md", children: [_jsx(FormField, { name: "title", label: "Title", type: "text", value: values.title ?? "", onChange: (v) => onChange({ title: v }), placeholder: "e.g. Charizard Base Set PSA 9" }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", weight: "medium", children: "What is in this listing?" }), _jsxs(Row, { gap: "sm", align: "center", wrap: true, children: [_jsx(Toggle, { checked: values.isLot === true, onChange: (next) => onChange(next
|
|
138
139
|
? /*
|
|
@@ -347,7 +348,7 @@ const LIVE_TRANSPORT_OPTIONS = [
|
|
|
347
348
|
{ value: "specialist", label: "Third-party specialist transport" },
|
|
348
349
|
];
|
|
349
350
|
function StepLiveItemSettings({ values, onChange, }) {
|
|
350
|
-
return (_jsxs(Stack, { gap: "md", children: [
|
|
351
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Alert, { variant: "warning", children: ["Live animal / plant listings must comply with all applicable laws, CITES regulations, and local jurisdiction rules. ", getBrandName(), " is not responsible for legal compliance \u2014 this is solely the seller's responsibility."] }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "liveSpecies", label: "Species / Common Name", type: "text", value: values.liveSpecies ?? "", onChange: (v) => onChange({ liveSpecies: v }), placeholder: "e.g. Axolotl (Ambystoma mexicanum)" }), _jsx(FormField, { name: "liveAgeMonths", label: "Age (months, optional)", type: "number", value: String(values.liveAgeMonths ?? ""), onChange: (v) => onChange({ liveAgeMonths: v ? Number(v) : undefined }), placeholder: "e.g. 6" })] }), _jsx(FormField, { name: "liveSex", label: "Sex", type: "select", value: values.liveSex ?? "unknown", onChange: (v) => onChange({ liveSex: v }), options: LIVE_SEX_OPTIONS }), _jsx(FormField, { name: "liveCareInfo", label: "Care Instructions (optional)", type: "textarea", value: values.liveCareInfo ?? "", onChange: (v) => onChange({ liveCareInfo: v }), placeholder: "Diet, temperature, habitat requirements\u2026" }), _jsx(FormField, { name: "liveTransportMethod", label: "Transport Method", type: "select", value: values.liveTransportMethod ?? "in-person", onChange: (v) => onChange({ liveTransportMethod: v }), options: LIVE_TRANSPORT_OPTIONS }), values.liveTransportMethod !== "in-person" && (_jsx(FormField, { name: "liveHandlingFee", label: "Handling / Transport Fee (\u20B9, optional)", type: "number", value: toRupeesString(values.liveHandlingFee), onChange: (v) => onChange({ liveHandlingFee: v ? fromRupeesString(v) : undefined }), placeholder: "0" })), _jsx(TagInput, { label: "Jurisdictions where sale is permitted", value: values.liveJurisdictions ?? [], onChange: (liveJurisdictions) => onChange({ liveJurisdictions }), placeholder: "e.g. Maharashtra, Karnataka", helperText: "List all Indian states / UTs where you can legally sell" }), _jsx(Toggle, { checked: !!values.liveCites, onChange: (checked) => onChange({ liveCites: checked }), label: "This species requires CITES documentation" })] }));
|
|
351
352
|
}
|
|
352
353
|
// ── Step: Print Meta (art / stickers) ───────────────────────────────────────
|
|
353
354
|
// Shared by both "art" and "stickers" — identical field set, only the intro
|
|
@@ -41,6 +41,7 @@ import { AssemblyDrawer, } from "../../parts/components/AssemblyDrawer.js";
|
|
|
41
41
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
42
42
|
import { buildBulkAction } from "../../../_internal/shared/actions/bulk-helpers.js";
|
|
43
43
|
import { productImageUrls } from "../../products/schemas/firestore.js";
|
|
44
|
+
import { formatMoney } from "../../../utils/number.formatter.js";
|
|
44
45
|
const FILTER_KEYS = [
|
|
45
46
|
"status",
|
|
46
47
|
"category",
|
|
@@ -520,7 +521,7 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, }) {
|
|
|
520
521
|
const auctionSecondary = kind === "auction"
|
|
521
522
|
? [
|
|
522
523
|
typeof item.reservePrice === "number"
|
|
523
|
-
? `Reserve
|
|
524
|
+
? `Reserve ${formatMoney(item.reservePrice)}`
|
|
524
525
|
: null,
|
|
525
526
|
`${typeof item.bidCount === "number" ? item.bidCount : 0} bids`,
|
|
526
527
|
item.auctionEndDate
|
|
@@ -577,7 +578,7 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, }) {
|
|
|
577
578
|
const availableRaw = typeof item.availableQuantity === "number" ? item.availableQuantity : null;
|
|
578
579
|
const stockRaw = stockOverride ?? (typeof item.stockQuantity === "number" ? item.stockQuantity : null);
|
|
579
580
|
const stockValue = stockOverride ?? availableRaw ?? stockRaw;
|
|
580
|
-
const priceLabel = kind !== "auction" && priceRaw ?
|
|
581
|
+
const priceLabel = kind !== "auction" && priceRaw ? formatMoney(priceRaw) : "";
|
|
581
582
|
const stockLabel = stockRaw === null ? "" : `${stockRaw} in stock`;
|
|
582
583
|
const secondaryBase = auctionSecondary || toStringValue(item.condition, "");
|
|
583
584
|
return {
|
|
@@ -589,7 +590,7 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, }) {
|
|
|
589
590
|
imageUrl: toStringValue(item.mainImage, undefined) || productImageUrls(item.images)[0],
|
|
590
591
|
image: toStringValue(item.mainImage, undefined) || productImageUrls(item.images)[0],
|
|
591
592
|
listingKind: kind,
|
|
592
|
-
price: priceRaw ?
|
|
593
|
+
price: priceRaw ? formatMoney(priceRaw) : "—",
|
|
593
594
|
stock: stockLabel || "—",
|
|
594
595
|
stockValue,
|
|
595
596
|
// Carried through ONLY when the document really has it — see the
|