@mohasinac/appkit 4.14.0 → 4.15.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 (157) hide show
  1. package/dist/_internal/server/features/auctions/actions.js +2 -2
  2. package/dist/_internal/server/features/cart/service.d.ts +3 -3
  3. package/dist/_internal/server/features/checkout/actions.js +19 -0
  4. package/dist/_internal/server/features/checkout/source-context.d.ts +5 -0
  5. package/dist/_internal/server/features/checkout/source-context.js +157 -0
  6. package/dist/_internal/server/features/offers/adapters.d.ts +103 -0
  7. package/dist/_internal/server/features/offers/adapters.js +88 -0
  8. package/dist/_internal/server/features/offers/index.d.ts +2 -0
  9. package/dist/_internal/server/features/offers/index.js +1 -0
  10. package/dist/_internal/server/features/orders/adapters.js +24 -0
  11. package/dist/_internal/server/jobs/core/offerExpiry.js +10 -10
  12. package/dist/_internal/shared/actions/action-registry.js +3 -1
  13. package/dist/_internal/shared/features/auctions/schema.d.ts +14 -2
  14. package/dist/_internal/shared/features/auctions/schema.js +13 -1
  15. package/dist/_internal/shared/features/cart/schema.d.ts +14 -9
  16. package/dist/_internal/shared/features/cart/schema.js +15 -0
  17. package/dist/_internal/shared/features/products/schema.d.ts +2 -2
  18. package/dist/_internal/shared/history/history.d.ts +22 -0
  19. package/dist/_internal/shared/history/history.js +41 -1
  20. package/dist/client.d.ts +13 -2
  21. package/dist/client.js +20 -2
  22. package/dist/constants/field-names.d.ts +8 -6
  23. package/dist/constants/field-names.js +48 -6
  24. package/dist/features/account/components/UserBidsView.js +12 -4
  25. package/dist/features/account/components/UserReturnsView.d.ts +3 -0
  26. package/dist/features/account/components/UserReturnsView.js +17 -1
  27. package/dist/features/admin/components/AdminAuditLogView.js +1 -0
  28. package/dist/features/admin/components/AdminBidsView.js +15 -2
  29. package/dist/features/admin/components/AdminBlogEditorView.js +65 -33
  30. package/dist/features/admin/components/AdminFeatureEditorView.js +35 -6
  31. package/dist/features/admin/components/AdminNavEditorView.js +15 -5
  32. package/dist/features/admin/components/AdminNewsletterView.js +18 -2
  33. package/dist/features/admin/components/AdminOffersView.js +62 -18
  34. package/dist/features/admin/components/AdminPaymentMethodsView.js +24 -4
  35. package/dist/features/admin/components/AdminScammerEditorView.d.ts +2 -1
  36. package/dist/features/admin/components/AdminScammerEditorView.js +3 -13
  37. package/dist/features/admin/components/AdminScammersView.js +3 -8
  38. package/dist/features/admin/components/AdminSectionsView.js +32 -23
  39. package/dist/features/admin/components/AdminSessionsView.js +17 -2
  40. package/dist/features/admin/components/ViewAuditLogEntryModal.js +1 -0
  41. package/dist/features/admin/components/sections/adminSectionsTypes.d.ts +4 -2
  42. package/dist/features/admin/components/sections/adminSectionsTypes.js +45 -27
  43. package/dist/features/admin/constants/filter-tabs.d.ts +3 -0
  44. package/dist/features/admin/constants/filter-tabs.js +1 -0
  45. package/dist/features/admin/schemas/homepage-section-form.d.ts +22 -0
  46. package/dist/features/admin/schemas/homepage-section-form.js +68 -0
  47. package/dist/features/admin/schemas/nav-item-form.d.ts +74 -0
  48. package/dist/features/admin/schemas/nav-item-form.js +72 -0
  49. package/dist/features/auctions/components/AuctionBidsTable.d.ts +10 -1
  50. package/dist/features/auctions/components/AuctionBidsTable.js +22 -4
  51. package/dist/features/auctions/components/bid-detail-fields.d.ts +29 -0
  52. package/dist/features/auctions/components/bid-detail-fields.js +103 -0
  53. package/dist/features/audit-log/schemas/firestore.d.ts +14 -0
  54. package/dist/features/audit-log/schemas/firestore.js +7 -0
  55. package/dist/features/blog/schemas/blog-form.d.ts +80 -0
  56. package/dist/features/blog/schemas/blog-form.js +100 -0
  57. package/dist/features/events/components/AdminEventEditorView.js +32 -54
  58. package/dist/features/events/components/AdminEventEntriesView.js +22 -3
  59. package/dist/features/events/schemas/event-form.d.ts +113 -0
  60. package/dist/features/events/schemas/event-form.js +82 -0
  61. package/dist/features/grouped/schemas/grouped-listing-form.d.ts +101 -0
  62. package/dist/features/grouped/schemas/grouped-listing-form.js +89 -0
  63. package/dist/features/homepage/actions/homepage-section-actions.d.ts +11 -3
  64. package/dist/features/homepage/actions/homepage-section-actions.js +33 -1
  65. package/dist/features/homepage/schemas/index.d.ts +31 -0
  66. package/dist/features/homepage/schemas/index.js +24 -0
  67. package/dist/features/orders/components/OrderStatusTimeline.d.ts +5 -1
  68. package/dist/features/orders/components/OrderStatusTimeline.js +39 -20
  69. package/dist/features/orders/repository/orders.repository.js +5 -0
  70. package/dist/features/orders/schemas/firestore.d.ts +36 -1
  71. package/dist/features/orders/types/index.d.ts +35 -7
  72. package/dist/features/products/components/GroupSettingsPanel.js +5 -1
  73. package/dist/features/products/schemas/index.d.ts +2 -2
  74. package/dist/features/products/schemas/product-features.validators.d.ts +73 -0
  75. package/dist/features/products/schemas/product-features.validators.js +63 -0
  76. package/dist/features/scams/constants/scammer-status-display.d.ts +28 -0
  77. package/dist/features/scams/constants/scammer-status-display.js +79 -0
  78. package/dist/features/seller/actions/offer-actions.js +32 -2
  79. package/dist/features/seller/components/OfferPhaseTimeline.d.ts +59 -0
  80. package/dist/features/seller/components/OfferPhaseTimeline.js +107 -0
  81. package/dist/features/seller/components/SellerBidsView.js +11 -4
  82. package/dist/features/seller/components/SellerCouponEditorView.js +17 -17
  83. package/dist/features/seller/components/SellerOffersView.js +2 -1
  84. package/dist/features/seller/components/SellerPayoutSettingsView.js +38 -14
  85. package/dist/features/seller/repository/offer.repository.d.ts +69 -8
  86. package/dist/features/seller/repository/offer.repository.js +127 -23
  87. package/dist/features/seller/schemas/coupon-form.d.ts +72 -0
  88. package/dist/features/seller/schemas/coupon-form.js +116 -0
  89. package/dist/features/seller/schemas/firestore.d.ts +56 -2
  90. package/dist/features/seller/schemas/firestore.js +27 -0
  91. package/dist/features/seller/schemas/offer-forms.d.ts +21 -0
  92. package/dist/features/seller/schemas/offer-forms.js +21 -0
  93. package/dist/features/shell/build-sections.d.ts +63 -0
  94. package/dist/features/shell/build-sections.js +207 -0
  95. package/dist/features/shell/field-ui-meta.d.ts +115 -0
  96. package/dist/features/shell/field-ui-meta.js +324 -0
  97. package/dist/features/shell/index.d.ts +4 -0
  98. package/dist/features/shell/index.js +2 -0
  99. package/dist/features/status-history/components/StatusTimeline.d.ts +74 -0
  100. package/dist/features/status-history/components/StatusTimeline.js +46 -0
  101. package/dist/features/status-history/index.d.ts +11 -0
  102. package/dist/features/status-history/index.js +10 -0
  103. package/dist/features/store-extensions/components/ReviewDecisionModal.d.ts +27 -0
  104. package/dist/features/store-extensions/components/ReviewDecisionModal.js +82 -0
  105. package/dist/features/store-extensions/index.d.ts +12 -0
  106. package/dist/features/store-extensions/index.js +12 -0
  107. package/dist/features/store-extensions/schemas/admin-notification-form.d.ts +36 -0
  108. package/dist/features/store-extensions/schemas/admin-notification-form.js +114 -0
  109. package/dist/features/store-extensions/schemas/analytics-forms.d.ts +57 -0
  110. package/dist/features/store-extensions/schemas/analytics-forms.js +158 -0
  111. package/dist/features/store-extensions/schemas/custom-role-form.d.ts +85 -0
  112. package/dist/features/store-extensions/schemas/custom-role-form.js +118 -0
  113. package/dist/features/store-extensions/schemas/firestore.d.ts +21 -1
  114. package/dist/features/store-extensions/schemas/firestore.js +60 -0
  115. package/dist/features/store-extensions/schemas/google-config-form.d.ts +53 -0
  116. package/dist/features/store-extensions/schemas/google-config-form.js +105 -0
  117. package/dist/features/store-extensions/schemas/item-request-create-form.d.ts +28 -0
  118. package/dist/features/store-extensions/schemas/item-request-create-form.js +80 -0
  119. package/dist/features/store-extensions/schemas/listing-template-form.d.ts +78 -0
  120. package/dist/features/store-extensions/schemas/listing-template-form.js +87 -0
  121. package/dist/features/store-extensions/schemas/moderation-review-form.d.ts +49 -0
  122. package/dist/features/store-extensions/schemas/moderation-review-form.js +101 -0
  123. package/dist/features/store-extensions/schemas/payout-method-form.d.ts +153 -0
  124. package/dist/features/store-extensions/schemas/payout-method-form.js +155 -0
  125. package/dist/features/store-extensions/schemas/report-create-form.d.ts +25 -0
  126. package/dist/features/store-extensions/schemas/report-create-form.js +79 -0
  127. package/dist/features/store-extensions/schemas/report-review-form.d.ts +47 -0
  128. package/dist/features/store-extensions/schemas/report-review-form.js +111 -0
  129. package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +148 -0
  130. package/dist/features/store-extensions/schemas/shipping-config-form.js +104 -0
  131. package/dist/features/store-extensions/schemas/store-category-form.d.ts +54 -0
  132. package/dist/features/store-extensions/schemas/store-category-form.js +73 -0
  133. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +326 -0
  134. package/dist/index.d.ts +12 -2
  135. package/dist/index.js +12 -2
  136. package/dist/next/routing/route-map.d.ts +16 -2
  137. package/dist/next/routing/route-map.js +9 -2
  138. package/dist/schemas/firestore-value.d.ts +8 -0
  139. package/dist/schemas/firestore-value.js +44 -0
  140. package/dist/schemas/index.d.ts +1 -1
  141. package/dist/schemas/index.js +1 -1
  142. package/dist/schemas/registry.d.ts +634 -1
  143. package/dist/schemas/registry.js +64 -1
  144. package/dist/seed/bids-seed-data.js +30 -0
  145. package/dist/seed/offers-seed-data.js +212 -7
  146. package/dist/seed/orders-seed-data.js +336 -1
  147. package/dist/seed/products-auctions-seed-data.js +41 -0
  148. package/dist/server.d.ts +2 -0
  149. package/dist/server.js +5 -0
  150. package/dist/styles.css +1 -1
  151. package/dist/tailwind-utilities.css +1 -1
  152. package/dist/ui/components/RecordDetailModal.d.ts +10 -1
  153. package/dist/ui/components/RecordDetailModal.js +2 -2
  154. package/package.json +2 -1
  155. package/scripts/node-esm-loader.mjs +65 -0
  156. package/scripts/seed-cli-loader.mjs +6 -49
  157. package/scripts/seed-cli.mjs +1 -1
