@mohasinac/appkit 4.62.1 → 4.63.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 (261) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +8 -29
  3. package/dist/_internal/client/features/layout/filterNavItems.d.ts +28 -0
  4. package/dist/_internal/client/features/layout/filterNavItems.js +35 -0
  5. package/dist/_internal/server/features/ads/booking.d.ts +118 -0
  6. package/dist/_internal/server/features/ads/booking.js +326 -0
  7. package/dist/_internal/server/features/checkout/actions.d.ts +51 -11
  8. package/dist/_internal/server/features/checkout/actions.js +335 -80
  9. package/dist/_internal/server/features/checkout/expand-order-items.d.ts +1 -2
  10. package/dist/_internal/server/features/checkout/expand-order-items.js +32 -0
  11. package/dist/_internal/server/features/checkout/freeze-parts.d.ts +50 -0
  12. package/dist/_internal/server/features/checkout/freeze-parts.js +94 -0
  13. package/dist/_internal/server/features/checkout/value-otp-gate.d.ts +55 -0
  14. package/dist/_internal/server/features/checkout/value-otp-gate.js +60 -0
  15. package/dist/_internal/server/features/dashboard/attention.d.ts +128 -0
  16. package/dist/_internal/server/features/dashboard/attention.js +183 -0
  17. package/dist/_internal/server/features/dashboard/data.d.ts +47 -0
  18. package/dist/_internal/server/features/dashboard/data.js +82 -0
  19. package/dist/_internal/server/features/inventory/reservations.d.ts +99 -0
  20. package/dist/_internal/server/features/inventory/reservations.js +273 -0
  21. package/dist/_internal/server/features/offers/destinations.d.ts +40 -0
  22. package/dist/_internal/server/features/offers/destinations.js +65 -0
  23. package/dist/_internal/server/features/orders/adapters.js +21 -0
  24. package/dist/_internal/server/features/orders/amend-delivery.d.ts +56 -0
  25. package/dist/_internal/server/features/orders/amend-delivery.js +124 -0
  26. package/dist/_internal/server/features/orders/balance.d.ts +89 -0
  27. package/dist/_internal/server/features/orders/balance.js +256 -0
  28. package/dist/_internal/server/features/pricing/actions.d.ts +58 -0
  29. package/dist/_internal/server/features/pricing/actions.js +144 -0
  30. package/dist/_internal/server/features/shipping/book-split.d.ts +86 -0
  31. package/dist/_internal/server/features/shipping/book-split.js +130 -0
  32. package/dist/_internal/server/features/shipping/book.d.ts +61 -0
  33. package/dist/_internal/server/features/shipping/book.js +176 -0
  34. package/dist/_internal/server/features/shipping/courier-block.js +26 -1
  35. package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
  36. package/dist/_internal/server/features/site-settings/adapters.js +14 -0
  37. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  38. package/dist/_internal/server/features/stores/adapters.js +14 -0
  39. package/dist/_internal/server/functions/scheduled.d.ts +2 -1
  40. package/dist/_internal/server/functions/scheduled.js +57 -1
  41. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +2 -0
  42. package/dist/_internal/server/jobs/core/jobRunners.js +45 -0
  43. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +4 -1
  44. package/dist/_internal/server/jobs/core/reservationExpiry.d.ts +17 -0
  45. package/dist/_internal/server/jobs/core/reservationExpiry.js +40 -0
  46. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +15 -0
  47. package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
  48. package/dist/_internal/server/jobs/handlers/index.js +1 -0
  49. package/dist/_internal/server/jobs/handlers/reservationExpiry.d.ts +2 -0
  50. package/dist/_internal/server/jobs/handlers/reservationExpiry.js +2 -0
  51. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  52. package/dist/_internal/shared/actions/action-registry.js +268 -0
  53. package/dist/_internal/shared/checkout/order-math.d.ts +31 -2
  54. package/dist/_internal/shared/checkout/order-math.js +22 -36
  55. package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
  56. package/dist/_internal/shared/checkout/rules/index.js +1 -1
  57. package/dist/_internal/shared/checkout/rules/payment-gate.d.ts +18 -0
  58. package/dist/_internal/shared/checkout/rules/payment-gate.js +25 -0
  59. package/dist/_internal/shared/checkout/split.d.ts +0 -12
  60. package/dist/_internal/shared/checkout/split.js +22 -2
  61. package/dist/_internal/shared/content/disclaimers.js +7 -7
  62. package/dist/_internal/shared/content/published-values.d.ts +6 -1
  63. package/dist/_internal/shared/content/published-values.js +8 -0
  64. package/dist/_internal/shared/features/admin/integrations.d.ts +32 -2
  65. package/dist/_internal/shared/features/admin/integrations.js +19 -9
  66. package/dist/_internal/shared/features/dashboard/hub-sections.d.ts +72 -0
  67. package/dist/_internal/shared/features/dashboard/hub-sections.js +75 -0
  68. package/dist/_internal/shared/features/emi/choice.d.ts +98 -0
  69. package/dist/_internal/shared/features/emi/choice.js +69 -0
  70. package/dist/_internal/shared/features/emi/group.d.ts +84 -0
  71. package/dist/_internal/shared/features/emi/group.js +126 -0
  72. package/dist/_internal/shared/features/ledger/posting.d.ts +107 -0
  73. package/dist/_internal/shared/features/ledger/posting.js +165 -0
  74. package/dist/_internal/shared/features/ledger/reports.d.ts +76 -0
  75. package/dist/_internal/shared/features/ledger/reports.js +127 -0
  76. package/dist/_internal/shared/features/ledger/rules.d.ts +170 -0
  77. package/dist/_internal/shared/features/ledger/rules.js +221 -0
  78. package/dist/_internal/shared/features/offers/coordination.d.ts +122 -0
  79. package/dist/_internal/shared/features/offers/coordination.js +80 -0
  80. package/dist/_internal/shared/features/orders/delivery-snapshot.d.ts +114 -0
  81. package/dist/_internal/shared/features/orders/delivery-snapshot.js +116 -0
  82. package/dist/_internal/shared/features/payouts/pay-period.d.ts +91 -0
  83. package/dist/_internal/shared/features/payouts/pay-period.js +104 -0
  84. package/dist/_internal/shared/features/points/accepts.d.ts +1 -8
  85. package/dist/_internal/shared/features/points/accepts.js +12 -6
  86. package/dist/_internal/shared/features/pricing/rate-registry.d.ts +150 -0
  87. package/dist/_internal/shared/features/pricing/rate-registry.js +246 -0
  88. package/dist/_internal/shared/features/shipping/parcel.d.ts +123 -0
  89. package/dist/_internal/shared/features/shipping/parcel.js +139 -0
  90. package/dist/_internal/shared/features/site-settings/fees.d.ts +16 -8
  91. package/dist/_internal/shared/features/site-settings/fees.js +12 -8
  92. package/dist/_internal/shared/fees/explain.d.ts +81 -0
  93. package/dist/_internal/shared/fees/explain.js +101 -0
  94. package/dist/_internal/shared/history/index.d.ts +1 -1
  95. package/dist/_internal/shared/history/index.js +1 -1
  96. package/dist/_internal/shared/history/types.d.ts +7 -1
  97. package/dist/_internal/shared/history/types.js +25 -0
  98. package/dist/client.d.ts +11 -1
  99. package/dist/client.js +13 -0
  100. package/dist/constants/api-endpoints.d.ts +72 -0
  101. package/dist/constants/api-endpoints.js +41 -0
  102. package/dist/constants/field-names.d.ts +41 -0
  103. package/dist/constants/field-names.js +47 -0
  104. package/dist/features/account/components/UserOrdersView.js +28 -2
  105. package/dist/features/admin/components/AdminIntegrationsView.d.ts +55 -0
  106. package/dist/features/admin/components/AdminIntegrationsView.js +74 -0
  107. package/dist/features/admin/components/AdminOrderEditorView.d.ts +16 -1
  108. package/dist/features/admin/components/AdminOrderEditorView.js +72 -3
  109. package/dist/features/admin/components/AdminOrdersView.js +20 -1
  110. package/dist/features/admin/schemas/firestore.d.ts +32 -0
  111. package/dist/features/ads/schemas/booking-write.d.ts +120 -0
  112. package/dist/features/ads/schemas/booking-write.js +75 -0
  113. package/dist/features/ads/schemas/creative-form.d.ts +18 -0
  114. package/dist/features/ads/schemas/creative-form.js +51 -0
  115. package/dist/features/ads/schemas/firestore.d.ts +140 -0
  116. package/dist/features/ads/schemas/firestore.js +109 -0
  117. package/dist/features/auctions/repository/bid.repository.js +3 -1
  118. package/dist/features/audit-log/schemas/firestore.d.ts +110 -0
  119. package/dist/features/audit-log/schemas/firestore.js +74 -0
  120. package/dist/features/auth/actions/profile-actions.d.ts +69 -1
  121. package/dist/features/auth/actions/profile-actions.js +62 -1
  122. package/dist/features/auth/schemas/firestore.d.ts +16 -2
  123. package/dist/features/auth/schemas/firestore.js +2 -1
  124. package/dist/features/blog/actions/blog-actions.d.ts +6 -0
  125. package/dist/features/blog/actions/blog-actions.js +17 -2
  126. package/dist/features/blog/api/route.js +4 -1
  127. package/dist/features/blog/columns/index.js +5 -0
  128. package/dist/features/blog/components/BlogIndexPageView.js +2 -1
  129. package/dist/features/blog/repository/blog.repository.d.ts +4 -0
  130. package/dist/features/blog/repository/blog.repository.js +5 -0
  131. package/dist/features/blog/schemas/blog-form.d.ts +3 -0
  132. package/dist/features/blog/schemas/blog-form.js +1 -0
  133. package/dist/features/blog/schemas/firestore.d.ts +38 -1
  134. package/dist/features/blog/schemas/firestore.js +23 -0
  135. package/dist/features/blog/schemas/index.d.ts +6 -0
  136. package/dist/features/blog/schemas/index.js +2 -0
  137. package/dist/features/blog/types/index.d.ts +2 -0
  138. package/dist/features/cart/components/CartPriceBreakdown.d.ts +12 -1
  139. package/dist/features/cart/components/CartPriceBreakdown.js +24 -5
  140. package/dist/features/cart/repository/cart.repository.d.ts +14 -0
  141. package/dist/features/cart/repository/cart.repository.js +20 -0
  142. package/dist/features/cart/schemas/firestore.d.ts +24 -0
  143. package/dist/features/dashboard/components/DashboardHubView.d.ts +83 -0
  144. package/dist/features/dashboard/components/DashboardHubView.js +56 -0
  145. package/dist/features/dashboard/components/DashboardSectionsClient.d.ts +23 -0
  146. package/dist/features/dashboard/components/DashboardSectionsClient.js +66 -0
  147. package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.d.ts +55 -0
  148. package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.js +175 -0
  149. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +24 -0
  150. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +28 -0
  151. package/dist/features/dispute-reasons/schemas/firestore.d.ts +3 -5
  152. package/dist/features/events/components/EventSaleBanner.js +1 -1
  153. package/dist/features/faq/components/FAQPageContent.js +2 -2
  154. package/dist/features/gateway-events/schemas/index.d.ts +2 -2
  155. package/dist/features/homepage/schemas/index.d.ts +8 -8
  156. package/dist/features/inventory/schemas/stock-reservation.d.ts +114 -0
  157. package/dist/features/inventory/schemas/stock-reservation.js +103 -0
  158. package/dist/features/layout/AppLayoutShell.d.ts +9 -1
  159. package/dist/features/layout/AppLayoutShell.js +12 -4
  160. package/dist/features/layout/BottomActions.js +2 -1
  161. package/dist/features/layout/BottomNavbar.d.ts +10 -2
  162. package/dist/features/layout/SideActionDock.js +52 -2
  163. package/dist/features/layout/TitleBarLayout.js +13 -4
  164. package/dist/features/layout/TitleBarLayout.style.css +59 -0
  165. package/dist/features/layout/useCtaReveal.d.ts +16 -1
  166. package/dist/features/layout/useCtaReveal.js +17 -26
  167. package/dist/features/ledger/repository/journal.repository.d.ts +110 -0
  168. package/dist/features/ledger/repository/journal.repository.js +312 -0
  169. package/dist/features/ledger/schemas/expenses.d.ts +87 -0
  170. package/dist/features/ledger/schemas/expenses.js +27 -0
  171. package/dist/features/ledger/schemas/firestore.d.ts +183 -0
  172. package/dist/features/ledger/schemas/firestore.js +69 -0
  173. package/dist/features/orders/components/BalancePaymentPanel.d.ts +12 -0
  174. package/dist/features/orders/components/BalancePaymentPanel.js +94 -0
  175. package/dist/features/orders/components/RaiseDisputeForm.js +19 -11
  176. package/dist/features/orders/repository/orders.repository.d.ts +28 -1
  177. package/dist/features/orders/repository/orders.repository.js +60 -6
  178. package/dist/features/orders/schemas/balance.d.ts +40 -0
  179. package/dist/features/orders/schemas/balance.js +47 -0
  180. package/dist/features/orders/schemas/firestore.d.ts +202 -0
  181. package/dist/features/orders/schemas/firestore.js +43 -0
  182. package/dist/features/orders/schemas/index.d.ts +2 -2
  183. package/dist/features/orders/types/index.d.ts +21 -0
  184. package/dist/features/payments/repository/payout.repository.js +3 -2
  185. package/dist/features/payments/schemas/firestore.d.ts +92 -1
  186. package/dist/features/payments/schemas/firestore.js +23 -0
  187. package/dist/features/pricing/components/AdminPricingView.d.ts +13 -0
  188. package/dist/features/pricing/components/AdminPricingView.js +157 -0
  189. package/dist/features/products/schemas/firestore.d.ts +26 -1
  190. package/dist/features/products/schemas/firestore.js +2 -0
  191. package/dist/features/products/schemas/index.d.ts +2 -2
  192. package/dist/features/reviews/repository/reviews.repository.d.ts +14 -0
  193. package/dist/features/reviews/repository/reviews.repository.js +16 -0
  194. package/dist/features/scams/repository/scammer.repository.js +2 -1
  195. package/dist/features/scams/schemas/firestore.d.ts +2 -1
  196. package/dist/features/scams/schemas/firestore.js +2 -1
  197. package/dist/features/seller/actions/offer-actions.js +56 -0
  198. package/dist/features/seller/actions/seller-actions.d.ts +1 -1
  199. package/dist/features/seller/actions/seller-actions.js +4 -0
  200. package/dist/features/seller/components/DispatchForm.d.ts +11 -0
  201. package/dist/features/seller/components/DispatchForm.js +190 -0
  202. package/dist/features/seller/components/SellerOrdersView.js +94 -2
  203. package/dist/features/seller/components/index.d.ts +1 -0
  204. package/dist/features/seller/components/index.js +1 -0
  205. package/dist/features/seller/repository/offer.repository.js +4 -3
  206. package/dist/features/seller/schemas/firestore.d.ts +23 -2
  207. package/dist/features/seller/schemas/firestore.js +3 -2
  208. package/dist/features/seller/schemas/index.d.ts +4 -4
  209. package/dist/features/seller/schemas/order-forms.d.ts +61 -4
  210. package/dist/features/seller/schemas/order-forms.js +41 -8
  211. package/dist/features/shipments/schemas/firestore.d.ts +25 -0
  212. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +39 -0
  213. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +55 -0
  214. package/dist/features/shipping-boxes/schemas/firestore.d.ts +2 -4
  215. package/dist/features/stores/repository/store.repository.d.ts +18 -0
  216. package/dist/features/stores/repository/store.repository.js +27 -1
  217. package/dist/features/stores/schemas/firestore.d.ts +41 -0
  218. package/dist/features/stores/schemas/firestore.js +20 -0
  219. package/dist/features/stores/schemas/index.d.ts +6 -6
  220. package/dist/features/support/repository/support.repository.js +72 -6
  221. package/dist/features/support/schemas/firestore.d.ts +17 -0
  222. package/dist/features/support/schemas/index.d.ts +2 -2
  223. package/dist/index.d.ts +5 -0
  224. package/dist/index.js +19 -0
  225. package/dist/next/routing/route-map.d.ts +92 -0
  226. package/dist/next/routing/route-map.js +53 -0
  227. package/dist/react/contexts/SessionContext.d.ts +17 -1
  228. package/dist/schemas/registry.d.ts +22 -16
  229. package/dist/security/pii-schemas.d.ts +1 -1
  230. package/dist/security/pii-schemas.js +45 -0
  231. package/dist/server.d.ts +27 -1
  232. package/dist/server.js +144 -0
  233. package/dist/styles.css +349 -1
  234. package/dist/tailwind-utilities.css +1 -1
  235. package/dist/ui/components/Button.d.ts +10 -1
  236. package/dist/ui/components/Button.js +25 -3
  237. package/dist/ui/components/Chart.d.ts +63 -0
  238. package/dist/ui/components/Chart.js +169 -0
  239. package/dist/ui/components/Chart.style.css +214 -0
  240. package/dist/ui/components/Disclaimer.js +10 -1
  241. package/dist/ui/components/OverflowRow.js +54 -2
  242. package/dist/ui/components/OverflowRow.style.css +38 -0
  243. package/dist/ui/components/RichTextEditor.js +99 -8
  244. package/dist/ui/components/chart-spec.d.ts +68 -0
  245. package/dist/ui/components/chart-spec.js +140 -0
  246. package/dist/ui/components/index.style.css +1 -0
  247. package/dist/ui/components/surface-tokens.d.ts +1 -1
  248. package/dist/ui/components/surface-tokens.js +9 -0
  249. package/dist/ui/components/toolbar-collapse.d.ts +13 -0
  250. package/dist/ui/components/toolbar-collapse.js +10 -1
  251. package/dist/ui/index.d.ts +6 -0
  252. package/dist/ui/index.js +3 -0
  253. package/dist/ui/rich-text/RichText.d.ts +10 -1
  254. package/dist/ui/rich-text/RichText.js +33 -2
  255. package/dist/ui/rich-text/RichText.style.css +35 -0
  256. package/dist/ui/rich-text/RichTextRenderer.d.ts +30 -1
  257. package/dist/ui/rich-text/RichTextRenderer.js +34 -2
  258. package/dist/ui/rich-text/chart-blocks.d.ts +62 -0
  259. package/dist/ui/rich-text/chart-blocks.js +87 -0
  260. package/package.json +1 -1
  261. package/scripts/seed-cli.mjs +3 -0
