@mohasinac/appkit 4.63.5 → 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 (117) 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/events/like-actions.d.ts +25 -0
  5. package/dist/_internal/server/features/events/like-actions.js +67 -0
  6. package/dist/_internal/server/features/events/likes.d.ts +65 -0
  7. package/dist/_internal/server/features/events/likes.js +146 -0
  8. package/dist/_internal/server/features/seo/index.d.ts +3 -0
  9. package/dist/_internal/server/features/seo/index.js +2 -0
  10. package/dist/_internal/server/features/seo/product-detail-types.d.ts +1 -0
  11. package/dist/_internal/server/features/seo/product-detail-types.js +26 -0
  12. package/dist/_internal/server/features/seo/robots.js +20 -1
  13. package/dist/_internal/server/features/seo/sitemap.js +125 -27
  14. package/dist/_internal/server/features/seo/static-params.d.ts +107 -0
  15. package/dist/_internal/server/features/seo/static-params.js +228 -0
  16. package/dist/_internal/server/features/seo/taxonomy-links.d.ts +44 -0
  17. package/dist/_internal/server/features/seo/taxonomy-links.js +85 -0
  18. package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
  19. package/dist/_internal/server/features/stores/adapters.js +87 -1
  20. package/dist/_internal/server/functions/scheduled.d.ts +16 -1
  21. package/dist/_internal/server/functions/scheduled.js +23 -1
  22. package/dist/_internal/server/jobs/core/sellerStandingRollup.d.ts +63 -0
  23. package/dist/_internal/server/jobs/core/sellerStandingRollup.js +412 -0
  24. package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
  25. package/dist/_internal/server/jobs/handlers/index.js +1 -0
  26. package/dist/_internal/server/jobs/handlers/sellerStandingRollup.d.ts +12 -0
  27. package/dist/_internal/server/jobs/handlers/sellerStandingRollup.js +14 -0
  28. package/dist/_internal/shared/actions/action-registry.js +61 -0
  29. package/dist/_internal/shared/features/seller-standing/config.d.ts +130 -0
  30. package/dist/_internal/shared/features/seller-standing/config.js +153 -0
  31. package/dist/constants/api-endpoints.d.ts +24 -0
  32. package/dist/constants/api-endpoints.js +8 -0
  33. package/dist/features/about/components/HelpPageView.js +9 -1
  34. package/dist/features/account/components/BecomeSellerView.d.ts +15 -3
  35. package/dist/features/account/components/BecomeSellerView.js +2 -2
  36. package/dist/features/admin/components/AdminStoreEditorView.d.ts +12 -1
  37. package/dist/features/admin/components/AdminStoreEditorView.js +30 -2
  38. package/dist/features/admin/components/AdminStoresGuideView.js +1 -1
  39. package/dist/features/admin/components/AdminStoresView.js +59 -2
  40. package/dist/features/admin/components/AdminTrustGuideView.js +10 -0
  41. package/dist/features/admin/schemas/admin-editor-forms.d.ts +10 -0
  42. package/dist/features/admin/schemas/admin-editor-forms.js +34 -0
  43. package/dist/features/blog/components/BlogIndexPageView.js +2 -2
  44. package/dist/features/blog/components/BlogListView.js +2 -1
  45. package/dist/features/blog/components/BlogPostView.js +2 -1
  46. package/dist/features/blog/constants/category-badge.d.ts +33 -0
  47. package/dist/features/blog/constants/category-badge.js +40 -0
  48. package/dist/features/categories/components/TaxonomyLinkIndex.d.ts +39 -0
  49. package/dist/features/categories/components/TaxonomyLinkIndex.js +11 -0
  50. package/dist/features/categories/schemas/firestore.d.ts +25 -0
  51. package/dist/features/categories/utils/load-category-options.d.ts +33 -0
  52. package/dist/features/categories/utils/load-category-options.js +50 -0
  53. package/dist/features/events/components/EventCard.js +4 -12
  54. package/dist/features/events/components/EventInsights.d.ts +27 -0
  55. package/dist/features/events/components/EventInsights.js +10 -0
  56. package/dist/features/events/components/EventLikeButton.d.ts +21 -0
  57. package/dist/features/events/components/EventLikeButton.js +59 -0
  58. package/dist/features/events/components/EventLikersRail.d.ts +23 -0
  59. package/dist/features/events/components/EventLikersRail.js +54 -0
  60. package/dist/features/events/components/EventTypeBadge.d.ts +26 -0
  61. package/dist/features/events/components/EventTypeBadge.js +7 -0
  62. package/dist/features/events/components/EventsListPageView.js +2 -2
  63. package/dist/features/events/components/SpinWheel.d.ts +41 -0
  64. package/dist/features/events/components/SpinWheel.js +128 -0
  65. package/dist/features/events/components/SpinWheelView.js +36 -15
  66. package/dist/features/events/constants/event-type-meta.d.ts +54 -0
  67. package/dist/features/events/constants/event-type-meta.js +109 -0
  68. package/dist/features/events/repository/event-likes.repository.d.ts +57 -0
  69. package/dist/features/events/repository/event-likes.repository.js +151 -0
  70. package/dist/features/events/schemas/firestore.d.ts +111 -0
  71. package/dist/features/events/schemas/likes.d.ts +81 -0
  72. package/dist/features/events/schemas/likes.js +59 -0
  73. package/dist/features/events/types/index.d.ts +7 -0
  74. package/dist/features/events/utils/carousel-slide.d.ts +42 -0
  75. package/dist/features/events/utils/carousel-slide.js +174 -0
  76. package/dist/features/events/utils/event-charts.d.ts +65 -0
  77. package/dist/features/events/utils/event-charts.js +172 -0
  78. package/dist/features/faq/constants/help-guides.js +9 -0
  79. package/dist/features/faq/schemas/firestore.js +1 -0
  80. package/dist/features/faq/types/index.d.ts +19 -0
  81. package/dist/features/homepage/components/BrandsSection.js +25 -1
  82. package/dist/features/homepage/components/HeroCarousel.js +47 -10
  83. package/dist/features/homepage/types/index.d.ts +7 -0
  84. package/dist/features/stores/components/InteractiveStoreCard.js +2 -1
  85. package/dist/features/stores/components/SellerStandingPanel.d.ts +31 -0
  86. package/dist/features/stores/components/SellerStandingPanel.js +25 -0
  87. package/dist/features/stores/components/SellerTierBadge.d.ts +57 -0
  88. package/dist/features/stores/components/SellerTierBadge.js +19 -0
  89. package/dist/features/stores/components/StoreCapabilitiesGuideView.js +32 -2
  90. package/dist/features/stores/components/StoreCreateForm.d.ts +46 -0
  91. package/dist/features/stores/components/StoreCreateForm.js +107 -0
  92. package/dist/features/stores/components/StoreGuideHubView.js +17 -4
  93. package/dist/features/stores/components/StoreHeader.js +2 -1
  94. package/dist/features/stores/repository/store.repository.js +23 -0
  95. package/dist/features/stores/schemas/firestore.d.ts +60 -1
  96. package/dist/features/stores/schemas/firestore.js +18 -0
  97. package/dist/features/stores/schemas/index.d.ts +2 -2
  98. package/dist/features/stores/schemas/store-create.d.ts +47 -0
  99. package/dist/features/stores/schemas/store-create.js +86 -0
  100. package/dist/features/stores/types/index.d.ts +42 -0
  101. package/dist/next/routing/route-map.d.ts +3 -1
  102. package/dist/next/routing/route-map.js +13 -0
  103. package/dist/schemas/registry.d.ts +10 -0
  104. package/dist/server.d.ts +5 -0
  105. package/dist/server.js +10 -0
  106. package/dist/styles.css +36 -1
  107. package/dist/tailwind-utilities.css +1 -1
  108. package/dist/ui/components/HorizontalScroller.js +15 -2
  109. package/dist/ui/components/PageHeroHeading.d.ts +40 -0
  110. package/dist/ui/components/PageHeroHeading.js +15 -0
  111. package/dist/ui/components/Typography.d.ts +3 -3
  112. package/dist/ui/components/Typography.js +59 -31
  113. package/dist/ui/components/Typography.style.css +35 -0
  114. package/dist/ui/index.d.ts +2 -0
  115. package/dist/ui/index.js +1 -0
  116. package/package.json +1 -1
  117. package/scripts/seed-cli.mjs +4 -2
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The store-creation form — step 2 of becoming a seller.
3
+ *
4
+ * 🛑 Why this exists. `createStoreAction` has been in the codebase with **no
5
+ * caller and no API route**, there was no `/store/create` page, and
6
+ * `/user/become-seller` collected ZERO fields — it was marketing copy and one
7
+ * button that flipped `role` to `seller` and set `storeStatus: "pending"`. So a
8
+ * real applicant became a seller and then could not open a storefront by any
9
+ * route in the product. Only `role: "tester"` got a store, via the auto-approve
10
+ * branch in `becomeSeller`. This form is the missing step.
11
+ *
12
+ * 🛑 `<SectionForm>`, not a bare `<Form>`. `audit-form-sectionised` is
13
+ * strict-zero on multi-field forms that skip the section engine, and the engine
14
+ * is what supplies the pinned mobile action bar, the per-section error badges
15
+ * and the schema-driven field rendering every other form in the app has. The
16
+ * sections come from `storeCreateSchema`'s own annotations, so the fields, their
17
+ * labels and their validation have exactly one definition.
18
+ *
19
+ * 🛑 `<SectionForm>` does NOT mount a `FormShellContext` — it consumes whichever
20
+ * one the owning view provides and tolerates none, which means a view rendering
21
+ * only `<SectionForm>` gets section badges and no readable message anywhere.
22
+ * This component mounts the context AND renders `<FormErrorSummary>` itself.
23
+ *
24
+ * The submit handler is injected rather than imported: the server action lives
25
+ * in the consumer (`src/actions/seller.actions.ts`), and appkit must not reach
26
+ * into it. Same slot shape as `StoreCreateProductShell`'s `onSave`/`onPublish`.
27
+ */
28
+ import React from "react";
29
+ import { type StoreCreateInput } from "../schemas/store-create";
30
+ export interface StoreCreateFormResult {
31
+ ok: boolean;
32
+ /** Stable error CODE, never a server message — see the submit handler. */
33
+ code?: string;
34
+ /** Which field the failure belongs to, when the server can say. */
35
+ field?: keyof StoreCreateInput;
36
+ }
37
+ export interface StoreCreateFormProps {
38
+ /**
39
+ * Performs the create. Resolves `{ ok: true }` on success, or
40
+ * `{ ok: false, code }` with a mapped error code.
41
+ */
42
+ onSubmit: (input: StoreCreateInput) => Promise<StoreCreateFormResult>;
43
+ /** Called after a successful create, for the consumer's redirect/toast. */
44
+ onCreated?: () => void;
45
+ }
46
+ export declare function StoreCreateForm({ onSubmit, onCreated }: StoreCreateFormProps): React.JSX.Element;
@@ -0,0 +1,107 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * The store-creation form — step 2 of becoming a seller.
5
+ *
6
+ * 🛑 Why this exists. `createStoreAction` has been in the codebase with **no
7
+ * caller and no API route**, there was no `/store/create` page, and
8
+ * `/user/become-seller` collected ZERO fields — it was marketing copy and one
9
+ * button that flipped `role` to `seller` and set `storeStatus: "pending"`. So a
10
+ * real applicant became a seller and then could not open a storefront by any
11
+ * route in the product. Only `role: "tester"` got a store, via the auto-approve
12
+ * branch in `becomeSeller`. This form is the missing step.
13
+ *
14
+ * 🛑 `<SectionForm>`, not a bare `<Form>`. `audit-form-sectionised` is
15
+ * strict-zero on multi-field forms that skip the section engine, and the engine
16
+ * is what supplies the pinned mobile action bar, the per-section error badges
17
+ * and the schema-driven field rendering every other form in the app has. The
18
+ * sections come from `storeCreateSchema`'s own annotations, so the fields, their
19
+ * labels and their validation have exactly one definition.
20
+ *
21
+ * 🛑 `<SectionForm>` does NOT mount a `FormShellContext` — it consumes whichever
22
+ * one the owning view provides and tolerates none, which means a view rendering
23
+ * only `<SectionForm>` gets section badges and no readable message anywhere.
24
+ * This component mounts the context AND renders `<FormErrorSummary>` itself.
25
+ *
26
+ * The submit handler is injected rather than imported: the server action lives
27
+ * in the consumer (`src/actions/seller.actions.ts`), and appkit must not reach
28
+ * into it. Same slot shape as `StoreCreateProductShell`'s `onSave`/`onPublish`.
29
+ */
30
+ import React from "react";
31
+ import { Div, Stack, Text } from "../../../ui/index.js";
32
+ import { FormShellContext, FormErrorSummary, useFormShellState } from "../../../ui/forms/index.js";
33
+ import { SectionForm, buildSectionsFromSchema, useSectionFormNav } from "../../shell/index.js";
34
+ import { PaginatedSelect } from "../../../ui/components/PaginatedSelect.js";
35
+ import { loadCategoryOptions } from "../../categories/utils/load-category-options.js";
36
+ import { storeCreateSchema } from "../schemas/store-create.js";
37
+ import { normalizeError } from "../../../errors/normalize.js";
38
+ import { toUserMessage } from "../../../errors/error-display-map.js";
39
+ import { useTranslations } from "next-intl";
40
+ export function StoreCreateForm({ onSubmit, onCreated }) {
41
+ const t = useTranslations();
42
+ const [values, setValues] = React.useState({
43
+ storeName: "",
44
+ storeDescription: "",
45
+ storeCategory: "",
46
+ });
47
+ const [isPending, setIsPending] = React.useState(false);
48
+ const { shellCtx, setFieldError, clearErrors, markSubmitAttempted } = useFormShellState(storeCreateSchema);
49
+ const handleChange = React.useCallback((partial) => {
50
+ setValues((prev) => Object.assign({}, prev, partial));
51
+ }, []);
52
+ /*
53
+ * 🛑 `PaginatedSelect`, not the inferred text input: there are over a
54
+ * thousand categories, so a flat list is unscannable and the >5-options rule
55
+ * applies. `loadCategoryOptions` passes `listingOnly=true`, without which
56
+ * this offers brands and bundles as categories.
57
+ *
58
+ * 🛑 `value={… || null}` — it renders the placeholder only on `value == null`,
59
+ * so an empty string paints a BLANK trigger with no prompt and no hint that
60
+ * it is a control.
61
+ */
62
+ const sections = React.useMemo(() => buildSectionsFromSchema(storeCreateSchema, {
63
+ renderers: {
64
+ storeCategory: ({ values: v, onChange }) => (_jsx(PaginatedSelect, { name: "storeCategory", value: v.storeCategory || null, onChange: (val) => onChange({ storeCategory: val ?? "" }), loadOptions: loadCategoryOptions, placeholder: "Search categories\u2026 (optional)", searchPlaceholder: "Type a category name\u2026", noResultsText: "No categories found", ariaLabel: "Main category" })),
65
+ },
66
+ }), []);
67
+ // Declared AFTER `sections`, because it takes them — it derives the ordered
68
+ // open/closed set from the sections that are currently visible.
69
+ const nav = useSectionFormNav(sections, values);
70
+ async function submit() {
71
+ clearErrors();
72
+ markSubmitAttempted();
73
+ const parsed = storeCreateSchema.safeParse(values);
74
+ if (!parsed.success) {
75
+ for (const issue of parsed.error.issues) {
76
+ setFieldError(String(issue.path[0]), issue.message);
77
+ }
78
+ return;
79
+ }
80
+ setIsPending(true);
81
+ try {
82
+ const result = await onSubmit(parsed.data);
83
+ if (result.ok) {
84
+ onCreated?.();
85
+ return;
86
+ }
87
+ /*
88
+ * 🛑 `toUserMessage(code, t)`, never the server's own message. A
89
+ * raw-message fallback is how a Node MODULE_NOT_FOUND with a full
90
+ * require stack got rendered inside a buyer-facing modal (Root Cause
91
+ * #86). `toUserMessage` terminates at a generic string and never
92
+ * returns server text.
93
+ */
94
+ setFieldError(result.field ?? "storeName", toUserMessage(result.code ?? "UNKNOWN", t));
95
+ }
96
+ catch (err) {
97
+ // Every `NormalizedError` variant carries a non-optional `code`, so
98
+ // there is nothing to default here.
99
+ const e = normalizeError(err);
100
+ setFieldError("storeName", toUserMessage(e.code, t));
101
+ }
102
+ finally {
103
+ setIsPending(false);
104
+ }
105
+ }
106
+ return (_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Div, { children: [_jsxs(Stack, { gap: "xs", className: "mb-4", children: [_jsx(Text, { size: "lg", weight: "semibold", children: "Open your storefront" }), _jsx(Text, { size: "sm", color: "muted", children: "You can change all of this later. Your store stays private until we review it." })] }), _jsx(SectionForm, { sections: sections, values: values, onChange: handleChange, onSubmit: submit, schema: storeCreateSchema, openIds: nav.openIds, onOpenChange: nav.setOpenIds, isLoading: isPending, submitLabel: "Create my store" }), _jsx(FormErrorSummary, {})] }) }));
107
+ }
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Package, ShoppingBag, Wallet, Settings, Shield, Headphones, CheckCircle2, Circle, BookOpen, } from "lucide-react";
3
3
  import { Div, Grid, Heading, Progress, Row, Section, Stack, Text, TextLink } from "../../../ui/index.js";
