@mohasinac/appkit 4.69.1 → 4.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/PROVIDERS.md +190 -0
  2. package/dist/.build-stamp.json +3 -3
  3. package/dist/_internal/client/features/lottery/LotteryAdminSlotView.js +2 -1
  4. package/dist/_internal/server/features/checkout/actions.js +43 -8
  5. package/dist/_internal/server/features/orders/actions.js +2 -1
  6. package/dist/_internal/server/features/orders/adapters.js +18 -0
  7. package/dist/_internal/server/features/orders/balance.js +2 -1
  8. package/dist/_internal/server/features/orders/cancellation-refund.d.ts +0 -89
  9. package/dist/_internal/server/features/orders/cancellation-refund.js +3 -2
  10. package/dist/_internal/server/features/payouts/actions.js +2 -1
  11. package/dist/_internal/server/features/points/award.js +3 -2
  12. package/dist/_internal/server/features/protection-claims/actions.js +2 -1
  13. package/dist/_internal/server/features/refunds/actions.js +3 -2
  14. package/dist/_internal/server/features/shipping/courier-block.js +5 -2
  15. package/dist/_internal/server/features/shipping/reconcile.js +3 -2
  16. package/dist/_internal/server/features/shipping/reimbursement.js +3 -2
  17. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  18. package/dist/_internal/server/features/stores/adapters.js +12 -2
  19. package/dist/_internal/server/jobs/core/dailyStatusDigest.js +7 -3
  20. package/dist/_internal/server/jobs/core/emiInstallmentReminder.js +2 -1
  21. package/dist/_internal/server/jobs/core/obligationSweep.js +5 -4
  22. package/dist/_internal/server/jobs/core/onScamReportCreate.js +2 -1
  23. package/dist/_internal/server/jobs/core/paymentReconcile.js +5 -4
  24. package/dist/_internal/shared/checkout/rules/auction.rule.js +25 -7
  25. package/dist/_internal/shared/checkout/rules/prize-draw.rule.js +26 -8
  26. package/dist/_internal/shared/config/schema.d.ts +15 -0
  27. package/dist/_internal/shared/content/published-values.d.ts +2 -2
  28. package/dist/_internal/shared/content/published-values.js +31 -12
  29. package/dist/_internal/shared/features/auctions/errors.js +2 -1
  30. package/dist/_internal/shared/features/orders/refund-copy.js +3 -2
  31. package/dist/_internal/shared/features/promotions/errors.js +2 -1
  32. package/dist/constants/field-names.d.ts +23 -0
  33. package/dist/constants/field-names.js +23 -0
  34. package/dist/constants/storage-keys.d.ts +90 -0
  35. package/dist/constants/storage-keys.js +119 -0
  36. package/dist/contracts/index.d.ts +1 -1
  37. package/dist/contracts/payment.d.ts +42 -0
  38. package/dist/contracts/registry.d.ts +8 -0
  39. package/dist/contracts/tax.d.ts +118 -0
  40. package/dist/contracts/tax.js +3 -0
  41. package/dist/core/brand-resolver.d.ts +92 -0
  42. package/dist/core/brand-resolver.js +120 -0
  43. package/dist/core/brand-text.d.ts +15 -0
  44. package/dist/core/brand-text.js +73 -0
  45. package/dist/core/hooks/useNewsletterSubscribe.d.ts +30 -0
  46. package/dist/core/hooks/useNewsletterSubscribe.js +121 -0
  47. package/dist/features/about/components/DeveloperView.js +2 -1
  48. package/dist/features/about/index.d.ts +0 -2
  49. package/dist/features/about/index.js +0 -1
  50. package/dist/features/account/components/BuyerAccountGuideView.js +2 -1
  51. package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -3
  52. package/dist/features/account/components/BuyerOrdersGuideView.js +6 -5
  53. package/dist/features/account/components/BuyerShoppingGuideView.js +3 -2
  54. package/dist/features/account/components/UserOffersPanel.js +2 -1
  55. package/dist/features/account/hooks/useCollapsedSections.js +5 -3
  56. package/dist/features/account/schemas/index.js +1 -1
  57. package/dist/features/admin/actions/notification-actions.js +9 -2
  58. package/dist/features/admin/components/AdminAnalyticsGuideView.js +2 -1
  59. package/dist/features/admin/components/AdminBundlesView.js +2 -1
  60. package/dist/features/admin/components/AdminCatalogGuideView.js +2 -1
  61. package/dist/features/admin/components/AdminContentGuideView.js +2 -1
  62. package/dist/features/admin/components/AdminFaqEditorView.js +2 -1
  63. package/dist/features/admin/components/AdminGatewayEventsView.js +2 -1
  64. package/dist/features/admin/components/AdminGuideHubView.js +2 -1
  65. package/dist/features/admin/components/AdminMoneyCasesView.js +2 -1
  66. package/dist/features/admin/components/AdminOrdersGuideView.js +2 -1
  67. package/dist/features/admin/components/AdminProductsView.js +3 -2
  68. package/dist/features/admin/components/AdminSectionsView.js +3 -2
  69. package/dist/features/admin/components/AdminSiteSettingsView.js +8 -7
  70. package/dist/features/admin/components/AdminStoresGuideView.js +2 -1
  71. package/dist/features/admin/components/AdminSupportTicketDetailView.js +2 -1
  72. package/dist/features/admin/components/AdminTeamGuideView.js +2 -1
  73. package/dist/features/admin/components/AdminTrustGuideView.js +3 -2
  74. package/dist/features/admin/components/AdminUsersGuideView.js +2 -1
  75. package/dist/features/admin/components/sections/adminSectionsTypes.js +1 -1
  76. package/dist/features/admin/schemas/firestore.js +0 -1
  77. package/dist/features/auth/actions/user-slug.js +21 -2
  78. package/dist/features/auth/hooks/useAuth.js +11 -4
  79. package/dist/features/cart/utils/pending-ops.js +16 -3
  80. package/dist/features/catalogue/utils/product-from-item.d.ts +18 -2
  81. package/dist/features/catalogue/utils/product-from-item.js +23 -4
  82. package/dist/features/categories/components/BundleItemsPicker.js +2 -1
  83. package/dist/features/categories/components/CategoryDetailPageView.js +2 -1
  84. package/dist/features/categories/components/ContextualTrail.d.ts +9 -2
  85. package/dist/features/categories/components/ContextualTrail.js +11 -3
  86. package/dist/features/categories/components/LastCategoryBeacon.js +2 -2
  87. package/dist/features/checkout/actions/checkout-value-otp-actions.js +9 -1
  88. package/dist/features/contact/email.js +11 -12
  89. package/dist/features/email/notification-templates.js +4 -5
  90. package/dist/features/email/render-static-markup.d.ts +34 -0
  91. package/dist/features/email/render-static-markup.js +4 -0
  92. package/dist/features/events/utils/event-charts.js +2 -1
  93. package/dist/features/homepage/components/AnnouncementBar.js +5 -3
  94. package/dist/features/homepage/components/FAQSection.js +1 -1
  95. package/dist/features/homepage/components/MarketplaceHomepageView.js +34 -0
  96. package/dist/features/homepage/components/WelcomeSection.d.ts +3 -1
  97. package/dist/features/homepage/components/WelcomeSection.js +2 -2
  98. package/dist/features/homepage/lib/section-renderer.d.ts +8 -0
  99. package/dist/features/homepage/lib/section-renderer.js +1 -1
  100. package/dist/features/layout/AppLayoutShell.d.ts +3 -1
  101. package/dist/features/layout/AppLayoutShell.js +2 -2
  102. package/dist/features/layout/TitleBarLayout.d.ts +8 -1
  103. package/dist/features/layout/TitleBarLayout.js +2 -2
  104. package/dist/features/media/MediaVideo.js +2 -1
  105. package/dist/features/orders/actions/refund-actions.js +3 -2
  106. package/dist/features/orders/components/RefundRequestView.js +15 -2
  107. package/dist/features/orders/repository/orders.repository.js +2 -1
  108. package/dist/features/orders/types/index.d.ts +14 -0
  109. package/dist/features/orders/utils/payment-proof-message.js +5 -2
  110. package/dist/features/price-index/components/PriceIndexToolView.js +6 -5
  111. package/dist/features/pricing/components/AdminPricingView.js +2 -1
  112. package/dist/features/print/components/labels.js +2 -1
  113. package/dist/features/products/components/CityCollectiblesView.js +2 -1
  114. package/dist/features/products/components/MakeOfferButton.js +2 -1
  115. package/dist/features/products/components/PrizeDrawCollage.js +3 -2
  116. package/dist/features/products/components/PrizeRevealModal.js +2 -1
  117. package/dist/features/products/repository/products.repository.js +2 -1
  118. package/dist/features/promotions/components/CouponCard.js +3 -2
  119. package/dist/features/promotions/components/CouponHelpDetails.js +2 -1
  120. package/dist/features/promotions/constants/coupon-help.d.ts +1 -1
  121. package/dist/features/promotions/constants/coupon-help.js +8 -1
  122. package/dist/features/scams/components/ScamAwarenessModal.js +2 -1
  123. package/dist/features/scams/components/ScamProfileView.js +5 -3
  124. package/dist/features/scams/components/ScamRegistryView.js +2 -1
  125. package/dist/features/scams/constants/scam-types.d.ts +13 -1
  126. package/dist/features/scams/constants/scam-types.js +67 -23
  127. package/dist/features/scams/schemas/firestore.js +1 -1
  128. package/dist/features/search/components/Search.js +5 -1
  129. package/dist/features/seller/actions/offer-actions.js +47 -9
  130. package/dist/features/seller/components/DispatchForm.js +3 -2
  131. package/dist/features/seller/components/PrintCenterView.js +4 -2
  132. package/dist/features/seller/components/SellerBundlesView.js +2 -1
  133. package/dist/features/seller/components/SellerPayoutSettingsView.js +3 -2
  134. package/dist/features/seller/components/SellerProductShell.js +3 -2
  135. package/dist/features/seller/components/SellerProductsView.js +4 -3
  136. package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +30 -4
  137. package/dist/features/store-extensions/repository/store-extensions.repositories.js +46 -7
  138. package/dist/features/store-extensions/schemas/firestore.d.ts +22 -3
  139. package/dist/features/store-extensions/schemas/firestore.js +20 -0
  140. package/dist/features/stores/components/StoreCapabilitiesGuideView.js +7 -6
  141. package/dist/features/stores/components/StoreFinanceGuideView.js +6 -5
  142. package/dist/features/stores/components/StoreGuideHubView.js +2 -1
  143. package/dist/features/stores/components/StoreListingsGuideView.js +2 -1
  144. package/dist/features/stores/components/StoreOrdersGuideView.js +5 -4
  145. package/dist/features/stores/components/StoreSettingsGuideView.js +2 -1
  146. package/dist/features/support/schemas/index.d.ts +2 -2
  147. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +1 -1
  148. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +3 -2
  149. package/dist/features/whatsapp-bot/helpers/whatsapp.js +5 -2
  150. package/dist/index.d.ts +0 -3
  151. package/dist/index.js +0 -6
  152. package/dist/providers/payment-manual/index.d.ts +11 -0
  153. package/dist/providers/payment-manual/index.js +13 -0
  154. package/dist/providers/payment-phonepe/index.d.ts +23 -7
  155. package/dist/providers/payment-phonepe/index.js +15 -0
  156. package/dist/providers/payment-razorpay/index.d.ts +13 -0
  157. package/dist/providers/payment-razorpay/index.js +21 -1
  158. package/dist/providers/tax-india-gst/index.d.ts +45 -0
  159. package/dist/providers/tax-india-gst/index.js +78 -0
  160. package/dist/react/hooks/useScrollToTop.d.ts +5 -0
  161. package/dist/react/hooks/useScrollToTop.js +70 -0
  162. package/dist/schemas/registry.d.ts +2 -2
  163. package/dist/seo/json-ld.js +25 -4
  164. package/dist/styles.css +20 -0
  165. package/dist/ui/components/SiteLogo.d.ts +20 -2
  166. package/dist/ui/components/SiteLogo.js +28 -6
  167. package/dist/ui/components/SiteMark.d.ts +1 -1
  168. package/dist/ui/components/SiteMark.js +21 -6
  169. package/dist/ui/components/Tabs.d.ts +9 -1
  170. package/dist/ui/components/Tabs.js +20 -3
  171. package/dist/ui/components/Tabs.style.css +20 -0
  172. package/dist/ui/rich-text/RichText.d.ts +0 -12
  173. package/dist/ui/rich-text/RichText.js +52 -1
  174. package/dist/utils/date.formatter.d.ts +20 -0
  175. package/dist/utils/date.formatter.js +25 -0
  176. package/dist/utils/number.formatter.d.ts +47 -0
  177. package/dist/utils/number.formatter.js +79 -1
  178. package/dist/validation/phone.validator.d.ts +0 -1
  179. package/dist/validation/phone.validator.js +17 -3
  180. package/package.json +17 -3
  181. package/dist/features/about/components/AboutView.d.ts +0 -30
  182. package/dist/features/about/components/AboutView.js +0 -14
