@mohasinac/appkit 4.62.1 → 4.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +8 -29
  3. package/dist/_internal/client/features/layout/filterNavItems.d.ts +28 -0
  4. package/dist/_internal/client/features/layout/filterNavItems.js +35 -0
  5. package/dist/_internal/server/features/ads/booking.d.ts +118 -0
  6. package/dist/_internal/server/features/ads/booking.js +326 -0
  7. package/dist/_internal/server/features/checkout/actions.d.ts +51 -11
  8. package/dist/_internal/server/features/checkout/actions.js +335 -80
  9. package/dist/_internal/server/features/checkout/expand-order-items.d.ts +1 -2
  10. package/dist/_internal/server/features/checkout/expand-order-items.js +32 -0
  11. package/dist/_internal/server/features/checkout/freeze-parts.d.ts +50 -0
  12. package/dist/_internal/server/features/checkout/freeze-parts.js +94 -0
  13. package/dist/_internal/server/features/checkout/value-otp-gate.d.ts +55 -0
  14. package/dist/_internal/server/features/checkout/value-otp-gate.js +60 -0
  15. package/dist/_internal/server/features/dashboard/attention.d.ts +128 -0
  16. package/dist/_internal/server/features/dashboard/attention.js +183 -0
  17. package/dist/_internal/server/features/dashboard/data.d.ts +47 -0
  18. package/dist/_internal/server/features/dashboard/data.js +82 -0
  19. package/dist/_internal/server/features/inventory/reservations.d.ts +99 -0
  20. package/dist/_internal/server/features/inventory/reservations.js +273 -0
  21. package/dist/_internal/server/features/offers/destinations.d.ts +40 -0
  22. package/dist/_internal/server/features/offers/destinations.js +65 -0
  23. package/dist/_internal/server/features/orders/adapters.js +21 -0
  24. package/dist/_internal/server/features/orders/amend-delivery.d.ts +56 -0
  25. package/dist/_internal/server/features/orders/amend-delivery.js +124 -0
  26. package/dist/_internal/server/features/orders/balance.d.ts +89 -0
  27. package/dist/_internal/server/features/orders/balance.js +256 -0
  28. package/dist/_internal/server/features/pricing/actions.d.ts +58 -0
  29. package/dist/_internal/server/features/pricing/actions.js +144 -0
  30. package/dist/_internal/server/features/shipping/book-split.d.ts +86 -0
  31. package/dist/_internal/server/features/shipping/book-split.js +130 -0
  32. package/dist/_internal/server/features/shipping/book.d.ts +61 -0
  33. package/dist/_internal/server/features/shipping/book.js +176 -0
  34. package/dist/_internal/server/features/shipping/courier-block.js +26 -1
  35. package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
  36. package/dist/_internal/server/features/site-settings/adapters.js +14 -0
  37. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  38. package/dist/_internal/server/features/stores/adapters.js +14 -0
  39. package/dist/_internal/server/functions/scheduled.d.ts +2 -1
  40. package/dist/_internal/server/functions/scheduled.js +57 -1
  41. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +2 -0
  42. package/dist/_internal/server/jobs/core/jobRunners.js +45 -0
  43. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +4 -1
  44. package/dist/_internal/server/jobs/core/reservationExpiry.d.ts +17 -0
  45. package/dist/_internal/server/jobs/core/reservationExpiry.js +40 -0
  46. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +15 -0
  47. package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
  48. package/dist/_internal/server/jobs/handlers/index.js +1 -0
  49. package/dist/_internal/server/jobs/handlers/reservationExpiry.d.ts +2 -0
  50. package/dist/_internal/server/jobs/handlers/reservationExpiry.js +2 -0
  51. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  52. package/dist/_internal/shared/actions/action-registry.js +268 -0
  53. package/dist/_internal/shared/checkout/order-math.d.ts +31 -2
  54. package/dist/_internal/shared/checkout/order-math.js +22 -36
  55. package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
  56. package/dist/_internal/shared/checkout/rules/index.js +1 -1
  57. package/dist/_internal/shared/checkout/rules/payment-gate.d.ts +18 -0
  58. package/dist/_internal/shared/checkout/rules/payment-gate.js +25 -0
  59. package/dist/_internal/shared/checkout/split.d.ts +0 -12
  60. package/dist/_internal/shared/checkout/split.js +22 -2
  61. package/dist/_internal/shared/content/disclaimers.js +7 -7
  62. package/dist/_internal/shared/content/published-values.d.ts +6 -1
  63. package/dist/_internal/shared/content/published-values.js +8 -0
  64. package/dist/_internal/shared/features/admin/integrations.d.ts +32 -2
  65. package/dist/_internal/shared/features/admin/integrations.js +19 -9
  66. package/dist/_internal/shared/features/dashboard/hub-sections.d.ts +72 -0
  67. package/dist/_internal/shared/features/dashboard/hub-sections.js +75 -0
  68. package/dist/_internal/shared/features/emi/choice.d.ts +98 -0
  69. package/dist/_internal/shared/features/emi/choice.js +69 -0
  70. package/dist/_internal/shared/features/emi/group.d.ts +84 -0
  71. package/dist/_internal/shared/features/emi/group.js +126 -0
  72. package/dist/_internal/shared/features/ledger/posting.d.ts +107 -0
  73. package/dist/_internal/shared/features/ledger/posting.js +165 -0
  74. package/dist/_internal/shared/features/ledger/reports.d.ts +76 -0
  75. package/dist/_internal/shared/features/ledger/reports.js +127 -0
  76. package/dist/_internal/shared/features/ledger/rules.d.ts +170 -0
  77. package/dist/_internal/shared/features/ledger/rules.js +221 -0
  78. package/dist/_internal/shared/features/offers/coordination.d.ts +122 -0
  79. package/dist/_internal/shared/features/offers/coordination.js +80 -0
  80. package/dist/_internal/shared/features/orders/delivery-snapshot.d.ts +114 -0
  81. package/dist/_internal/shared/features/orders/delivery-snapshot.js +116 -0
  82. package/dist/_internal/shared/features/payouts/pay-period.d.ts +91 -0
  83. package/dist/_internal/shared/features/payouts/pay-period.js +104 -0
  84. package/dist/_internal/shared/features/points/accepts.d.ts +1 -8
  85. package/dist/_internal/shared/features/points/accepts.js +12 -6
  86. package/dist/_internal/shared/features/pricing/rate-registry.d.ts +150 -0
  87. package/dist/_internal/shared/features/pricing/rate-registry.js +246 -0
  88. package/dist/_internal/shared/features/shipping/parcel.d.ts +123 -0
  89. package/dist/_internal/shared/features/shipping/parcel.js +139 -0
  90. package/dist/_internal/shared/features/site-settings/fees.d.ts +16 -8
  91. package/dist/_internal/shared/features/site-settings/fees.js +12 -8
  92. package/dist/_internal/shared/fees/explain.d.ts +81 -0
  93. package/dist/_internal/shared/fees/explain.js +101 -0
  94. package/dist/_internal/shared/history/index.d.ts +1 -1
  95. package/dist/_internal/shared/history/index.js +1 -1
  96. package/dist/_internal/shared/history/types.d.ts +7 -1
  97. package/dist/_internal/shared/history/types.js +25 -0
  98. package/dist/client.d.ts +11 -1
  99. package/dist/client.js +13 -0
  100. package/dist/constants/api-endpoints.d.ts +72 -0
  101. package/dist/constants/api-endpoints.js +41 -0
  102. package/dist/constants/field-names.d.ts +41 -0
  103. package/dist/constants/field-names.js +47 -0
  104. package/dist/features/account/components/UserOrdersView.js +28 -2
  105. package/dist/features/admin/components/AdminIntegrationsView.d.ts +55 -0
  106. package/dist/features/admin/components/AdminIntegrationsView.js +74 -0
  107. package/dist/features/admin/components/AdminOrderEditorView.d.ts +16 -1
  108. package/dist/features/admin/components/AdminOrderEditorView.js +72 -3
  109. package/dist/features/admin/components/AdminOrdersView.js +20 -1
  110. package/dist/features/admin/schemas/firestore.d.ts +32 -0
  111. package/dist/features/ads/schemas/booking-write.d.ts +120 -0
  112. package/dist/features/ads/schemas/booking-write.js +75 -0
  113. package/dist/features/ads/schemas/creative-form.d.ts +18 -0
  114. package/dist/features/ads/schemas/creative-form.js +51 -0
  115. package/dist/features/ads/schemas/firestore.d.ts +140 -0
  116. package/dist/features/ads/schemas/firestore.js +109 -0
  117. package/dist/features/auctions/repository/bid.repository.js +3 -1
  118. package/dist/features/audit-log/schemas/firestore.d.ts +110 -0
  119. package/dist/features/audit-log/schemas/firestore.js +74 -0
  120. package/dist/features/auth/actions/profile-actions.d.ts +69 -1
  121. package/dist/features/auth/actions/profile-actions.js +62 -1
  122. package/dist/features/auth/schemas/firestore.d.ts +16 -2
  123. package/dist/features/auth/schemas/firestore.js +2 -1
  124. package/dist/features/blog/actions/blog-actions.d.ts +6 -0
  125. package/dist/features/blog/actions/blog-actions.js +17 -2
  126. package/dist/features/blog/api/route.js +4 -1
  127. package/dist/features/blog/columns/index.js +5 -0
  128. package/dist/features/blog/components/BlogIndexPageView.js +2 -1
  129. package/dist/features/blog/repository/blog.repository.d.ts +4 -0
  130. package/dist/features/blog/repository/blog.repository.js +5 -0
  131. package/dist/features/blog/schemas/blog-form.d.ts +3 -0
  132. package/dist/features/blog/schemas/blog-form.js +1 -0
  133. package/dist/features/blog/schemas/firestore.d.ts +38 -1
  134. package/dist/features/blog/schemas/firestore.js +23 -0
  135. package/dist/features/blog/schemas/index.d.ts +6 -0
  136. package/dist/features/blog/schemas/index.js +2 -0
  137. package/dist/features/blog/types/index.d.ts +2 -0
  138. package/dist/features/cart/components/CartPriceBreakdown.d.ts +12 -1
  139. package/dist/features/cart/components/CartPriceBreakdown.js +24 -5
  140. package/dist/features/cart/repository/cart.repository.d.ts +14 -0
  141. package/dist/features/cart/repository/cart.repository.js +20 -0
  142. package/dist/features/cart/schemas/firestore.d.ts +24 -0
  143. package/dist/features/dashboard/components/DashboardHubView.d.ts +83 -0
  144. package/dist/features/dashboard/components/DashboardHubView.js +56 -0
  145. package/dist/features/dashboard/components/DashboardSectionsClient.d.ts +23 -0
  146. package/dist/features/dashboard/components/DashboardSectionsClient.js +66 -0
  147. package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.d.ts +55 -0
  148. package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.js +175 -0
  149. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +24 -0
  150. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +28 -0
  151. package/dist/features/dispute-reasons/schemas/firestore.d.ts +3 -5
  152. package/dist/features/events/components/EventSaleBanner.js +1 -1
  153. package/dist/features/faq/components/FAQPageContent.js +2 -2
  154. package/dist/features/gateway-events/schemas/index.d.ts +2 -2
  155. package/dist/features/homepage/schemas/index.d.ts +8 -8
  156. package/dist/features/inventory/schemas/stock-reservation.d.ts +114 -0
  157. package/dist/features/inventory/schemas/stock-reservation.js +103 -0
  158. package/dist/features/layout/AppLayoutShell.d.ts +9 -1
  159. package/dist/features/layout/AppLayoutShell.js +12 -4
  160. package/dist/features/layout/BottomActions.js +2 -1
  161. package/dist/features/layout/BottomNavbar.d.ts +10 -2
  162. package/dist/features/layout/SideActionDock.js +52 -2
  163. package/dist/features/layout/TitleBarLayout.js +13 -4
  164. package/dist/features/layout/TitleBarLayout.style.css +59 -0
  165. package/dist/features/layout/useCtaReveal.d.ts +16 -1
  166. package/dist/features/layout/useCtaReveal.js +17 -26
  167. package/dist/features/ledger/repository/journal.repository.d.ts +110 -0
  168. package/dist/features/ledger/repository/journal.repository.js +312 -0
  169. package/dist/features/ledger/schemas/expenses.d.ts +87 -0
  170. package/dist/features/ledger/schemas/expenses.js +27 -0
  171. package/dist/features/ledger/schemas/firestore.d.ts +183 -0
  172. package/dist/features/ledger/schemas/firestore.js +69 -0
  173. package/dist/features/orders/components/BalancePaymentPanel.d.ts +12 -0
  174. package/dist/features/orders/components/BalancePaymentPanel.js +94 -0
  175. package/dist/features/orders/components/RaiseDisputeForm.js +19 -11
  176. package/dist/features/orders/repository/orders.repository.d.ts +28 -1
  177. package/dist/features/orders/repository/orders.repository.js +60 -6
  178. package/dist/features/orders/schemas/balance.d.ts +40 -0
  179. package/dist/features/orders/schemas/balance.js +47 -0
  180. package/dist/features/orders/schemas/firestore.d.ts +202 -0
  181. package/dist/features/orders/schemas/firestore.js +43 -0
  182. package/dist/features/orders/schemas/index.d.ts +2 -2
  183. package/dist/features/orders/types/index.d.ts +21 -0
  184. package/dist/features/payments/repository/payout.repository.js +3 -2
  185. package/dist/features/payments/schemas/firestore.d.ts +92 -1
  186. package/dist/features/payments/schemas/firestore.js +23 -0
  187. package/dist/features/pricing/components/AdminPricingView.d.ts +13 -0
  188. package/dist/features/pricing/components/AdminPricingView.js +157 -0
  189. package/dist/features/products/schemas/firestore.d.ts +26 -1
  190. package/dist/features/products/schemas/firestore.js +2 -0
  191. package/dist/features/products/schemas/index.d.ts +2 -2
  192. package/dist/features/reviews/repository/reviews.repository.d.ts +14 -0
  193. package/dist/features/reviews/repository/reviews.repository.js +16 -0
  194. package/dist/features/scams/repository/scammer.repository.js +2 -1
  195. package/dist/features/scams/schemas/firestore.d.ts +2 -1
  196. package/dist/features/scams/schemas/firestore.js +2 -1
  197. package/dist/features/seller/actions/offer-actions.js +56 -0
  198. package/dist/features/seller/actions/seller-actions.d.ts +1 -1
  199. package/dist/features/seller/actions/seller-actions.js +4 -0
  200. package/dist/features/seller/components/DispatchForm.d.ts +11 -0
  201. package/dist/features/seller/components/DispatchForm.js +190 -0
  202. package/dist/features/seller/components/SellerOrdersView.js +94 -2
  203. package/dist/features/seller/components/index.d.ts +1 -0
  204. package/dist/features/seller/components/index.js +1 -0
  205. package/dist/features/seller/repository/offer.repository.js +4 -3
  206. package/dist/features/seller/schemas/firestore.d.ts +23 -2
  207. package/dist/features/seller/schemas/firestore.js +3 -2
  208. package/dist/features/seller/schemas/index.d.ts +4 -4
  209. package/dist/features/seller/schemas/order-forms.d.ts +61 -4
  210. package/dist/features/seller/schemas/order-forms.js +41 -8
  211. package/dist/features/shipments/schemas/firestore.d.ts +25 -0
  212. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +39 -0
  213. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +55 -0
  214. package/dist/features/shipping-boxes/schemas/firestore.d.ts +2 -4
  215. package/dist/features/stores/repository/store.repository.d.ts +18 -0
  216. package/dist/features/stores/repository/store.repository.js +27 -1
  217. package/dist/features/stores/schemas/firestore.d.ts +41 -0
  218. package/dist/features/stores/schemas/firestore.js +20 -0
  219. package/dist/features/stores/schemas/index.d.ts +6 -6
  220. package/dist/features/support/repository/support.repository.js +72 -6
  221. package/dist/features/support/schemas/firestore.d.ts +17 -0
  222. package/dist/features/support/schemas/index.d.ts +2 -2
  223. package/dist/index.d.ts +5 -0
  224. package/dist/index.js +19 -0
  225. package/dist/next/routing/route-map.d.ts +92 -0
  226. package/dist/next/routing/route-map.js +53 -0
  227. package/dist/react/contexts/SessionContext.d.ts +17 -1
  228. package/dist/schemas/registry.d.ts +22 -16
  229. package/dist/security/pii-schemas.d.ts +1 -1
  230. package/dist/security/pii-schemas.js +45 -0
  231. package/dist/server.d.ts +27 -1
  232. package/dist/server.js +144 -0
  233. package/dist/styles.css +349 -1
  234. package/dist/tailwind-utilities.css +1 -1
  235. package/dist/ui/components/Button.d.ts +10 -1
  236. package/dist/ui/components/Button.js +25 -3
  237. package/dist/ui/components/Chart.d.ts +63 -0
  238. package/dist/ui/components/Chart.js +169 -0
  239. package/dist/ui/components/Chart.style.css +214 -0
  240. package/dist/ui/components/Disclaimer.js +10 -1
  241. package/dist/ui/components/OverflowRow.js +54 -2
  242. package/dist/ui/components/OverflowRow.style.css +38 -0
  243. package/dist/ui/components/RichTextEditor.js +99 -8
  244. package/dist/ui/components/chart-spec.d.ts +68 -0
  245. package/dist/ui/components/chart-spec.js +140 -0
  246. package/dist/ui/components/index.style.css +1 -0
  247. package/dist/ui/components/surface-tokens.d.ts +1 -1
  248. package/dist/ui/components/surface-tokens.js +9 -0
  249. package/dist/ui/components/toolbar-collapse.d.ts +13 -0
  250. package/dist/ui/components/toolbar-collapse.js +10 -1
  251. package/dist/ui/index.d.ts +6 -0
  252. package/dist/ui/index.js +3 -0
  253. package/dist/ui/rich-text/RichText.d.ts +10 -1
  254. package/dist/ui/rich-text/RichText.js +33 -2
  255. package/dist/ui/rich-text/RichText.style.css +35 -0
  256. package/dist/ui/rich-text/RichTextRenderer.d.ts +30 -1
  257. package/dist/ui/rich-text/RichTextRenderer.js +34 -2
  258. package/dist/ui/rich-text/chart-blocks.d.ts +62 -0
  259. package/dist/ui/rich-text/chart-blocks.js +87 -0
  260. package/package.json +1 -1
  261. package/scripts/seed-cli.mjs +3 -0