@@ -0,0 +1,66 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useMemo } from "react";
4
+ import { useSiteSettings } from "../../../core/hooks/useSiteSettings.js";
5
+ import { filterNavGroups } from "../../../_internal/client/features/layout/filterNavItems.js";
6
+ import { hubSections } from "../../../_internal/shared/features/dashboard/hub-sections.js";
7
+ import { ROUTES } from "../../../next/routing/route-map.js";
8
+ import { DashboardHubView } from "./DashboardHubView.js";
9
+ /**
10
+ * The sectioned index of a dashboard, for a landing page that is already a
11
+ * CLIENT component.
12
+ *
13
+ * ## Why a client variant exists at all
14
+ *
15
+ * `/store/page.tsx` and `/user/page.tsx` are `"use client"` and carry live stat
16
+ * tiles (`useStoreDashboard`, `useNotifications`, an avatar uploader). Turning
17
+ * either into a server component to fetch hub data would mean moving a working
18
+ * 250–360 line page into a sibling file for the sake of a card grid — a large
19
+ * diff over a working feature, which is the shape of Root Cause #60.
20
+ *
21
+ * So the DATA source differs and the RENDERER does not: this resolves sections
22
+ * from the same `useSiteSettings` navConfig the sidebar reads, and hands them to
23
+ * the same `<DashboardHubView>` the admin landing renders server-side. One card
24
+ * grid, two ways in — not two card grids.
25
+ *
26
+ * ## 🛑 No attention counts here, and that is honest rather than lazy
27
+ *
28
+ * Counts need Firestore, which a client component can only reach through an
29
+ * endpoint — a second billed invocation per landing visit for numbers the page
30
+ * did not previously show (Root Cause #94). The admin landing, which is a
31
+ * server component under a session-reading layout, gets them for free in its
32
+ * own render. A seller's one countable figure (pending orders) is already a
33
+ * live stat tile on `/store`, so the badge would be the second place for it.
34
+ *
35
+ * `counts={{}}` means every card renders without a badge — never with a `0`,
36
+ * which is the one answer that would wrongly read as "nothing waiting"
37
+ * (Root Cause #72).
38
+ */
39
+ /**
40
+ * 🛑 `Record<NavPortal, …>` so a fourth portal is a COMPILE error here rather
41
+ * than a landing page whose section cards all link to `""`. An object keyed on
42
+ * `string` would accept the omission silently (Root Cause #61).
43
+ */
44
+ const SECTION_ROUTE = {
45
+ admin: (slug) => String(ROUTES.ADMIN.SECTION(slug)),
46
+ store: (slug) => String(ROUTES.STORE.SECTION(slug)),
47
+ user: (slug) => String(ROUTES.USER.SECTION(slug)),
48
+ };
49
+ export function DashboardSectionsClient({ portal, groups, permissions, title, description, above, emptyMessage, }) {
50
+ const { data: settings } = useSiteSettings();
51
+ /*
52
+ * Derived, never copied into state — the settings query can resolve after
53
+ * first paint, and a `useState` seeded from it would freeze the pre-settings
54
+ * answer. Same reasoning as `AdminIntegrationsView`.
55
+ */
56
+ const sections = useMemo(() => hubSections(filterNavGroups([...groups], settings?.navConfig, permissions)), [groups, settings?.navConfig, permissions]);
57
+ const sectionHrefs = useMemo(() => Object.fromEntries(sections.map((s) => [s.slug, SECTION_ROUTE[portal](s.slug)])), [sections, portal]);
58
+ return (_jsx(DashboardHubView, { title: title, description: description, mode: "sections", sections: sections, sectionHrefs: sectionHrefs, counts: {}, above: above, emptyMessage: emptyMessage,
59
+ /*
60
+ * 🛑 Withheld deliberately. `/store`'s entitlement filter lives in the
61
+ * layout and on the hub pages, not here, so a count rendered from these
62
+ * groups would over-promise on the Listings card. See the prop's own
63
+ * header — the section is right, the number would not be.
64
+ */
65
+ showScreenCount: false }));
66
+ }
@@ -0,0 +1,55 @@
1
+ import React from "react";
2
+ import { type DisputeKind } from "../../../_internal/shared/features/orders/dispute-reasons";
3
+ /**
4
+ * Admin → Trust & Safety → Dispute reasons.
5
+ *
6
+ * ## Why a management page exists at all
7
+ *
8
+ * Inline create in the triage drawer was the cheaper half and was rejected on
9
+ * its own, for a reason worth keeping: **with no management page nothing can
10
+ * ever retire a reason**, so the list only grows and a typo'd row is permanent.
11
+ * Both shipped — the drawer so an agent never leaves a ticket to add a missing
12
+ * reason, this page so the eleven seeded rows are reviewable and retire has a
13
+ * home.
14
+ *
15
+ * ## 🛑 A row is OPENABLE, not just actionable
16
+ *
17
+ * `audit-listing-detail-affordance` requires it and the reason is Root Cause
18
+ * #56: a menu of pure mutations lets staff act on a record they were never able
19
+ * to read. Here that would mean retiring a reason without seeing its KIND — and
20
+ * the kind is the only field that decides which gate the reason inherits, so
21
+ * retiring the wrong one silently narrows the wrong form.
22
+ *
23
+ * `RecordDetailModal` is the shared primitive for exactly this; a bespoke modal
24
+ * would have tripped the Duplication Framework's Rule of Three on sight.
25
+ */
26
+ export interface AdminDisputeReasonsViewProps {
27
+ /**
28
+ * The staff list endpoint, supplied by the consumer.
29
+ *
30
+ * 🛑 A PROP rather than a literal in here, for the same reason
31
+ * `RaiseDisputeForm` takes `reasonsEndpoint`: appkit holds no consumer
32
+ * routes. The path belongs in `API_ROUTES`, which is the one registry, and a
33
+ * literal here would be a second place it is written — exactly what
34
+ * `audit-hardcoded-api-routes` blocks.
35
+ */
36
+ endpoint: string;
37
+ /** `PATCH /api/admin/dispute-reasons/{id}` with `{ isActive }`. */
38
+ onSetActive: (id: string, isActive: boolean) => Promise<void>;
39
+ /** `POST /api/dispute-reasons` — the same endpoint the triage drawer uses. */
40
+ onCreate: (input: {
41
+ label: string;
42
+ kind: DisputeKind;
43
+ }) => Promise<void>;
44
+ }
45
+ /**
46
+ * The two fields, exported so the triage drawer's inline create and this page
47
+ * cannot drift on the label bound or the kind vocabulary.
48
+ */
49
+ export declare function DisputeReasonFields({ label, kind, onLabel, onKind, }: {
50
+ label: string;
51
+ kind: DisputeKind;
52
+ onLabel: (v: string) => void;
53
+ onKind: (v: DisputeKind) => void;
54
+ }): React.JSX.Element;
55
+ export declare function AdminDisputeReasonsView({ endpoint, onSetActive, onCreate, }: AdminDisputeReasonsViewProps): React.JSX.Element;
@@ -0,0 +1,175 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useCallback, useState } from "react";
4
+ import { Badge, Button, Row, Stack, Text } from "../../../ui/index.js";
5
+ import { RecordDetailModal } from "../../../ui/components/RecordDetailModal.js";
6
+ import { useToast } from "../../../ui/components/Toast.js";
7
+ import { FieldInput } from "../../../ui/forms/FieldInput.js";
8
+ import { FieldSelect } from "../../../ui/forms/FieldSelect.js";
9
+ import { normalizeError } from "../../../errors/normalize.js";
10
+ import { DataListingView } from "../../admin/components/DataListingView.js";
11
+ import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
12
+ import { DISPUTE_KIND, } from "../../../_internal/shared/features/orders/dispute-reasons.js";
13
+ import { DISPUTE_REASON_LABEL_MAX, } from "../schemas/firestore.js";
14
+ /**
15
+ * What each kind MEANS, in one place.
16
+ *
17
+ * 🛑 A `Record<DisputeKind, string>`, so a third kind is a compile error here
18
+ * rather than a picker option rendering a bare token. The kind is the only
19
+ * field on a reason with a consequence — it decides which gate a dispute
20
+ * raised under it inherits — so it is the one that must never read as jargon.
21
+ */
22
+ const KIND_MEANING = {
23
+ item: "Item problem — it arrived wrong or damaged",
24
+ not_reached: "Never arrived — the parcel did not reach the buyer",
25
+ };
26
+ /*
27
+ * Chips over `isActive`, as ordinary Sieve clauses. `listAll` applies no
28
+ * `isActive` filter of its own precisely so these work without the repository
29
+ * growing a method per chip.
30
+ *
31
+ * 🛑 "All" carries NO filter rather than `isActive==true|false`. A pipe-joined
32
+ * OR over a boolean would be two equalities merged for no reason, and — worse —
33
+ * it would exclude any row written before the field existed, which is the `!=`
34
+ * trap one step removed.
35
+ */
36
+ const SCOPE_CHIPS = [
37
+ { id: "active", label: "Active", filter: "isActive==true" },
38
+ { id: "retired", label: "Retired", filter: "isActive==false" },
39
+ { id: "all", label: "All", filter: undefined },
40
+ ];
41
+ /**
42
+ * The two fields, exported so the triage drawer's inline create and this page
43
+ * cannot drift on the label bound or the kind vocabulary.
44
+ */
45
+ export function DisputeReasonFields({ label, kind, onLabel, onKind, }) {
46
+ return (_jsxs(Stack, { gap: "md", children: [_jsx(FieldInput, { name: "label", label: "Reason wording", required: true, value: label, onChange: onLabel, maxLength: DISPUTE_REASON_LABEL_MAX, placeholder: "The courier left it at the wrong flat" }), _jsx(FieldSelect, { name: "kind", label: "Kind", required: true, value: kind, onChange: (v) => onKind(v),
47
+ /*
48
+ * Two options, so `<FieldSelect>` is correct — `PaginatedSelect` is for
49
+ * >5 (Rule #9), and a searchable dropdown over two values is worse than
50
+ * a native select. Derived from the shipped union so a third kind
51
+ * cannot exist in the gate and be missing here.
52
+ */
53
+ options: DISPUTE_KIND.map((k) => ({ value: k, label: KIND_MEANING[k] })) })] }));
54
+ }
55
+ /** Create form. Shares `DisputeReasonFields` and the endpoint with the drawer. */
56
+ function CreateReasonForm({ onSubmit, }) {
57
+ const [label, setLabel] = useState("");
58
+ const [kind, setKind] = useState(DISPUTE_KIND[0]);
59
+ const [busy, setBusy] = useState(false);
60
+ const { showToast } = useToast();
61
+ return (_jsxs(Stack, { gap: "md", children: [_jsx(Text, { size: "sm", color: "muted", children: "Short wording, as a buyer would read it. The detail goes in their own free-text field \u2014 this is a picker option, not the explanation." }), _jsx(DisputeReasonFields, { label: label, kind: kind, onLabel: setLabel, onKind: setKind }), _jsx(Button, { action: ACTIONS.ADMIN["add-dispute-reason"], isLoading: busy, disabled: busy || label.trim().length < 3, onClick: async () => {
62
+ setBusy(true);
63
+ try {
64
+ await onSubmit({ label: label.trim(), kind });
65
+ }
66
+ catch (err) {
67
+ void normalizeError(err);
68
+ showToast("Couldn't add that reason.", "error");
69
+ }
70
+ finally {
71
+ setBusy(false);
72
+ }
73
+ } })] }));
74
+ }
75
+ export function AdminDisputeReasonsView({ endpoint, onSetActive, onCreate, }) {
76
+ const { showToast } = useToast();
77
+ const [open, setOpen] = useState(null);
78
+ const [pending, setPending] = useState(null);
79
+ const setActive = useCallback(async (row, isActive) => {
80
+ setPending(row.id);
81
+ try {
82
+ await onSetActive(row.id, isActive);
83
+ showToast(isActive ? `"${row.label}" is back in the picker.` : `"${row.label}" retired.`, "success");
84
+ }
85
+ catch (err) {
86
+ void normalizeError(err);
87
+ /*
88
+ * No optimistic flip anywhere here, so a failure needs no rollback —
89
+ * the row still shows what the server last told us. Only a successful
90
+ * write and the refetch behind it move the list.
91
+ */
92
+ showToast("Couldn't save that. Nothing was changed.", "error");
93
+ }
94
+ finally {
95
+ setPending(null);
96
+ }
97
+ }, [onSetActive, showToast]);
98
+ const rowControls = useCallback((row) => (_jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setOpen(row), children: "View" }), _jsx(Button, { action: row.isActive
99
+ ? ACTIONS.ADMIN["retire-dispute-reason"]
100
+ : ACTIONS.ADMIN["reactivate-dispute-reason"], size: "sm", isLoading: pending === row.id, disabled: pending !== null, onClick: () => setActive(row, !row.isActive) })] })), [pending, setActive]);
101
+ return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: {
102
+ portal: "admin",
103
+ title: "Dispute reasons",
104
+ subtitle: "What buyers can pick when reporting a problem. A reason's kind decides which gate it inherits — the wording is yours.",
105
+ endpoint: endpoint,
106
+ queryKey: ["admin", "dispute-reasons"],
107
+ defaultSort: "label",
108
+ /*
109
+ * 🛑 REQUIRED on `ListingViewConfig`, and the default must be one of
110
+ * these or the dropdown opens with nothing selected (Root Cause #63).
111
+ * Eleven rows, so the only orders worth offering are the buyer-facing
112
+ * wording and which kind a reason belongs to.
113
+ */
114
+ sortOptions: [
115
+ { value: "label", label: "Wording (A–Z)" },
116
+ { value: "-label", label: "Wording (Z–A)" },
117
+ { value: "kind", label: "Kind" },
118
+ ],
119
+ filterKeys: ["scope"],
120
+ filterDefaults: { scope: "active" },
121
+ buildFilters: (state) => SCOPE_CHIPS.find((c) => c.id === (state.scope ?? "active"))?.filter,
122
+ mapRows: (res) => (res.items ?? []).map((r) => ({
123
+ id: r.id,
124
+ label: r.label,
125
+ kind: r.kind,
126
+ isActive: r.isActive,
127
+ createdByName: r.createdByName,
128
+ })),
129
+ getTotal: (res, rows) => res.total ?? rows.length,
130
+ /*
131
+ * 🛑 No `searchPlaceholder` and no `search` — the endpoint does not read
132
+ * `q`, and declaring either is what makes the box exist. ~39 views here
133
+ * render a box whose endpoint ignores the term: it accepts typing, the
134
+ * request carries it, and the results do not change, with no error
135
+ * anywhere. Eleven rows with scope chips need no search.
136
+ */
137
+ emptyLabel: "No reasons in this scope.",
138
+ primaryAction: {
139
+ label: ACTIONS.ADMIN["add-dispute-reason"].label,
140
+ onClick: ({ openCreatePanel }) => openCreatePanel(),
141
+ },
142
+ renderEditor: ({ closePanel }) => (_jsx(CreateReasonForm, { onSubmit: async (input) => {
143
+ await onCreate(input);
144
+ closePanel();
145
+ } })),
146
+ resolveEditorTitle: () => "Add a dispute reason",
147
+ onRowClick: (row) => setOpen(row),
148
+ renderRowActions: rowControls,
149
+ renderCards: (rows) => (_jsx(Stack, { gap: "sm", children: rows.map((row) => (_jsxs(Row, { surface: "card", padding: "md", rounded: "lg", border: "default", align: "center", justify: "between", gap: "md", className: "min-w-0", children: [_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsxs(Row, { align: "center", gap: "sm", className: "min-w-0", children: [_jsx(Text, { weight: "semibold", color: "primary", className: "min-w-0", children: row.label }), _jsx(Badge, { variant: row.isActive ? "success" : "inactive", children: row.isActive ? "Active" : "Retired" })] }), _jsx(Text, { size: "xs", color: "muted", children: KIND_MEANING[row.kind] }), _jsxs(Text, { size: "xs", color: "faint", children: ["Added by ", row.createdByName] })] }), rowControls(row)] }, row.id))) })),
150
+ } }), open && (_jsx(RecordDetailModal, { isOpen: true, onClose: () => setOpen(null), title: open.label, badges: [
151
+ {
152
+ label: open.isActive ? "Active" : "Retired",
153
+ variant: open.isActive ? "success" : "secondary",
154
+ },
155
+ ], fields: [
156
+ { label: "Buyer-facing wording", value: open.label },
157
+ { label: "Kind", value: `${open.kind} — ${KIND_MEANING[open.kind]}` },
158
+ {
159
+ label: "State",
160
+ value: open.isActive
161
+ ? "Active — buyers see it on the dispute form"
162
+ : "Retired — hidden from the form",
163
+ },
164
+ { label: "Added by", value: open.createdByName },
165
+ ], metadata: {
166
+ id: open.id,
167
+ /*
168
+ * Stated on the record itself, because it is the fact an agent is
169
+ * most likely to be unsure about at the moment they are deciding
170
+ * whether to retire: the order snapshots the LABEL, so retiring
171
+ * cannot rewrite what a past dispute said.
172
+ */
173
+ retiringIsNeverADelete: "Disputes already raised under this reason keep their wording — the order snapshots the label rather than pointing at this row.",
174
+ } }))] }));
175
+ }
@@ -32,7 +32,31 @@ export declare class DisputeReasonsRepository extends BaseRepository<DisputeReas
32
32
  reason: DisputeReasonDocument;