@@ -16,9 +16,11 @@ import { EmptyState } from "../../../ui/components/EmptyState.js";
16
16
  import { Heading, Span, Text } from "../../../ui/components/Typography.js";
17
17
  import { Main, Section } from "../../../ui/components/Semantic.js";
18
18
  import { MediaImage } from "../../media/MediaImage.js";
19
+ import { withBrand } from "../../../core/brand-text.js";
19
20
  import { SCAM_PLATFORM_LABELS, SCAMMER_STATUS_LABELS, SOCIAL_PLATFORM_LABELS, CONTEST_TYPE_LABELS, } from "../schemas/firestore.js";
20
21
  import { SCAM_TYPE_LABELS, getScamType } from "../constants/scam-types.js";
21
22
  import { ROUTES } from "../../../next/routing/route-map.js";
23
+ import { getBrandName } from "../../../core/brand-resolver.js";
22
24
  function formatDate(d) {
23
25
  if (!d)
24
26
  return "";
@@ -42,10 +44,10 @@ function statusVariant(status) {
42
44
  }
43
45
  // ─── Sub-components ───────────────────────────────────────────────────────────
44
46
  function ScammerHeaderCard({ scammer }) {
45
- return (_jsx(Card, { variant: "elevated", padding: "lg", children: _jsxs(Stack, { gap: "md", children: [_jsx(Disclaimer, { id: "scam-registry" }), _jsxs(Row, { justify: "between", gap: "md", align: "start", wrap: true, children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 1, weight: "bold", size: "2xl", children: scammer.displayNames[0] }), scammer.displayNames.length > 1 && (_jsxs(Text, { variant: "secondary", size: "sm", children: ["Also known as: ", scammer.displayNames.slice(1).join(", ")] }))] }), _jsx(Badge, { variant: statusVariant(scammer.status), children: SCAMMER_STATUS_LABELS[scammer.status] ?? scammer.status })] }), _jsxs(Row, { gap: "xs", wrap: true, children: [_jsx(Badge, { variant: "warning", children: SCAM_TYPE_LABELS[scammer.scamType] ?? scammer.scamType }), _jsxs(Badge, { variant: "default", children: ["via ", SCAM_PLATFORM_LABELS[scammer.scamPlatform] ?? scammer.scamPlatform] }), scammer.tags.includes("repeat_offender") && (_jsx(Badge, { variant: "danger", children: "Repeat Offender" }))] }), _jsxs(Row, { gap: "md", wrap: true, children: [_jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Eye, { className: "h-4 w-4" }), scammer.views, " views"] }), _jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Users, { className: "h-4 w-4" }), scammer.incidentCount + 1, " victim", scammer.incidentCount !== 0 ? "s" : "", " reported"] }), _jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Calendar, { className: "h-4 w-4" }), "Reported ", formatDate(scammer.createdAt)] }), scammer.amountLost ? (_jsxs(Text, { className: "text-[color:var(--appkit-color-danger,theme(colors.red.600))]", size: "sm", weight: "medium", children: [formatCurrency(scammer.amountLost), " lost (primary incident)"] })) : null] })] }) }));
47
+ return (_jsx(Card, { variant: "elevated", padding: "lg", children: _jsxs(Stack, { gap: "md", children: [_jsx(Disclaimer, { id: "scam-registry" }), _jsxs(Row, { justify: "between", gap: "md", align: "start", wrap: true, children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 1, weight: "bold", size: "2xl", children: scammer.displayNames[0] }), scammer.displayNames.length > 1 && (_jsxs(Text, { variant: "secondary", size: "sm", children: ["Also known as: ", scammer.displayNames.slice(1).join(", ")] }))] }), _jsx(Badge, { variant: statusVariant(scammer.status), children: SCAMMER_STATUS_LABELS[scammer.status] ?? scammer.status })] }), _jsxs(Row, { gap: "xs", wrap: true, children: [_jsx(Badge, { variant: "warning", children: SCAM_TYPE_LABELS[scammer.scamType] ?? scammer.scamType }), _jsxs(Badge, { variant: "default", children: ["via ", withBrand(SCAM_PLATFORM_LABELS[scammer.scamPlatform] ?? scammer.scamPlatform)] }), scammer.tags.includes("repeat_offender") && (_jsx(Badge, { variant: "danger", children: "Repeat Offender" }))] }), _jsxs(Row, { gap: "md", wrap: true, children: [_jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Eye, { className: "h-4 w-4" }), scammer.views, " views"] }), _jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Users, { className: "h-4 w-4" }), scammer.incidentCount + 1, " victim", scammer.incidentCount !== 0 ? "s" : "", " reported"] }), _jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Calendar, { className: "h-4 w-4" }), "Reported ", formatDate(scammer.createdAt)] }), scammer.amountLost ? (_jsxs(Text, { className: "text-[color:var(--appkit-color-danger,theme(colors.red.600))]", size: "sm", weight: "medium", children: [formatCurrency(scammer.amountLost), " lost (primary incident)"] })) : null] })] }) }));
46
48
  }