@@ -32,10 +32,30 @@ export declare const sellerOrderUpdateSchema: z.ZodEffects<z.ZodObject<{
32
32
  }>;
33
33
  export type SellerOrderUpdateValues = z.infer<typeof sellerOrderUpdateSchema>;
34
34
  /**
35
- * Manual shipment entry. `method` is a literal rather than an enum because
36
- * `ManualShippingProvider` is the only provider — see § "Provider Resolution".
35
+ * Shipment entry — manual, or booked through the platform.
36
+ *
37
+ * 🛑 **`method` was `z.literal("custom")`, and that literal WAS bug 0e.**
38
+ *
39
+ * Its comment said *"a literal rather than an enum because
40
+ * `ManualShippingProvider` is the only provider"* — no longer true:
41
+ * `providers/shipping-shiprocket/` is 570 lines with all six methods, and
42
+ * `providers.config.ts` already resolves it from
43
+ * `siteSettings.shipping.provider`. The seller simply could not EXPRESS
44
+ * "book this through us", so there was no option to offer, and
45
+ * `platformBookedShipment` — which `order-math.ts` gates reimbursement on —
46
+ * had zero writers.
47
+ *
48
+ * A DISCRIMINATED UNION rather than an enum plus optional fields, because
49
+ * the two branches need different things and an optional-everything shape
50
+ * cannot say so:
51
+ *
52
+ * · `custom` — the seller posted it and types the carrier and tracking id.
53
+ * Those are REQUIRED, which is the rule enforced in three places today.
54
+ * · `platform` — we book it, so there is no tracking id to type; it comes
55
+ * back from the carrier. Requiring one here would be asking the seller to
56
+ * invent the thing the booking produces.
37
57
  */
