@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,144 @@
1
+ /**
2
+ * The ONE write path for a platform rate.
3
+ *
4
+ * ## Why there is exactly one
5
+ *
6
+ * Rates live on `siteSettings`, which `PUT /api/admin/site` can already write
7
+ * — so this is not the only way the numbers CAN move. It is the only way they
8
+ * move with a recorded reason, an effective date and an `adminAuditLog` row,
9
+ * and that difference is the whole point of `/admin/pricing`.
10
+ *
11
+ * The general settings form stays able to write them. Locking it out would
12
+ * mean an admin fixing a typo in the COD ceiling gets a 400 from the page that
13
+ * has always owned it, and the fix would be to come here — which is a worse
14
+ * outcome than an occasional unlogged edit. What this buys is that the
15
+ * DELIBERATE act of repricing has one door with a record behind it.
16
+ *
17
+ * ## 🛑 Forward-only, enforced at the WRITE
18
+ *
19
+ * `rejectRateChange` is a pure function and lives in the registry so the form
20
+ * can run it too — but the form running it is a courtesy, and this is the
21
+ * enforcement. A backdated change here would restate figures already invoiced
22
+ * and already paid out; see the registry header.
23
+ */
24
+ import { getAdminDb } from "../../../../providers/db-firebase/index.js";
25
+ import { siteSettingsRepository } from "../../../../features/admin/repository/site-settings.repository.js";
26
+ import { SITE_SETTINGS_COLLECTION, } from "../../../../features/admin/schemas/firestore.js";
27
+ import { canEditRate, rateByPath, rejectRateChange, } from "../../../shared/features/pricing/rate-registry.js";
28
+ import { recordAdminAction } from "../audit-log/actions.js";
29
+ import { AdminAuditActionValues } from "../../../../features/audit-log/schemas/firestore.js";
30
+ import { normalizeError } from "../../../../errors/normalize.js";
31
+ import { serverLogger } from "../../../../monitoring/server-logger.js";
32
+ import { ValidationError } from "../../../../errors/validation-error.js";
33
+ const SINGLETON_ID = "global";
34
+ /**
35
+ * How many changes the log keeps, newest first.
36
+ *
37
+ * Sized so a year of ordinary repricing fits: 16 rates changed a few times
38
+ * each is well under this, and the page shows only the most recent per rate
39
+ * anyway. The cap exists because the array rides on a document every request
40
+ * reads, not because 200 entries is a lot of data.
41
+ */
42
+ export const RATE_CHANGE_LOG_MAX = 200;
43
+ /** Read a dotted path off the settings document. `null` when absent. */
44
+ function readRate(settings, path) {
45
+ const value = path
46
+ .split(".")
47
+ .reduce((acc, key) => (acc && typeof acc === "object" ? acc[key] : undefined), settings);
48
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
49
+ }
50
+ /**
51
+ * Change one rate.
52
+ *
53
+ * 🛑 **One rate per call, on purpose.** A bulk form would need one reason
54
+ * covering a heterogeneous set, which is the same argument that removed the
55
+ * bulk offer-cancel: one shared reason across unlike records is worse audit
56
+ * data than no bulk action. Repricing three things is three decisions.
57
+ */
58
+ export async function applyRateChange(input, actor, now = new Date()) {
59
+ const descriptor = rateByPath(input.path);
60
+ if (!descriptor) {
61
+ throw new ValidationError("That is not a rate this platform charges.", {
62
+ path: `Unknown rate: ${input.path}`,
63
+ });
64
+ }
65
+ if (!canEditRate(descriptor.tier, actor.isAdmin)) {
66
+ /*
67
+ * A statutory rate reaches this branch for EVERYONE, admins included —
68
+ * which is why the message names the real destination rather than
69
+ * implying the caller lacks a permission they could be granted.
70
+ */
71
+ throw new ValidationError(descriptor.tier === "statutory"
72
+ ? "Statutory rates are not set here — they are sourced per HSN code in Tax codes."
73
+ : "Only an administrator can change a policy ceiling.", { path: descriptor.label });
74
+ }
75
+ const settings = await siteSettingsRepository.getSingleton();
76
+ const previousValue = readRate(settings, descriptor.path);
77
+ const effectiveFrom = input.effectiveFrom ?? now;
78
+ const problem = rejectRateChange({ effectiveFrom, previousValue, newValue: input.newValue, reason: input.reason }, descriptor.tier, now);
79
+ if (problem) {
80
+ throw new ValidationError(problem, { newValue: problem });
81
+ }
82
+ const entry = {
83
+ path: descriptor.path,
84
+ previousValue,
85
+ newValue: input.newValue,
86
+ effectiveFrom,
87
+ changedByUid: actor.uid,
88
+ changedByName: actor.name ?? "",
89
+ ...(input.reason?.trim() ? { reason: input.reason.trim() } : {}),
90
+ changedAt: now,
91
+ };
92
+ /*
93
+ * 🛑 A DOTTED path for the rate, and a whole-array write for the log.
94
+ *
95
+ * `update()` replaces a nested map wholesale (D13, recorded at length in
96
+ * `updateSingleton`), so writing `{ commissions: { platformFeePercent: 2 } }`
97
+ * would delete every other commission. The dotted form touches one leaf.
98
+ *
99
+ * The log cannot use `arrayUnion` because that cannot enforce the cap — the
100
+ * same reason `withHistory`'s batch paths fold in memory. Newest first, so
101
+ * the slice drops the OLDEST.
102
+ */
103
+ const nextLog = [entry, ...(settings.rateChanges ?? [])].slice(0, RATE_CHANGE_LOG_MAX);
104
+ await getAdminDb()
105
+ .collection(SITE_SETTINGS_COLLECTION)
106
+ .doc(SINGLETON_ID)
107
+ .update({
108
+ [descriptor.path]: input.newValue,
109
+ rateChanges: nextLog,
110
+ updatedAt: now,
111
+ });
112
+ /*
113
+ * Best-effort, never fails the change — the same contract every other
114
+ * `recordAdminAction` caller has. The rate has already moved; refusing to
115
+ * acknowledge it because the log write failed would leave the admin
116
+ * believing it had not.
117
+ */
118
+ try {
119
+ await recordAdminAction({
120
+ actorUid: actor.uid,
121
+ actorName: actor.name,
122
+ action: AdminAuditActionValues.PRICING_UPDATE,
123
+ targetType: "siteSettings",
124
+ targetId: descriptor.path,
125
+ targetLabel: descriptor.label,
126
+ ...(input.reason?.trim() ? { reason: input.reason.trim() } : {}),
127
+ metadata: {
128
+ tier: descriptor.tier,
129
+ unit: descriptor.unit,
130
+ previousValue,
131
+ newValue: input.newValue,
132
+ effectiveFrom: effectiveFrom.toISOString(),
133
+ },
134
+ });
135
+ }
136
+ catch (err) {
137
+ const normalized = normalizeError(err);
138
+ serverLogger.warn("applyRateChange: audit log write failed", {
139
+ path: descriptor.path,
140
+ message: normalized.message,
141
+ });
142
+ }
143
+ return { path: descriptor.path, previousValue, newValue: input.newValue, effectiveFrom };
144
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * S5a — one parcel per destination on a coordinated purchase.
3
+ *
4
+ * A coordinated purchase can send each member's share to their own address
5
+ * (`CoordinationMember.destinationAddressId`). That is N parcels from one
6
+ * order, which is N bookings and N serviceability checks.
7
+ *
8
+ * ## 🛑 Serviceability is checked for EVERY destination BEFORE any booking
9
+ *
10
+ * Booking as you go and failing on the fourth leaves three parcels already
11
+ * accepted by the carrier and a coordinator who cannot undo them — the
12
+ * cancel path exists but the money and the pickup slot are gone. A
13
+ * pre-flight over all destinations turns that into one refusal naming the
14
+ * pincode that cannot be served, before anything is committed.
15
+ *
16
+ * Two-phase on purpose, and it is NOT a transaction: a carrier booking has
17
+ * no rollback. What the pre-flight buys is that the common failure — an
18
+ * unservable pincode — is caught while rolling back is still free.
19
+ *
20
+ * ## 🛑 Bounded by `SHARED_PURCHASE_MAX_MEMBERS`
21
+ *
22
+ * Five destinations is five outbound calls for serviceability and five more
23
+ * to book, on a path somebody is waiting on. That ceiling is why the limit
24
+ * exists at all; see the registry's own header.
25
+ */
26
+ import { type CoordinationMember } from "../../../shared/features/offers/coordination";
27
+ export interface DestinationCheck {
28
+ addressId: string;
29
+ pincode: string;
30
+ serviceable: boolean;
31
+ }
32
+ /**
33
+ * Can every named destination actually be delivered to?
34
+ *
35
+ * 🛑 Returns the FULL list, not the first failure. A coordinator fixing one
36
+ * unservable address only to be told about the next one is three round trips
37
+ * for a problem that was knowable in one — and each round trip is another
38
+ * chance for someone to give up with the arrangement half-built.
39
+ *
40
+ * 🛑 An UNREADABLE check is treated as serviceable. A provider timeout is not
41
+ * evidence that a pincode cannot be served, and refusing on it would block a
42
+ * legitimate purchase on an outage we caused. The booking itself will fail
43
+ * loudly if the address really is unservable, which is the recoverable
44
+ * direction — and it is the same reasoning `bookShipment` applies to a wallet
45
+ * balance it could not read.
46
+ *
47
+ * A MISSING pincode is the opposite case and is reported unserviceable: that
48
+ * is a fact about the stored address, not about the provider, and it will not
49
+ * fix itself on a retry.
50
+ */
51
+ export declare function checkDestinations(members: readonly CoordinationMember[],
52
+ /**
53
+ * 🛑 The parcel's weight in kg, NOT an origin pincode.
54
+ *
55
+ * `IShippingProvider.checkServiceability(pincode, weight)` reads its first
56
+ * argument as the DESTINATION. A reasonable guess of `(origin, destination)`
57
+ * would pass two strings into a `(string, number)` signature — caught by
58
+ * `tsc` here, but worth naming because the argument ORDER is the part a
59
+ * reader assumes.
60
+ */
61
+ weightKg: number): Promise<DestinationCheck[]>;
62
+ /**
63
+ * Refuse the whole arrangement if any destination cannot be served.
64
+ *
65
+ * Names every failing pincode, for the reason above.
66
+ */
67
+ export declare function assertAllServiceable(checks: readonly DestinationCheck[]): void;
68
+ /**
69
+ * The parcels one coordinated order produces.
70
+ *
71
+ * 🛑 **Keyed by ADDRESS, not by member.** Two members naming the same address
72
+ * are one parcel, not two — that is the whole reason `destinationIds` returns
73
+ * a distinct set. Fanning out per member would book the courier twice to the
74
+ * same door and bill for both.
75
+ *
76
+ * 🛑 **A member with no `destinationAddressId` is NOT in this list.** Absent
77
+ * means "the coordinator's address", which is the ordinary one-parcel case
78
+ * and is handled by `bookShipment`'s existing single-delivery path — the
79
+ * order's own snapshot. Inventing a parcel for them here would duplicate it.
80
+ */
81
+ export interface ParcelPlan {
82
+ addressId: string;
83
+ /** Members whose share rides in this parcel. Never empty. */
84
+ memberUids: string[];
85
+ }
86
+ export declare function planParcels(members: readonly CoordinationMember[]): ParcelPlan[];
@@ -0,0 +1,130 @@
1
+ /**
2
+ * S5a — one parcel per destination on a coordinated purchase.
3
+ *
4
+ * A coordinated purchase can send each member's share to their own address
5
+ * (`CoordinationMember.destinationAddressId`). That is N parcels from one
6
+ * order, which is N bookings and N serviceability checks.
7
+ *
8
+ * ## 🛑 Serviceability is checked for EVERY destination BEFORE any booking
9
+ *
10
+ * Booking as you go and failing on the fourth leaves three parcels already
11
+ * accepted by the carrier and a coordinator who cannot undo them — the
12
+ * cancel path exists but the money and the pickup slot are gone. A
13
+ * pre-flight over all destinations turns that into one refusal naming the
14
+ * pincode that cannot be served, before anything is committed.
15
+ *
16
+ * Two-phase on purpose, and it is NOT a transaction: a carrier booking has
17
+ * no rollback. What the pre-flight buys is that the common failure — an
18
+ * unservable pincode — is caught while rolling back is still free.
19
+ *
20
+ * ## 🛑 Bounded by `SHARED_PURCHASE_MAX_MEMBERS`
21
+ *
22
+ * Five destinations is five outbound calls for serviceability and five more
23
+ * to book, on a path somebody is waiting on. That ceiling is why the limit
24
+ * exists at all; see the registry's own header.
25
+ */
26
+ import { getProviders } from "../../../../contracts/registry.js";
27
+ import { addressesRepository } from "../../../../features/addresses/repository/addresses.repository.js";
28
+ import { SHARED_PURCHASE_MAX_MEMBERS, destinationIds, } from "../../../shared/features/offers/coordination.js";
29
+ import { ValidationError } from "../../../../errors/validation-error.js";
30
+ import { normalizeError } from "../../../../errors/normalize.js";
31
+ import { serverLogger } from "../../../../monitoring/server-logger.js";
32
+ /**
33
+ * Can every named destination actually be delivered to?
34
+ *
35
+ * 🛑 Returns the FULL list, not the first failure. A coordinator fixing one
36
+ * unservable address only to be told about the next one is three round trips
37
+ * for a problem that was knowable in one — and each round trip is another
38
+ * chance for someone to give up with the arrangement half-built.
39
+ *
40
+ * 🛑 An UNREADABLE check is treated as serviceable. A provider timeout is not
41
+ * evidence that a pincode cannot be served, and refusing on it would block a
42
+ * legitimate purchase on an outage we caused. The booking itself will fail
43
+ * loudly if the address really is unservable, which is the recoverable
44
+ * direction — and it is the same reasoning `bookShipment` applies to a wallet
45
+ * balance it could not read.
46
+ *
47
+ * A MISSING pincode is the opposite case and is reported unserviceable: that
48
+ * is a fact about the stored address, not about the provider, and it will not
49
+ * fix itself on a retry.
50
+ */
51
+ export async function checkDestinations(members,
52
+ /**
53
+ * 🛑 The parcel's weight in kg, NOT an origin pincode.
54
+ *
55
+ * `IShippingProvider.checkServiceability(pincode, weight)` reads its first
56
+ * argument as the DESTINATION. A reasonable guess of `(origin, destination)`
57
+ * would pass two strings into a `(string, number)` signature — caught by
58
+ * `tsc` here, but worth naming because the argument ORDER is the part a
59
+ * reader assumes.
60
+ */
61
+ weightKg) {
62
+ const ids = destinationIds(members);
63
+ if (ids.length === 0)
64
+ return [];
65
+ if (ids.length > SHARED_PURCHASE_MAX_MEMBERS) {
66
+ throw new ValidationError(`A coordinated purchase can ship to at most ${SHARED_PURCHASE_MAX_MEMBERS} addresses.`, { destinations: `${ids.length} distinct addresses named.` });
67
+ }
68
+ /*
69
+ * 🛑 Optional on the registry. This is the serviceability PRE-FLIGHT, so
70
+ * with no provider the honest answer is "unknown", not "unserviceable":
71
+ * reporting every destination as unserviceable would block an accept that
72
+ * manual dispatch handles perfectly well.
73
+ */
74
+ const provider = getProviders().shipping;
75
+ if (!provider) {
76
+ return ids.map((addressId) => ({ addressId, pincode: "", serviceable: true }));
77
+ }
78
+ const docs = await Promise.all(ids.map((id) => addressesRepository.findById(id)));
79
+ return Promise.all(ids.map(async (addressId, i) => {
80
+ /*
81
+ * 🛑 `.postalCode` needs no reveal — it is not PII and
82
+ * `AddressRepository` deliberately leaves it unencrypted so it stays
83
+ * queryable (see its own `computeAddressHash` comment). Calling
84
+ * `revealAddress` here would decrypt a name and a phone number we have
85
+ * no use for, on a path that only needs six digits.
86
+ */
87
+ const pincode = docs[i]?.postalCode ?? "";
88
+ if (!pincode)
89
+ return { addressId, pincode, serviceable: false };
90
+ try {
91
+ const res = await provider.checkServiceability(pincode, weightKg);
92
+ return { addressId, pincode, serviceable: res.isServiceable };
93
+ }
94
+ catch (err) {
95
+ void normalizeError(err);
96
+ serverLogger.warn("checkDestinations: serviceability unreadable", { addressId, pincode });
97
+ return { addressId, pincode, serviceable: true };
98
+ }
99
+ }));
100
+ }
101
+ /**
102
+ * Refuse the whole arrangement if any destination cannot be served.
103
+ *
104
+ * Names every failing pincode, for the reason above.
105
+ */
106
+ export function assertAllServiceable(checks) {
107
+ const bad = checks.filter((c) => !c.serviceable);
108
+ if (bad.length === 0)
109
+ return;
110
+ throw new ValidationError(bad.length === 1
111
+ ? "One delivery address cannot be served by the courier."
112
+ : `${bad.length} delivery addresses cannot be served by the courier.`, {
113
+ destinations: bad
114
+ .map((b) => (b.pincode ? `${b.pincode}: not serviceable` : "missing pincode"))
115
+ .join("; "),
116
+ });
117
+ }
118
+ export function planParcels(members) {
119
+ const byAddress = new Map();
120
+ for (const m of members) {
121
+ if (!m.destinationAddressId)
122
+ continue;
123
+ const uids = byAddress.get(m.destinationAddressId);
124
+ if (uids)
125
+ uids.push(m.uid);
126
+ else
127
+ byAddress.set(m.destinationAddressId, [m.uid]);
128
+ }
129
+ return [...byAddress].map(([addressId, memberUids]) => ({ addressId, memberUids }));
130
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Booking a parcel through the platform's own carrier account — Part 1′.
3
+ *
4
+ * ## 🛑 Why this could not be built until Part 14
5
+ *
6
+ * `CreateShipmentInput` needs a structured `{name, phone, addressLine1, city,
7
+ * state, pincode, country}` for BOTH ends. Until the delivery snapshot landed
8
+ * the order carried `shippingAddress?: string` — flat free text with no
9
+ * pincode and no phone — and the only way to produce a party from it was to
10
+ * parse prose. The `placeOfSupply` comment already refuses that for tax, and
11
+ * the same argument holds here: a mis-parsed pincode is a parcel that goes to
12
+ * the wrong city, except nothing errors and the label prints fine.
13
+ *
14
+ * `deliveryPartyFor(order)` now returns exactly that shape, or `null` when a
15
+ * required field is missing — which is the only honest answer for an order
16
+ * placed before the snapshot existed.
17
+ *
18
+ * ## 🛑 `platformBookedShipment` is the flag this writes
19
+ *
20
+ * `order-math.ts` gates shipping reimbursement on it
21
+ * (`if (order.platformBookedShipment === true) return 0`) and
22
+ * `quote.ts:241` records that it has had **ZERO writers** — so the
23
+ * reimbursement rule has been reading a flag nothing sets. This is its
24
+ * writer, which is why the booking and the dispatch form are one unit of
25
+ * work and why the form must not ship first: a "Book through us" button with
26
+ * no booking behind it is a control the server refuses.
27
+ */
28
+ export interface BookShipmentInput {
29
+ orderId: string;
30
+ /** Centimetres and kilograms, from the dispatch form's box preset. */
31
+ dimensions: {
32
+ weight: number;
33
+ length: number;
34
+ width: number;
35
+ height: number;
36
+ };
37
+ /** The store address the courier collects from. */
38
+ pickupAddressId: string;
39
+ }
40
+ export interface BookShipmentResult {
41
+ trackingId: string;
42
+ courier?: string;
43
+ estimatedDelivery?: string;
44
+ }
45
+ /**
46
+ * Book one parcel.
47
+ *
48
+ * ## 🛑 The wallet is checked BEFORE the booking, and a failure is a 409
49
+ *
50
+ * A carrier account with no balance rejects the booking — but some providers
51
+ * accept it and then silently fail to hand over, which produces a tracking id
52
+ * that never moves. Checking first turns "the parcel vanished" into "top up
53
+ * the account", and the 409 is deliberate: the request is well-formed and the
54
+ * seller is not confused, the world is simply not ready.
55
+ *
56
+ * 🛑 `getWalletBalance` is NOT abstract on `IShippingProvider` — manual
57
+ * shipping has no wallet, so a provider that does not implement it returns
58
+ * undefined and the check is SKIPPED rather than failing. A missing balance
59
+ * must never block a manual dispatch.
60
+ */
61
+ export declare function bookShipment(input: BookShipmentInput): Promise<BookShipmentResult>;
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Booking a parcel through the platform's own carrier account — Part 1′.
3
+ *
4
+ * ## 🛑 Why this could not be built until Part 14
5
+ *
6
+ * `CreateShipmentInput` needs a structured `{name, phone, addressLine1, city,
7
+ * state, pincode, country}` for BOTH ends. Until the delivery snapshot landed
8
+ * the order carried `shippingAddress?: string` — flat free text with no
9
+ * pincode and no phone — and the only way to produce a party from it was to
10
+ * parse prose. The `placeOfSupply` comment already refuses that for tax, and
11
+ * the same argument holds here: a mis-parsed pincode is a parcel that goes to
12
+ * the wrong city, except nothing errors and the label prints fine.
13
+ *
14
+ * `deliveryPartyFor(order)` now returns exactly that shape, or `null` when a
15
+ * required field is missing — which is the only honest answer for an order
16
+ * placed before the snapshot existed.
17
+ *
18
+ * ## 🛑 `platformBookedShipment` is the flag this writes
19
+ *
20
+ * `order-math.ts` gates shipping reimbursement on it
21
+ * (`if (order.platformBookedShipment === true) return 0`) and
22
+ * `quote.ts:241` records that it has had **ZERO writers** — so the
23
+ * reimbursement rule has been reading a flag nothing sets. This is its
24
+ * writer, which is why the booking and the dispatch form are one unit of
25
+ * work and why the form must not ship first: a "Book through us" button with
26
+ * no booking behind it is a control the server refuses.
27
+ */
28
+ import { getProviders } from "../../../../contracts/registry.js";
29
+ import { orderRepository } from "../../../../features/orders/repository/orders.repository.js";
30
+ import { storeRepository } from "../../../../features/stores/repository/store.repository.js";
31
+ import { addressesRepository } from "../../../../features/addresses/repository/addresses.repository.js";
32
+ import { deliveryPartyFor } from "../../../shared/features/orders/delivery-snapshot.js";
33
+ import { revealPii } from "../../../../security/pii-accessors.js";
34
+ import { ORDER_PII_FIELDS } from "../../../../security/pii-schemas.js";
35
+ import { ValidationError } from "../../../../errors/validation-error.js";
36
+ import { NotFoundError } from "../../../../errors/not-found-error.js";
37
+ import { normalizeError } from "../../../../errors/normalize.js";
38
+ import { serverLogger } from "../../../../monitoring/server-logger.js";
39
+ /**
40
+ * Book one parcel.
41
+ *
42
+ * ## 🛑 The wallet is checked BEFORE the booking, and a failure is a 409
43
+ *
44
+ * A carrier account with no balance rejects the booking — but some providers
45
+ * accept it and then silently fail to hand over, which produces a tracking id
46
+ * that never moves. Checking first turns "the parcel vanished" into "top up
47
+ * the account", and the 409 is deliberate: the request is well-formed and the
48
+ * seller is not confused, the world is simply not ready.
49
+ *
50
+ * 🛑 `getWalletBalance` is NOT abstract on `IShippingProvider` — manual
51
+ * shipping has no wallet, so a provider that does not implement it returns
52
+ * undefined and the check is SKIPPED rather than failing. A missing balance
53
+ * must never block a manual dispatch.
54
+ */
55
+ export async function bookShipment(input) {
56
+ const order = (await orderRepository.findById(input.orderId));
57
+ if (!order)
58
+ throw new NotFoundError("Order not found.");
59
+ if (order.trackingNumber) {
60
+ /*
61
+ * 409, not 400. Double-booking is the expensive mistake here — two
62
+ * parcels, two charges, one of them never collected — and it is easy to
63
+ * cause by refreshing a form.
64
+ */
65
+ throw new ValidationError("This order already has a shipment.", {
66
+ orderId: `Already booked as ${order.trackingNumber}.`,
67
+ });
68
+ }
69
+ /*
70
+ * 🛑 DECRYPTED first. `OrderDocument`'s delivery name, phone and address
71
+ * lines are `Encrypted<string>` (ciphertext-by-default), and `Encrypted<T>`
72
+ * is deliberately assignable to nothing that wants a plain `string` — which
73
+ * is what made this a compile error rather than a courier label printed
74
+ * with `enc:v1:…` on it.
75
+ *
76
+ * A courier booking is the one place the plaintext is genuinely required:
77
+ * the carrier has to deliver to a real person at a real door. Masked would
78
+ * be the wrong default HERE and the right one almost everywhere else, which
79
+ * is why the two accessors are separately named.
80
+ */
81
+ const delivery = deliveryPartyFor(revealPii(order, ORDER_PII_FIELDS));
82
+ if (!delivery) {
83
+ /*
84
+ * An order placed before the delivery snapshot existed. Refused, loudly,
85
+ * rather than guessed at — see the module header on parsing prose.
86
+ */
87
+ throw new ValidationError("This order has no structured delivery address, so it cannot be booked automatically.", { orderId: "Placed before structured addresses; dispatch it manually." });
88
+ }
89
+ const pickupDoc = await addressesRepository.findById(input.pickupAddressId);
90
+ const store = order.storeId ? await storeRepository.findBySlug(order.storeId) : null;
91
+ /*
92
+ * 🛑 The pickup address must belong to the ORDER'S STORE. Checking only
93
+ * that it exists would let one seller's parcel be collected from another
94
+ * seller's premises — the same class of hole `assertOwnedAddresses` closes
95
+ * for coordinated purchases, and it is reachable here by id alone.
96
+ */
97
+ if (!pickupDoc ||
98
+ pickupDoc.ownerType !== "store" ||
99
+ !store ||
100
+ pickupDoc.ownerId !== store.id) {
101
+ throw new ValidationError("That pickup address is not available for this store.", {
102
+ pickupAddressId: "Pick one of this store's own addresses.",
103
+ });
104
+ }
105
+ const pickupPlain = addressesRepository.revealAddress(pickupDoc);
106
+ const pickup = {
107
+ name: String(pickupPlain.fullName ?? store.storeName ?? ""),
108
+ phone: String(pickupPlain.phone ?? ""),
109
+ addressLine1: String(pickupPlain.addressLine1 ?? ""),
110
+ ...(pickupPlain.addressLine2 ? { addressLine2: String(pickupPlain.addressLine2) } : {}),
111
+ city: pickupPlain.city ?? "",
112
+ state: pickupPlain.state ?? "",
113
+ pincode: pickupPlain.postalCode ?? "",
114
+ country: pickupPlain.country ?? "",
115
+ };
116
+ /*
117
+ * 🛑 `getProviders().shipping` is OPTIONAL on the registry, and refusing
118
+ * here is the honest handling. `providers.config.ts` always registers one
119
+ * today, so this is unreachable in this app — but a non-null assertion
120
+ * would turn the day somebody ships a build without it into a
121
+ * `Cannot read properties of undefined` at the moment money is involved,
122
+ * instead of a sentence a seller can act on.
123
+ */
124
+ const provider = getProviders().shipping;
125
+ if (!provider) {
126
+ throw new ValidationError("No shipping provider is configured.", {
127
+ orderId: "Automatic booking is unavailable — dispatch this order manually.",
128
+ });
129
+ }
130
+ /*
131
+ * 🛑 `WalletBalance` is a THREE-state union and collapsing it is the whole
132
+ * hazard the contract's own header describes. Only `{known: true, amount: 0}`
133
+ * is a decision to skip. `{known: false}` means either "no wallet exists"
134
+ * (manual shipping) or "could not read it right now" — and refusing on an
135
+ * unreadable balance would fail every dispatch on a transient blip.
136
+ *
137
+ * An earlier draft of this function tested `typeof balance === "number"`,
138
+ * which is never true against this contract: the guard was dead, so a
139
+ * genuinely empty wallet would have been booked into anyway.
140
+ */
141
+ const balance = await provider.getWalletBalance().catch((err) => {
142
+ void normalizeError(err);
143
+ serverLogger.warn("bookShipment: wallet balance unavailable", { orderId: input.orderId });
144
+ return { known: false, supported: true };
145
+ });
146
+ if (balance.known && balance.amount <= 0) {
147
+ throw new ValidationError("The courier account has no balance, so the booking was not attempted.", { orderId: "Top up the carrier wallet and try again." });
148
+ }
149
+ const payload = {
150
+ orderId: input.orderId,
151
+ dimensions: input.dimensions,
152
+ pickup,
153
+ delivery,
154
+ ...(order.paymentMethod === "cod"
155
+ ? { isCod: true, codAmount: order.codRemainingAmount ?? order.totalPrice }
156
+ : {}),
157
+ };
158
+ const shipment = await provider.createShipment(payload);
159
+ /*
160
+ * 🛑 `platformBookedShipment: true` is the WHOLE POINT of this function.
161
+ *
162
+ * `order-math.ts` already gates shipping reimbursement on it and nothing
163
+ * has ever set it. Writing the tracking id without it would book the parcel
164
+ * AND reimburse the seller for postage the platform just paid.
165
+ */
166
+ await orderRepository.update(input.orderId, {
167
+ trackingNumber: shipment.trackingId,
168
+ ...(shipment.courier ? { carrier: shipment.courier } : {}),
169
+ platformBookedShipment: true,
170
+ });
171
+ return {
172
+ trackingId: shipment.trackingId,
173
+ ...(shipment.courier ? { courier: shipment.courier } : {}),
174
+ ...(shipment.estimatedDelivery ? { estimatedDelivery: shipment.estimatedDelivery } : {}),
175
+ };
176
+ }
@@ -1,3 +1,4 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
2
  /**
2
3
  * Raising and clearing a courier block, and telling the ADMIN about it.
3
4
  *
@@ -21,6 +22,9 @@
21
22
  * whole 80/day budget saying the same thing fifty times and leave the
22
23
  * operator blind to everything else.
23
24
  */
25
+ import { Fragment } from "react";
26
+ import { renderToStaticMarkup } from "react-dom/server";
27
+ import { EmailContainer, EmailDoc, EmailRow, } from "../../../../features/email/primitives.js";
24
28
  import { paymentCasesRepository } from "../../../../features/payment-cases/repository/payment-cases.repository.js";
25
29
  // `adminNotificationsRepository` lives behind the repositories barrel — the
26
30
  // same import the two existing staff-signal call sites use.
@@ -227,7 +231,16 @@ async function emailStaff(input) {
227
231
  const { error } = await sendEmail({
228
232
  to: recipients,
229
233
  subject: input.title,
230
- html: `<p>${body.replace(/\n\n/g, "</p><p>").replace(/\n/g, "<br/>")}</p>`,
234
+ /*
235
+ * Composed from the Email primitives rather than hand-built HTML.
236
+ *
237
+ * The old line string-replaced `\n\n` into `</p><p>`, which produces
238
+ * markup no email client was ever tested against and which
239
+ * `audit-email-raw-html` blocks for that reason. `EmailDoc` renders the
240
+ * table-based, inline-styled structure clients actually honour, and the
241
+ * paragraph split now happens on the DATA before it reaches the markup.
242
+ */
243
+ html: renderStaffAlertEmail(input.title, body),
231
244
  text: body,
232
245
  }, { channel: "email", feature: "courier_wallet_low", audience: "staff" });
233
246
  if (error) {
@@ -237,3 +250,15 @@ async function emailStaff(input) {
237
250
  });
238
251
  }
239
252
  }
253
+ /**
254
+ * The staff courier-block alert, as an email document.
255
+ *
256
+ * Kept local to this file rather than exported: it is one alert with one
257
+ * shape, and a shared "generic staff alert" helper would invite the next
258
+ * caller to pass admin-editable text through it — which is how an email
259
+ * template becomes an injection surface. The body here is built entirely from
260
+ * our own constants and the provider/reason enums.
261
+ */
262
+ function renderStaffAlertEmail(title, body) {
263
+ return `<!DOCTYPE html>${renderToStaticMarkup(_jsx(EmailDoc, { title: title, children: _jsx(EmailContainer, { children: _jsx(EmailRow, { children: body.split("\n\n").map((para, i) => (_jsxs(Fragment, { children: [i > 0 ? _jsx("br", {}) : null, i > 0 ? _jsx("br", {}) : null, para.split("\n").map((line, j) => (_jsxs(Fragment, { children: [j > 0 ? _jsx("br", {}) : null, line] }, j)))] }, i))) }) }) }))}`;
264
+ }
@@ -39,7 +39,7 @@ import type { PublicSiteSettings } from "../../../shared/features/site-settings/
39
39
  */
40
40
  export declare const PUBLIC_SITE_SETTINGS_FIELDS: readonly ["contact", "payment", "listings", "notificationChannels", "announcementBar", "support", "navConfig", "actionConfig", "background", "watermark", "disabledRoutes", "chat", "search"];
41
41
  /** Everything else, with the reason it stays server-side. */
42
- export declare const PRIVATE_SITE_SETTINGS_FIELDS: readonly ["id", "createdAt", "updatedAt", "credentials", "emailSettings", "legalPages", "commissions", "emi", "gst", "laborRate", "adSettings", "integrations", "platformLimits", "auctionConfig", "siteName", "tagline", "motto", "logo", "favicon", "seo", "socialLinks", "features", "shipping", "returns", "faq", "aboutContent", "navbarConfig", "footerConfig", "theme", "featuredResults", "messaging"];
42
+ export declare const PRIVATE_SITE_SETTINGS_FIELDS: readonly ["rateChanges", "id", "createdAt", "updatedAt", "credentials", "emailSettings", "legalPages", "commissions", "emi", "gst", "laborRate", "adSettings", "integrations", "platformLimits", "auctionConfig", "siteName", "tagline", "motto", "logo", "favicon", "seo", "socialLinks", "features", "shipping", "returns", "faq", "aboutContent", "navbarConfig", "footerConfig", "theme", "featuredResults", "messaging"];
43
43
  export interface ToPublicSiteSettingsOptions {
44
44
  /**
45
45
  * The resolved marker → wordmark → text watermark chain. Injected because