@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
@@ -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";
@@ -129,6 +129,25 @@ export declare class CartRepository extends BaseRepository<CartDocument> {
129
129
  * and keeping both shapes around invites readers that handle only one.
130
130
  */
131
131
  setStoreAddons(userId: string, storeId: string, addons: CartStoreAddons | null): Promise<void>;
132
+ /**
133
+ * Opt the cart in or out of paying with points (D46/D47, decision 24).
134
+ *
135
+ * 🛑 A BOOLEAN, and the schema deliberately has no numeric sibling. Opt in
136
+ * and the server applies as many points as the 50% cap allows; whatever
137
+ * cannot be applied stays in the balance for a later order. A client-supplied
138
+ * amount is a client-supplied discount, which is the whole reason
139
+ * `storeAddons` is the source of truth for add-on fees rather than a request
140
+ * body.
141
+ *
142
+ * Written with the same `set(..., { merge: true })` as its neighbours so a
143
+ * concurrent add-on or selection write is not clobbered.
144
+ *
145
+ * 🛑 Opting OUT deletes the field rather than writing `false` — absent and
146
+ * `false` mean the same thing to every reader (`cart.useAllPoints === true`),
147
+ * and keeping both shapes invites a reader that handles only one. Same
148
+ * reasoning as `setStoreAddons` directly above.
149
+ */
150
+ setUseAllPoints(userId: string, useAllPoints: boolean): Promise<void>;
132
151
  /**
133
152
  * Cloud Functions: return refs of stale carts not updated within TTL.
134
153
  */
@@ -590,6 +590,33 @@ export class CartRepository extends BaseRepository {
590
590
  updatedAt: new Date(),
591
591
  }, { merge: true });
592
592
  }
593
+ /**
594
+ * Opt the cart in or out of paying with points (D46/D47, decision 24).
595
+ *
596
+ * 🛑 A BOOLEAN, and the schema deliberately has no numeric sibling. Opt in
597
+ * and the server applies as many points as the 50% cap allows; whatever
598
+ * cannot be applied stays in the balance for a later order. A client-supplied
599
+ * amount is a client-supplied discount, which is the whole reason
600
+ * `storeAddons` is the source of truth for add-on fees rather than a request
601
+ * body.
602
+ *
603
+ * Written with the same `set(..., { merge: true })` as its neighbours so a
604
+ * concurrent add-on or selection write is not clobbered.
605
+ *
606
+ * 🛑 Opting OUT deletes the field rather than writing `false` — absent and
607
+ * `false` mean the same thing to every reader (`cart.useAllPoints === true`),
608
+ * and keeping both shapes invites a reader that handles only one. Same
609
+ * reasoning as `setStoreAddons` directly above.
610
+ */
611
+ async setUseAllPoints(userId, useAllPoints) {
612
+ await this.db
613
+ .collection(this.collection)
614
+ .doc(userId)
615
+ .set({
616
+ useAllPoints: useAllPoints ? true : FieldValue.delete(),
617
+ updatedAt: new Date(),
618
+ }, { merge: true });
619
+ }
593
620
  /*
594
621
  * `pruneStoreAddons(userId, keepStoreIds)` used to live here. It had zero
595
622
  * callers from the day it was written, and `pruneForItems` now does the same
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import type { BaseDocument } from "../../../_internal/shared/types/base-document";
8
8
  import type { ListingType } from "../../products/types/index";
9
+ import type { CouponType } from "../../promotions/types/index";
9
10
  /**
10
11
  * What KIND of thing one cart line represents.
11
12
  *
@@ -57,6 +58,33 @@ export interface CartLineMember {
57
58
  gstRate?: 0 | 5 | 12 | 18 | 28;
58
59
  hsnCode?: string;
59
60
  listingType?: ListingType;
61
+ /**
62
+ * How the coordinator expressed this member's share of the line.
63
+ *
64
+ * Absent means `"amount"` — every line written before splits existed, so no
65
+ * migration and no behaviour change until a coordinator picks percentages.
66
+ *
67
+ * 🛑 The PERCENTAGE is the stored fact, not the resolved rupees. Percentages
68
+ * apply to the LANDED cost (goods + shipping + fees), so a member's figure
69
+ * MOVES when shipping is re-quoted — storing the computed amount instead
70
+ * would freeze a number that no longer matches the agreed share. The
71
+ * computed figures are snapshotted at COMMIT, which is where the
72
+ * coordinator's agreement actually becomes binding (D61).
73
+ *
74
+ * `resolveSplit` (`_internal/shared/checkout/split.ts`) is the only thing
75
+ * that turns these into money.
76
+ */
77
+ splitMode?: "amount" | "percent";
78
+ /** 0–100, read only when `splitMode === "percent"`. */
79
+ percent?: number;
80
+ /**
81
+ * The member's agreed contribution in decimal rupees, when typed directly.
82
+ *
83
+ * 🛑 NOT the same as `unitPrice`. `unitPrice` is what the ITEM costs;
84
+ * this is what this PERSON agreed to put in, and the two differ by design
85
+ * the moment a split is uneven or the total exceeds the landed cost.
86
+ */
87
+ contribution?: number;
60
88
  }
