@mohasinac/appkit 4.60.1 → 4.62.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 (202) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +19 -3
  3. package/dist/_internal/client/features/maintenance/views/AnalysisRunnerView.js +1 -1
  4. package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +1 -1
  5. package/dist/_internal/server/features/auctions/actions.d.ts +1 -1
  6. package/dist/_internal/server/features/auctions/actions.js +12 -1
  7. package/dist/_internal/server/features/blog/actions.d.ts +1 -1
  8. package/dist/_internal/server/features/blog/actions.js +12 -1
  9. package/dist/_internal/server/features/brands/actions.d.ts +1 -1
  10. package/dist/_internal/server/features/brands/actions.js +12 -1
  11. package/dist/_internal/server/features/cart/actions.d.ts +1 -1
  12. package/dist/_internal/server/features/cart/actions.js +12 -1
  13. package/dist/_internal/server/features/checkout/actions.js +0 -1
  14. package/dist/_internal/server/features/events/actions.d.ts +1 -1
  15. package/dist/_internal/server/features/events/actions.js +12 -1
  16. package/dist/_internal/server/features/gateway-events/redact.js +14 -2
  17. package/dist/_internal/server/features/history/actions.d.ts +1 -1
  18. package/dist/_internal/server/features/history/actions.js +12 -1
  19. package/dist/_internal/server/features/lottery/actions.d.ts +1 -1
  20. package/dist/_internal/server/features/lottery/actions.js +12 -1
  21. package/dist/_internal/server/features/orders/actions.d.ts +1 -1
  22. package/dist/_internal/server/features/orders/actions.js +12 -1
  23. package/dist/_internal/server/features/orders/cancellation-refund.js +0 -1
  24. package/dist/_internal/server/features/orders/service.js +31 -1
  25. package/dist/_internal/server/features/payouts/actions.d.ts +1 -1
  26. package/dist/_internal/server/features/payouts/actions.js +12 -1
  27. package/dist/_internal/server/features/points/award.js +9 -1
  28. package/dist/_internal/server/features/pre-orders/actions.d.ts +1 -1
  29. package/dist/_internal/server/features/pre-orders/actions.js +12 -1
  30. package/dist/_internal/server/features/prize-draws/actions.d.ts +1 -1
  31. package/dist/_internal/server/features/prize-draws/actions.js +12 -1
  32. package/dist/_internal/server/features/products/actions.d.ts +1 -1
  33. package/dist/_internal/server/features/products/actions.js +12 -1
  34. package/dist/_internal/server/features/promotions/actions.d.ts +1 -1
  35. package/dist/_internal/server/features/promotions/actions.js +12 -1
  36. package/dist/_internal/server/features/protection-claims/actions.d.ts +2 -2
  37. package/dist/_internal/server/features/protection-claims/actions.js +21 -12
  38. package/dist/_internal/server/features/raffle/actions.d.ts +1 -1
  39. package/dist/_internal/server/features/raffle/actions.js +12 -1
  40. package/dist/_internal/server/features/refunds/actions.d.ts +1 -3
  41. package/dist/_internal/server/features/refunds/actions.js +14 -2
  42. package/dist/_internal/server/features/reviews/actions.d.ts +1 -1
  43. package/dist/_internal/server/features/reviews/actions.js +12 -1
  44. package/dist/_internal/server/features/wishlist/actions.d.ts +1 -1
  45. package/dist/_internal/server/features/wishlist/actions.js +12 -1
  46. package/dist/_internal/server/jobs/core/obligationSweep.js +0 -1
  47. package/dist/_internal/server/jobs/core/paymentReconcile.js +93 -76
  48. package/dist/_internal/server/jobs/core/testDataCleanup.d.ts +1 -1
  49. package/dist/_internal/server/jobs/core/testDataCleanup.js +22 -1
  50. package/dist/_internal/server/jobs/handlers/onShipmentAllocationSync.js +16 -4
  51. package/dist/_internal/shared/actions/action-registry.js +149 -9
  52. package/dist/_internal/shared/features/promotions/schema.d.ts +66 -0
  53. package/dist/_internal/shared/features/promotions/schema.js +7 -0
  54. package/dist/_internal/shared/media/limits.d.ts +10 -0
  55. package/dist/_internal/shared/media/limits.js +10 -0
  56. package/dist/_internal/shared/obligations/deadlines.js +11 -1
  57. package/dist/_internal/shared/tester/window.d.ts +25 -0
  58. package/dist/_internal/shared/tester/window.js +66 -0
  59. package/dist/_internal/shared/types/action-result.js +15 -1
  60. package/dist/client/api/surface-error.js +16 -2
  61. package/dist/client/api/useApiMutation.js +17 -1
  62. package/dist/client.d.ts +1 -1
  63. package/dist/client.js +1 -1
  64. package/dist/constants/api-endpoints.d.ts +3 -0
  65. package/dist/constants/api-endpoints.js +1 -0
  66. package/dist/constants/field-names.d.ts +17 -0
  67. package/dist/constants/field-names.js +23 -0
  68. package/dist/core/server-action.js +19 -3
  69. package/dist/core/unit-of-work.js +15 -1
  70. package/dist/errors/error-mapping.js +26 -1
  71. package/dist/errors/firestore-codes.d.ts +37 -0
  72. package/dist/errors/firestore-codes.js +52 -0
  73. package/dist/errors/index.d.ts +0 -1
  74. package/dist/errors/index.js +12 -2
  75. package/dist/features/account/components/UserOffersPanel.js +5 -3
  76. package/dist/features/admin/components/AdminActionIndexView.js +100 -5
  77. package/dist/features/admin/components/AdminAddressClustersView.js +21 -2
  78. package/dist/features/admin/components/AdminBlogEditorView.js +8 -8
  79. package/dist/features/admin/components/AdminCarouselEditorView.js +1 -1
  80. package/dist/features/admin/components/AdminCarouselView.js +96 -4
  81. package/dist/features/admin/components/AdminCouponEditorView.js +8 -0
  82. package/dist/features/admin/components/AdminFulfillmentView.js +3 -2
  83. package/dist/features/admin/components/AdminGatewayEventsView.js +2 -2
  84. package/dist/features/admin/components/AdminMediaView.js +36 -3
  85. package/dist/features/admin/components/AdminNavigationView.js +4 -1
  86. package/dist/features/admin/components/AdminOffersView.js +2 -1
  87. package/dist/features/admin/components/AdminPaymentClustersView.js +19 -2
  88. package/dist/features/admin/components/AdminProductEditorView.js +2 -2
  89. package/dist/features/admin/components/AdminSectionsView.d.ts +10 -0
  90. package/dist/features/admin/components/AdminSectionsView.js +486 -224
  91. package/dist/features/admin/components/DataListingView.js +2 -0
  92. package/dist/features/admin/components/ReferenceRecordEditor.js +1 -1
  93. package/dist/features/admin/components/analytics/AdminStatCard.js +4 -1
  94. package/dist/features/admin/hooks/useOptimisticRowFlag.d.ts +33 -0
  95. package/dist/features/admin/hooks/useOptimisticRowFlag.js +87 -0
  96. package/dist/features/admin/repository/notification.repository.d.ts +15 -0
  97. package/dist/features/admin/repository/notification.repository.js +32 -0
  98. package/dist/features/admin/repository/site-settings.repository.d.ts +2 -1
  99. package/dist/features/admin/repository/site-settings.repository.js +7 -0
  100. package/dist/features/admin/schemas/action-index-form.d.ts +34 -0
  101. package/dist/features/admin/schemas/action-index-form.js +115 -0
  102. package/dist/features/auctions/components/MarketplaceAuctionCard.js +1 -1
  103. package/dist/features/auth/components/RegisterForm.js +2 -1
  104. package/dist/features/catalogue/actions/approve-catalogue-listing.d.ts +1 -1
  105. package/dist/features/catalogue/actions/approve-catalogue-listing.js +12 -1
  106. package/dist/features/catalogue/actions/list-from-catalogue.d.ts +1 -1
  107. package/dist/features/catalogue/actions/list-from-catalogue.js +12 -1
  108. package/dist/features/catalogue/actions/submit-for-approval.d.ts +1 -1
  109. package/dist/features/catalogue/actions/submit-for-approval.js +12 -1
  110. package/dist/features/catalogue/components/CatalogueItemEditorView.js +3 -3
  111. package/dist/features/categories/repository/price-observation.repository.d.ts +19 -0
  112. package/dist/features/categories/repository/price-observation.repository.js +21 -2
  113. package/dist/features/events/actions/event-actions.js +4 -1
  114. package/dist/features/events/components/AdminEventEditorView.js +79 -50
  115. package/dist/features/events/components/EventsListPageView.js +2 -1
  116. package/dist/features/events/schemas/firestore.d.ts +17 -0
  117. package/dist/features/events/schemas/firestore.js +1 -0
  118. package/dist/features/events/utils/sort-pinned.d.ts +33 -0
  119. package/dist/features/events/utils/sort-pinned.js +46 -0
  120. package/dist/features/gateway-events/repository/gateway-events.repository.js +3 -40
  121. package/dist/features/gateway-events/schemas/index.d.ts +2 -2
  122. package/dist/features/inventory/components/AdminStorageBoxesView.js +55 -39
  123. package/dist/features/inventory/components/ReceiveStationView.js +61 -17
  124. package/dist/features/inventory/repository/storage-boxes.repository.d.ts +3 -1
  125. package/dist/features/inventory/repository/storage-boxes.repository.js +36 -3
  126. package/dist/features/layout/hooks/useFormBottomActions.js +20 -1
  127. package/dist/features/obligations/repository/obligations.repository.js +10 -1
  128. package/dist/features/orders/components/EmiInstallmentsPanel.js +25 -7
  129. package/dist/features/orders/components/RefundRequestView.js +4 -4
  130. package/dist/features/orders/schemas/firestore.d.ts +19 -0
  131. package/dist/features/orders/schemas/firestore.js +19 -0
  132. package/dist/features/orders/schemas/index.d.ts +6 -6
  133. package/dist/features/payment-cases/repository/payment-cases.repository.d.ts +1 -0
  134. package/dist/features/payment-cases/repository/payment-cases.repository.js +3 -14
  135. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
  136. package/dist/features/products/components/GroupMemberPicker.js +4 -1
  137. package/dist/features/products/components/MarketplaceBundleCard.js +1 -1
  138. package/dist/features/products/components/MarketplacePrizeDrawCard.js +1 -1
  139. package/dist/features/products/components/PrizeDrawEntryActions.js +2 -1
  140. package/dist/features/products/components/ProductDetailActions.js +2 -1
  141. package/dist/features/products/components/ProductForm.js +2 -2
  142. package/dist/features/products/components/ProductGrid.js +1 -1
  143. package/dist/features/promotions/repository/claimed-coupons.repository.js +31 -1
  144. package/dist/features/promotions/schemas/firestore.d.ts +22 -0
  145. package/dist/features/promotions/schemas/firestore.js +22 -0
  146. package/dist/features/promotions/schemas/index.d.ts +479 -6
  147. package/dist/features/promotions/schemas/index.js +153 -0
  148. package/dist/features/protection-claims/repository/protection-claims.repository.js +22 -15
  149. package/dist/features/seller/components/FulfillmentView.js +3 -2
  150. package/dist/features/seller/components/SellerAddressesView.js +32 -4
  151. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +62 -3
  152. package/dist/features/seller/components/SellerBundlesView.js +53 -14
  153. package/dist/features/seller/components/SellerGroupedListingsView.js +23 -3
  154. package/dist/features/seller/components/SellerListingTypeView.js +56 -10
  155. package/dist/features/seller/components/SellerPayoutMethodsView.js +33 -1
  156. package/dist/features/seller/components/SellerPayoutSettingsView.js +48 -9
  157. package/dist/features/seller/components/SellerProductsView.js +5 -1
  158. package/dist/features/seller/components/SellerShippingConfigsView.js +33 -1
  159. package/dist/features/seller/components/SellerShippingView.js +2 -1
  160. package/dist/features/seller/components/SellerStoreCategoriesView.js +33 -1
  161. package/dist/features/seller/components/SellerSupportView.js +69 -2
  162. package/dist/features/seller/schemas/coupon-form.d.ts +5 -0
  163. package/dist/features/seller/schemas/coupon-form.js +18 -0
  164. package/dist/features/shell/SectionForm.js +35 -9
  165. package/dist/features/shell/build-sections.js +14 -1
  166. package/dist/features/shipments/components/AdminShipmentLotItemsView.js +54 -1
  167. package/dist/features/shipments/components/AdminShipmentProjectionsView.js +3 -2
  168. package/dist/features/sticker-trace/components/StickerCommitPanel.js +2 -1
  169. package/dist/features/sticker-trace/components/StickerPrintPanel.js +2 -1
  170. package/dist/features/sticker-trace/components/StickerRetryPanel.js +24 -4
  171. package/dist/features/sticker-trace/components/StickerTraceStudio.js +2 -1
  172. package/dist/features/sticker-trace/server.d.ts +2 -0
  173. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +3 -2
  174. package/dist/http/api-handler.js +4 -1
  175. package/dist/index.d.ts +8 -4
  176. package/dist/index.js +6 -4
  177. package/dist/next/routing/route-map.d.ts +4 -2
  178. package/dist/next/routing/route-map.js +19 -1
  179. package/dist/schemas/registry.d.ts +15 -10
  180. package/dist/schemas/sieve.d.ts +3 -3
  181. package/dist/server.d.ts +1 -2
  182. package/dist/server.js +6 -4
  183. package/dist/styles.css +51 -0
  184. package/dist/ui/components/Checkbox.js +10 -2
  185. package/dist/ui/components/DateInput.js +8 -1
  186. package/dist/ui/components/Input.js +22 -2
  187. package/dist/ui/components/PaginatedSelect.d.ts +8 -19
  188. package/dist/ui/components/PaginatedSelect.js +0 -0
  189. package/dist/ui/components/Radio.js +8 -1
  190. package/dist/ui/components/Select.js +10 -2
  191. package/dist/ui/components/TagInput.d.ts +8 -1
  192. package/dist/ui/components/TagInput.js +4 -3
  193. package/dist/ui/components/Textarea.js +10 -2
  194. package/dist/ui/forms/FieldCheckbox.js +1 -1
  195. package/dist/ui/forms/FormErrorList.d.ts +0 -16
  196. package/dist/ui/forms/FormErrorList.js +34 -1
  197. package/dist/ui/forms/apply-zod-issues.d.ts +19 -1
  198. package/dist/ui/forms/apply-zod-issues.js +23 -1
  199. package/package.json +1 -1
  200. package/scripts/seed-cli.mjs +1822 -1812
  201. package/dist/features/tester/utils/phases.d.ts +0 -48
  202. package/dist/features/tester/utils/phases.js +0 -139