38
- export declare const shipOrderSchema: z.ZodObject<{
58
+ export declare const shipOrderSchema: z.ZodDiscriminatedUnion<"method", [z.ZodObject<{
39
59
  method: z.ZodLiteral<"custom">;
40
60
  shippingCarrier: z.ZodString;
41
61
  trackingNumber: z.ZodString;
@@ -50,7 +70,44 @@ export declare const shipOrderSchema: z.ZodObject<{
50
70
  shippingCarrier: string;
51
71
  trackingUrl: string;
52
72
  method: "custom";
53
- }>;
73
+ }>, z.ZodObject<{
74
+ method: z.ZodLiteral<"platform">;
75
+ pickupAddressId: z.ZodString;
76
+ dimensions: z.ZodObject<{
77
+ weight: z.ZodNumber;
78
+ length: z.ZodNumber;
79
+ width: z.ZodNumber;
80
+ height: z.ZodNumber;
81
+ }, "strip", z.ZodTypeAny, {
82
+ length: number;
83
+ weight: number;
84
+ height: number;
85
+ width: number;
86
+ }, {
87
+ length: number;
88
+ weight: number;
89
+ height: number;
90
+ width: number;
91
+ }>;
92
+ }, "strip", z.ZodTypeAny, {
93
+ pickupAddressId: string;
94
+ method: "platform";
95
+ dimensions: {
96
+ length: number;
97
+ weight: number;
98
+ height: number;
99
+ width: number;
100
+ };
101
+ }, {
102
+ pickupAddressId: string;
103
+ method: "platform";
104
+ dimensions: {
105
+ length: number;
106
+ weight: number;
107
+ height: number;
108
+ width: number;
109
+ };
110
+ }>]>;
54
111
  export type ShipOrderValues = z.infer<typeof shipOrderSchema>;
55
112
  /**
56
113
  * Marking one EMI installment paid. `installmentIndex` is 1-based, matching
@@ -92,15 +92,48 @@ export const sellerOrderUpdateSchema = z
92
92
  * it is also called directly from a server action, where no route ran first.
93
93
  */
94
94
  /**
95
- * Manual shipment entry. `method` is a literal rather than an enum because
96
- * `ManualShippingProvider` is the only provider — see § "Provider Resolution".
95
+ * Shipment entry — manual, or booked through the platform.
96
+ *
97
+ * 🛑 **`method` was `z.literal("custom")`, and that literal WAS bug 0e.**
98
+ *
99
+ * Its comment said *"a literal rather than an enum because
100
+ * `ManualShippingProvider` is the only provider"* — no longer true:
101
+ * `providers/shipping-shiprocket/` is 570 lines with all six methods, and
102
+ * `providers.config.ts` already resolves it from
103
+ * `siteSettings.shipping.provider`. The seller simply could not EXPRESS
104
+ * "book this through us", so there was no option to offer, and
105
+ * `platformBookedShipment` — which `order-math.ts` gates reimbursement on —
106
+ * had zero writers.
107
+ *
108
+ * A DISCRIMINATED UNION rather than an enum plus optional fields, because
109
+ * the two branches need different things and an optional-everything shape
110
+ * cannot say so:
111
+ *
112
+ * · `custom` — the seller posted it and types the carrier and tracking id.
113
+ * Those are REQUIRED, which is the rule enforced in three places today.
114
+ * · `platform` — we book it, so there is no tracking id to type; it comes
115
+ * back from the carrier. Requiring one here would be asking the seller to
116
+ * invent the thing the booking produces.
97
117
  */
98
- export const shipOrderSchema = z.object({
99
- method: z.literal("custom"),
100
- shippingCarrier: z.string().min(1),
101
- trackingNumber: z.string().min(1),
102
- trackingUrl: z.string().url(),
103
- });
118
+ export const shipOrderSchema = z.discriminatedUnion("method", [
119
+ z.object({
120
+ method: z.literal("custom"),
121
+ shippingCarrier: z.string().min(1),
122
+ trackingNumber: z.string().min(1),
123
+ trackingUrl: z.string().url(),
124
+ }),
125
+ z.object({
126
+ method: z.literal("platform"),
127
+ /* The store address the courier collects from. */
128
+ pickupAddressId: z.string().min(1),
129
+ dimensions: z.object({
130
+ weight: z.number().positive().max(50),
131
+ length: z.number().positive().max(200),
132
+ width: z.number().positive().max(200),
133
+ height: z.number().positive().max(200),
134
+ }),
135
+ }),
136
+ ]);
104
137
  /**
105
138
  * Marking one EMI installment paid. `installmentIndex` is 1-based, matching
106
139
  * how the schedule is presented to the buyer.
@@ -43,7 +43,32 @@ export interface ShipmentTotals {
43
43
  export declare const DEFAULT_SHIPMENT_TOTALS: ShipmentTotals;
44
44
  export interface ShipmentDocument extends BaseDocument {
45
45
  shipmentNumber: string;
46
+ /**
47
+ * The supplier as TYPED. Kept, and kept required, for display.
48
+ *
49
+ * 🛑 It is free text, so four shipments from one supplier are four
50
+ * unjoined strings — "Acme Toys", "acme toys", "Acme Toys Ltd", "ACME".
51
+ * Nothing can total what we have spent with anyone, which is the first
52
+ * question anybody asks of procurement data.
53
+ */
46
54
  supplierName: string;
55
+ /**
56
+ * The joinable identity — a `vendors` row.
57
+ *
58
+ * 🛑 OPTIONAL, and that is deliberate rather than a migration left half
59
+ * done. Every existing shipment has only the string, and backfilling by
60
+ * fuzzy-matching names is precisely how two real suppliers get merged into
61
+ * one spend figure that nobody can unpick afterwards. A shipment gains an
62
+ * id when a human links it; until then it is honestly unattributed, and
63
+ * the vendor report can say how many are.
64
+ *
65
+ * 🛑 The NAME is not derived from the vendor row on read. Two reasons: a
66
+ * vendor rename would retroactively rewrite what every past shipment says
67
+ * it was bought from, and the typed string is evidence of what the invoice
68
+ * actually said. Same stance the order item's frozen listing snapshot
69
+ * takes (Part 14).
70
+ */
71
+ supplierId?: string;
47
72
  originCountry?: string;
48
73
  status: ShipmentStatus;
49
74
  trackingNumber?: string;
@@ -39,5 +39,44 @@ export declare class ShippingBoxesRepository extends BaseRepository<ShippingBoxD
39
39
  box: ShippingBoxDocument;
40
40
  created: boolean;
41
41
  }>;