47
49
  function ScammerIncidentsSection({ incidents }) {
48
- return (_jsxs(Stack, { gap: "sm", children: [_jsx(Row, { justify: "between", align: "center", children: _jsxs(Heading, { level: 2, size: "base", weight: "semibold", children: ["Additional Incidents", incidents.length > 0 && ` (${incidents.length})`] }) }), incidents.length === 0 ? (_jsx(EmptyState, { icon: _jsx(FileText, { className: "h-8 w-8" }), title: "No additional incidents yet", description: "Other victims' verified reports linked to this profile will appear here." })) : (_jsx(Stack, { gap: "sm", children: incidents.map((inc) => (_jsx(Card, { variant: "outlined", padding: "md", children: _jsxs(Stack, { gap: "xs", children: [_jsxs(Row, { justify: "between", align: "start", gap: "sm", wrap: true, children: [_jsxs(Row, { gap: "xs", wrap: true, children: [_jsx(Badge, { variant: "warning", children: SCAM_TYPE_LABELS[inc.scamType] ?? inc.scamType }), _jsxs(Badge, { variant: "default", children: ["via ", SCAM_PLATFORM_LABELS[inc.scamPlatform] ?? inc.scamPlatform] })] }), _jsx(Text, { variant: "secondary", size: "xs", children: formatDate(inc.createdAt) })] }), inc.itemInvolved && (_jsxs(Text, { variant: "secondary", size: "xs", children: ["Item: ", inc.itemInvolved] })), inc.amountLost ? (_jsxs(Text, { className: "text-[color:var(--appkit-color-danger,theme(colors.red.600))]", size: "xs", weight: "medium", children: [formatCurrency(inc.amountLost), " lost"] })) : null, _jsx(Text, { variant: "secondary", className: "leading-relaxed", size: "sm", children: inc.description.length > 200
50
+ return (_jsxs(Stack, { gap: "sm", children: [_jsx(Row, { justify: "between", align: "center", children: _jsxs(Heading, { level: 2, size: "base", weight: "semibold", children: ["Additional Incidents", incidents.length > 0 && ` (${incidents.length})`] }) }), incidents.length === 0 ? (_jsx(EmptyState, { icon: _jsx(FileText, { className: "h-8 w-8" }), title: "No additional incidents yet", description: "Other victims' verified reports linked to this profile will appear here." })) : (_jsx(Stack, { gap: "sm", children: incidents.map((inc) => (_jsx(Card, { variant: "outlined", padding: "md", children: _jsxs(Stack, { gap: "xs", children: [_jsxs(Row, { justify: "between", align: "start", gap: "sm", wrap: true, children: [_jsxs(Row, { gap: "xs", wrap: true, children: [_jsx(Badge, { variant: "warning", children: SCAM_TYPE_LABELS[inc.scamType] ?? inc.scamType }), _jsxs(Badge, { variant: "default", children: ["via ", withBrand(SCAM_PLATFORM_LABELS[inc.scamPlatform] ?? inc.scamPlatform)] })] }), _jsx(Text, { variant: "secondary", size: "xs", children: formatDate(inc.createdAt) })] }), inc.itemInvolved && (_jsxs(Text, { variant: "secondary", size: "xs", children: ["Item: ", inc.itemInvolved] })), inc.amountLost ? (_jsxs(Text, { className: "text-[color:var(--appkit-color-danger,theme(colors.red.600))]", size: "xs", weight: "medium", children: [formatCurrency(inc.amountLost), " lost"] })) : null, _jsx(Text, { variant: "secondary", className: "leading-relaxed", size: "sm", children: inc.description.length > 200
49
51
  ? `${inc.description.slice(0, 200).trimEnd()}…`
50
52
  : inc.description }), _jsxs(Text, { variant: "secondary", size: "xs", children: ["Reported by: ", inc.reportedByAnon ? "Anonymous" : "Verified victim"] })] }) }, inc.id))) }))] }));
51
53
  }
@@ -54,7 +56,7 @@ function ScammerCommentsSection({ comments, isAuthenticated, scammerId, }) {
54
56
  return (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Heading, { level: 2, size: "base", weight: "semibold", children: ["Community Discussion", comments.length > 0 && ` (${comments.length})`] }), isAuthenticated ? (_jsx(Link, { href: String(ROUTES.PUBLIC.SCAM_REPORT), className: "text-[length:var(--appkit-text-xs)] text-[color:var(--appkit-color-primary,theme(colors.blue.600))] hover:underline", children: "Leave a comment" })) : (_jsx(Link, { href: commentLoginHref, className: "text-[length:var(--appkit-text-xs)] text-[color:var(--appkit-color-primary,theme(colors.blue.600))] hover:underline", children: "Sign in to comment" }))] }), comments.length === 0 ? (_jsx(EmptyState, { icon: _jsx(MessageSquare, { className: "h-8 w-8" }), title: "No comments yet", description: "Community comments and victim testimonials appear here." })) : (_jsx(Stack, { gap: "sm", children: comments.map((c) => (_jsx(Card, { variant: "flat", padding: "md", children: _jsxs(Stack, { gap: "xs", children: [_jsxs(Row, { justify: "between", align: "center", gap: "sm", children: [_jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Text, { size: "sm", weight: "medium", children: c.authorDisplayName }), c.authorRole !== "user" && (_jsx(Badge, { variant: "default", className: "text-[10px]", children: c.authorRole })), c.isAccused && c.isAccusedVerified && (_jsx(Badge, { variant: "warning", className: "text-[10px]", children: "Accused" })), c.isVerifiedVictim && (_jsx(Badge, { variant: "success", className: "text-[10px]", children: "Verified Victim" }))] }), _jsx(Text, { variant: "secondary", size: "xs", children: formatDate(c.createdAt) })] }), _jsx(Text, { variant: "secondary", className: "leading-relaxed", size: "sm", children: c.body })] }) }, c.id))) }))] }));
55
57
  }
56
58
  function ScammerActionsColumn({ scammer, isAuthenticated, reportHref, contestLoginHref, }) {
57
- return (_jsxs(Stack, { gap: "md", children: [_jsxs(Card, { variant: "elevated", padding: "md", children: [_jsx(CardHeader, { children: _jsx(Heading, { level: 3, className: "tracking-wide", size: "sm", weight: "semibold", transform: "uppercase", children: "Actions" }) }), _jsx(CardBody, { children: _jsxs(Stack, { gap: "sm", children: [isAuthenticated ? (_jsxs(Link, { href: reportHref, className: CLS_OUTLINE_BTN, children: [_jsx(AlertOctagon, { className: "h-4 w-4 text-[color:var(--appkit-color-danger,theme(colors.red.500))]" }), "Report another incident"] })) : (_jsxs(Link, { href: `${String(ROUTES.AUTH.LOGIN)}?redirect=${encodeURIComponent(reportHref)}`, className: CLS_OUTLINE_BTN, children: [_jsx(AlertOctagon, { className: "h-4 w-4 text-[color:var(--appkit-color-danger,theme(colors.red.500))]" }), "Sign in to report an incident"] })), isAuthenticated ? (_jsxs(Link, { href: `/scams/${scammer.id}/contest`, className: CLS_OUTLINE_BTN, children: [_jsx(Flag, { className: "h-4 w-4 text-[color:var(--appkit-color-warning,theme(colors.amber.500))]" }), "Contest this profile"] })) : (_jsxs(Link, { href: contestLoginHref, className: "appkit-button appkit-button--ghost appkit-button--md flex w-full items-center gap-[var(--appkit-space-2)]", children: [_jsx(Flag, { className: "h-4 w-4" }), "Sign in to contest"] })), _jsx(Text, { variant: "secondary", size: "xs", children: "All contest submissions are reviewed by our moderation team before any changes are made." })] }) })] }), _jsxs(Card, { variant: "outlined", padding: "md", children: [_jsx(CardHeader, { children: _jsx(Heading, { level: 3, className: "tracking-wide", size: "sm", weight: "semibold", transform: "uppercase", children: "Contest Options" }) }), _jsx(CardBody, { children: _jsx(Stack, { gap: "xs", as: "ul", children: Object.entries(CONTEST_TYPE_LABELS).map(([, label]) => (_jsxs(Row, { gap: "sm", align: "start", as: "li", children: [_jsx(Span, { className: "mt-1.5 h-1.5 w-1.5 shrink-0 bg-[color:var(--appkit-color-border,theme(colors.zinc.300))]", rounded: "full" }), _jsx(Text, { variant: "secondary", size: "xs", children: label })] }, label))) }) })] }), scammer.verifiedAt && (_jsx(Alert, { variant: "success", compact: true, children: _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Shield, { className: "h-3.5 w-3.5 shrink-0" }), _jsxs(Text, { size: "xs", weight: "medium", children: ["Verified by LetItRip moderation on ", formatDate(scammer.verifiedAt)] })] }) }))] }));
59
+ return (_jsxs(Stack, { gap: "md", children: [_jsxs(Card, { variant: "elevated", padding: "md", children: [_jsx(CardHeader, { children: _jsx(Heading, { level: 3, className: "tracking-wide", size: "sm", weight: "semibold", transform: "uppercase", children: "Actions" }) }), _jsx(CardBody, { children: _jsxs(Stack, { gap: "sm", children: [isAuthenticated ? (_jsxs(Link, { href: reportHref, className: CLS_OUTLINE_BTN, children: [_jsx(AlertOctagon, { className: "h-4 w-4 text-[color:var(--appkit-color-danger,theme(colors.red.500))]" }), "Report another incident"] })) : (_jsxs(Link, { href: `${String(ROUTES.AUTH.LOGIN)}?redirect=${encodeURIComponent(reportHref)}`, className: CLS_OUTLINE_BTN, children: [_jsx(AlertOctagon, { className: "h-4 w-4 text-[color:var(--appkit-color-danger,theme(colors.red.500))]" }), "Sign in to report an incident"] })), isAuthenticated ? (_jsxs(Link, { href: `/scams/${scammer.id}/contest`, className: CLS_OUTLINE_BTN, children: [_jsx(Flag, { className: "h-4 w-4 text-[color:var(--appkit-color-warning,theme(colors.amber.500))]" }), "Contest this profile"] })) : (_jsxs(Link, { href: contestLoginHref, className: "appkit-button appkit-button--ghost appkit-button--md flex w-full items-center gap-[var(--appkit-space-2)]", children: [_jsx(Flag, { className: "h-4 w-4" }), "Sign in to contest"] })), _jsx(Text, { variant: "secondary", size: "xs", children: "All contest submissions are reviewed by our moderation team before any changes are made." })] }) })] }), _jsxs(Card, { variant: "outlined", padding: "md", children: [_jsx(CardHeader, { children: _jsx(Heading, { level: 3, className: "tracking-wide", size: "sm", weight: "semibold", transform: "uppercase", children: "Contest Options" }) }), _jsx(CardBody, { children: _jsx(Stack, { gap: "xs", as: "ul", children: Object.entries(CONTEST_TYPE_LABELS).map(([, label]) => (_jsxs(Row, { gap: "sm", align: "start", as: "li", children: [_jsx(Span, { className: "mt-1.5 h-1.5 w-1.5 shrink-0 bg-[color:var(--appkit-color-border,theme(colors.zinc.300))]", rounded: "full" }), _jsx(Text, { variant: "secondary", size: "xs", children: label })] }, label))) }) })] }), scammer.verifiedAt && (_jsx(Alert, { variant: "success", compact: true, children: _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Shield, { className: "h-3.5 w-3.5 shrink-0" }), _jsxs(Text, { size: "xs", weight: "medium", children: ["Verified by ", getBrandName(), " moderation on ", formatDate(scammer.verifiedAt)] })] }) }))] }));
58
60
  }