33
33
  created: boolean;
34
34
  }>;
35
+ /**
36
+ * Every row, retired included — the staff management list.
37
+ *
38
+ * 🛑 Deliberately NOT `listActiveByKind` with the `isActive` clause dropped.
39
+ * That method also pins `kind`, and a management view must span both kinds:
40
+ * a reason retired for the wrong gate is exactly the row staff need to find.
41
+ *
42
+ * No `isActive` filter is applied here at all, so the caller's own Sieve
43
+ * filters decide — which is what lets the view offer Active / Retired / All
44
+ * chips without this method growing a parameter per chip.
45
+ */
46
+ listAll(model: SieveModel): Promise<FirebaseSieveResult<DisputeReasonDocument>>;
35
47
  /** Soft-retire. See the schema for why this is never a delete. */
36
48
  retire(id: string): Promise<void>;
49
+ /**
50
+ * Put a retired reason back in the picker.
51
+ *
52
+ * `createReason` already reactivates on a colliding re-add, which covers the
53
+ * agent who types the wording again. This covers the different case the
54
+ * management view creates: a row you can SEE and want back, where re-typing
55
+ * its exact label to resurrect it would be a trick rather than a control.
56
+ *
57
+ * The original `createdByName` is kept — they wrote the wording, and a
58
+ * reactivation is not authorship.
59
+ */
60
+ reactivate(id: string): Promise<void>;
37
61
  }