4
+ import { SellerStandingPanel } from "./SellerStandingPanel.js";
4
5
  import { ROUTES } from "../../../next/routing/route-map.js";
5
6
  const CLS_CHECK_ICON = "w-5 h-5 flex-shrink-0 text-emerald-500";
6
7
  const CLS_SUCCESS_BOX = "rounded-xl border border-emerald-200 bg-emerald-50 dark:bg-emerald-950/30 dark:border-emerald-800 px-[var(--appkit-space-5)] py-[var(--appkit-space-4)]";
@@ -65,10 +66,22 @@ function buildChecklist(store) {
65
66
  done: (store?.shippingConfig?.providers?.length ?? 0) > 0,
66
67
  href: String(ROUTES.STORE.SHIPPING_CONFIGS),
67
68
  },
69
+ /*
70
+ * 🛑 Was "Request verified badge" → a SUPPORT TICKET, and the cast beside
71
+ * it (`store as StoreDocument & { isVerified?: boolean }`) was the tell
72
+ * that `isVerified` was not even on the type. Two things were wrong: the
73
+ * badge was not something to request (an admin grants it after checking
74
+ * identity, and no ticket category asked for it), and the step it pointed
75
+ * at could not complete itself.
76
+ *
77
+ * The earned tier is a real step with a real destination — the
78
+ * capabilities guide, where the criteria and the seller's own figures are
79
+ * both rendered from `SELLER_STANDING_DEFAULTS`.
80
+ */
68
81
  {
69
- label: "Request verified badge",
70
- done: Boolean(store?.isVerified),
71
- href: String(ROUTES.USER.SUPPORT),
82
+ label: "Earn Trusted Seller status",
83
+ done: store?.sellerTier === "trusted",
84
+ href: String(ROUTES.STORE.GUIDE_CAPABILITIES),
72
85
  },
