@mohasinac/appkit 4.55.0-next.0 → 4.56.0
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 +4 -4
- package/dist/_internal/server/features/checkout/actions.d.ts +22 -0
- package/dist/_internal/server/features/checkout/actions.js +100 -5
- package/dist/_internal/server/features/shipping/reconcile.d.ts +28 -0
- package/dist/_internal/server/features/shipping/reconcile.js +93 -0
- package/dist/_internal/shared/config/schema.d.ts +6 -0
- package/dist/_internal/shared/features/checkout/config.d.ts +1 -1
- package/dist/_internal/shared/features/checkout/config.js +1 -1
- package/dist/_internal/shared/features/site-settings/fees.d.ts +1 -1
- package/dist/_internal/shared/features/site-settings/fees.js +1 -1
- package/dist/_internal/shared/fees/calculator.d.ts +35 -3
- package/dist/_internal/shared/fees/calculator.js +34 -4
- package/dist/_internal/shared/listing-types/route-gating.d.ts +56 -0
- package/dist/_internal/shared/listing-types/route-gating.js +114 -0
- package/dist/_internal/shared/shipping/quote.d.ts +153 -0
- package/dist/_internal/shared/shipping/quote.js +124 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +8 -0
- package/dist/errors/error-codes.d.ts +15 -0
- package/dist/errors/error-codes.js +15 -0
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/features/account/components/UserBidsView.js +4 -2
- package/dist/features/admin/components/AdminSiteSettingsView.js +18 -2
- package/dist/features/admin/schemas/firestore.d.ts +37 -2
- package/dist/features/admin/schemas/firestore.js +12 -1
- package/dist/features/auctions/components/AuctionDetailPageView.js +2 -1
- package/dist/features/auth/schemas/firestore.d.ts +15 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +1 -1
- package/dist/features/checkout/schemas/firestore.d.ts +12 -1
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +2 -1
- package/dist/features/homepage/components/HeroCarousel.js +4 -2
- package/dist/features/homepage/components/MarketplaceHomepageView.js +16 -1
- package/dist/features/homepage/components/WelcomeSection.d.ts +16 -3
- package/dist/features/homepage/components/WelcomeSection.js +3 -3
- package/dist/features/homepage/lib/section-renderer.d.ts +9 -0
- package/dist/features/homepage/lib/section-renderer.js +22 -5
- package/dist/features/homepage/lib/slide-gating.d.ts +52 -0
- package/dist/features/homepage/lib/slide-gating.js +98 -0
- package/dist/features/layout/NavbarLayout.js +139 -32
- package/dist/features/live/components/LiveItemDetailPageView.js +2 -1
- package/dist/features/media/MediaAvatar.js +1 -1
- package/dist/features/media/MediaImage.d.ts +18 -1
- package/dist/features/media/MediaImage.js +1 -1
- package/dist/features/media/messages/en.json +67 -67
- package/dist/features/orders/schemas/firestore.d.ts +39 -1
- package/dist/features/points/repository/points.repository.d.ts +81 -0
- package/dist/features/points/repository/points.repository.js +155 -0
- package/dist/features/points/schemas/firestore.d.ts +102 -0
- package/dist/features/points/schemas/firestore.js +99 -0
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -1
- package/dist/features/products/components/ProductDetailPageView.js +5 -26
- package/dist/features/products/components/ProductOwnerQuickActions.d.ts +11 -0
- package/dist/features/products/components/ProductOwnerQuickActions.js +120 -0
- package/dist/features/products/components/ProductsIndexListing.js +35 -5
- package/dist/features/products/components/ProductsIndexPageView.js +40 -7
- package/dist/features/seller/actions/seller-actions.d.ts +17 -0
- package/dist/features/seller/actions/seller-actions.js +33 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +8 -0
- package/dist/providers/shipping-shiprocket/index.d.ts +13 -4
- package/dist/providers/shipping-shiprocket/index.js +34 -10
- package/dist/security/csp.d.ts +1 -16
- package/dist/security/csp.js +3 -5
- package/dist/seed/carousel-slides-seed-data.js +48 -7
- package/dist/seed/faq-seed-data.js +1 -1
- package/dist/seed/site-settings-seed-data.js +1 -1
- package/dist/seo/json-ld.d.ts +8 -1
- package/dist/seo/json-ld.js +39 -2
- package/dist/styles.css +419 -542
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/index.d.ts +1 -3
- package/dist/tokens/themes/index.js +1 -11
- package/dist/tokens/tokens.css +0 -155
- package/dist/ui/components/GatedPrice.style.css +43 -43
- package/dist/ui/components/ImageMosaic.d.ts +17 -1
- package/dist/ui/components/ImageMosaic.js +28 -5
- package/dist/ui/components/ImageMosaic.style.css +37 -5
- package/dist/ui/components/QuickCreateModal.style.css +78 -78
- package/dist/ui/forms/FormShell.style.css +260 -260
- package/package.json +1 -1
- package/dist/tokens/themes/technical-dark.d.ts +0 -30
- package/dist/tokens/themes/technical-dark.js +0 -103
- package/dist/tokens/themes/technical-light.d.ts +0 -22
- package/dist/tokens/themes/technical-light.js +0 -89
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState, useCallback } from "react";
|
|
3
|
+
import { useState, useCallback, useMemo } from "react";
|
|
4
4
|
import { ShoppingCart, Heart, Columns } from "lucide-react";
|
|
5
5
|
import { useRouter } from "next/navigation.js";
|
|
6
6
|
import { useUrlTable } from "../../../react/hooks/useUrlTable.js";
|
|
@@ -31,6 +31,7 @@ import { PRODUCT_FIELDS } from "../../../constants/field-names.js";
|
|
|
31
31
|
import { useBottomActions } from "../../layout/index.js";
|
|
32
32
|
import { GENERIC_PRODUCT_LISTING_TYPES, PRODUCT_TYPE_FILTER_TABS } from "../constants/listing-tabs.js";
|
|
33
33
|
import { parseSelectedListingTypes } from "../utils/listing-type.js";
|
|
34
|
+
import { useListingTypeFlags } from "../../../react/hooks/useListingTypeFlags.js";
|
|
34
35
|
import { commonSortOptionsFor } from "../../../_internal/shared/listing-types/_registry.js";
|
|
35
36
|
import { AVAILABILITY_VALUES } from "../../../constants/field-names.js";
|
|
36
37
|
import { AvailabilityTabs } from "./AvailabilityTabs.js";
|
|
@@ -77,13 +78,42 @@ export function ProductsIndexListing({ initialData, listingTypes = GENERIC_PRODU
|
|
|
77
78
|
const [filterOpen, setFilterOpen] = useState(false);
|
|
78
79
|
const [compareIds, setCompareIds] = useState([]);
|
|
79
80
|
const [view, setView] = useState(table.get(TABLE_KEYS.VIEW) || VIEW_MODE.GRID);
|
|
81
|
+
/*
|
|
82
|
+
* ── What this page actually spans ────────────────────────────────────────
|
|
83
|
+
*
|
|
84
|
+
* 🛑 A type an admin has switched OFF must not appear as a filter option.
|
|
85
|
+
* `listingTypes` / `typeTabs` describe what the page covers by DESIGN
|
|
86
|
+
* (`GENERIC_PRODUCT_LISTING_TYPES`, or a store tab's own set); the flags
|
|
87
|
+
* describe what this site offers. Only the intersection is real.
|
|
88
|
+
*
|
|
89
|
+
* Ticking a disabled type was not merely untidy — `listPublicProducts`
|
|
90
|
+
* narrows the request to `selected ∩ span` and falls back to the FULL span
|
|
91
|
+
* when that intersection is empty, so a chip for a disabled type silently
|
|
92
|
+
* WIDENED the results instead of narrowing them (the same trap documented
|
|
93
|
+
* for out-of-span types on `/products`).
|
|
94
|
+
*
|
|
95
|
+
* Falling back to the ungated sets when the filter empties them is
|
|
96
|
+
* deliberate: an empty span is not "show nothing", it is a settings document
|
|
97
|
+
* that disabled everything this page covers, and rendering a type-less
|
|
98
|
+
* listing is more useful than rendering an empty one. `useListingTypeFlags`
|
|
99
|
+
* already fails open while settings load, so there is no first-paint flash.
|
|
100
|
+
*/
|
|
101
|
+
const listingTypeFlags = useListingTypeFlags();
|
|
102
|
+
const enabledSpan = useMemo(() => {
|
|
103
|
+
const kept = listingTypes.filter((t) => listingTypeFlags[t]);
|
|
104
|
+
return (kept.length > 0 ? kept : listingTypes);
|
|
105
|
+
}, [listingTypes, listingTypeFlags]);
|
|
106
|
+
const enabledTypeTabs = useMemo(() => {
|
|
107
|
+
const kept = typeTabs.filter((tab) => enabledSpan.includes(tab.id));
|
|
108
|
+
return kept.length > 0 ? kept : typeTabs;
|
|
109
|
+
}, [typeTabs, enabledSpan]);
|
|
80
110
|
// ── Type filter (multi-select) ───────────────────────────────────────────
|
|
81
111
|
// The URL value is a pipe-joined set; empty means "every type this page
|
|
82
112
|
// spans". Unknown tokens are dropped rather than passed through to a
|
|
83
113
|
// Firestore `==` that could never match (Root Cause #33).
|
|
84
114
|
const typeParam = table.get(TABLE_KEYS.LISTING_TYPE);
|
|
85
|
-
const selectedTypes = parseSelectedListingTypes(typeParam).filter((t) =>
|
|
86
|
-
const effectiveTypes = (selectedTypes.length > 0 ? selectedTypes :
|
|
115
|
+
const selectedTypes = parseSelectedListingTypes(typeParam).filter((t) => enabledSpan.includes(t));
|
|
116
|
+
const effectiveTypes = (selectedTypes.length > 0 ? selectedTypes : enabledSpan);
|
|
87
117
|
// Sorts valid across every selected type. Pick Auctions alone and you get
|
|
88
118
|
// "Ending Soon"; pick Auctions + Products and you get only what both
|
|
89
119
|
// support, because a sort on `auctionEndDate` would order the products
|
|
@@ -109,9 +139,9 @@ export function ProductsIndexListing({ initialData, listingTypes = GENERIC_PRODU
|
|
|
109
139
|
// The drawer's own, uncommitted type selection. The "Full <type> filters →"
|
|
110
140
|
// link tracks THIS rather than the applied URL value, so it appears the
|
|
111
141
|
// moment the user ticks a single type instead of only after Apply.
|
|
112
|
-
const pendingTypes = parseSelectedListingTypes(pendingTable.get(TABLE_KEYS.LISTING_TYPE)).filter((t) =>
|
|
142
|
+
const pendingTypes = parseSelectedListingTypes(pendingTable.get(TABLE_KEYS.LISTING_TYPE)).filter((t) => enabledSpan.includes(t));
|
|
113
143
|
const dedicatedPage = dedicatedPageFor(pendingTypes);
|
|
114
|
-
const listingTypeOptions =
|
|
144
|
+
const listingTypeOptions = enabledTypeTabs.map((t) => ({ value: t.id, label: t.label }));
|
|
115
145
|
const openFilters = useCallback(() => {
|
|
116
146
|
onFilterReset();
|
|
117
147
|
setFilterOpen(true);
|
|
@@ -10,6 +10,9 @@ import { GENERIC_PRODUCT_LISTING_TYPES } from "../constants/listing-tabs.js";
|
|
|
10
10
|
import { pluginFor } from "../../../_internal/shared/listing-types/_registry.js";
|
|
11
11
|
import { listPublicProducts, parsePublicProductParams, defaultAvailabilityForListingTypes, } from "../../../_internal/server/features/products/list-public.js";
|
|
12
12
|
import { parseSelectedListingTypes } from "../utils/listing-type.js";
|
|
13
|
+
import { isListingTypeEnabled } from "../../../_internal/shared/listing-types/feature-flags.js";
|
|
14
|
+
import { siteSettingsRepository } from "../../../repositories/index.js";
|
|
15
|
+
import { safeRead } from "../../../errors/safe-read.js";
|
|
13
16
|
const DEFAULT_PAGE_SIZE = 24;
|
|
14
17
|
const DEFAULT_SORT = sortBy(PRODUCT_FIELDS.CREATED_AT);
|
|
15
18
|
export async function ProductsIndexPageView({ searchParams = {} }) {
|
|
@@ -47,11 +50,41 @@ export async function ProductsIndexPageView({ searchParams = {} }) {
|
|
|
47
50
|
const effectiveTypes = selectedTypes.length > 0 ? selectedTypes : GENERIC_PRODUCT_LISTING_TYPES;
|
|
48
51
|
// See ArtStickersListView — one shared filter implementation with
|
|
49
52
|
// /api/products, so SSR and the client refetch agree by construction.
|
|
50
|
-
const products = await
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
const [products, settings] = await Promise.all([
|
|
54
|
+
listPublicProducts(parsePublicProductParams(searchParams, {
|
|
55
|
+
listingTypes: GENERIC_PRODUCT_LISTING_TYPES,
|
|
56
|
+
pageSize: DEFAULT_PAGE_SIZE,
|
|
57
|
+
sorts: DEFAULT_SORT,
|
|
58
|
+
...defaultAvailabilityForListingTypes(effectiveTypes),
|
|
59
|
+
})),
|
|
60
|
+
/*
|
|
61
|
+
* Read in parallel with the listing — the banner below is the only
|
|
62
|
+
* consumer, and serialising a settings read in front of the page's main
|
|
63
|
+
* query would spend a round trip against Rule #6's budget for one link.
|
|
64
|
+
*
|
|
65
|
+
* 🛑 `safeRead`, so a settings failure hides the banner rather than
|
|
66
|
+
* failing the catalogue. Auctions have their own nav entry and their own
|
|
67
|
+
* page; the banner is a shortcut, and a shortcut is the correct thing to
|
|
68
|
+
* lose when its gate cannot be evaluated.
|
|
69
|
+
*/
|
|
70
|
+
safeRead(() => siteSettingsRepository.findById("global"), {
|
|
71
|
+
route: "/products",
|
|
72
|
+
key: "products.siteSettings",
|
|
73
|
+
fallback: null,
|
|
74
|
+
}),
|
|
75
|
+
]);
|
|
76
|
+
/*
|
|
77
|
+
* 🛑 Auctions are a LISTING TYPE an admin can switch off, and this banner
|
|
78
|
+
* advertised them unconditionally.
|
|
79
|
+
*
|
|
80
|
+
* Every other auction entry point already respects the flag — the navbar
|
|
81
|
+
* child (filtered by key in `LayoutShellClient`), the footer link
|
|
82
|
+
* (`FOOTER_LINK_LISTING_TYPES`), the store tabs (`tabs.ts`), the
|
|
83
|
+
* category/brand tabs, and the homepage section (`gatedListing`). This was
|
|
84
|
+
* the one surface left promoting a page that renders nothing on a site with
|
|
85
|
+
* auctions disabled, which is Root Cause #37's dead-link shape reached from
|
|
86
|
+
* the other direction.
|
|
87
|
+
*/
|
|
88
|
+
const showAuctionsBanner = isListingTypeEnabled("auction", settings);
|
|
89
|
+
return (_jsxs(Main, { children: [_jsx(Section, { border: "bottom-subtle", paddingY: "b-md", padding: "t-xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, size: "3xl", weight: "bold", color: "primary", children: "Products" }), _jsx(Text, { className: "mt-1", color: "muted", size: "sm", children: "Discover amazing products and deals" }), showAuctionsBanner && (_jsx(Div, { className: "mt-3", children: _jsx(TextLink, { variant: "bare", href: String(ROUTES.PUBLIC.AUCTIONS), rounded: "full", paddingX: "sm", paddingY: "2xs", size: "xs", weight: "medium", layout: "inline-flex", align: "center", gap: "sm", className: "border border-primary/30 bg-primary/10 text-primary-700 dark:text-primary-400 hover:bg-primary/15 transition-colors", children: "\uD83C\uDFF7\uFE0F Looking for unique deals? Browse Auctions \u2192" }) }))] }) }), _jsxs(Container, { size: "xl", padding: "x-md", children: [_jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-4 mt-4" }), _jsx(ProductsIndexListing, { initialData: products ?? undefined }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] })] }));
|
|
57
90
|
}
|
|
@@ -83,6 +83,23 @@ export interface CustomShipOrderInput {
|
|
|
83
83
|
shippingCarrier: string;
|
|
84
84
|
trackingNumber: string;
|
|
85
85
|
trackingUrl: string;
|
|
86
|
+
/**
|
|
87
|
+
* What this parcel actually cost the seller to post, in decimal rupees.
|
|
88
|
+
*
|
|
89
|
+
* 🛑 OPTIONAL, and that is the whole design. Making it required would break
|
|
90
|
+
* every existing dispatch and would force a number out of sellers who ship
|
|
91
|
+
* manually and have no quote to be measured against. Supplying it is what
|
|
92
|
+
* opts the order into reconciliation: the buyer gets points back when the
|
|
93
|
+
* seller spent less than was charged, and the seller absorbs the overage
|
|
94
|
+
* when they spent more.
|
|
95
|
+
*
|
|
96
|
+
* Absence therefore means "not reconciled", never "cost nothing" — the same
|
|
97
|
+
* distinction `resolveShippingCost.shippingUnavailable` exists to preserve,
|
|
98
|
+
* where a silent zero "would ship every parcel free at our expense".
|
|
99
|
+
*/
|
|
100
|
+
actualShippingCost?: number;
|
|
101
|
+
/** The courier actually used, when it differs from the one the buyer picked. */
|
|
102
|
+
actualCourierName?: string;
|
|
86
103
|
}
|
|
87
104
|
export interface MarkEmiInstallmentPaidInput {
|
|
88
105
|
/** 1-based installment number, matches EmiInstallment.index. */
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { serverLogger } from "../../../monitoring/index.js";
|
|
12
12
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
13
|
+
import { reconcileOrderShipping } from "../../../_internal/server/features/shipping/reconcile.js";
|
|
13
14
|
import { enqueueJob } from "../../jobs/actions/enqueue-job.js";
|
|
14
15
|
import { isAdminUser, isStrictSellerUser, isTesterUser } from "../../auth/role-predicates.js";
|
|
15
16
|
import { ApiError, AuthorizationError, NotFoundError, ValidationError, } from "../../../errors/index.js";
|
|
@@ -712,6 +713,38 @@ export async function customShipOrder(userId, userRole, orderId, input) {
|
|
|
712
713
|
shippingDate: new Date(),
|
|
713
714
|
payoutStatus: "eligible",
|
|
714
715
|
});
|
|
716
|
+
/*
|
|
717
|
+
* 🛑 AFTER the order is marked shipped, and never allowed to fail the
|
|
718
|
+
* dispatch.
|
|
719
|
+
*
|
|
720
|
+
* The parcel has physically gone. If reconciliation throws — the points
|
|
721
|
+
* ledger is unreachable, the order read fails — the correct outcome is a
|
|
722
|
+
* shipped order that has not been reconciled yet, which is recoverable by
|
|
723
|
+
* re-running it. Letting it propagate would leave the seller unable to mark
|
|
724
|
+
* a parcel shipped because of an accounting step they have no control over,
|
|
725
|
+
* and they would retry the dispatch, which is the one thing that must not
|
|
726
|
+
* happen twice.
|
|
727
|
+
*
|
|
728
|
+
* `reconcileOrderShipping` is itself idempotent on
|
|
729
|
+
* `order.shippingReconciliation`, so a retry is safe.
|
|
730
|
+
*/
|
|
731
|
+
if (typeof input.actualShippingCost === "number") {
|
|
732
|
+
try {
|
|
733
|
+
const outcome = await reconcileOrderShipping(orderId, {
|
|
734
|
+
courierId: input.actualCourierName ?? input.shippingCarrier,
|
|
735
|
+
courierName: input.actualCourierName ?? input.shippingCarrier,
|
|
736
|
+
actualRate: input.actualShippingCost,
|
|
737
|
+
});
|
|
738
|
+
serverLogger.info("shipping reconciled", { orderId, ...outcome });
|
|
739
|
+
}
|
|
740
|
+
catch (err) {
|
|
741
|
+
const normalized = normalizeError(err);
|
|
742
|
+
serverLogger.error("shipping reconciliation failed after dispatch", {
|
|
743
|
+
orderId,
|
|
744
|
+
message: normalized.message,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
}
|
|
715
748
|
serverLogger.info("customShipOrder", {
|
|
716
749
|
orderId,
|
|
717
750
|
userId,
|
package/dist/index.d.ts
CHANGED
|
@@ -3123,6 +3123,7 @@ export { withRetry } from "./http/retry";
|
|
|
3123
3123
|
export { normalizeError, getErrorMessage, isApiNormalized, isAppNormalized, isFirebaseAuthNormalized, isFirebaseFirestoreNormalized, isFirebaseStorageNormalized, isNativeNormalized, isNetworkNormalized, isUnknownNormalized, isZodNormalized, } from "./errors/normalize";
|
|
3124
3124
|
export type { NormalizedError, NormalizedApiError, NormalizedAppError, NormalizedFirebaseAuthError, NormalizedFirebaseFirestoreError, NormalizedFirebaseStorageError, NormalizedNativeError, NormalizedNetworkError, NormalizedUnknownThrownValue, NormalizedZodError, } from "./errors/normalize";
|
|
3125
3125
|
export { CART_LANE, CART_LANE_PRIORITY, CART_LANE_LABELS, laneOf, activeLane, laneItems, laneCounts, isLaneCheckoutable, laneBlockReason, isLockedLane, canAddNewItems, type CartLane, type LaneAssignable, } from "./_internal/shared/checkout/lanes";
|
|
3126
|
+
export { roundShippingUp, toQuoteOptions, reconcileShipping, SHIPPING_ROUND_UP_TO, type ShippingQuoteOption, type SelectedShippingQuote, type ActualShipmentCost, type ShippingReconciliation, } from "./_internal/shared/shipping/quote";
|
|
3126
3127
|
export { newsletterSubscribeSchema, quickCreateTaxonomySchema, payoutMarkPaidSchema, } from "./features/admin/schemas/small-forms";
|
|
3127
3128
|
export { raffleEntrySchema } from "./features/contact/schemas/inquiry-forms";
|
|
3128
3129
|
export { orderCancelSchema, paymentProofSchema, } from "./features/orders/schemas/buyer-forms";
|
package/dist/index.js
CHANGED
|
@@ -5559,6 +5559,14 @@ export { normalizeError, getErrorMessage, isApiNormalized, isAppNormalized, isFi
|
|
|
5559
5559
|
// Checkout lanes — the derived auction > offer > standard partition of the
|
|
5560
5560
|
// cart, and the priority rule that decides which one may be checked out.
|
|
5561
5561
|
export { CART_LANE, CART_LANE_PRIORITY, CART_LANE_LABELS, laneOf, activeLane, laneItems, laneCounts, isLaneCheckoutable, laneBlockReason, isLockedLane, canAddNewItems, } from "./_internal/shared/checkout/lanes.js";
|
|
5562
|
+
/*
|
|
5563
|
+
* Shipping quote model. Public because the CONSUMER owns the route that asks
|
|
5564
|
+
* the carrier for rates (`/api/shipping/quote`) and the checkout UI that
|
|
5565
|
+
* renders them — the rounding rule has to be reachable from both, and it must
|
|
5566
|
+
* be the same rule the server-side reconciliation uses, or the buyer is
|
|
5567
|
+
* credited against a figure they were never charged.
|
|
5568
|
+
*/
|
|
5569
|
+
export { roundShippingUp, toQuoteOptions, reconcileShipping, SHIPPING_ROUND_UP_TO, } from "./_internal/shared/shipping/quote.js";
|
|
5562
5570
|
export { newsletterSubscribeSchema, quickCreateTaxonomySchema, payoutMarkPaidSchema, } from "./features/admin/schemas/small-forms.js";
|
|
5563
5571
|
export { raffleEntrySchema } from "./features/contact/schemas/inquiry-forms.js";
|
|
5564
5572
|
export { orderCancelSchema, paymentProofSchema, } from "./features/orders/schemas/buyer-forms.js";
|
|
@@ -12,10 +12,19 @@
|
|
|
12
12
|
* `loginResponse` requires — the rest are ignored, as Zod ignores unknown
|
|
13
13
|
* keys by default.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* ✅ ALSO VERIFIED (2026-10-20): `GET /courier/serviceability/` from pickup
|
|
16
|
+
* `413001`. HTTP 200, and `data.available_courier_companies` came back with
|
|
17
|
+
* 10-11 entries for Mumbai, Bengaluru and Delhi carrying `courier_name`,
|
|
18
|
+
* `rate` and `estimated_delivery_days` — so `serviceabilityResponse` below
|
|
19
|
+
* is read off a real payload, not inferred. It is a read-only quote: no
|
|
20
|
+
* shipment is created and nothing is booked.
|
|
21
|
+
*
|
|
22
|
+
* 🛑 NOT VERIFIED: order creation, AWB assignment, tracking and label
|
|
23
|
+
* generation. Each MUTATES or bills a real carrier account — creating an
|
|
24
|
+
* order can schedule a pickup, an AWB can consume an allocation, a cancel
|
|
25
|
+
* may still bill — so none was called, and none is discoverable without
|
|
26
|
+
* incurring it. Their field names below are claims from the published docs,
|
|
27
|
+
* not facts read off a payload.
|
|
19
28
|
*
|
|
20
29
|
* That asymmetry is the reason every response is parsed through Zod rather
|
|
21
30
|
* than cast: an unverified contract that is wrong surfaces as a named error at
|
|
@@ -12,10 +12,19 @@
|
|
|
12
12
|
* `loginResponse` requires — the rest are ignored, as Zod ignores unknown
|
|
13
13
|
* keys by default.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* ✅ ALSO VERIFIED (2026-10-20): `GET /courier/serviceability/` from pickup
|
|
16
|
+
* `413001`. HTTP 200, and `data.available_courier_companies` came back with
|
|
17
|
+
* 10-11 entries for Mumbai, Bengaluru and Delhi carrying `courier_name`,
|
|
18
|
+
* `rate` and `estimated_delivery_days` — so `serviceabilityResponse` below
|
|
19
|
+
* is read off a real payload, not inferred. It is a read-only quote: no
|
|
20
|
+
* shipment is created and nothing is booked.
|
|
21
|
+
*
|
|
22
|
+
* 🛑 NOT VERIFIED: order creation, AWB assignment, tracking and label
|
|
23
|
+
* generation. Each MUTATES or bills a real carrier account — creating an
|
|
24
|
+
* order can schedule a pickup, an AWB can consume an allocation, a cancel
|
|
25
|
+
* may still bill — so none was called, and none is discoverable without
|
|
26
|
+
* incurring it. Their field names below are claims from the published docs,
|
|
27
|
+
* not facts read off a payload.
|
|
19
28
|
*
|
|
20
29
|
* That asymmetry is the reason every response is parsed through Zod rather
|
|
21
30
|
* than cast: an unverified contract that is wrong surfaces as a named error at
|
|
@@ -312,15 +321,30 @@ export class ShiprocketShippingProvider extends IShippingProvider {
|
|
|
312
321
|
}
|
|
313
322
|
async checkServiceability(pincode, weight) {
|
|
314
323
|
/*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
* "
|
|
319
|
-
*
|
|
324
|
+
* `pickup_postcode` is required by their API and comes from
|
|
325
|
+
* `SHIPROCKET_PICKUP_PINCODE`.
|
|
326
|
+
*
|
|
327
|
+
* 🛑 This comment used to say "we do not have it on this contract", which
|
|
328
|
+
* was wrong and cost the whole method: the consumer's `appkit.config.js →
|
|
329
|
+
* business.registeredAddress` has carried `postalCode` all along. Nothing
|
|
330
|
+
* set the var, so every call returned `{ isServiceable: false }` WITHOUT
|
|
331
|
+
* calling the API — checkout would have told every buyer we cannot ship to
|
|
332
|
+
* them, for every destination. A quote that is silently always "no" is
|
|
333
|
+
* indistinguishable from a carrier with no coverage, which is why it had no
|
|
334
|
+
* symptom. Verified live once the var was populated: 10-11 couriers quoted
|
|
335
|
+
* from 413001 to Mumbai, Bengaluru and Delhi.
|
|
336
|
+
*
|
|
337
|
+
* 🛑 appkit has no reader for a consumer config file, so the DEFAULT is the
|
|
338
|
+
* consumer's job: `src/providers.config.ts` populates this var from the
|
|
339
|
+
* business block at boot. Deliberately not threaded through
|
|
340
|
+
* `configurePlatformSupplier` — that seam's own 🛑 says it "is NOT a cache
|
|
341
|
+
* of site settings and must not become one"; it answers *who the invoice
|
|
342
|
+
* supplier is*, and a carrier pickup field there is mission creep on a seam
|
|
343
|
+
* with one stated purpose. One var, one reader, no precedence rule.
|
|
320
344
|
*/
|
|
321
345
|
const origin = process.env.SHIPROCKET_PICKUP_PINCODE?.trim();
|
|
322
346
|
if (!origin) {
|
|
323
|
-
serverLogger.warn("shiprocket:
|
|
347
|
+
serverLogger.warn("shiprocket: no pickup postcode — set business.registeredAddress.postalCode in appkit.config.js, or SHIPROCKET_PICKUP_PINCODE directly", { pincode });
|
|
324
348
|
return { isServiceable: false, couriers: [] };
|
|
325
349
|
}
|
|
326
350
|
try {
|
package/dist/security/csp.d.ts
CHANGED
|
@@ -12,20 +12,5 @@
|
|
|
12
12
|
*/
|
|
13
13
|
/** Generate a 16-byte base64 nonce for a single request. */
|
|
14
14
|
export declare function generateNonce(): string;
|
|
15
|
-
export interface BuildCSPOptions {
|
|
16
|
-
/**
|
|
17
|
-
* Add `'wasm-unsafe-eval'` to `script-src`, permitting
|
|
18
|
-
* `WebAssembly.compile`/`instantiate` from bytes.
|
|
19
|
-
*
|
|
20
|
-
* Required by any page that compiles WebAssembly at runtime — without it the
|
|
21
|
-
* production policy (`script-src 'self' 'nonce-…'`) blocks compilation
|
|
22
|
-
* outright, because WebAssembly compilation counts as script evaluation.
|
|
23
|
-
*
|
|
24
|
-
* Narrower than `'unsafe-eval'` on purpose: it unblocks WASM only and still
|
|
25
|
-
* forbids `eval` and `new Function` for JavaScript. Default `false`, so
|
|
26
|
-
* existing consumers are unaffected.
|
|
27
|
-
*/
|
|
28
|
-
wasm?: boolean;
|
|
29
|
-
}
|
|
30
15
|
/** Build the full CSP header value for the given nonce. */
|
|
31
|
-
export declare function buildCSP(nonce: string
|
|
16
|
+
export declare function buildCSP(nonce: string): string;
|
package/dist/security/csp.js
CHANGED
|
@@ -17,15 +17,13 @@ export function generateNonce() {
|
|
|
17
17
|
return btoa(String.fromCharCode(...bytes));
|
|
18
18
|
}
|
|
19
19
|
/** Build the full CSP header value for the given nonce. */
|
|
20
|
-
export function buildCSP(nonce
|
|
20
|
+
export function buildCSP(nonce) {
|
|
21
21
|
const isDev = process.env.NODE_ENV === "development";
|
|
22
|
-
const wasm = options.wasm === true ? " 'wasm-unsafe-eval'" : "";
|
|
23
22
|
return [
|
|
24
23
|
"default-src 'self'",
|
|
25
24
|
isDev
|
|
26
|
-
?
|
|
27
|
-
|
|
28
|
-
: `script-src 'self' 'nonce-${nonce}'${wasm}`,
|
|
25
|
+
? "script-src 'self' 'unsafe-eval' 'unsafe-inline'"
|
|
26
|
+
: `script-src 'self' 'nonce-${nonce}'`,
|
|
29
27
|
"style-src 'self' 'unsafe-inline'",
|
|
30
28
|
"img-src 'self' data: https: blob:",
|
|
31
29
|
"font-src 'self' data:",
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* WHY: Seeds 6 carousel slides for the marketplace homepage promo carousel.
|
|
3
|
-
* WHAT:
|
|
3
|
+
* WHAT: 4 active + 2 inactive. Deterministic Picsum backgrounds with a dim
|
|
4
4
|
* overlay, plus a headline / supporting line / CTA on each.
|
|
5
5
|
*
|
|
6
|
+
* 🛑 FOUR ACTIVE, NOT FIVE, SINCE 2026-10-20. `MAX_ACTIVE_SLIDES` is a
|
|
7
|
+
* ceiling, not a quota — `slide-beyblade-auction` went inactive with
|
|
8
|
+
* `listings.listingTypes.auction: false` and nothing was promoted to backfill
|
|
9
|
+
* it. Do NOT activate `slide-upcoming-promo` to get back to five: that one is
|
|
10
|
+
* the fixture `getActiveSlides()`' own filter is tested against, and it says
|
|
11
|
+
* "A draft slide kept unpublished so admins have a starting point to copy."
|
|
12
|
+
*
|
|
6
13
|
* 🛑 EVERY ACTIVE SLIDE MUST CARRY AN `overlay` OR `cards`, OR IT RENDERS BLANK.
|
|
7
14
|
* `HeroCarousel` gates all slide copy on `slide.overlay` (line ~412) and the
|
|
8
15
|
* card grid on `slide.cards.length` (line ~451). Before this pass every seeded
|
|
@@ -107,16 +114,34 @@ const _rawSlides = [
|
|
|
107
114
|
hover: { effect: "scale" },
|
|
108
115
|
},
|
|
109
116
|
{
|
|
110
|
-
|
|
117
|
+
/*
|
|
118
|
+
* 🛑 THIS WAS THE AUCTIONS CARD ("Bid / Live auctions / Place a bid
|
|
119
|
+
* before the timer runs out" → `/auctions`), and it was still on the
|
|
120
|
+
* first screen of the site weeks after `listings.listingTypes.auction`
|
|
121
|
+
* went `false`. The section-level gate in `MarketplaceHomepageView` is
|
|
122
|
+
* keyed on `SectionType` and a carousel is type `carousel` whatever it
|
|
123
|
+
* contains, so nothing could see it. `gateCarouselSlides` now can — but
|
|
124
|
+
* a filter that leaves a hole in a `repeat(3, 1fr)` row is a fallback,
|
|
125
|
+
* not a fix, so the slot gets real content instead.
|
|
126
|
+
*
|
|
127
|
+
* 🛑 NOT `/spares` and NOT `/art`, which were the obvious swaps: both
|
|
128
|
+
* types are ENABLED, both have a working browse page, and both held
|
|
129
|
+
* ZERO published products when this was written (measured against
|
|
130
|
+
* production, 2026-10-20). Replacing a dead-end card with a different
|
|
131
|
+
* dead-end card is not a fix. `/categories` is populated — six roots,
|
|
132
|
+
* 28 brands, 8 franchises — and browsing the tree is genuinely the
|
|
133
|
+
* third way to shop here.
|
|
134
|
+
*/
|
|
135
|
+
id: "card-hero-browse",
|
|
111
136
|
zone: 2,
|
|
112
137
|
mobileZone: 2,
|
|
113
138
|
background: { type: "image", url: MEDIA.lDrago, dimOverlay: { enabled: true, opacity: 0.45 } },
|
|
114
139
|
content: {
|
|
115
|
-
eyebrow: "
|
|
116
|
-
title: "
|
|
117
|
-
description: "
|
|
140
|
+
eyebrow: "Explore",
|
|
141
|
+
title: "Shop by category",
|
|
142
|
+
description: "Walk the tree from generation down to a single part.",
|
|
118
143
|
},
|
|
119
|
-
buttons: [{ id: "card-hero-
|
|
144
|
+
buttons: [{ id: "card-hero-browse-cta", text: "Browse categories", href: String(ROUTES.PUBLIC.CATEGORIES), variant: "primary" }],
|
|
120
145
|
hover: { effect: "scale" },
|
|
121
146
|
},
|
|
122
147
|
{
|
|
@@ -139,10 +164,26 @@ const _rawSlides = [
|
|
|
139
164
|
updatedAt: daysAgo(3),
|
|
140
165
|
},
|
|
141
166
|
{
|
|
167
|
+
/*
|
|
168
|
+
* 🛑 DISABLED, NOT DELETED — the same arrangement the auctions RAIL uses in
|
|
169
|
+
* `homepage-sections-seed-data.ts`, and for the same reason. Every word of
|
|
170
|
+
* the copy, the video background and the CTA stay exactly as they were, so
|
|
171
|
+
* flipping `listings.listingTypes.auction` back to `true` and this flag
|
|
172
|
+
* with it restores the slide intact.
|
|
173
|
+
*
|
|
174
|
+
* Two locks, deliberately. `gateCarouselSlides` already drops this at
|
|
175
|
+
* render because `overlay.button.link` and `link.url` both resolve to
|
|
176
|
+
* `/auctions` — but that one is invisible, and a slide listed as active in
|
|
177
|
+
* the carousel admin while never appearing on the page reads as a bug in
|
|
178
|
+
* the admin. `active: false` is the lock a human sees.
|
|
179
|
+
*
|
|
180
|
+
* 🛑 The copy names no listing type — "Win it at your price" — which is
|
|
181
|
+
* exactly why the gate matches on the LINK and not on the words.
|
|
182
|
+
*/
|
|
142
183
|
id: "slide-beyblade-auction",
|
|
143
184
|
title: "Live auctions",
|
|
144
185
|
order: 2,
|
|
145
|
-
active:
|
|
186
|
+
active: false,
|
|
146
187
|
background: {
|
|
147
188
|
type: "video",
|
|
148
189
|
// Direct MP4 URL, deliberately NOT wrapped in seedExtMedia() — that
|
|
@@ -525,7 +525,7 @@ const faqSeedRecords = [
|
|
|
525
525
|
{
|
|
526
526
|
id: "faq-emi-installments",
|
|
527
527
|
question: "Can I pay in installments (EMI)?",
|
|
528
|
-
answer: html(`<p>Yes — on eligible orders. If the seller has EMI enabled and your order with them meets the minimum order value, "Pay in EMI" appears as a payment option at checkout. Choose a tenure of 2–6 months, pay a small token amount upfront, and the remaining balance plus a small monthly surcharge is billed automatically in equal installments each month. Your order ships
|
|
528
|
+
answer: html(`<p>Yes — on eligible orders. If the seller has EMI enabled and your order with them meets the minimum order value, "Pay in EMI" appears as a payment option at checkout. Choose a tenure of 2–6 months, pay a small token amount upfront, and the remaining balance plus a small monthly surcharge is billed automatically in equal installments each month. Your order ships once the EMI is fully paid — the token confirms your commitment and secures the item, it is not a dispatch trigger. A seller may opt an individual listing into earlier dispatch, and that is stated on the listing.</p>`),
|
|
529
529
|
category: "orders_payment",
|
|
530
530
|
showOnHomepage: false,
|
|
531
531
|
showInFooter: false,
|
|
@@ -474,7 +474,7 @@ export const siteSettingsSeedData = {
|
|
|
474
474
|
minimumTransactionFee: 0,
|
|
475
475
|
gatewayFeePercent: 2,
|
|
476
476
|
codDepositPercent: 10,
|
|
477
|
-
|
|
477
|
+
codHandlingFeeCap: 200,
|
|
478
478
|
codHandlingFeePercent: 10,
|
|
479
479
|
// Enabled so a seeded install actually shows the add-ons — see the matching
|
|
480
480
|
// comment on DEFAULT_SITE_SETTINGS_DATA.commissions.
|
package/dist/seo/json-ld.d.ts
CHANGED
|
@@ -29,7 +29,14 @@ export interface ProductJsonLdInput {
|
|
|
29
29
|
price: number;
|
|
30
30
|
currency: string;
|
|
31
31
|
mainImage?: string;
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Prefer `productImageUrls(doc.images)`. The object form is accepted so a
|
|
34
|
+
* document holding `ProductImage[]` cannot take the page down — see the
|
|
35
|
+
* note on `imageUrlOf` in `productJsonLd`.
|
|
36
|
+
*/
|
|
37
|
+
images?: readonly (string | {
|
|
38
|
+
url?: string;
|
|
39
|
+
})[];
|
|
33
40
|
sellerName?: string;
|
|
34
41
|
category?: string;
|
|
35
42
|
status?: string;
|
package/dist/seo/json-ld.js
CHANGED
|
@@ -27,7 +27,27 @@ const SITE_NAME = process.env.NEXT_PUBLIC_SITE_NAME ?? "App";
|
|
|
27
27
|
* an empty value so `JSON.stringify` drops the key rather than emitting `""`.
|
|
28
28
|
*/
|
|
29
29
|
function absoluteUrl(value, base = SITE_URL) {
|
|
30
|
-
|
|
30
|
+
/*
|
|
31
|
+
* 🛑 `unknown` + a typeof test, NOT `string | null | undefined` + `?.trim()`.
|
|
32
|
+
*
|
|
33
|
+
* This took production down for hours. The signature said `string`, every
|
|
34
|
+
* caller type-checked, and then `ProductImage[]` replaced `string[]` on the
|
|
35
|
+
* product document — so `images.map(absoluteUrl)` received OBJECTS, `?.trim`
|
|
36
|
+
* was not a function, and the throw escaped into the RSC render and 500'd
|
|
37
|
+
* `/[locale]/products/[slug]` and `/[locale]/auctions/[id]`: **72 recorded
|
|
38
|
+
* failures**, the whole page lost to a decorative metadata field.
|
|
39
|
+
*
|
|
40
|
+
* A declared parameter type is a compile-time claim about CALLERS. It is
|
|
41
|
+
* worth nothing at the boundary where data arrives from Firestore, which is
|
|
42
|
+
* exactly where this function sits — and a schema migration is precisely the
|
|
43
|
+
* event that makes the claim false everywhere at once.
|
|
44
|
+
*
|
|
45
|
+
* So the contract is now behavioural: anything that is not a usable string
|
|
46
|
+
* is DROPPED, and the key is omitted. JSON-LD is optional enrichment —
|
|
47
|
+
* losing one image costs a rich-result thumbnail, while throwing costs the
|
|
48
|
+
* entire page. Those are not close.
|
|
49
|
+
*/
|
|
50
|
+
const v = typeof value === "string" ? value.trim() : undefined;
|
|
31
51
|
if (!v)
|
|
32
52
|
return undefined;
|
|
33
53
|
if (/^(https?:)?\/\//i.test(v) || v.startsWith("data:"))
|
|
@@ -92,11 +112,28 @@ export function productJsonLd(product) {
|
|
|
92
112
|
const url = `${SITE_URL}${product.detailPath ?? `/products/${product.slug}`}`;
|
|
93
113
|
// Absolutise, then de-duplicate: `mainImage` is very often also `images[0]`,
|
|
94
114
|
// which produced the same URL twice in the emitted array.
|
|
115
|
+
/*
|
|
116
|
+
* 🛑 Accepts BOTH shapes on purpose. `absoluteUrl` now drops a non-string
|
|
117
|
+
* instead of throwing, which stops the 500 — but silently dropping every
|
|
118
|
+
* image would trade a crash for an invisible loss of the `image` property
|
|
119
|
+
* that Product rich results REQUIRE. Reading `.url` off an object keeps the
|
|
120
|
+
* rich result working whichever shape the document holds, so a caller that
|
|
121
|
+
* forgets `productImageUrls()` degrades to nothing worse than correct.
|
|
122
|
+
*/
|
|
123
|
+
/*
|
|
124
|
+
* The cast narrows to `{ url?: string }` rather than `{ url?: unknown }`
|
|
125
|
+
* (`audit-unknown-leakage` blocks the latter, correctly — an `unknown` in a
|
|
126
|
+
* member position defeats checking on every read below it). The assertion is
|
|
127
|
+
* safe without being trusted: `absoluteUrl` tests `typeof value === "string"`
|
|
128
|
+
* itself, so a document whose `url` is a number is dropped rather than
|
|
129
|
+
* reaching `.trim()`. Narrow for the compiler, verify at runtime.
|
|
130
|
+
*/
|
|
131
|
+
const imageUrlOf = (entry) => typeof entry === "string" ? entry : entry?.url;
|
|
95
132
|
const images = [
|
|
96
133
|
...(product.mainImage ? [product.mainImage] : []),
|
|
97
134
|
...(product.images || []),
|
|
98
135
|
]
|
|
99
|
-
.map((img) => absoluteUrl(img))
|
|
136
|
+
.map((img) => absoluteUrl(imageUrlOf(img)))
|
|
100
137
|
.filter((img) => Boolean(img))
|
|
101
138
|
.filter((img, i, arr) => arr.indexOf(img) === i);
|
|
102
139
|
return {
|