@mohasinac/appkit 4.63.4 → 4.64.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 (232) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/server/features/categories/adapters.d.ts +1 -1
  3. package/dist/_internal/server/features/categories/adapters.js +13 -0
  4. package/dist/_internal/server/features/checkout/actions.d.ts +2 -0
  5. package/dist/_internal/server/features/checkout/actions.js +22 -5
  6. package/dist/_internal/server/features/events/like-actions.d.ts +25 -0
  7. package/dist/_internal/server/features/events/like-actions.js +67 -0
  8. package/dist/_internal/server/features/events/likes.d.ts +65 -0
  9. package/dist/_internal/server/features/events/likes.js +146 -0
  10. package/dist/_internal/server/features/orders/adapters.js +3 -1
  11. package/dist/_internal/server/features/seo/index.d.ts +3 -0
  12. package/dist/_internal/server/features/seo/index.js +2 -0
  13. package/dist/_internal/server/features/seo/product-detail-types.d.ts +1 -0
  14. package/dist/_internal/server/features/seo/product-detail-types.js +26 -0
  15. package/dist/_internal/server/features/seo/robots.js +20 -1
  16. package/dist/_internal/server/features/seo/sitemap.js +125 -27
  17. package/dist/_internal/server/features/seo/static-params.d.ts +107 -0
  18. package/dist/_internal/server/features/seo/static-params.js +228 -0
  19. package/dist/_internal/server/features/seo/taxonomy-links.d.ts +44 -0
  20. package/dist/_internal/server/features/seo/taxonomy-links.js +85 -0
  21. package/dist/_internal/server/features/site-settings/adapters.js +20 -2
  22. package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
  23. package/dist/_internal/server/features/stores/adapters.js +88 -1
  24. package/dist/_internal/server/functions/scheduled.d.ts +16 -1
  25. package/dist/_internal/server/functions/scheduled.js +23 -1
  26. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +5 -2
  27. package/dist/_internal/server/jobs/core/jobRunners.js +5 -0
  28. package/dist/_internal/server/jobs/core/onOrderCreate.js +104 -10
  29. package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +2 -0
  30. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +1 -0
  31. package/dist/_internal/server/jobs/core/onScamReportCreate.js +1 -0
  32. package/dist/_internal/server/jobs/core/onScamReportRejected.js +1 -0
  33. package/dist/_internal/server/jobs/core/onScamReportVerified.js +1 -0
  34. package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +1 -0
  35. package/dist/_internal/server/jobs/core/pendingOrderTimeout.js +1 -0
  36. package/dist/_internal/server/jobs/core/sellerStandingRollup.d.ts +63 -0
  37. package/dist/_internal/server/jobs/core/sellerStandingRollup.js +412 -0
  38. package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +35 -0
  39. package/dist/_internal/server/jobs/core/whatsappNotify.js +140 -0
  40. package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
  41. package/dist/_internal/server/jobs/handlers/index.js +1 -0
  42. package/dist/_internal/server/jobs/handlers/sellerStandingRollup.d.ts +12 -0
  43. package/dist/_internal/server/jobs/handlers/sellerStandingRollup.js +14 -0
  44. package/dist/_internal/server/notifications/channel-health.d.ts +8 -6
  45. package/dist/_internal/server/notifications/channel-health.js +2 -2
  46. package/dist/_internal/server/notifications/send-guard.d.ts +1 -1
  47. package/dist/_internal/server/notifications/send-guard.js +4 -2
  48. package/dist/_internal/shared/actions/action-registry.js +89 -0
  49. package/dist/_internal/shared/checkout/order-math.d.ts +1 -1
  50. package/dist/_internal/shared/checkout/order-math.js +1 -1
  51. package/dist/_internal/shared/features/checkout/config.d.ts +2 -0
  52. package/dist/_internal/shared/features/checkout/config.js +2 -0
  53. package/dist/_internal/shared/features/messaging/config.d.ts +5 -7
  54. package/dist/_internal/shared/features/messaging/config.js +4 -1
  55. package/dist/_internal/shared/features/seller-standing/config.d.ts +130 -0
  56. package/dist/_internal/shared/features/seller-standing/config.js +153 -0
  57. package/dist/_internal/shared/features/site-settings/fees.d.ts +2 -0
  58. package/dist/_internal/shared/features/site-settings/fees.js +2 -0
  59. package/dist/_internal/shared/features/site-settings/types.d.ts +27 -2
  60. package/dist/_internal/shared/fees/calculator.d.ts +8 -0
  61. package/dist/_internal/shared/fees/calculator.js +7 -0
  62. package/dist/cli/index.js +2 -0
  63. package/dist/client.d.ts +2 -2
  64. package/dist/client.js +1 -1
  65. package/dist/constants/api-endpoints.d.ts +39 -0
  66. package/dist/constants/api-endpoints.js +16 -0
  67. package/dist/constants/notification-types.d.ts +1 -1
  68. package/dist/constants/notification-types.js +1 -1
  69. package/dist/core/integration-keys.d.ts +5 -0
  70. package/dist/core/integration-keys.js +10 -0
  71. package/dist/features/about/components/FeesView.js +6 -0
  72. package/dist/features/about/components/HelpPageView.js +9 -1
  73. package/dist/features/account/components/BecomeSellerView.d.ts +15 -3
  74. package/dist/features/account/components/BecomeSellerView.js +2 -2
  75. package/dist/features/account/components/BuyerShoppingGuideView.js +1 -1
  76. package/dist/features/account/components/NotificationPreferencesPanel.js +4 -3
  77. package/dist/features/account/types/index.d.ts +2 -0
  78. package/dist/features/admin/actions/notification-actions.d.ts +17 -2
  79. package/dist/features/admin/actions/notification-actions.js +154 -9
  80. package/dist/features/admin/components/AdminOrderEditorView.d.ts +1 -1
  81. package/dist/features/admin/components/AdminOrdersView.js +2 -0
  82. package/dist/features/admin/components/AdminSiteSettingsView.js +66 -6
  83. package/dist/features/admin/components/AdminStoreEditorView.d.ts +12 -1
  84. package/dist/features/admin/components/AdminStoreEditorView.js +31 -2
  85. package/dist/features/admin/components/AdminStoresGuideView.js +1 -1
  86. package/dist/features/admin/components/AdminStoresView.js +59 -2
  87. package/dist/features/admin/components/AdminTrustGuideView.js +10 -0
  88. package/dist/features/admin/constants/site-settings-tabs.d.ts +12 -7
  89. package/dist/features/admin/constants/site-settings-tabs.js +12 -6
  90. package/dist/features/admin/repository/site-settings.repository.js +1 -1
  91. package/dist/features/admin/schemas/admin-editor-forms.d.ts +10 -0
  92. package/dist/features/admin/schemas/admin-editor-forms.js +34 -0
  93. package/dist/features/admin/schemas/admin-user-form.d.ts +61 -0
  94. package/dist/features/admin/schemas/admin-user-form.js +44 -3
  95. package/dist/features/admin/schemas/firestore.d.ts +73 -0
  96. package/dist/features/admin/schemas/firestore.js +8 -3
  97. package/dist/features/auth/checkout-value-otp.d.ts +1 -1
  98. package/dist/features/auth/checkout-value-otp.js +1 -1
  99. package/dist/features/auth/permissions/constants.d.ts +2 -2
  100. package/dist/features/auth/permissions/constants.js +2 -1
  101. package/dist/features/blog/components/BlogIndexPageView.js +2 -2
  102. package/dist/features/blog/components/BlogListView.js +2 -1
  103. package/dist/features/blog/components/BlogPostView.js +2 -1
  104. package/dist/features/blog/constants/category-badge.d.ts +33 -0
  105. package/dist/features/blog/constants/category-badge.js +40 -0
  106. package/dist/features/cart/components/CartPriceBreakdown.d.ts +2 -0
  107. package/dist/features/cart/components/CartPriceBreakdown.js +1 -1
  108. package/dist/features/cart/components/StoreAddonsPicker.d.ts +5 -2
  109. package/dist/features/cart/components/StoreAddonsPicker.js +5 -4
  110. package/dist/features/cart/repository/cart.repository.js +2 -1
  111. package/dist/features/cart/schemas/firestore.d.ts +2 -1
  112. package/dist/features/categories/components/TaxonomyLinkIndex.d.ts +39 -0
  113. package/dist/features/categories/components/TaxonomyLinkIndex.js +11 -0
  114. package/dist/features/categories/schemas/firestore.d.ts +25 -0
  115. package/dist/features/categories/utils/load-category-options.d.ts +33 -0
  116. package/dist/features/categories/utils/load-category-options.js +50 -0
  117. package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +5 -10
  118. package/dist/features/checkout/actions/checkout-value-otp-actions.js +88 -2
  119. package/dist/features/events/components/EventCard.js +4 -12
  120. package/dist/features/events/components/EventInsights.d.ts +27 -0
  121. package/dist/features/events/components/EventInsights.js +10 -0
  122. package/dist/features/events/components/EventLikeButton.d.ts +21 -0
  123. package/dist/features/events/components/EventLikeButton.js +59 -0
  124. package/dist/features/events/components/EventLikersRail.d.ts +23 -0
  125. package/dist/features/events/components/EventLikersRail.js +54 -0
  126. package/dist/features/events/components/EventTypeBadge.d.ts +26 -0
  127. package/dist/features/events/components/EventTypeBadge.js +7 -0
  128. package/dist/features/events/components/EventsListPageView.js +2 -2
  129. package/dist/features/events/components/SpinWheel.d.ts +41 -0
  130. package/dist/features/events/components/SpinWheel.js +128 -0
  131. package/dist/features/events/components/SpinWheelView.js +36 -15
  132. package/dist/features/events/constants/event-type-meta.d.ts +54 -0
  133. package/dist/features/events/constants/event-type-meta.js +109 -0
  134. package/dist/features/events/repository/event-likes.repository.d.ts +57 -0
  135. package/dist/features/events/repository/event-likes.repository.js +151 -0
  136. package/dist/features/events/schemas/firestore.d.ts +111 -0
  137. package/dist/features/events/schemas/likes.d.ts +81 -0
  138. package/dist/features/events/schemas/likes.js +59 -0
  139. package/dist/features/events/types/index.d.ts +7 -0
  140. package/dist/features/events/utils/carousel-slide.d.ts +42 -0
  141. package/dist/features/events/utils/carousel-slide.js +174 -0
  142. package/dist/features/events/utils/event-charts.d.ts +65 -0
  143. package/dist/features/events/utils/event-charts.js +172 -0
  144. package/dist/features/faq/constants/help-guides.js +9 -0
  145. package/dist/features/faq/schemas/firestore.js +1 -0
  146. package/dist/features/faq/types/index.d.ts +19 -0
  147. package/dist/features/homepage/components/BrandsSection.js +25 -1
  148. package/dist/features/homepage/components/HeroCarousel.js +47 -10
  149. package/dist/features/homepage/lib/section-renderer.js +1 -1
  150. package/dist/features/homepage/types/index.d.ts +7 -0
  151. package/dist/features/orders/actions/refund-actions.js +1 -0
  152. package/dist/features/orders/components/OrderAddonBadges.d.ts +3 -1
  153. package/dist/features/orders/components/OrderAddonBadges.js +4 -3
  154. package/dist/features/orders/repository/orders.repository.d.ts +8 -0
  155. package/dist/features/orders/repository/orders.repository.js +2 -0
  156. package/dist/features/orders/schemas/firestore.d.ts +4 -0
  157. package/dist/features/orders/types/index.d.ts +5 -2
  158. package/dist/features/search/action-index/settings-entries.js +9 -0
  159. package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +6 -1
  160. package/dist/features/store-extensions/repository/store-extensions.repositories.js +10 -1
  161. package/dist/features/store-extensions/schemas/firestore.d.ts +17 -1
  162. package/dist/features/store-extensions/schemas/firestore.js +10 -0
  163. package/dist/features/stores/components/InteractiveStoreCard.js +2 -1
  164. package/dist/features/stores/components/SellerStandingPanel.d.ts +31 -0
  165. package/dist/features/stores/components/SellerStandingPanel.js +25 -0
  166. package/dist/features/stores/components/SellerTierBadge.d.ts +57 -0
  167. package/dist/features/stores/components/SellerTierBadge.js +19 -0
  168. package/dist/features/stores/components/StoreCapabilitiesGuideView.js +33 -2
  169. package/dist/features/stores/components/StoreCreateForm.d.ts +46 -0
  170. package/dist/features/stores/components/StoreCreateForm.js +107 -0
  171. package/dist/features/stores/components/StoreGuideHubView.js +18 -5
  172. package/dist/features/stores/components/StoreHeader.js +2 -1
  173. package/dist/features/stores/components/StoreSettingsGuideView.js +6 -2
  174. package/dist/features/stores/repository/store.repository.d.ts +23 -1
  175. package/dist/features/stores/repository/store.repository.js +52 -1
  176. package/dist/features/stores/schemas/firestore.d.ts +76 -7
  177. package/dist/features/stores/schemas/firestore.js +21 -6
  178. package/dist/features/stores/schemas/index.d.ts +88 -2
  179. package/dist/features/stores/schemas/index.js +14 -0
  180. package/dist/features/stores/schemas/store-create.d.ts +47 -0
  181. package/dist/features/stores/schemas/store-create.js +86 -0
  182. package/dist/features/stores/types/index.d.ts +42 -0
  183. package/dist/features/whatsapp-bot/api/send-status/route.d.ts +12 -0
  184. package/dist/features/whatsapp-bot/api/send-status/route.js +38 -0
  185. package/dist/features/whatsapp-bot/api/webhook/route.d.ts +12 -0
  186. package/dist/features/whatsapp-bot/api/webhook/route.js +43 -0
  187. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +57 -0
  188. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +76 -0
  189. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.d.ts +9 -0
  190. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +7 -0
  191. package/dist/features/whatsapp-bot/components/index.d.ts +3 -0
  192. package/dist/features/whatsapp-bot/components/index.js +2 -0
  193. package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +52 -0
  194. package/dist/features/whatsapp-bot/helpers/whatsapp.js +288 -0
  195. package/dist/features/whatsapp-bot/index.d.ts +3 -0
  196. package/dist/features/whatsapp-bot/index.js +3 -0
  197. package/dist/features/whatsapp-bot/manifest.d.ts +2 -0
  198. package/dist/features/whatsapp-bot/manifest.js +13 -0
  199. package/dist/features/whatsapp-bot/messages/en.json +7 -0
  200. package/dist/features/whatsapp-bot/server.d.ts +7 -0
  201. package/dist/features/whatsapp-bot/server.js +7 -0
  202. package/dist/features/whatsapp-bot/types/index.d.ts +126 -0
  203. package/dist/features/whatsapp-bot/types/index.js +1 -0
  204. package/dist/index.d.ts +23 -6
  205. package/dist/index.js +38 -12
  206. package/dist/next/routing/route-map.d.ts +6 -1
  207. package/dist/next/routing/route-map.js +14 -0
  208. package/dist/providers/db-firebase/base.repository.d.ts +1 -1
  209. package/dist/providers/db-firebase/base.repository.js +1 -1
  210. package/dist/repositories/index.d.ts +1 -1
  211. package/dist/repositories/index.js +1 -1
  212. package/dist/schemas/registry.d.ts +115 -0
  213. package/dist/schemas/registry.js +2 -1
  214. package/dist/security/pii-encrypt.js +1 -1
  215. package/dist/server.d.ts +15 -2
  216. package/dist/server.js +32 -2
  217. package/dist/styles.css +36 -1
  218. package/dist/tailwind-utilities.css +1 -1
  219. package/dist/ui/components/HorizontalScroller.js +15 -2
  220. package/dist/ui/components/NavigationLoader.d.ts +14 -1
  221. package/dist/ui/components/NavigationLoader.js +79 -30
  222. package/dist/ui/components/PageHeroHeading.d.ts +40 -0
  223. package/dist/ui/components/PageHeroHeading.js +15 -0
  224. package/dist/ui/components/Typography.d.ts +3 -3
  225. package/dist/ui/components/Typography.js +59 -31
  226. package/dist/ui/components/Typography.style.css +35 -0
  227. package/dist/ui/index.d.ts +2 -0
  228. package/dist/ui/index.js +1 -0
  229. package/package.json +1 -1
  230. package/scripts/seed-cli.mjs +4 -2
  231. package/dist/features/orders/utils/order-announcement-message.d.ts +0 -18
  232. package/dist/features/orders/utils/order-announcement-message.js +0 -16