42
+ /**
43
+ * Every preset, retired included — the staff management list.
44
+ *
45
+ * Separate from `listActive` for the same reason the dispute reasons are: the
46
+ * picker must never offer a retired row, and a management view must be able
47
+ * to find one in order to put it back.
48
+ */
49
+ listAll(model: SieveModel): Promise<FirebaseSieveResult<ShippingBoxDocument>>;
50
+ /**
51
+ * Correct a preset's dimensions. **Staff only** — enforced at the route.
52
+ *
53
+ * ## 🛑 This does NOT change any dispatched parcel, by construction
54
+ *
55
+ * The dimensions are COPIED onto the shipment at use time (the schema's own
56
+ * header: *"a preset is a DEFAULT, never a lock"*), so there is no stored
57
+ * reference for this to reach through. That is precisely why staff editing is
58
+ * safe to offer at all: a seller's past declaration is a snapshot, and the
59
+ * courier billed against the figures at dispatch.
60
+ *
61
+ * It is staff-only for the mirror reason — these numbers are what the NEXT
62
+ * seller's dispatch form defaults to, so a wrong edit misdeclares other
63
+ * people's parcels. A seller who finds a preset wrong adds a new one; staff
64
+ * decide whether the shared row changes.
65
+ *
66
+ * 🛑 The NAME is deliberately not editable. The id is `box-<slug of name>`,
67
+ * so renaming would either leave the id disagreeing with the name (the
68
+ * mirror-drift trap) or require a new document, which is just a create. Add
69
+ * the correctly-named preset and retire this one.
70
+ */
71
+ updatePreset(id: string, input: {
72
+ length?: number;
73
+ width?: number;
74
+ height?: number;
75
+ tareWeight?: number;
76
+ }): Promise<void>;
77
+ /** Soft-retire. See the schema for why this is never a delete. */
78
+ retire(id: string): Promise<void>;
79
+ /** Put a retired preset back in the picker. */
80
+ reactivate(id: string): Promise<void>;
42
81
  }
