@mohasinac/appkit 4.62.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 (204) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/server/features/checkout/actions.d.ts +80 -0
  3. package/dist/_internal/server/features/checkout/actions.js +542 -26
  4. package/dist/_internal/server/features/invoicing/allocate.d.ts +63 -0
  5. package/dist/_internal/server/features/invoicing/allocate.js +162 -0
  6. package/dist/_internal/server/features/invoicing/credit-note.d.ts +57 -0
  7. package/dist/_internal/server/features/invoicing/credit-note.js +126 -0
  8. package/dist/_internal/server/features/invoicing/gstr1.d.ts +74 -0
  9. package/dist/_internal/server/features/invoicing/gstr1.js +251 -0
  10. package/dist/_internal/server/features/invoicing/issue.d.ts +96 -0
  11. package/dist/_internal/server/features/invoicing/issue.js +202 -0
  12. package/dist/_internal/server/features/orders/actions.d.ts +8 -1
  13. package/dist/_internal/server/features/orders/actions.js +81 -8
  14. package/dist/_internal/server/features/orders/adapters.js +33 -0
  15. package/dist/_internal/server/features/orders/cancellation-refund.d.ts +15 -9
  16. package/dist/_internal/server/features/orders/cancellation-refund.js +15 -9
  17. package/dist/_internal/server/features/points/award.js +46 -4
  18. package/dist/_internal/server/features/refunds/actions.js +26 -0
  19. package/dist/_internal/server/features/shipping/courier-block.d.ts +69 -0
  20. package/dist/_internal/server/features/shipping/courier-block.js +239 -0
  21. package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
  22. package/dist/_internal/server/features/site-settings/adapters.js +15 -0
  23. package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
  24. package/dist/_internal/server/features/stores/adapters.js +51 -0
  25. package/dist/_internal/server/functions/scheduled.js +38 -1
  26. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +9 -2
  27. package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +4 -0
  28. package/dist/_internal/server/jobs/core/dailyStatusDigest.js +19 -2
  29. package/dist/_internal/server/jobs/core/index.d.ts +2 -1
  30. package/dist/_internal/server/jobs/core/index.js +2 -1
  31. package/dist/_internal/server/jobs/core/onJobCreated.d.ts +30 -7
  32. package/dist/_internal/server/jobs/core/onJobCreated.js +78 -45
  33. package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +7 -0
  34. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +62 -0
  35. package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +33 -0
  36. package/dist/_internal/server/jobs/core/runJobOnce.d.ts +48 -0
  37. package/dist/_internal/server/jobs/core/runJobOnce.js +195 -0
  38. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +10 -2
  39. package/dist/_internal/server/jobs/handlers/onOrderStatusChange.js +17 -0
  40. package/dist/_internal/server/notifications/channel-health.d.ts +26 -3
  41. package/dist/_internal/shared/actions/action-registry.js +28 -0
  42. package/dist/_internal/shared/checkout/coupon-discount.d.ts +43 -1
  43. package/dist/_internal/shared/checkout/coupon-discount.js +26 -1
  44. package/dist/_internal/shared/checkout/order-math.d.ts +145 -0
  45. package/dist/_internal/shared/checkout/order-math.js +173 -0
  46. package/dist/_internal/shared/checkout/refundable-base.d.ts +25 -0
  47. package/dist/_internal/shared/checkout/refundable-base.js +13 -1
  48. package/dist/_internal/shared/checkout/split.d.ts +87 -0
  49. package/dist/_internal/shared/checkout/split.js +100 -0
  50. package/dist/_internal/shared/content/disclaimers.d.ts +88 -0
  51. package/dist/_internal/shared/content/disclaimers.js +217 -0
  52. package/dist/_internal/shared/content/published-values.d.ts +88 -0
  53. package/dist/_internal/shared/content/published-values.js +198 -0
  54. package/dist/_internal/shared/features/admin/integrations.d.ts +109 -0
  55. package/dist/_internal/shared/features/admin/integrations.js +253 -0
  56. package/dist/_internal/shared/features/orders/dispute-reasons.d.ts +113 -0
  57. package/dist/_internal/shared/features/orders/dispute-reasons.js +181 -0
  58. package/dist/_internal/shared/features/orders/refund-copy.d.ts +1 -1
  59. package/dist/_internal/shared/features/orders/refund-copy.js +18 -1
  60. package/dist/_internal/shared/features/points/accepts.d.ts +36 -0
  61. package/dist/_internal/shared/features/points/accepts.js +43 -0
  62. package/dist/_internal/shared/features/shipping/shared-purchase.d.ts +91 -0
  63. package/dist/_internal/shared/features/shipping/shared-purchase.js +108 -0
  64. package/dist/_internal/shared/features/site-settings/types.d.ts +11 -0
  65. package/dist/_internal/shared/fees/calculator.d.ts +33 -1
  66. package/dist/_internal/shared/fees/calculator.js +32 -2
  67. package/dist/client.d.ts +11 -0
  68. package/dist/client.js +30 -0
  69. package/dist/constants/api-endpoints.d.ts +42 -0
  70. package/dist/constants/api-endpoints.js +14 -0
  71. package/dist/constants/field-names.d.ts +1 -0
  72. package/dist/constants/field-names.js +5 -0
  73. package/dist/contracts/index.d.ts +1 -1
  74. package/dist/contracts/shipping.d.ts +45 -0
  75. package/dist/contracts/shipping.js +15 -0
  76. package/dist/features/about/components/AboutView.js +1 -1
  77. package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -1
  78. package/dist/features/account/components/UserOrdersView.js +19 -33
  79. package/dist/features/admin/components/AdminCouponEditorView.js +17 -0
  80. package/dist/features/admin/components/AdminInvoicingView.d.ts +2 -0
  81. package/dist/features/admin/components/AdminInvoicingView.js +65 -0
  82. package/dist/features/admin/components/AdminOrderEditorView.d.ts +18 -1
  83. package/dist/features/admin/components/AdminOrderEditorView.js +11 -2
  84. package/dist/features/admin/components/AdminOrdersGuideView.js +23 -2
  85. package/dist/features/admin/components/AdminOrdersView.js +67 -5
  86. package/dist/features/admin/components/OwnerQuickActions.js +24 -2
  87. package/dist/features/admin/constants/filter-tabs.d.ts +6 -0
  88. package/dist/features/admin/constants/filter-tabs.js +6 -0
  89. package/dist/features/admin/constants/site-settings-tabs.d.ts +1 -1
  90. package/dist/features/admin/hooks/useAdminListingData.js +16 -0
  91. package/dist/features/admin/schemas/admin-user-form.d.ts +2 -2
  92. package/dist/features/admin/schemas/firestore.d.ts +30 -0
  93. package/dist/features/admin/schemas/firestore.js +5 -0
  94. package/dist/features/audit-log/schemas/firestore.d.ts +30 -0
  95. package/dist/features/audit-log/schemas/firestore.js +15 -0
  96. package/dist/features/auth/permissions/constants.js +13 -0
  97. package/dist/features/cart/components/CartPriceBreakdown.d.ts +13 -0
  98. package/dist/features/cart/components/CartPriceBreakdown.js +6 -2
  99. package/dist/features/cart/components/PointsTenderPicker.d.ts +61 -0
  100. package/dist/features/cart/components/PointsTenderPicker.js +15 -0
  101. package/dist/features/cart/components/StoreAddonsPicker.js +2 -1
  102. package/dist/features/cart/components/index.d.ts +2 -0
  103. package/dist/features/cart/components/index.js +1 -0
  104. package/dist/features/cart/repository/cart.repository.d.ts +19 -0
  105. package/dist/features/cart/repository/cart.repository.js +27 -0
  106. package/dist/features/cart/schemas/firestore.d.ts +92 -0
  107. package/dist/features/contact/email.d.ts +27 -10
  108. package/dist/features/contact/email.js +20 -3
  109. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +38 -0
  110. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +80 -0
  111. package/dist/features/dispute-reasons/schemas/firestore.d.ts +64 -0
  112. package/dist/features/dispute-reasons/schemas/firestore.js +40 -0
  113. package/dist/features/faq/components/FAQAccordion.d.ts +13 -1
  114. package/dist/features/faq/components/FAQAccordion.js +29 -4
  115. package/dist/features/invoicing/schemas/firestore.d.ts +112 -0
  116. package/dist/features/invoicing/schemas/firestore.js +45 -0
  117. package/dist/features/invoicing/utils/financial-year.d.ts +43 -0
  118. package/dist/features/invoicing/utils/financial-year.js +53 -0
  119. package/dist/features/jobs/actions/enqueue-job.js +18 -1
  120. package/dist/features/jobs/repository/jobs.repository.d.ts +62 -1
  121. package/dist/features/jobs/repository/jobs.repository.js +84 -1
  122. package/dist/features/jobs/schemas/firestore.d.ts +80 -0
  123. package/dist/features/jobs/schemas/firestore.js +37 -0
  124. package/dist/features/lottery/schemas/zod.d.ts +6 -6
  125. package/dist/features/media/MediaImage.js +22 -1
  126. package/dist/features/media/upload/ImageUpload.js +1 -1
  127. package/dist/features/obligations/constants/policies.js +24 -1
  128. package/dist/features/orders/components/OrderPaymentSummary.js +10 -1
  129. package/dist/features/orders/components/OrderProvenanceCard.d.ts +8 -0
  130. package/dist/features/orders/components/OrderProvenanceCard.js +185 -0
  131. package/dist/features/orders/components/RaiseDisputeForm.d.ts +14 -0
  132. package/dist/features/orders/components/RaiseDisputeForm.js +111 -0
  133. package/dist/features/orders/components/RefundRequestView.js +19 -1
  134. package/dist/features/orders/components/index.d.ts +4 -0
  135. package/dist/features/orders/components/index.js +2 -0
  136. package/dist/features/orders/constants/order-lane-tabs.d.ts +69 -0
  137. package/dist/features/orders/constants/order-lane-tabs.js +88 -0
  138. package/dist/features/orders/repository/orders.repository.d.ts +57 -0
  139. package/dist/features/orders/repository/orders.repository.js +145 -0
  140. package/dist/features/orders/schemas/firestore.d.ts +196 -1
  141. package/dist/features/orders/schemas/index.d.ts +54 -6
  142. package/dist/features/orders/schemas/index.js +12 -0
  143. package/dist/features/orders/types/index.d.ts +26 -0
  144. package/dist/features/orders/utils/courier-block.d.ts +25 -0
  145. package/dist/features/orders/utils/courier-block.js +23 -0
  146. package/dist/features/payment-cases/schemas/firestore.d.ts +26 -1
  147. package/dist/features/payment-cases/schemas/firestore.js +1 -0
  148. package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
  149. package/dist/features/products/components/ProductDetailPageView.d.ts +17 -1
  150. package/dist/features/products/components/ProductDetailPageView.js +2 -2
  151. package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
  152. package/dist/features/promotions/actions/coupon-stacking.d.ts +30 -3
  153. package/dist/features/promotions/actions/coupon-stacking.js +21 -2
  154. package/dist/features/promotions/components/CouponCard.js +13 -0
  155. package/dist/features/promotions/constants/coupon-help.d.ts +2 -2
  156. package/dist/features/promotions/constants/coupon-help.js +9 -1
  157. package/dist/features/promotions/schemas/firestore.d.ts +24 -0
  158. package/dist/features/promotions/schemas/firestore.js +4 -0
  159. package/dist/features/promotions/types/index.d.ts +19 -1
  160. package/dist/features/scams/components/ScamProfileView.js +2 -2
  161. package/dist/features/scams/components/ScamRegistryView.js +2 -2
  162. package/dist/features/search/action-index/tab-entries.js +1 -1
  163. package/dist/features/seller/actions/seller-actions.js +33 -3
  164. package/dist/features/seller/components/SellerOrdersView.js +129 -27
  165. package/dist/features/seller/hooks/useSellerListingData.js +25 -0
  166. package/dist/features/shipments/schemas/validation.d.ts +4 -4
  167. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +43 -0
  168. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +90 -0
  169. package/dist/features/shipping-boxes/schemas/firestore.d.ts +82 -0
  170. package/dist/features/shipping-boxes/schemas/firestore.js +53 -0
  171. package/dist/features/store-extensions/schemas/report-create-form.d.ts +2 -2
  172. package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +2 -2
  173. package/dist/features/stores/components/StoreOrdersGuideView.js +6 -1
  174. package/dist/features/stores/schemas/firestore.d.ts +71 -0
  175. package/dist/features/stores/types/index.d.ts +19 -0
  176. package/dist/features/support/schemas/index.d.ts +6 -6
  177. package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
  178. package/dist/index.d.ts +6 -1
  179. package/dist/index.js +14 -1
  180. package/dist/next/routing/route-map.d.ts +20 -0
  181. package/dist/next/routing/route-map.js +10 -0
  182. package/dist/providers/shipping-shiprocket/index.d.ts +27 -1
  183. package/dist/providers/shipping-shiprocket/index.js +84 -1
  184. package/dist/schemas/registry.d.ts +47 -18
  185. package/dist/schemas/sieve.d.ts +2 -2
  186. package/dist/server-entry.d.ts +2 -2
  187. package/dist/server-entry.js +8 -1
  188. package/dist/server.d.ts +10 -1
  189. package/dist/server.js +39 -1
  190. package/dist/styles.css +64 -80
  191. package/dist/tailwind-utilities.css +1 -1
  192. package/dist/tokens/themes/default-dark.js +1 -0
  193. package/dist/tokens/themes/default-light.js +1 -0
  194. package/dist/tokens/themes/required.d.ts +1 -1
  195. package/dist/tokens/themes/required.js +1 -0
  196. package/dist/tokens/tokens.css +15 -0
  197. package/dist/ui/components/Disclaimer.d.ts +19 -0
  198. package/dist/ui/components/Disclaimer.js +74 -0
  199. package/dist/ui/components/tabs-overflow.js +64 -11
  200. package/dist/ui/index.d.ts +2 -0
  201. package/dist/ui/index.js +6 -0
  202. package/dist/ui/rich-text/RichText.style.css +48 -79
  203. package/package.json +1 -1
  204. package/scripts/seed-cli.mjs +1831 -1822
