@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.
Files changed (86) hide show
  1. package/dist/.build-stamp.json +4 -4
  2. package/dist/_internal/server/features/checkout/actions.d.ts +22 -0
  3. package/dist/_internal/server/features/checkout/actions.js +100 -5
  4. package/dist/_internal/server/features/shipping/reconcile.d.ts +28 -0
  5. package/dist/_internal/server/features/shipping/reconcile.js +93 -0
  6. package/dist/_internal/shared/config/schema.d.ts +6 -0
  7. package/dist/_internal/shared/features/checkout/config.d.ts +1 -1
  8. package/dist/_internal/shared/features/checkout/config.js +1 -1
  9. package/dist/_internal/shared/features/site-settings/fees.d.ts +1 -1
  10. package/dist/_internal/shared/features/site-settings/fees.js +1 -1
  11. package/dist/_internal/shared/fees/calculator.d.ts +35 -3
  12. package/dist/_internal/shared/fees/calculator.js +34 -4
  13. package/dist/_internal/shared/listing-types/route-gating.d.ts +56 -0
  14. package/dist/_internal/shared/listing-types/route-gating.js +114 -0
  15. package/dist/_internal/shared/shipping/quote.d.ts +153 -0
  16. package/dist/_internal/shared/shipping/quote.js +124 -0
  17. package/dist/client.d.ts +1 -0
  18. package/dist/client.js +8 -0
  19. package/dist/errors/error-codes.d.ts +15 -0
  20. package/dist/errors/error-codes.js +15 -0
  21. package/dist/errors/index.d.ts +1 -1
  22. package/dist/errors/index.js +1 -1
  23. package/dist/features/account/components/UserBidsView.js +4 -2
  24. package/dist/features/admin/components/AdminSiteSettingsView.js +18 -2
  25. package/dist/features/admin/schemas/firestore.d.ts +37 -2
  26. package/dist/features/admin/schemas/firestore.js +12 -1
  27. package/dist/features/auctions/components/AuctionDetailPageView.js +2 -1
  28. package/dist/features/auth/schemas/firestore.d.ts +15 -0
  29. package/dist/features/cart/components/CartPriceBreakdown.js +1 -1
  30. package/dist/features/checkout/schemas/firestore.d.ts +12 -1
  31. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +2 -1
  32. package/dist/features/homepage/components/HeroCarousel.js +4 -2
  33. package/dist/features/homepage/components/MarketplaceHomepageView.js +16 -1
  34. package/dist/features/homepage/components/WelcomeSection.d.ts +16 -3
  35. package/dist/features/homepage/components/WelcomeSection.js +3 -3
  36. package/dist/features/homepage/lib/section-renderer.d.ts +9 -0
  37. package/dist/features/homepage/lib/section-renderer.js +22 -5
  38. package/dist/features/homepage/lib/slide-gating.d.ts +52 -0
  39. package/dist/features/homepage/lib/slide-gating.js +98 -0
  40. package/dist/features/layout/NavbarLayout.js +139 -32
  41. package/dist/features/live/components/LiveItemDetailPageView.js +2 -1
  42. package/dist/features/media/MediaAvatar.js +1 -1
  43. package/dist/features/media/MediaImage.d.ts +18 -1
  44. package/dist/features/media/MediaImage.js +1 -1
  45. package/dist/features/media/messages/en.json +67 -67
  46. package/dist/features/orders/schemas/firestore.d.ts +39 -1
  47. package/dist/features/points/repository/points.repository.d.ts +81 -0
  48. package/dist/features/points/repository/points.repository.js +155 -0
  49. package/dist/features/points/schemas/firestore.d.ts +102 -0
  50. package/dist/features/points/schemas/firestore.js +99 -0
  51. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -1
  52. package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -1
  53. package/dist/features/products/components/ProductDetailPageView.js +5 -26
  54. package/dist/features/products/components/ProductOwnerQuickActions.d.ts +11 -0
  55. package/dist/features/products/components/ProductOwnerQuickActions.js +120 -0
  56. package/dist/features/products/components/ProductsIndexListing.js +35 -5
  57. package/dist/features/products/components/ProductsIndexPageView.js +40 -7
  58. package/dist/features/seller/actions/seller-actions.d.ts +17 -0
  59. package/dist/features/seller/actions/seller-actions.js +33 -0
  60. package/dist/index.d.ts +1 -0
  61. package/dist/index.js +8 -0
  62. package/dist/providers/shipping-shiprocket/index.d.ts +13 -4
  63. package/dist/providers/shipping-shiprocket/index.js +34 -10
  64. package/dist/security/csp.d.ts +1 -16
  65. package/dist/security/csp.js +3 -5
  66. package/dist/seed/carousel-slides-seed-data.js +48 -7
  67. package/dist/seed/faq-seed-data.js +1 -1
  68. package/dist/seed/site-settings-seed-data.js +1 -1
  69. package/dist/seo/json-ld.d.ts +8 -1
  70. package/dist/seo/json-ld.js +39 -2
  71. package/dist/styles.css +419 -542
  72. package/dist/tailwind-utilities.css +1 -1
  73. package/dist/tokens/themes/index.d.ts +1 -3
  74. package/dist/tokens/themes/index.js +1 -11
  75. package/dist/tokens/tokens.css +0 -155
  76. package/dist/ui/components/GatedPrice.style.css +43 -43
  77. package/dist/ui/components/ImageMosaic.d.ts +17 -1
  78. package/dist/ui/components/ImageMosaic.js +28 -5
  79. package/dist/ui/components/ImageMosaic.style.css +37 -5
  80. package/dist/ui/components/QuickCreateModal.style.css +78 -78
  81. package/dist/ui/forms/FormShell.style.css +260 -260
  82. package/package.json +1 -1
  83. package/dist/tokens/themes/technical-dark.d.ts +0 -30
  84. package/dist/tokens/themes/technical-dark.js +0 -103
  85. package/dist/tokens/themes/technical-light.d.ts +0 -22
  86. 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) => listingTypes.includes(t));