61
89
  export interface CartItemDocument {
62
90
  itemId: string;
@@ -183,6 +211,26 @@ export interface CartItemDocument {
183
211
  chosenShippingProviderId?: string;
184
212
  /** Fee in decimal rupees for the chosen provider, snapshotted at cart time. */
185
213
  chosenShippingFee?: number;
214
+ /**
215
+ * ── S5a: which address THIS line ships to ────────────────────────────────
216
+ *
217
+ * One payer, N destinations. Absent means "the order's own shipping
218
+ * address", which is every cart that predates this and every ordinary
219
+ * single-destination order — so nothing changes for them.
220
+ *
221
+ * 🛑 An `addresses/{id}` REFERENCE, never a copied address. Copying would
222
+ * put a name, a phone number and a street address onto the cart document,
223
+ * which is a PII store with no encryption, no `*_PII_FIELDS` entry and no
224
+ * retention story — and carts are swept by a TTL job that is not written to
225
+ * handle personal data. The id resolves server-side at checkout, where the
226
+ * address is already being read and decrypted.
227
+ *
228
+ * 🛑 It must belong to the BUYER. An arbitrary `addresses` id would let a
229
+ * caller address a parcel to any stored address on the platform, which is
230
+ * both a delivery hijack and a way to read whether an id exists. Checked
231
+ * server-side at the write, not trusted from the client.
232
+ */
233
+ destinationAddressId?: string;
186
234
  addedAt: Date;
187
235
  updatedAt: Date;
188
236
  }
@@ -196,6 +244,28 @@ export interface CartAppliedCoupon {
196
244
  storeId?: string;
197
245
  /** Item IDs (CartItemDocument.itemId) this coupon was calculated against */
198
246
  applicableItemIds?: string[];
247
+ /**
248
+ * The coupon's type, snapshotted at apply time (D51).
249
+ *
250
+ * 🛑 Needed because `detectCouponConflict` buckets on TYPE, not scope: a
251
+ * `points_earn` coupon is admin-scoped like every platform coupon but
252
+ * competes for its own slot, so without this field on the stored entry the
253
+ * detector could not tell a points coupon already in the cart from an
254
+ * ordinary platform one, and `RIPLI05` would block every other admin coupon
255
+ * — the exact defect the third bucket exists to prevent.
256
+ *
257
+ * Absent means "a discount coupon", which is what every cart written before
258
+ * this field holds, and is the reading that preserves the previous
259
+ * behaviour exactly.
260
+ *
261
+ * 🛑 Snapshotted deliberately, unlike the restrictions: a coupon's TYPE
262
+ * decides which slot it occupies, so re-reading it live would mean an admin
263
+ * editing a coupon's type could retroactively make a live cart's
264
+ * combination illegal. The money is still re-validated against the live
265
+ * coupon at placement (`revalidateAppliedCoupons`), which is where a changed
266
+ * discount has to be caught.
267
+ */
268
+ type?: CouponType;
199
269
  }
200
270
  /**
201
271
  * Paid add-ons the buyer opted into, for ONE store.
@@ -239,6 +309,28 @@ export interface CartDocument extends BaseDocument {
239
309
  * rather than a flag someone has to remember to check.
240
310
  */
241
311
  storeAddons?: Record<string, CartStoreAddons>;
312
+ /**
313
+ * The buyer has opted to spend points on this checkout.
314
+ *
315
+ * ## 🛑 A BOOLEAN, and the amount is never stored or sent
316
+ *
317
+ * Owner's rule: *"if use point it is all and then the balance is returned if
318
+ * excess"* — one opt-in, all-or-nothing. Ticked, the checkout applies as many
319
+ * points as the 50% cap allows; whatever cannot be applied stays in the
320
+ * balance. The buyer never types a number.
321
+ *
322
+ * So this is the ONLY thing the client may say about points. The amount is
323
+ * derived server-side from the live balance and
324
+ * `POINTS_MAX_REDEMPTION_PERCENT` at the moment the order is priced — the
325
+ * same rule that makes `storeAddons` above the source of truth rather than a
326
+ * request body, and for the same reason: a client-supplied money figure is a
327
+ * client-supplied discount.
328
+ *
329
+ * 🛑 This replaces a deleted `pointsToRedeem?: number` on the checkout input,
330
+ * whose handler debited the buyer AFTER the gateway had captured full price
331
+ * (D46). A number was the wrong shape as well as the wrong place.
332
+ */
333
+ useAllPoints?: boolean;
242
334
  }
