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