43
82
  export declare const shippingBoxesRepository: ShippingBoxesRepository;
@@ -58,6 +58,61 @@ export class ShippingBoxesRepository extends BaseRepository {
58
58
  await this.createWithId(id, box);
59
59
  return { box, created: true };
60
60
  }
61
+ /**
62
+ * Every preset, retired included — the staff management list.
63
+ *
64
+ * Separate from `listActive` for the same reason the dispute reasons are: the
65
+ * picker must never offer a retired row, and a management view must be able
66
+ * to find one in order to put it back.
67
+ */
68
+ async listAll(model) {
69
+ return this.sieveQuery(model, ShippingBoxesRepository.SIEVE_FIELDS, {
70
+ baseQuery: this.getCollection(),
71
+ defaultPageSize: 20,
72
+ maxPageSize: 50,
73
+ });
74
+ }
75
+ /**
76
+ * Correct a preset's dimensions. **Staff only** — enforced at the route.
77
+ *
78
+ * ## 🛑 This does NOT change any dispatched parcel, by construction
79
+ *
80
+ * The dimensions are COPIED onto the shipment at use time (the schema's own
81
+ * header: *"a preset is a DEFAULT, never a lock"*), so there is no stored
82
+ * reference for this to reach through. That is precisely why staff editing is
83
+ * safe to offer at all: a seller's past declaration is a snapshot, and the
84
+ * courier billed against the figures at dispatch.
85
+ *
86
+ * It is staff-only for the mirror reason — these numbers are what the NEXT
87
+ * seller's dispatch form defaults to, so a wrong edit misdeclares other
88
+ * people's parcels. A seller who finds a preset wrong adds a new one; staff
89
+ * decide whether the shared row changes.
90
+ *
91
+ * 🛑 The NAME is deliberately not editable. The id is `box-<slug of name>`,
92
+ * so renaming would either leave the id disagreeing with the name (the
93
+ * mirror-drift trap) or require a new document, which is just a create. Add
94
+ * the correctly-named preset and retire this one.
95
+ */
96
+ async updatePreset(id, input) {
97
+ const patch = { updatedAt: new Date() };
98
+ if (input.length !== undefined)
99
+ patch.length = clampCm(input.length);
100
+ if (input.width !== undefined)
101
+ patch.width = clampCm(input.width);
102
+ if (input.height !== undefined)
103
+ patch.height = clampCm(input.height);
104
+ if (input.tareWeight !== undefined)
105
+ patch.tareWeight = clampKg(input.tareWeight);
106
+ await this.update(id, patch);
107
+ }
108
+ /** Soft-retire. See the schema for why this is never a delete. */
109
+ async retire(id) {
110
+ await this.update(id, { isActive: false, updatedAt: new Date() });
111
+ }
112
+ /** Put a retired preset back in the picker. */
113
+ async reactivate(id) {
114
+ await this.update(id, { isActive: true, updatedAt: new Date() });
115
+ }
61
116
  }