243
335
  export declare const CART_COLLECTION: "carts";
244
336
  export declare const CART_INDEXED_FIELDS: string[];
@@ -95,11 +95,11 @@ export function CatalogueItemEditorView({ item, onSaved }) {
95
95
  // than pointing at a photo the user has since removed.
96
96
  onChange({ images: urls, mainImage: urls[0] ?? "" });
97
97
  }, onUpload: handleUpload, accept: IMAGE_ACCEPT_ATTR, maxItems: 8, helperText: "Photos older than 30 days must be refreshed before this item can be listed." })),
98
- categorySlugs: ({ values, onChange }) => (
98
+ categorySlugs: ({ values, onChange, errors }) => (
99
99
  // Explicit `<string>`: with `multiple`, `value` is `V[]`, so an
100
100
  // inferred V reads the array itself as the option type.
101
- _jsx(PaginatedSelect, { multiple: true, value: values.categorySlugs, onChange: (v) => onChange({ categorySlugs: v ?? [] }), loadOptions: loadCategoryOptions, placeholder: "Search categories\u2026", searchPlaceholder: "Type a category name\u2026", noResultsText: "No categories found", ariaLabel: "Categories" })),
102
- brandSlug: ({ values, onChange }) => (_jsx(PaginatedSelect, { value: values.brandSlug || null, onChange: (v) => onChange({ brandSlug: v ?? "" }), loadOptions: loadBrandOptions, placeholder: "Search brands\u2026", searchPlaceholder: "Type a brand name\u2026", noResultsText: "No brands found", ariaLabel: "Brand" })),
101
+ _jsx(PaginatedSelect, { multiple: true, name: "categorySlugs", error: errors.categorySlugs, value: values.categorySlugs, onChange: (v) => onChange({ categorySlugs: v ?? [] }), loadOptions: loadCategoryOptions, placeholder: "Search categories\u2026", searchPlaceholder: "Type a category name\u2026", noResultsText: "No categories found", ariaLabel: "Categories" })),
102
+ brandSlug: ({ values, onChange, errors }) => (_jsx(PaginatedSelect, { name: "brandSlug", error: errors.brandSlug, value: values.brandSlug || null, onChange: (v) => onChange({ brandSlug: v ?? "" }), loadOptions: loadBrandOptions, placeholder: "Search brands\u2026", searchPlaceholder: "Type a brand name\u2026", noResultsText: "No brands found", ariaLabel: "Brand" })),
103
103
  },