@@ -2,6 +2,16 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { ShoppingBag, Hash, Scale, Wallet, RotateCcw, DollarSign, Gavel } from "lucide-react";
3
3
  import { Code, Div, Heading, Li, Row, Section, Span, Stack, Text, Ul } from "../../../ui/index.js";
4
4
  import { GC } from "../../_guide-cls.js";
5
+ import { resolvePublishedValues } from "../../../_internal/shared/content/published-values.js";
6
+ /*
7
+ * Phase 9 — read from the published-value registry rather than restated.
8
+ *
9
+ * 🛑 No settings argument, deliberately: this is the OBLIGATION-derived window
10
+ * and it has no settings override, so passing settings would imply an
11
+ * admin-editable number that does not exist (the 18-dead-settings pattern,
12
+ * from the "control that appears to do something the code ignores" direction).
13
+ */
14
+ const { returnRespondHours: RETURN_RESPOND_HOURS, refundBusinessDaysMin: REFUND_DAYS_MIN, refundBusinessDaysMax: REFUND_DAYS_MAX, } = resolvePublishedValues();
5
15
  export function AdminOrdersGuideView() {
6
16
  return (_jsxs(Stack, { className: "max-w-3xl mx-auto", padding: "b-2xl", gap: "xl", children: [_jsxs(Section, { children: [_jsxs(Row, { className: "mb-2", align: "center", gap: "3", children: [_jsx(Row, { className: "flex-shrink-0 w-10 h-10 [background:linear-gradient(135deg,var(--appkit-color-primary-700)_0%,var(--appkit-color-cobalt)_100%)]", align: "center", justify: "center", rounded: "xl", children: _jsx(ShoppingBag, { className: "w-5 h-5 text-white" }) }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] tracking-widest", size: "sm", weight: "semibold", transform: "uppercase", children: "Admin Guide" })] }), _jsx(Heading, { level: 1, className: "text-[var(--appkit-color-text)] mb-2", mdSize: "3xl", size: "2xl", weight: "bold", children: "Orders & Finance" }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", children: "Order statuses, payouts, disputes, returns, and commission math on LetItRip." })] }), [
7
17
  {
@@ -11,7 +21,18 @@ export function AdminOrdersGuideView() {
11
21
  ["PROCESSING", "Seller has acknowledged and is packing."],
12
22
  ["SHIPPED", "Dispatched. Tracking number provided by seller."],
13
23
  ["DELIVERED", "Carrier confirmed delivery."],
14
- ["RETURN_REQUESTED", "Buyer opened a return. Seller has 3 days to respond."],
24
+ /*
25
+ * 🛑 Phase 9, decision 23 — this said "3 days" while the FAQ
26
+ * said 48 hours, and the FAQ WINS: it is the buyer-facing
27
+ * promise and the one a buyer has already read. The number now
28
+ * comes from `OBLIGATION_POLICIES["return-respond"]`, which is
29
+ * what actually closes the window — so a staff-facing guide can
30
+ * no longer quietly contradict what the buyer was told.
31
+ */
32
+ [
33
+ "RETURN_REQUESTED",
34
+ `Buyer opened a return. Seller has ${RETURN_RESPOND_HOURS} hours to respond.`,
35
+ ],
15
36
  ["REFUNDED", "Return accepted and refund issued via Razorpay."],
16
37
  ["CANCELLED", "Cancelled before shipping. Payment returned."],
17
38
  ].map(([status, note]) => (_jsxs(Row, { gap: "3", children: [_jsx(Text, { className: "flex-shrink-0 font-mono text-[var(--appkit-color-primary)] w-40", size: "xs", weight: "semibold", children: status }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", children: note })] }, status))), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] pt-[0.5rem]", size: "sm", children: "Admin-only transitions: force-cancel (any status before DELIVERED), force-refund (post-DELIVERED dispute)." })] })),
@@ -30,7 +51,7 @@ export function AdminOrdersGuideView() {
30
51
  },
31
52
  {
32
53
  Icon: RotateCcw, title: "Return Requests",
33
- content: (_jsxs(Ul, { className: GC.listMuted, children: [_jsx(Li, { children: "Admin can view the return reason and attached photos on the order detail page." }), _jsxs(Li, { children: [_jsx(Span, { weight: "bold", children: "When to override a seller rejection" }), ": Only when photographic evidence clearly supports the buyer's claim of a significant not-as-described defect. Do not override for buyer's remorse."] }), _jsx(Li, { children: "After admin override: refund is issued automatically via Razorpay (3\u20137 business days for card, 1\u20132 for UPI). Seller payout for that order is withheld." })] })),
54
+ content: (_jsxs(Ul, { className: GC.listMuted, children: [_jsx(Li, { children: "Admin can view the return reason and attached photos on the order detail page." }), _jsxs(Li, { children: [_jsx(Span, { weight: "bold", children: "When to override a seller rejection" }), ": Only when photographic evidence clearly supports the buyer's claim of a significant not-as-described defect. Do not override for buyer's remorse."] }), _jsxs(Li, { children: ["After admin override: the refund is issued automatically (", REFUND_DAYS_MIN, "\u2013", REFUND_DAYS_MAX, " business days for card, 1\u20132 for UPI). Seller payout for that order is withheld."] })] })),
34
55
  },
35
56
  {
36
57
  Icon: DollarSign, title: "Commission & Settlement Math",
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { groupOrderItemsByLine } from "../../orders/utils/bundle-grouping.js";
3
4
  import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit/client";
4
5
  import { sortBy } from "@mohasinac/appkit/client";
5
6
  import React, { useState, useCallback } from "react";
@@ -61,14 +62,70 @@ function toOrderItemRows(item) {
61
62
  quantity: typeof entry.quantity === "number" ? entry.quantity : 1,
62
63
  unitPrice: typeof entry.unitPrice === "number" ? entry.unitPrice : 0,
63
64
  totalPrice: typeof entry.totalPrice === "number" ? entry.totalPrice : 0,
65
+ /*
66
+ * The collapse keys, carried so the editor can group a bundle or
67
+ * buyer-assembled group back into one line the way the buyer's receipt
68
+ * does. Narrowed by `typeof` like every field above rather than cast —
69
+ * this function's whole job is turning a loose record into a typed row,
70
+ * and a cast here is how `title`/`price` once slipped through (RC#45).
71
+ */
72
+ groupSlug: typeof entry.groupSlug === "string" ? entry.groupSlug : undefined,
73
+ groupTitle: typeof entry.groupTitle === "string" ? entry.groupTitle : undefined,
74
+ bundleCategorySlug: typeof entry.bundleCategorySlug === "string" ? entry.bundleCategorySlug : undefined,
75
+ bundleProductIds: Array.isArray(entry.bundleProductIds)
76
+ ? entry.bundleProductIds.filter((v) => typeof v === "string")
77
+ : undefined,
64
78
  }));
65
79
  }
80
+ /*
81
+ * The row's headline, counted in LINES rather than item rows.
82
+ *
83
+ * 🛑 `extraCount` deliberately changed meaning when grouping landed: it was
84
+ * `items.length - 1`, so a bundle of four read "Dranzer S +3 more" — naming an
85
+ * arbitrary member and implying four separate things were bought. It is now
86
+ * `groups.length - 1`, so the same order reads "Starter Bundle +0" and a
87
+ * bundle plus one standalone reads "+1 more". The number a buyer or admin
88
+ * reads on a row should match the number of lines they will see inside it.
89
+ */
90
+ /*
91
+ * `sourceContext` arrives as JsonValue off the raw admin-list doc, so it can
92
+ * be a string, an array or null — none of which `OrderProvenanceCard` can
93
+ * switch on. This narrows to "an object carrying a `path` tag" and nothing
94
+ * further: that is deliberately NOT the card's own switch duplicated, it only
95
+ * establishes that there is a tag to switch on, which is what makes the single
96
+ * cast true instead of hopeful. `as unknown as OrderSourceContext` would
97
+ * assert exactly the same thing while checking nothing (Root Cause #98).
98
+ */
99
+ function toSourceContext(value) {
100
+ if (!value || typeof value !== "object" || Array.isArray(value))
101
+ return undefined;
102
+ /*
103
+ * Via `object` rather than straight off the JsonValue union, for two
104
+ * reasons that pull the same way. `Array.isArray` does not narrow
105
+ * `JsonArray | JsonObject` apart, so `value.path` does not resolve; and
106
+ * `OrderSourceContext` is not assignable to `JsonObject` (its optional
107
+ * members admit `undefined`, which is not a JsonValue), so TS refuses that
108
+ * conversion outright. Probing as `{ path?: string }` keeps both the probe
109
+ * and the assertion legal without an `unknown` anywhere.
110
+ */
111
+ const obj = value;
112
+ return typeof obj.path === "string"
113
+ ? obj
114
+ : undefined;
115
+ }
66
116
  function firstOrderItem(item) {
67
- const items = toOrderItemRows(item);
117
+ const groups = groupOrderItemsByLine(toOrderItemRows(item));
118
+ const first = groups[0];
119
+ const extraCount = Math.max(0, groups.length - 1);
120
+ if (!first)
121
+ return { extraCount: 0 };
122
+ if (first.kind === "single") {
123
+ return { image: first.item.image, title: first.item.title, extraCount };
124
+ }
68
125
  return {
69
- image: items[0]?.image,
70
- title: items[0]?.title,
71
- extraCount: Math.max(0, items.length - 1),
126
+ image: first.items[0]?.item.image,
127
+ title: first.groupTitle ?? "Grouped purchase",
128
+ extraCount,
72
129
  };
73
130
  }
74
131
  /**
@@ -206,6 +263,11 @@ export function AdminOrdersView({ children, ...props }) {
206
263
  couponCode: toStringValue(item.couponCode) || undefined,
207
264
  couponDiscount: Number(item.couponDiscount ?? 0) || undefined,
208
265
  },
266
+ // Raw doc in, so no serializer entry — but unlike `addons` above,
267
+ // which coerces every field by hand, this one needs a tag check
268
+ // before it can be trusted. See `toSourceContext`.
269
+ sourceContext: toSourceContext(item.sourceContext),
270
+ currency: toStringValue(item.currency, "") || undefined,
209
271
  };
210
272
  }),
211
273
  getTotal: (response, mappedRows) => typeof response.meta?.total === "number" ? response.meta.total : mappedRows.length,
@@ -289,5 +351,5 @@ export function AdminOrdersView({ children, ...props }) {
289
351
  ] })),
290
352
  renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsxs(_Fragment, { children: [_jsx(FilterChipGroup, { label: "Status", tabs: ADMIN_ORDER_STATUS_TABS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id, paymentReview: "" })) }), _jsx(FilterChipGroup, { label: "Manual payment", tabs: ADMIN_ORDER_PAYMENT_REVIEW_TABS, value: pendingFilters.paymentReview ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, paymentReview: id, status: "" })) })] })),
291
353
  };
292
- return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(AdminOrderEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), orderId: selectedRow?.id, orderLabel: selectedRow?.primary, currentStatus: selectedRow?.status, items: selectedRow?.items, paymentProofUrl: selectedRow?.paymentProofUrl, paymentTransactionId: selectedRow?.paymentTransactionId, paymentMethod: selectedRow?.paymentMethod, paymentStatus: selectedRow?.paymentStatus, displayedUpiId: selectedRow?.displayedUpiId, buyerReportedUpiId: selectedRow?.buyerReportedUpiId, paymentUpiMismatch: selectedRow?.paymentUpiMismatch, buyerMarkedPaid: selectedRow?.buyerMarkedPaid, buyerFraudAgreementAccepted: selectedRow?.buyerFraudAgreementAccepted, paymentReviewOutcome: selectedRow?.paymentReviewOutcome, addons: selectedRow?.addons })] }));
354
+ return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(AdminOrderEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), orderId: selectedRow?.id, orderLabel: selectedRow?.primary, currentStatus: selectedRow?.status, items: selectedRow?.items, paymentProofUrl: selectedRow?.paymentProofUrl, paymentTransactionId: selectedRow?.paymentTransactionId, paymentMethod: selectedRow?.paymentMethod, paymentStatus: selectedRow?.paymentStatus, displayedUpiId: selectedRow?.displayedUpiId, buyerReportedUpiId: selectedRow?.buyerReportedUpiId, paymentUpiMismatch: selectedRow?.paymentUpiMismatch, buyerMarkedPaid: selectedRow?.buyerMarkedPaid, buyerFraudAgreementAccepted: selectedRow?.buyerFraudAgreementAccepted, paymentReviewOutcome: selectedRow?.paymentReviewOutcome, addons: selectedRow?.addons, sourceContext: selectedRow?.sourceContext, currency: selectedRow?.currency })] }));
293
355
  }
@@ -236,6 +236,28 @@ export function OwnerQuickActions({ resource, editHref, onDelete, onClone, owner
236
236
  * 🛑 `inline-flex`, never `w-full`. In normal flow this must shrink to its
237
237
  * own content — a full-width bar would read as a section divider and would
238
238
  * stretch across a card body it is only a corner of.
239
+ *
240
+ * 🛑 `shrink-0` IS THE MOBILE FIX, and it is one word because the bug was
241
+ * one missing word.
242
+ *
243
+ * A flex item's default is `flex-shrink: 1`, so in the title/actions row
244
+ * this toolbar lives in — `<Row justify="between">` with an `<h1>` on the
245
+ * left — the heading and the toolbar compete for width, and at ~390px the
246
+ * toolbar LOSES. It compresses below its own content: the icons slide under
247
+ * each other, the padding collapses, and the 44px tap targets stop being
248
+ * 44px wide. That is the "works incorrectly on mobile" report.
249
+ *
250
+ * With `shrink-0` the toolbar keeps its intrinsic size and the HEADING gives
251
+ * way instead — which is right, because a heading wraps gracefully onto a
252
+ * second line and a row of hit targets does not.
253
+ *
254
+ * 🛑 Note what this is NOT: the desktop fix (`d94496c42`) gave the toolbar a
255
+ * container and added an audit whose two rules are about DOM ORDER — must
256
+ * come after the `<h1>`, must not share the heading's row unless that row is
257
+ * `justify="between"`. Both pass here. R2's exemption actively WHITELISTS
258
+ * the layout that breaks, so the surfaces that satisfy that audit are
259
+ * precisely the ones most likely to be wrong at phone width. A source-order
260
+ * rule cannot see a width problem.
239
261
  */
240
262
  const boxClass = onDark
241
263
  ? /*
@@ -244,12 +266,12 @@ export function OwnerQuickActions({ resource, editHref, onDelete, onClone, owner
244
266
  * LIGHT background in every light theme, so a themed box on a dark hero
245
267
  * photo is a white rectangle over the artwork.
246
268
  */
247
- "inline-flex rounded-[var(--appkit-radius-lg)] bg-black/55 backdrop-blur-sm p-[var(--appkit-space-1)] shadow-md"
269
+ "inline-flex shrink-0 rounded-[var(--appkit-radius-lg)] bg-black/55 backdrop-blur-sm p-[var(--appkit-space-1)] shadow-md"
248
270
  : /*
249
271
  * Off media: the real surface tokens, so it inverts with the theme
250
272
  * instead of being a fixed near-white that disappears in dark mode.
251
273
  */
252
- "inline-flex rounded-[var(--appkit-radius-lg)] bg-[var(--appkit-color-surface-elevated)] border border-[var(--appkit-color-border)] p-[var(--appkit-space-1)] shadow-sm";
274
+ "inline-flex shrink-0 rounded-[var(--appkit-radius-lg)] bg-[var(--appkit-color-surface-elevated)] border border-[var(--appkit-color-border)] p-[var(--appkit-space-1)] shadow-sm";
253
275
  return (_jsxs(_Fragment, { children: [_jsxs(Row, { gap: "xs", align: "center", className: `${boxClass} ${className}`.trim(),
254
276
  /*
255
277
  * Stops the click reaching an ancestor `<Link>` or the card's own
@@ -457,6 +457,9 @@ export declare const ADMIN_COUPON_TYPE_TABS: readonly [{
457
457
  }, {
458
458
  readonly id: "buy_x_get_y";
459
459
  readonly label: "Buy X Get Y";
460
+ }, {
461
+ readonly id: "points_earn";
462
+ readonly label: "Points Back";
460
463
  }];
461
464
  /** Empty-sentinel variant for views (like SellerBidsView) that drive the
462
465
  * filter URL with `""` instead of `"All"` so the `"" == no filter` invariant
@@ -757,6 +760,9 @@ export declare const ADMIN_PAYMENT_CASE_KIND_TABS: readonly [{
757
760
  }, {
758
761
  readonly id: "shipping_unpriced";
759
762
  readonly label: "Shipping unpriced";
763
+ }, {
764
+ readonly id: "courier_unavailable";
765
+ readonly label: "Courier unavailable";
760
766
  }, {
761
767
  readonly id: "dead_job";
762
768
  readonly label: "Dead job";
@@ -245,6 +245,11 @@ export const ADMIN_COUPON_TYPE_TABS = [
245
245
  { id: "fixed", label: "Fixed" },
246
246
  { id: "free_shipping", label: "Free Shipping" },
247
247
  { id: "buy_x_get_y", label: "Buy X Get Y" },
248
+ // D51. `audit-filter-tab-enums`' COVERAGE rule fails on a real union value
249
+ // with no chip — which is what it was extended to catch after ten such gaps
250
+ // were found at once (Root Cause #72), including sellers being unable to
251
+ // filter their own `in_review` listings.
252
+ { id: "points_earn", label: "Points Back" },
248
253
  ];
249
254
  // ---------------------------------------------------------------------------
250
255
  // Seller-dashboard filter-chip tab sets
@@ -441,6 +446,7 @@ export const ADMIN_PAYMENT_CASE_KIND_TABS = [
441
446
  { id: "protection_claim", label: "Protection claim" },
442
447
  { id: "grievance", label: "Grievance" },
443
448
  { id: "shipping_unpriced", label: "Shipping unpriced" },
449
+ { id: "courier_unavailable", label: "Courier unavailable" },
444
450
  { id: "dead_job", label: "Dead job" },
445
451
  { id: "ledger_drift", label: "Points drift" },
446
452
  ];
@@ -102,7 +102,7 @@ export declare const SITE_SETTINGS_TABS: readonly [{
102
102
  readonly description: "Which listing types sellers may create, and their defaults.";
103
103
  readonly keywords: ["listing types", "auctions on", "enable", "categories"];
104
104
  }];
105
- export declare const SITE_SETTINGS_TAB_IDS: ("about" | "shipping" | "notifications" | "contact" | "auction" | "seo" | "listings" | "appearance" | "emi" | "gst" | "watermark" | "integrations" | "limits" | "announcement" | "branding" | "themes" | "fees" | "legal" | "procurement")[];
105
+ export declare const SITE_SETTINGS_TAB_IDS: ("about" | "shipping" | "notifications" | "contact" | "auction" | "seo" | "listings" | "emi" | "legal" | "appearance" | "gst" | "watermark" | "integrations" | "limits" | "announcement" | "branding" | "themes" | "fees" | "procurement")[];
106
106
  /**
107
107
  * The union `?tab=` is validated against.
108
108
  *
@@ -2,6 +2,9 @@
2
2
  import { useQuery } from "@tanstack/react-query";
3
3
  import { apiClient, ApiClientError } from "../../../http/index.js";
4
4
  import { formatCurrency } from "../../../utils/number.formatter.js";
5
+ // The defining module, not a barrel (Root Cause #18). `hasEncPrefix` is a pure
6
+ // string test with no crypto in its import chain, so it is client-safe.
7
+ import { hasEncPrefix } from "../../../security/pii-mask.js";
5
8
  function withQueryParams(endpoint, params) {
6
9
  const hasQuery = endpoint.includes("?");
7
10
  const query = new URLSearchParams(params).toString();
@@ -50,6 +53,19 @@ export function toRecordArray(value) {
50
53
  }
51
54
  export function toStringValue(value, fallback = "-") {
52
55
  if (typeof value === "string") {
56
+ /*
57
+ * 🛑 Ciphertext is not a display value. See the twin in
58
+ * `features/seller/hooks/useSellerListingData.ts` for the full account —
59
+ * the short version is that `mapDoc` does not decrypt, so a route that
60
+ * forgets `revealOrders`/`maskedOrders` hands this a stored `enc:v1:…`
61
+ * blob which is a perfectly ordinary `string` and renders verbatim.
62
+ *
63
+ * `/api/admin/orders` already hit this ("measured live at 3 visible
64
+ * `enc:v1:` strings") and fixed it at the route. This is the backstop for
65
+ * the next one.
66
+ */
67
+ if (hasEncPrefix(value))
68
+ return fallback;
53
69
  return value.trim() || fallback;
54
70
  }
55
71
  if (typeof value === "number" || typeof value === "boolean") {
@@ -82,13 +82,13 @@ export declare const lotteryPullSchema: z.ZodObject<{
82
82
  userPhone: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
83
83
  }, "strip", z.ZodTypeAny, {
84
84
  transactionId: string;
85
- sourceType: "event" | "product";
85
+ sourceType: "product" | "event";
86
86
  paymentTime: string;
87
87
  purchasedItemNumber: number;
88
88
  userPhone?: string | undefined;
89
89
  }, {
90
90
  transactionId: string;
91
- sourceType: "event" | "product";
91
+ sourceType: "product" | "event";
92
92
  paymentTime: string;
93
93
  purchasedItemNumber: number;
94
94
  userPhone?: string | undefined;
@@ -690,6 +690,36 @@ export interface SiteSettingsDocument extends BaseDocument {
690
690
  returns: {
691
691
  windowDays: number;
692
692
  };
693
+ /**
694
+ * The support integration's on/off switch.
695
+ *
696
+ * 🛑 The ONLY new integration flag. The other three the admin page switches
697
+ * already have an authoritative field — `payment.razorpayEnabled`,
698
+ * `messaging.emailEnabled` (which `guardSend` reads), and
699
+ * `shipping.provider`. Adding a parallel `integrationSwitches.*` beside
700
+ * those would be a denormalised mirror that drifts the first time one path
701
+ * writes only one of a pair (Root Cause #42), and the page would then
702
+ * disagree with the code that enforces it. The page is a VIEW over the real
703
+ * fields; this is the one field that did not exist.
704
+ *
705
+ * 🛑 Also not under `siteSettings.integrations` — that key is live and holds
706
+ * analytics/tag IDs (GA, FB Pixel, GTM).
707
+ *
708
+ * OFF hides the WHOLE support surface from BUYERS: no nav entry, no ticket
709
+ * list, no contact form. Staff keep full access, so an open ticket is still
710
+ * workable and the employee pay-per-ticket basis survives.
711
+ *
712
+ * 🛑 A buyer mid-conversation loses sight of their thread. That is the
713
+ * owner's decision, accepted knowingly — and the admin row must SAY it
714
+ * rather than implying the switch only stops new tickets.
715
+ *
716
+ * Defaults to `true`: support being reachable is the safe state, and a
717
+ * settings document written before this field existed must not read as
718
+ * "support is off".
719
+ */
720
+ support: {
721
+ enabled: boolean;
722
+ };
693
723
  faq: {
694
724
  variables: {
695
725
  shippingDays: number;
@@ -377,6 +377,11 @@ export const DEFAULT_SITE_SETTINGS_DATA = {
377
377
  returns: {
378
378
  windowDays: 7,
379
379
  },
380
+ // Reachable is the safe default, and an older settings document with no
381
+ // `support` group must not read as "support is off".
382
+ support: {
383
+ enabled: true,
384
+ },
380
385
  announcementBar: {
381
386
  enabled: true,
382
387
  message: "🎉 Up to 15% Off on Pokémon TCG this week — Use code SAVE15",
@@ -44,6 +44,21 @@ export declare const AdminAuditActionValues: {
44
44
  * same class as `payout_mark_paid` and `coupon_update`, which are both here.
45
45
  */
46
46
  readonly POINTS_ADJUST: "points_adjust";
47
+ /**
48
+ * Staff added a buyer-facing dispute reason.
49
+ *
50
+ * 🛑 Here because the list is POLICY VOCABULARY, even though no single row
51
+ * decides money. The reasons are what buyers are offered when something goes
52
+ * wrong, so the wording shapes what gets reported and how it is triaged —
53
+ * and a badly-worded or leading option is the kind of thing that is only
54
+ * ever noticed later, by which time "who added this, and when" is the
55
+ * question. `createdByName` on the row answers it for the list; this answers
56
+ * it in the one place an admin looks for "what have staff done lately".
57
+ *
58
+ * Recorded only on a REAL creation. Re-adding wording that already exists
59
+ * returns the existing row, which is not an act worth logging.
60
+ */
61
+ readonly DISPUTE_REASON_CREATE: "dispute_reason_create";
47
62
  };
48
63
  export type AdminAuditAction = (typeof AdminAuditActionValues)[keyof typeof AdminAuditActionValues];
49
64
  export interface AdminAuditLogDocument extends BaseDocument {
@@ -97,5 +112,20 @@ export declare const ADMIN_AUDIT_LOG_FIELDS: {
97
112
  * same class as `payout_mark_paid` and `coupon_update`, which are both here.
98
113
  */
99
114
  readonly POINTS_ADJUST: "points_adjust";
115
+ /**
116
+ * Staff added a buyer-facing dispute reason.
117
+ *
118
+ * 🛑 Here because the list is POLICY VOCABULARY, even though no single row
119
+ * decides money. The reasons are what buyers are offered when something goes
120
+ * wrong, so the wording shapes what gets reported and how it is triaged —
121
+ * and a badly-worded or leading option is the kind of thing that is only
122
+ * ever noticed later, by which time "who added this, and when" is the
123
+ * question. `createdByName` on the row answers it for the list; this answers
124
+ * it in the one place an admin looks for "what have staff done lately".
125
+ *
126
+ * Recorded only on a REAL creation. Re-adding wording that already exists
127
+ * returns the existing row, which is not an act worth logging.
128
+ */
129
+ readonly DISPUTE_REASON_CREATE: "dispute_reason_create";
100
130
  };
101
131
  };
@@ -42,6 +42,21 @@ export const AdminAuditActionValues = {
42
42
  * same class as `payout_mark_paid` and `coupon_update`, which are both here.
43
43
  */
44
44
  POINTS_ADJUST: "points_adjust",
45
+ /**
46
+ * Staff added a buyer-facing dispute reason.
47
+ *
48
+ * 🛑 Here because the list is POLICY VOCABULARY, even though no single row
49
+ * decides money. The reasons are what buyers are offered when something goes
50
+ * wrong, so the wording shapes what gets reported and how it is triaged —
51
+ * and a badly-worded or leading option is the kind of thing that is only
52
+ * ever noticed later, by which time "who added this, and when" is the
53
+ * question. `createdByName` on the row answers it for the list; this answers
54
+ * it in the one place an admin looks for "what have staff done lately".
55
+ *
56
+ * Recorded only on a REAL creation. Re-adding wording that already exists
57
+ * returns the existing row, which is not an act worth logging.
58
+ */
59
+ DISPUTE_REASON_CREATE: "dispute_reason_create",
45
60
  };
46
61
  export const ADMIN_AUDIT_LOG_FIELDS = {
47
62
  ACTOR_UID: "actorUid",
@@ -344,6 +344,19 @@ export const ROUTE_PERMISSION_MAP = {
344
344
  // Payment protection — admin-only, see the permission union's own 🛑 note.
345
345
  "/admin/money-cases": "admin:payment-cases:read",
346
346
  "/admin/protection-claims": "admin:protection-claims:read",
347
+ /*
348
+ * Phase 7 invoicing — the GSTR-1 draft export.
349
+ *
350
+ * 🛑 `settings:write` rather than a new permission, and that is a judgement
351
+ * worth stating. The page exports the company's tax position for a quarter
352
+ * plus every buyer's place of supply; the set of people who should see that
353
+ * is the set who can already change the GSTIN and the fee schedule, which is
354
+ * exactly what `settings:write` names. Minting
355
+ * `admin:invoicing:read` would add a permission with one route and one
356
+ * holder — a distinction with no decision behind it, which is how a
357
+ * permission union becomes unreadable.
358
+ */
359
+ "/admin/invoicing": "admin:settings:write",
347
360
  };
348
361
  /**
349
362
  * Every capability, as a runtime value.
@@ -32,6 +32,11 @@ export interface CartPriceBreakdownStore {
32
32
  supportsShippingProtection: boolean;
33
33
  supportsSaleProtection: boolean;
34
34
  hasFinalSaleItem: boolean;
35
+ /** D46/D47 + decision 22 — whether this seller accepts points, and how many. */
36
+ acceptsPoints: boolean;
37
+ pointsApplied: number;
38
+ /** S5a — handling + extra shipping legs for this store's order. */
39
+ sharedPurchaseFee: number;
35
40
  }
36
41
  /** Mirrors `CheckoutPricingPreview` without importing server code. */
37
42
  export interface CartPriceBreakdownData {
@@ -46,7 +51,15 @@ export interface CartPriceBreakdownData {
46
51
  couponDiscount: number;
47
52
  platformFee: number;
48
53
  gstOnFee: number;
54
+ /** S5a — handling + extra shipping legs across the cart. */
55
+ sharedPurchaseFee: number;
49
56
  total: number;
57
+ /** D46/D47 — points applied across every accepting store. */
58
+ pointsApplied: number;
59
+ /** What the buyer actually pays: `total − pointsApplied`. */
60
+ amountDue: number;
61
+ /** Their remaining balance, for the toggle's own label. */
62
+ pointsBalance: number;
50
63
  stores: CartPriceBreakdownStore[];
51
64
  }
52
65
  export interface CartPriceBreakdownProps {
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { Div, Row, Stack, Text } from "../../../ui/index.js";
4
4
  import { formatCurrency } from "../../../utils/number.formatter.js";
5
5
  function Line({ label, amount, tone = "muted", strong = false, }) {
@@ -21,5 +21,9 @@ export function CartPriceBreakdown({ preview, itemCount, currency = "INR", unava
21
21
  const subtotalLabel = itemCount !== undefined
22
22
  ? `Subtotal (${itemCount} item${itemCount !== 1 ? "s" : ""}${storeCount > 1 ? `, ${storeCount} stores` : ""})`
23
23
  : "Subtotal";
24
- return (_jsxs(Stack, { gap: "xs", className: className, children: [_jsx(Line, { label: subtotalLabel, amount: money(preview.subtotal) }), preview.shippingFee > 0 && (_jsx(Line, { label: withStoreCount("Shipping", storeCount), amount: money(preview.shippingFee) })), preview.codHandlingFee > 0 && (_jsx(Line, { label: "COD handling fee", amount: money(preview.codHandlingFee) })), preview.giftWrapFee > 0 && (_jsx(Line, { label: withStoreCount("Gift wrap", countCharged((s) => s.giftWrapFee)), amount: money(preview.giftWrapFee) })), preview.shipmentProtectionFee > 0 && (_jsx(Line, { label: withStoreCount("Shipment protection", countCharged((s) => s.shipmentProtectionFee)), amount: money(preview.shipmentProtectionFee) })), preview.saleProtectionFee > 0 && (_jsx(Line, { label: withStoreCount("Sale protection", countCharged((s) => s.saleProtectionFee)), amount: money(preview.saleProtectionFee) })), preview.emiSurcharge > 0 && (_jsx(Line, { label: "EMI surcharge", amount: money(preview.emiSurcharge) })), preview.platformFee > 0 && _jsx(Line, { label: "Platform fee", amount: money(preview.platformFee) }), preview.gstOnFee > 0 && (_jsx(Line, { label: "GST on platform fee", amount: money(preview.gstOnFee) })), preview.couponDiscount > 0 && (_jsx(Line, { label: "Coupon discount", amount: `−${money(preview.couponDiscount)}`, tone: "success" })), _jsx(Div, { border: "top-subtle", paddingY: "t-sm", children: _jsx(Line, { label: "Total", amount: money(preview.total), tone: "primary", strong: true }) }), preview.gstAmount > 0 && (_jsxs(Text, { size: "xs", color: "muted", children: ["Includes GST of ", money(preview.gstAmount)] })), errorNote ? (_jsx(Text, { size: "xs", color: "error", children: errorNote })) : (isLoading && (_jsx(Text, { size: "xs", color: "muted", children: "Updating\u2026" })))] }));
24
+ return (_jsxs(Stack, { gap: "xs", className: className, children: [_jsx(Line, { label: subtotalLabel, amount: money(preview.subtotal) }), preview.shippingFee > 0 && (_jsx(Line, { label: withStoreCount("Shipping", storeCount), amount: money(preview.shippingFee) })), preview.codHandlingFee > 0 && (_jsx(Line, { label: "COD handling fee", amount: money(preview.codHandlingFee) })), preview.giftWrapFee > 0 && (_jsx(Line, { label: withStoreCount("Gift wrap", countCharged((s) => s.giftWrapFee)), amount: money(preview.giftWrapFee) })), preview.shipmentProtectionFee > 0 && (_jsx(Line, { label: withStoreCount("Shipment protection", countCharged((s) => s.shipmentProtectionFee)), amount: money(preview.shipmentProtectionFee) })), preview.saleProtectionFee > 0 && (_jsx(Line, { label: withStoreCount("Sale protection", countCharged((s) => s.saleProtectionFee)), amount: money(preview.saleProtectionFee) })), preview.emiSurcharge > 0 && (_jsx(Line, { label: "EMI surcharge", amount: money(preview.emiSurcharge) })), preview.sharedPurchaseFee > 0 && (_jsx(Line, { label: withStoreCount("Split delivery", countCharged((s) => s.sharedPurchaseFee)), amount: money(preview.sharedPurchaseFee) })), preview.platformFee > 0 && _jsx(Line, { label: "Platform fee", amount: money(preview.platformFee) }), preview.gstOnFee > 0 && (_jsx(Line, { label: "GST on platform fee", amount: money(preview.gstOnFee) })), preview.couponDiscount > 0 && (_jsx(Line, { label: "Coupon discount", amount: `−${money(preview.couponDiscount)}`, tone: "success" })), _jsx(Div, { border: "top-subtle", paddingY: "t-sm", children: _jsx(Line, { label: "Total", amount: money(preview.total), tone: "primary", strong: preview.pointsApplied <= 0 }) }), preview.pointsApplied > 0 && (_jsxs(_Fragment, { children: [_jsx(Line, { label: `Points applied (${preview.pointsApplied.toLocaleString("en-IN")} pts)`, amount: `−${money(preview.pointsApplied)}`, tone: "success" }), _jsx(Div, { border: "top-subtle", paddingY: "t-sm", children: _jsx(Line, { label: "Amount due", amount: money(preview.amountDue), tone: "primary", strong: true }) })] })), preview.pointsApplied > 0 && preview.pointsBalance > preview.pointsApplied && (_jsxs(Text, { size: "xs", color: "muted", children: [(preview.pointsBalance - preview.pointsApplied).toLocaleString("en-IN"), " points stay in your balance \u2014 up to half of an order can be paid with points."] })), preview.pointsApplied === 0 && preview.pointsBalance > 0 && (_jsx(Text, { size: "xs", color: "muted", children: preview.stores.some((s) => s.acceptsPoints)
25
+ ? "Your points could not be applied to this order."
26
+ : storeCount > 1
27
+ ? "None of these sellers accepts points."
28
+ : "This seller does not accept points." })), preview.gstAmount > 0 && (_jsxs(Text, { size: "xs", color: "muted", children: ["Includes GST of ", money(preview.gstAmount)] })), errorNote ? (_jsx(Text, { size: "xs", color: "error", children: errorNote })) : (isLoading && (_jsx(Text, { size: "xs", color: "muted", children: "Updating\u2026" })))] }));
25
29
  }
@@ -0,0 +1,61 @@
1
+ import React from "react";
2
+ /**
3
+ * "Pay with my points" — one checkbox for the whole cart (D46/D47).
4
+ *
5
+ * ## Why ONE control for the cart, where add-ons get one per store
6
+ *
7
+ * An add-on is a thing the buyer buys from a particular seller, so choosing it
8
+ * per seller is the only coherent shape. Points are the buyer's own balance:
9
+ * they do not belong to a seller, and asking "how many of your points would you
10
+ * like to spend at each of these three shops" is a question nobody wants to
11
+ * answer. So the buyer opts in once and the server spends what it can, in group
12
+ * order, at the sellers who accept them (decision 22).
13
+ *
14
+ * ## 🛑 A checkbox and NOT a number input, deliberately
15
+ *
16
+ * Decision 24: tick it and as many points as the 50% cap allows are applied;
17
+ * whatever cannot be applied stays in the balance for a later order. Nothing is
18
+ * forfeited, so there is no amount worth agonising over — and more importantly,
19
+ * an amount the client can send is an amount the client can lie about. The
20
+ * figure is always derived server-side from the live balance.
21
+ *
22
+ * ## It renders nothing when there is nothing to offer
23
+ *
24
+ * A zero balance gets no control at all rather than a disabled one. A disabled
25
+ * checkbox beside "0 points" is an advertisement, and the place to advertise
26
+ * the programme is the points page, not the cart of somebody who cannot use it.
27
+ */
28
+ export interface PointsTenderPickerProps {
29
+ /** Current cart flag. */
30
+ value: boolean;
31
+ onChange: (next: boolean) => void;
32
+ /**
33
+ * The buyer's live balance, in points (1 point = ₹1).
34
+ *
35
+ * 🛑 Zero when the cart has not opted in, because the server only reads a
36
+ * balance it is going to use — so this cannot be relied on to decide whether
37
+ * to RENDER. `hasBalance` below exists for that.
38
+ */
39
+ balance: number;
40
+ /**
41
+ * Points actually applied to this cart, from the server preview. Undefined
42
+ * while the preview is in flight.
43
+ */
44
+ applied?: number;
45
+ /**
46
+ * Whether the buyer has ANY points, independent of the opt-in state.
47
+ *
48
+ * Separate from `balance` for the reason above: with the toggle off the
49
+ * preview returns `pointsBalance: 0`, so a control gated on `balance > 0`
50
+ * could never be switched back on once switched off — a one-way door, the
51
+ * `BackToTop` failure in Root Cause #41.
52
+ */
53
+ hasBalance: boolean;
54
+ /** No store in the cart accepts points — the control says so instead of hiding. */
55
+ noAcceptingStore?: boolean;
56
+ currency?: string;
57
+ disabled?: boolean;
58
+ disabledReason?: string;
59
+ className?: string;
60
+ }
61
+ export declare function PointsTenderPicker({ value, onChange, balance, applied, hasBalance, noAcceptingStore, currency, disabled, disabledReason, className, }: PointsTenderPickerProps): React.JSX.Element | null;
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Div, Row, Stack, Text } from "../../../ui/index.js";
4
+ import { IconBox } from "../../../ui/components/IconBox.js";
5
+ import { Icon } from "../../../ui/components/Icon.js";
6
+ import { FieldCheckbox } from "../../../ui/forms/FieldCheckbox.js";
7
+ import { formatCurrency } from "../../../utils/number.formatter.js";
8
+ export function PointsTenderPicker({ value, onChange, balance, applied, hasBalance, noAcceptingStore = false, currency = "INR", disabled = false, disabledReason, className = "", }) {
9
+ if (!hasBalance)
10
+ return null;
11
+ const shownBalance = value ? balance : 0;
12
+ return (_jsx(Div, { surface: "muted", rounded: "lg", padding: "sm", className: className, children: _jsxs(Stack, { gap: "xs", children: [_jsxs(Row, { align: "center", gap: "sm", className: "min-w-0", children: [_jsx(IconBox, { size: "sm", tone: "brand", rounded: "lg", children: _jsx(Icon, { name: "points", size: "sm" }) }), _jsx(Div, { className: "min-w-0 flex-1", children: _jsx(FieldCheckbox, { name: "useAllPoints", label: "Pay with my points", checked: value, onChange: onChange, disabled: disabled || noAcceptingStore }) })] }), noAcceptingStore ? (_jsx(Text, { size: "xs", color: "muted", children: "Points cannot be used with the seller(s) in this cart." })) : disabled && disabledReason ? (_jsx(Text, { size: "xs", color: "muted", children: disabledReason })) : value && applied !== undefined ? (applied > 0 ? (_jsxs(Text, { size: "xs", color: "muted", children: [applied.toLocaleString("en-IN"), " points applied (", formatCurrency(applied, currency), ")", shownBalance > applied
13
+ ? ` · ${(shownBalance - applied).toLocaleString("en-IN")} left over`
14
+ : ""] })) : (_jsx(Text, { size: "xs", color: "muted", children: "No points could be applied to this order." }))) : (_jsx(Text, { size: "xs", color: "muted", children: "Up to half of an order can be paid with points." }))] }) }));
15
+ }
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Gift, ShieldAlert, ShieldCheck, ShieldOff } from "lucide-react";
4
4
  import { Div, Row, Stack, Text } from "../../../ui/index.js";
5
+ import { Disclaimer } from "../../../ui/components/Disclaimer.js";
5
6
  import { IconBox } from "../../../ui/components/IconBox.js";
6
7
  import { FieldCheckbox } from "../../../ui/forms/FieldCheckbox.js";
7
8
  import { FieldTextarea } from "../../../ui/forms/FieldTextarea.js";
@@ -54,5 +55,5 @@ export function StoreAddonsPicker({ storeId, storeSubtotal, value, onChange, rat
54
55
  ? "This seller does not offer shipment or sale protection."
55
56
  : shippingDeclined
56
57
  ? "This seller does not offer shipment protection."
57
- : "This seller does not offer sale protection." })] })), saleBlockedByFinalSale && (_jsxs(Row, { gap: "sm", align: "start", className: "min-w-0", children: [_jsx(IconBox, { size: "sm", tone: "muted", children: _jsx(ShieldOff, { size: 14 }) }), _jsx(Text, { size: "xs", color: "muted", className: "flex-1 min-w-0", children: "Sale protection is not available \u2014 this basket contains a final-sale item. Shipment protection is unaffected." })] }))] }));
58
+ : "This seller does not offer sale protection." })] })), saleBlockedByFinalSale && (_jsxs(Row, { gap: "sm", align: "start", className: "min-w-0", children: [_jsx(IconBox, { size: "sm", tone: "muted", children: _jsx(ShieldOff, { size: 14 }) }), _jsx(Text, { size: "xs", color: "muted", className: "flex-1 min-w-0", children: "Sale protection is not available \u2014 this basket contains a final-sale item. Shipment protection is unaffected." })] })), (shippingOffered || saleOffered) && _jsx(Disclaimer, { id: "protection-cover", inline: true })] }));
58
59
  }
@@ -7,6 +7,8 @@ export { CartSummary } from "./CartSummary";
7
7
  export type { CartSummaryProps } from "./CartSummary";
8
8
  export { StoreAddonsPicker, hasAnyStoreAddon } from "./StoreAddonsPicker";
9
9
  export type { StoreAddonsPickerProps, StoreAddonsValue, StoreAddonsRates, } from "./StoreAddonsPicker";
10
+ export { PointsTenderPicker } from "./PointsTenderPicker";
11
+ export type { PointsTenderPickerProps } from "./PointsTenderPicker";
10
12
  export { CartPriceBreakdown } from "./CartPriceBreakdown";
11
13
  export type { CartPriceBreakdownProps, CartPriceBreakdownData, CartPriceBreakdownStore, } from "./CartPriceBreakdown";
12
14
  export { CheckoutView } from "./CheckoutView";
@@ -3,6 +3,7 @@ export { CartGroupLineRow } from "./CartGroupLineRow.js";
3
3
  export { CartView } from "./CartView.js";
4
4
  export { CartSummary } from "./CartSummary.js";
5
5
  export { StoreAddonsPicker, hasAnyStoreAddon } from "./StoreAddonsPicker.js";
6
+ export { PointsTenderPicker } from "./PointsTenderPicker.js";
6
7
  export { CartPriceBreakdown } from "./CartPriceBreakdown.js";
7
8
  export { CheckoutView } from "./CheckoutView.js";
8
9
  export { CheckoutAddressStep } from "./CheckoutAddressStep.js";