@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
@@ -6,7 +6,18 @@
6
6
  * handlers also work in a Vercel lambda (10s cap honoured by raffle pool
7
7
  * size).
8
8
  */
9
- import { wrapAction } from "@mohasinac/appkit/server";
9
+ /*
10
+ * 🛑 From the DEFINING module, never "@mohasinac/appkit/server".
11
+ *
12
+ * appkit importing its own public server barrel is a self-reference that drags
13
+ * the whole server surface in behind it — `dist/server.js` reaches next/og,
14
+ * next/navigation, next/headers and driver.js. Twenty `actions.ts` files did
15
+ * this for two symbols, and because `obligationSweep` reaches one of them, the
16
+ * Firebase Functions tsup bundle (noExternal: appkit) had to resolve Next:
17
+ * it failed on two `react-server-dom-webpack/*` requires and two @vercel/og
18
+ * .wasm files, naming none of the responsible files. Root Cause #18.
19
+ */
20
+ import { wrapAction } from "../../../shared/types/action-result.js";
10
21
  import { getAdminDb } from "../../../../providers/db-firebase/admin.js";
11
22
  import { runTriggerEventRaffle, } from "../../jobs/core/triggerEventRaffle.js";
12
23
  import { runAssignSpinPrize, } from "../../jobs/core/assignSpinPrize.js";
@@ -1,9 +1,7 @@
1
- import { type ActionResult } from "@mohasinac/appkit/server";
2
- import type { RefundType } from "../../../../features/orders/schemas";
1
+ import { type ActionResult } from "../../../shared/types/action-result";
3
2
  import { type ReturnReason } from "../../../shared/features/orders/return-reasons";