104
104
  }), [handleUpload]);
105
105
  const nav = useSectionFormNav(sections, form, { scope: "user:catalogue-item" });
@@ -1,14 +1,9 @@
1
1
  import "server-only";
2
2
  import type { SendGuardContext } from "../../_internal/shared/features/messaging/config";
3
3
  import type { JsonValue } from "@mohasinac/appkit";
4
- type SendEmailOptions = Record<string, JsonValue> & {
5
- to: string | string[];
6
- subject: string;
4
+ import type { EmailOptions } from "../../contracts/email";
5
+ type SendEmailOptions = Omit<EmailOptions, "html"> & {
7
6
  html?: string;
8
- text?: string;
9
- from?: string;
10
- cc?: string | string[];
11
- replyTo?: string;
12
7
  };
13
8
  /**
14
9
  * Everything `sendEmail` needs beyond the message itself.
@@ -22,9 +17,9 @@ type SendEmailOptions = Record<string, JsonValue> & {
22
17
  * (CLAUDE.md Rule #22: make the wrong thing unrepresentable).
23
18
  *
24
19
  * Kept as a separate second parameter rather than a field on
25
- * `SendEmailOptions` because that type has a `Record<string, JsonValue>` index
26
- * signature — a structured object would not satisfy it, and widening the index
27
- * signature to accommodate one field would weaken every other field's typing.
20
+ * `SendEmailOptions`, which is now the provider's own `EmailOptions`. The
21
+ * guard is about who may send and why; it is not part of the message, and
22
+ * folding it in would put it on the wire to the provider.
28
23
  */
29
24
  export type SendEmailGuard = SendGuardContext;
30
25
  /**
@@ -59,6 +54,28 @@ export interface OrderConfirmationEmailParams {
59
54
  unitPrice: number;
60
55
  totalPrice: number;
61
56
  }>;
57
+ /**
58
+ * Phase 7 — the invoice PDF to attach, as STORED bytes.
59
+ *
60
+ * 🛑 Bytes, never a path and never a renderer. Two renders can disagree — a
61
+ * changed template, a different library version — and the buyer's copy must
62
+ * be byte-identical to ours. Letting this function render would be a second
63
+ * render by definition.
64
+ *
65
+ * 🛑 Optional, and absence must stay silent. The invoice is numbered on the
66
+ * PAID transition and its bytes are produced on first request, so a COD
67
+ * order confirmed before payment legitimately has none — and this email has
68
+ * always sent without an attachment. Failing or warning here would turn the
69
+ * normal case into a noisy one.
70
+ *
71
+ * Resend supports attachments already (`email-resend/provider.ts`), so this
72
+ * needs no new send, no new credential and no change to the 80/day ceiling.
73
+ * Only the payload is bigger.
74
+ */
75
+ invoicePdf?: {
76
+ filename: string;
77
+ content: Buffer;
78
+ };
62
79
  }
63
80
  export declare function sendOrderConfirmationEmail(params: OrderConfirmationEmailParams): Promise<{
64
81
  success: boolean;
@@ -124,9 +124,10 @@ export async function sendEmail(opts, guard) {
124
124
  ? opts.cc
125
125
  : undefined,
126
126
  replyTo: typeof opts.replyTo === "string" ? opts.replyTo : undefined,
127
- headers: typeof opts.headers === "object" && opts.headers
128
- ? opts.headers
129
- : undefined,
127
+ headers: opts.headers,
128
+ // Forwarded, not dropped — the provider has mapped these to Resend's
129
+ // `attachments` all along; nothing here ever handed them over.
130
+ attachments: opts.attachments,
130
131
  });