86
- const effectiveTypes = (selectedTypes.length > 0 ? selectedTypes : listingTypes);
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) => listingTypes.includes(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 = typeTabs.map((t) => ({ value: t.id, label: t.label }));
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 listPublicProducts(parsePublicProductParams(searchParams, {
51
- listingTypes: GENERIC_PRODUCT_LISTING_TYPES,
52
- pageSize: DEFAULT_PAGE_SIZE,
53
- sorts: DEFAULT_SORT,
54
- ...defaultAvailabilityForListingTypes(effectiveTypes),
55
- }));
56
- 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" }), _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" })] })] }));
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
- * 🛑 NOT VERIFIED: every other endpoint. Order creation, AWB assignment,
16
- * tracking, serviceability and label generation all MUTATE or bill against
17
- * a real carrier account, so none was called. Their field names below are
18
- * claims taken from the published docs, not facts read off a payload.
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
- * 🛑 NOT VERIFIED: every other endpoint. Order creation, AWB assignment,
16
- * tracking, serviceability and label generation all MUTATE or bill against
17
- * a real carrier account, so none was called. Their field names below are
18
- * claims taken from the published docs, not facts read off a payload.
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
- * 🛑 `pickup_postcode` is required by their API and we do not have it on
316
- * this contract. Taken from the registered pickup location's name is NOT
317
- * possible — it is a nickname, not a pincode — so this returns
318
- * "unserviceable" rather than guessing a origin pincode, which would
319
- * produce confidently wrong rates.
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: SHIPROCKET_PICKUP_PINCODE unset — serviceability cannot be checked", { pincode });
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 {
@@ -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, options?: BuildCSPOptions): string;
16
+ export declare function buildCSP(nonce: string): string;
@@ -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, options = {}) {
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
- ? // 'unsafe-eval' already subsumes 'wasm-unsafe-eval' in dev.
27
- "script-src 'self' 'unsafe-eval' 'unsafe-inline'"
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: 5 active + 1 inactive. Deterministic Picsum backgrounds with a dim
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
- id: "card-hero-bid",
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: "Bid",
116
- title: "Live auctions",
117
- description: "Place a bid before the timer runs out.",
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-bid-cta", text: "Browse auctions", href: String(ROUTES.PUBLIC.AUCTIONS), variant: "primary" }],
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: true,
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 as soon as the token payment is confirmed — you don't wait for the full amount to be paid.</p>`),
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
- codHandlingFeeMin: 200,
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.
@@ -29,7 +29,14 @@ export interface ProductJsonLdInput {
29
29
  price: number;
30
30
  currency: string;
31
31
  mainImage?: string;
32
- images?: string[];
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;
@@ -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
- const v = value?.trim();
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 {