@mohasinac/appkit 4.61.0 → 4.62.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) 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/checkout/actions.d.ts +80 -0
  6. package/dist/_internal/server/features/checkout/actions.js +542 -26
  7. package/dist/_internal/server/features/invoicing/allocate.d.ts +63 -0
  8. package/dist/_internal/server/features/invoicing/allocate.js +162 -0
  9. package/dist/_internal/server/features/invoicing/credit-note.d.ts +57 -0
  10. package/dist/_internal/server/features/invoicing/credit-note.js +126 -0
  11. package/dist/_internal/server/features/invoicing/gstr1.d.ts +74 -0
  12. package/dist/_internal/server/features/invoicing/gstr1.js +251 -0
  13. package/dist/_internal/server/features/invoicing/issue.d.ts +96 -0
  14. package/dist/_internal/server/features/invoicing/issue.js +202 -0
  15. package/dist/_internal/server/features/orders/actions.d.ts +8 -1
  16. package/dist/_internal/server/features/orders/actions.js +81 -8
  17. package/dist/_internal/server/features/orders/adapters.js +33 -0
  18. package/dist/_internal/server/features/orders/cancellation-refund.d.ts +15 -9
  19. package/dist/_internal/server/features/orders/cancellation-refund.js +15 -9
  20. package/dist/_internal/server/features/points/award.js +46 -4
  21. package/dist/_internal/server/features/refunds/actions.js +26 -0
  22. package/dist/_internal/server/features/shipping/courier-block.d.ts +69 -0
  23. package/dist/_internal/server/features/shipping/courier-block.js +239 -0
  24. package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
  25. package/dist/_internal/server/features/site-settings/adapters.js +15 -0
  26. package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
  27. package/dist/_internal/server/features/stores/adapters.js +51 -0
  28. package/dist/_internal/server/functions/scheduled.js +38 -1
  29. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +9 -2
  30. package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +4 -0
  31. package/dist/_internal/server/jobs/core/dailyStatusDigest.js +19 -2
  32. package/dist/_internal/server/jobs/core/index.d.ts +2 -1
  33. package/dist/_internal/server/jobs/core/index.js +2 -1
  34. package/dist/_internal/server/jobs/core/onJobCreated.d.ts +30 -7
  35. package/dist/_internal/server/jobs/core/onJobCreated.js +78 -45
  36. package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +7 -0
  37. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +62 -0
  38. package/dist/_internal/server/jobs/core/paymentReconcile.js +87 -97
  39. package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +33 -0
  40. package/dist/_internal/server/jobs/core/runJobOnce.d.ts +48 -0
  41. package/dist/_internal/server/jobs/core/runJobOnce.js +195 -0
  42. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +10 -2
  43. package/dist/_internal/server/jobs/handlers/onOrderStatusChange.js +17 -0
  44. package/dist/_internal/server/notifications/channel-health.d.ts +26 -3
  45. package/dist/_internal/shared/actions/action-registry.js +177 -9
  46. package/dist/_internal/shared/checkout/coupon-discount.d.ts +43 -1
  47. package/dist/_internal/shared/checkout/coupon-discount.js +26 -1
  48. package/dist/_internal/shared/checkout/order-math.d.ts +145 -0
  49. package/dist/_internal/shared/checkout/order-math.js +173 -0
  50. package/dist/_internal/shared/checkout/refundable-base.d.ts +25 -0
  51. package/dist/_internal/shared/checkout/refundable-base.js +13 -1
  52. package/dist/_internal/shared/checkout/split.d.ts +87 -0
  53. package/dist/_internal/shared/checkout/split.js +100 -0
  54. package/dist/_internal/shared/content/disclaimers.d.ts +88 -0
  55. package/dist/_internal/shared/content/disclaimers.js +217 -0
  56. package/dist/_internal/shared/content/published-values.d.ts +88 -0
  57. package/dist/_internal/shared/content/published-values.js +198 -0
  58. package/dist/_internal/shared/features/admin/integrations.d.ts +109 -0
  59. package/dist/_internal/shared/features/admin/integrations.js +253 -0
  60. package/dist/_internal/shared/features/orders/dispute-reasons.d.ts +113 -0
  61. package/dist/_internal/shared/features/orders/dispute-reasons.js +181 -0
  62. package/dist/_internal/shared/features/orders/refund-copy.d.ts +1 -1
  63. package/dist/_internal/shared/features/orders/refund-copy.js +18 -1
  64. package/dist/_internal/shared/features/points/accepts.d.ts +36 -0
  65. package/dist/_internal/shared/features/points/accepts.js +43 -0
  66. package/dist/_internal/shared/features/shipping/shared-purchase.d.ts +91 -0
  67. package/dist/_internal/shared/features/shipping/shared-purchase.js +108 -0
  68. package/dist/_internal/shared/features/site-settings/types.d.ts +11 -0
  69. package/dist/_internal/shared/fees/calculator.d.ts +33 -1
  70. package/dist/_internal/shared/fees/calculator.js +32 -2
  71. package/dist/_internal/shared/media/limits.d.ts +10 -0
  72. package/dist/_internal/shared/media/limits.js +10 -0
  73. package/dist/_internal/shared/types/action-result.js +15 -1
  74. package/dist/client/api/surface-error.js +16 -2
  75. package/dist/client/api/useApiMutation.js +17 -1
  76. package/dist/client.d.ts +12 -1
  77. package/dist/client.js +31 -1
  78. package/dist/constants/api-endpoints.d.ts +45 -0
  79. package/dist/constants/api-endpoints.js +15 -0
  80. package/dist/constants/field-names.d.ts +18 -0
  81. package/dist/constants/field-names.js +28 -0
  82. package/dist/contracts/index.d.ts +1 -1
  83. package/dist/contracts/shipping.d.ts +45 -0
  84. package/dist/contracts/shipping.js +15 -0
  85. package/dist/core/server-action.js +19 -3
  86. package/dist/errors/error-mapping.js +26 -1
  87. package/dist/features/about/components/AboutView.js +1 -1
  88. package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -1
  89. package/dist/features/account/components/UserOffersPanel.js +5 -3
  90. package/dist/features/account/components/UserOrdersView.js +19 -33
  91. package/dist/features/admin/components/AdminActionIndexView.js +100 -5
  92. package/dist/features/admin/components/AdminAddressClustersView.js +21 -2
  93. package/dist/features/admin/components/AdminBlogEditorView.js +8 -8
  94. package/dist/features/admin/components/AdminCarouselEditorView.js +1 -1
  95. package/dist/features/admin/components/AdminCarouselView.js +96 -4
  96. package/dist/features/admin/components/AdminCouponEditorView.js +17 -0
  97. package/dist/features/admin/components/AdminFulfillmentView.js +3 -2
  98. package/dist/features/admin/components/AdminInvoicingView.d.ts +2 -0
  99. package/dist/features/admin/components/AdminInvoicingView.js +65 -0
  100. package/dist/features/admin/components/AdminMediaView.js +36 -3
  101. package/dist/features/admin/components/AdminNavigationView.js +4 -1
  102. package/dist/features/admin/components/AdminOffersView.js +2 -1
  103. package/dist/features/admin/components/AdminOrderEditorView.d.ts +18 -1
  104. package/dist/features/admin/components/AdminOrderEditorView.js +11 -2
  105. package/dist/features/admin/components/AdminOrdersGuideView.js +23 -2
  106. package/dist/features/admin/components/AdminOrdersView.js +67 -5
  107. package/dist/features/admin/components/AdminPaymentClustersView.js +19 -2
  108. package/dist/features/admin/components/AdminProductEditorView.js +2 -2
  109. package/dist/features/admin/components/AdminSectionsView.d.ts +10 -0
  110. package/dist/features/admin/components/AdminSectionsView.js +486 -224
  111. package/dist/features/admin/components/OwnerQuickActions.js +24 -2
  112. package/dist/features/admin/components/ReferenceRecordEditor.js +1 -1
  113. package/dist/features/admin/constants/filter-tabs.d.ts +6 -0
  114. package/dist/features/admin/constants/filter-tabs.js +6 -0
  115. package/dist/features/admin/constants/site-settings-tabs.d.ts +1 -1
  116. package/dist/features/admin/hooks/useAdminListingData.js +16 -0
  117. package/dist/features/admin/hooks/useOptimisticRowFlag.d.ts +33 -0
  118. package/dist/features/admin/hooks/useOptimisticRowFlag.js +87 -0
  119. package/dist/features/admin/repository/notification.repository.d.ts +15 -0
  120. package/dist/features/admin/repository/notification.repository.js +32 -0
  121. package/dist/features/admin/schemas/action-index-form.d.ts +34 -0
  122. package/dist/features/admin/schemas/action-index-form.js +115 -0
  123. package/dist/features/admin/schemas/admin-user-form.d.ts +2 -2
  124. package/dist/features/admin/schemas/firestore.d.ts +30 -0
  125. package/dist/features/admin/schemas/firestore.js +5 -0
  126. package/dist/features/audit-log/schemas/firestore.d.ts +30 -0
  127. package/dist/features/audit-log/schemas/firestore.js +15 -0
  128. package/dist/features/auth/components/RegisterForm.js +2 -1
  129. package/dist/features/auth/permissions/constants.js +13 -0
  130. package/dist/features/cart/components/CartPriceBreakdown.d.ts +13 -0
  131. package/dist/features/cart/components/CartPriceBreakdown.js +6 -2
  132. package/dist/features/cart/components/PointsTenderPicker.d.ts +61 -0
  133. package/dist/features/cart/components/PointsTenderPicker.js +15 -0
  134. package/dist/features/cart/components/StoreAddonsPicker.js +2 -1
  135. package/dist/features/cart/components/index.d.ts +2 -0
  136. package/dist/features/cart/components/index.js +1 -0
  137. package/dist/features/cart/repository/cart.repository.d.ts +19 -0
  138. package/dist/features/cart/repository/cart.repository.js +27 -0
  139. package/dist/features/cart/schemas/firestore.d.ts +92 -0
  140. package/dist/features/catalogue/components/CatalogueItemEditorView.js +3 -3
  141. package/dist/features/contact/email.d.ts +27 -10
  142. package/dist/features/contact/email.js +20 -3
  143. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +38 -0
  144. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +80 -0
  145. package/dist/features/dispute-reasons/schemas/firestore.d.ts +64 -0
  146. package/dist/features/dispute-reasons/schemas/firestore.js +40 -0
  147. package/dist/features/events/components/AdminEventEditorView.js +8 -6
  148. package/dist/features/faq/components/FAQAccordion.d.ts +13 -1
  149. package/dist/features/faq/components/FAQAccordion.js +29 -4
  150. package/dist/features/invoicing/schemas/firestore.d.ts +112 -0
  151. package/dist/features/invoicing/schemas/firestore.js +45 -0
  152. package/dist/features/invoicing/utils/financial-year.d.ts +43 -0
  153. package/dist/features/invoicing/utils/financial-year.js +53 -0
  154. package/dist/features/jobs/actions/enqueue-job.js +18 -1
  155. package/dist/features/jobs/repository/jobs.repository.d.ts +62 -1
  156. package/dist/features/jobs/repository/jobs.repository.js +84 -1
  157. package/dist/features/jobs/schemas/firestore.d.ts +80 -0
  158. package/dist/features/jobs/schemas/firestore.js +37 -0
  159. package/dist/features/layout/hooks/useFormBottomActions.js +20 -1
  160. package/dist/features/lottery/schemas/zod.d.ts +6 -6
  161. package/dist/features/media/MediaImage.js +22 -1
  162. package/dist/features/media/upload/ImageUpload.js +1 -1
  163. package/dist/features/obligations/constants/policies.js +24 -1
  164. package/dist/features/orders/components/EmiInstallmentsPanel.js +25 -7
  165. package/dist/features/orders/components/OrderPaymentSummary.js +10 -1
  166. package/dist/features/orders/components/OrderProvenanceCard.d.ts +8 -0
  167. package/dist/features/orders/components/OrderProvenanceCard.js +185 -0
  168. package/dist/features/orders/components/RaiseDisputeForm.d.ts +14 -0
  169. package/dist/features/orders/components/RaiseDisputeForm.js +111 -0
  170. package/dist/features/orders/components/RefundRequestView.js +22 -4
  171. package/dist/features/orders/components/index.d.ts +4 -0
  172. package/dist/features/orders/components/index.js +2 -0
  173. package/dist/features/orders/constants/order-lane-tabs.d.ts +69 -0
  174. package/dist/features/orders/constants/order-lane-tabs.js +88 -0
  175. package/dist/features/orders/repository/orders.repository.d.ts +57 -0
  176. package/dist/features/orders/repository/orders.repository.js +145 -0
  177. package/dist/features/orders/schemas/firestore.d.ts +196 -1
  178. package/dist/features/orders/schemas/index.d.ts +60 -12
  179. package/dist/features/orders/schemas/index.js +12 -0
  180. package/dist/features/orders/types/index.d.ts +26 -0
  181. package/dist/features/orders/utils/courier-block.d.ts +25 -0
  182. package/dist/features/orders/utils/courier-block.js +23 -0
  183. package/dist/features/payment-cases/schemas/firestore.d.ts +26 -1
  184. package/dist/features/payment-cases/schemas/firestore.js +1 -0
  185. package/dist/features/products/components/GroupMemberPicker.js +4 -1
  186. package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
  187. package/dist/features/products/components/PrizeDrawEntryActions.js +2 -1
  188. package/dist/features/products/components/ProductDetailActions.js +2 -1
  189. package/dist/features/products/components/ProductDetailPageView.d.ts +17 -1
  190. package/dist/features/products/components/ProductDetailPageView.js +2 -2
  191. package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
  192. package/dist/features/promotions/actions/coupon-stacking.d.ts +30 -3
  193. package/dist/features/promotions/actions/coupon-stacking.js +21 -2
  194. package/dist/features/promotions/components/CouponCard.js +13 -0
  195. package/dist/features/promotions/constants/coupon-help.d.ts +2 -2
  196. package/dist/features/promotions/constants/coupon-help.js +9 -1
  197. package/dist/features/promotions/schemas/firestore.d.ts +24 -0
  198. package/dist/features/promotions/schemas/firestore.js +4 -0
  199. package/dist/features/promotions/types/index.d.ts +19 -1
  200. package/dist/features/scams/components/ScamProfileView.js +2 -2
  201. package/dist/features/scams/components/ScamRegistryView.js +2 -2
  202. package/dist/features/search/action-index/tab-entries.js +1 -1
  203. package/dist/features/seller/actions/seller-actions.js +33 -3
  204. package/dist/features/seller/components/FulfillmentView.js +3 -2
  205. package/dist/features/seller/components/SellerAddressesView.js +32 -4
  206. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +62 -3
  207. package/dist/features/seller/components/SellerBundlesView.js +53 -14
  208. package/dist/features/seller/components/SellerGroupedListingsView.js +23 -3
  209. package/dist/features/seller/components/SellerListingTypeView.js +56 -10
  210. package/dist/features/seller/components/SellerOrdersView.js +129 -27
  211. package/dist/features/seller/components/SellerPayoutMethodsView.js +33 -1
  212. package/dist/features/seller/components/SellerPayoutSettingsView.js +48 -9
  213. package/dist/features/seller/components/SellerProductsView.js +5 -1
  214. package/dist/features/seller/components/SellerShippingConfigsView.js +33 -1
  215. package/dist/features/seller/components/SellerShippingView.js +2 -1
  216. package/dist/features/seller/components/SellerStoreCategoriesView.js +33 -1
  217. package/dist/features/seller/components/SellerSupportView.js +69 -2
  218. package/dist/features/seller/hooks/useSellerListingData.js +25 -0
  219. package/dist/features/shell/SectionForm.js +20 -16
  220. package/dist/features/shell/build-sections.js +14 -1
  221. package/dist/features/shipments/components/AdminShipmentLotItemsView.js +54 -1
  222. package/dist/features/shipments/components/AdminShipmentProjectionsView.js +3 -2
  223. package/dist/features/shipments/schemas/validation.d.ts +4 -4
  224. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +43 -0
  225. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +90 -0
  226. package/dist/features/shipping-boxes/schemas/firestore.d.ts +82 -0
  227. package/dist/features/shipping-boxes/schemas/firestore.js +53 -0
  228. package/dist/features/sticker-trace/components/StickerCommitPanel.js +2 -1
  229. package/dist/features/sticker-trace/components/StickerPrintPanel.js +2 -1
  230. package/dist/features/sticker-trace/components/StickerRetryPanel.js +24 -4
  231. package/dist/features/sticker-trace/components/StickerTraceStudio.js +2 -1
  232. package/dist/features/store-extensions/schemas/report-create-form.d.ts +2 -2
  233. package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +2 -2
  234. package/dist/features/stores/components/StoreOrdersGuideView.js +6 -1
  235. package/dist/features/stores/schemas/firestore.d.ts +71 -0
  236. package/dist/features/stores/types/index.d.ts +19 -0
  237. package/dist/features/support/schemas/index.d.ts +6 -6
  238. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +3 -2
  239. package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
  240. package/dist/index.d.ts +11 -2
  241. package/dist/index.js +17 -2
  242. package/dist/next/routing/route-map.d.ts +24 -0
  243. package/dist/next/routing/route-map.js +19 -0
  244. package/dist/providers/shipping-shiprocket/index.d.ts +27 -1
  245. package/dist/providers/shipping-shiprocket/index.js +84 -1
  246. package/dist/schemas/registry.d.ts +51 -22
  247. package/dist/schemas/sieve.d.ts +5 -5
  248. package/dist/server-entry.d.ts +2 -2
  249. package/dist/server-entry.js +8 -1
  250. package/dist/server.d.ts +11 -2
  251. package/dist/server.js +40 -2
  252. package/dist/styles.css +64 -80
  253. package/dist/tailwind-utilities.css +1 -1
  254. package/dist/tokens/themes/default-dark.js +1 -0
  255. package/dist/tokens/themes/default-light.js +1 -0
  256. package/dist/tokens/themes/required.d.ts +1 -1
  257. package/dist/tokens/themes/required.js +1 -0
  258. package/dist/tokens/tokens.css +15 -0
  259. package/dist/ui/components/Checkbox.js +10 -2
  260. package/dist/ui/components/DateInput.js +8 -1
  261. package/dist/ui/components/Disclaimer.d.ts +19 -0
  262. package/dist/ui/components/Disclaimer.js +74 -0
  263. package/dist/ui/components/Input.js +22 -2
  264. package/dist/ui/components/PaginatedSelect.d.ts +8 -19
  265. package/dist/ui/components/PaginatedSelect.js +43 -9
  266. package/dist/ui/components/Radio.js +8 -1
  267. package/dist/ui/components/Select.js +10 -2
  268. package/dist/ui/components/TagInput.d.ts +8 -1
  269. package/dist/ui/components/TagInput.js +4 -3
  270. package/dist/ui/components/Textarea.js +10 -2
  271. package/dist/ui/components/tabs-overflow.js +64 -11
  272. package/dist/ui/forms/FieldCheckbox.js +1 -1
  273. package/dist/ui/forms/FormErrorList.d.ts +0 -16
  274. package/dist/ui/forms/FormErrorList.js +34 -1
  275. package/dist/ui/forms/apply-zod-issues.d.ts +19 -1
  276. package/dist/ui/forms/apply-zod-issues.js +23 -1
  277. package/dist/ui/index.d.ts +2 -0
  278. package/dist/ui/index.js +6 -0
  279. package/dist/ui/rich-text/RichText.style.css +48 -79
  280. package/package.json +1 -1
  281. package/scripts/seed-cli.mjs +1831 -1822
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Run ONE attempt of a job, and decide what happens next (Phase 8).
3
+ *
4
+ * ## 🛑 ONE dispatch path, shared by the trigger and the retry sweep
5
+ *
6
+ * `handleJobCreated` is the `jobs/{jobId}` create trigger; `runDueJobRetries`
7
+ * is the sweep that picks up scheduled retries. Both call THIS, so there is
8
+ * exactly one place that knows how to run a job, classify its failure and
9
+ * schedule the next attempt. Two dispatch paths would drift the first time one
10
+ * of them learned something — which is the shape of every defect in this
11
+ * plan's § "Recurrent Root Cause Patterns" that begins "a fix applied to the
12
+ * route but never back-ported".
13
+ *
14
+ * ## 🛑 A retry is a NEW DOCUMENT
15
+ *
16
+ * `onJobCreated` fires on CREATE. Re-running a job by flipping its status back
17
+ * to `pending` would never wake the trigger, so the job would sit `pending`
18
+ * forever looking scheduled — a stall that reads as "queued". So a retry is a
19
+ * fresh `jobs` document carrying `attempt + 1`, `parentJobId` and a
20
+ * `nextAttemptAt`, and the sweep is what runs it when due.
21
+ *
22
+ * ## What is retryable
23
+ *
24
+ * `mapToHttpError(err).status >= 500`. Reused rather than invented: that
25
+ * function already encodes "is this our fault or the caller's" for every error
26
+ * class in the codebase, and a second classification would answer differently
27
+ * the first time either changed.
28
+ *
29
+ * A 4xx is terminal IMMEDIATELY — no backoff, no attempts burned. A validation
30
+ * failure does not become valid by waiting, and retrying it three times over
31
+ * 35 minutes just delays the case by 35 minutes.
32
+ */
33
+ import { normalizeError } from "../../../../errors/normalize.js";
34
+ import { mapToHttpError } from "../../../../errors/error-mapping.js";
35
+ import { jobsRepository } from "../../../../repositories/index.js";
36
+ import { paymentCasesRepository } from "../../../../features/payment-cases/repository/payment-cases.repository.js";
37
+ import { getAdminRealtimeDb } from "../../../../providers/db-firebase/index.js";
38
+ import { RTDB_PATHS } from "../../../../providers/db-firebase/rtdb-paths.js";
39
+ import { JOB_BACKOFF_MS, JOB_MAX_ATTEMPTS, } from "../../../../features/jobs/schemas/firestore.js";
40
+ import { JOB_RUNNERS } from "./jobRunners.js";
41
+ async function pingBulkEvent(jobId, payload, ctx) {
42
+ try {
43
+ await getAdminRealtimeDb()
44
+ .ref(`${RTDB_PATHS.BULK_EVENTS}/${jobId}`)
45
+ .set({ ...payload, updatedAt: Date.now() });
46
+ }
47
+ catch (err) {
48
+ void normalizeError(err);
49
+ ctx.logger.warn("runJobOnce: RTDB ping failed (non-fatal)", {
50
+ jobId,
51
+ error: err instanceof Error ? err.message : String(err),
52
+ });
53
+ }
54
+ }
55
+ /**
56
+ * The delay before the next attempt.
57
+ *
58
+ * 🛑 Clamped to the LAST element rather than indexing past the end. An
59
+ * out-of-range index yields `undefined`, `new Date(Date.now() + undefined)` is
60
+ * `Invalid Date`, and the sweep's `nextAttemptAt <= now` comparison is false
61
+ * for an invalid date forever — so the retry would be scheduled and never run.
62
+ * A stall that looks like a queue is the exact failure this phase removes.
63
+ */
64
+ function backoffFor(attempt) {
65
+ const idx = Math.max(0, Math.min(attempt - 1, JOB_BACKOFF_MS.length - 1));
66
+ return JOB_BACKOFF_MS[idx];
67
+ }
68
+ /**
69
+ * Run one attempt. Marks the job done, schedules a retry, or declares it dead.
70
+ *
71
+ * Never throws: a job's failure is recorded on the job, and letting it escape
72
+ * would make the Firebase trigger retry the whole handler — a second,
73
+ * uncontrolled retry mechanism layered under this one, which is precisely the
74
+ * doubling that cost 55 sessions in Root Cause #91 (`run.mjs`'s internal retry
75
+ * multiplying the pool's).
76
+ */
77
+ export async function runJobOnce(input, ctx) {
78
+ const { jobId, job } = input;
79
+ const attempt = job.attempt ?? 1;
80
+ const maxAttempts = job.maxAttempts ?? JOB_MAX_ATTEMPTS;
81
+ await jobsRepository.markProcessing(jobId);
82
+ const runner = JOB_RUNNERS[job.jobType];
83
+ if (!runner) {
84
+ /*
85
+ * 🛑 Terminal immediately, and DEAD rather than failed. An unregistered
86
+ * jobType cannot become registered by waiting, and the thing that would
87
+ * fix it is a deploy — so this is the clearest possible case for a human,
88
+ * not a retry. It is also the failure mode of the seed-loader registry
89
+ * (Root Cause #90): a missing map entry whose symptom is silence.
90
+ */
91
+ const error = `No runner registered for jobType: ${job.jobType}`;
92
+ await jobsRepository.markDead(jobId, error);
93
+ await pingBulkEvent(jobId, { status: "failed", action: job.jobType, error }, ctx);
94
+ await openDeadJobCase(jobId, job, error, ctx);
95
+ ctx.logger.error("runJobOnce: unregistered jobType", null, { jobId, jobType: job.jobType });
96
+ return;
97
+ }
98
+ try {
99
+ const result = await runner(job.payload, ctx);
100
+ await jobsRepository.markDone(jobId, result);
101
+ await pingBulkEvent(jobId, { status: "success", action: job.jobType, ...result }, ctx);
102
+ ctx.logger.info("runJobOnce: job complete", {
103
+ jobId,
104
+ jobType: job.jobType,
105
+ attempt,
106
+ summary: result.summary,
107
+ });
108
+ return;
109
+ }
110
+ catch (err) {
111
+ void normalizeError(err);
112
+ const message = err instanceof Error ? err.message : String(err);
113
+ /*
114
+ * 🛑 `mapToHttpError` rather than a bespoke test. It already decides "ours
115
+ * or theirs" for every error class here, so a 5xx is a transient fault
116
+ * worth retrying and a 4xx is a permanent one that will fail identically
117
+ * in five minutes.
118
+ */
119
+ const status = mapToHttpError(err).status;
120
+ const retryable = status >= 500;
121
+ const attemptsLeft = attempt < maxAttempts;
122
+ if (retryable && attemptsLeft) {
123
+ const nextAttemptAt = new Date(Date.now() + backoffFor(attempt));
124
+ /*
125
+ * 🛑 The NEW document is created BEFORE this one is marked failed. The
126
+ * reverse ordering leaves a window where the job reads as failed with no
127
+ * retry scheduled — and if the process dies in that window, nothing
128
+ * retries and nothing says so. This way the worst case is a scheduled
129
+ * retry whose parent still reads `processing`, which the sweep handles
130
+ * and a human can see.
131
+ */
132
+ const retryId = await jobsRepository.createRetry(jobId, job, {
133
+ attempt: attempt + 1,
134
+ maxAttempts,
135
+ nextAttemptAt,
136
+ });
137
+ await jobsRepository.markFailed(jobId, message);
138
+ /*
139
+ * The client is told "retrying", not "failed". `useBulkEvent` watches
140
+ * for a terminal state, and reporting failure here would stop a progress
141
+ * UI on a job that is still going.
142
+ */
143
+ await pingBulkEvent(jobId, { status: "retrying", action: job.jobType, error: message, retryJobId: retryId }, ctx);
144
+ ctx.logger.warn("runJobOnce: attempt failed, retry scheduled", {
145
+ jobId,
146
+ retryId,
147
+ jobType: job.jobType,
148
+ attempt,
149
+ status,
150
+ nextAttemptAt: nextAttemptAt.toISOString(),
151
+ });
152
+ return;
153
+ }
154
+ await jobsRepository.markDead(jobId, message);
155
+ await pingBulkEvent(jobId, { status: "failed", action: job.jobType, error: message }, ctx);
156
+ await openDeadJobCase(jobId, job, message, ctx);
157
+ ctx.logger.error("runJobOnce: job DEAD", err, {
158
+ jobId,
159
+ jobType: job.jobType,
160
+ attempt,
161
+ status,
162
+ reason: retryable ? "attempts exhausted" : "permanent failure",
163
+ });
164
+ }
165
+ }
166
+ /**
167
+ * A dead job is a case, not a log line.
168
+ *
169
+ * 🛑 Keyed on the ORIGINAL job id (`parentJobId ?? jobId`), so three exhausted
170
+ * attempts of one job produce ONE case rather than three. `openOrObserve`
171
+ * increments its own counter — the property that makes it safe to open a case
172
+ * from a hot path at all.
173
+ *
174
+ * Best-effort: a job is already dead by this point, and failing to file the
175
+ * case must not turn that into a thrown error from a trigger.
176
+ */
177
+ async function openDeadJobCase(jobId, job, error, ctx) {
178
+ const subjectId = job.parentJobId ?? jobId;
179
+ try {
180
+ await paymentCasesRepository().openOrObserve({
181
+ kind: "dead_job",
182
+ subjectId,
183
+ severity: "error",
184
+ entityType: "job",
185
+ entityId: subjectId,
186
+ title: `Job ${job.jobType} gave up after ${job.attempt ?? 1} attempt(s)`,
187
+ detail: `The last error was: ${error}. ` +
188
+ `Requested by ${job.requestedBy}. The job document holds the payload.`,
189
+ });
190
+ }
191
+ catch (err) {
192
+ void normalizeError(err);
193
+ ctx.logger.error("runJobOnce: could not open dead_job case", err, { jobId });
194
+ }
195
+ }
@@ -8,7 +8,7 @@ import { getDefaultCurrency } from "../../../../core/index.js";
8
8
  import { computePayoutDeduction } from "../../../shared/fees/calculator.js";