38
62
  export declare const disputeReasonsRepository: DisputeReasonsRepository;
@@ -59,10 +59,38 @@ export class DisputeReasonsRepository extends BaseRepository {
59
59
  await this.createWithId(id, reason);
60
60
  return { reason, created: true };
61
61
  }
62
+ /**
63
+ * Every row, retired included — the staff management list.
64
+ *
65
+ * 🛑 Deliberately NOT `listActiveByKind` with the `isActive` clause dropped.
66
+ * That method also pins `kind`, and a management view must span both kinds:
67
+ * a reason retired for the wrong gate is exactly the row staff need to find.
68
+ *
69
+ * No `isActive` filter is applied here at all, so the caller's own Sieve
70
+ * filters decide — which is what lets the view offer Active / Retired / All
71
+ * chips without this method growing a parameter per chip.
72
+ */
73
+ async listAll(model) {
74
+ return this.sieveQuery(model, DisputeReasonsRepository.SIEVE_FIELDS, { baseQuery: this.getCollection(), defaultPageSize: 20, maxPageSize: 50 });
75
+ }
62
76
  /** Soft-retire. See the schema for why this is never a delete. */
63
77
  async retire(id) {
64
78
  await this.update(id, { isActive: false, updatedAt: new Date() });
65
79
  }