59
61
  export function ScamProfileView({ scammer, isAuthenticated, incidents = [], comments = [], relatedScammers = [], similarScamReports = [], }) {
60
62
  const reportHref = String(ROUTES.PUBLIC.SCAM_REPORT);
@@ -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
- /** Get a scam type definition by its ID. */
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, LetItRip support, or a trusted community member to steal your money or credentials.",
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 LetItRip, eBay, and local Facebook groups before agreeing to any price.",
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 LetItRip's on-platform messaging so all communications are recorded.",
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 LetItRip hologram sticker.",
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 LetItRip's order flow — payment is held by the platform until delivery is confirmed.",
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 LetItRip account before shipping.",
219
- "On LetItRip's platform, all transactions are logged — dispute resolution can use platform records.",
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
- "LetItRip preorders require store verification — prefer platform transactions over off-platform deals.",
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 LetItRip-verified stores.",
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 LetItRip, orders past their expected date can be escalated to admin — do not wait.",
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 LetItRip, 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
+ 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 LetItRip, their review history, and how long they've been active.",
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 LetItRip's order flow — off-platform payments have no protection.",
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 LetItRip support via WhatsApp/DM and requests OTP, bank details, or login credentials.",
295
- howItHappens: "You receive a message from 'LetItRip 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'.",
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
- "LetItRip support will NEVER ask for your password, OTP, or bank account details via WhatsApp.",
298
- "Official LetItRip communication comes only from @letitrip.in email addresses.",
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 LetItRip's platform for escrow protection — never third-party escrow services you haven't independently verified.",
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 LetItRip support.",
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
- /** Get a scam type definition by its ID. */
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
- return SCAM_TYPES.find((t) => t.id === id);
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
- return SCAM_CATEGORIES.find((c) => c.id === type.category);
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: "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", storageKey = "letitrip_search_type", searchIndexEnabled, }) {
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 ₹${bounds.max}.`);
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 ₹${bounds.min} (${product.minOfferPercent ?? DEFAULT_MIN_OFFER_PERCENT}% of listing price).`);
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 ₹${product.price}.`
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 ₹${offerAmount} on "${product.title}"`,
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 ₹${offer.offerAmount} on "${offer.productTitle}" was accepted! Complete checkout now.`
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
- : `${offer.storeName} countered with ₹${counterAmount} on "${offer.productTitle}".`;
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 ₹${offer.counterAmount} on "${offer.productTitle}". They can now checkout.`,
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: ₹${minAllowed}–₹${maxAllowed}.`);
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 ₹${counterAmount} on "${offer.productTitle}".`,
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 LetItRip · ${trackingNumber}`
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" }), _jsx(Button, { size: "sm", variant: method === "platform" ? "primary" : "outline", "aria-pressed": method === "platform", onClick: () => setMethod("platform"), children: "Book through LetItRip" })] }), _jsx(Form, { onSubmit: (e) => e.preventDefault(), children: ({ setFieldError, clearErrors }) => {
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] }), _jsxs(Text, { size: "xs", color: "muted", children: ["\u20B9", item.price.toLocaleString("en-IN")] })] })] }, i))) })] }));
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 = "LetItRip", initialProducts = [], initialOrders = [], }) {
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 `₹${value.toLocaleString("en-IN")}`;
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 LetItRip's (resolveSupplierIdentity).
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" }), _jsx(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 LetItRip'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
+ 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",