9
9
  import { payoutCutoff } from "../../../shared/fees/payout-hold.js";
10
10
  import { PROTECTION_CLAIM_WINDOW_HOURS } from "../../../shared/features/shipping/protections.js";
11
- import { ordersTaxableValue, ordersShippingReimbursement, } from "../../../shared/checkout/order-math.js";
11
+ import { ordersTaxableValue, ordersShippingReimbursement, ordersPointsBorneByStore, } from "../../../shared/checkout/order-math.js";
12
12
  export async function runWeeklyPayoutEligibility(ctx) {
13
13
  // `FEATURE_PAYOUTS` gate deleted 2026-10-19 — see `autoPayoutEligibility.ts`.
14
14
  const siteSettings = await siteSettingsRepository.getSingleton();
@@ -75,7 +75,15 @@ export async function runWeeklyPayoutEligibility(ctx) {
75
75
  * `{ id, data }` wrappers, and passing the wrapper would read every field
76
76
  * as undefined and silently reimburse zero.
77
77
  */
78
- ordersShippingReimbursement(orders.map((o) => o.data)));
78
+ ordersShippingReimbursement(orders.map((o) => o.data)),
79
+ /*
80
+ * D46/D47 — the store-funded points, both ends. Must match
81
+ * `autoPayoutEligibility` exactly, for the reason stated above
82
+ * `grossAmount`: the weekly run and the automatic one pay the same orders,
83
+ * and a term present in one and absent in the other is a seller paid two
84
+ * different amounts depending on which job got there first.
85
+ */
86
+ ordersPointsBorneByStore(orders.map((o) => o.data)));
79
87
  /*
80
88
  * 🛑 REVEALED before building a payout — see the identical note in
81
89
  * `autoPayoutEligibility.ts`. `findById` is ciphertext-by-default and
@@ -1,4 +1,21 @@
1
1
  import { handleOrderStatusChange, } from "../core/onOrderStatusChange.js";
2
+ /*
3
+ * 🛑 Bound as **`documentUpdated`**, which is what Phase 7's numbering needs
4
+ * and is already what this function uses — verified rather than assumed.
5
+ *
6
+ * `paymentStatus: pending → paid` is frequently the ONLY field that moves on a
7
+ * COD or manual order, and `documentUpdated` fires on any update, so the
8
+ * handler sees it. `documentCreated` would miss every one of them, and
9
+ * `documentWritten` would add nothing except a wake-up on create, where an
10
+ * order is `pending` and there is nothing to number.
11
+ *
12
+ * 🛑 What makes a write-back from this handler safe is the allocation claim:
13
+ * it updates `orders` only when IT allocated, so the re-fire caused by its own
14
+ * write finds the claim taken and writes nothing. That is the guard Root Cause
15
+ * #92's 12M-invocation loop lacked — there the no-op check compared Firestore
16
+ * maps with `JSON.stringify`, which is key-order sensitive and could never
17
+ * match, so every invocation wrote and every write re-triggered.
18
+ */
2
19
  export const onOrderStatusChangeHandler = async (event, ctx) => {
3
20
  await handleOrderStatusChange({ orderId: event.params.orderId, before: event.before, after: event.after }, ctx);
4
21
  };