80
+ /**
81
+ * Put a retired reason back in the picker.
82
+ *
83
+ * `createReason` already reactivates on a colliding re-add, which covers the
84
+ * agent who types the wording again. This covers the different case the
85
+ * management view creates: a row you can SEE and want back, where re-typing
86
+ * its exact label to resurrect it would be a trick rather than a control.
87
+ *
88
+ * The original `createdByName` is kept — they wrote the wording, and a
89
+ * reactivation is not authorship.
90
+ */
91
+ async reactivate(id) {
92
+ await this.update(id, { isActive: true, updatedAt: new Date() });
93
+ }
66
94
  }
67
95
  DisputeReasonsRepository.SIEVE_FIELDS = {
68
96
  /*
@@ -22,10 +22,10 @@
22
22
  * should be able to add it without a release.
23
23
  */
24
24
  import type { DisputeKind } from "../../../_internal/shared/features/orders/dispute-reasons";
25
+ import type { BaseDocument } from "../../../_internal/shared/types/base-document";
25
26
  export declare const DISPUTE_REASON_COLLECTION = "disputeReasons";
26
- export interface DisputeReasonDocument {
27
- /** `dreason-<slug of label>`, so the same reason cannot be added twice. */
28
- id: string;
27
+ /** `id` is `dreason-<slug of label>`, so the same reason cannot be added twice. */
28
+ export interface DisputeReasonDocument extends BaseDocument {
29
29
  /** Buyer-facing. Shown verbatim in the picker. */
30
30
  label: string;
31
31
  /** Which gate this reason is subject to. Never free-form. */
@@ -43,8 +43,6 @@ export interface DisputeReasonDocument {
43
43
  createdByName: string;
44
44
  /** The uid, for moderation. Never rendered. */
45
45
  createdByUid: string;
46
- createdAt: Date;
47
- updatedAt: Date;
48
46
  }
49
47
  export declare const DISPUTE_REASON_FIELDS: {
50
48
  readonly ID: "id";
@@ -16,5 +16,5 @@ export function EventSaleBanner({ discountPercent, couponCode, title, subtitle,
16
16
  const href = filterParam
17
17
  ? `${ROUTES.PUBLIC.PRODUCTS}?${encodeURIComponent(filterParam.key)}=${encodeURIComponent(filterParam.value)}`
18
18
  : `${ROUTES.PUBLIC.PRODUCTS}?onSale=true`;
19
- return (_jsx(Section, { color: "inverse", tone: "accent-banner", className: `${__P.p6}`, rounded: "2xl", shadow: "lg", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, weight: "bold", size: "3xl", children: headline }), subtitle ? _jsx(Text, { color: "inverse-muted", size: "base", children: subtitle }) : null] }), couponCode ? (_jsxs(Div, { layout: "inline-flex", gap: "3", align: "center", className: "backdrop-blur", surface: "default", paddingY: "y-xs", paddingX: "x-md", rounded: "lg", children: [_jsx(Span, { size: "xs", className: "tracking-wide opacity-80", transform: "uppercase", children: "Code" }), _jsx(Span, { size: "lg", weight: "bold", className: "font-mono", children: couponCode })] })) : null, _jsx(Div, { children: _jsx(Link, { href: href, children: _jsx(Button, { variant: "secondary", children: "Shop the sale" }) }) })] }) }));
19
+ return (_jsx(Section, { color: "inverse", tone: "accent-banner", className: `${__P.p6}`, rounded: "2xl", shadow: "lg", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { color: "inverse", variant: "none", level: 2, weight: "bold", size: "3xl", children: headline }), subtitle ? _jsx(Text, { color: "inverse-muted", size: "base", children: subtitle }) : null] }), couponCode ? (_jsxs(Div, { layout: "inline-flex", gap: "3", align: "center", className: "backdrop-blur", surface: "default", paddingY: "y-xs", paddingX: "x-md", rounded: "lg", children: [_jsx(Span, { size: "xs", className: "tracking-wide opacity-80", transform: "uppercase", children: "Code" }), _jsx(Span, { size: "lg", weight: "bold", className: "font-mono", children: couponCode })] })) : null, _jsx(Div, { children: _jsx(Link, { href: href, children: _jsx(Button, { variant: "secondary", children: "Shop the sale" }) }) })] }) }));
20
20
  }
