@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.
- package/dist/.build-stamp.json +3 -3
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +19 -3
- package/dist/_internal/client/features/maintenance/views/AnalysisRunnerView.js +1 -1
- package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +1 -1
- package/dist/_internal/server/features/checkout/actions.d.ts +80 -0
- package/dist/_internal/server/features/checkout/actions.js +542 -26
- package/dist/_internal/server/features/invoicing/allocate.d.ts +63 -0
- package/dist/_internal/server/features/invoicing/allocate.js +162 -0
- package/dist/_internal/server/features/invoicing/credit-note.d.ts +57 -0
- package/dist/_internal/server/features/invoicing/credit-note.js +126 -0
- package/dist/_internal/server/features/invoicing/gstr1.d.ts +74 -0
- package/dist/_internal/server/features/invoicing/gstr1.js +251 -0
- package/dist/_internal/server/features/invoicing/issue.d.ts +96 -0
- package/dist/_internal/server/features/invoicing/issue.js +202 -0
- package/dist/_internal/server/features/orders/actions.d.ts +8 -1
- package/dist/_internal/server/features/orders/actions.js +81 -8
- package/dist/_internal/server/features/orders/adapters.js +33 -0
- package/dist/_internal/server/features/orders/cancellation-refund.d.ts +15 -9
- package/dist/_internal/server/features/orders/cancellation-refund.js +15 -9
- package/dist/_internal/server/features/points/award.js +46 -4
- package/dist/_internal/server/features/refunds/actions.js +26 -0
- package/dist/_internal/server/features/shipping/courier-block.d.ts +69 -0
- package/dist/_internal/server/features/shipping/courier-block.js +239 -0
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +15 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
- package/dist/_internal/server/features/stores/adapters.js +51 -0
- package/dist/_internal/server/functions/scheduled.js +38 -1
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +9 -2
- package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +4 -0
- package/dist/_internal/server/jobs/core/dailyStatusDigest.js +19 -2
- package/dist/_internal/server/jobs/core/index.d.ts +2 -1
- package/dist/_internal/server/jobs/core/index.js +2 -1
- package/dist/_internal/server/jobs/core/onJobCreated.d.ts +30 -7
- package/dist/_internal/server/jobs/core/onJobCreated.js +78 -45
- package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +7 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +62 -0
- package/dist/_internal/server/jobs/core/paymentReconcile.js +87 -97
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +33 -0
- package/dist/_internal/server/jobs/core/runJobOnce.d.ts +48 -0
- package/dist/_internal/server/jobs/core/runJobOnce.js +195 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +10 -2
- package/dist/_internal/server/jobs/handlers/onOrderStatusChange.js +17 -0
- package/dist/_internal/server/notifications/channel-health.d.ts +26 -3
- package/dist/_internal/shared/actions/action-registry.js +177 -9
- package/dist/_internal/shared/checkout/coupon-discount.d.ts +43 -1
- package/dist/_internal/shared/checkout/coupon-discount.js +26 -1
- package/dist/_internal/shared/checkout/order-math.d.ts +145 -0
- package/dist/_internal/shared/checkout/order-math.js +173 -0
- package/dist/_internal/shared/checkout/refundable-base.d.ts +25 -0
- package/dist/_internal/shared/checkout/refundable-base.js +13 -1
- package/dist/_internal/shared/checkout/split.d.ts +87 -0
- package/dist/_internal/shared/checkout/split.js +100 -0
- package/dist/_internal/shared/content/disclaimers.d.ts +88 -0
- package/dist/_internal/shared/content/disclaimers.js +217 -0
- package/dist/_internal/shared/content/published-values.d.ts +88 -0
- package/dist/_internal/shared/content/published-values.js +198 -0
- package/dist/_internal/shared/features/admin/integrations.d.ts +109 -0
- package/dist/_internal/shared/features/admin/integrations.js +253 -0
- package/dist/_internal/shared/features/orders/dispute-reasons.d.ts +113 -0
- package/dist/_internal/shared/features/orders/dispute-reasons.js +181 -0
- package/dist/_internal/shared/features/orders/refund-copy.d.ts +1 -1
- package/dist/_internal/shared/features/orders/refund-copy.js +18 -1
- package/dist/_internal/shared/features/points/accepts.d.ts +36 -0
- package/dist/_internal/shared/features/points/accepts.js +43 -0
- package/dist/_internal/shared/features/shipping/shared-purchase.d.ts +91 -0
- package/dist/_internal/shared/features/shipping/shared-purchase.js +108 -0
- package/dist/_internal/shared/features/site-settings/types.d.ts +11 -0
- package/dist/_internal/shared/fees/calculator.d.ts +33 -1
- package/dist/_internal/shared/fees/calculator.js +32 -2
- package/dist/_internal/shared/media/limits.d.ts +10 -0
- package/dist/_internal/shared/media/limits.js +10 -0
- package/dist/_internal/shared/types/action-result.js +15 -1
- package/dist/client/api/surface-error.js +16 -2
- package/dist/client/api/useApiMutation.js +17 -1
- package/dist/client.d.ts +12 -1
- package/dist/client.js +31 -1
- package/dist/constants/api-endpoints.d.ts +45 -0
- package/dist/constants/api-endpoints.js +15 -0
- package/dist/constants/field-names.d.ts +18 -0
- package/dist/constants/field-names.js +28 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/shipping.d.ts +45 -0
- package/dist/contracts/shipping.js +15 -0
- package/dist/core/server-action.js +19 -3
- package/dist/errors/error-mapping.js +26 -1
- package/dist/features/about/components/AboutView.js +1 -1
- package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -1
- package/dist/features/account/components/UserOffersPanel.js +5 -3
- package/dist/features/account/components/UserOrdersView.js +19 -33
- package/dist/features/admin/components/AdminActionIndexView.js +100 -5
- package/dist/features/admin/components/AdminAddressClustersView.js +21 -2
- package/dist/features/admin/components/AdminBlogEditorView.js +8 -8
- package/dist/features/admin/components/AdminCarouselEditorView.js +1 -1
- package/dist/features/admin/components/AdminCarouselView.js +96 -4
- package/dist/features/admin/components/AdminCouponEditorView.js +17 -0
- package/dist/features/admin/components/AdminFulfillmentView.js +3 -2
- package/dist/features/admin/components/AdminInvoicingView.d.ts +2 -0
- package/dist/features/admin/components/AdminInvoicingView.js +65 -0
- package/dist/features/admin/components/AdminMediaView.js +36 -3
- package/dist/features/admin/components/AdminNavigationView.js +4 -1
- package/dist/features/admin/components/AdminOffersView.js +2 -1
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +18 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +11 -2
- package/dist/features/admin/components/AdminOrdersGuideView.js +23 -2
- package/dist/features/admin/components/AdminOrdersView.js +67 -5
- package/dist/features/admin/components/AdminPaymentClustersView.js +19 -2
- package/dist/features/admin/components/AdminProductEditorView.js +2 -2
- package/dist/features/admin/components/AdminSectionsView.d.ts +10 -0
- package/dist/features/admin/components/AdminSectionsView.js +486 -224
- package/dist/features/admin/components/OwnerQuickActions.js +24 -2
- package/dist/features/admin/components/ReferenceRecordEditor.js +1 -1
- package/dist/features/admin/constants/filter-tabs.d.ts +6 -0
- package/dist/features/admin/constants/filter-tabs.js +6 -0
- package/dist/features/admin/constants/site-settings-tabs.d.ts +1 -1
- package/dist/features/admin/hooks/useAdminListingData.js +16 -0
- package/dist/features/admin/hooks/useOptimisticRowFlag.d.ts +33 -0
- package/dist/features/admin/hooks/useOptimisticRowFlag.js +87 -0
- package/dist/features/admin/repository/notification.repository.d.ts +15 -0
- package/dist/features/admin/repository/notification.repository.js +32 -0
- package/dist/features/admin/schemas/action-index-form.d.ts +34 -0
- package/dist/features/admin/schemas/action-index-form.js +115 -0
- package/dist/features/admin/schemas/admin-user-form.d.ts +2 -2
- package/dist/features/admin/schemas/firestore.d.ts +30 -0
- package/dist/features/admin/schemas/firestore.js +5 -0
- package/dist/features/audit-log/schemas/firestore.d.ts +30 -0
- package/dist/features/audit-log/schemas/firestore.js +15 -0
- package/dist/features/auth/components/RegisterForm.js +2 -1
- package/dist/features/auth/permissions/constants.js +13 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +13 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +6 -2
- package/dist/features/cart/components/PointsTenderPicker.d.ts +61 -0
- package/dist/features/cart/components/PointsTenderPicker.js +15 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +2 -1
- package/dist/features/cart/components/index.d.ts +2 -0
- package/dist/features/cart/components/index.js +1 -0
- package/dist/features/cart/repository/cart.repository.d.ts +19 -0
- package/dist/features/cart/repository/cart.repository.js +27 -0
- package/dist/features/cart/schemas/firestore.d.ts +92 -0
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +3 -3
- package/dist/features/contact/email.d.ts +27 -10
- package/dist/features/contact/email.js +20 -3
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +38 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +80 -0
- package/dist/features/dispute-reasons/schemas/firestore.d.ts +64 -0
- package/dist/features/dispute-reasons/schemas/firestore.js +40 -0
- package/dist/features/events/components/AdminEventEditorView.js +8 -6
- package/dist/features/faq/components/FAQAccordion.d.ts +13 -1
- package/dist/features/faq/components/FAQAccordion.js +29 -4
- package/dist/features/invoicing/schemas/firestore.d.ts +112 -0
- package/dist/features/invoicing/schemas/firestore.js +45 -0
- package/dist/features/invoicing/utils/financial-year.d.ts +43 -0
- package/dist/features/invoicing/utils/financial-year.js +53 -0
- package/dist/features/jobs/actions/enqueue-job.js +18 -1
- package/dist/features/jobs/repository/jobs.repository.d.ts +62 -1
- package/dist/features/jobs/repository/jobs.repository.js +84 -1
- package/dist/features/jobs/schemas/firestore.d.ts +80 -0
- package/dist/features/jobs/schemas/firestore.js +37 -0
- package/dist/features/layout/hooks/useFormBottomActions.js +20 -1
- package/dist/features/lottery/schemas/zod.d.ts +6 -6
- package/dist/features/media/MediaImage.js +22 -1
- package/dist/features/media/upload/ImageUpload.js +1 -1
- package/dist/features/obligations/constants/policies.js +24 -1
- package/dist/features/orders/components/EmiInstallmentsPanel.js +25 -7
- package/dist/features/orders/components/OrderPaymentSummary.js +10 -1
- package/dist/features/orders/components/OrderProvenanceCard.d.ts +8 -0
- package/dist/features/orders/components/OrderProvenanceCard.js +185 -0
- package/dist/features/orders/components/RaiseDisputeForm.d.ts +14 -0
- package/dist/features/orders/components/RaiseDisputeForm.js +111 -0
- package/dist/features/orders/components/RefundRequestView.js +22 -4
- package/dist/features/orders/components/index.d.ts +4 -0
- package/dist/features/orders/components/index.js +2 -0
- package/dist/features/orders/constants/order-lane-tabs.d.ts +69 -0
- package/dist/features/orders/constants/order-lane-tabs.js +88 -0
- package/dist/features/orders/repository/orders.repository.d.ts +57 -0
- package/dist/features/orders/repository/orders.repository.js +145 -0
- package/dist/features/orders/schemas/firestore.d.ts +196 -1
- package/dist/features/orders/schemas/index.d.ts +60 -12
- package/dist/features/orders/schemas/index.js +12 -0
- package/dist/features/orders/types/index.d.ts +26 -0
- package/dist/features/orders/utils/courier-block.d.ts +25 -0
- package/dist/features/orders/utils/courier-block.js +23 -0
- package/dist/features/payment-cases/schemas/firestore.d.ts +26 -1
- package/dist/features/payment-cases/schemas/firestore.js +1 -0
- package/dist/features/products/components/GroupMemberPicker.js +4 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -1
- package/dist/features/products/components/ProductDetailActions.js +2 -1
- package/dist/features/products/components/ProductDetailPageView.d.ts +17 -1
- package/dist/features/products/components/ProductDetailPageView.js +2 -2
- package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
- package/dist/features/promotions/actions/coupon-stacking.d.ts +30 -3
- package/dist/features/promotions/actions/coupon-stacking.js +21 -2
- package/dist/features/promotions/components/CouponCard.js +13 -0
- package/dist/features/promotions/constants/coupon-help.d.ts +2 -2
- package/dist/features/promotions/constants/coupon-help.js +9 -1
- package/dist/features/promotions/schemas/firestore.d.ts +24 -0
- package/dist/features/promotions/schemas/firestore.js +4 -0
- package/dist/features/promotions/types/index.d.ts +19 -1
- package/dist/features/scams/components/ScamProfileView.js +2 -2
- package/dist/features/scams/components/ScamRegistryView.js +2 -2
- package/dist/features/search/action-index/tab-entries.js +1 -1
- package/dist/features/seller/actions/seller-actions.js +33 -3
- package/dist/features/seller/components/FulfillmentView.js +3 -2
- package/dist/features/seller/components/SellerAddressesView.js +32 -4
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +62 -3
- package/dist/features/seller/components/SellerBundlesView.js +53 -14
- package/dist/features/seller/components/SellerGroupedListingsView.js +23 -3
- package/dist/features/seller/components/SellerListingTypeView.js +56 -10
- package/dist/features/seller/components/SellerOrdersView.js +129 -27
- package/dist/features/seller/components/SellerPayoutMethodsView.js +33 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +48 -9
- package/dist/features/seller/components/SellerProductsView.js +5 -1
- package/dist/features/seller/components/SellerShippingConfigsView.js +33 -1
- package/dist/features/seller/components/SellerShippingView.js +2 -1
- package/dist/features/seller/components/SellerStoreCategoriesView.js +33 -1
- package/dist/features/seller/components/SellerSupportView.js +69 -2
- package/dist/features/seller/hooks/useSellerListingData.js +25 -0
- package/dist/features/shell/SectionForm.js +20 -16
- package/dist/features/shell/build-sections.js +14 -1
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +54 -1
- package/dist/features/shipments/components/AdminShipmentProjectionsView.js +3 -2
- package/dist/features/shipments/schemas/validation.d.ts +4 -4
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +43 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +90 -0
- package/dist/features/shipping-boxes/schemas/firestore.d.ts +82 -0
- package/dist/features/shipping-boxes/schemas/firestore.js +53 -0
- package/dist/features/sticker-trace/components/StickerCommitPanel.js +2 -1
- package/dist/features/sticker-trace/components/StickerPrintPanel.js +2 -1
- package/dist/features/sticker-trace/components/StickerRetryPanel.js +24 -4
- package/dist/features/sticker-trace/components/StickerTraceStudio.js +2 -1
- package/dist/features/store-extensions/schemas/report-create-form.d.ts +2 -2
- package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +2 -2
- package/dist/features/stores/components/StoreOrdersGuideView.js +6 -1
- package/dist/features/stores/schemas/firestore.d.ts +71 -0
- package/dist/features/stores/types/index.d.ts +19 -0
- package/dist/features/support/schemas/index.d.ts +6 -6
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +3 -2
- package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
- package/dist/index.d.ts +11 -2
- package/dist/index.js +17 -2
- package/dist/next/routing/route-map.d.ts +24 -0
- package/dist/next/routing/route-map.js +19 -0
- package/dist/providers/shipping-shiprocket/index.d.ts +27 -1
- package/dist/providers/shipping-shiprocket/index.js +84 -1
- package/dist/schemas/registry.d.ts +51 -22
- package/dist/schemas/sieve.d.ts +5 -5
- package/dist/server-entry.d.ts +2 -2
- package/dist/server-entry.js +8 -1
- package/dist/server.d.ts +11 -2
- package/dist/server.js +40 -2
- package/dist/styles.css +64 -80
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/default-dark.js +1 -0
- package/dist/tokens/themes/default-light.js +1 -0
- package/dist/tokens/themes/required.d.ts +1 -1
- package/dist/tokens/themes/required.js +1 -0
- package/dist/tokens/tokens.css +15 -0
- package/dist/ui/components/Checkbox.js +10 -2
- package/dist/ui/components/DateInput.js +8 -1
- package/dist/ui/components/Disclaimer.d.ts +19 -0
- package/dist/ui/components/Disclaimer.js +74 -0
- package/dist/ui/components/Input.js +22 -2
- package/dist/ui/components/PaginatedSelect.d.ts +8 -19
- package/dist/ui/components/PaginatedSelect.js +43 -9
- package/dist/ui/components/Radio.js +8 -1
- package/dist/ui/components/Select.js +10 -2
- package/dist/ui/components/TagInput.d.ts +8 -1
- package/dist/ui/components/TagInput.js +4 -3
- package/dist/ui/components/Textarea.js +10 -2
- package/dist/ui/components/tabs-overflow.js +64 -11
- package/dist/ui/forms/FieldCheckbox.js +1 -1
- package/dist/ui/forms/FormErrorList.d.ts +0 -16
- package/dist/ui/forms/FormErrorList.js +34 -1
- package/dist/ui/forms/apply-zod-issues.d.ts +19 -1
- package/dist/ui/forms/apply-zod-issues.js +23 -1
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +6 -0
- package/dist/ui/rich-text/RichText.style.css +48 -79
- package/package.json +1 -1
- 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:
|
|
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
|
|
5
|
-
|
|
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
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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:
|
|
128
|
-
|
|
129
|
-
|
|
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();
|