73
86
  ];
74
87
  }
@@ -77,7 +90,7 @@ export function StoreGuideHubView({ store }) {
77
90
  const storeName = store?.storeName ?? "Your Store";
78
91
  const steps = buildChecklist(store);
79
92
  const completedCount = steps.filter((s) => s.done).length;
80
- return (_jsxs(Stack, { gap: "xl", padding: "b-2xl", children: [_jsx(Section, { overflow: "hidden", className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "2xl", shadow: "sm", children: _jsxs(Div, { paddingX: "x-md-xl", paddingY: "y-xl", className: "[background:linear-gradient(135deg,var(--appkit-color-primary-700)_0%,var(--appkit-color-cobalt)_60%,var(--appkit-color-secondary-400)_100%)]", children: [_jsxs(Row, { className: "mb-3", align: "center", gap: "3", children: [_jsx(Row, { className: "flex-shrink-0 w-10 h-10", surface: "default", align: "center", justify: "center", rounded: "xl", children: _jsx(BookOpen, { className: "w-5 h-5 text-white" }) }), _jsx(Text, { color: "inverse-muted", className: "tracking-widest", size: "sm", weight: "semibold", transform: "uppercase", children: "Seller Guide" })] }), _jsx(Heading, { color: "inverse", level: 1, className: "mb-2", mdSize: "3xl", size: "2xl", weight: "bold", children: storeName }), _jsx(Text, { color: "inverse-muted", size: "base", children: "Everything you need to know about selling on LetItRip." })] }) }), _jsxs(Div, { layout: "grid", gap: "8", className: "grid-cols-1 lg:grid-cols-3", children: [_jsxs(Stack, { className: "lg:col-span-2", gap: "md", children: [_jsx(Heading, { level: 2, className: "text-[var(--appkit-color-text)]", size: "lg", weight: "semibold", children: "Guides" }), _jsx(Grid, { cols: "cardsWide", gap: "md", children: GUIDE_CARDS.map(({ Icon, title, description, href }) => (_jsxs(TextLink, { variant: "bare", href: href, rounded: "xl", layout: "flex-col", gap: "md", paddingX: "lg", paddingY: "lg", shadow: "hover-md", className: "group border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] hover:border-[var(--appkit-color-primary)] transition-all", children: [_jsxs(Row, { align: "center", gap: "3", children: [_jsx(Row, { className: "flex-shrink-0 w-9 h-9 [background:linear-gradient(135deg,var(--appkit-color-primary-700)_0%,var(--appkit-color-cobalt)_100%)]", align: "center", justify: "center", rounded: "lg", children: _jsx(Icon, { className: "w-4 h-4 text-white" }) }), _jsx(Text, { className: "text-[var(--appkit-color-text)] group-hover:text-[var(--appkit-color-primary)] transition-colors", weight: "semibold", children: title })] }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] leading-relaxed", size: "sm", children: description }), _jsx(Text, { className: "text-[var(--appkit-color-primary)] mt-auto", size: "xs", weight: "medium", children: "Read guide \u2192" })] }, title))) })] }), _jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 2, className: "text-[var(--appkit-color-text)]", size: "lg", weight: "semibold", children: "Getting started" }), _jsxs(Div, { overflow: "hidden", className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "xl", children: [_jsxs(Div, { className: "px-[1.25rem] pt-[1.25rem] border-b border-[var(--appkit-color-border)]", padding: "b-md", children: [_jsxs(Row, { className: "mb-2", align: "center", justify: "between", children: [_jsxs(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", weight: "medium", children: [completedCount, " of ", steps.length, " complete"] }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: [Math.round((completedCount / steps.length) * 100), "%"] })] }), _jsx(Progress, { value: completedCount, max: steps.length, size: "sm", variant: "gradient" })] }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)]", children: steps.map(({ label, done, href }) => (_jsxs(TextLink, { variant: "bare", href: done ? "#" : href, "aria-disabled": done, layout: "flex", align: "center", gap: "sm", paddingX: "lg", paddingY: "sm-tall", size: "sm", className: `transition-colors ${done
93
+ return (_jsxs(Stack, { gap: "xl", padding: "b-2xl", children: [_jsx(Section, { overflow: "hidden", className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "2xl", shadow: "sm", children: _jsxs(Div, { paddingX: "x-md-xl", paddingY: "y-xl", className: "[background:linear-gradient(135deg,var(--appkit-color-primary-700)_0%,var(--appkit-color-cobalt)_60%,var(--appkit-color-secondary-400)_100%)]", children: [_jsxs(Row, { className: "mb-3", align: "center", gap: "3", children: [_jsx(Row, { className: "flex-shrink-0 w-10 h-10", surface: "default", align: "center", justify: "center", rounded: "xl", children: _jsx(BookOpen, { className: "w-5 h-5 text-white" }) }), _jsx(Text, { color: "inverse-muted", className: "tracking-widest", size: "sm", weight: "semibold", transform: "uppercase", children: "Seller Guide" })] }), _jsx(Heading, { color: "inverse", level: 1, className: "mb-2", mdSize: "3xl", size: "2xl", weight: "bold", children: storeName }), _jsx(Text, { color: "inverse-muted", size: "base", children: "Everything you need to know about selling on LetItRip." })] }) }), _jsxs(Div, { layout: "grid", gap: "8", className: "grid-cols-1 lg:grid-cols-3", children: [_jsxs(Stack, { className: "lg:col-span-2", gap: "md", children: [_jsx(Heading, { level: 2, className: "text-[var(--appkit-color-text)]", size: "lg", weight: "semibold", children: "Guides" }), _jsx(Grid, { cols: "cardsWide", gap: "md", children: GUIDE_CARDS.map(({ Icon, title, description, href }) => (_jsxs(TextLink, { variant: "bare", href: href, rounded: "xl", layout: "flex-col", gap: "md", paddingX: "lg", paddingY: "lg", shadow: "hover-md", className: "group border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] hover:border-[var(--appkit-color-primary)] transition-all", children: [_jsxs(Row, { align: "center", gap: "3", children: [_jsx(Row, { className: "flex-shrink-0 w-9 h-9 [background:linear-gradient(135deg,var(--appkit-color-primary-700)_0%,var(--appkit-color-cobalt)_100%)]", align: "center", justify: "center", rounded: "lg", children: _jsx(Icon, { className: "w-4 h-4 text-white" }) }), _jsx(Text, { className: "text-[var(--appkit-color-text)] group-hover:text-[var(--appkit-color-primary)] transition-colors", weight: "semibold", children: title })] }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] leading-relaxed", size: "sm", children: description }), _jsx(Text, { className: "text-[var(--appkit-color-primary)] mt-auto", size: "xs", weight: "medium", children: "Read guide \u2192" })] }, title))) }), _jsx(SellerStandingPanel, { standing: store?.standing, tier: store?.sellerTier, showProgress: true, heading: "Your Trusted Seller standing" })] }), _jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 2, className: "text-[var(--appkit-color-text)]", size: "lg", weight: "semibold", children: "Getting started" }), _jsxs(Div, { overflow: "hidden", className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "xl", children: [_jsxs(Div, { className: "px-[1.25rem] pt-[1.25rem] border-b border-[var(--appkit-color-border)]", padding: "b-md", children: [_jsxs(Row, { className: "mb-2", align: "center", justify: "between", children: [_jsxs(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", weight: "medium", children: [completedCount, " of ", steps.length, " complete"] }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: [Math.round((completedCount / steps.length) * 100), "%"] })] }), _jsx(Progress, { value: completedCount, max: steps.length, size: "sm", variant: "gradient" })] }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)]", children: steps.map(({ label, done, href }) => (_jsxs(TextLink, { variant: "bare", href: done ? "#" : href, "aria-disabled": done, layout: "flex", align: "center", gap: "sm", paddingX: "lg", paddingY: "sm-tall", size: "sm", className: `transition-colors ${done
81
94
  ? "cursor-default"
82
95
  : "hover:bg-[var(--appkit-color-border)]/30"}`, children: [done ? (_jsx(CheckCircle2, { className: CLS_CHECK_ICON })) : (_jsx(Circle, { className: "w-5 h-5 flex-shrink-0 text-[var(--appkit-color-text-muted)]" })), _jsx(Text, { className: `leading-snug ${done
83
96
  ? "line-through text-[var(--appkit-color-text-muted)]"
@@ -4,6 +4,7 @@ import { MediaImage } from "../../media/MediaImage.js";
4
4
  import { normalizeRichTextHtml } from "../../../utils/string.formatter.js";
5
5
  import { ShareButton } from "../../products/components/ShareButton.js";
6
6
  import { SellerTrustBadge } from "../../scams/components/SellerTrustBadge.js";
7
+ import { SellerTierBadge } from "./SellerTierBadge.js";
7
8
  import { OwnerQuickActions } from "../../admin/components/OwnerQuickActions.js";
8
9
  import { ROUTES } from "../../../next/routing/route-map.js";
9
10
  const __O = {
@@ -14,7 +15,7 @@ const CLS_STARS = "inline-flex items-center gap-[var(--appkit-space-1)] text-war
14
15
  const CLS_FOLLOW_BTN = "rounded-lg border border-warning px-[var(--appkit-space-4)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] font-medium text-warning hover:bg-warning-surface transition-colors";
15
16
  const CLS_WARN_BANNER = "mt-3 rounded-lg bg-warning-surface dark:bg-warning-surface border border-warning dark:border-warning px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] text-warning dark:text-warning";
16
17
  export function StoreHeader({ store, labels = {}, onFollow, className = "", trust, }) {
17
- return (_jsxs(Section, { surface: "default", border: "bottom", className: className, children: [store.storeBannerURL && (_jsx(Div, { className: `relative h-40 md:h-56 ${__O.hidden} bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)]`, children: _jsx(MediaImage, { src: store.storeBannerURL, alt: `${store.storeName} banner`, size: "banner" }) })), _jsxs(Div, { paddingX: "x-page", className: "max-w-7xl mx-auto", padding: "y-md", children: [_jsxs(Row, { align: "end", gap: "md", children: [store.storeLogoURL ? (_jsx(Div, { className: "relative -mt-8 h-16 w-16 border-2 border-white", rounded: "xl", shadow: "sm", overflow: "hidden", children: _jsx(MediaImage, { src: store.storeLogoURL, alt: store.storeName, size: "avatar" }) })) : (_jsx(Div, { className: CLS_AVATAR, children: store.storeName[0]?.toUpperCase() })), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsxs(Row, { className: "mb-0.5", align: "center", gap: "sm", wrap: true, children: [_jsx(Heading, { level: 1, className: "text-[var(--appkit-color-text)]", size: "xl", weight: "bold", children: store.storeName }), store.averageRating != null && store.averageRating > 0 && (_jsxs(Span, { size: "sm", weight: "medium", className: CLS_STARS, children: ["\u2605 ", store.averageRating.toFixed(1)] })), trust && _jsx(SellerTrustBadge, { trust: trust })] }), _jsx(OwnerQuickActions, { resource: "STORE", placement: "detail", editHref: String(ROUTES.ADMIN.STORE_DETAIL(store.storeSlug)), recordLabel: store.storeName }), _jsxs(Row, { textSize: "xs", className: "text-[var(--appkit-color-text-muted)] mb-0.5", gap: "3", children: [store.category && _jsx(Span, { transform: "capitalize", children: store.category }), store.totalProducts != null && store.totalProducts > 0 && (_jsxs(Span, { children: [store.totalProducts, " ", labels.products ?? "products"] })), store.totalReviews != null && store.totalReviews > 0 && (_jsxs(Span, { children: [store.totalReviews, " ", labels.reviews ?? "reviews"] })), store.itemsSold != null && store.itemsSold > 0 && (_jsxs(Span, { children: [store.itemsSold, " ", labels.sold ?? "sold"] }))] }), store.storeDescription && (_jsx(RichText, { html: normalizeRichTextHtml(store.storeDescription), copyableCode: true, className: "mt-0.5" }))] }), _jsxs(Row, { gap: "sm", align: "center", className: "shrink-0", children: [_jsx(ShareButton, { title: store.storeName }), onFollow && (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onFollow(store.storeSlug), className: CLS_FOLLOW_BTN, children: labels.follow ?? "Follow" }))] })] }), store.isVacationMode && (_jsx(Text, { className: CLS_WARN_BANNER, children: store.vacationMessage ??
18
+ return (_jsxs(Section, { surface: "default", border: "bottom", className: className, children: [store.storeBannerURL && (_jsx(Div, { className: `relative h-40 md:h-56 ${__O.hidden} bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)]`, children: _jsx(MediaImage, { src: store.storeBannerURL, alt: `${store.storeName} banner`, size: "banner" }) })), _jsxs(Div, { paddingX: "x-page", className: "max-w-7xl mx-auto", padding: "y-md", children: [_jsxs(Row, { align: "end", gap: "md", children: [store.storeLogoURL ? (_jsx(Div, { className: "relative -mt-8 h-16 w-16 border-2 border-white", rounded: "xl", shadow: "sm", overflow: "hidden", children: _jsx(MediaImage, { src: store.storeLogoURL, alt: store.storeName, size: "avatar" }) })) : (_jsx(Div, { className: CLS_AVATAR, children: store.storeName[0]?.toUpperCase() })), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsxs(Row, { className: "mb-0.5", align: "center", gap: "sm", wrap: true, children: [_jsx(Heading, { level: 1, className: "text-[var(--appkit-color-text)]", size: "xl", weight: "bold", children: store.storeName }), store.averageRating != null && store.averageRating > 0 && (_jsxs(Span, { size: "sm", weight: "medium", className: CLS_STARS, children: ["\u2605 ", store.averageRating.toFixed(1)] })), _jsx(SellerTierBadge, { isVerified: store.isVerified, sellerTier: store.sellerTier, href: String(ROUTES.PUBLIC.HELP_SELLER_BADGES) }), trust && _jsx(SellerTrustBadge, { trust: trust })] }), _jsx(OwnerQuickActions, { resource: "STORE", placement: "detail", editHref: String(ROUTES.ADMIN.STORE_DETAIL(store.storeSlug)), recordLabel: store.storeName }), _jsxs(Row, { textSize: "xs", className: "text-[var(--appkit-color-text-muted)] mb-0.5", gap: "3", children: [store.category && _jsx(Span, { transform: "capitalize", children: store.category }), store.totalProducts != null && store.totalProducts > 0 && (_jsxs(Span, { children: [store.totalProducts, " ", labels.products ?? "products"] })), store.totalReviews != null && store.totalReviews > 0 && (_jsxs(Span, { children: [store.totalReviews, " ", labels.reviews ?? "reviews"] })), store.itemsSold != null && store.itemsSold > 0 && (_jsxs(Span, { children: [store.itemsSold, " ", labels.sold ?? "sold"] }))] }), store.storeDescription && (_jsx(RichText, { html: normalizeRichTextHtml(store.storeDescription), copyableCode: true, className: "mt-0.5" }))] }), _jsxs(Row, { gap: "sm", align: "center", className: "shrink-0", children: [_jsx(ShareButton, { title: store.storeName }), onFollow && (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onFollow(store.storeSlug), className: CLS_FOLLOW_BTN, children: labels.follow ?? "Follow" }))] })] }), store.isVacationMode && (_jsx(Text, { className: CLS_WARN_BANNER, children: store.vacationMessage ??
18
19
  labels.vacationMode ??
19
20
  "Store is on vacation mode" }))] })] }));
20
21
  }
@@ -149,6 +149,29 @@ export class StoreRepository extends BaseRepository {
149
149
  // Auto-set isPublic when approved
150
150
  ...(status === StoreStatusValues.ACTIVE && { isPublic: true }),
151
151
  ...(status !== StoreStatusValues.ACTIVE && { isPublic: false }),
152
+ /*
153
+ * 🛑 LEAVING `active` REVOKES TRUST IMMEDIATELY — and this cannot
154
+ * wait for the monthly recompute.
155
+ *
156
+ * `trustedSeller` means the store is paid at SHIPPING rather than at
157
+ * delivery, so a suspended store that kept the flag would go on
158
+ * being paid before parcels land for up to 31 days, on a decision
159
+ * the suspension has just reversed. `sellerStandingRollup` caches
160
+ * its verdict monthly; this is the one place that cache must not be
161
+ * trusted.
162
+ *
163
+ * Both fields are tracked, so the clear earns its own timeline
164
+ * entry alongside the status change. Re-granting is NOT automatic on
165
+ * re-activation: the next month-end run decides, which is the
166
+ * correct direction for a flag that moves money.
167
+ *
168
+ * `extra` is spread AFTER, so an admin explicitly setting a tier in
169
+ * the same request still wins — the override stays authoritative.
170
+ */
171
+ ...(status !== StoreStatusValues.ACTIVE && {
172
+ trustedSeller: false,
173
+ sellerTier: "seller",
174
+ }),
152
175
  ...extra,
153
176
  updatedAt: new Date(),
154
177
  }, ctx, "setStatus"));
@@ -5,6 +5,7 @@
5
5
  * and helpers for the stores feature.
6
6
  */
7
7
  import type { StatusChangeEntry } from "../../../_internal/shared/history/types";
8
+ import type { SellerTier, SellerStandingCriterion } from "../../../_internal/shared/features/seller-standing/config";
8
9
  import type { StoreCapability } from "../../auth/permissions/constants";
9
10
  import type { BaseDocument } from "../../../_internal/shared/types/base-document";
10
11
  export type ShippingProviderType = "self-courier" | "store-pickup" | "custom";
@@ -141,6 +142,61 @@ export interface StoreDocument extends BaseDocument {
141
142
  * payout.
142
143
  */
143
144
  trustedSeller?: boolean;
145
+ /**
146
+ * The EARNED, PUBLIC tier.
147
+ *
148
+ * 🛑 Deliberately a second field rather than reusing `trustedSeller`, which
149
+ * means "paid at shipping" and is private by design (see directly above —
150
+ * "A store does not get to display that we trust it"). The monthly rollup
151
+ * writes BOTH together on a transition: `sellerTier` is what a buyer sees,
152
+ * `trustedSeller` is what the payout sweep reads. Collapsing them into one
153
+ * field would either leak the risk posture into the public projection or
154
+ * make the badge unrenderable.
155
+ *
156
+ * Absent means `"seller"`. There is no `"pending"` — a store that has not
157
+ * earned the tier simply has the base one.
158
+ */
159
+ sellerTier?: SellerTier;
160
+ /**
161
+ * The cached standing computation behind `sellerTier`.
162
+ *
163
+ * 🛑 This IS the cache. It is recomputed by `sellerStandingRollup` on the
164
+ * last day of each month and read straight off the store document by the
165
+ * seller's progress panel, the badge and the admin view — none of which
166
+ * recompute anything. `computedAt` / `nextComputeAt` are rendered to the
167
+ * seller, because without them a seller who shipped twenty orders this week
168
+ * will read an unchanged panel as broken rather than as monthly.
169
+ *
170
+ * Metrics only — no buyer uids, no addresses, nothing that could make this
171
+ * a PII store. The address criterion is counted from HMAC fingerprints and
172
+ * only the COUNT is kept.
173
+ */
174
+ standing?: {
175
+ computedAt: Date | string;
176
+ nextComputeAt?: Date | string;
177
+ windowDays: number;
178
+ deliveredOrders: number;
179
+ distinctBuyers: number;
180
+ distinctAddresses: number;
181
+ ratedOrders: number;
182
+ averageRating: number;
183
+ met: Record<SellerStandingCriterion, boolean>;
184
+ };
185
+ /**
186
+ * An admin set `sellerTier` by hand; the monthly rollup must not move it.
187
+ *
188
+ * 🛑 TOP-LEVEL, not a key inside `standing`, and the distinction is the
189
+ * point: `standing` is a MEASUREMENT — every field in it is recomputed from
190
+ * scratch each month — while this is a DECISION that must outlive the
191
+ * measurement. Nesting it meant a store could not be pinned without also
192
+ * fabricating a metrics block, which is why the platform store's seed could
193
+ * not express "trusted, don't touch" without inventing figures nobody had
194
+ * measured.
195
+ *
196
+ * PRIVATE. It tells a buyer nothing about the goods or the seller; it is a
197
+ * note about how we administer this record.
198
+ */
199
+ tierLockedByAdmin?: boolean;
144
200
  /** Reason recorded when `status` is set to "suspended". */
145
201
  suspensionReason?: string;
146
202
  stats?: {
@@ -381,7 +437,7 @@ export interface StoreDocument extends BaseDocument {
381
437
  * the store owner or the public, and `statusHistory` rides on the store
382
438
  * document the owner can read.
383
439
  */
384
- export declare const STORE_TRACKED_FIELDS: readonly ["status", "isPublic", "isVerified", "isFeatured", "suspensionReason", "capabilities"];
440
+ export declare const STORE_TRACKED_FIELDS: readonly ["status", "isPublic", "isVerified", "isFeatured", "suspensionReason", "capabilities", "sellerTier", "trustedSeller"];
385
441
  /**
386
442
  * PII on this document, for `withHistory`'s scrub. `whatsappConfig.accessToken`
387
443
  * is a live Meta credential; `mapDoc` decrypts it on EVERY read, so it is
@@ -418,6 +474,9 @@ export declare const STORE_FIELDS: {
418
474
  readonly IS_FEATURED: "isFeatured";
419
475
  readonly IS_VERIFIED: "isVerified";
420
476
  readonly TRUSTED_SELLER: "trustedSeller";
477
+ readonly SELLER_TIER: "sellerTier";
478
+ readonly STANDING: "standing";
479
+ readonly TIER_LOCKED_BY_ADMIN: "tierLockedByAdmin";
421
480
  readonly SUSPENSION_REASON: "suspensionReason";
422
481
  readonly CAPABILITIES: "capabilities";
423
482
  readonly STATUS_VALUES: {
@@ -31,6 +31,21 @@ export const STORE_TRACKED_FIELDS = [
31
31
  "isFeatured",
32
32
  "suspensionReason",
33
33
  "capabilities",
34
+ /*
35
+ * Both halves of the trust decision are tracked, and both matter for
36
+ * different reasons. `sellerTier` is what the seller and every buyer saw, so
37
+ * "when did I lose the badge" has an answer. `trustedSeller` moves MONEY —
38
+ * it is the flag that pays this store before delivery — so the timeline is
39
+ * the record of when the platform started and stopped carrying that risk,
40
+ * alongside the `STORE_TRUST_CHANGE` audit row that names who decided it.
41
+ *
42
+ * 🛑 `standing` is deliberately NOT tracked. It is recomputed in full every
43
+ * month, so every field inside it changes on every run; tracking it would
44
+ * write a diff of five numbers monthly and exhaust the 50-entry FIFO cap
45
+ * within four years while burying the two transitions anyone reads.
46
+ */
47
+ "sellerTier",
48
+ "trustedSeller",
34
49
  ];
35
50
  /**
36
51
  * PII on this document, for `withHistory`'s scrub. `whatsappConfig.accessToken`
@@ -72,6 +87,9 @@ export const STORE_FIELDS = {
72
87
  IS_FEATURED: "isFeatured",
73
88
  IS_VERIFIED: "isVerified",
74
89
  TRUSTED_SELLER: "trustedSeller",
90
+ SELLER_TIER: "sellerTier",
91
+ STANDING: "standing",
92
+ TIER_LOCKED_BY_ADMIN: "tierLockedByAdmin",
75
93
  SUSPENSION_REASON: "suspensionReason",
76
94
  CAPABILITIES: "capabilities",
77
95
  STATUS_VALUES: {
@@ -616,10 +616,10 @@ export declare const storeListItemSchema: z.ZodObject<{
616
616
  storeDescription?: string | undefined;
617
617
  storeLogoURL?: string | undefined;
618
618
  storeBannerURL?: string | undefined;
619
+ averageRating?: number | undefined;
619
620
  totalProducts?: number | undefined;
620
621
  itemsSold?: number | undefined;
621
622
  totalReviews?: number | undefined;
622
- averageRating?: number | undefined;
623
623
  }, {
624
624
  status: string;
625
625
  id: string;
@@ -632,10 +632,10 @@ export declare const storeListItemSchema: z.ZodObject<{
632
632
  storeDescription?: string | undefined;
633
633
  storeLogoURL?: string | undefined;
634
634
  storeBannerURL?: string | undefined;
635
+ averageRating?: number | undefined;
635
636
  totalProducts?: number | undefined;
636
637
  itemsSold?: number | undefined;
637
638
  totalReviews?: number | undefined;
638
- averageRating?: number | undefined;
639
639
  }>;
640
640
  export declare const storeListParamsSchema: z.ZodObject<{
641
641
  category: z.ZodOptional<z.ZodString>;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The store-creation input contract — ONE definition, shared by the form and
3
+ * the server action that writes it.
4
+ *
5
+ * 🛑 Why this file exists at all. `createStoreAction` carried its own inline
6
+ * `z.object({ storeName, storeDescription, storeCategory })` while nothing
7
+ * rendered a form, so there was exactly one copy and no drift. The moment a
8
+ * form exists there are two places that must agree about "2–80 characters",
9
+ * and a client that validates more loosely than the server produces a form
10
+ * that submits and then fails with a message pointing at no field; a client
11
+ * that validates more strictly refuses input the server would have accepted.
12
+ * Both are Root Cause #75's shape. The action imports this; it does not
13
+ * restate it.
14
+ *
15
+ * The bounds are not new — they are lifted verbatim from the action's own
16
+ * schema so this is a move, not a retune.
17
+ */
18
+ import { z } from "zod";
19
+ /**
20
+ * 🛑 `.or(z.literal(""))` on both optional fields, matching the action.
21
+ *
22
+ * An untouched text input submits `""`, not `undefined`. A bare
23
+ * `.max(80).optional()` rejects it, so a seller who filled in only the
24
+ * required name would be refused for leaving an optional field alone — which
25
+ * reads as a broken form, because the field they are being blocked on is one
26
+ * they were told was optional.
27
+ */
28
+ export declare const storeCreateSchema: z.ZodObject<{
29
+ storeName: z.ZodString;
30
+ storeDescription: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
31
+ storeCategory: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
32
+ }, "strip", z.ZodTypeAny, {
33
+ storeName: string;
34
+ storeCategory?: string | undefined;
35
+ storeDescription?: string | undefined;
36
+ }, {
37
+ storeName: string;
38
+ storeCategory?: string | undefined;
39
+ storeDescription?: string | undefined;
40
+ }>;
41
+ export type StoreCreateInput = z.infer<typeof storeCreateSchema>;
42
+ /** Field names, so a form and an error path cannot disagree by typo. */
43
+ export declare const STORE_CREATE_FIELDS: {
44
+ readonly STORE_NAME: "storeName";
45
+ readonly STORE_DESCRIPTION: "storeDescription";
46
+ readonly STORE_CATEGORY: "storeCategory";
47
+ };
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The store-creation input contract — ONE definition, shared by the form and
3
+ * the server action that writes it.
4
+ *
5
+ * 🛑 Why this file exists at all. `createStoreAction` carried its own inline
6
+ * `z.object({ storeName, storeDescription, storeCategory })` while nothing
7
+ * rendered a form, so there was exactly one copy and no drift. The moment a
8
+ * form exists there are two places that must agree about "2–80 characters",
9
+ * and a client that validates more loosely than the server produces a form
10
+ * that submits and then fails with a message pointing at no field; a client
11
+ * that validates more strictly refuses input the server would have accepted.
12
+ * Both are Root Cause #75's shape. The action imports this; it does not
13
+ * restate it.
14
+ *
15
+ * The bounds are not new — they are lifted verbatim from the action's own
16
+ * schema so this is a move, not a retune.
17
+ */
18
+ import { z } from "zod";
19
+ import { annotate } from "../../shell/field-ui-meta.js";
20
+ /**
21
+ * 🛑 `.or(z.literal(""))` on both optional fields, matching the action.
22
+ *
23
+ * An untouched text input submits `""`, not `undefined`. A bare
24
+ * `.max(80).optional()` rejects it, so a seller who filled in only the
25
+ * required name would be refused for leaving an optional field alone — which
26
+ * reads as a broken form, because the field they are being blocked on is one
27
+ * they were told was optional.
28
+ */
29
+ /*
30
+ * 🛑 ANNOTATED, and the annotations do not change what this parses.
31
+ *
32
+ * `annotate()` returns the same schema and registers its UI metadata in a side
33
+ * registry, so the server action validates exactly what it validated before.
34
+ * That is what lets ONE schema drive both the form's sections
35
+ * (`buildSectionsFromSchema`) and the server-side parse — the alternative is a
36
+ * second form-only schema, which is the drift this file exists to prevent.
37
+ *
38
+ * One section, because there is one: a store has a name, what it sells, and a
39
+ * category. `audit-form-sectionised` requires the fields to live in a section
40
+ * engine rather than a bare `<Form>`, which is also what gets this step the
41
+ * pinned mobile action bar every other form in the app has.
42
+ */
43
+ export const storeCreateSchema = z.object({
44
+ storeName: annotate(z
45
+ .string()
46
+ .min(2, "Store name must be at least 2 characters")
47
+ .max(80, "Store name must be 80 characters or fewer"), {
48
+ section: "store",
49
+ sectionLabel: "Your store",
50
+ sectionRequired: true,
51
+ quick: true,
52
+ order: 1,
53
+ row: "full",
54
+ label: "Store name",
55
+ help: "Buyers see this. It also forms your store's web address.",
56
+ }),
57
+ storeDescription: annotate(z
58
+ .string()
59
+ .max(10000, "Description must be 10,000 characters or fewer")
60
+ .optional()
61
+ .or(z.literal("")), {
62
+ section: "store",
63
+ order: 2,
64
+ row: "full",
65
+ kind: "textarea",
66
+ label: "What do you sell?",
67
+ help: "Optional. A short introduction for your storefront page.",
68
+ }),
69
+ storeCategory: annotate(z
70
+ .string()
71
+ .max(80, "Category must be 80 characters or fewer")
72
+ .optional()
73
+ .or(z.literal("")), {
74
+ section: "store",
75
+ order: 3,
76
+ row: "full",
77
+ label: "Main category",
78
+ help: "Optional. Helps buyers browsing that category find your store.",
79
+ }),
80
+ });
81
+ /** Field names, so a form and an error path cannot disagree by typo. */
82
+ export const STORE_CREATE_FIELDS = {
83
+ STORE_NAME: "storeName",
84
+ STORE_DESCRIPTION: "storeDescription",
85
+ STORE_CATEGORY: "storeCategory",
86
+ };
@@ -1,4 +1,5 @@
1
1
  import type { ListingType } from "../../products/types";
2
+ import type { SellerTier, SellerStandingCriterion } from "../../../_internal/shared/features/seller-standing/config";
2
3
  export interface StoreListItem {
3
4
  id: string;
4
5
  storeSlug: string;
@@ -43,8 +44,49 @@ export interface StoreListItem {
43
44
  */
44
45
  sharedPurchaseHandlingFee?: number;
45
46
  sharedPurchaseExtraShippingPerDestination?: number;
47
+ /**
48
+ * Seller trust, as a buyer sees it.
49
+ *
50
+ * 🛑 Neither is optional, for the same reason as the flags above: the
51
+ * projection resolves absence (`?? false` / `?? "seller"`), so no client has
52
+ * to decide what `undefined` means. A badge component testing `!== "seller"`
53
+ * on an undefined value would render a TRUSTED badge on a store that has
54
+ * earned nothing.
55
+ *
56
+ * 🛑 There is deliberately no `trustedSeller` here and there must never be.
57
+ * That field means "we pay this store before delivery" — our risk posture,
58
+ * not an endorsement — and it is in `PRIVATE_STORE_FIELDS` for that reason.
59
+ */
60
+ isVerified: boolean;
61
+ sellerTier: SellerTier;
62
+ /**
63
+ * The cached standing metrics behind `sellerTier`, when they have ever been
64
+ * computed.
65
+ *
66
+ * OPTIONAL, unlike the two above, and that asymmetry is the point: absent
67
+ * means "never measured", which the seller's panel must render differently
68
+ * from a measured zero. Defaulting it would turn "we have not looked yet"
69
+ * into a confident "0 of 25".
70
+ */
71
+ standing?: SellerStandingSnapshot;
46
72
  createdAt?: string;
47
73
  }
74
+ /**
75
+ * The public half of `StoreDocument.standing` — counts and a mean, nothing
76
+ * that identifies a buyer. The address criterion is counted from HMAC
77
+ * fingerprints and only the count is kept.
78
+ */
79
+ export interface SellerStandingSnapshot {
80
+ computedAt: Date | string;
81
+ nextComputeAt?: Date | string;
82
+ windowDays: number;
83
+ deliveredOrders: number;
84
+ distinctBuyers: number;
85
+ distinctAddresses: number;
86
+ ratedOrders: number;
87
+ averageRating: number;
88
+ met: Record<SellerStandingCriterion, boolean>;
89
+ }
48
90
  export interface StoreDetail extends StoreListItem {
49
91
  bio?: string;
50
92
  location?: string;
@@ -71,6 +71,7 @@ export declare const DEFAULT_ROUTE_MAP: {
71
71
  readonly HELP_AUCTIONS: "/help/auctions";
72
72
  readonly HELP_ORDERS: "/help/orders";
73
73
  readonly HELP_ACCOUNT: "/help/account";
74
+ readonly HELP_SELLER_BADGES: "/help/seller-badges";
74
75
  readonly CREDITS: "/credits";
75
76
  readonly TERMS: "/terms";
76
77
  readonly PRIVACY: "/privacy";
@@ -581,6 +582,7 @@ export declare const ROUTES: {
581
582
  readonly HELP_AUCTIONS: "/help/auctions";
582
583
  readonly HELP_ORDERS: "/help/orders";
583
584
  readonly HELP_ACCOUNT: "/help/account";
585
+ readonly HELP_SELLER_BADGES: "/help/seller-badges";
584
586
  readonly CREDITS: "/credits";
585
587
  readonly TERMS: "/terms";
586
588
  readonly PRIVACY: "/privacy";
@@ -1123,6 +1125,6 @@ export declare const SELLER_ROUTES: {
1123
1125
  readonly GUIDE_CAPABILITIES: "/store/guide/capabilities";
1124
1126
  readonly GUIDE_SETTINGS: "/store/guide/settings";
1125
1127
  };
1126
- export declare const PUBLIC_ROUTES: readonly ["/", string, string, string, string, string, string, string, string, string, string, string, string, string, string, string, string, string, string, string, string, "/unauthorized", "/auth/login", "/auth/register", "/auth/forgot-password", "/auth/reset-password", "/auth/verify-email"];
1128
+ export declare const PUBLIC_ROUTES: readonly ["/", string, string, string, string, string, string, string, string, string, string, string, string, string, string, string, string, string, string, string, string, string, "/unauthorized", "/auth/login", "/auth/register", "/auth/forgot-password", "/auth/reset-password", "/auth/verify-email"];
1127
1129
  export declare const PROTECTED_ROUTES: readonly [string, string, string, string, string, string, string];
1128
1130
  export declare const AUTH_ROUTES: readonly ["/auth/login", "/auth/register"];