62
117
  ShippingBoxesRepository.SIEVE_FIELDS = {
63
118
  /*
@@ -27,10 +27,10 @@
27
27
  * cannot dispute without the parcel. The form says so rather than treating it
28
28
  * as a required field to get past.
29
29
  */
30
+ import type { BaseDocument } from "../../../_internal/shared/types/base-document";
30
31
  /** `box-` prefixed slug, so the id reads as what it is. */
31
32
  export declare const SHIPPING_BOX_COLLECTION = "shippingBoxes";
32
- export interface ShippingBoxDocument {
33
- id: string;
33
+ export interface ShippingBoxDocument extends BaseDocument {
34
34
  /** What a seller picks it by: "Standard Beyblade carton", "Poly mailer S". */
35
35
  name: string;
36
36
  /** cm. */
@@ -55,8 +55,6 @@ export interface ShippingBoxDocument {
55
55
  * not vanish mid-dispatch, and a deleted row takes its author with it.
56
56
  */
57
57
  isActive: boolean;
58
- createdAt: Date;
59
- updatedAt: Date;
60
58
  }
61
59
  export declare const SHIPPING_BOX_FIELDS: {
62
60
  readonly ID: "id";
@@ -29,6 +29,24 @@ export declare class StoreRepository extends BaseRepository<StoreDocument> {
29
29
  * Find a store by its URL slug (also the document ID).
30
30
  */
31
31
  findBySlug(storeSlug: string): Promise<StoreDocument | null>;
32
+ /**
33
+ * The slugs of every store paid at shipping rather than at delivery.
34
+ *
35
+ * 🛑 A `Set` of IDS, not the documents. The payout sweep needs only
36
+ * membership, and loading full store documents for a question answered by a
37
+ * string would spend the ~3-round-trip budget (Rule #6) on data it discards.
38
+ *
39
+ * 🛑 An EQUALITY on `trustedSeller == true`, never `!= false`. A Firestore
40
+ * inequality excludes every document that LACKS the field — which is every
41
+ * store, since the flag is new and optional — so `!=` would return the
42
+ * trusted set and nothing else, or everything, depending on which way you
43
+ * wrote it. Both are wrong and neither raises.
44
+ *
45
+ * Bounded at 500: trust is granted by hand, one store at a time, on risk
46
+ * grounds. If this ever saturates, the grant has stopped being deliberate
47
+ * and that is the thing to fix rather than the limit.
48
+ */
49
+ findTrustedStoreIds(): Promise<Set<string>>;
32
50
  /**
33
51
  * Find the store owned by a specific user UID.
34
52
  */
@@ -59,6 +59,32 @@ export class StoreRepository extends BaseRepository {
59
59
  async findBySlug(storeSlug) {
60
60
  return this.findById(storeSlug);
61
61
  }
62
+ /**
63
+ * The slugs of every store paid at shipping rather than at delivery.
64
+ *
65
+ * 🛑 A `Set` of IDS, not the documents. The payout sweep needs only
66
+ * membership, and loading full store documents for a question answered by a
67
+ * string would spend the ~3-round-trip budget (Rule #6) on data it discards.
68
+ *
69
+ * 🛑 An EQUALITY on `trustedSeller == true`, never `!= false`. A Firestore
70
+ * inequality excludes every document that LACKS the field — which is every
71
+ * store, since the flag is new and optional — so `!=` would return the
72
+ * trusted set and nothing else, or everything, depending on which way you
73
+ * wrote it. Both are wrong and neither raises.
74
+ *
75
+ * Bounded at 500: trust is granted by hand, one store at a time, on risk
76
+ * grounds. If this ever saturates, the grant has stopped being deliberate
77
+ * and that is the thing to fix rather than the limit.
78
+ */
79
+ async findTrustedStoreIds() {
80
+ const snap = await this.db
81
+ .collection(this.collection)
82
+ .where(STORE_FIELDS.TRUSTED_SELLER, "==", true)
83
+ .select()
84
+ .limit(500)
85
+ .get();
86
+ return new Set(snap.docs.map((d) => d.id));
87
+ }
62
88
  /**
63
89
  * Find the store owned by a specific user UID.
64
90
  */
@@ -117,7 +143,7 @@ export class StoreRepository extends BaseRepository {
117
143
  trigger: ctx?.trigger ?? defaultTrigger,
118
144
  reason: ctx?.reason,
119
145
  note: ctx?.note,
120
- // `encryptPiiFields` never descends into arrays, so a nested secret
146
+ // `encryptPiiFields` covers only NAMED paths, so an undeclared nested secret
121
147
  // would be stored in plaintext — the list below is the scrub.
122
148
  piiFields: STORE_HISTORY_PII_FIELDS,
123
149
  });
@@ -107,6 +107,27 @@ export interface StoreDocument extends BaseDocument {
107
107
  adminNotes?: string;
108
108
  /** Admin "verified store" badge — distinct from `status`/`isPublic`. */
109
109
  isVerified?: boolean;
110
+ /**
111
+ * Paid at SHIPPING rather than at delivery.
112
+ *
113
+ * 🛑 **A risk transfer, not a courtesy or a badge.** Paying before delivery
114
+ * means the platform carries a lost parcel and a non-delivery dispute with
115
+ * the money already gone. Admin-granted only, `recordAdminAction`-logged,
116
+ * revocable — and **PRIVATE in the public projection**, because it describes
117
+ * our risk posture rather than anything a buyer should weigh. A store does
118
+ * not get to display that we trust it.
119
+ *
120
+ * 🛑 A faster path through the SAME machinery, never a second one. The
121
+ * eligibility sweep moves the delivered-status test, not the payout, the
122
+ * hold, the deductions or the dispatch.
123
+ *
124
+ * 🛑 **A refund after a trusted payout has no money left to net against.**
125
+ * The ordinary path deducts from a still-`pending` payout; once paid there
126
+ * is nothing to deduct from, so it becomes a `manual_refund_due` case —
127
+ * which already exists and already means exactly this. Never a negative
128
+ * payout.
129
+ */
130
+ trustedSeller?: boolean;
110
131
  /** Reason recorded when `status` is set to "suspended". */
111
132
  suspensionReason?: string;
112
133
  stats?: {
@@ -386,6 +407,7 @@ export declare const STORE_FIELDS: {
386
407
  readonly ADMIN_NOTES: "adminNotes";
387
408
  readonly IS_FEATURED: "isFeatured";
388
409
  readonly IS_VERIFIED: "isVerified";
410
+ readonly TRUSTED_SELLER: "trustedSeller";
389
411
  readonly SUSPENSION_REASON: "suspensionReason";
390
412
  readonly CAPABILITIES: "capabilities";
391
413
  readonly STATUS_VALUES: {
@@ -404,3 +426,22 @@ export type StoreAddressUpdateInput = _AddrUpdate;
404
426
  export { ADDRESS_FIELDS as STORE_ADDRESS_FIELDS, ADDRESS_INDEXED_FIELDS as STORE_ADDRESS_INDEXED_FIELDS, ADDRESS_PUBLIC_FIELDS as STORE_ADDRESS_PUBLIC_FIELDS, ADDRESS_UPDATABLE_FIELDS as STORE_ADDRESS_UPDATABLE_FIELDS, DEFAULT_ADDRESS_DATA as DEFAULT_STORE_ADDRESS_DATA, } from "../../addresses/schemas";
405
427
  /** @deprecated SB-UNI-A — literal alias only. Use ADDRESSES_COLLECTION. */
406
428
  export declare const STORE_ADDRESS_SUBCOLLECTION: "addresses";
429
+ /**
430
+ * The platform's own storefront.
431
+ *
432
+ * An IDENTITY, not a setting — it is the same id the seed's `REAL_STORE_IDS`
433
+ * and `realListingsOnly` already treat as the platform's own, and resolving it
434
+ * in code is what stops it being switched off by an edit, a reseed, or a
435
+ * mis-clicked admin toggle. `storeAcceptsPoints` is the motivating reader: the
436
+ * one store the points programme is advertised on cannot be allowed to decline
437
+ * to fund it.
438
+ *
439
+ * 🛑 It lives HERE, in `features/`, and not beside its reader in
440
+ * `_internal/shared/features/points/` — `audit-ssr-in-appkit` Rule 3 forbids a
441
+ * brand-bearing literal inside `_internal/`, because a consumer of appkit would
442
+ * inherit OUR store id with no way to override it. `features/` is where the
443
+ * same literal already lives legitimately (`CONSIGNMENT_STORE_ID` in
444
+ * `features/catalogue/utils/product-from-item.ts`), and this file is the one
445
+ * that already documents the rule on `acceptsPoints` above.
446
+ */
447
+ export declare const PLATFORM_STORE_ID = "store-letitrip-official";
@@ -74,6 +74,7 @@ export const STORE_FIELDS = {
74
74
  ADMIN_NOTES: "adminNotes",
75
75
  IS_FEATURED: "isFeatured",
76
76
  IS_VERIFIED: "isVerified",
77
+ TRUSTED_SELLER: "trustedSeller",
77
78
  SUSPENSION_REASON: "suspensionReason",
78
79
  CAPABILITIES: "capabilities",
79
80
  STATUS_VALUES: {
@@ -101,3 +102,22 @@ export function generateStoreSlug(storeName, ownerDisplayName) {
101
102
  export { ADDRESS_FIELDS as STORE_ADDRESS_FIELDS, ADDRESS_INDEXED_FIELDS as STORE_ADDRESS_INDEXED_FIELDS, ADDRESS_PUBLIC_FIELDS as STORE_ADDRESS_PUBLIC_FIELDS, ADDRESS_UPDATABLE_FIELDS as STORE_ADDRESS_UPDATABLE_FIELDS, DEFAULT_ADDRESS_DATA as DEFAULT_STORE_ADDRESS_DATA, } from "../../addresses/schemas/index.js";
102
103
  /** @deprecated SB-UNI-A — literal alias only. Use ADDRESSES_COLLECTION. */
103
104
  export const STORE_ADDRESS_SUBCOLLECTION = "addresses";
105
+ /**
106
+ * The platform's own storefront.
107
+ *
108
+ * An IDENTITY, not a setting — it is the same id the seed's `REAL_STORE_IDS`
109
+ * and `realListingsOnly` already treat as the platform's own, and resolving it
110
+ * in code is what stops it being switched off by an edit, a reseed, or a
111
+ * mis-clicked admin toggle. `storeAcceptsPoints` is the motivating reader: the
112
+ * one store the points programme is advertised on cannot be allowed to decline
113
+ * to fund it.
114
+ *
115
+ * 🛑 It lives HERE, in `features/`, and not beside its reader in
116
+ * `_internal/shared/features/points/` — `audit-ssr-in-appkit` Rule 3 forbids a
117
+ * brand-bearing literal inside `_internal/`, because a consumer of appkit would
118
+ * inherit OUR store id with no way to override it. `features/` is where the
119
+ * same literal already lives legitimately (`CONSIGNMENT_STORE_ID` in
120
+ * `features/catalogue/utils/product-from-item.ts`), and this file is the one
121
+ * that already documents the rule on `acceptsPoints` above.
122
+ */
123
+ export const PLATFORM_STORE_ID = "store-letitrip-official";
@@ -218,13 +218,13 @@ export declare const storeFirestoreSchema: z.ZodObject<{
218
218
  totalReviews: z.ZodNumber;
219
219
  averageRating: z.ZodOptional<z.ZodNumber>;
220
220
  }, "strip", z.ZodTypeAny, {
221
- itemsSold: number;
222
221
  totalProducts: number;
222
+ itemsSold: number;
223
223
  totalReviews: number;
224
224
  averageRating?: number | undefined;
225
225
  }, {
226
- itemsSold: number;
227
226
  totalProducts: number;
227
+ itemsSold: number;
228
228
  totalReviews: number;
229
229
  averageRating?: number | undefined;
230
230
  }>>;
@@ -363,8 +363,8 @@ export declare const storeFirestoreSchema: z.ZodObject<{
363
363
  location?: string | undefined;
364
364
  returnPolicy?: string | undefined;
365
365
  stats?: {
366
- itemsSold: number;
367
366
  totalProducts: number;
367
+ itemsSold: number;
368
368
  totalReviews: number;
369
369
  averageRating?: number | undefined;
370
370
  } | undefined;
@@ -432,8 +432,8 @@ export declare const storeFirestoreSchema: z.ZodObject<{
432
432
  location?: string | undefined;
433
433
  returnPolicy?: string | undefined;
434
434
  stats?: {
435
- itemsSold: number;
436
435
  totalProducts: number;
436
+ itemsSold: number;
437
437
  totalReviews: number;
438
438
  averageRating?: number | undefined;
439
439
  } | undefined;
@@ -530,8 +530,8 @@ export declare const storeListItemSchema: z.ZodObject<{
530
530
  storeDescription?: string | undefined;
531
531
  storeLogoURL?: string | undefined;
532
532
  storeBannerURL?: string | undefined;
533
- itemsSold?: number | undefined;
534
533
  totalProducts?: number | undefined;
534
+ itemsSold?: number | undefined;
535
535
  totalReviews?: number | undefined;
536
536
  averageRating?: number | undefined;
537
537
  }, {
@@ -546,8 +546,8 @@ export declare const storeListItemSchema: z.ZodObject<{
546
546
  storeDescription?: string | undefined;
547
547
  storeLogoURL?: string | undefined;
548
548
  storeBannerURL?: string | undefined;
549
- itemsSold?: number | undefined;
550
549
  totalProducts?: number | undefined;
550
+ itemsSold?: number | undefined;
551
551
  totalReviews?: number | undefined;
552
552
  averageRating?: number | undefined;
553
553
  }>;
@@ -9,6 +9,41 @@ import { sieveAnd, sieveFilter, SIEVE_OP } from "../../../utils/sieve-builder.js
9
9
  import { maskPii, revealPii } from "../../../security/pii-accessors.js";
10
10
  import { SUPPORT_TICKET_COLLECTION, SUPPORT_TICKET_PII_FIELDS, SUPPORT_TICKET_TRACKED_FIELDS, TicketStatusValues, ACTIVE_TICKET_STATUSES, } from "../schemas/firestore.js";
11
11
  import { withHistory } from "../../../_internal/shared/history/index.js";
12
+ /**
13
+ * Who, if anyone, gets credited with resolving this ticket.
14
+ *
15
+ * 🛑 **A BUYER is never the resolver**, even though a buyer can drive the
16
+ * transition — replying "that's sorted, thanks" is a legitimate way for a
17
+ * ticket to reach `resolved`, and crediting it would put a customer's uid in
18
+ * a field a payslip is computed from. `system` has no uid at all, which is
19
+ * the other case that must not be paid.
20
+ *
21
+ * Returning `undefined` is correct and expected: the ticket is still resolved,
22
+ * it simply earns nobody wages. A fabricated fallback — the assignee, the
23
+ * ticket owner — would be a wage attributed to someone who did not do the
24
+ * work, and it would look exactly like a real one.
25
+ */
26
+ const WAGE_EARNING_ACTOR_ROLES = new Set(["seller", "admin"]);
27
+ function resolverUid(ctx) {
28
+ const actor = ctx?.actor;
29
+ if (!actor?.uid)
30
+ return undefined;
31
+ /*
32
+ * An ALLOW-LIST, not `role !== "buyer"`. A tenth actor role added later
33
+ * would silently start earning wages under a deny-list, which is the
34
+ * `Record<Union, T>`-over-`Set<string>` argument this codebase makes
35
+ * everywhere else — and the roles that can be credited is the shorter,
36
+ * more stable list.
37
+ *
38
+ * 🛑 `HistoryActorRole` has no `employee` member, and that is FINE: this
39
+ * field records WHO resolved it, and the payroll decides whether that
40
+ * person is on it. A seller resolving their own store's ticket is recorded
41
+ * here and appears on no payslip, because they are not an employee.
42
+ */
43
+ if (!WAGE_EARNING_ACTOR_ROLES.has(actor.role))
44
+ return undefined;
45
+ return actor.uid;
46
+ }
12
47
  export class SupportRepository extends BaseRepository {
13
48
  constructor() {
14
49
  super(SUPPORT_TICKET_COLLECTION);
@@ -210,7 +245,7 @@ export class SupportRepository extends BaseRepository {
210
245
  // `piiFields`, which is what encrypts `assignedToName` on the way down.
211
246
  const patch = {
212
247
  ...update,
213
- ...this.resolutionStamps(current, update.status),
248
+ ...this.resolutionStamps(current, update.status, resolverUid(ctx)),
214
249
  updatedAt: new Date(),
215
250
  };
216
251
  return this.update(ticketId, this.withTicketHistory(current, patch, ctx, "updateTicketStatus"));
@@ -233,14 +268,45 @@ export class SupportRepository extends BaseRepository {
233
268
  * against the document would poison that patch's inferred type with
234
269
  * `Encrypted<string>` members it never actually sets.
235
270
  */
236
- resolutionStamps(current, next) {
271
+ resolutionStamps(current, next, actorUid) {
237
272
  if (!next || next === current?.status)
238
273
  return {};
239
274
  const now = new Date();
240
- if (next === TicketStatusValues.RESOLVED && !current?.resolvedAt)
241
- return { resolvedAt: now };
275
+ if (next === TicketStatusValues.RESOLVED && !current?.resolvedAt) {
276
+ /*
277
+ * 🛑 `resolvedBy` is stamped HERE and is not the same thing as
278
+ * `assignedTo`.
279
+ *
280
+ * `assignedTo` is the CURRENT assignee: reassign a resolved ticket for
281
+ * follow-up and it moves, so paying from it credits whoever happens to
282
+ * hold the ticket at month end rather than whoever did the work. That is
283
+ * the whole reason this field exists.
284
+ *
285
+ * Absent when the caller supplied no actor — a system transition has no
286
+ * person to pay, and a fabricated uid would be a wage attributed to
287
+ * somebody who did nothing.
288
+ */
289
+ return actorUid ? { resolvedAt: now, resolvedBy: actorUid } : { resolvedAt: now };
290
+ }
242
291
  if (next === TicketStatusValues.CLOSED && !current?.closedAt)
243
292
  return { closedAt: now };
293
+ /*
294
+ * 🛑 A REOPEN clears both, and clearing `resolvedBy` is load-bearing.
295
+ *
296
+ * Pay is computed from tickets STILL RESOLVED at period end, so a ticket
297
+ * that goes back to open must stop counting — otherwise close-it,
298
+ * let-it-reopen, close-it-again pays twice for one problem.
299
+ *
300
+ * Cleared rather than left in place with the status as the only signal,
301
+ * because `resolvedTicketsForPeriod` filters on all three and a stale
302
+ * `resolvedAt` surviving a reopen is a row that looks payable to anything
303
+ * that checks fewer of them.
304
+ */
305
+ const reopened = current?.resolvedAt != null &&
306
+ next !== TicketStatusValues.RESOLVED &&
307
+ next !== TicketStatusValues.CLOSED;
308
+ if (reopened)
309
+ return { resolvedAt: undefined, resolvedBy: undefined };
244
310
  return {};
245
311
  }
246
312
  /** Append a timeline entry when the patch touches a tracked field. */
@@ -259,7 +325,7 @@ export class SupportRepository extends BaseRepository {
259
325
  trigger: ctx?.trigger ?? defaultTrigger,
260
326
  reason: ctx?.reason,
261
327
  note: ctx?.note,
262
- // `encryptPiiFields` never descends into arrays, so a PII-named value
328
+ // `encryptPiiFields` covers only NAMED paths, so an undeclared PII-named value
263
329
  // reaching `changes` would persist in PLAINTEXT inside statusHistory.
264
330
  piiFields: SUPPORT_TICKET_PII_FIELDS,
265
331
  });
@@ -288,7 +354,7 @@ export class SupportRepository extends BaseRepository {
288
354
  };
289
355
  if (newStatus) {
290
356
  const current = prior !== undefined ? prior : await this.getTicketById(ticketId);
291
- Object.assign(updateData, this.withTicketHistory(current, { status: newStatus, ...this.resolutionStamps(current, newStatus) }, ctx, "addMessage"));
357
+ Object.assign(updateData, this.withTicketHistory(current, { status: newStatus, ...this.resolutionStamps(current, newStatus, resolverUid(ctx)) }, ctx, "addMessage"));
292
358
  }
293
359
  await ref.update(updateData);
294
360
  }
@@ -196,6 +196,23 @@ export interface SupportTicketDocument extends BaseDocument {
196
196
  * `Date` field and split the collection into two shapes.
197
197
  */
198
198
  resolvedAt?: Date;
199
+ /**
200
+ * The uid of whoever moved the ticket to `resolved`.
201
+ *
202
+ * 🛑 **Not `assignedTo`.** That is the CURRENT assignee and moves on
203
+ * reassignment, so paying from it credits whoever holds the ticket at period
204
+ * end rather than whoever did the work. Employee pay is computed from this
205
+ * field; see `resolvedTicketsForPeriod`.
206
+ *
207
+ * Server-stamped, never client-sent, and **cleared on reopen** — pay counts
208
+ * tickets still resolved at period end, so a reopened ticket must stop
209
+ * counting or closing it twice pays twice.
210
+ *
211
+ * Absent on a system transition and on a buyer-driven one: both are real
212
+ * resolutions that earn nobody wages, and a fallback to the assignee would
213
+ * be a wage attributed to someone who did nothing.
214
+ */
215
+ resolvedBy?: string;
199
216
  closedAt?: Date;
200
217
  /** When we acknowledged receipt. Server-stamped at intake. */
201
218
  acknowledgedAt?: Date;