@@ -0,0 +1,74 @@
1
+ /**
2
+ * StatusTimeline — the one rail that renders any entity's status history.
3
+ *
4
+ * Both the order timeline and the offer timeline were about to be written
5
+ * separately, against the same `StatusChangeEntry[]` shape, with the same
6
+ * dot-and-connector rail and the same two load-bearing rules. That is the
7
+ * duplication the Rule of Three exists to prevent, so this is the single
8
+ * implementation and each feature supplies only its own phase vocabulary.
9
+ *
10
+ * ## Two rules this component must never break
11
+ *
12
+ * **1. Never fabricate a timestamp.** A step with no recorded date renders an
13
+ * em-dash. Not `new Date()`, and not a proxy field: `updatedAt` means "last
14
+ * write of any kind" and `expiresAt` is a deadline, not an event. An offer
15
+ * that expired has no timestamp at all because `expireMany` never wrote one
16
+ * before this wave, and inventing one would make the record lie.
17
+ *
18
+ * **2. It never fetches.** A client component that loaded its own chain would
19
+ * fire one request per opened modal per portal. The API returns the chain;
20
+ * this renders it.
21
+ */
22
+ import type { ReactNode } from "react";
23
+ import type { FieldChange } from "../../../_internal/shared/history/index";
24
+ /** The client-facing entry shape — dates already serialised to ISO. */
25
+ export interface TimelineEntry {
26
+ at: string;
27
+ actorUid?: string;
28
+ actorRole: "buyer" | "seller" | "admin" | "system";
29
+ changes: Record<string, FieldChange>;
30
+ reason?: string;
31
+ note?: string;
32
+ trigger: string;
33
+ }
34
+ export interface TimelineStep {
35
+ key: string;
36
+ label: string;
37
+ /** ISO string, or absent — absent renders an em-dash, never a guess. */
38
+ date?: string;
39
+ /** Terminal-negative: cancelled, declined, rejected. Renders in error tone. */
40
+ negative?: boolean;
41
+ /**
42
+ * Neutral-but-ended, e.g. a superseded offer round. Deliberately distinct
43
+ * from `negative`: the buyer did not walk away, they countered, and the
44
+ * negotiation continued.
45
+ */
46
+ muted?: boolean;
47
+ actorRole?: TimelineEntry["actorRole"];
48
+ reason?: string;
49
+ note?: string;
50
+ /** Round divider text for multi-round chains ("Round 2"). */
51
+ roundLabel?: string;
52
+ }
53
+ export interface StatusTimelineProps {
54
+ title?: string;
55
+ steps: TimelineStep[];
56
+ /** Entries trimmed off the front of the capped history, if any. */
57
+ truncatedCount?: number;
58
+ /** Rendered below the rail — tracking details, chain summary, etc. */
59
+ footer?: ReactNode;
60
+ /** Suppress `footer` when the last step is terminal-negative. */
61
+ hideFooterWhenNegative?: boolean;
62
+ emptyLabel?: string;
63
+ className?: string;
64
+ }
65
+ export declare function StatusTimeline({ title, steps, truncatedCount, footer, hideFooterWhenNegative, emptyLabel, className, }: StatusTimelineProps): import("react").JSX.Element;
66
+ /**
67
+ * Turn recorded history entries into steps.
68
+ *
69
+ * Only entries that actually changed `status` become steps — an entry
70
+ * recording a tracking number or a payment-review outcome is real history but
71
+ * is not a phase, and putting every entry on the rail would bury the five
72
+ * transitions anyone reads.
73
+ */
74
+ export declare function stepsFromEntries(entries: TimelineEntry[] | undefined, labelFor: (status: string) => string, isNegative: (status: string) => boolean): TimelineStep[];
@@ -0,0 +1,46 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Badge, Div, Heading, Row, Stack, Text } from "../../../ui";
4
+ const ACTOR_LABEL = {
5
+ buyer: "Buyer",
6
+ seller: "Store",
7
+ admin: "Admin",
8
+ system: "System",
9
+ };
10
+ export function StatusTimeline({ title = "Timeline", steps, truncatedCount, footer, hideFooterWhenNegative = true, emptyLabel = "No history yet", className = "", }) {
11
+ const isTerminalNegative = steps[steps.length - 1]?.negative === true;
12
+ const showFooter = footer && !(hideFooterWhenNegative && isTerminalNegative);
13
+ return (_jsxs(Div, { className: className, rounded: "xl", padding: "md", surface: "muted", border: "default", children: [_jsx(Heading, { level: 3, size: "sm", weight: "semibold", color: "muted", className: "mb-3", children: title }), truncatedCount && truncatedCount > 0 ? (
14
+ // Say what is not being shown. Silent truncation reads as "this is all
15
+ // of it", which is the same lie as a silently dropped field.
16
+ _jsxs(Text, { size: "xs", color: "muted", className: "mb-2", children: [truncatedCount, " earlier ", truncatedCount === 1 ? "entry" : "entries", " trimmed"] })) : null, steps.length === 0 ? (_jsx(Badge, { variant: "default", children: emptyLabel })) : (_jsx(Stack, { gap: "none", children: steps.map((step, i) => {
17
+ const isLast = i === steps.length - 1;
18
+ return (_jsxs(Stack, { gap: "none", children: [step.roundLabel && (_jsx(Text, { size: "xs", weight: "semibold", color: "muted", className: "mb-1 mt-2", children: step.roundLabel })), _jsxs(Row, { gap: "sm", padding: isLast ? "none" : "b-md", children: [_jsxs(Stack, { gap: "none", align: "center", className: "shrink-0", children: [_jsx(Div, { className: `h-2.5 w-2.5 ${step.negative || step.muted ? "" : "bg-[var(--appkit-color-primary)]"}`, rounded: "full", surface: step.negative ? "danger-surface" : step.muted ? "subtle" : "none" }), !isLast && _jsx(Div, { className: "w-px flex-1 min-h-[24px]", surface: "subtle" })] }), _jsxs(Stack, { gap: "none", className: "min-w-0 flex-1", children: [_jsxs(Row, { gap: "xs", align: "center", wrap: true, children: [_jsx(Text, { size: "sm", weight: "medium", className: step.negative ? "text-error" : undefined, color: step.muted ? "muted" : undefined, children: step.label }), step.actorRole && step.actorRole !== "system" && (_jsx(Badge, { variant: "default", size: "xs", children: ACTOR_LABEL[step.actorRole] }))] }), _jsx(Text, { size: "xs", color: "muted", children: step.date ? new Date(step.date).toLocaleString() : "—" }), step.reason && (_jsx(Text, { size: "xs", color: "muted", className: "mt-0.5", children: step.reason })), step.note && (_jsxs(Text, { size: "xs", color: "muted", italic: true, className: "mt-0.5", children: ["\u201C", step.note, "\u201D"] }))] })] })] }, `${step.key}-${i}`));
19
+ }) })), showFooter ? (_jsx(Div, { className: "mt-3 border-t border-[var(--appkit-color-border)]", padding: "t-sm", children: footer })) : null] }));
20
+ }
21
+ /**
22
+ * Turn recorded history entries into steps.
23
+ *
24
+ * Only entries that actually changed `status` become steps — an entry
25
+ * recording a tracking number or a payment-review outcome is real history but
26
+ * is not a phase, and putting every entry on the rail would bury the five
27
+ * transitions anyone reads.
28
+ */
29
+ export function stepsFromEntries(entries, labelFor, isNegative) {
30
+ if (!entries?.length)
31
+ return [];
32
+ return entries
33
+ .filter((e) => "status" in e.changes)
34
+ .map((e) => {
35
+ const to = String(e.changes.status?.to ?? "");
36
+ return {
37
+ key: to,
38
+ label: labelFor(to),
39
+ date: e.at,
40
+ negative: isNegative(to),
41
+ actorRole: e.actorRole,
42
+ reason: e.reason,
43
+ note: e.note,
44
+ };
45
+ });
46
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Status history — the shared timeline rail for any entity that records how
3
+ * it reached its current state (orders, offers, and the 8 more adopting the
4
+ * primitive in W15).
5
+ *
6
+ * Deliberately NOT `features/history/` — that name was already taken by the
7
+ * buyer's browsing-history feature (`useHistory`, guest-history utils), which
8
+ * is an entirely unrelated concept.
9
+ */
10
+ export { StatusTimeline, stepsFromEntries } from "./components/StatusTimeline";
11
+ export type { StatusTimelineProps, TimelineEntry, TimelineStep } from "./components/StatusTimeline";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Status history — the shared timeline rail for any entity that records how
3
+ * it reached its current state (orders, offers, and the 8 more adopting the
4
+ * primitive in W15).
5
+ *
6
+ * Deliberately NOT `features/history/` — that name was already taken by the
7
+ * buyer's browsing-history feature (`useHistory`, guest-history utils), which
8
+ * is an entirely unrelated concept.
9
+ */
10
+ export { StatusTimeline, stepsFromEntries } from "./components/StatusTimeline";
@@ -0,0 +1,27 @@
1
+ import type { ZodType } from "zod";
2
+ /**
3
+ * Generic over the schema's OUTPUT type, so a caller's `onConfirm` receives
4
+ * `ModerationReviewFormValues` / `ReportReviewFormValues` rather than an
5
+ * untyped bag. A `Record<string, unknown>` here would put the very seam this
6
+ * component exists to close straight back into the callback signature.
7
+ */
8
+ export interface ReviewDecisionModalProps<TValues> {
9
+ isOpen: boolean;
10
+ onClose: () => void;
11
+ /** Modal heading, e.g. "Reject media". */
12
+ title: string;
13
+ /** The schema BOTH this modal and its route validate against. */
14
+ schema: ZodType<TValues>;
15
+ /** The status this decision sets — parsed as part of the payload. */
16
+ status: string;
17
+ /** Which key carries the note: `reason` for moderation, `resolution` for reports. */
18
+ noteField: string;
19
+ noteLabel: string;
20
+ noteHelp?: string;
21
+ notePlaceholder?: string;
22
+ confirmLabel: string;
23
+ confirmVariant?: "primary" | "danger";
24
+ /** Called only once the payload has parsed cleanly, with the PARSED value. */
25
+ onConfirm: (values: TValues) => Promise<void>;
26
+ }
27
+ export declare function ReviewDecisionModal<TValues>({ isOpen, onClose, title, schema, status, noteField, noteLabel, noteHelp, notePlaceholder, confirmLabel, confirmVariant, onConfirm, }: ReviewDecisionModalProps<TValues>): import("react").JSX.Element;
@@ -0,0 +1,82 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /*
4
+ * WHY: Four surfaces asked a moderator to justify a decision, and not one of
5
+ * them checked that they had. `admin/moderation/page.tsx` and
6
+ * `ModerationDetailActions` each rendered their own reject-reason modal;
7
+ * `admin/reports/page.tsx` and `ReportDetailActions` each rendered their
8
+ * own resolution-note modal. All four submitted an empty string happily,
9
+ * so media could be blocked and a report closed with no record of why.
10
+ *
11
+ * WHAT: One modal for both decisions.
12
+ *
13
+ * ## Why one component and not two
14
+ *
15
+ * The two are the same observable thing — *a decision plus a note that is
16
+ * required before it can be made* — differing only in vocabulary. Per the
17
+ * Duplication Decision Framework this is a consolidate, on two counts: the
18
+ * copies have the same prop surface, and a single validation fix would
19
+ * otherwise have needed four commits. The differences are all DATA (labels,
20
+ * which field carries the note, which schema validates it), not branching, so
21
+ * there are no conditional props to trade away.
22
+ *
23
+ * ## The note field is named by the caller
24
+ *
25
+ * Moderation calls it `reason`, reports call it `resolution`. Naming it
26
+ * generically here would mean translating on the way out, and a schema issue
27
+ * on `resolution` would then have no field to land on — the error would fall
28
+ * back to a banner, which is the thing this replaces.
29
+ *
30
+ * EXPORTS: ReviewDecisionModal, type ReviewDecisionModalProps
31
+ *
32
+ * @tag domain:store-extensions,moderation,reports
33
+ * @tag layer:component
34
+ * @tag pattern:form
35
+ * @tag access:client
36
+ * @tag consumers:admin/moderation,admin/reports
37
+ * @tag sideEffects:none
38
+ */
39
+ import { useState } from "react";
40
+ import { Modal } from "../../../ui/components/Modal";
41
+ import { Button } from "../../../ui/components/Button";
42
+ import { Row } from "../../../ui/components/Layout";
43
+ import { Stack } from "../../../ui/components/Layout";
44
+ import { Form } from "../../../ui/components/Form";
45
+ import { FieldTextarea } from "../../../ui/forms/FieldTextarea";
46
+ import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
47
+ import { applyZodIssues } from "../../../ui/forms/FormShell";
48
+ import { useToast } from "../../../ui/components/Toast";
49
+ import { normalizeError } from "../../../errors/normalize";
50
+ export function ReviewDecisionModal({ isOpen, onClose, title, schema, status, noteField, noteLabel, noteHelp, notePlaceholder, confirmLabel, confirmVariant = "primary", onConfirm, }) {
51
+ const { showToast } = useToast();
52
+ const [note, setNote] = useState("");
53
+ const [submitting, setSubmitting] = useState(false);
54
+ const close = () => {
55
+ setNote("");
56
+ onClose();
57
+ };
58
+ return (_jsx(Modal, { isOpen: isOpen, onClose: close, title: title, size: "sm", children: _jsx(Form, { schema: schema, onSubmit: (e) => e.preventDefault(), children: ({ setFieldError, clearErrors }) => (_jsxs(Stack, { gap: "md", children: [_jsx(FormErrorSummary, {}), _jsx(FieldTextarea, { name: noteField, label: noteLabel, hint: noteHelp, rows: 4, required: true, value: note, onChange: setNote, placeholder: notePlaceholder }), _jsxs(Row, { justify: "end", gap: "sm", children: [_jsx(Button, { variant: "ghost", type: "button", onClick: close, disabled: submitting, children: "Cancel" }), _jsx(Button, { variant: confirmVariant, type: "submit", disabled: submitting, isLoading: submitting, onClick: async () => {
59
+ clearErrors();
60
+ const payload = { status, [noteField]: note };
61
+ const parsed = schema.safeParse(payload);
62
+ if (!parsed.success) {
63
+ applyZodIssues(parsed.error.issues, setFieldError);
64
+ return;
65
+ }
66
+ setSubmitting(true);
67
+ try {
68
+ await onConfirm(parsed.data);
69
+ close();
70
+ }
71
+ catch (err) {
72
+ // Deliberately stays OPEN on failure. Closing would discard
73
+ // the note the reviewer just typed, and a decision that did
74
+ // not save must not look like one that did.
75
+ void normalizeError(err);
76
+ showToast("Couldn't save that decision. Try again.", "error");
77
+ }
78
+ finally {
79
+ setSubmitting(false);
80
+ }
81
+ }, children: confirmLabel })] })] })) }) }));
82
+ }
@@ -1,4 +1,16 @@
1
1
  export * from "./schemas/firestore";
2
+ export * from "./schemas/store-category-form";
3
+ export * from "./schemas/listing-template-form";
4
+ export * from "./schemas/payout-method-form";
5
+ export * from "./schemas/shipping-config-form";
6
+ export * from "./schemas/custom-role-form";
7
+ export * from "./schemas/moderation-review-form";
8
+ export * from "./schemas/report-review-form";
9
+ export * from "./schemas/report-create-form";
10
+ export * from "./schemas/item-request-create-form";
11
+ export * from "./schemas/analytics-forms";
12
+ export * from "./schemas/google-config-form";
13
+ export * from "./schemas/admin-notification-form";
2
14
  export * from "./schemas/rbac";
3
15
  export * from "./repository/store-extensions.repositories";
4
16
  export * from "./repository/rbac.repositories";
@@ -1,4 +1,16 @@
1
1
  export * from "./schemas/firestore";
2
+ export * from "./schemas/store-category-form";
3
+ export * from "./schemas/listing-template-form";
4
+ export * from "./schemas/payout-method-form";
5
+ export * from "./schemas/shipping-config-form";
6
+ export * from "./schemas/custom-role-form";
7
+ export * from "./schemas/moderation-review-form";
8
+ export * from "./schemas/report-review-form";
9
+ export * from "./schemas/report-create-form";
10
+ export * from "./schemas/item-request-create-form";
11
+ export * from "./schemas/analytics-forms";
12
+ export * from "./schemas/google-config-form";
13
+ export * from "./schemas/admin-notification-form";
2
14
  export * from "./schemas/rbac";
3
15
  export * from "./repository/store-extensions.repositories";
4
16
  export * from "./repository/rbac.repositories";
@@ -0,0 +1,36 @@
1
+ import { z } from "zod";
2
+ import type { AdminNotificationCategory, AdminNotificationDocument } from "./rbac";
3
+ export declare const ADMIN_NOTIFICATION_CATEGORIES: [AdminNotificationCategory, ...AdminNotificationCategory[]];
4
+ type Severity = AdminNotificationDocument["severity"];
5
+ export declare const ADMIN_NOTIFICATION_SEVERITIES: [Severity, ...Severity[]];
6
+ /**
7
+ * 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
8
+ * by schema instance and every zod wrapper returns a new one.
9
+ */
10
+ export declare const adminNotificationCreateSchema: z.ZodObject<{
11
+ title: z.ZodString;
12
+ body: z.ZodString;
13
+ category: z.ZodEnum<[AdminNotificationCategory, ...AdminNotificationCategory[]]>;
14
+ severity: z.ZodEnum<["warning" | "error" | "info", ...("warning" | "error" | "info")[]]>;
15
+ audienceUserIds: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
16
+ entityType: z.ZodOptional<z.ZodString>;
17
+ entityId: z.ZodOptional<z.ZodString>;
18
+ }, "strict", z.ZodTypeAny, {
19
+ title: string;
20
+ body: string;
21
+ category: AdminNotificationCategory;
22
+ severity: "warning" | "error" | "info";
23
+ entityId?: string | undefined;
24
+ entityType?: string | undefined;
25
+ audienceUserIds?: string[] | undefined;
26
+ }, {
27
+ title: string;
28
+ body: string;
29
+ category: AdminNotificationCategory;
30
+ severity: "warning" | "error" | "info";
31
+ entityId?: string | undefined;
32
+ entityType?: string | undefined;
33
+ audienceUserIds?: string[] | undefined;
34
+ }>;
35
+ export type AdminNotificationCreateValues = z.infer<typeof adminNotificationCreateSchema>;
36
+ export {};
@@ -0,0 +1,114 @@
1
+ /*
2
+ * WHY: `POST /api/admin/admin-notifications` spread the raw request body into
3
+ * Firestore with only `isRead: false` pinned. The last unvalidated write
4
+ * path in `src/app/api`.
5
+ *
6
+ * `category` and `severity` are both closed unions that the admin
7
+ * notification bell filters and colours by, and neither was checked — so
8
+ * a notification could be stored with a category no filter chip matches
9
+ * and a severity no badge renders, which is the shape that produces a row
10
+ * visible in the raw collection and invisible in the UI (Recurrent Root
11
+ * Cause #33).
12
+ *
13
+ * `audienceUserIds` decides WHO SEES IT. Unvalidated and unbounded, it
14
+ * could arrive as a string rather than an array — silently narrowing or
15
+ * widening the audience of an admin notification.
16
+ *
17
+ * WHAT: The create contract.
18
+ *
19
+ * ## `isRead` / `readAt` are absent on purpose
20
+ *
21
+ * Both are per-admin read state, owned by the read/dismiss path. A creator
22
+ * marking its own notification as already read would file it straight into the
23
+ * history nobody looks at.
24
+ *
25
+ * EXPORTS:
26
+ * adminNotificationCreateSchema, ADMIN_NOTIFICATION_CATEGORIES,
27
+ * ADMIN_NOTIFICATION_SEVERITIES, type AdminNotificationCreateValues
28
+ *
29
+ * @tag domain:store-extensions,admin-notifications
30
+ * @tag layer:schema
31
+ * @tag pattern:none
32
+ * @tag access:isomorphic
33
+ * @tag consumers:/api/admin/admin-notifications
34
+ * @tag sideEffects:none
35
+ */
36
+ import { z } from "zod";
37
+ import { annotate } from "../../shell/field-ui-meta";
38
+ /*
39
+ * Keyed `Record<Union, true>` so BOTH mistakes are compile errors — a member
40
+ * added to the union and forgotten here, and a value here that is not real.
41
+ */
42
+ const CATEGORY_MAP = {
43
+ system: true,
44
+ security: true,
45
+ moderation: true,
46
+ payouts: true,
47
+ fraud: true,
48
+ growth: true,
49
+ };
50
+ export const ADMIN_NOTIFICATION_CATEGORIES = Object.keys(CATEGORY_MAP);
51
+ const SEVERITY_MAP = {
52
+ info: true,
53
+ warning: true,
54
+ error: true,
55
+ };
56
+ export const ADMIN_NOTIFICATION_SEVERITIES = Object.keys(SEVERITY_MAP);
57
+ /**
58
+ * 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
59
+ * by schema instance and every zod wrapper returns a new one.
60
+ */
61
+ export const adminNotificationCreateSchema = z
62
+ .object({
63
+ title: annotate(z.string().min(1, "Give the notification a title.").max(200), {
64
+ section: "message",
65
+ sectionLabel: "Message",
66
+ sectionRequired: true,
67
+ quick: true,
68
+ order: 1,
69
+ row: "full",
70
+ }),
71
+ body: annotate(z.string().min(1, "Say what happened.").max(2000), {
72
+ section: "message",
73
+ quick: true,
74
+ order: 2,
75
+ row: "full",
76
+ kind: "textarea",
77
+ }),
78
+ category: annotate(z.enum(ADMIN_NOTIFICATION_CATEGORIES), {
79
+ section: "routing",
80
+ sectionLabel: "Routing",
81
+ sectionRequired: true,
82
+ quick: true,
83
+ order: 1,
84
+ row: "pair",
85
+ }),
86
+ severity: annotate(z.enum(ADMIN_NOTIFICATION_SEVERITIES), {
87
+ section: "routing",
88
+ quick: true,
89
+ order: 2,
90
+ row: "pair",
91
+ }),
92
+ // Empty (or omitted) means every admin — that is the documented meaning of
93
+ // the field, so an empty array must stay expressible and is NOT `.min(1)`.
94
+ // Capped because this is the audience of a broadcast.
95
+ audienceUserIds: annotate(z.array(z.string().min(1)).max(200).optional(), {
96
+ section: "routing",
97
+ order: 3,
98
+ row: "full",
99
+ kind: "list",
100
+ help: "Leave empty to notify every admin.",
101
+ }),
102
+ entityType: annotate(z.string().max(80).optional(), {
103
+ section: "link",
104
+ sectionLabel: "Linked record",
105
+ order: 1,
106
+ row: "pair",
107
+ }),
108
+ entityId: annotate(z.string().max(200).optional(), {
109
+ section: "link",
110
+ order: 2,
111
+ row: "pair",
112
+ }),
113
+ })
114
+ .strict();
@@ -0,0 +1,57 @@
1
+ import { z } from "zod";
2
+ import type { AlertOperator, AnalyticsCardType } from "./firestore";
3
+ export declare const ANALYTICS_ALERT_OPERATORS: [AlertOperator, ...AlertOperator[]];
4
+ export declare const ANALYTICS_CARD_TYPES: [AnalyticsCardType, ...AnalyticsCardType[]];
5
+ /**
6
+ * 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
7
+ * by schema instance and every zod wrapper returns a new one.
8
+ */
9
+ export declare const analyticsAlertCreateSchema: z.ZodObject<{
10
+ label: z.ZodString;
11
+ metric: z.ZodString;
12
+ operator: z.ZodEnum<[AlertOperator, ...AlertOperator[]]>;
13
+ threshold: z.ZodNumber;
14
+ windowHours: z.ZodNumber;
15
+ isActive: z.ZodOptional<z.ZodBoolean>;
16
+ notifyChannels: z.ZodOptional<z.ZodArray<z.ZodEnum<["in-app", "email", "whatsapp"]>, "many">>;
17
+ }, "strict", z.ZodTypeAny, {
18
+ label: string;
19
+ operator: AlertOperator;
20
+ threshold: number;
21
+ metric: string;
22
+ windowHours: number;
23
+ isActive?: boolean | undefined;
24
+ notifyChannels?: ("email" | "whatsapp" | "in-app")[] | undefined;
25
+ }, {
26
+ label: string;
27
+ operator: AlertOperator;
28
+ threshold: number;
29
+ metric: string;
30
+ windowHours: number;
31
+ isActive?: boolean | undefined;
32
+ notifyChannels?: ("email" | "whatsapp" | "in-app")[] | undefined;
33
+ }>;
34
+ export type AnalyticsAlertCreateValues = z.infer<typeof analyticsAlertCreateSchema>;
35
+ export declare const analyticsCardCreateSchema: z.ZodObject<{
36
+ title: z.ZodString;
37
+ type: z.ZodEnum<[AnalyticsCardType, ...AnalyticsCardType[]]>;
38
+ metric: z.ZodString;
39
+ filters: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<import("../../..").FirestoreValue, z.ZodTypeDef, import("../../..").FirestoreValue>>>;
40
+ position: z.ZodOptional<z.ZodNumber>;
41
+ isVisible: z.ZodOptional<z.ZodBoolean>;
42
+ }, "strict", z.ZodTypeAny, {
43
+ type: AnalyticsCardType;
44
+ title: string;
45
+ metric: string;
46
+ position?: number | undefined;
47
+ filters?: Record<string, import("../../..").FirestoreValue> | undefined;
48
+ isVisible?: boolean | undefined;
49
+ }, {
50
+ type: AnalyticsCardType;
51
+ title: string;
52
+ metric: string;
53
+ position?: number | undefined;
54
+ filters?: Record<string, import("../../..").FirestoreValue> | undefined;
55
+ isVisible?: boolean | undefined;
56
+ }>;
57
+ export type AnalyticsCardCreateValues = z.infer<typeof analyticsCardCreateSchema>;
@@ -0,0 +1,158 @@
1
+ /*
2
+ * WHY: `POST /api/store/analytics/alerts` and `POST /api/store/analytics/cards`
3
+ * both spread the raw request body into Firestore.
4
+ *
5
+ * The alert one matters most. An analytics alert is a rule that FIRES —
6
+ * `metric`, `operator`, `threshold` and `windowHours` are what a scheduled
7
+ * job evaluates, and none of the four was checked. A `threshold` arriving
8
+ * as the string "50" from a number input was written as a string, so the
9
+ * comparison the alert exists to perform would run against a string; an
10
+ * `operator` outside the real six matched nothing; and `windowHours` was
11
+ * unbounded, so a rule could ask a job to look back a hundred years.
12
+ *
13
+ * Neither failure surfaces as an error. The alert is created, listed, and
14
+ * looks configured — it simply never fires, or fires wrongly.
15
+ *
16
+ * WHAT: The two create contracts.
17
+ *
18
+ * ## `scope` / `ownerId` / `isBuiltIn` are absent on purpose
19
+ *
20
+ * The routes pin all three. `scope` in particular decides whether a row is a
21
+ * seller's or the platform's, so accepting it from the body would let a seller
22
+ * file an admin-scoped card.
23
+ *
24
+ * EXPORTS:
25
+ * analyticsAlertCreateSchema, analyticsCardCreateSchema,
26
+ * ANALYTICS_ALERT_OPERATORS, ANALYTICS_CARD_TYPES,
27
+ * type AnalyticsAlertCreateValues, type AnalyticsCardCreateValues
28
+ *
29
+ * @tag domain:store-extensions,analytics
30
+ * @tag layer:schema
31
+ * @tag pattern:none
32
+ * @tag access:isomorphic
33
+ * @tag consumers:/api/store/analytics/alerts,/api/store/analytics/cards
34
+ * @tag sideEffects:none
35
+ */
36
+ import { z } from "zod";
37
+ import { annotate } from "../../shell/field-ui-meta";
38
+ import { firestoreValueSchema } from "../../../schemas/firestore-value";
39
+ /*
40
+ * Keyed `Record<Union, true>` so BOTH mistakes are compile errors — a value
41
+ * added to the union and forgotten here, and a value here that is not real.
42
+ */
43
+ const ALERT_OPERATOR_MAP = {
44
+ ">": true,
45
+ "<": true,
46
+ ">=": true,
47
+ "<=": true,
48
+ "==": true,
49
+ "!=": true,
50
+ };
51
+ export const ANALYTICS_ALERT_OPERATORS = Object.keys(ALERT_OPERATOR_MAP);
52
+ const CARD_TYPE_MAP = {
53
+ metric: true,
54
+ line: true,
55
+ bar: true,
56
+ pie: true,
57
+ table: true,
58
+ custom: true,
59
+ };
60
+ export const ANALYTICS_CARD_TYPES = Object.keys(CARD_TYPE_MAP);
61
+ /** One year. A window beyond this is a typo, not a reporting period. */
62
+ const MAX_WINDOW_HOURS = 24 * 365;
63
+ /**
64
+ * 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
65
+ * by schema instance and every zod wrapper returns a new one.
66
+ */
67
+ export const analyticsAlertCreateSchema = z
68
+ .object({
69
+ label: annotate(z.string().min(1, "Give the alert a name.").max(120), {
70
+ section: "alert",
71
+ sectionLabel: "Alert",
72
+ sectionRequired: true,
73
+ quick: true,
74
+ order: 1,
75
+ row: "full",
76
+ }),
77
+ metric: annotate(z.string().min(1, "Choose a metric to watch."), {
78
+ section: "rule",
79
+ sectionLabel: "When to fire",
80
+ sectionRequired: true,
81
+ quick: true,
82
+ order: 1,
83
+ row: "pair",
84
+ }),
85
+ operator: annotate(z.enum(ANALYTICS_ALERT_OPERATORS), {
86
+ section: "rule",
87
+ quick: true,
88
+ order: 2,
89
+ row: "pair",
90
+ }),
91
+ // Coerced. A number input sends a string, and an uncoerced threshold is
92
+ // compared as a string by whatever evaluates the rule — "9" > "10".
93
+ threshold: annotate(z.coerce.number({ invalid_type_error: "Set a numeric threshold." }), {
94
+ section: "rule",
95
+ quick: true,
96
+ order: 3,
97
+ row: "pair",
98
+ kind: "number",
99
+ }),
100
+ windowHours: annotate(z.coerce
101
+ .number()
102
+ .int("The window must be a whole number of hours.")
103
+ .min(1, "The window must be at least an hour.")
104
+ .max(MAX_WINDOW_HOURS, "That window is longer than a year."), { section: "rule", quick: true, order: 4, row: "pair", kind: "number" }),
105
+ isActive: annotate(z.boolean().optional(), {
106
+ section: "delivery",
107
+ sectionLabel: "Delivery",
108
+ order: 1,
109
+ row: "quarter",
110
+ }),
111
+ notifyChannels: annotate(z.array(z.enum(["in-app", "email", "whatsapp"])).min(1).optional(), { section: "delivery", order: 2, row: "full", kind: "list" }),
112
+ })
113
+ .strict();
114
+ export const analyticsCardCreateSchema = z
115
+ .object({
116
+ title: annotate(z.string().min(1, "Give the card a title.").max(120), {
117
+ section: "card",
118
+ sectionLabel: "Card",
119
+ sectionRequired: true,
120
+ quick: true,
121
+ order: 1,
122
+ row: "full",
123
+ }),
124
+ type: annotate(z.enum(ANALYTICS_CARD_TYPES), {
125
+ section: "card",
126
+ quick: true,
127
+ order: 2,
128
+ row: "pair",
129
+ }),
130
+ metric: annotate(z.string().min(1, "Choose a metric."), {
131
+ section: "card",
132
+ quick: true,
133
+ order: 3,
134
+ row: "pair",
135
+ }),
136
+ // Deliberately open — filter shape varies by card type, and a closed schema
137
+ // would silently strip whatever the builder produced for a type it did not
138
+ // enumerate.
139
+ filters: annotate(z.record(firestoreValueSchema).optional(), {
140
+ section: "card",
141
+ order: 4,
142
+ row: "full",
143
+ kind: "list",
144
+ }),
145
+ position: annotate(z.coerce.number().int().min(0).optional(), {
146
+ section: "layout",
147
+ sectionLabel: "Layout",
148
+ order: 1,
149
+ row: "pair",
150
+ kind: "number",
151
+ }),
152
+ isVisible: annotate(z.boolean().optional(), {
153
+ section: "layout",
154
+ order: 2,
155
+ row: "quarter",
156
+ }),
157
+ })
158
+ .strict();