@@ -45,12 +45,12 @@ export function FAQPageContent({ initialCategory = "all", categories, routeHelpe
45
45
  total,
46
46
  categoryCounts,
47
47
  onSelect: handleCategorySelect,
48
- }) }), _jsxs(Div, { layout: "grid", gap: "8", className: "grid-cols-1 lg:grid-cols-12", children: [_jsx(Div, { className: "hidden lg:col-span-4 lg:block xl:col-span-3", children: _jsx(FAQCategorySidebar, { selectedCategory: selectedCategory, onCategorySelect: handleCategorySelect, categoryCounts: categoryCounts, categories: categories, allFaqsHref: routeHelpers.allFaqsHref, categoryHref: routeHelpers.faqCategoryHref, contactHref: routeHelpers.contactHref, labels: {
48
+ }) }), _jsxs(Div, { layout: "grid", gap: "8", className: "lg:grid-cols-12", children: [_jsx(Div, { className: "hidden lg:col-span-4 lg:block xl:col-span-3", children: _jsx(FAQCategorySidebar, { selectedCategory: selectedCategory, onCategorySelect: handleCategorySelect, categoryCounts: categoryCounts, categories: categories, allFaqsHref: routeHelpers.allFaqsHref, categoryHref: routeHelpers.faqCategoryHref, contactHref: routeHelpers.contactHref, labels: {
49
49
  title: labels.categoriesTitle,
50
50
  allFaqs: labels.allFaqs,
51
51
  stillHaveQuestions: labels.stillHaveQuestions,
52
52
  contactSupport: labels.contactSupport,
53
- } }) }), _jsxs(Div, { className: "col-span-1 lg:col-span-8 xl:col-span-9", children: [_jsx(SlottedListingView, { portal: "public", inlineToolbar: true, className: "space-y-6", renderHeader: () => {
53
+ } }) }), _jsxs(Div, { className: "lg:col-span-8 xl:col-span-9", children: [_jsx(SlottedListingView, { portal: "public", inlineToolbar: true, className: "space-y-6", renderHeader: () => {
54
54
  /*
55
55
  * The fuller answer, next to the short ones.
56
56
  *
@@ -44,13 +44,13 @@ export declare const gatewayEventFirestoreSchema: z.ZodObject<{
44
44
  terminal?: boolean | undefined;
45
45
  amount?: number | undefined;
46
46
  seedPurpose?: string | undefined;
47
+ payload?: Record<string, unknown> | undefined;
47
48
  orderIds?: string[] | undefined;
48
49
  processedAt?: Date | undefined;
49
50
  gatewayPaymentId?: string | undefined;
50
51
  gatewayOrderId?: string | undefined;
51
52
  providerEventId?: string | undefined;
52
53
  lastError?: string | undefined;
53
- payload?: Record<string, unknown> | undefined;
54
54
  payloadTruncated?: boolean | undefined;
55
55
  }, {
56
56
  status: "received" | "failed" | "processed" | "processing" | "conflict" | "ignored";
@@ -69,13 +69,13 @@ export declare const gatewayEventFirestoreSchema: z.ZodObject<{
69
69
  terminal?: boolean | undefined;
70
70
  amount?: number | undefined;
71
71
  seedPurpose?: string | undefined;
72
+ payload?: Record<string, unknown> | undefined;
72
73
  orderIds?: string[] | undefined;
73
74
  processedAt?: Date | undefined;
74
75
  gatewayPaymentId?: string | undefined;
75
76
  gatewayOrderId?: string | undefined;
76
77
  providerEventId?: string | undefined;
77
78
  lastError?: string | undefined;
78
- payload?: Record<string, unknown> | undefined;
79
79
  payloadTruncated?: boolean | undefined;
80
80
  }>;
81
81
  export type GatewayEventFromSchema = z.infer<typeof gatewayEventFirestoreSchema>;
@@ -867,16 +867,16 @@ export declare const carouselSlideFirestoreSchema: z.ZodObject<{
867
867
  opacity?: number | undefined;
868
868
  subtitle?: string | undefined;
869
869
  } | undefined;
870
+ settings?: {
871
+ height?: "medium" | "viewport" | "tall" | undefined;
872
+ autoplayDelayMs?: number | undefined;
873
+ } | undefined;
870
874
  analytics?: {
871
875
  views: number;
872
876
  lastViewed?: string | Date | z.objectOutputType<{
873
877
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
874
878
  }, z.ZodTypeAny, "passthrough"> | undefined;
875
879
  } | undefined;
876
- settings?: {
877
- height?: "medium" | "viewport" | "tall" | undefined;
878
- autoplayDelayMs?: number | undefined;
879
- } | undefined;
880
880
  carouselId?: string | null | undefined;
881
881
  }, {
882
882
  id: string;
@@ -976,16 +976,16 @@ export declare const carouselSlideFirestoreSchema: z.ZodObject<{
976
976
  opacity?: number | undefined;
977
977
  subtitle?: string | undefined;
978
978
  } | undefined;
979
+ settings?: {
980
+ height?: "medium" | "viewport" | "tall" | undefined;
981
+ autoplayDelayMs?: number | undefined;
982
+ } | undefined;
979
983
  analytics?: {
980
984
  views: number;
981
985
  lastViewed?: string | Date | z.objectInputType<{
982
986
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
983
987
  }, z.ZodTypeAny, "passthrough"> | undefined;
984
988
  } | undefined;
985
- settings?: {
986
- height?: "medium" | "viewport" | "tall" | undefined;
987
- autoplayDelayMs?: number | undefined;
988
- } | undefined;
989
989
  carouselId?: string | null | undefined;
990
990
  }>;
991
991
  export type HomepageSectionFromSchema = z.infer<typeof homepageSectionFirestoreSchema>;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * A HOLD on stock that is not yet an order.
3
+ *
4
+ * ## 🛑 Why this has to exist before S5b (Part 6)
5
+ *
6
+ * The plan calls this "the largest hidden cost" of the shared-purchase work,
7
+ * and the reason is that today there is no way to hold stock at all. The only
8
+ * mechanism is: `onOrderCreate` decrements `availableQuantity`, and
9
+ * `pendingOrderTimeout` puts it back if the buyer does not pay inside the
10
+ * 15-minute window. Both are keyed to an ORDER.
11
+ *
12
+ * A shared purchase runs for DAYS while co-buyers pay. With no primitive,
13
+ * either the coordinator places an order up front — which starts a 15-minute
14
+ * payment clock on a multi-day agreement — or nothing is held, and the item is
15
+ * sold out from under the arrangement by an ordinary buyer who happened to
16
+ * check out first. Neither is a product.
17
+ *
18
+ * ## The invariant, and where it already lives
19
+ *
20
+ * `products.repository.ts` already states it:
21
+ *
22
+ * reserved = stockQuantity - availableQuantity
23
+ *
24
+ * …as the comment on `resolveStockResync` puts it, *"the difference between
25
+ * the two IS the outstanding reservation"*. This collection does not invent a
26
+ * new idea; it makes that difference ADDRESSABLE — every held unit gets a row
27
+ * saying who holds it and until when, instead of being an anonymous gap
28
+ * between two numbers that only an order can close.
29
+ *
30
+ * 🛑 So a reservation MUST move `availableQuantity` in the same transaction
31
+ * that writes its row. A row without the decrement holds nothing; a decrement
32
+ * without a row is the anonymous gap again, and nothing can expire it.
33
+ *
34
+ * ## Why a TTL rather than a cancel button
35
+ *
36
+ * A hold with no expiry is a leak: the holder closes the tab, and those units
37
+ * are off the shelf until a human notices. `expiresAt` means the worst case is
38
+ * bounded by the sweep interval, and the seller never has to ask anyone to
39
+ * release anything.
40
+ */
41
+ import type { BaseDocument } from "../../../_internal/shared/types/base-document";
42
+ export declare const STOCK_RESERVATION_COLLECTION = "stockReservations";
43
+ /**
44
+ * Who is holding, and therefore what releases it.
45
+ *
46
+ * 🛑 A plain `holderId: string` was the first draft and it is wrong: the sweep
47
+ * and the release path both need to know WHICH system owns the hold before
48
+ * they can decide whether it is still legitimate. A discriminator makes
49
+ * "release every hold belonging to arrangement X" expressible; an opaque id
50
+ * makes it a string-prefix convention nobody enforces.
51
+ */
52
+ export declare const RESERVATION_HOLDER_KINDS: readonly ["shared-purchase", "manual"];
53
+ export type ReservationHolderKind = (typeof RESERVATION_HOLDER_KINDS)[number];
54
+ /**
55
+ * 🛑 `cart` and `order` are deliberately NOT holder kinds.
56
+ *
57
+ * A cart holds nothing today — adding an item does not decrement anything —
58
+ * and making it hold would silently turn every abandoned cart into held
59
+ * stock. An order already has its own hold through `onOrderCreate` +
60
+ * `pendingOrderTimeout`; a second mechanism over the same units would
61
+ * double-decrement, which is exactly the class of bug `resolveStockResync`'s
62
+ * delta exists to prevent. Add a kind only when the thing that owns it also
63
+ * owns its release.
64
+ */
65
+ export declare const RESERVATION_STATUSES: readonly ["held", "consumed", "released", "expired"];
66
+ export type ReservationStatus = (typeof RESERVATION_STATUSES)[number];
67
+ export interface StockReservationDocument extends BaseDocument {
68
+ productId: string;
69
+ /** Denormalised so the sweep can restore stock without reading the product first. */
70
+ quantity: number;
71
+ holderKind: ReservationHolderKind;
72
+ /** The arrangement / manual-hold id. Scoped by `holderKind`, never globally unique. */
73
+ holderId: string;
74
+ /** Who asked for it — an Auth uid, for the audit trail and for admin release. */
75
+ createdBy: string;
76
+ status: ReservationStatus;
77
+ /**
78
+ * 🛑 The whole point. A hold with no expiry is a leak: the holder closes the
79
+ * tab and the units are gone until a human notices.
80
+ */
81
+ expiresAt: Date;
82
+ /**
83
+ * Set when the hold ENDS, whichever way it ended. One field rather than
84
+ * three (`consumedAt`/`releasedAt`/`expiredAt`) because `status` already
85
+ * says which, and three nullable dates is three chances for the one that
86
+ * matters to be the one nobody set.
87
+ */
88
+ endedAt?: Date;
89
+ /** Free text on a manual release — why an admin took the hold back. */
90
+ endedReason?: string;
91
+ }
92
+ /** Default hold, when a caller names none. */
93
+ export declare const RESERVATION_DEFAULT_TTL_HOURS = 72;
94
+ /**
95
+ * 🛑 A ceiling, and it is load-bearing rather than defensive.
96
+ *
97
+ * A caller may pass any TTL it likes, which makes "hold this for a year" one
98
+ * typo away — and a year-long hold is indistinguishable from permanently
99
+ * deleted stock, because nothing in the UI shows a seller why their own
100
+ * product is unbuyable. 30 days is longer than any agreement window Part 6
101
+ * contemplates and short enough that a mistake self-heals.
102
+ */
103
+ export declare const RESERVATION_MAX_TTL_HOURS: number;
104
+ /** A reservation is only holding stock while it is `held` and unexpired. */
105
+ export declare function isReservationActive(reservation: Pick<StockReservationDocument, "status" | "expiresAt">, now?: Date): boolean;
106
+ /**
107
+ * Firestore hands back a `Timestamp`, a `Date` or an ISO string depending on
108
+ * the path a document took to get here, and the adapter that normalises those
109
+ * is not reachable from this file. Returning `null` on anything unparseable is
110
+ * the honest answer — and callers treat an unparseable expiry as NOT active,
111
+ * so the failure mode is a hold released early rather than one held forever.
112
+ */
113
+ declare function toDate(value: unknown): Date | null;
114
+ export { toDate as reservationExpiryDate };