131
132
  void recordSendAttempt({
132
133
  to: opts.to,
@@ -184,6 +185,22 @@ export async function sendOrderConfirmationEmail(params) {
184
185
  to,
185
186
  subject: `Order Confirmed — ${orderId}`,
186
187
  html,
188
+ /*
189
+ * Phase 7 — the invoice, when there is one. The STORED bytes, handed
190
+ * in by the caller: see `invoicePdf` on the params type for why this
191
+ * function must not render its own.
192
+ */
193
+ ...(params.invoicePdf
194
+ ? {
195
+ attachments: [
196
+ {
197
+ filename: params.invoicePdf.filename,
198
+ content: params.invoicePdf.content,
199
+ contentType: "application/pdf",
200
+ },
201
+ ],
202
+ }
203
+ : {}),
187
204
  text: `Hi ${userName},\n\nYour order ${orderId} has been confirmed!\n\n${itemsText}\nTotal: ${formattedTotal}\nPayment: ${paymentLabel}\nShip to: ${shippingAddress ?? "-"}\n\nView your order: ${orderUrl}\n\n© ${currentYear()} ${siteName}`,
188
205
  }, {
189
206
  channel: "email",
@@ -0,0 +1,38 @@
1
+ import type { FirebaseSieveFields, FirebaseSieveResult, SieveModel } from "../../../providers/db-firebase";
2
+ import { BaseRepository } from "../../../providers/db-firebase";
3
+ import type { DisputeKind } from "../../../_internal/shared/features/orders/dispute-reasons";
4
+ import { type DisputeReasonDocument } from "../schemas/firestore";
5
+ export declare class DisputeReasonsRepository extends BaseRepository<DisputeReasonDocument> {
6
+ static readonly SIEVE_FIELDS: FirebaseSieveFields;
7
+ constructor();
8
+ /**
9
+ * The picker's list, scoped to one kind.
10
+ *
11
+ * 🛑 Scoped by KIND, not merged. A buyer reporting a damaged item must not be
12
+ * offered "it never arrived" — that reason belongs to the other gate, and
13
+ * picking it would produce a dispute the gate then refuses, which reads to
14
+ * the buyer as the form being broken rather than as the wrong choice.
15
+ */
16
+ listActiveByKind(kind: DisputeKind, model: SieveModel): Promise<FirebaseSieveResult<DisputeReasonDocument>>;
17
+ /**
18
+ * Add a reason. Staff only — enforced at the route, not here.
19
+ *
20
+ * 🛑 Keyed `dreason-<slug>`, so two agents cannot add the same wording twice.
21
+ * A colliding create returns the EXISTING row rather than overwriting it:
22
+ * that row has an author and may already be named on live disputes, and the
23
+ * second agent's intent ("there should be a reason for this") is satisfied by
24
+ * what exists. Same rule as the shipping-box presets.
25
+ */
26
+ createReason(input: {
27
+ label: string;
28
+ kind: DisputeKind;
29
+ createdByUid: string;
30
+ createdByName: string;
31
+ }): Promise<{
32
+ reason: DisputeReasonDocument;
33
+ created: boolean;
34
+ }>;
35
+ /** Soft-retire. See the schema for why this is never a delete. */
36
+ retire(id: string): Promise<void>;
37
+ }
38
+ export declare const disputeReasonsRepository: DisputeReasonsRepository;
@@ -0,0 +1,80 @@
1
+ import { BaseRepository } from "../../../providers/db-firebase/index.js";
2
+ import { slugify } from "../../../utils/string.formatter.js";
3
+ import { DISPUTE_REASON_COLLECTION, DISPUTE_REASON_FIELDS, DISPUTE_REASON_LABEL_MAX, } from "../schemas/firestore.js";
4
+ export class DisputeReasonsRepository extends BaseRepository {
5
+ constructor() {
6
+ super(DISPUTE_REASON_COLLECTION);
7
+ }
8
+ /**
9
+ * The picker's list, scoped to one kind.
10
+ *
11
+ * 🛑 Scoped by KIND, not merged. A buyer reporting a damaged item must not be
12
+ * offered "it never arrived" — that reason belongs to the other gate, and
13
+ * picking it would produce a dispute the gate then refuses, which reads to
14
+ * the buyer as the form being broken rather than as the wrong choice.
15
+ */
16
+ async listActiveByKind(kind, model) {
17
+ const baseQuery = this.getCollection()
18
+ .where(DISPUTE_REASON_FIELDS.IS_ACTIVE, "==", true)
19
+ .where(DISPUTE_REASON_FIELDS.KIND, "==", kind);
20
+ return this.sieveQuery(model, DisputeReasonsRepository.SIEVE_FIELDS, { baseQuery, defaultPageSize: 20, maxPageSize: 50 });
21
+ }
22
+ /**
23
+ * Add a reason. Staff only — enforced at the route, not here.
24
+ *
25
+ * 🛑 Keyed `dreason-<slug>`, so two agents cannot add the same wording twice.
26
+ * A colliding create returns the EXISTING row rather than overwriting it:
27
+ * that row has an author and may already be named on live disputes, and the
28
+ * second agent's intent ("there should be a reason for this") is satisfied by
29
+ * what exists. Same rule as the shipping-box presets.
30
+ */
31
+ async createReason(input) {
32
+ const label = input.label.trim().slice(0, DISPUTE_REASON_LABEL_MAX);
33
+ const id = `dreason-${slugify(label)}`;
34
+ const existing = await this.findById(id);
35
+ /*
36
+ * A retired row that someone re-adds is REACTIVATED rather than duplicated
37
+ * — otherwise the slug collides forever and the agent can never get their
38
+ * reason back. Reactivating keeps the original author, which is correct:
39
+ * they wrote the wording.
40
+ */
41
+ if (existing) {
42
+ if (!existing.isActive) {
43
+ await this.update(id, { isActive: true, updatedAt: new Date() });
44
+ return { reason: { ...existing, isActive: true }, created: false };
45
+ }
46
+ return { reason: existing, created: false };
47
+ }
48
+ const now = new Date();
49
+ const reason = {
50
+ id,
51
+ label,
52
+ kind: input.kind,
53
+ isActive: true,
54
+ createdByUid: input.createdByUid,
55
+ createdByName: input.createdByName,
56
+ createdAt: now,
57
+ updatedAt: now,
58
+ };
59
+ await this.createWithId(id, reason);
60
+ return { reason, created: true };
61
+ }
62
+ /** Soft-retire. See the schema for why this is never a delete. */
63
+ async retire(id) {
64
+ await this.update(id, { isActive: false, updatedAt: new Date() });
65
+ }
66
+ }
67
+ DisputeReasonsRepository.SIEVE_FIELDS = {
68
+ /*
69
+ * 🛑 `label` must be here because `PaginatedSelect` SEARCHES it. An
70
+ * unallowlisted searched field does not raise — `sieve.ts` sets
71
+ * `throwExceptions: false`, so `findField` returns undefined, the clause is
72
+ * silently dropped, and the picker returns the WHOLE list while looking
73
+ * exactly like it filtered (Root Cause #62).
74
+ */
75
+ label: { canFilter: true, canSort: true },
76
+ kind: { canFilter: true, canSort: false },
77
+ isActive: { canFilter: true, canSort: false },
78
+ createdByUid: { canFilter: true, canSort: false },
79
+ };
80
+ export const disputeReasonsRepository = new DisputeReasonsRepository();