@@ -10,10 +10,33 @@
10
10
  * its own outcome decides whether the circuit re-closes or re-opens.
11
11
  */
12
12
  /**
13
- * Email is the only outbound channel with a transport. "whatsapp" was the
14
- * other arm until 2026-10-20, when the Cloud API integration was removed.
13
+ * Email is the only outbound *notification* channel with a transport.
14
+ * "whatsapp" was the other arm until 2026-10-20, when the Cloud API
15
+ * integration was removed.
16
+ *
17
+ * ## 🛑 Phase 8 widened this to the RECONCILERS' reads — and only those
18
+ *
19
+ * `razorpay_read` and `shiprocket_read` name the one placement where "stop
20
+ * hammering a dead API" is the right behaviour. Two placements are **actively
21
+ * harmful** and must never be added:
22
+ *
23
+ * 1. **NOT the webhook ingest path.** An open breaker converts a transient
24
+ * blip into a DROPPED money event — and dropping is precisely what a
25
+ * breaker does. The ingest path's whole job is to persist first and ack
26
+ * honestly so the gateway retries; a breaker there would ack a 5xx we never
27
+ * recorded.
28
+ *
29
+ * 2. **NOT around `payment.refund()`.** A refund POST that timed out may have
30
+ * SUCCEEDED, so retrying it issues two refunds — and the idempotency claim
31
+ * does not help, because it is the same call with the same key making a
32
+ * second network attempt. The two-phase reserve/settle exists so a human
33
+ * can resolve that by reading the gateway, not so a breaker can guess.
34
+ *
35
+ * The names carry `_read` for exactly that reason: a reader is idempotent, so
36
+ * backing off costs only freshness. Anything that moves money is not a reader
37
+ * and does not belong in this union.
15
38
  */
