@mohasinac/appkit 4.62.1 โ†’ 4.63.1

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 (263) 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 +157 -0
  112. package/dist/features/ads/schemas/booking-write.js +88 -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-entry.d.ts +1 -1
  232. package/dist/server-entry.js +10 -1
  233. package/dist/server.d.ts +29 -1
  234. package/dist/server.js +158 -0
  235. package/dist/styles.css +349 -1
  236. package/dist/tailwind-utilities.css +1 -1
  237. package/dist/ui/components/Button.d.ts +10 -1
  238. package/dist/ui/components/Button.js +25 -3
  239. package/dist/ui/components/Chart.d.ts +63 -0
  240. package/dist/ui/components/Chart.js +169 -0
  241. package/dist/ui/components/Chart.style.css +214 -0
  242. package/dist/ui/components/Disclaimer.js +10 -1
  243. package/dist/ui/components/OverflowRow.js +54 -2
  244. package/dist/ui/components/OverflowRow.style.css +38 -0
  245. package/dist/ui/components/RichTextEditor.js +99 -8
  246. package/dist/ui/components/chart-spec.d.ts +68 -0
  247. package/dist/ui/components/chart-spec.js +140 -0
  248. package/dist/ui/components/index.style.css +1 -0
  249. package/dist/ui/components/surface-tokens.d.ts +1 -1
  250. package/dist/ui/components/surface-tokens.js +9 -0
  251. package/dist/ui/components/toolbar-collapse.d.ts +13 -0
  252. package/dist/ui/components/toolbar-collapse.js +10 -1
  253. package/dist/ui/index.d.ts +6 -0
  254. package/dist/ui/index.js +3 -0
  255. package/dist/ui/rich-text/RichText.d.ts +10 -1
  256. package/dist/ui/rich-text/RichText.js +33 -2
  257. package/dist/ui/rich-text/RichText.style.css +35 -0
  258. package/dist/ui/rich-text/RichTextRenderer.d.ts +30 -1
  259. package/dist/ui/rich-text/RichTextRenderer.js +34 -2
  260. package/dist/ui/rich-text/chart-blocks.d.ts +62 -0
  261. package/dist/ui/rich-text/chart-blocks.js +87 -0
  262. package/package.json +1 -1
  263. package/scripts/seed-cli.mjs +3 -0
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The one read a dashboard hub or landing page needs.
3
+ *
4
+ * ## ๐Ÿ›‘ Read on the SERVER, not fetched from the client
5
+ *
6
+ * The plan named an endpoint โ€” `/api/dashboard/attention` โ€” and the shape it
7
+ * asked for is "one read covering every section", which this is. It is read
8
+ * here instead because every one of these pages is **already per-request**:
9
+ * `admin/layout.tsx` and `store/layout.tsx` `await getServerSessionUser()`, so
10
+ * the whole subtree is dynamic with or without an endpoint (ยง "โ€ฆa dynamic API
11
+ * in a DASHBOARD layout is the correct lever").
12
+ *
13
+ * An endpoint would therefore have added a SECOND billed invocation per hub
14
+ * visit to fetch data the page's own render could already have, which is
15
+ * exactly the shape of Root Cause #94 โ€” a client fetch that reads as free in
16
+ * review and bills per mount. There is no endpoint, and one should not be added
17
+ * unless something outside a dashboard page needs these numbers.
18
+ *
19
+ * ## Two reads, and both fail OPEN
20
+ *
21
+ * `navConfig` decides which nav entries an admin has switched off, and the hub
22
+ * must agree with the sidebar about that โ€” so it is read rather than ignored.
23
+ * On failure it resolves to `undefined`, which means "nothing is disabled" and
24
+ * shows MORE cards than it should. That is the right direction: a disabled nav
25
+ * entry is a configuration choice rather than a permission, and the page behind
26
+ * every card still enforces its own route guard. Hiding sections because a
27
+ * settings read blipped would be the damaging failure.
28
+ *
29
+ * The counts fail to `null` per card โ€” see `attention.ts`, which exists to
30
+ * explain why `0` is the one answer a badge must never invent.
31
+ */
32
+ import { type DashboardHubSection } from "../../../shared/features/dashboard/hub-sections";
33
+ import type { SidebarNavGroup } from "../../../shared/features/layout/types";
34
+ import { type AttentionCounts } from "./attention";
35
+ export interface DashboardHubData {
36
+ /** Permission- and toggle-filtered, in nav order. Never empty-group padded. */
37
+ sections: DashboardHubSection[];
38
+ counts: AttentionCounts;
39
+ }
40
+ /**
41
+ * Resolve the sections a viewer may see, plus their attention counts.
42
+ *
43
+ * @param groups the portal's nav groups, unfiltered โ€” this applies the filter
44
+ * @param permissions `null` means admin (the convention `filterNavGroups` owns)
45
+ * @param storeId the viewer's store SLUG, for seller-scoped counts
46
+ */
47
+ export declare function getDashboardHubData(groups: readonly SidebarNavGroup[], permissions: string[] | null | undefined, storeId?: string): Promise<DashboardHubData>;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The one read a dashboard hub or landing page needs.
3
+ *
4
+ * ## ๐Ÿ›‘ Read on the SERVER, not fetched from the client
5
+ *
6
+ * The plan named an endpoint โ€” `/api/dashboard/attention` โ€” and the shape it
7
+ * asked for is "one read covering every section", which this is. It is read
8
+ * here instead because every one of these pages is **already per-request**:
9
+ * `admin/layout.tsx` and `store/layout.tsx` `await getServerSessionUser()`, so
10
+ * the whole subtree is dynamic with or without an endpoint (ยง "โ€ฆa dynamic API
11
+ * in a DASHBOARD layout is the correct lever").
12
+ *
13
+ * An endpoint would therefore have added a SECOND billed invocation per hub
14
+ * visit to fetch data the page's own render could already have, which is
15
+ * exactly the shape of Root Cause #94 โ€” a client fetch that reads as free in
16
+ * review and bills per mount. There is no endpoint, and one should not be added
17
+ * unless something outside a dashboard page needs these numbers.
18
+ *
19
+ * ## Two reads, and both fail OPEN
20
+ *
21
+ * `navConfig` decides which nav entries an admin has switched off, and the hub
22
+ * must agree with the sidebar about that โ€” so it is read rather than ignored.
23
+ * On failure it resolves to `undefined`, which means "nothing is disabled" and
24
+ * shows MORE cards than it should. That is the right direction: a disabled nav
25
+ * entry is a configuration choice rather than a permission, and the page behind
26
+ * every card still enforces its own route guard. Hiding sections because a
27
+ * settings read blipped would be the damaging failure.
28
+ *
29
+ * The counts fail to `null` per card โ€” see `attention.ts`, which exists to
30
+ * explain why `0` is the one answer a badge must never invent.
31
+ */
32
+ import { cache } from "react";
33
+ import { safeRead } from "../../../../errors/safe-read.js";
34
+ import { siteSettingsRepository } from "../../../../features/admin/repository/site-settings.repository.js";
35
+ import { filterNavGroups } from "../../../client/features/layout/filterNavItems.js";
36
+ import { hubSections, } from "../../../shared/features/dashboard/hub-sections.js";
37
+ import { collectAttentionCounts } from "./attention.js";
38
+ /**
39
+ * `navConfig` alone, cached per request.
40
+ *
41
+ * `React.cache` because a landing page renders the hub once but a hub page may
42
+ * also want the section list for its own heading, and the narrowest shared call
43
+ * is the one to cache (ยง "`React.cache` discipline" โ€” cache the repository
44
+ * read, not the viewer-specific wrapper around it, or two call sites with
45
+ * different arguments dedupe nothing).
46
+ */
47
+ const getNavConfig = cache(async () => safeRead(async () => {
48
+ const settings = (await siteSettingsRepository.getSingleton());
49
+ return settings?.navConfig;
50
+ }, {
51
+ route: "dashboard-hub",
52
+ key: "navConfig",
53
+ fallback: undefined,
54
+ }));
55
+ /**
56
+ * Resolve the sections a viewer may see, plus their attention counts.
57
+ *
58
+ * @param groups the portal's nav groups, unfiltered โ€” this applies the filter
59
+ * @param permissions `null` means admin (the convention `filterNavGroups` owns)
60
+ * @param storeId the viewer's store SLUG, for seller-scoped counts
61
+ */
62
+ export async function getDashboardHubData(groups, permissions, storeId) {
63
+ const navConfig = await getNavConfig();
64
+ /*
65
+ * Filter FIRST, then count โ€” the visible ids are the scope (see
66
+ * `collectAttentionCounts`). Doing it the other way round costs an employee
67
+ * five queries to render one card, and computes figures from collections
68
+ * their permission set does not cover.
69
+ *
70
+ * Sequential rather than parallel with the settings read for the same
71
+ * reason: the second call's input is the first call's output. Two
72
+ * round trips, inside Rule #6's ~3.
73
+ */
74
+ const sections = hubSections(filterNavGroups([...groups], navConfig, permissions));
75
+ const visibleItemIds = sections.flatMap((s) => s.items.map((i) => i.id).filter(Boolean));
76
+ const counts = await safeRead(() => collectAttentionCounts(visibleItemIds, storeId), {
77
+ route: "dashboard-hub",
78
+ key: "attention",
79
+ fallback: {},
80
+ });
81
+ return { sections, counts };
82
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Hold stock for something that is not an order yet.
3
+ *
4
+ * ## What this is for
5
+ *
6
+ * The plan's Part 6 (shared purchase) names this "the largest hidden cost",
7
+ * and it is the only one of its three prerequisites that had NOTHING behind
8
+ * it. See the schema file for why the existing order-keyed mechanism cannot
9
+ * serve a multi-day arrangement.
10
+ *
11
+ * ## ๐Ÿ›‘ Every function here moves the row and the counter TOGETHER
12
+ *
13
+ * `availableQuantity` is what a buyer can take; the gap between it and
14
+ * `stockQuantity` is the outstanding hold (`resolveStockResync`'s own words).
15
+ * A reservation row written without the decrement holds nothing at all, and a
16
+ * decrement written without a row is a hold nothing can expire โ€” so both
17
+ * happen inside ONE `runTransaction`, with the product read inside it so two
18
+ * concurrent reserves cannot both see the same `availableQuantity`.
19
+ *
20
+ * ## ๐Ÿ›‘ Firestore wants every READ before every WRITE
21
+ *
22
+ * So `endReservation` reads the reservation AND the product up front, even
23
+ * on the consume path where the product read is thrown away. The natural
24
+ * shape โ€” read the reservation, decide, then read the product โ€” throws
25
+ * `Firestore transactions require all reads to be executed before all
26
+ * writes`, and only at runtime, which is exactly the class of fault
27
+ * `restoreStockForOrder`'s header already warns about one directory over.
28
+ *
29
+ * ## Why it writes `availableQuantity` EXPLICITLY
30
+ *
31
+ * `ProductRepository.update`'s `resolveStockResync` returns `{}` the moment a
32
+ * write names `availableQuantity` โ€” deliberately, so the checkout decrement,
33
+ * the stock-restore job and `updateAvailableQuantity` stay authoritative over
34
+ * their own number. This is a fourth member of that set and takes the same
35
+ * deal: it is the one deciding the value, so the resync must keep its hands
36
+ * off it. (These writes go through the raw collection rather than the
37
+ * repository for the same reason `deriveTaxonomy` reads categories through
38
+ * `this.db` โ€” a transaction needs the ref, not a repository method.)
39
+ */
40
+ import { type ReservationHolderKind, type StockReservationDocument } from "../../../../features/inventory/schemas/stock-reservation";
41
+ export interface ReserveStockInput {
42
+ productId: string;
43
+ quantity: number;
44
+ holderKind: ReservationHolderKind;
45
+ holderId: string;
46
+ createdBy: string;
47
+ /** Defaults to `RESERVATION_DEFAULT_TTL_HOURS`; capped at the 30-day ceiling. */
48
+ ttlHours?: number;
49
+ }
50
+ /**
51
+ * Take a hold. Returns the reservation id.
52
+ *
53
+ * ๐Ÿ›‘ Refuses rather than over-reserving. A seller's stock is a real number of
54
+ * real objects, so "reserve 5, only 3 left" is a **409** โ€” the request is
55
+ * well-formed and the world moved, which is the same distinction the lottery
56
+ * config write makes for a slot somebody has already pulled.
57
+ */
58
+ export declare function reserveStock(input: ReserveStockInput): Promise<string>;
59
+ /**
60
+ * End a hold and PUT THE UNITS BACK โ€” the holder changed their mind, an admin
61
+ * took it back, or the sweep found it expired.
62
+ */
63
+ export declare function releaseReservation(reservationId: string, opts?: {
64
+ status?: "released" | "expired";
65
+ reason?: string;
66
+ }): Promise<void>;
67
+ /**
68
+ * End a hold and KEEP the units off the shelf โ€” the arrangement completed and
69
+ * an order now owns them.
70
+ *
71
+ * ๐Ÿ›‘ This is the one path that must NOT restore. The order's own decrement is
72
+ * what holds the units from here, so restoring first would hand the same
73
+ * stock to the next buyer for the moment between the two writes โ€” and if the
74
+ * order write never lands, restoring would have been right. That race is why
75
+ * the caller should place the order and consume in one flow it can reason
76
+ * about; this function exists so it can do that rather than calling release
77
+ * and hoping.
78
+ */
79
+ export declare function consumeReservation(reservationId: string): Promise<void>;
80
+ /**
81
+ * Release every hold a holder owns. Used when a shared purchase is cancelled
82
+ * or abandoned โ€” the arrangement knows its own id, and should not have to
83
+ * remember which reservations it took.
84
+ */
85
+ export declare function releaseReservationsForHolder(holderKind: ReservationHolderKind, holderId: string, reason?: string): Promise<number>;
86
+ /**
87
+ * The sweep. Expire every hold past its `expiresAt` and restore its units.
88
+ *
89
+ * ๐Ÿ›‘ Bounded, and the bound is REPORTED. A silent cap reads as "nothing left
90
+ * to expire" on the one run where it mattered โ€” the same lie a truncated
91
+ * recount tells (Root Cause #102).
92
+ */
93
+ export declare function expireDueReservations(limit?: number): Promise<{
94
+ scanned: number;
95
+ expired: number;
96
+ truncated: boolean;
97
+ }>;
98
+ /** Units currently held on a product, for an admin explaining an unbuyable listing. */
99
+ export declare function activeReservationsForProduct(productId: string): Promise<StockReservationDocument[]>;
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Hold stock for something that is not an order yet.
3
+ *
4
+ * ## What this is for
5
+ *
6
+ * The plan's Part 6 (shared purchase) names this "the largest hidden cost",
7
+ * and it is the only one of its three prerequisites that had NOTHING behind
8
+ * it. See the schema file for why the existing order-keyed mechanism cannot
9
+ * serve a multi-day arrangement.
10
+ *
11
+ * ## ๐Ÿ›‘ Every function here moves the row and the counter TOGETHER
12
+ *
13
+ * `availableQuantity` is what a buyer can take; the gap between it and
14
+ * `stockQuantity` is the outstanding hold (`resolveStockResync`'s own words).
15
+ * A reservation row written without the decrement holds nothing at all, and a
16
+ * decrement written without a row is a hold nothing can expire โ€” so both
17
+ * happen inside ONE `runTransaction`, with the product read inside it so two
18
+ * concurrent reserves cannot both see the same `availableQuantity`.
19
+ *
20
+ * ## ๐Ÿ›‘ Firestore wants every READ before every WRITE
21
+ *
22
+ * So `endReservation` reads the reservation AND the product up front, even
23
+ * on the consume path where the product read is thrown away. The natural
24
+ * shape โ€” read the reservation, decide, then read the product โ€” throws
25
+ * `Firestore transactions require all reads to be executed before all
26
+ * writes`, and only at runtime, which is exactly the class of fault
27
+ * `restoreStockForOrder`'s header already warns about one directory over.
28
+ *
29
+ * ## Why it writes `availableQuantity` EXPLICITLY
30
+ *
31
+ * `ProductRepository.update`'s `resolveStockResync` returns `{}` the moment a
32
+ * write names `availableQuantity` โ€” deliberately, so the checkout decrement,
33
+ * the stock-restore job and `updateAvailableQuantity` stay authoritative over
34
+ * their own number. This is a fourth member of that set and takes the same
35
+ * deal: it is the one deciding the value, so the resync must keep its hands
36
+ * off it. (These writes go through the raw collection rather than the
37
+ * repository for the same reason `deriveTaxonomy` reads categories through
38
+ * `this.db` โ€” a transaction needs the ref, not a repository method.)
39
+ */
40
+ import { getAdminDb } from "../../../../providers/db-firebase/index.js";
41
+ import { ValidationError } from "../../../../errors/validation-error.js";
42
+ import { NotFoundError } from "../../../../errors/not-found-error.js";
43
+ import { ConflictError } from "../../../../errors/conflict-error.js";
44
+ import { normalizeError } from "../../../../errors/normalize.js";
45
+ import { serverLogger } from "../../../../monitoring/server-logger.js";
46
+ import { PRODUCT_COLLECTION } from "../../../../features/products/schemas/firestore.js";
47
+ import { STOCK_RESERVATION_FIELDS } from "../../../../constants/field-names.js";
48
+ import { RESERVATION_DEFAULT_TTL_HOURS, RESERVATION_MAX_TTL_HOURS, STOCK_RESERVATION_COLLECTION, } from "../../../../features/inventory/schemas/stock-reservation.js";
49
+ /**
50
+ * Take a hold. Returns the reservation id.
51
+ *
52
+ * ๐Ÿ›‘ Refuses rather than over-reserving. A seller's stock is a real number of
53
+ * real objects, so "reserve 5, only 3 left" is a **409** โ€” the request is
54
+ * well-formed and the world moved, which is the same distinction the lottery
55
+ * config write makes for a slot somebody has already pulled.
56
+ */
57
+ export async function reserveStock(input) {
58
+ const quantity = Math.floor(input.quantity);
59
+ if (!Number.isFinite(quantity) || quantity < 1) {
60
+ throw new ValidationError("Reserve at least one unit.");
61
+ }
62
+ const ttlHours = clampTtl(input.ttlHours);
63
+ const db = getAdminDb();
64
+ const productRef = db.collection(PRODUCT_COLLECTION).doc(input.productId);
65
+ const reservationRef = db.collection(STOCK_RESERVATION_COLLECTION).doc();
66
+ await db.runTransaction(async (tx) => {
67
+ const snap = await tx.get(productRef);
68
+ if (!snap.exists)
69
+ throw new NotFoundError("Product not found.");
70
+ const available = readAvailable(snap.data());
71
+ if (available < quantity) {
72
+ throw new ConflictError(`Only ${Math.max(0, available)} left โ€” cannot hold ${quantity}.`);
73
+ }
74
+ const now = new Date();
75
+ const doc = {
76
+ productId: input.productId,
77
+ quantity,
78
+ holderKind: input.holderKind,
79
+ holderId: input.holderId,
80
+ createdBy: input.createdBy,
81
+ status: "held",
82
+ expiresAt: new Date(now.getTime() + ttlHours * 60 * 60 * 1000),
83
+ createdAt: now,
84
+ updatedAt: now,
85
+ };
86
+ tx.set(reservationRef, doc);
87
+ tx.update(productRef, { availableQuantity: available - quantity, updatedAt: now });
88
+ });
89
+ return reservationRef.id;
90
+ }
91
+ /**
92
+ * End a hold and PUT THE UNITS BACK โ€” the holder changed their mind, an admin
93
+ * took it back, or the sweep found it expired.
94
+ */
95
+ export async function releaseReservation(reservationId, opts = {}) {
96
+ await endReservation(reservationId, opts.status ?? "released", true, opts.reason);
97
+ }
98
+ /**
99
+ * End a hold and KEEP the units off the shelf โ€” the arrangement completed and
100
+ * an order now owns them.
101
+ *
102
+ * ๐Ÿ›‘ This is the one path that must NOT restore. The order's own decrement is
103
+ * what holds the units from here, so restoring first would hand the same
104
+ * stock to the next buyer for the moment between the two writes โ€” and if the
105
+ * order write never lands, restoring would have been right. That race is why
106
+ * the caller should place the order and consume in one flow it can reason
107
+ * about; this function exists so it can do that rather than calling release
108
+ * and hoping.
109
+ */
110
+ export async function consumeReservation(reservationId) {
111
+ await endReservation(reservationId, "consumed", false);
112
+ }
113
+ /**
114
+ * Release every hold a holder owns. Used when a shared purchase is cancelled
115
+ * or abandoned โ€” the arrangement knows its own id, and should not have to
116
+ * remember which reservations it took.
117
+ */
118
+ export async function releaseReservationsForHolder(holderKind, holderId, reason) {
119
+ const db = getAdminDb();
120
+ const snap = await db
121
+ .collection(STOCK_RESERVATION_COLLECTION)
122
+ .where(STOCK_RESERVATION_FIELDS.HOLDER_KIND, "==", holderKind)
123
+ .where(STOCK_RESERVATION_FIELDS.HOLDER_ID, "==", holderId)
124
+ .where(STOCK_RESERVATION_FIELDS.STATUS, "==", "held")
125
+ .get();
126
+ let released = 0;
127
+ for (const doc of snap.docs) {
128
+ try {
129
+ await endReservation(doc.id, "released", true, reason);
130
+ released += 1;
131
+ }
132
+ catch (error) {
133
+ const normalized = normalizeError(error);
134
+ /*
135
+ * One bad row must not strand the rest. A holder with eight holds and
136
+ * one deleted product should still get the other seven back.
137
+ */
138
+ serverLogger.warn("releaseReservationsForHolder: skipped one", {
139
+ reservationId: doc.id,
140
+ message: normalized.message,
141
+ });
142
+ }
143
+ }
144
+ return released;
145
+ }
146
+ /**
147
+ * The sweep. Expire every hold past its `expiresAt` and restore its units.
148
+ *
149
+ * ๐Ÿ›‘ Bounded, and the bound is REPORTED. A silent cap reads as "nothing left
150
+ * to expire" on the one run where it mattered โ€” the same lie a truncated
151
+ * recount tells (Root Cause #102).
152
+ */
153
+ export async function expireDueReservations(limit = 200) {
154
+ const db = getAdminDb();
155
+ const snap = await db
156
+ .collection(STOCK_RESERVATION_COLLECTION)
157
+ .where(STOCK_RESERVATION_FIELDS.STATUS, "==", "held")
158
+ .where(STOCK_RESERVATION_FIELDS.EXPIRES_AT, "<=", new Date())
159
+ .orderBy(STOCK_RESERVATION_FIELDS.EXPIRES_AT, "asc")
160
+ .limit(limit)
161
+ .get();
162
+ let expired = 0;
163
+ for (const doc of snap.docs) {
164
+ try {
165
+ await endReservation(doc.id, "expired", true, "TTL elapsed");
166
+ expired += 1;
167
+ }
168
+ catch (error) {
169
+ const normalized = normalizeError(error);
170
+ serverLogger.warn("expireDueReservations: skipped one", {
171
+ reservationId: doc.id,
172
+ message: normalized.message,
173
+ });
174
+ }
175
+ }
176
+ return { scanned: snap.size, expired, truncated: snap.size === limit };
177
+ }
178
+ /** Units currently held on a product, for an admin explaining an unbuyable listing. */
179
+ export async function activeReservationsForProduct(productId) {
180
+ const db = getAdminDb();
181
+ const snap = await db
182
+ .collection(STOCK_RESERVATION_COLLECTION)
183
+ .where(STOCK_RESERVATION_FIELDS.PRODUCT_ID, "==", productId)
184
+ .where(STOCK_RESERVATION_FIELDS.STATUS, "==", "held")
185
+ .get();
186
+ return snap.docs.map((d) => ({ id: d.id, ...d.data() }));
187
+ }
188
+ function clampTtl(hours) {
189
+ if (hours === undefined)
190
+ return RESERVATION_DEFAULT_TTL_HOURS;
191
+ if (!Number.isFinite(hours) || hours <= 0) {
192
+ throw new ValidationError("A hold must last longer than zero hours.");
193
+ }
194
+ return Math.min(hours, RESERVATION_MAX_TTL_HOURS);
195
+ }
196
+ /**
197
+ * ๐Ÿ›‘ `?? stockQuantity` and NOT `?? 0`.
198
+ *
199
+ * `availableQuantity` is optional on rows written before it existed, and
200
+ * treating an absent value as zero would refuse every reservation on a
201
+ * fully-stocked legacy listing โ€” a sold-out answer for a product that has
202
+ * stock, which reads as a stock bug rather than a schema gap.
203
+ */
204
+ function readAvailable(data) {
205
+ const product = (data ?? {});
206
+ return product.availableQuantity ?? product.stockQuantity ?? 0;
207
+ }
208
+ /**
209
+ * The one write path. Every transition goes through here so the
210
+ * row-and-counter pairing cannot be forgotten by a new caller โ€” the same
211
+ * argument `deriveLinePrice` makes inside the cart repository.
212
+ */
213
+ async function endReservation(reservationId, status, restore, reason) {
214
+ const db = getAdminDb();
215
+ const reservationRef = db.collection(STOCK_RESERVATION_COLLECTION).doc(reservationId);
216
+ await db.runTransaction(async (tx) => {
217
+ const snap = await tx.get(reservationRef);
218
+ if (!snap.exists)
219
+ throw new NotFoundError("Reservation not found.");
220
+ const reservation = snap.data();
221
+ /*
222
+ * ๐Ÿ›‘ Idempotent by STATUS, not by a flag. The sweep and a manual release
223
+ * can reach the same row in the same second; a second restore would
224
+ * invent units that do not exist, which is worse than a hold that lingers
225
+ * one sweep longer.
226
+ */
227
+ if (reservation.status !== "held")
228
+ return;
229
+ /*
230
+ * Read the product BEFORE the first write, even when `restore` is false
231
+ * and the value is discarded โ€” see this file's header. One wasted read on
232
+ * the consume path buys a function whose transaction shape cannot be
233
+ * broken by a later edit reordering the branches.
234
+ */
235
+ const productRef = db.collection(PRODUCT_COLLECTION).doc(reservation.productId);
236
+ const productSnap = restore ? await tx.get(productRef) : null;
237
+ const now = new Date();
238
+ tx.update(reservationRef, {
239
+ status,
240
+ endedAt: now,
241
+ updatedAt: now,
242
+ ...(reason ? { endedReason: reason } : {}),
243
+ });
244
+ if (!restore)
245
+ return;
246
+ if (!productSnap?.exists) {
247
+ /*
248
+ * The product is gone, so there is no counter to restore โ€” but the row
249
+ * still has to close, or the sweep retries it forever. Recorded rather
250
+ * than thrown: an orphaned hold is a data problem, not a reason to fail
251
+ * the release of the others beside it.
252
+ */
253
+ serverLogger.warn("endReservation: product missing, counter not restored", {
254
+ reservationId,
255
+ productId: reservation.productId,
256
+ });
257
+ return;
258
+ }
259
+ const product = productSnap.data();
260
+ const available = readAvailable(product);
261
+ const stock = product.stockQuantity ?? available;
262
+ /*
263
+ * ๐Ÿ›‘ Clamped at `stockQuantity`. A seller who lowered stock while units
264
+ * were held would otherwise get an `availableQuantity` ABOVE their own
265
+ * stock on release โ€” a listing offering more than exists, which is the
266
+ * one direction a stock bug must never go.
267
+ */
268
+ tx.update(productRef, {
269
+ availableQuantity: Math.min(stock, available + reservation.quantity),
270
+ updatedAt: now,
271
+ });
272
+ });
273
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Server-side verification that a coordinated purchase's destinations are real
3
+ * and belong to the people naming them.
4
+ *
5
+ * ## ๐Ÿ›‘ Why this file exists at all
6
+ *
7
+ * `CoordinationMember.destinationAddressId` is a REFERENCE, which is the right
8
+ * shape โ€” copying the address inline would put a name, a phone number and a
9
+ * street into an offer document with no encryption story and no retention
10
+ * story. But a reference has a failure mode a copy does not:
11
+ *
12
+ * **An id is guessable in a way an address is not.** Without this check a
13
+ * member supplies any `addresses` document id and the parcel ships to a
14
+ * stranger's house, at the coordinator's expense, with every other part of
15
+ * the flow behaving correctly. Nothing errors; the order is valid; the goods
16
+ * simply go somewhere nobody chose.
17
+ *
18
+ * So the rule is: **an id that crosses the wire is never trusted, and is
19
+ * resolved against its claimed owner before anything is written.**
20
+ */
21
+ import type { CoordinationMember } from "../../../shared/features/offers/coordination";
22
+ /**
23
+ * Every named destination must exist and belong to the member who named it.
24
+ *
25
+ * ๐Ÿ›‘ **Owned by THAT MEMBER, not merely by someone.** Checking only that the
26
+ * address exists and has `ownerType: "user"` would let a member ship to any
27
+ * other user's saved address โ€” the weaker check reads as a real one and
28
+ * closes nothing.
29
+ *
30
+ * ๐Ÿ›‘ **Fails the whole request, naming the member.** Dropping a bad
31
+ * destination and shipping that member's parcel to the coordinator instead
32
+ * would be a silent substitution of somebody's delivery address, which is
33
+ * exactly the class of quiet wrongness this check exists to prevent. Grouped
34
+ * cart lines take the same position for the same reason.
35
+ *
36
+ * One `getAll` over the distinct ids, not one read per member: five members
37
+ * sharing two destinations is two reads, and Rule #6's budget is per request
38
+ * rather than per member.
39
+ */
40
+ export declare function assertOwnedAddresses(members: readonly CoordinationMember[]): Promise<void>;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Server-side verification that a coordinated purchase's destinations are real
3
+ * and belong to the people naming them.
4
+ *
5
+ * ## ๐Ÿ›‘ Why this file exists at all
6
+ *
7
+ * `CoordinationMember.destinationAddressId` is a REFERENCE, which is the right
8
+ * shape โ€” copying the address inline would put a name, a phone number and a
9
+ * street into an offer document with no encryption story and no retention
10
+ * story. But a reference has a failure mode a copy does not:
11
+ *
12
+ * **An id is guessable in a way an address is not.** Without this check a
13
+ * member supplies any `addresses` document id and the parcel ships to a
14
+ * stranger's house, at the coordinator's expense, with every other part of
15
+ * the flow behaving correctly. Nothing errors; the order is valid; the goods
16
+ * simply go somewhere nobody chose.
17
+ *
18
+ * So the rule is: **an id that crosses the wire is never trusted, and is
19
+ * resolved against its claimed owner before anything is written.**
20
+ */
21
+ import { addressesRepository } from "../../../../features/addresses/repository/addresses.repository.js";
22
+ import { ValidationError } from "../../../../errors/validation-error.js";
23
+ /**
24
+ * Every named destination must exist and belong to the member who named it.
25
+ *
26
+ * ๐Ÿ›‘ **Owned by THAT MEMBER, not merely by someone.** Checking only that the
27
+ * address exists and has `ownerType: "user"` would let a member ship to any
28
+ * other user's saved address โ€” the weaker check reads as a real one and
29
+ * closes nothing.
30
+ *
31
+ * ๐Ÿ›‘ **Fails the whole request, naming the member.** Dropping a bad
32
+ * destination and shipping that member's parcel to the coordinator instead
33
+ * would be a silent substitution of somebody's delivery address, which is
34
+ * exactly the class of quiet wrongness this check exists to prevent. Grouped
35
+ * cart lines take the same position for the same reason.
36
+ *
37
+ * One `getAll` over the distinct ids, not one read per member: five members
38
+ * sharing two destinations is two reads, and Rule #6's budget is per request
39
+ * rather than per member.
40
+ */
41
+ export async function assertOwnedAddresses(members) {
42
+ const named = members.filter((m) => !!m.destinationAddressId);
43
+ if (named.length === 0)
44
+ return;
45
+ const ids = [...new Set(named.map((m) => m.destinationAddressId))];
46
+ const docs = await Promise.all(ids.map((id) => addressesRepository.findById(id)));
47
+ const byId = new Map(ids.map((id, i) => [id, docs[i]]));
48
+ for (const member of named) {
49
+ const address = byId.get(member.destinationAddressId);
50
+ /*
51
+ * ๐Ÿ›‘ ONE message for "does not exist" and "is not yours".
52
+ *
53
+ * Distinguishing them turns this endpoint into an oracle for which
54
+ * `addresses` ids are real โ€” a caller probes ids and reads existence off
55
+ * the error text. The member who typed a genuinely stale id and the
56
+ * member probing somebody else's get the same answer, and only the first
57
+ * of those has a legitimate next step anyway (pick again).
58
+ */
59
+ if (!address || address.ownerType !== "user" || address.ownerId !== member.uid) {
60
+ throw new ValidationError("A chosen delivery address is not available.", {
61
+ destinationAddressId: `${member.uid}: that address cannot be used.`,
62
+ });
63
+ }
64
+ }
65
+ }
@@ -346,6 +346,27 @@ export function orderDocumentToOrder(doc) {
346
346
  timelineTruncated: doc.statusHistoryTruncated,
347
347
  /** How the buyer acquired the right to buy. Written once, at creation. */
348
348
  sourceContext: doc.sourceContext,
349
+ /*
350
+ * D55 โ€” the pre-order balance, present only once a seller has opened it.
351
+ *
352
+ * ๐Ÿ›‘ `reviewedBy` is deliberately NOT mapped. It is the reviewing staff
353
+ * member's uid and no buyer surface reads it, so it stays server-side by
354
+ * default (ยง Public Data Projections โ€” a field is private until someone
355
+ * names the client that reads it).
356
+ */
357
+ balance: doc.balance
358
+ ? {
359
+ amount: doc.balance.amount,
360
+ method: doc.balance.method,
361
+ status: doc.balance.status,
362
+ dueBy: toIsoOrUndefined(doc.balance.dueBy),
363
+ proofUrl: doc.balance.proofUrl,
364
+ transactionId: doc.balance.transactionId,
365
+ submittedAt: toIsoOrUndefined(doc.balance.submittedAt),
366
+ reviewedAt: toIsoOrUndefined(doc.balance.reviewedAt),
367
+ reviewNote: doc.balance.reviewNote,
368
+ }
369
+ : undefined,
349
370
  createdAt: doc.createdAt instanceof Date
350
371
  ? doc.createdAt.toISOString()
351
372
  : String(doc.createdAt),