4
3
  export type ProcessRefundInput = {
5
4
  orderId: string;
6
- type: RefundType;
7
5
  /** Decimal rupees. Must be > 0 and ≤ remaining refundable amount on the order. */
8
6
  amount: number;
9
7
  /**
@@ -1,5 +1,16 @@
1
1
  "use server";
2
- import { wrapAction } from "@mohasinac/appkit/server";
2
+ /*
3
+ * 🛑 From the DEFINING module, never "@mohasinac/appkit/server".
4
+ *
5
+ * appkit importing its own public server barrel is a self-reference that drags
6
+ * the whole server surface in behind it — `dist/server.js` reaches next/og,
7
+ * next/navigation, next/headers and driver.js. Twenty `actions.ts` files did
8
+ * this for two symbols, and because `obligationSweep` reaches one of them, the
9
+ * Firebase Functions tsup bundle (noExternal: appkit) had to resolve Next:
10
+ * it failed on two `react-server-dom-webpack/*` requires and two @vercel/og
11
+ * .wasm files, naming none of the responsible files. Root Cause #18.
12
+ */
13
+ import { wrapAction } from "../../../shared/types/action-result.js";
3
14
  /**
4
15
  * processRefundAction — append a refund event to an order.
5
16
  *
@@ -235,7 +246,8 @@ export async function processRefundAction(input) {
235
246
  const isFull = wouldBeFullRefund(order, input.amount, siteSettings?.commissions?.gstPercent);
236
247
  const event = {
237
248
  refundId,
238
- type: input.type,
249
+ // Derived from the authoritative `isFull` above, never from a caller.
250
+ type: isFull ? "full" : "partial",
239
251
  amount: input.amount,
240
252
  ...(input.itemIds?.length ? { itemIds: input.itemIds } : {}),
241
253
  reasonCode: input.reasonCode,
@@ -1,4 +1,4 @@
1
- import { type ActionResult } from "@mohasinac/appkit/server";
1
+ import { type ActionResult } from "../../../shared/types/action-result";
2
2
  export declare function createReviewAction(input: unknown): Promise<ActionResult<unknown>>;
3
3
  export declare function replyToReviewAction(input: unknown): Promise<ActionResult<unknown>>;
4
4
  export declare function deleteReviewAction(input: unknown): Promise<ActionResult<unknown>>;
@@ -1,5 +1,16 @@
1
1
  "use server";
2
- import { wrapAction } from "@mohasinac/appkit/server";
2
+ /*
3
+ * 🛑 From the DEFINING module, never "@mohasinac/appkit/server".
4
+ *
5
+ * appkit importing its own public server barrel is a self-reference that drags
6
+ * the whole server surface in behind it — `dist/server.js` reaches next/og,
7
+ * next/navigation, next/headers and driver.js. Twenty `actions.ts` files did
8
+ * this for two symbols, and because `obligationSweep` reaches one of them, the
9
+ * Firebase Functions tsup bundle (noExternal: appkit) had to resolve Next:
10
+ * it failed on two `react-server-dom-webpack/*` requires and two @vercel/og
11
+ * .wasm files, naming none of the responsible files. Root Cause #18.
12
+ */
13
+ import { wrapAction } from "../../../shared/types/action-result.js";
3
14
  import { requireRoleUser } from "../../../../providers/auth-firebase/helpers.js";
4
15
  import { isAdminUser } from "../../../../features/auth/role-predicates.js";
5
16
  import { reviewRepository, productRepository, userRepository, } from "../../../../repositories/index.js";
@@ -1,4 +1,4 @@
1
- import { type ActionResult } from "@mohasinac/appkit/server";
1
+ import { type ActionResult } from "../../../shared/types/action-result";
2
2
  import { type WishlistItem } from "../../../../repositories";
3
3
  export declare function addToWishlistAction(productId: string, extras?: {
4
4
  productType?: WishlistItem["productType"];
@@ -1,6 +1,17 @@
1
1
  "use server";
2
2
  import { normalizeError } from "../../../../errors/normalize.js";
3
- import { wrapAction } from "@mohasinac/appkit/server";
3
+ /*
4
+ * 🛑 From the DEFINING module, never "@mohasinac/appkit/server".
5
+ *
6
+ * appkit importing its own public server barrel is a self-reference that drags
7
+ * the whole server surface in behind it — `dist/server.js` reaches next/og,
8
+ * next/navigation, next/headers and driver.js. Twenty `actions.ts` files did
9
+ * this for two symbols, and because `obligationSweep` reaches one of them, the
10
+ * Firebase Functions tsup bundle (noExternal: appkit) had to resolve Next:
11
+ * it failed on two `react-server-dom-webpack/*` requires and two @vercel/og
12
+ * .wasm files, naming none of the responsible files. Root Cause #18.
13
+ */
14
+ import { wrapAction } from "../../../shared/types/action-result.js";
4
15
  import { requireRoleUser } from "../../../../providers/auth-firebase/helpers.js";
5
16
  import { wishlistRepository, WishlistFullError, } from "../../../../repositories/index.js";
6
17
  import { WISHLIST_MAX } from "../../../shared/features/wishlist/config.js";
@@ -279,7 +279,6 @@ async function performAction(ctx, obligation, order) {
279
279
  }
280
280
  const result = await processRefundAction({
281
281
  orderId: order.id,
282
- type: "full",
283
282
  amount: capacity.remaining,
284
283
  reasonCode: order.returnReasonCode ?? "not_as_described",
285
284
  reasonNote: `Auto-resolved in the buyer's favour — ${policy.describe}`,
@@ -36,7 +36,8 @@ import { gatewayEventsRepository } from "../../../../features/gateway-events/rep
36
36
  import { GATEWAY_EVENT_MAX_ATTEMPTS, } from "../../../../features/gateway-events/schemas/firestore.js";
37
37
  import { paymentCasesRepository } from "../../../../features/payment-cases/repository/payment-cases.repository.js";
38
38
  import { orderRepository } from "../../../../features/orders/repository/orders.repository.js";
39
- import { REFUND_CLAIMS_COLLECTION } from "../../../../features/orders/schemas/firestore.js";
39
+ import { REFUND_CLAIMS_COLLECTION, REFUND_CLAIM_FIELDS, ORDER_COLLECTION, } from "../../../../features/orders/schemas/firestore.js";
40
+ import { ORDER_FIELDS } from "../../../../constants/field-names.js";
40
41
  import { reconcileDisputes } from "../../features/payment-cases/disputes.js";
41
42
  import { refundableBase, committedRefundTotal, MONEY_EPSILON, } from "../../../shared/checkout/refundable-base.js";
42
43
  /** How far back R2 looks. Two days of overlap, so a run that fails is covered by the next. */
@@ -223,76 +224,9 @@ async function reconcileGatewayPayments(ctx) {
223
224
  continue;
224
225
  }
225
226
  for (const order of orders) {
226
- /*
227
- * Amount check. Compared against the order's own recorded total rather
228
- * than recomputed from its lines — recomputing would re-derive the
229
- * pricing pipeline here and disagree with it the day a rate changes,
230
- * which is a sixth copy of the money rule (Root Cause #75).
231
- */
232
- const recorded = order.totalPrice ?? 0;
233
- if (Math.abs(recorded - record.amount) > MONEY_EPSILON) {
234
- amountMismatch += 1;
235
- await paymentCasesRepository().openOrObserve({
236
- kind: "amount_mismatch",
237
- subjectId: `${record.id}:${order.id}`,
238
- severity: "error",
239
- title: `Captured ${record.amount} but recorded ${recorded} — ${order.id}`,
240
- detail: [
241
- `The gateway captured a different amount from the one the order records.`,
242
- `Either the buyer was over-charged or the order under-states what was paid.`,
243
- "",
244
- `Gateway: ${record.gateway}`,
245
- `Payment: ${record.id}`,
246
- `Order: ${order.id}`,
247
- `Captured: ${record.amount} ${record.currency}`,
248
- `Order total: ${recorded}`,
249
- `Difference: ${(record.amount - recorded).toFixed(2)}`,
250
- ].join("\n"),
251
- entityType: "order",
252
- entityId: order.id,
253
- amountAtRisk: Math.abs(record.amount - recorded),
254
- orderIds: [order.id],
255
- gatewayPaymentId: record.id,
256
- now: ctx.now,
257
- });
258
- }
259
- /*
260
- * Refund check — the gateway's `amount_refunded` against our own
261
- * non-failed refund events. `committedRefundTotal` is the same helper
262
- * the refund cap uses, so "what we think we refunded" has ONE
263
- * definition rather than a second one written here.
264
- */
265
- const ours = committedRefundTotal(order);
266
- if (Math.abs(ours - record.amountRefunded) > MONEY_EPSILON) {
267
- refundMismatch += 1;
268
- await paymentCasesRepository().openOrObserve({
269
- kind: "refund_mismatch",
270
- subjectId: `${record.id}:refunds`,
271
- severity: record.amountRefunded > ours ? "error" : "warning",
272
- title: `Refund total disagrees with the gateway — ${order.id}`,
273
- detail: [
274
- record.amountRefunded > ours
275
- ? `🛑 The gateway has refunded MORE than our records show. Money has left that we did not record.`
276
- : `Our records show more refunded than the gateway did — a refund may have been reserved and never completed.`,
277
- "",
278
- `Gateway: ${record.gateway}`,
279
- `Payment: ${record.id}`,
280
- `Order: ${order.id}`,
281
- `Gateway refunded: ${record.amountRefunded}`,
282
- `Our committed refunds: ${ours}`,
283
- `Refundable base: ${refundableBase(order)}`,
284
- record.refundStatus ? `Gateway refund status: ${record.refundStatus}` : null,
285
- ]
286
- .filter((l) => l !== null)
287
- .join("\n"),
288
- entityType: "order",
289
- entityId: order.id,
290
- amountAtRisk: Math.abs(record.amountRefunded - ours),
291
- orderIds: [order.id],
292
- gatewayPaymentId: record.id,
293
- now: ctx.now,
294
- });
295
- }
227
+ const outcome = await reconcilePaymentAgainstOrder(record, order, ctx);
228
+ amountMismatch += outcome.amountMismatch;
229
+ refundMismatch += outcome.refundMismatch;
296
230
  }
297
231
  }
298
232
  catch (err) {
@@ -304,6 +238,89 @@ async function reconcileGatewayPayments(ctx) {
304
238
  }
305
239
  return { examined, missing, amountMismatch, refundMismatch, reads };
306
240
  }
241
+ /**
242
+ * The two per-order checks R2 runs against one gateway record.
243
+ *
244
+ * Extracted so each `openOrObserve` call is a statement in a function body
245
+ * rather than five braces deep inside `for record -> try -> for order -> if`.
246
+ * Returns the counts rather than mutating the caller's accumulators, so the
247
+ * loop above reads as a sum rather than as two side effects.
248
+ */
249
+ async function reconcilePaymentAgainstOrder(record, order, ctx) {
250
+ let amountMismatch = 0;
251
+ let refundMismatch = 0;
252
+ /*
253
+ * Amount check. Compared against the order's own recorded total rather
254
+ * than recomputed from its lines — recomputing would re-derive the
255
+ * pricing pipeline here and disagree with it the day a rate changes,
256
+ * which is a sixth copy of the money rule (Root Cause #75).
257
+ */
258
+ const recorded = order.totalPrice ?? 0;
259
+ if (Math.abs(recorded - record.amount) > MONEY_EPSILON) {
260
+ amountMismatch += 1;
261
+ await paymentCasesRepository().openOrObserve({
262
+ kind: "amount_mismatch",
263
+ subjectId: `${record.id}:${order.id}`,
264
+ severity: "error",
265
+ title: `Captured ${record.amount} but recorded ${recorded} — ${order.id}`,
266
+ detail: [
267
+ `The gateway captured a different amount from the one the order records.`,
268
+ `Either the buyer was over-charged or the order under-states what was paid.`,
269
+ "",
270
+ `Gateway: ${record.gateway}`,
271
+ `Payment: ${record.id}`,
272
+ `Order: ${order.id}`,
273
+ `Captured: ${record.amount} ${record.currency}`,
274
+ `Order total: ${recorded}`,
275
+ `Difference: ${(record.amount - recorded).toFixed(2)}`,
276
+ ].join("\n"),
277
+ entityType: "order",
278
+ entityId: order.id,
279
+ amountAtRisk: Math.abs(record.amount - recorded),
280
+ orderIds: [order.id],
281
+ gatewayPaymentId: record.id,
282
+ now: ctx.now,
283
+ });
284
+ }
285
+ /*
286
+ * Refund check — the gateway's `amount_refunded` against our own
287
+ * non-failed refund events. `committedRefundTotal` is the same helper
288
+ * the refund cap uses, so "what we think we refunded" has ONE
289
+ * definition rather than a second one written here.
290
+ */
291
+ const ours = committedRefundTotal(order);
292
+ if (Math.abs(ours - record.amountRefunded) > MONEY_EPSILON) {
293
+ refundMismatch += 1;
294
+ await paymentCasesRepository().openOrObserve({
295
+ kind: "refund_mismatch",
296
+ subjectId: `${record.id}:refunds`,
297
+ severity: record.amountRefunded > ours ? "error" : "warning",
298
+ title: `Refund total disagrees with the gateway — ${order.id}`,
299
+ detail: [
300
+ record.amountRefunded > ours
301
+ ? `🛑 The gateway has refunded MORE than our records show. Money has left that we did not record.`
302
+ : `Our records show more refunded than the gateway did — a refund may have been reserved and never completed.`,
303
+ "",
304
+ `Gateway: ${record.gateway}`,
305
+ `Payment: ${record.id}`,
306
+ `Order: ${order.id}`,
307
+ `Gateway refunded: ${record.amountRefunded}`,
308
+ `Our committed refunds: ${ours}`,
309
+ `Refundable base: ${refundableBase(order)}`,
310
+ record.refundStatus ? `Gateway refund status: ${record.refundStatus}` : null,
311
+ ]
312
+ .filter((l) => l !== null)
313
+ .join("\n"),
314
+ entityType: "order",
315
+ entityId: order.id,
316
+ amountAtRisk: Math.abs(record.amountRefunded - ours),
317
+ orderIds: [order.id],
318
+ gatewayPaymentId: record.id,
319
+ now: ctx.now,
320
+ });
321
+ }
322
+ return { amountMismatch, refundMismatch };
323
+ }
307
324
  /* ──────────────── R3: refund claimed, never settled ─────────────────────── */
308
325
  /**
309
326
  * A `refundClaims` row still `claimed` an hour later is a crash between
@@ -321,8 +338,8 @@ async function reconcileStaleRefundClaims(ctx) {
321
338
  try {
322
339
  const snap = await ctx.db
323
340
  .collection(REFUND_CLAIMS_COLLECTION)
324
- .where("state", "==", "claimed")
325
- .where("claimedAt", "<", cutoff)
341
+ .where(REFUND_CLAIM_FIELDS.STATE, "==", "claimed")
342
+ .where(REFUND_CLAIM_FIELDS.CLAIMED_AT, "<", cutoff)
326
343
  .limit(50)
327
344
  .get();
328
345
  for (const doc of snap.docs) {
@@ -404,9 +421,9 @@ async function reconcileUnpaidCancellations(ctx) {
404
421
  let stale = 0;
405
422
  try {
406
423
  const snap = await ctx.db
407
- .collection("orders")
408
- .where("status", "==", "cancelled")
409
- .where("refundStatus", "==", "pending")
424
+ .collection(ORDER_COLLECTION)
425
+ .where(ORDER_FIELDS.STATUS, "==", "cancelled")
426
+ .where(ORDER_FIELDS.REFUND_STATUS, "==", "pending")
410
427
  .limit(100)
411
428
  .get();
412
429
  /*
@@ -14,7 +14,7 @@ import type { JobContext } from "../runtime/types";
14
14
  * (This used to be shared with testerSandboxRefresh.ts, which was removed when
15
15
  * the tester runner took over fixture state — see scheduled.ts.)
16
16
  */
17
- export declare const TEST_DATA_COLLECTIONS: readonly ["categories", "stores", "products", "blogPosts", "events"];
17
+ export declare const TEST_DATA_COLLECTIONS: readonly ["categories", "stores", "products", "blogPosts", "events", "orders", "obligations", "protectionClaims"];
18
18
  export interface TestDataCleanupOptions {
19
19
  /** true = wipe every isTestData:true doc regardless of testDataExpiresAt (manual force-purge). */
20
20
  force?: boolean;
@@ -41,7 +41,28 @@ import { BID_FIELDS } from "../../../../constants/field-names.js";
41
41
  * is both `test` AND `isTestData`. `audit-seed-purpose` R6 now asserts every
42
42
  * entry here declares the field, so the `offers` shape cannot return.
43
43
  */
44
- export const TEST_DATA_COLLECTIONS = ["categories", "stores", "products", "blogPosts", "events"];
44
+ /*
45
+ * 🛑 Every schema that declares `isTestData` must appear here, or the field is
46
+ * WRITE-ONLY and the row is never swept — a flag that reads as "this will be
47
+ * cleaned up" and never is.
48
+ *
49
+ * `orders`, `obligations` and `protectionClaims` were added 2026-10-22 after
50
+ * `audit-seed-purpose`'s inverse rule found all three declaring the field with
51
+ * no entry. They are the propagation chain: an order carries the flag, writing
52
+ * an obligation copies it (`orders.repository.ts` → `repo.open({ …isTestData
53
+ * })`), and a claim inherits it in turn — so a test order left three
54
+ * generations of descendants behind that nothing would ever collect.
55
+ */
56
+ export const TEST_DATA_COLLECTIONS = [
57
+ "categories",
58
+ "stores",
59
+ "products",
60
+ "blogPosts",
61
+ "events",
62
+ "orders",
63
+ "obligations",
64
+ "protectionClaims",
65
+ ];
45
66
  const BID_CHUNK_SIZE = 30; // Firestore `in` query cap
46
67
  export async function runTestDataCleanup(ctx, opts = {}) {
47
68
  const cutoff = opts.force ? null : new Date();
@@ -1,5 +1,6 @@
1
1
  import { allocateShipmentCosts } from "../../../../features/shipments/utils/cost-allocation.js";
2
2
  import { siteSettingsRepository } from "../../../../features/admin/repository/site-settings.repository.js";
3
+ import { safeRead } from "../../../../errors/safe-read.js";
3
4
  /*
4
5
  * ── D58: the admin saw 6 while this used 4 ───────────────────────────────
5
6
  *
@@ -97,10 +98,21 @@ async function recomputeShipmentAllocation(shipmentId, ctx) {
97
98
  * single shipment, not a sweep, so the read is bounded — and the
98
99
  * alternative (keep ignoring the admin control) is the defect.
99
100
  */
100
- const laborSettings = await siteSettingsRepository
101
- .getSingleton()
102
- .then((s) => s.laborRate)
103
- .catch(() => undefined);
101
+ /*
102
+ * `safeRead`, not `.catch(() => undefined)`.
103
+ *
104
+ * The fallback is unchanged — a settings read that fails must not stop the
105
+ * allocation, and `settingsMaxHours` being absent simply means "no admin
106
+ * ceiling". What changes is that the failure is now RECORDED: a bare catch
107
+ * returning `undefined` is indistinguishable from an admin who never set a
108
+ * ceiling, so the labour cap could silently stop being enforced with
109
+ * nothing anywhere to say so. `installDegradedReadReporter` persists it.
110
+ */
111
+ const laborSettings = await safeRead(() => siteSettingsRepository.getSingleton().then((s) => s.laborRate), {
112
+ route: "job:onShipmentAllocationSync",
113
+ key: "siteSettings:laborRate",
114
+ fallback: undefined,
115
+ });
104
116
  const settingsMaxHours = laborSettings?.maxHoursPerDay;
105
117
  const { perLot, totals } = allocateShipmentCosts({
106
118
  lots: allocationInputs,
@@ -1427,6 +1427,40 @@ export const ACTIONS = {
1427
1427
  confirmKind: "danger",
1428
1428
  },
1429
1429
  },
1430
+ /*
1431
+ * 🛑 The confirmation names the consequence rather than the mechanism.
1432
+ * "Revoke session" means nothing to a buyer; "that device is signed out"
1433
+ * is the fact they need to weigh, and the warning about their own device
1434
+ * is the one mistake this control makes easy to make.
1435
+ */
1436
+ "revoke-session": {
1437
+ iconKey: "lock",
1438
+ id: "user.revoke-session",
1439
+ label: "Sign out device",
1440
+ ariaLabel: "Sign out this device",
1441
+ description: "End an active session so that device has to sign in again.",
1442
+ kind: "danger",
1443
+ confirmation: {
1444
+ title: "Sign out this device?",
1445
+ body: "Whoever is using it will be signed out and will need your password to get back in. If this is the device you are on right now, you will be signed out too.",
1446
+ confirmLabel: "Sign out device",
1447
+ confirmKind: "danger",
1448
+ },
1449
+ },
1450
+ "delete-payment-method": {
1451
+ iconKey: "delete",
1452
+ id: "user.delete-payment-method",
1453
+ label: "Remove card",
1454
+ ariaLabel: "Remove this saved payment method",
1455
+ description: "Remove a saved payment method from the account.",
1456
+ kind: "danger",
1457
+ confirmation: {
1458
+ title: "Remove this payment method?",
1459
+ body: "It will no longer appear at checkout. Any order already placed with it is unaffected.",
1460
+ confirmLabel: "Remove",
1461
+ confirmKind: "danger",
1462
+ },
1463
+ },
1430
1464
  "set-default-address": {
1431
1465
  iconKey: "confirm",
1432
1466
  id: "user.set-default-address",
@@ -2397,17 +2431,44 @@ export const ACTIONS = {
2397
2431
  kind: "ghost",
2398
2432
  permissions: ["admin"],
2399
2433
  },
2434
+ "activate-carousel": {
2435
+ iconKey: "resume",
2436
+ id: "admin.activate-carousel",
2437
+ label: "Activate",
2438
+ ariaLabel: "Show this slide in the carousel",
2439
+ description: "Set a carousel slide to active so the hero carousel renders it.",
2440
+ kind: "primary",
2441
+ permissions: ["admin"],
2442
+ },
2443
+ "deactivate-carousel": {
2444
+ iconKey: "hide",
2445
+ id: "admin.deactivate-carousel",
2446
+ label: "Deactivate",
2447
+ ariaLabel: "Hide this slide from the carousel",
2448
+ description: "Set a carousel slide to inactive without deleting it.",
2449
+ kind: "secondary",
2450
+ permissions: ["admin"],
2451
+ },
2400
2452
  /*
2401
- * Homepage sections have no bucket of their own, so their edit leaf lives
2402
- * here beside the carousel pair for the same reason — both are homepage
2403
- * chrome an admin edits from a list rather than from a detail page.
2453
+ * Homepage sections have no bucket of their own, so their leaves live here
2454
+ * beside the carousel ones for the same reason — both are homepage chrome
2455
+ * an admin edits from a list rather than from a detail page.
2456
+ *
2457
+ * 🛑 `delete-section` exists but is NOT reachable from the public homepage.
2458
+ * Section `order` is POSITIONAL, so removing one leaves a gap in its
2459
+ * neighbours' numbering — not something to offer from a public page, where
2460
+ * the only confirmation a person gets is a dialog.
2461
+ *
2462
+ * That prohibition is enforced INDEPENDENTLY of this registry, which is
2463
+ * why the leaf can exist: `RESOURCE_LEAVES.SECTION` in `OwnerQuickActions`
2464
+ * omits `remove`, and `showDelete` gates on `leaves.remove` rather than on
2465
+ * what a call site passes — so the pencil strip renders no bin regardless.
2466
+ * This comment read "there is deliberately NO delete-section counterpart"
2467
+ * until 2026-10-22, when the admin sections listing gained a real one.
2404
2468
  *
2405
- * 🛑 There is deliberately NO `delete-section` counterpart. Section `order`
2406
- * is POSITIONAL, so removing one silently renumbers its neighbours — not
2407
- * something to offer from the public homepage, where the only confirmation
2408
- * a person gets is a dialog. `OwnerQuickActions` enforces the absence by
2409
- * omitting `remove` from the SECTION mapping, so no call site can pass a
2410
- * delete handler and have it render.
2469
+ * The listing is the right home for it: it owns these records, it shows
2470
+ * the `order` column the gap appears in, and its Reorder panel has a
2471
+ * Reindex 1..N button beside it to close one.
2411
2472
  */
2412
2473
  "edit-section": {
2413
2474
  iconKey: "edit",
@@ -2418,6 +2479,62 @@ export const ACTIONS = {
2418
2479
  kind: "ghost",
2419
2480
  permissions: ["admin"],
2420
2481
  },
2482
+ "enable-section": {
2483
+ iconKey: "resume",
2484
+ id: "admin.enable-section",
2485
+ label: "Enable",
2486
+ ariaLabel: "Enable this homepage section",
2487
+ description: "Render this section on its surface again.",
2488
+ kind: "primary",
2489
+ permissions: ["admin"],
2490
+ },
2491
+ "disable-section": {
2492
+ iconKey: "hide",
2493
+ id: "admin.disable-section",
2494
+ label: "Disable",
2495
+ ariaLabel: "Disable this homepage section",
2496
+ description: "Stop rendering this section without deleting it or its saved config.",
2497
+ kind: "secondary",
2498
+ permissions: ["admin"],
2499
+ },
2500
+ "delete-section": {
2501
+ iconKey: "delete",
2502
+ id: "admin.delete-section",
2503
+ label: "Delete Section",
2504
+ ariaLabel: "Delete this homepage section",
2505
+ description: "Permanently remove this homepage section and its saved config.",
2506
+ kind: "danger",
2507
+ permissions: ["admin"],
2508
+ confirmation: {
2509
+ title: "Delete this section?",
2510
+ /*
2511
+ * The last sentence is load-bearing: it names the reversible
2512
+ * alternative at the moment of the irreversible choice, and that
2513
+ * alternative is one click away in the same row.
2514
+ */
2515
+ body: "The section and its saved config are removed permanently. Display order is positional, so the sections after it keep their current numbers and a gap is left — Reorder sections → Reindex 1..N closes it. To hide the section from the homepage while keeping its config, disable it instead.",
2516
+ confirmLabel: "Delete section",
2517
+ confirmKind: "danger",
2518
+ },
2519
+ },
2520
+ "show-nav-item": {
2521
+ iconKey: "view",
2522
+ id: "admin.show-nav-item",
2523
+ label: "Show",
2524
+ ariaLabel: "Show this navigation item",
2525
+ description: "Make a navigation item visible in the sidebar and menus.",
2526
+ kind: "primary",
2527
+ permissions: ["admin"],
2528
+ },
2529
+ "hide-nav-item": {
2530
+ iconKey: "hide",
2531
+ id: "admin.hide-nav-item",
2532
+ label: "Hide",
2533
+ ariaLabel: "Hide this navigation item",
2534
+ description: "Hide a navigation item without deleting it.",
2535
+ kind: "secondary",
2536
+ permissions: ["admin"],
2537
+ },
2421
2538
  "delete-carousel": {
2422
2539
  iconKey: "delete",
2423
2540
  id: "admin.delete-carousel",
@@ -3085,6 +3202,29 @@ export const ACTIONS = {
3085
3202
  confirmKind: "danger",
3086
3203
  },
3087
3204
  },
3205
+ /*
3206
+ * 🛑 Distinct from `clear-previews` and `discard-staged`, which are
3207
+ * SESSION/STAGING operations over files no record points at yet. This one
3208
+ * removes a FINALISED asset, so the confirmation has to say what nothing
3209
+ * else in this bucket has to: the bytes are referenced by whatever already
3210
+ * embedded them, and the proxy resolves `/media/<shortId>` through the
3211
+ * `mediaAssets` row — so deleting is how a live page gets a broken image.
3212
+ */
3213
+ "delete-media": {
3214
+ iconKey: "delete",
3215
+ id: "media.delete-media",
3216
+ label: "Delete file",
3217
+ ariaLabel: "Delete this media file",
3218
+ description: "Permanently delete a media file and the asset row that resolves it.",
3219
+ kind: "danger",
3220
+ permissions: ["admin"],
3221
+ confirmation: {
3222
+ title: "Delete this file?",
3223
+ body: "The file is removed from storage and stops resolving. Any product, blog post or section still embedding it will show a broken image — this cannot be undone.",
3224
+ confirmLabel: "Delete file",
3225
+ confirmKind: "danger",
3226
+ },
3227
+ },
3088
3228
  },
3089
3229
  SUPPORT: {
3090
3230
  "create-ticket": {