16
- export type NotificationChannel = "email";
39
+ export type NotificationChannel = "email" | "razorpay_read" | "shiprocket_read";
17
40
  /**
18
41
  * Returns false only when the circuit is open and still within its cooldown
19
42
  * window. Any other state (closed, or open-but-cooldown-elapsed) is treated
@@ -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.
2404
2461
  *
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.
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.
2468
+ *
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": {
@@ -3116,6 +3256,34 @@ export const ACTIONS = {
3116
3256
  confirmLabel: "Close ticket",
3117
3257
  },
3118
3258
  },
3259
+ /*
3260
+ * Raising a dispute on an order.
3261
+ *
3262
+ * Filed under SUPPORT rather than a new ORDER bucket: there is no such
3263
+ * bucket, and a dispute's destination is a support investigation. A first
3264
+ * draft of the form wrote `ACTIONS.ORDERS?.["raise-dispute"]`, where the
3265
+ * optional chain would have silently resolved to `undefined` and rendered
3266
+ * a button with no label, no aria-label and no confirmation — which is
3267
+ * precisely what Rule #7 exists to stop.
3268
+ *
3269
+ * NO `confirmation`, deliberately. It is not destructive: nothing is
3270
+ * cancelled, no money moves, and the action's own docstring says it "does
3271
+ * not itself reverse payment/order status". A confirm dialog in front of
3272
+ * "tell us something went wrong" adds friction to the one path a buyer
3273
+ * with a problem needs to be able to take.
3274
+ */
3275
+ "raise-dispute": {
3276
+ // `report`, which IS the Flag glyph (icon-registry.ts:133). `"flag"` is
3277
+ // not a registered key — and because `iconKey` is typed `IconKey` rather
3278
+ // than `string`, that would have been a compile error rather than the
3279
+ // silent no-op it used to be.
3280
+ iconKey: "report",
3281
+ id: "support.raise-dispute",
3282
+ label: "Report this",
3283
+ ariaLabel: "Report a problem with this order",
3284
+ description: "Raise a dispute on an order for support to investigate.",
3285
+ kind: "primary",
3286
+ },
3119
3287
  },