@@ -78,6 +78,7 @@ const CAPABILITY_META = {
78
78
  lower_commission_rate: { group: CAP_GROUP.PLATFORM, label: "Lower Commission Rate" },
79
79
  early_access_features: { group: CAP_GROUP.PLATFORM, label: "Early Access Features" },
80
80
  advanced_analytics: { group: CAP_GROUP.PLATFORM, label: "Advanced Analytics" },
81
+ whatsapp_catalog_sync: { group: CAP_GROUP.PLATFORM, label: "WhatsApp Catalog Sync" },
81
82
  };
82
83
  const CAPABILITY_GROUP_ORDER = [
83
84
  CAP_GROUP.LISTINGS,
@@ -110,13 +111,15 @@ function CapabilityPicker({ selected, onToggle, }) {
110
111
  return (_jsxs(Stack, { gap: "sm", children: [_jsxs(Span, { size: "sm", weight: "medium", color: "muted", children: ["Capabilities", _jsxs(Span, { size: "xs", weight: "normal", className: "ml-2", color: "muted", children: ["(", selected.size, " active)"] })] }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)]", rounded: "xl", border: "default", children: CAPABILITY_GROUPS.map((group) => (_jsxs(Details, { className: "group", children: [_jsxs(Summary, { paddingX: "x-sm", paddingY: "y-xs", weight: "semibold", color: "muted", layout: "flex", align: "center", justify: "between", className: "uppercase tracking-wide hover:bg-[var(--appkit-color-bg)] transition-colors", children: [_jsx(Span, { size: "xs", children: group.label }), _jsxs(Span, { size: "xs", weight: "normal", className: "normal-case", color: "faint", children: [group.caps.filter((c) => selected.has(c.key)).length, "/", group.caps.length] })] }), _jsx(Div, { layout: "grid", paddingY: "y-xs-tall", className: "grid-cols-2 gap-x-2 gap-y-1.5", surface: "muted", padding: "x-sm", children: group.caps.map((cap) => (_jsx(Checkbox, { label: cap.label, checked: selected.has(cap.key), onChange: () => onToggle(cap.key), wrapperClassName: "flex items-center gap-[var(--appkit-space-2)] cursor-pointer text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)]" }, cap.key))) })] }, group.label))) })] }));
111
112
  }
112
113
  // --- Component ---------------------------------------------------------------
113
- export function AdminStoreEditorView({ open, onClose, storeId, storeName, currentStatus, currentIsVerified, currentIsFeatured, currentCapabilities, currentAdminNotes, }) {
114
+ export function AdminStoreEditorView({ open, onClose, storeId, storeName, currentStatus, currentIsVerified, currentIsFeatured, currentSellerTier, currentTrustedSeller, currentCapabilities, currentAdminNotes, }) {
114
115
  const queryClient = useQueryClient();
115
116
  const { showToast } = useToast();
116
117
  const [values, setValues] = React.useState(() => ({
117
118
  storeStatus: currentStatus ?? "pending",
118
119
  isVerified: currentIsVerified ?? false,
119
120
  isFeatured: currentIsFeatured ?? false,
121
+ sellerTier: currentSellerTier ?? "seller",
122
+ trustedSeller: currentTrustedSeller ?? false,
120
123
  suspensionReason: "",
121
124
  capabilities: currentCapabilities ?? DEFAULT_CAPABILITIES,
122
125
  adminNotes: currentAdminNotes ?? "",
@@ -131,11 +134,22 @@ export function AdminStoreEditorView({ open, onClose, storeId, storeName, curren
131
134
  storeStatus: currentStatus ?? "pending",
132
135
  isVerified: currentIsVerified ?? false,
133
136
  isFeatured: currentIsFeatured ?? false,
137
+ sellerTier: currentSellerTier ?? "seller",
138
+ trustedSeller: currentTrustedSeller ?? false,
134
139
  suspensionReason: "",
135
140
  capabilities: currentCapabilities ?? DEFAULT_CAPABILITIES,
136
141
  adminNotes: currentAdminNotes ?? "",
137
142
  });
138
- }, [open, currentStatus, currentIsVerified, currentIsFeatured, currentCapabilities, currentAdminNotes]);
143
+ }, [
144
+ open,
145
+ currentStatus,
146
+ currentIsVerified,
147
+ currentIsFeatured,
148
+ currentSellerTier,
149
+ currentTrustedSeller,
150
+ currentCapabilities,
151
+ currentAdminNotes,
152
+ ]);
139
153
  const saveMutation = useApiMutation({
140
154
  errorMessage: "Failed to update store.",
141
155
  mutationFn: async () => {
@@ -157,6 +171,21 @@ export function AdminStoreEditorView({ open, onClose, storeId, storeName, curren
157
171
  adminNotes: parsed.adminNotes ?? "",
158
172
  isFeatured: parsed.isFeatured,
159
173
  isVerified: parsed.isVerified,
174
+ /*
175
+ * 🛑 `sellerTier` is sent ONLY when the admin actually moved it away
176
+ * from the stored value, because the route treats any `sellerTier` in
177
+ * the body as a deliberate override and PINS it against the monthly
178
+ * recompute. Sending it unconditionally would lock every store an
179
+ * admin ever opened and saved — quietly freezing the automatic tier
180
+ * for the whole catalogue, one unrelated edit at a time.
181
+ *
182
+ * `trustedSeller` has no such side effect and is sent every time, so
183
+ * it can also be cleared.
184
+ */
185
+ ...(parsed.sellerTier !== undefined && parsed.sellerTier !== (currentSellerTier ?? "seller")
186
+ ? { sellerTier: parsed.sellerTier }
187
+ : {}),
188
+ trustedSeller: parsed.trustedSeller ?? false,
160
189
  suspensionReason: parsed.suspensionReason || undefined,
161
190
  capabilities: parsed.capabilities ?? [],
162
191
  });
@@ -6,7 +6,7 @@ export function AdminStoresGuideView() {
6
6
  return (_jsxs(Stack, { className: "max-w-3xl mx-auto", padding: "b-2xl", gap: "xl", children: [_jsxs(Section, { children: [_jsxs(Row, { className: "mb-2", align: "center", gap: "3", children: [_jsx(Row, { className: "flex-shrink-0 w-10 h-10 [background:linear-gradient(135deg,var(--appkit-color-primary-700)_0%,var(--appkit-color-cobalt)_100%)]", align: "center", justify: "center", rounded: "xl", children: _jsx(Store, { className: "w-5 h-5 text-white" }) }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] tracking-widest", size: "sm", weight: "semibold", transform: "uppercase", children: "Admin Guide" })] }), _jsx(Heading, { level: 1, className: "text-[var(--appkit-color-text)] mb-2", mdSize: "3xl", size: "2xl", weight: "bold", children: "Stores & Sellers" }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", children: "Store lifecycle, identity architecture, capabilities, and suspension on LetItRip." })] }), [
7
7
  {
8
8
  Icon: Store, title: "Store Lifecycle",
9
- content: (_jsxs(Ul, { className: GC.listMuted, children: [_jsxs(Li, { children: [_jsx(Span, { weight: "bold", children: "Creation" }), ": Seller registers \u2192 completes onboarding wizard \u2192 admin reviews \u2192 store goes ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "active" }), "."] }), _jsxs(Li, { children: [_jsx(Span, { weight: "bold", children: "Status values" }), ": ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "active" }), " (visible and selling), ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "suspended" }), " (hidden, not selling), ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "pending_review" }), " (awaiting admin approval)."] }), _jsxs(Li, { children: [_jsx(Span, { weight: "bold", children: "isVerified badge" }), ": Awarded when the store has a consistent track record (\u226520 delivered orders, \u22654.0 rating, 3+ months active). Click the toggle in AdminStoreEditorView after reviewing the store's history."] })] })),
9
+ content: (_jsxs(Ul, { className: GC.listMuted, children: [_jsxs(Li, { children: [_jsx(Span, { weight: "bold", children: "Creation" }), ": Seller registers \u2192 completes onboarding wizard \u2192 admin reviews \u2192 store goes ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "active" }), "."] }), _jsxs(Li, { children: [_jsx(Span, { weight: "bold", children: "Status values" }), ": ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "pending" }), " (awaiting approval, not public), ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "active" }), " (visible and selling), ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "suspended" }), " (hidden, not selling), ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "rejected" }), " (application refused). Only ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "active" }), " sets ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "isPublic" }), ", and leaving it clears any earned tier immediately."] }), _jsxs(Li, { children: [_jsx(Span, { weight: "bold", children: "isVerified badge" }), ": An IDENTITY check, not a performance one \u2014 grant it after reviewing the seller's submitted ID. There is no order-count or rating threshold, and it does not expire. Toggle it in AdminStoreEditorView."] }), _jsxs(Li, { children: [_jsx(Span, { weight: "bold", children: "sellerTier" }), ": The EARNED tier, computed by ", _jsx(Code, { size: "xs", children: "sellerStandingRollup" }), " on the last day of each month. Setting it by hand pins it (", _jsx(Code, { size: "xs", children: "tierLockedByAdmin" }), ") so the job stops moving it \u2014 and it carries early payout with it. See the Trust guide."] })] })),
10
10
  },
11
11
  {
12
12
  Icon: GitBranch, title: "Store Identity Architecture",
@@ -37,6 +37,34 @@ export function AdminStoresView({ children, ...props }) {
37
37
  void queryClient.invalidateQueries({ queryKey: ["admin", "stores", "listing"] });
38
38
  },
39
39
  });
40
+ /*
41
+ * 🛑 Approve/reject — the step that had no affordance anywhere.
42
+ *
43
+ * A store is born `pending` + `isPublic: false`, and this view offered only
44
+ * verify and suspend, so a real applicant's store could never be published.
45
+ * Both mutations go through the same STORE PATCH the rest of this view uses,
46
+ * which dispatches to `storeRepository.setStatus` and is therefore the only
47
+ * path that keeps `status` and `isPublic` in step. Do not reach for
48
+ * `adminUpdateStoreStatus`: it writes the user-level `storeStatus` mirror
49
+ * and never touches the store, so it would report success and publish
50
+ * nothing.
51
+ */
52
+ const approveStore = useApiMutation({
53
+ errorMessage: "Failed to approve store.",
54
+ mutationFn: (storeId) => apiClient.patch(ADMIN_ENDPOINTS.STORE_BY_ID(storeId), { storeStatus: "active" }),
55
+ onSuccess: () => {
56
+ toast.showToast("Store approved — it is now public.", "success");
57
+ void queryClient.invalidateQueries({ queryKey: ["admin", "stores", "listing"] });
58
+ },
59
+ });
60
+ const rejectStore = useApiMutation({
61
+ errorMessage: "Failed to reject store.",
62
+ mutationFn: (storeId) => apiClient.patch(ADMIN_ENDPOINTS.STORE_BY_ID(storeId), { storeStatus: "rejected" }),
63
+ onSuccess: () => {
64
+ toast.showToast("Store application rejected.", "success");
65
+ void queryClient.invalidateQueries({ queryKey: ["admin", "stores", "listing"] });
66
+ },
67
+ });
40
68
  if (React.Children.count(children) > 0) {
41
69
  return (_jsx(ListingLayout, { portal: "admin", ...props, children: children }));
42
70
  }
@@ -108,7 +136,10 @@ export function AdminStoresView({ children, ...props }) {
108
136
  },
109
137
  })),
110
138
  renderRowActions: (row) => {
111
- const isSuspended = row.status?.toLowerCase() === "suspended";
139
+ const status = row.status?.toLowerCase();
140
+ const isSuspended = status === "suspended";
141
+ const isPending = status === "pending";
142
+ const isActive = status === "active";
112
143
  const isVerified = Boolean(row._raw?.isVerified);
113
144
  return (_jsx(RowActionMenu, { actions: [
114
145
  {
@@ -119,6 +150,24 @@ export function AdminStoresView({ children, ...props }) {
119
150
  label: "Open full page",
120
151
  onClick: () => router.push(String(ROUTES.ADMIN.STORE_DETAIL(row.id))),
121
152
  },
153
+ /*
154
+ * Approve is offered for anything not already active — that
155
+ * deliberately includes `rejected` and `suspended`, which is how a
156
+ * decision gets reversed without hunting for a second control.
157
+ * Reject is offered only while `pending`: rejecting a store that
158
+ * has been live and has orders is a suspension, not a rejection,
159
+ * and conflating them would lose that distinction in the audit log.
160
+ */
161
+ {
162
+ label: ACTIONS.ADMIN["approve-store"].label,
163
+ onClick: () => approveStore.mutate(row.id),
164
+ disabled: isActive || approveStore.isPending,
165
+ },
166
+ {
167
+ label: ACTIONS.ADMIN["reject-store"].label,
168
+ onClick: () => rejectStore.mutate(row.id),
169
+ disabled: !isPending || rejectStore.isPending,
170
+ },
122
171
  {
123
172
  label: ACTIONS.ADMIN["verify-store"].label,
124
173
  onClick: () => verifyStore.mutate(row.id),
@@ -133,7 +182,15 @@ export function AdminStoresView({ children, ...props }) {
133
182
  },
134
183
  renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsx(FilterChipGroup, { label: "Status", tabs: ADMIN_STORE_STATUS_TABS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) })),
135
184
  };
136
- return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(AdminStoreEditorView, { open: Boolean(editorRow), onClose: () => setEditorRow(null), storeId: editorRow?.id, storeName: editorRow?.primary, currentStatus: editorRow?.status?.toLowerCase(), currentIsVerified: Boolean(editorRow?._raw?.isVerified), currentIsFeatured: Boolean(editorRow?._raw?.isFeatured), currentCapabilities: Array.isArray(editorRow?._raw?.capabilities)
185
+ return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(AdminStoreEditorView, { open: Boolean(editorRow), onClose: () => setEditorRow(null), storeId: editorRow?.id, storeName: editorRow?.primary, currentStatus: editorRow?.status?.toLowerCase(), currentIsVerified: Boolean(editorRow?._raw?.isVerified), currentIsFeatured: Boolean(editorRow?._raw?.isFeatured),
186
+ /*
187
+ * Both now travel from the list route's serializer through to the
188
+ * editor. `trustedSeller` was ALREADY emitted by the route and simply
189
+ * never passed down — the API accepted the field, nothing could send
190
+ * it, and the control did not exist. Root Cause #38's shape at the
191
+ * prop, which is where it hides best.
192
+ */
193
+ currentSellerTier: editorRow?._raw?.sellerTier === "trusted" ? "trusted" : "seller", currentTrustedSeller: Boolean(editorRow?._raw?.trustedSeller), currentCapabilities: Array.isArray(editorRow?._raw?.capabilities)
137
194
  ? editorRow._raw.capabilities
138
195
  : undefined,
139
196
  /*
@@ -3,8 +3,18 @@ import { Shield, Ban, AlertTriangle, Headphones, Flag, ClipboardList } from "luc
3
3
  import { Alert, Code, Div, Heading, Li, Row, Section, Span, Stack, Table, Tbody, Td, Text, Th, Thead, Tr, Ul } from "../../../ui/index.js";
4
4
  import { GC } from "../../_guide-cls.js";
5
5
  const STILL_ALLOWED = "✓ Still allowed";
6
+ /** Named because it is used on three rows in the seller-trust table. */
7
+ const ROW_BORDER = "border-b border-[var(--appkit-color-border)]";
6
8
  export function AdminTrustGuideView() {
7
9
  return (_jsxs(Stack, { className: "max-w-3xl mx-auto", padding: "b-2xl", gap: "xl", children: [_jsxs(Section, { children: [_jsxs(Row, { className: "mb-2", align: "center", gap: "3", children: [_jsx(Row, { className: "flex-shrink-0 w-10 h-10 [background:linear-gradient(135deg,var(--appkit-color-primary-700)_0%,var(--appkit-color-cobalt)_100%)]", align: "center", justify: "center", rounded: "xl", children: _jsx(Shield, { className: "w-5 h-5 text-white" }) }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] tracking-widest", size: "sm", weight: "semibold", transform: "uppercase", children: "Admin Guide" })] }), _jsx(Heading, { level: 1, className: "text-[var(--appkit-color-text)] mb-2", mdSize: "3xl", size: "2xl", weight: "bold", children: "Trust & Safety" }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", children: "Bans, scam registry, support tickets, moderation, and reports on LetItRip." })] }), [
10
+ {
11
+ Icon: Shield, title: "Seller trust: three fields, two of them visible",
12
+ content: (_jsxs(_Fragment, { children: [_jsx(Div, { overflow: "x-auto", children: _jsxs(Table, { size: "sm", children: [_jsx(Thead, { children: _jsx(Tr, { className: ROW_BORDER, children: ["Field", "Public?", "What it means", "Who sets it"].map((h) => (_jsx(Th, { align: "left", paddingSide: "pr-md", className: "text-[var(--appkit-color-text)]", padding: "xs-tall", weight: "semibold", children: h }, h))) }) }), _jsx(Tbody, { size: "sm", color: "muted", children: [
13
+ ["isVerified", "Yes — badge", "We checked WHO they are (ID review). No volume threshold; does not expire.", "Admin, by hand"],
14
+ ["sellerTier", "Yes — badge", "EARNED from the last 90 days of trading. Recomputed monthly.", "sellerStandingRollup, or an admin override that pins it"],
15
+ ["trustedSeller", "NO — never", "We pay this store at SHIPPING, not at delivery. A risk transfer.", "The tier carries it; an admin can also set it alone"],
16
+ ].map((row) => (_jsx(Tr, { className: ROW_BORDER, children: row.map((cell, i) => (_jsx(Td, { paddingSide: "pr-md", padding: "xs-tall", children: i === 0 ? _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: cell }) : cell }, i))) }, row[0]))) })] }) }), _jsxs(Alert, { variant: "warning", title: "trustedSeller moves money", children: ["It means the platform pays before the parcel lands, so we carry a lost parcel and a non-delivery dispute with the money already gone. It is never shown to buyers \u2014 a store does not get to display that we trust it. Every change writes a ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "STORE_TRUST_CHANGE" }), " ", "audit row naming who accepted the risk, including when the monthly job is the one that decided."] }), _jsxs(Ul, { spacing: "tight", indent: "lg", children: [_jsxs(Li, { className: GC.listDiscMuted, children: [_jsx(Span, { weight: "bold", children: "Setting sellerTier by hand PINS it." }), " It writes", " ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "tierLockedByAdmin" }), ", and the monthly job then skips that store entirely \u2014 it still refreshes their figures, it just stops moving the tier. Without the pin an override would silently revert at the next month-end."] }), _jsxs(Li, { className: GC.listDiscMuted, children: [_jsx(Span, { weight: "bold", children: "Suspending a store revokes trust immediately" }), ", not at month end. Monthly caching would otherwise keep paying a suspended store early for up to 31 days. Re-activating does NOT re-grant it; the next month-end decides."] }), _jsxs(Li, { className: GC.listDiscMuted, children: [_jsx(Span, { weight: "bold", children: "The criteria live in one place" }), " \u2014", " ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "SELLER_STANDING_DEFAULTS" }), ", admin-tunable under Site Settings. The seller's dashboard panel, the public seller guide and the buyer-facing badge page all render from it, so they cannot state different numbers from the job that grants it."] }), _jsxs(Li, { className: GC.listDiscMuted, children: [_jsx(Span, { weight: "bold", children: "Self-bought and test orders do not count" }), ", and both a distinct-buyer and a distinct-address floor must be cleared \u2014 the tier releases money early, so buying from yourself 25 times is the attack it has to survive."] })] })] })),
17
+ },
8
18
  {
9
19
  Icon: Ban, title: "Soft Ban vs Hard Ban",
10
20
  content: (_jsxs(_Fragment, { children: [_jsx(Div, { overflow: "x-auto", children: _jsxs(Table, { size: "sm", children: [_jsx(Thead, { children: _jsx(Tr, { className: "border-b border-[var(--appkit-color-border)]", children: ["", "Soft Ban", "Hard Ban"].map((h) => (_jsx(Th, { align: "left", paddingSide: "pr-md", className: "text-[var(--appkit-color-text)]", padding: "xs-tall", weight: "semibold", children: h }, h))) }) }), _jsx(Tbody, { size: "sm", color: "muted", children: [
@@ -76,33 +76,38 @@ export declare const SITE_SETTINGS_TABS: readonly [{
76
76
  readonly label: "⑫ Legal";
77
77
  readonly description: "Terms, privacy policy, refund policy and the GST registration details.";
78
78
  readonly keywords: ["terms", "privacy", "policy", "gstin", "refund policy", "compliance"];
79
+ }, {
80
+ readonly id: "whatsapp";
81
+ readonly label: "⑬ WhatsApp";
82
+ readonly description: "WhatsApp Business credentials and which messages go out over it.";
83
+ readonly keywords: ["whatsapp", "meta", "business api", "order updates"];
79
84
  }, {
80
85
  readonly id: "notifications";
81
- readonly label: "⑬ Notifications";
82
- readonly description: "Which channels are on — email and SMS — and the daily digest.";
86
+ readonly label: "⑭ Notifications";
87
+ readonly description: "Which channels are on — email, WhatsApp, SMS — and the daily digest.";
83
88
  readonly keywords: ["email", "sms", "alerts", "digest", "notify"];
84
89
  }, {
85
90
  readonly id: "procurement";
86
- readonly label: "⑭ Procurement";
91
+ readonly label: "⑮ Procurement";
87
92
  readonly description: "Labour rate and the defaults used to price inbound shipments.";
88
93
  readonly keywords: ["labour rate", "labor", "import", "landed cost", "supplier"];
89
94
  }, {
90
95
  readonly id: "emi";
91
- readonly label: "⑮ EMI";
96
+ readonly label: "⑯ EMI";
92
97
  readonly description: "Instalment plans: eligibility threshold, tenures and the surcharge split.";
93
98
  readonly keywords: ["instalments", "installments", "emi", "pay monthly", "finance"];
94
99
  }, {
95
100
  readonly id: "gst";
96
- readonly label: "⑯ GST";
101
+ readonly label: "⑰ GST";
97
102
  readonly description: "GST rates, HSN defaults and the registered business details.";
98
103
  readonly keywords: ["tax", "gst", "hsn", "vat", "invoice tax"];
99
104
  }, {
100
105
  readonly id: "listings";
101
- readonly label: "⑰ Listings";
106
+ readonly label: "⑱ Listings";
102
107
  readonly description: "Which listing types sellers may create, and their defaults.";
103
108
  readonly keywords: ["listing types", "auctions on", "enable", "categories"];
104
109
  }];
105
- export declare const SITE_SETTINGS_TAB_IDS: ("about" | "shipping" | "notifications" | "contact" | "auction" | "seo" | "listings" | "emi" | "legal" | "appearance" | "gst" | "watermark" | "integrations" | "limits" | "announcement" | "branding" | "themes" | "fees" | "procurement")[];
110
+ export declare const SITE_SETTINGS_TAB_IDS: ("about" | "shipping" | "notifications" | "contact" | "auction" | "seo" | "listings" | "whatsapp" | "emi" | "legal" | "appearance" | "gst" | "watermark" | "integrations" | "limits" | "announcement" | "branding" | "themes" | "fees" | "procurement")[];
106
111
  /**
107
112
  * The union `?tab=` is validated against.
108
113
  *
@@ -108,33 +108,39 @@ export const SITE_SETTINGS_TABS = [
108
108
  description: "Terms, privacy policy, refund policy and the GST registration details.",
109
109
  keywords: ["terms", "privacy", "policy", "gstin", "refund policy", "compliance"],
110
110
  },
111
+ {
112
+ id: "whatsapp",
113
+ label: "⑬ WhatsApp",
114
+ description: "WhatsApp Business credentials and which messages go out over it.",
115
+ keywords: ["whatsapp", "meta", "business api", "order updates"],
116
+ },
111
117
  {
112
118
  id: "notifications",
113
- label: "⑬ Notifications",
114
- description: "Which channels are on — email and SMS — and the daily digest.",
119
+ label: "⑭ Notifications",
120
+ description: "Which channels are on — email, WhatsApp, SMS — and the daily digest.",
115
121
  keywords: ["email", "sms", "alerts", "digest", "notify"],
116
122
  },
117
123
  {
118
124
  id: "procurement",
119
- label: "⑭ Procurement",
125
+ label: "⑮ Procurement",
120
126
  description: "Labour rate and the defaults used to price inbound shipments.",
121
127
  keywords: ["labour rate", "labor", "import", "landed cost", "supplier"],
122
128
  },
123
129
  {
124
130
  id: "emi",
125
- label: "⑮ EMI",
131
+ label: "⑯ EMI",
126
132
  description: "Instalment plans: eligibility threshold, tenures and the surcharge split.",
127
133
  keywords: ["instalments", "installments", "emi", "pay monthly", "finance"],
128
134
  },
129
135
  {
130
136
  id: "gst",
131
- label: "⑯ GST",
137
+ label: "⑰ GST",
132
138
  description: "GST rates, HSN defaults and the registered business details.",
133
139
  keywords: ["tax", "gst", "hsn", "vat", "invoice tax"],
134
140
  },
135
141
  {
136
142
  id: "listings",
137
- label: "⑰ Listings",
143
+ label: "⑱ Listings",
138
144
  description: "Which listing types sellers may create, and their defaults.",
139
145
  keywords: ["listing types", "auctions on", "enable", "categories"],
140
146
  },
@@ -137,7 +137,7 @@ export class SiteSettingsRepository extends BaseRepository {
137
137
  encrypted[key] = encryptSecret(value.trim());
138
138
  }
139
139
  else if (value !== undefined && typeof value !== "string") {
140
- // Non-string credential fields (e.g. a nested Record<string,string>)
140
+ // Non-string credential fields (e.g. whatsappTemplates: Record<string,string>)
141
141
  // are stored as-is — they are not secret values to encrypt.
142
142
  encrypted[key] = value;
143
143
  }
@@ -11,6 +11,8 @@ export declare const adminStoreUpdateSchema: z.ZodEffects<z.ZodObject<{
11
11
  storeStatus: z.ZodEnum<[StoreStatus, ...StoreStatus[]]>;
12
12
  isVerified: z.ZodOptional<z.ZodBoolean>;
13
13
  isFeatured: z.ZodOptional<z.ZodBoolean>;
14
+ sellerTier: z.ZodOptional<z.ZodEnum<["seller", "trusted"]>>;
15
+ trustedSeller: z.ZodOptional<z.ZodBoolean>;
14
16
  suspensionReason: z.ZodOptional<z.ZodString>;
15
17
  capabilities: z.ZodOptional<z.ZodArray<z.ZodEnum<[string, ...string[]]>, "many">>;
16
18
  adminNotes: z.ZodOptional<z.ZodString>;
@@ -21,6 +23,8 @@ export declare const adminStoreUpdateSchema: z.ZodEffects<z.ZodObject<{
21
23
  adminNotes?: string | undefined;
22
24
  suspensionReason?: string | undefined;
23
25
  capabilities?: string[] | undefined;
26
+ sellerTier?: "seller" | "trusted" | undefined;
27
+ trustedSeller?: boolean | undefined;
24
28
  }, {
25
29
  storeStatus: StoreStatus;
26
30
  isVerified?: boolean | undefined;
@@ -28,6 +32,8 @@ export declare const adminStoreUpdateSchema: z.ZodEffects<z.ZodObject<{
28
32
  adminNotes?: string | undefined;
29
33
  suspensionReason?: string | undefined;
30
34
  capabilities?: string[] | undefined;
35
+ sellerTier?: "seller" | "trusted" | undefined;
36
+ trustedSeller?: boolean | undefined;
31
37
  }>, {
32
38
  storeStatus: StoreStatus;
33
39
  isVerified?: boolean | undefined;
@@ -35,6 +41,8 @@ export declare const adminStoreUpdateSchema: z.ZodEffects<z.ZodObject<{
35
41
  adminNotes?: string | undefined;
36
42
  suspensionReason?: string | undefined;
37
43
  capabilities?: string[] | undefined;
44
+ sellerTier?: "seller" | "trusted" | undefined;
45
+ trustedSeller?: boolean | undefined;
38
46
  }, {
39
47
  storeStatus: StoreStatus;
40
48
  isVerified?: boolean | undefined;
@@ -42,6 +50,8 @@ export declare const adminStoreUpdateSchema: z.ZodEffects<z.ZodObject<{
42
50
  adminNotes?: string | undefined;
43
51
  suspensionReason?: string | undefined;
44
52
  capabilities?: string[] | undefined;
53
+ sellerTier?: "seller" | "trusted" | undefined;
54
+ trustedSeller?: boolean | undefined;
45
55
  }>;
46
56
  export type AdminStoreUpdateValues = z.infer<typeof adminStoreUpdateSchema>;
47
57
  /**
@@ -75,6 +75,40 @@ export const adminStoreUpdateSchema = z
75
75
  isFeatured: annotate(z.boolean().optional(), {
76
76
  section: "standing", order: 3, row: "quarter",
77
77
  }),
78
+ /*
79
+ * The EARNED tier, overridable by hand.
80
+ *
81
+ * 🛑 Setting it here PINS it — the monthly `sellerStandingRollup` skips a
82
+ * store whose `standing.tierLockedByAdmin` is true. Without the pin an
83
+ * override would silently revert at the next month-end, which is worse
84
+ * than no override: an admin grants a tier, sees it work, and finds it
85
+ * undone weeks later with nothing saying why.
86
+ *
87
+ * 🛑 It carries EARLY PAYOUT with it (see `trustedSeller` below) — the
88
+ * owner's decision is that reaching the tier grants pay-at-shipping. This
89
+ * control therefore moves money, which is why the help text says so.
90
+ */
91
+ sellerTier: annotate(z.enum(["seller", "trusted"]).optional(), {
92
+ section: "standing", order: 5, row: "pair", kind: "select",
93
+ help: "Normally earned automatically at each month-end. Setting it here pins it and also switches payouts to start at shipping.",
94
+ }),
95
+ /*
96
+ * 🛑 This field has existed and been accepted by the API since it was
97
+ * added, with NO control anywhere in the admin UI — so the only way to set
98
+ * it was a hand-written PATCH. A flag that moves money and cannot be
99
+ * operated is worse than one that does not exist, because the code reads
100
+ * as though an admin is in control of it.
101
+ *
102
+ * Separate from `sellerTier` on purpose: the tier is the public, earned
103
+ * thing, and this is the private risk posture. An admin occasionally needs
104
+ * one without the other — early payout for a high-volume store that has
105
+ * not met the rating bar, or a badge for the platform's own store without
106
+ * changing its payout timing.
107
+ */
108
+ trustedSeller: annotate(z.boolean().optional(), {
109
+ section: "standing", order: 6, row: "quarter",
110
+ help: "Pay this store when it ships, not when the parcel is delivered. The platform carries a lost parcel with the money already gone. Never shown to buyers.",
111
+ }),
78
112
  /*
79
113
  * `when` mirrors the superRefine below, which is what stops the two from
80
114
  * disagreeing. The page used to keep this textarea live at all times and
@@ -65,6 +65,67 @@ export declare function toPublicProfilePayload(v: {
65
65
  linkedin?: string;
66
66
  }): Record<string, JsonValue> | undefined;
67
67
  export type AdminUserUpdateValues = z.infer<typeof adminUserUpdateSchema>;
68
+ /**
69
+ * A store's WhatsApp Business connection.
70
+ *
71
+ * `accessToken` is optional because the form shows a masked placeholder and
72
+ * only sends the field when the seller types a NEW value — so "absent" means
73
+ * "unchanged", not "clear it". The `.min(20)` therefore applies only to a real
74
+ * replacement, and is there because a truncated token fails silently at sync
75
+ * time rather than on save.
76
+ */
77
+ export declare const whatsappSettingsSchema: z.ZodObject<{
78
+ catalogSyncEnabled: z.ZodBoolean;
79
+ phoneNumber: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
80
+ wabaId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
81
+ catalogId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
82
+ accessToken: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
83
+ }, "strip", z.ZodTypeAny, {
84
+ catalogSyncEnabled: boolean;
85
+ phoneNumber?: string | undefined;
86
+ accessToken?: string | undefined;
87
+ wabaId?: string | undefined;
88
+ catalogId?: string | undefined;
89
+ }, {
90
+ catalogSyncEnabled: boolean;
91
+ phoneNumber?: string | undefined;
92
+ accessToken?: string | undefined;
93
+ wabaId?: string | undefined;
94
+ catalogId?: string | undefined;
95
+ }>;
96
+ export type WhatsappSettingsValues = z.infer<typeof whatsappSettingsSchema>;
97
+ /**
98
+ * The CONNECTION form's fields — credentials only.
99
+ *
100
+ * `catalogSyncEnabled` is deliberately absent. Its toggle lives in a different
101
+ * card and owns its own write (`syncToggleMutation`), because the credentials
102
+ * form used to send it unconditionally from two cards away: a seller flipped
103
+ * the switch, saw it move, and nothing persisted it. A field in this schema is
104
+ * a field this form's Save button owns.
105
+ *
106
+ * 🛑 `.omit()` rather than a second literal, so the two cannot drift — it
107
+ * carries the SAME field instances into the new shape, which is what keeps the
108
+ * `annotate()` WeakMap entries alive. (A wrapper such as `.superRefine()` is
109
+ * transparent for the same reason; a field-level re-wrap is not.)
110
+ */
111
+ export declare const whatsappConnectionSchema: z.ZodObject<Omit<{
112
+ catalogSyncEnabled: z.ZodBoolean;
113
+ phoneNumber: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
114
+ wabaId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
115
+ catalogId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
116
+ accessToken: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
117
+ }, "catalogSyncEnabled">, "strip", z.ZodTypeAny, {
118
+ phoneNumber?: string | undefined;
119
+ accessToken?: string | undefined;
120
+ wabaId?: string | undefined;
121
+ catalogId?: string | undefined;
122
+ }, {
123
+ phoneNumber?: string | undefined;
124
+ accessToken?: string | undefined;
125
+ wabaId?: string | undefined;
126
+ catalogId?: string | undefined;
127
+ }>;
128
+ export type WhatsappConnectionValues = z.infer<typeof whatsappConnectionSchema>;
68
129
  /**
69
130
  * A buyer's lottery entry.
70
131
  *
@@ -8,20 +8,24 @@
8
8
  * false, and saving any unrelated field silently stripped a real
9
9
  * tester's access.
10
10
  *
11
+ * `SellerWhatsAppSettingsView` writes `accessToken` — a live Meta
12
+ * credential — plus the WABA and catalog ids the sync runs against. A
13
+ * malformed id there fails at sync time, hours later, with no obvious
14
+ * cause.
11
15
  *
12
16
  * `LotteryPullForm` is a buyer-facing entry: a transaction id, a payment
13
17
  * time and an item number, all parsed with a bare `parseInt` and a `new
14
18
  * Date(...)` that yields `Invalid Date` on anything unparseable.
15
19
  *
16
20
  * EXPORTS:
17
- * adminUserUpdateSchema, lotteryPullSchema,
21
+ * adminUserUpdateSchema, whatsappSettingsSchema, lotteryPullSchema,
18
22
  * and their inferred value types
19
23
  *
20
- * @tag domain:admin,lottery
24
+ * @tag domain:admin,whatsapp,lottery
21
25
  * @tag layer:schema
22
26
  * @tag pattern:none
23
27
  * @tag access:isomorphic
24
- * @tag consumers:AdminUserEditorView,LotteryPullForm
28
+ * @tag consumers:AdminUserEditorView,SellerWhatsAppSettingsView,LotteryPullForm
25
29
  * @tag sideEffects:none
26
30
  */
27
31
  import { z } from "zod";
@@ -127,6 +131,43 @@ export function toPublicProfilePayload(v) {
127
131
  publicProfile.socialLinks = socialLinks;
128
132
  return Object.keys(publicProfile).length > 0 ? publicProfile : undefined;
129
133
  }
134
+ /**
135
+ * A store's WhatsApp Business connection.
136
+ *
137
+ * `accessToken` is optional because the form shows a masked placeholder and
138
+ * only sends the field when the seller types a NEW value — so "absent" means
139
+ * "unchanged", not "clear it". The `.min(20)` therefore applies only to a real
140
+ * replacement, and is there because a truncated token fails silently at sync
141
+ * time rather than on save.
142
+ */
143
+ export const whatsappSettingsSchema = z.object({
144
+ catalogSyncEnabled: annotate(z.boolean(), {
145
+ section: "sync", sectionLabel: "Catalog sync", sectionRequired: true, quick: true, order: 1, row: "quarter",
146
+ }),
147
+ phoneNumber: annotate(z.string().trim().regex(PHONE_RE, "Enter the 10-digit WhatsApp Business number.").optional().or(z.literal("")), { section: "connection", sectionLabel: "Connection", order: 1, row: "pair" }),
148
+ // Meta ids are numeric strings; a pasted URL or a stray space is the usual
149
+ // mistake and produces a sync that never runs.
150
+ wabaId: annotate(z.string().trim().regex(/^\d{5,32}$/, "A WABA ID is a numeric id, not a URL.").optional().or(z.literal("")), { section: "connection", order: 2, row: "pair" }),
151
+ catalogId: annotate(z.string().trim().regex(/^\d{5,32}$/, "A catalog ID is a numeric id, not a URL.").optional().or(z.literal("")), { section: "connection", order: 3, row: "pair" }),
152
+ accessToken: annotate(z.string().trim().min(20, "That access token looks truncated.").optional().or(z.literal("")), { section: "connection", order: 4, row: "full", help: "Leave blank to keep the current token." }),
153
+ });
154
+ /**
155
+ * The CONNECTION form's fields — credentials only.
156
+ *
157
+ * `catalogSyncEnabled` is deliberately absent. Its toggle lives in a different
158
+ * card and owns its own write (`syncToggleMutation`), because the credentials
159
+ * form used to send it unconditionally from two cards away: a seller flipped
160
+ * the switch, saw it move, and nothing persisted it. A field in this schema is
161
+ * a field this form's Save button owns.
162
+ *
163
+ * 🛑 `.omit()` rather than a second literal, so the two cannot drift — it
164
+ * carries the SAME field instances into the new shape, which is what keeps the
165
+ * `annotate()` WeakMap entries alive. (A wrapper such as `.superRefine()` is
166
+ * transparent for the same reason; a field-level re-wrap is not.)
167
+ */
168
+ export const whatsappConnectionSchema = whatsappSettingsSchema.omit({
169
+ catalogSyncEnabled: true,
170
+ });
130
171
  /**
131
172
  * A buyer's lottery entry.
132
173
  *