@@ -36,6 +36,25 @@ export declare const createCouponSchema: z.ZodObject<{
36
36
  minPurchase?: number | undefined;
37
37
  maxDiscount?: number | undefined;
38
38
  }>;
39
+ bxgy: z.ZodOptional<z.ZodObject<{
40
+ buyQuantity: z.ZodNumber;
41
+ getQuantity: z.ZodNumber;
42
+ maxItemPrice: z.ZodOptional<z.ZodEffects<z.ZodEffects<z.ZodNumber, number, number>, number, number>>;
43
+ applicableProducts: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
44
+ applicableCategories: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
45
+ }, "strip", z.ZodTypeAny, {
46
+ buyQuantity: number;
47
+ getQuantity: number;
48
+ maxItemPrice?: number | undefined;
49
+ applicableProducts?: string[] | undefined;
50
+ applicableCategories?: string[] | undefined;
51
+ }, {
52
+ buyQuantity: number;
53
+ getQuantity: number;
54
+ maxItemPrice?: number | undefined;
55
+ applicableProducts?: string[] | undefined;
56
+ applicableCategories?: string[] | undefined;
57
+ }>>;
39
58
  usage: z.ZodObject<{
40
59
  totalLimit: z.ZodOptional<z.ZodNumber>;
41
60
  perUserLimit: z.ZodDefault<z.ZodNumber>;
@@ -91,6 +110,13 @@ export declare const createCouponSchema: z.ZodObject<{
91
110
  endDate: string;
92
111
  startDate: string;
93
112
  };
113
+ bxgy?: {
114
+ buyQuantity: number;
115
+ getQuantity: number;
116
+ maxItemPrice?: number | undefined;
117
+ applicableProducts?: string[] | undefined;
118
+ applicableCategories?: string[] | undefined;
119
+ } | undefined;
94
120
  restrictions?: {
95
121
  firstTimeUserOnly: boolean;
96
122
  applicableProducts?: string[] | undefined;
@@ -115,6 +141,13 @@ export declare const createCouponSchema: z.ZodObject<{
115
141
  startDate: string;
116
142
  isActive?: boolean | undefined;
117
143
  };
144
+ bxgy?: {
145
+ buyQuantity: number;
146
+ getQuantity: number;
147
+ maxItemPrice?: number | undefined;
148
+ applicableProducts?: string[] | undefined;
149
+ applicableCategories?: string[] | undefined;
150
+ } | undefined;
118
151
  restrictions?: {
119
152
  firstTimeUserOnly?: boolean | undefined;
120
153
  applicableProducts?: string[] | undefined;
@@ -139,6 +172,25 @@ export declare const updateCouponSchema: z.ZodObject<Omit<{
139
172
  minPurchase?: number | undefined;
140
173
  maxDiscount?: number | undefined;
141
174
  }>>;
175
+ bxgy: z.ZodOptional<z.ZodOptional<z.ZodObject<{
176
+ buyQuantity: z.ZodNumber;
177
+ getQuantity: z.ZodNumber;
178
+ maxItemPrice: z.ZodOptional<z.ZodEffects<z.ZodEffects<z.ZodNumber, number, number>, number, number>>;
179
+ applicableProducts: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
180
+ applicableCategories: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
181
+ }, "strip", z.ZodTypeAny, {
182
+ buyQuantity: number;
183
+ getQuantity: number;
184
+ maxItemPrice?: number | undefined;
185
+ applicableProducts?: string[] | undefined;
186
+ applicableCategories?: string[] | undefined;
187
+ }, {
188
+ buyQuantity: number;
189
+ getQuantity: number;
190
+ maxItemPrice?: number | undefined;
191
+ applicableProducts?: string[] | undefined;
192
+ applicableCategories?: string[] | undefined;
193
+ }>>>;
142
194
  usage: z.ZodOptional<z.ZodObject<{
143
195
  totalLimit: z.ZodOptional<z.ZodNumber>;
144
196
  perUserLimit: z.ZodDefault<z.ZodNumber>;
@@ -184,6 +236,13 @@ export declare const updateCouponSchema: z.ZodObject<Omit<{
184
236
  minPurchase: number;
185
237
  maxDiscount?: number | undefined;
186
238
  } | undefined;
239
+ bxgy?: {
240
+ buyQuantity: number;
241
+ getQuantity: number;
242
+ maxItemPrice?: number | undefined;
243
+ applicableProducts?: string[] | undefined;
244
+ applicableCategories?: string[] | undefined;
245
+ } | undefined;
187
246
  usage?: {
188
247
  perUserLimit: number;
189
248
  totalLimit?: number | undefined;
@@ -207,6 +266,13 @@ export declare const updateCouponSchema: z.ZodObject<Omit<{
207
266
  minPurchase?: number | undefined;
208
267
  maxDiscount?: number | undefined;
209
268
  } | undefined;
269
+ bxgy?: {
270
+ buyQuantity: number;
271
+ getQuantity: number;
272
+ maxItemPrice?: number | undefined;
273
+ applicableProducts?: string[] | undefined;
274
+ applicableCategories?: string[] | undefined;
275
+ } | undefined;
210
276
  usage?: {
211
277
  totalLimit?: number | undefined;
212
278
  perUserLimit?: number | undefined;
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { COUPON_CODE_PATTERN } from "./config.js";
3
+ import { bxgyConfigSchema } from "../../../../features/promotions/schemas/index.js";
3
4
  export const applyCouponSchema = z.object({
4
5
  code: z.string().regex(COUPON_CODE_PATTERN, "Invalid coupon code format").toUpperCase(),
5
6
  cartTotal: z.number().min(0),
@@ -17,6 +18,12 @@ export const createCouponSchema = z.object({
17
18
  maxDiscount: z.number().min(0).optional(),
18
19
  minPurchase: z.number().min(0).default(0),
19
20
  }),
21
+ /*
22
+ * 🛑 Without this, `z.object()` strips the config the `buy_x_get_y` member
23
+ * of the enum above depends on — see the fuller note on `bxgyConfigSchema`.
24
+ * Imported rather than restated so all five write paths move together.
25
+ */
26
+ bxgy: bxgyConfigSchema.optional(),
20
27
  usage: z.object({
21
28
  totalLimit: z.number().int().min(1).optional(),
22
29
  perUserLimit: z.number().int().min(1).default(1),
@@ -27,6 +27,16 @@ export type MediaKind = "image" | "video" | "pdf";
27
27
  export declare const PRODUCT_MAX_IMAGES = 24;
28
28
  /** Videos per listing. Bounded by `ProductDocument.video` being singular. */
29
29
  export declare const PRODUCT_MAX_VIDEOS = 1;
30
+ /**
31
+ * Photos a buyer may attach to one review.
32
+ *
33
+ * 🛑 Deliberately NOT `PRODUCT_MAX_IMAGES`, which is 24 and is a SELLER's
34
+ * ceiling for a whole listing. `audit-media-limit-literals` suggests that
35
+ * constant for any `images: z.array(…).max(n)`, and taking the suggestion
36
+ * here would have quadrupled what a review accepts — the right shape, the
37
+ * wrong number. A review is a few photos of one thing.
38
+ */
39
+ export declare const REVIEW_MAX_IMAGES = 6;
30
40
  /**
31
41
  * Highest legal `index` in a product media filename.
32
42
  *
@@ -41,6 +41,16 @@ export const MAX_VIDEO_BYTES = 50 * MEGABYTE;
41
41
  export const PRODUCT_MAX_IMAGES = 24;
42
42
  /** Videos per listing. Bounded by `ProductDocument.video` being singular. */
43
43
  export const PRODUCT_MAX_VIDEOS = 1;
44
+ /**
45
+ * Photos a buyer may attach to one review.
46
+ *
47
+ * 🛑 Deliberately NOT `PRODUCT_MAX_IMAGES`, which is 24 and is a SELLER's
48
+ * ceiling for a whole listing. `audit-media-limit-literals` suggests that
49
+ * constant for any `images: z.array(…).max(n)`, and taking the suggestion
50
+ * here would have quadrupled what a review accepts — the right shape, the
51
+ * wrong number. A review is a few photos of one thing.
52
+ */
53
+ export const REVIEW_MAX_IMAGES = 6;
44
54
  /**
45
55
  * Highest legal `index` in a product media filename.
46
56
  *
@@ -33,7 +33,17 @@
33
33
  import { addBusinessDays } from "../time/business-days.js";
34
34
  import { OBLIGATION_POLICIES, durationToMinutes, } from "../../../features/obligations/constants/policies.js";
35
35
  import { ALL_OBLIGATION_KINDS, } from "../../../features/obligations/schemas/firestore.js";
36
- import { windowMinutes } from "../../../features/tester/seed-data/tester-window.js";
36
+ /*
37
+ * 🛑 From _internal/shared/tester, NOT features/tester/seed-data.
38
+ *
39
+ * appkit/package.json excludes "!dist/features/tester/seed-data" from the
40
+ * published tarball, so this production module importing from there left the
41
+ * package with a DANGLING import: it resolved locally (the dir exists in a
42
+ * local build) and broke the consumer Turbopack build, which installs the
43
+ * tarball — "Module not found: tester-window.js". Nothing in appkit could see
44
+ * it; only a real deploy could.
45
+ */
46
+ import { windowMinutes } from "../tester/window.js";
37
47
  /**
38
48
  * How much a test fixture's deadlines are compressed.
39
49
  *
@@ -0,0 +1,25 @@
1
+ /** Reproduces the historical 1h/2h/3h auction stagger when nothing overrides it. */
2
+ export declare const DEFAULT_TESTER_WINDOW_MINUTES = 180;
3
+ /** Below this nothing is observable — a page load alone can outlast it. */
4
+ export declare const MIN_TESTER_WINDOW_MINUTES = 5;
5
+ /** The sandbox TTL is 7 days; a window longer than that outlives its own fixtures. */
6
+ export declare const MAX_TESTER_WINDOW_MINUTES: number;
7
+ /**
8
+ * Length of the testing window, in minutes.
9
+ *
10
+ * Read at call time rather than captured at module scope so a caller that sets the
11
+ * env var before importing a seed file still wins. Anything unparseable, non-finite
12
+ * or out of range falls back to the default rather than throwing: a malformed env
13
+ * var must not be able to brick `appkit-seed load`.
14
+ */
15
+ export declare function windowMinutes(): number;
16
+ /**
17
+ * A point `fraction` of the way through the window, measured from now.
18
+ *
19
+ * `windowOffset(0)` is now, `windowOffset(1)` is the window's end. Fractions above 1
20
+ * are allowed and mean "after this run" — that is how a fixture says "still open when
21
+ * testing finishes" without hard-coding a duration.
22
+ */
23
+ export declare function windowOffset(fraction: number): Date;
24
+ /** The mirror of windowOffset, into the past — for fixtures that must already be closed. */
25
+ export declare function windowAgo(fraction: number): Date;
@@ -0,0 +1,66 @@
1
+ /*
2
+ * WHY: Every time-bound tester fixture — auction end dates, coupon validity, event
3
+ * start/end, prize-draw reveal windows, offer expiry, lottery draw windows —
4
+ * must open and close INSIDE one testing session. Hand-written offsets like
5
+ * `Date.now() + 2 * 60 * 60 * 1000` cannot do that: they bake in a duration
6
+ * nobody can shorten, so a case reading "watch the auction actually end" is
7
+ * untestable without sitting there for two hours.
8
+ *
9
+ * This file is the one knob. Fixtures express WHEN as a fraction of the run
10
+ * window; the window's real length comes from TESTER_WINDOW_MINUTES.
11
+ *
12
+ * WHAT: windowMinutes() / windowOffset(fraction) / windowAgo(fraction).
13
+ *
14
+ * 🛑 The default is 180 minutes ON PURPOSE — it reproduces the pre-existing 1h/2h/3h
15
+ * auction stagger exactly (fractions 1/3, 2/3, 1), so an ordinary `appkit-seed load`
16
+ * behaves as it always has. Only the automated runner shortens it. Changing this
17
+ * default silently re-times every fixture for human testers too.
18
+ *
19
+ * @tag domain:tester
20
+ * @tag layer:seed
21
+ * @tag pattern:none
22
+ * @tag access:server-only
23
+ * @tag consumers:seed-data/*-tester-seed-data.ts
24
+ * @tag sideEffects:none
25
+ */
26
+ /** Reproduces the historical 1h/2h/3h auction stagger when nothing overrides it. */
27
+ export const DEFAULT_TESTER_WINDOW_MINUTES = 180;
28
+ /** Below this nothing is observable — a page load alone can outlast it. */
29
+ export const MIN_TESTER_WINDOW_MINUTES = 5;
30
+ /** The sandbox TTL is 7 days; a window longer than that outlives its own fixtures. */
31
+ export const MAX_TESTER_WINDOW_MINUTES = 7 * 24 * 60;
32
+ /**
33
+ * Length of the testing window, in minutes.
34
+ *
35
+ * Read at call time rather than captured at module scope so a caller that sets the
36
+ * env var before importing a seed file still wins. Anything unparseable, non-finite
37
+ * or out of range falls back to the default rather than throwing: a malformed env
38
+ * var must not be able to brick `appkit-seed load`.
39
+ */
40
+ export function windowMinutes() {
41
+ const raw = process.env.TESTER_WINDOW_MINUTES;
42
+ if (!raw)
43
+ return DEFAULT_TESTER_WINDOW_MINUTES;
44
+ const parsed = Number(raw);
45
+ if (!Number.isFinite(parsed))
46
+ return DEFAULT_TESTER_WINDOW_MINUTES;
47
+ if (parsed < MIN_TESTER_WINDOW_MINUTES)
48
+ return MIN_TESTER_WINDOW_MINUTES;
49
+ if (parsed > MAX_TESTER_WINDOW_MINUTES)
50
+ return MAX_TESTER_WINDOW_MINUTES;
51
+ return parsed;
52
+ }
53
+ /**
54
+ * A point `fraction` of the way through the window, measured from now.
55
+ *
56
+ * `windowOffset(0)` is now, `windowOffset(1)` is the window's end. Fractions above 1
57
+ * are allowed and mean "after this run" — that is how a fixture says "still open when
58
+ * testing finishes" without hard-coding a duration.
59
+ */
60
+ export function windowOffset(fraction) {
61
+ return new Date(Date.now() + fraction * windowMinutes() * 60000);
62
+ }
63
+ /** The mirror of windowOffset, into the past — for fixtures that must already be closed. */
64
+ export function windowAgo(fraction) {
65
+ return windowOffset(-fraction);
66
+ }
@@ -1,5 +1,6 @@
1
1
  import { normalizeError } from "../../../errors/normalize.js";
2
2
  import { mapToHttpError } from "../../../errors/error-mapping.js";
3
+ import { toApiIssues } from "../../../errors/error-envelope.js";
3
4
  import { describeCauseChain } from "../../../errors/describe-cause.js";
4
5
  import { reportActionError } from "./action-error-reporter.js";
5
6
  /**
@@ -57,7 +58,20 @@ export async function wrapAction(fn) {
57
58
  ok: false,
58
59
  code: mapped.code,
59
60
  error: mapped.message,
60
- ...(mapped.issues ? { issues: mapped.issues } : {}),
61
+ /*
62
+ * 🛑 `toApiIssues`, not the raw issues — this was the ONE producer in
63
+ * the codebase that skipped the scrubber, and it is the one with 221
64
+ * call sites.
65
+ *
66
+ * A raw `ZodIssue` carries `received`: the value the user actually
67
+ * submitted. On a failed signup, address save or payout-details save
68
+ * that is an email, a phone number or a bank account, echoed straight
69
+ * back into the client payload. `toApiIssues` narrows each issue to
70
+ * `{ message, path?, code? }`, which is exactly why it exists — every
71
+ * other producer (`routeHandler`, `createApiHandler`, the offer
72
+ * actions) already routes through it.
73
+ */
74
+ ...(mapped.issues ? { issues: toApiIssues(mapped.issues) } : {}),
61
75
  };
62
76
  }
63
77
  }
@@ -1,6 +1,6 @@
1
1
  import { isApiError } from "./ApiError.js";
2
2
  import { ApiClientError } from "../../http/ApiClient.js";
3
- import { applyZodIssues, hasAttachableIssue } from "../../ui/forms/apply-zod-issues.js";
3
+ import { applyZodIssues, hasAttachableIssue, rootLevelMessages } from "../../ui/forms/apply-zod-issues.js";
4
4
  import { normalizeError } from "../../errors/normalize.js";
5
5
  import { getErrorDisplay, toUserMessage } from "../../errors/error-display-map.js";
6
6
  import { HTTP_ERROR_CODES } from "../../errors/error-mapping.js";
@@ -36,7 +36,21 @@ export function surfaceError(err, opts) {
36
36
  const issues = issuesOf(err);
37
37
  if (setFieldError && hasAttachableIssue(issues)) {
38
38
  applyZodIssues(issues, (name, message) => setFieldError(name, message ?? ""));
39
- return; // inline errors only — no toast
39
+ /*
40
+ * 🛑 Root-level issues are NOT covered by the line above and must still be
41
+ * surfaced, or a cross-field rule fails silently.
42
+ *
43
+ * `applyZodIssues` skips any issue with an empty `path` — there is no
44
+ * field to put it on. For a mixed response (one field error plus one
45
+ * cross-field `.refine()`, the shape of "passwords must match" or "end
46
+ * date must be after start date") `hasAttachableIssue` is true, so we used
47
+ * to take this branch and `return` with the refine message discarded: the
48
+ * save was refused and nothing on screen said why.
49
+ */
50
+ for (const message of rootLevelMessages(issues)) {
51
+ showToast(message, "error");
52
+ }
53
+ return; // field errors inline; only the unattachable ones toast
40
54
  }
41
55
  if (hasStableErrorCode(err)) {
42
56
  const display = getErrorDisplay(err.code);
@@ -3,6 +3,7 @@ import * as React from "react";
3
3
  import { useMutation, } from "@tanstack/react-query";
4
4
  import { useToast } from "../../ui/components/Toast.js";
5
5
  import { surfaceError } from "./surface-error.js";
6
+ import { toUserMessage } from "../../errors/error-display-map.js";
6
7
  export function useApiMutation(options) {
7
8
  const { showToast, updateToast } = useToast();
8
9
  const loadingIdRef = React.useRef(null);
@@ -40,7 +41,22 @@ export function useApiMutation(options) {
40
41
  onSettled: (data, err, variables, context, ctx) => {
41
42
  if (loadingIdRef.current) {
42
43
  if (err) {
43
- updateToast(loadingIdRef.current, "error", err instanceof Error ? err.message : "Action failed");
44
+ /*
45
+ * 🛑 This was `err instanceof Error ? err.message : "Action failed"`
46
+ * — verbatim the inverted ternary `audit-raw-error-text`'s own
47
+ * header calls "the dominant shape", sitting two lines below the
48
+ * correct `surfaceError` call in the same hook. The authored
49
+ * sentence showed only when the value was NOT an Error, i.e. almost
50
+ * never, so the normal case published the raw message.
51
+ *
52
+ * It was dormant only because `loadingMessage` currently has no
53
+ * call sites; the first consumer to set one would have got two
54
+ * toasts, one of them raw. `updateToast` is also absent from that
55
+ * audit's SINKS list, so nothing would have caught it.
56
+ */
57
+ updateToast(loadingIdRef.current, "error", toUserMessage(err?.code, undefined, {
58
+ fallback: "Action failed",
59
+ }));
44
60
  }
45
61
  else if (!successMessage) {
46
62
  updateToast(loadingIdRef.current, "success", "Done");
package/dist/client.d.ts CHANGED
@@ -455,7 +455,7 @@ export type { OwnerQuickActionsProps, OwnerQuickActionsResource } from "./featur
455
455
  export type { StoreListingEntitlements } from "./features/seller/hooks/useStoreListingEntitlements";
456
456
  export type { ListingTypePlugin } from "./_internal/shared/listing-types/_registry";
457
457
  export type { ListingType } from "./features/products/types/index";
458
- export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, IMAGE_ACCEPT_ATTR, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
458
+ export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, IMAGE_ACCEPT_ATTR, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, REVIEW_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
459
459
  export type { MediaKind, AllowedImageMime, AllowedVideoMime, AllowedDocMime, AllowedMime, } from "./_internal/shared/media/limits";
460
460
  export { ScamAwarenessModal } from "./features/scams/components/ScamAwarenessModal";
461
461
  export type { ScamAwarenessModalProps } from "./features/scams/components/ScamAwarenessModal";
package/dist/client.js CHANGED
@@ -493,7 +493,7 @@ export { YourPurchaseStrip } from "./features/orders/components/YourPurchaseStri
493
493
  export { ReviewFormDrawer, reviewFormSchema } from "./features/reviews/components/ReviewFormDrawer.js";
494
494
  export { PurchaseReviewAffordance } from "./features/reviews/components/PurchaseReviewAffordance.js";
495
495
  // Media upload limits — shared by client uploaders + server sign/finalize routes.
496
- export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, IMAGE_ACCEPT_ATTR, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits.js";
496
+ export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, IMAGE_ACCEPT_ATTR, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, REVIEW_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits.js";
497
497
  export { ScamAwarenessModal } from "./features/scams/components/ScamAwarenessModal.js";
498
498
  export { SellerTrustBadge } from "./features/scams/components/SellerTrustBadge.js";
499
499
  // [CLIENT-ONLY] — Admin panel components for action/nav permission management.
@@ -535,6 +535,7 @@ export declare const SELLER_ENDPOINTS: {
535
535
  readonly BUNDLES: "/api/store/bundles";
536
536
  /** Tickets raised ABOUT this store (W11). */
537
537
  readonly SUPPORT: "/api/store/support";
538
+ readonly SUPPORT_TICKET_BY_ID: (id: string) => string;
538
539
  readonly BUNDLE_BY_ID: (id: string) => string;
539
540
  readonly ORDERS_ASSIGN: (orderId: string) => string;
540
541
  readonly PRODUCT_BY_ID: (id: string) => string;
@@ -1061,6 +1062,7 @@ export declare const API_ENDPOINTS: {
1061
1062
  readonly BUNDLES: "/api/store/bundles";
1062
1063
  /** Tickets raised ABOUT this store (W11). */
1063
1064
  readonly SUPPORT: "/api/store/support";
1065
+ readonly SUPPORT_TICKET_BY_ID: (id: string) => string;
1064
1066
  readonly BUNDLE_BY_ID: (id: string) => string;
1065
1067
  readonly ORDERS_ASSIGN: (orderId: string) => string;
1066
1068
  readonly PRODUCT_BY_ID: (id: string) => string;
@@ -1608,6 +1610,7 @@ export declare const API_ROUTES: {
1608
1610
  readonly BUNDLES: "/api/store/bundles";
1609
1611
  /** Tickets raised ABOUT this store (W11). */
1610
1612
  readonly SUPPORT: "/api/store/support";
1613
+ readonly SUPPORT_TICKET_BY_ID: (id: string) => string;
1611
1614
  readonly BUNDLE_BY_ID: (id: string) => string;
1612
1615
  readonly ORDERS_ASSIGN: (orderId: string) => string;
1613
1616
  readonly PRODUCT_BY_ID: (id: string) => string;
@@ -690,6 +690,7 @@ export const SELLER_ENDPOINTS = {
690
690
  BUNDLES: "/api/store/bundles",
691
691
  /** Tickets raised ABOUT this store (W11). */
692
692
  SUPPORT: "/api/store/support",
693
+ SUPPORT_TICKET_BY_ID: (id) => `/api/store/support/${id}`,
693
694
  BUNDLE_BY_ID: (id) => `/api/store/bundles/${id}`,
694
695
  ORDERS_ASSIGN: (orderId) => `/api/store/orders/${orderId}/assign`,
695
696
  PRODUCT_BY_ID: (id) => `/api/store/products/${id}`,
@@ -982,6 +982,23 @@ export declare const COMMON_FIELDS: {
982
982
  readonly IS_ACTIVE: "isActive";
983
983
  readonly ORDER: "order";
984
984
  };
985
+ /**
986
+ * The idempotency rows behind a refund.
987
+ *
988
+ * 🛑 `STATE` is the claim's own lifecycle ("claimed" → settled), NOT a
989
+ * postal state. `audit-route-nav-field-constants` matches a raw field
990
+ * string against every constant that happens to share its VALUE, so it
991
+ * suggested `ADDRESS_FIELDS.STATE` here — right string, wrong concept, and
992
+ * taking the suggestion would have made the query read as address filtering.
993
+ */
994
+ export declare const REFUND_CLAIM_FIELDS: {
995
+ readonly STATE: "state";
996
+ readonly CLAIMED_AT: "claimedAt";
997
+ };
998
+ /** Observed market prices, keyed by the taxonomy node they were seen under. */
999
+ export declare const PRICE_OBSERVATION_FIELDS: {
1000
+ readonly CATEGORY_ID: "categoryId";
1001
+ };
985
1002
  export declare const OAUTH_STATE_VALUES: {
986
1003
  readonly PENDING: "pending";
987
1004
  readonly USED: "used";
@@ -1175,6 +1175,29 @@ export const COMMON_FIELDS = {
1175
1175
  ORDER: "order",
1176
1176
  };
1177
1177
  // ============================================================================
1178
+ // REFUND CLAIMS
1179
+ // ============================================================================
1180
+ /**
1181
+ * The idempotency rows behind a refund.
1182
+ *
1183
+ * 🛑 `STATE` is the claim's own lifecycle ("claimed" → settled), NOT a
1184
+ * postal state. `audit-route-nav-field-constants` matches a raw field
1185
+ * string against every constant that happens to share its VALUE, so it
1186
+ * suggested `ADDRESS_FIELDS.STATE` here — right string, wrong concept, and
1187
+ * taking the suggestion would have made the query read as address filtering.
1188
+ */
1189
+ export const REFUND_CLAIM_FIELDS = {
1190
+ STATE: "state",
1191
+ CLAIMED_AT: "claimedAt",
1192
+ };
1193
+ // ============================================================================
1194
+ // PRICE OBSERVATIONS
1195
+ // ============================================================================
1196
+ /** Observed market prices, keyed by the taxonomy node they were seen under. */
1197
+ export const PRICE_OBSERVATION_FIELDS = {
1198
+ CATEGORY_ID: "categoryId",
1199
+ };
1200
+ // ============================================================================
1178
1201
  // OAUTH STATE VALUES
1179
1202
  // ============================================================================
1180
1203
  export const OAUTH_STATE_VALUES = {
@@ -1,6 +1,6 @@
1
1
  import { normalizeError } from "../errors/normalize.js";
2
2
  import { toApiIssues } from "../errors/error-envelope.js";
3
- import { HTTP_ERROR_CODES } from "../errors/error-mapping.js";
3
+ import { HTTP_ERROR_CODES, mapToHttpError } from "../errors/error-mapping.js";
4
4
  /**
5
5
  * createServerAction — @mohasinac/appkit/core
6
6
  *
@@ -97,11 +97,27 @@ export function createServerAction(options) {
97
97
  }
98
98
  catch (err) {
99
99
  void normalizeError(err);
100
- const message = err instanceof Error ? err.message : "An unexpected error occurred";
100
+ /*
101
+ * 🛑 This never got the Rule #9 fix its sibling `wrapAction` did.
102
+ *
103
+ * It took `err.message` verbatim — unscrubbed for 5xx, so a thrown
104
+ * Node/bundler error's internals reached the client — and emitted NO
105
+ * `code`, so `toUserMessage(result.code)` downstream could only ever
106
+ * resolve to the generic sentence.
107
+ *
108
+ * It has no in-repo callers, but it is exported from the public barrel,
109
+ * so it is a trap for the next consumer rather than a live bug. Route it
110
+ * through the same mapper `wrapAction` uses: `mapToHttpError` scrubs 5xx
111
+ * messages in production and carries the real one in `internalMessage`
112
+ * for recorders only.
113
+ */
114
+ const mapped = mapToHttpError(err, {
115
+ isProduction: process.env.NODE_ENV === "production",
116
+ });
101
117
  logger.error(`[createServerAction] handler error in "${actionName}"`, {
102
118
  err,
103
119
  });
104
- result = { ok: false, error: message };
120
+ result = { ok: false, code: mapped.code, error: mapped.message };
105
121
  }
106
122
  // -- 4. After hooks (per-action → global) ------------------------------
107
123
  try {
@@ -13,7 +13,21 @@ import { normalizeError } from "../errors/normalize.js";
13
13
  // db-firebase reaches firebase-admin only through `import type` plus its own
14
14
  // bare-specifier runtime requires. See CLAUDE.md Root Cause #24.
15
15
  import { getAdminDb } from "../providers/db-firebase/index.js";
16
- import { DatabaseError } from "../errors/index.js";
16
+ /*
17
+ * 🛑 From `errors/database-error`, NEVER the `../errors` barrel.
18
+ *
19
+ * That barrel re-exports `error-handler`, which does
20
+ * `import { NextResponse } from "next/server.js"` — and its own comment above
21
+ * that line says "only import in server/API-route contexts". This module is
22
+ * reachable from the Firebase Functions entry
23
+ * (`jobs.js → repositories → unit-of-work`), so the barrel dragged Next's
24
+ * whole server runtime into the tsup bundle. It failed with four errors that
25
+ * name neither this file nor the barrel: two unresolved
26
+ * `react-server-dom-webpack/*` requires and two `.wasm` files from
27
+ * `@vercel/og`. Nothing in appkit's own typecheck can see it; only
28
+ * `npm --prefix functions run build` can. Root Cause #18.
29
+ */
30
+ import { DatabaseError } from "../errors/database-error.js";
17
31
  import { userRepository, tokenRepository, sessionRepository, } from "../features/auth/repository/index.js";
18
32
  import { addressesRepository } from "../features/addresses/repository/addresses.repository.js";
19
33
  import { cartRepository } from "../features/cart/repository/cart.repository.js";
@@ -42,6 +42,31 @@ function isZodLikeError(err) {
42
42
  "issues" in err &&
43
43
  Array.isArray(err.issues));
44
44
  }
45
+ /**
46
+ * The issues carried by a `ValidationError`, whichever of the two shapes the
47
+ * thrower used.
48
+ *
49
+ * 🛑 `new ValidationError(msg, parsed.error.issues)` silently lost everything.
50
+ * `ValidationError` forwards its second argument to `AppError.data`, and the
51
+ * old test was `isZodLikeError(err.data)` — which requires `"issues" in data`.
52
+ * An ARRAY has no `.issues` property (`"issues" in [] === false`), so passing
53
+ * `parsed.error.issues` (the obvious thing to pass, and what four routes do)
54
+ * produced a 400 with no field information at all. Only passing the whole
55
+ * `ZodError` worked.
56
+ *
57
+ * `media/finalize/route.ts` even carried a comment asserting the opposite —
58
+ * "carries `issues` through to the client, so the caller learns WHICH field it
59
+ * got wrong". It did not.
60
+ *
61
+ * Accept both, so the next caller cannot get it wrong either way.
62
+ */
63
+ function issuesFromErrorData(data) {
64
+ if (Array.isArray(data))
65
+ return data.length > 0 ? data : undefined;
66
+ if (isZodLikeError(data))
67
+ return data.issues;
68
+ return undefined;
69
+ }
45
70
  function isFirestoreLikeError(err) {
46
71
  if (typeof err !== "object" || err === null)
47
72
  return false;
@@ -110,7 +135,7 @@ function classifyError(err, opts) {
110
135
  status: 400,
111
136
  code: HTTP_ERROR_CODES.VALIDATION_FAILED,
112
137
  message: err.message,
113
- issues: isZodLikeError(err.data) ? err.data.issues : undefined,
138
+ issues: issuesFromErrorData(err.data),
114
139
  };
115
140
  }
116
141
  if (err instanceof AuthenticationError) {
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Firestore / gRPC error-code predicates, in one place.
3
+ *
4
+ * ## Why this file exists
5
+ *
6
+ * `isAlreadyExists` had been written THREE times — in the gateway-events,
7
+ * payment-cases and protection-claims repositories, two of them byte-identical
8
+ * — and each copy duck-typed the caught error the same way:
9
+ *
10
+ * const code = (err as { code?: unknown }).code;
11
+ *
12
+ * That is past the Rule of Three, and `audit-unknown-leakage` was reporting all
13
+ * six of those field reads: casting to `{ code?: unknown }` narrows the OBJECT
14
+ * and leaves the FIELD unknown, so every comparison below it is unchecked.
15
+ * Naming the shape once gives the comparisons a real type and leaves one
16
+ * implementation to fix when the SDK changes what it throws.
17
+ *
18
+ * ## 🛑 Why three checks and not one
19
+ *
20
+ * The Admin SDK has surfaced this condition as a numeric gRPC code, as a string
21
+ * code, and as a bare message, depending on version and transport. All three
22
+ * are tested, and the message test is in ADDITION to the codes rather than
23
+ * instead of them — a stringly-typed guard alone is what breaks silently on an
24
+ * SDK bump, and the failure mode is a create that reports success while having
25
+ * collided.
26
+ */
27
+ /** gRPC status 6. The numeric form, which is what the Admin SDK usually sets. */
28
+ export declare const GRPC_ALREADY_EXISTS = 6;
29
+ /**
30
+ * Did Firestore refuse this write because the document already exists?
31
+ *
32
+ * The one question every `createWithId`-style insert needs answered, and the
33
+ * reason it matters is that "already there" is usually SUCCESS for an
34
+ * idempotent writer — a claim, a case or an inbox row that a retry re-sends.
35
+ * Treating it as a failure turns a safe retry into a 500.
36
+ */
37
+ export declare function isAlreadyExistsError(err: unknown): boolean;