3120
3288
  TESTER: {
3121
3289
  "save-note": {
@@ -32,12 +32,54 @@ export interface GroupCouponDiscountResult {
32
32
  appliedDiscounts: {
33
33
  code: string;
34
34
  couponId?: string;
35
+ /**
36
+ * 🛑 This is NOT `CouponType`, and the two are easy to confuse now that
37
+ * both live in this file. This one records WHERE the discount came from —
38
+ * a coupon the buyer typed, a deal, or an automatic rule — and it is what
39
+ * the order document stores in `appliedDiscounts[]`.
40
+ *
41
+ * `CartAppliedCoupon.type` is the coupon's own kind (`percentage`,
42
+ * `points_earn`, …) and is what the loop below skips on. Reading one where
43
+ * the other is meant would compile cleanly and be wrong: every value of
44
+ * this union is absent from that one, so the `points_earn` skip would
45
+ * never fire and a points coupon would discount the order.
46
+ */
35
47
  type: "coupon" | "deal" | "auto";
36
48
  discountAmount: number;
37
49
  scope?: "admin" | "seller";
38
50
  storeId?: string;
39
51
  }[];
40
52
  }
53
+ /**
54
+ * What the CART-level call returns: the same totals, plus the per-group
55
+ * breakdown.
56
+ *
57
+ * 🛑 A separate interface rather than one optional field on the result above.
58
+ * `computeGroupCouponDiscount` scores ONE group, so it has no per-group list
59
+ * to report and never will — giving it an empty `perGroup` would be a value
60
+ * that type-checks and means nothing, and the capture route's points tender
61
+ * reads this array positionally. A caller that gets `[]` where it expected one
62
+ * entry per group apportions every point against group zero.
63
+ */
64
+ export interface CartCouponDiscountResult extends GroupCouponDiscountResult {
65
+ /**
66
+ * The per-group discount, in the order the groups were passed.
67
+ *
68
+ * 🛑 Surfaced rather than aggregated-and-forgotten. It was already being
69
+ * computed inside the loop below and then summed away — the same shape as
70
+ * `CheckoutPricingPreviewStore`'s per-store fees, which existed only as an
71
+ * aggregate until a seller card needed them.
72
+ *
73
+ * The caller that needs it is the capture route's points tender: the
74
+ * per-group base includes that group's own discount, and re-deriving it
75
+ * there would be a second copy of the apportionment rule (Root Cause #75) on
76
+ * the one path D37 proves gets left behind.
77
+ */
78
+ perGroup: {
79
+ storeId: string | undefined;
80
+ couponDiscount: number;
81
+ }[];
82
+ }
41
83
  /**
42
84
  * Prorates every buyer-applied coupon against one seller-group's total.
43
85
  *
@@ -70,4 +112,4 @@ export declare function computeCartCouponDiscount(appliedCoupons: CartAppliedCou
70
112
  itemId: string;
71
113
  lineTotal: number;
72
114
  }[];
73
- }[]): GroupCouponDiscountResult;
115
+ }[]): CartCouponDiscountResult;
@@ -42,6 +42,20 @@ export function computeGroupCouponDiscount(appliedCoupons, groupLines, groupTota
42
42
  let couponDiscount = 0;
43
43
  const appliedDiscounts = [];
44
44
  for (const coupon of appliedCoupons) {
45
+ /*
46
+ * 🛑 D51 — a `points_earn` coupon DISCOUNTS NOTHING. It awards points on
47
+ * the post-discount amount paid, and it carries `pointsPercent` rather than
48
+ * a discount value.
49
+ *
50
+ * Skipped here rather than relied on to carry `discountAmount: 0`. The
51
+ * apply route writes whatever `validateCouponForCart` returned, and a
52
+ * points coupon that ever came back with a non-zero `discountAmount` —
53
+ * through a mis-seeded `discount.value`, or an admin editing a coupon's
54
+ * type after creation — would silently discount the order AND award points.
55
+ * A type check cannot be bypassed by bad data; a zero cannot be relied on.
56
+ */
57
+ if (coupon.type === "points_earn")
58
+ continue;
45
59
  let couponGroupDiscount = 0;
46
60
  const isSellerScoped = coupon.scope === "seller" && coupon.storeId;
47
61
  if (isSellerScoped) {
@@ -93,11 +107,22 @@ export function computeCartCouponDiscount(appliedCoupons, groups) {
93
107
  const cartSubtotal = groups.reduce((sum, g) => sum + g.lines.reduce((s, l) => s + l.lineTotal, 0), 0);
94
108
  let couponDiscount = 0;
95
109
  const appliedDiscounts = [];
110
+ const perGroup = [];
96
111
  for (const group of groups) {
97
112
  const groupTotal = group.lines.reduce((s, l) => s + l.lineTotal, 0);
98
113
  const res = computeGroupCouponDiscount(appliedCoupons, group.lines, groupTotal, cartSubtotal, group.storeId);
99
114
  couponDiscount += res.couponDiscount;
100
115
  appliedDiscounts.push(...res.appliedDiscounts);
116
+ /*
117
+ * 🛑 NOT rounded here, and the aggregate below IS. Rounding each group
118
+ * then summing can differ from summing then rounding by a paisa per group,
119
+ * and the aggregate is what the gateway captures — so the group figures
120
+ * stay raw and the caller rounds its own composed base once, exactly as
121
+ * `createOrderForGroup` does with a single `roundRupees` on `orderTotal`
122
+ * (D42). A rounded-then-summed discount is how a total ends up a paisa
123
+ * short of its own lines.
124
+ */
125
+ perGroup.push({ storeId: group.storeId, couponDiscount: res.couponDiscount });
101
126
  }
102
- return { couponDiscount: roundRupees(couponDiscount), appliedDiscounts };
127
+ return { couponDiscount: roundRupees(couponDiscount), appliedDiscounts, perGroup };
103
128
  }