@mohasinac/appkit 3.1.7 → 3.2.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 (41) hide show
  1. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +39 -9
  2. package/dist/_internal/client/features/tour/TourProvider.d.ts +12 -0
  3. package/dist/_internal/client/features/tour/TourProvider.js +19 -0
  4. package/dist/_internal/server/features/checkout/actions.js +8 -2
  5. package/dist/_internal/server/features/media/contextGuards.js +2 -2
  6. package/dist/_internal/server/features/orders/actions.d.ts +6 -0
  7. package/dist/_internal/server/features/orders/actions.js +43 -1
  8. package/dist/_internal/server/jobs/core/assignSpinPrize.js +4 -0
  9. package/dist/_internal/server/jobs/core/auctionSettlement.js +4 -0
  10. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +4 -0
  11. package/dist/_internal/server/jobs/core/bundleStockSync.js +4 -0
  12. package/dist/_internal/server/jobs/core/payoutBatch.js +4 -0
  13. package/dist/_internal/server/jobs/core/prizeRevealClose.js +4 -0
  14. package/dist/_internal/server/jobs/core/prizeRevealExpiry.js +4 -0
  15. package/dist/_internal/server/jobs/core/prizeRevealOpen.js +4 -0
  16. package/dist/_internal/server/jobs/core/prizeRevealReminder.js +4 -0
  17. package/dist/_internal/server/jobs/core/promotions.js +4 -0
  18. package/dist/_internal/server/jobs/core/triggerEventRaffle.js +4 -0
  19. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +4 -0
  20. package/dist/_internal/shared/actions/action-registry.js +13 -0
  21. package/dist/_internal/shared/features/checkout/config.d.ts +1 -1
  22. package/dist/_internal/shared/features/checkout/config.js +1 -1
  23. package/dist/features/admin/components/AdminOrderEditorView.d.ts +5 -1
  24. package/dist/features/admin/components/AdminOrderEditorView.js +30 -4
  25. package/dist/features/admin/components/AdminOrdersView.js +5 -1
  26. package/dist/features/layout/AppLayoutShell.d.ts +3 -1
  27. package/dist/features/layout/AppLayoutShell.js +2 -2
  28. package/dist/features/layout/TitleBarLayout.d.ts +3 -1
  29. package/dist/features/layout/TitleBarLayout.js +3 -2
  30. package/dist/features/orders/schemas/firestore.d.ts +9 -0
  31. package/dist/features/orders/schemas/firestore.js +1 -0
  32. package/dist/features/products/components/ProductForm.js +5 -3
  33. package/dist/next/routing/route-map.d.ts +2 -0
  34. package/dist/next/routing/route-map.js +1 -0
  35. package/dist/seed/orders-seed-data.js +55 -1
  36. package/dist/seed/site-settings-seed-data.js +15 -5
  37. package/dist/server.d.ts +1 -0
  38. package/dist/server.js +2 -0
  39. package/dist/utils/id-generators.d.ts +6 -0
  40. package/dist/utils/id-generators.js +9 -0
  41. package/package.json +1 -1
@@ -34,36 +34,65 @@ import { Div } from "../../../../ui";
34
34
  * Hoisted drawer-state hook — the matchMedia-aware open/close logic that was
35
35
  * triplicated across admin/store/user layouts. Used internally by
36
36
  * DashboardLayoutClient; not exported because it's not generically useful.
37
+ *
38
+ * storageKey: variant-scoped localStorage key (`appkit:sidebar-open:{variant}`)
39
+ * so admin/store/user each persist their own collapse state independently.
37
40
  */
38
- function useResponsiveDrawer() {
41
+ function useResponsiveDrawer(storageKey) {
39
42
  const [desktopOpen, setDesktopOpen] = useState(false);
40
43
  const [mobileOpen, setMobileOpen] = useState(false);
41
44
  const { registerNav, unregisterNav } = useDashboardNav();
42
45
  const isDesktop = useCallback(() => typeof window !== "undefined" && window.matchMedia(DASHBOARD_DESKTOP_MEDIA_QUERY).matches, []);
46
+ // Restore persisted desktop-open state after hydration (avoids SSR mismatch).
47
+ useEffect(() => {
48
+ try {
49
+ if (localStorage.getItem(storageKey) === "true")
50
+ setDesktopOpen(true);
51
+ }
52
+ catch { /* localStorage unavailable (private-browse / SSR fallback) */ }
53
+ }, [storageKey]);
43
54
  const open = useCallback(() => {
44
55
  startTransition(() => {
45
- if (isDesktop())
56
+ if (isDesktop()) {
57
+ try {
58
+ localStorage.setItem(storageKey, "true");
59
+ }
60
+ catch { /* noop */ }
46
61
  setDesktopOpen(true);
62
+ }
47
63
  else
48
64
  setMobileOpen(true);
49
65
  });
50
- }, [isDesktop]);
66
+ }, [isDesktop, storageKey]);
51
67
  const close = useCallback(() => {
52
68
  startTransition(() => {
53
- if (isDesktop())
69
+ if (isDesktop()) {
70
+ try {
71
+ localStorage.setItem(storageKey, "false");
72
+ }
73
+ catch { /* noop */ }
54
74
  setDesktopOpen(false);
75
+ }
55
76
  else
56
77
  setMobileOpen(false);
57
78
  });
58
- }, [isDesktop]);
79
+ }, [isDesktop, storageKey]);
59
80
  const toggle = useCallback(() => {
60
81
  startTransition(() => {
61
- if (isDesktop())
62
- setDesktopOpen((prev) => !prev);
82
+ if (isDesktop()) {
83
+ setDesktopOpen((prev) => {
84
+ const next = !prev;
85
+ try {
86
+ localStorage.setItem(storageKey, String(next));
87
+ }
88
+ catch { /* noop */ }
89
+ return next;
90
+ });
91
+ }
63
92
  else
64
93
  setMobileOpen((prev) => !prev);
65
94
  });
66
- }, [isDesktop]);
95
+ }, [isDesktop, storageKey]);
67
96
  useEffect(() => {
68
97
  registerNav({ open, close, toggle });
69
98
  return () => unregisterNav();
@@ -87,7 +116,8 @@ const DEFAULT_CONTENT_MAX_WIDTH = "max-w-screen-2xl";
87
116
  export function DashboardLayoutClient({ variant, groups, permissions, activeHref: explicitActiveHref, responsive: _responsive, className, contentPadding, contentSurface, contentMaxWidth, children, }) {
88
117
  const pathname = usePathname();
89
118
  const activeHref = explicitActiveHref ?? pathname ?? "";
90
- const { desktopOpen, mobileOpen, close, toggle } = useResponsiveDrawer();
119
+ const storageKey = `appkit:sidebar-open:${variant}`;
120
+ const { desktopOpen, mobileOpen, close, toggle } = useResponsiveDrawer(storageKey);
91
121
  const { data: settings } = useSiteSettings();
92
122
  const navConfig = settings?.navConfig;
93
123
  const filteredGroups = filterGroups(groups, navConfig, permissions);
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+ export interface TourContextValue {
3
+ startTour: () => void;
4
+ }
5
+ export declare function useTour(): TourContextValue;
6
+ /**
7
+ * TourProvider — skeleton shell for Patch 1.
8
+ * driver.js steps are wired in Patch 2+; this provider is an identity wrapper.
9
+ */
10
+ export declare function TourProvider({ children }: {
11
+ children: React.ReactNode;
12
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import React from "react";
4
+ const TourContext = React.createContext({
5
+ startTour: () => { },
6
+ });
7
+ export function useTour() {
8
+ return React.useContext(TourContext);
9
+ }
10
+ /**
11
+ * TourProvider — skeleton shell for Patch 1.
12
+ * driver.js steps are wired in Patch 2+; this provider is an identity wrapper.
13
+ */
14
+ export function TourProvider({ children }) {
15
+ const startTour = React.useCallback(() => {
16
+ // driver.js will be imported and configured here in Patch 2+.
17
+ }, []);
18
+ return (_jsx(TourContext.Provider, { value: { startTour }, children: children }));
19
+ }
@@ -232,12 +232,18 @@ export async function createCheckoutOrderAction(input) {
232
232
  const { userId: uid, userName, userEmail, addressId, paymentMethod, notes, excludedProductIds = [], adminBypass = false, adminBypassBy, } = input;
233
233
  const siteSettings = await siteSettingsRepository.getSingleton();
234
234
  const commissions = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
235
- // W1-43 — enforce COD toggle. Admin bypass + non-COD payments still flow through.
235
+ // W1-43 — enforce COD toggle.
236
236
  if (!adminBypass &&
237
237
  paymentMethod === "cod" &&
238
238
  siteSettings?.payment?.codEnabled === false) {
239
239
  throw new ValidationError("Cash on Delivery is not currently accepted. Please choose another payment method.");
240
240
  }
241
+ // P-1 — enforce UPI/cash manual payment toggle.
242
+ if (!adminBypass &&
243
+ (paymentMethod === "upi_manual" || paymentMethod === "cash") &&
244
+ siteSettings?.payment?.upiManualEnabled === false) {
245
+ throw new ValidationError("Manual UPI / cash payment is not currently accepted. Please choose another payment method.");
246
+ }
241
247
  const cart = await unitOfWork.carts.getOrCreate(uid);
242
248
  if (!cart.items || cart.items.length === 0) {
243
249
  throw new ValidationError(ERROR_MESSAGES.CHECKOUT.CART_EMPTY);
@@ -470,7 +476,7 @@ export async function createCheckoutOrderAction(input) {
470
476
  });
471
477
  const totalQuantity = group.reduce((sum, { item }) => sum + item.quantity, 0);
472
478
  const { shippingFee, storeOwnerId } = await resolveShippingCost(firstItem.storeId, groupTotal, commissions);
473
- const isCodLike = paymentMethod === "cod" || paymentMethod === "upi_manual";
479
+ const isCodLike = paymentMethod === "cod" || paymentMethod === "upi_manual" || paymentMethod === "cash";
474
480
  const depositAmount = isCodLike
475
481
  ? Math.round(groupTotal * (commissions.codDepositPercent / 100) * 100) / 100
476
482
  : undefined;
@@ -45,7 +45,7 @@ export const CONTEXT_LIMITS = {
45
45
  RICH_TEXT_IMAGE_MAX: 20,
46
46
  };
47
47
  // Contexts that accept both image and PDF (proof documents).
48
- const IMAGE_OR_PDF_CONTEXTS = ["shipping-proof", "refund-proof"];
48
+ const IMAGE_OR_PDF_CONTEXTS = ["shipping-proof", "refund-proof", "payment-proof"];
49
49
  const PDF_ONLY_CONTEXTS = ["invoice", "payout-doc"];
50
50
  const IMAGE_ONLY_CONTEXT_TYPES = new Set([
51
51
  "store-logo",
@@ -215,7 +215,7 @@ export function applyMediaContextGuards({ detectedMime, context: ctx, }) {
215
215
  return {
216
216
  ok: false,
217
217
  status: 400,
218
- error: "PDF uploads are only allowed for invoice, payout-doc, shipping-proof, or refund-proof contexts",
218
+ error: "PDF uploads are only allowed for invoice, payout-doc, shipping-proof, refund-proof, or payment-proof contexts",
219
219
  details: { context: ctx.type, detected: detectedMime },
220
220
  };
221
221
  }
@@ -3,3 +3,9 @@ export declare function createOrderAction(input: unknown): Promise<ActionResult<
3
3
  export declare function cancelOrderAction(input: unknown): Promise<ActionResult<unknown>>;
4
4
  export declare function requestReturnAction(input: unknown): Promise<ActionResult<unknown>>;
5
5
  export declare function updateOrderStatusAction(input: unknown): Promise<ActionResult<unknown>>;
6
+ export declare function attachPaymentProofAction(orderId: string, proof: {
7
+ proofUrl: string;
8
+ transactionId?: string;
9
+ mimeType?: string;
10
+ }): Promise<ActionResult<void>>;
11
+ export declare function adminVerifyPaymentAction(orderId: string): Promise<ActionResult<void>>;
@@ -6,7 +6,7 @@ import { createOrderSchema, updateOrderStatusSchema, cancelOrderSchema, } from "
6
6
  import { assertOrderCancellable, assertReturnWindowOpen } from "./service";
7
7
  import { ValidationError } from "../../../shared/errors/index";
8
8
  import { OrderNotFoundError, OrderOwnershipError } from "../../../shared/features/orders/errors";
9
- import { isAdminUser } from "../../../../features/auth/role-predicates";
9
+ import { isAdminUser, isModeratorUser } from "../../../../features/auth/role-predicates";
10
10
  export async function createOrderAction(input) {
11
11
  return wrapAction(async () => {
12
12
  const user = await requireRoleUser(["buyer", "seller", "admin"]);
@@ -58,3 +58,45 @@ export async function updateOrderStatusAction(input) {
58
58
  });
59
59
  });
60
60
  }
61
+ // ── P-1 Manual payment proof ─────────────────────────────────────────────────
62
+ export async function attachPaymentProofAction(orderId, proof) {
63
+ return wrapAction(async () => {
64
+ const user = await requireRoleUser(["buyer", "seller", "admin"]);
65
+ const order = await orderRepository.findById(orderId).catch(() => null);
66
+ if (!order)
67
+ throw new OrderNotFoundError(orderId);
68
+ if (!isAdminUser(user) && order.userId !== user.uid)
69
+ throw new OrderOwnershipError(orderId);
70
+ const pm = order.paymentMethod ?? "";
71
+ if (pm !== "cash" && pm !== "upi_manual") {
72
+ throw new ValidationError("Payment proof can only be attached to cash or UPI orders");
73
+ }
74
+ if (order.paymentProofUrl) {
75
+ throw new ValidationError("PROOF_ALREADY_ATTACHED");
76
+ }
77
+ await orderRepository.update(orderId, {
78
+ paymentProofUrl: proof.proofUrl,
79
+ paymentTransactionId: proof.transactionId,
80
+ paymentProofMimeType: proof.mimeType,
81
+ paymentProofUploadedAt: new Date(),
82
+ });
83
+ });
84
+ }
85
+ export async function adminVerifyPaymentAction(orderId) {
86
+ return wrapAction(async () => {
87
+ const user = await requireRoleUser(["admin", "moderator"]);
88
+ if (!isAdminUser(user) && !isModeratorUser(user)) {
89
+ throw new ValidationError("Only admin or moderator can verify payments");
90
+ }
91
+ const order = await orderRepository.findById(orderId).catch(() => null);
92
+ if (!order)
93
+ throw new OrderNotFoundError(orderId);
94
+ if (order.paymentStatus === "paid")
95
+ return; // idempotent
96
+ await orderRepository.update(orderId, {
97
+ paymentStatus: "paid",
98
+ paymentId: order.paymentTransactionId ?? order.paymentId ?? `manual-${orderId}`,
99
+ status: "processing",
100
+ });
101
+ });
102
+ }
@@ -8,6 +8,10 @@ const EVENTS_COLLECTION = "events";
8
8
  const EVENT_ENTRIES_COLLECTION = "eventEntries";
9
9
  const COUPONS_COLLECTION = "coupons";
10
10
  export async function runAssignSpinPrize(input, ctx) {
11
+ if (ctx.env("FEATURE_PRIZE_DRAWS") !== "true") {
12
+ ctx.logger.info("FEATURE_PRIZE_DRAWS disabled — skipping spin prize assignment");
13
+ return { eventId: input.eventId, userId: input.userId, reason: "feature_disabled" };
14
+ }
11
15
  const { eventId, userId } = input;
12
16
  const eventSnap = await ctx.db.collection(EVENTS_COLLECTION).doc(eventId).get();
13
17
  if (!eventSnap.exists) {
@@ -57,6 +57,10 @@ async function settleAuction(ctx, product) {
57
57
  });
58
58
  }
59
59
  export async function runAuctionSettlement(ctx) {
60
+ if (ctx.env("FEATURE_AUCTIONS") !== "true") {
61
+ ctx.logger.info("FEATURE_AUCTIONS disabled — skipping auction settlement");
62
+ return;
63
+ }
60
64
  ctx.logger.info("Starting auction settlement sweep");
61
65
  const expired = await productRepository.getExpiredAuctions(ctx.now);
62
66
  if (expired.length === 0) {
@@ -13,6 +13,10 @@ function getBusinessDayCutoff(daysAgo) {
13
13
  return cutoff;
14
14
  }
15
15
  export async function runAutoPayoutEligibility(ctx) {
16
+ if (ctx.env("FEATURE_PAYOUTS") !== "true") {
17
+ ctx.logger.info("FEATURE_PAYOUTS disabled — skipping auto-payout eligibility");
18
+ return;
19
+ }
16
20
  ctx.logger.info("Starting daily auto-payout eligibility sweep", {
17
21
  windowDays: AUTO_PAYOUT_WINDOW_DAYS,
18
22
  });
@@ -36,6 +36,10 @@ async function computeBundleStockStatus(productIds, ctx) {
36
36
  return "in_stock";
37
37
  }
38
38
  export async function runBundleStockSync(ctx) {
39
+ if (ctx.env("FEATURE_BUNDLES") !== "true") {
40
+ ctx.logger.info("FEATURE_BUNDLES disabled — skipping bundle stock sync");
41
+ return;
42
+ }
39
43
  ctx.logger.info("Bundle stock sync starting (SB-UNI-V categories)");
40
44
  const snap = await ctx.db
41
45
  .collection(CATEGORIES_COLLECTION)
@@ -87,6 +87,10 @@ async function dispatch(ctx, entry) {
87
87
  }
88
88
  }
89
89
  export async function runPayoutBatch(ctx) {
90
+ if (ctx.env("FEATURE_PAYOUTS") !== "true") {
91
+ ctx.logger.info("FEATURE_PAYOUTS disabled — skipping payout batch");
92
+ return;
93
+ }
90
94
  ctx.logger.info("Starting payout batch sweep");
91
95
  const pending = await payoutRepository.getPending();
92
96
  if (pending.length === 0) {
@@ -2,6 +2,10 @@ import { PRODUCT_FIELDS, COMMON_FIELDS } from "../../../../constants/field-names
2
2
  const PRODUCT_COLLECTION = "products";
3
3
  const PRIZE_DRAW_LISTING_TYPE = "prize-draw";
4
4
  export async function runPrizeRevealClose(ctx) {
5
+ if (ctx.env("FEATURE_PRIZE_DRAWS") !== "true") {
6
+ ctx.logger.info("FEATURE_PRIZE_DRAWS disabled — skipping prize reveal close");
7
+ return;
8
+ }
5
9
  ctx.logger.info("Prize reveal close sweep starting");
6
10
  const snap = await ctx.db
7
11
  .collection(PRODUCT_COLLECTION)
@@ -3,6 +3,10 @@ import { sendNotification } from "../../../../features/admin/actions/notificatio
3
3
  import { ORDER_FIELDS, PRODUCT_FIELDS, COMMON_FIELDS } from "../../../../constants/field-names";
4
4
  const ORDER_COLLECTION = "orders";
5
5
  export async function runPrizeRevealExpiry(ctx) {
6
+ if (ctx.env("FEATURE_PRIZE_DRAWS") !== "true") {
7
+ ctx.logger.info("FEATURE_PRIZE_DRAWS disabled — skipping prize reveal expiry");
8
+ return;
9
+ }
6
10
  ctx.logger.info("Prize reveal expiry sweep starting");
7
11
  const snap = await ctx.db
8
12
  .collection(ORDER_COLLECTION)
@@ -5,6 +5,10 @@ const PRODUCT_COLLECTION = "products";
5
5
  const ORDER_COLLECTION = "orders";
6
6
  const PRIZE_DRAW_LISTING_TYPE = "prize-draw";
7
7
  export async function runPrizeRevealOpen(ctx) {
8
+ if (ctx.env("FEATURE_PRIZE_DRAWS") !== "true") {
9
+ ctx.logger.info("FEATURE_PRIZE_DRAWS disabled — skipping prize reveal open");
10
+ return;
11
+ }
8
12
  ctx.logger.info("Prize reveal open sweep starting");
9
13
  const snap = await ctx.db
10
14
  .collection(PRODUCT_COLLECTION)
@@ -4,6 +4,10 @@ import { ORDER_FIELDS, PRODUCT_FIELDS } from "../../../../constants/field-names"
4
4
  const ORDER_COLLECTION = "orders";
5
5
  const ONE_DAY_MS = 86400000;
6
6
  export async function runPrizeRevealReminder(ctx) {
7
+ if (ctx.env("FEATURE_PRIZE_DRAWS") !== "true") {
8
+ ctx.logger.info("FEATURE_PRIZE_DRAWS disabled — skipping prize reveal reminder");
9
+ return;
10
+ }
7
11
  ctx.logger.info("Prize reveal reminder sweep starting");
8
12
  const cutoff = new Date(ctx.now.getTime() + ONE_DAY_MS);
9
13
  const snap = await ctx.db
@@ -6,6 +6,10 @@ import { ProductStatusValues } from "../../../../features/products/schemas/fires
6
6
  * (coupon.startDate IS NULL OR <= now) filter Firestore can't express.
7
7
  */
8
8
  export async function runPromotions(_input, ctx) {
9
+ if (ctx.env("FEATURE_EVENTS") !== "true") {
10
+ ctx.logger.info("FEATURE_EVENTS disabled — skipping promotions");
11
+ return { promotedProducts: [], featuredProducts: [], activeCoupons: [] };
12
+ }
9
13
  ctx.logger.info("Promotions data requested");
10
14
  const now = new Date();
11
15
  const nowIso = now.toISOString();
@@ -7,6 +7,10 @@ import { EVENT_ENTRY_FIELDS } from "../../../../constants/field-names";
7
7
  const EVENTS_COLLECTION = "events";
8
8
  const EVENT_ENTRIES_COLLECTION = "eventEntries";
9
9
  export async function runTriggerEventRaffle(input, ctx) {
10
+ if (ctx.env("FEATURE_PRIZE_DRAWS") !== "true") {
11
+ ctx.logger.info("FEATURE_PRIZE_DRAWS disabled — skipping event raffle");
12
+ return { eventId: input.eventId, raffleEntryCount: 0 };
13
+ }
10
14
  const eventRef = ctx.db.collection(EVENTS_COLLECTION).doc(input.eventId);
11
15
  const eventSnap = await eventRef.get();
12
16
  if (!eventSnap.exists) {
@@ -7,6 +7,10 @@ import { BATCH_LIMIT } from "../handlers/messages";
7
7
  import { getDefaultCurrency } from "../../../../core";
8
8
  const PLATFORM_COMMISSION_RATE = 0.05;
9
9
  export async function runWeeklyPayoutEligibility(ctx) {
10
+ if (ctx.env("FEATURE_PAYOUTS") !== "true") {
11
+ ctx.logger.info("FEATURE_PAYOUTS disabled — skipping weekly payout eligibility");
12
+ return;
13
+ }
10
14
  ctx.logger.info("Starting weekly payout eligibility sweep");
11
15
  const eligible = await orderRepository.getEligibleShiprocket();
12
16
  if (eligible.length === 0) {
@@ -835,6 +835,19 @@ export const ACTIONS = {
835
835
  kind: "secondary",
836
836
  permissions: ["admin"],
837
837
  },
838
+ "verify-payment": {
839
+ id: "admin.verify-payment",
840
+ label: "Verify Payment",
841
+ ariaLabel: "Mark payment as received and verified",
842
+ description: "Confirms manual cash/UPI payment was received. Moves order to Processing.",
843
+ kind: "primary",
844
+ permissions: ["admin", "moderator"],
845
+ confirmation: {
846
+ title: "Verify payment?",
847
+ body: "This marks the payment as received. The order will move to Processing. This action cannot be undone.",
848
+ confirmLabel: "Yes, verify",
849
+ },
850
+ },
838
851
  "verify-vendor": {
839
852
  id: "admin.verify-vendor",
840
853
  label: "Verify vendor",
@@ -4,5 +4,5 @@ export declare const CHECKOUT_DEFAULT_COMMISSIONS: {
4
4
  readonly platformShippingPercent: 10;
5
5
  readonly platformShippingFixedMin: 50;
6
6
  };
7
- export declare const CHECKOUT_PAYMENT_METHODS: readonly ["cod", "online", "upi_manual", "admin_bypass"];
7
+ export declare const CHECKOUT_PAYMENT_METHODS: readonly ["cash", "cod", "online", "upi_manual", "admin_bypass"];
8
8
  export type CheckoutPaymentMethod = (typeof CHECKOUT_PAYMENT_METHODS)[number];
@@ -4,4 +4,4 @@ export const CHECKOUT_DEFAULT_COMMISSIONS = {
4
4
  platformShippingPercent: 10,
5
5
  platformShippingFixedMin: 50,
6
6
  };
7
- export const CHECKOUT_PAYMENT_METHODS = ["cod", "online", "upi_manual", "admin_bypass"];
7
+ export const CHECKOUT_PAYMENT_METHODS = ["cash", "cod", "online", "upi_manual", "admin_bypass"];
@@ -4,5 +4,9 @@ export interface AdminOrderEditorViewProps {
4
4
  orderId?: string;
5
5
  orderLabel?: string;
6
6
  currentStatus?: string;
7
+ paymentProofUrl?: string;
8
+ paymentTransactionId?: string;
9
+ paymentMethod?: string;
10
+ paymentStatus?: string;
7
11
  }
8
- export declare function AdminOrderEditorView({ open, onClose, orderId, orderLabel, currentStatus, }: AdminOrderEditorViewProps): import("react/jsx-runtime").JSX.Element;
12
+ export declare function AdminOrderEditorView({ open, onClose, orderId, orderLabel, currentStatus, paymentProofUrl, paymentTransactionId, paymentMethod, paymentStatus, }: AdminOrderEditorViewProps): import("react/jsx-runtime").JSX.Element;
@@ -1,11 +1,14 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useApiMutation } from "@mohasinac/appkit/client";
4
- import React from "react";
4
+ import React, { useState } from "react";
5
+ import { normalizeError } from "../../../errors/normalize";
5
6
  import { useQueryClient } from "@tanstack/react-query";
6
- import { Button, Form, FormActions, Input, Label, Select, SideDrawer, Stack, useToast } from "../../../ui";
7
+ import { Button, Div, Form, FormActions, Input, Label, Select, SideDrawer, Stack, Text, useToast } from "../../../ui";
8
+ import { MediaImage } from "../../media";
7
9
  import { apiClient } from "../../../http";
8
10
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
11
+ import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
9
12
  const STATUS_OPTIONS = [
10
13
  { label: "Pending", value: "pending" },
11
14
  { label: "Processing", value: "processing" },
@@ -25,7 +28,7 @@ const CARRIER_OPTIONS = [
25
28
  { label: "Other", value: "Other" },
26
29
  ];
27
30
  // --- Component ---------------------------------------------------------------
28
- export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, currentStatus, }) {
31
+ export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, currentStatus, paymentProofUrl, paymentTransactionId, paymentMethod, paymentStatus, }) {
29
32
  const queryClient = useQueryClient();
30
33
  const { showToast } = useToast();
31
34
  const [status, setStatus] = React.useState(currentStatus ?? "pending");
@@ -33,6 +36,9 @@ export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, curre
33
36
  const [carrier, setCarrier] = React.useState("");
34
37
  const [notes, setNotes] = React.useState("");
35
38
  const [refundAmount, setRefundAmount] = React.useState("");
39
+ const [isVerifyingPayment, setIsVerifyingPayment] = useState(false);
40
+ const isCashOrUpi = paymentMethod === "cash" || paymentMethod === "upi_manual";
41
+ const needsVerification = isCashOrUpi && paymentStatus === "pending";
36
42
  React.useEffect(() => {
37
43
  if (open) {
38
44
  setStatus(currentStatus ?? "pending");
@@ -68,8 +74,28 @@ export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, curre
68
74
  showToast(err?.message ?? "Failed to update order.", "error");
69
75
  },
70
76
  });
77
+ const handleVerifyPayment = async () => {
78
+ if (!orderId)
79
+ return;
80
+ setIsVerifyingPayment(true);
81
+ try {
82
+ await apiClient.patch(`/api/admin/orders/${orderId}/payment-verify`, {});
83
+ showToast("Payment verified. Order moved to Processing.", "success");
84
+ queryClient.invalidateQueries({ queryKey: ["admin", "orders"] });
85
+ onClose();
86
+ }
87
+ catch (err) {
88
+ void normalizeError(err);
89
+ showToast(err?.message ?? "Failed to verify payment.", "error");
90
+ }
91
+ finally {
92
+ setIsVerifyingPayment(false);
93
+ }
94
+ };
71
95
  return (_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: orderLabel ? `Order: ${orderLabel}` : "Update Order", children: _jsxs(Form, { onSubmit: (e) => {
72
96
  e.preventDefault();
73
97
  saveMutation.mutate();
74
- }, spacing: "md", padding: "md", children: [_jsx(Select, { label: "Order status", options: STATUS_OPTIONS, value: status, onValueChange: setStatus }), _jsx(Input, { label: "Tracking number (optional)", value: trackingNumber, onChange: (e) => setTrackingNumber(e.target.value), placeholder: "e.g. DEL1234567890IN" }), _jsx(Select, { label: "Carrier (optional)", options: CARRIER_OPTIONS, value: carrier, onValueChange: setCarrier }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Internal note (optional)" }), _jsx("textarea", { value: notes, onChange: (e) => setNotes(e.target.value), rows: 3, placeholder: "Reason for status change, escalation notes\u2026", className: "w-full rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary-500" })] }), (status === "refunded" || status === "return_requested") && (_jsx(Input, { label: "Refund amount \u20B9 (optional)", type: "number", min: "0", step: "0.01", value: refundAmount, onChange: (e) => setRefundAmount(e.target.value), placeholder: "e.g. 499.00" })), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "secondary", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", isLoading: saveMutation.isPending, disabled: !orderId || saveMutation.isPending, children: "Save changes" })] })] }) }));
98
+ }, spacing: "md", padding: "md", children: [_jsx(Select, { label: "Order status", options: STATUS_OPTIONS, value: status, onValueChange: setStatus }), _jsx(Input, { label: "Tracking number (optional)", value: trackingNumber, onChange: (e) => setTrackingNumber(e.target.value), placeholder: "e.g. DEL1234567890IN" }), _jsx(Select, { label: "Carrier (optional)", options: CARRIER_OPTIONS, value: carrier, onValueChange: setCarrier }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Internal note (optional)" }), _jsx("textarea", { value: notes, onChange: (e) => setNotes(e.target.value), rows: 3, placeholder: "Reason for status change, escalation notes\u2026", className: "w-full rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary-500" })] }), (status === "refunded" || status === "return_requested") && (_jsx(Input, { label: "Refund amount \u20B9 (optional)", type: "number", min: "0", step: "0.01", value: refundAmount, onChange: (e) => setRefundAmount(e.target.value), placeholder: "e.g. 499.00" })), isCashOrUpi && (_jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Payment Proof" }), paymentProofUrl ? (_jsxs(Stack, { gap: "xs", children: [_jsx(MediaImage, { src: paymentProofUrl, alt: "Payment screenshot", size: "card", className: "rounded-lg border border-zinc-200 dark:border-zinc-700" }), paymentTransactionId && (_jsxs(Text, { size: "xs", color: "muted", children: ["UTR: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: paymentTransactionId })] })), needsVerification && (_jsx(Button, { type: "button", action: ACTIONS.ADMIN["verify-payment"], onClick: handleVerifyPayment, isLoading: isVerifyingPayment, disabled: isVerifyingPayment, variant: "primary", className: "mt-1 w-full" })), !needsVerification && paymentStatus === "paid" && (
99
+ // audit-variant-ok: themed success border color not in BORDER_MAP
100
+ _jsx(Div, { rounded: "lg", padding: "inlineSm", className: "border border-success/20", surface: "success-surface", children: _jsx(Text, { size: "xs", className: "text-success", weight: "medium", children: "Payment verified" }) }))] })) : (_jsx(Text, { size: "xs", color: "faint", children: "No proof uploaded yet." }))] })), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "secondary", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", isLoading: saveMutation.isPending, disabled: !orderId || saveMutation.isPending, children: "Save changes" })] })] }) }));
75
101
  }
@@ -51,6 +51,10 @@ export function AdminOrdersView({ children, ...props }) {
51
51
  ].join(" · "),
52
52
  status: toStringValue(item.status, "Unknown"),
53
53
  updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
54
+ paymentProofUrl: toStringValue(item.paymentProofUrl, "") || undefined,
55
+ paymentTransactionId: toStringValue(item.paymentTransactionId, "") || undefined,
56
+ paymentMethod: toStringValue(item.paymentMethod, "") || undefined,
57
+ paymentStatus: toStringValue(item.paymentStatus, "") || undefined,
54
58
  })),
55
59
  getTotal: (response, mappedRows) => typeof response.meta?.total === "number" ? response.meta.total : mappedRows.length,
56
60
  buildFilters: (f) => (f.status && f.status !== "All" ? sieveFilter("status", SIEVE_OP.EQ, f.status) : undefined),
@@ -117,5 +121,5 @@ export function AdminOrdersView({ children, ...props }) {
117
121
  ] })),
118
122
  renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsx(FilterChipGroup, { label: "Status", tabs: ADMIN_ORDER_STATUS_TABS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) })),
119
123
  };
120
- return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(AdminOrderEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), orderId: selectedRow?.id, orderLabel: selectedRow?.primary, currentStatus: selectedRow?.status })] }));
124
+ return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(AdminOrderEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), orderId: selectedRow?.id, orderLabel: selectedRow?.primary, currentStatus: selectedRow?.status, paymentProofUrl: selectedRow?.paymentProofUrl, paymentTransactionId: selectedRow?.paymentTransactionId, paymentMethod: selectedRow?.paymentMethod, paymentStatus: selectedRow?.paymentStatus })] }));
121
125
  }
@@ -88,6 +88,8 @@ export interface AppLayoutShellProps {
88
88
  logout?: string;
89
89
  };
90
90
  eventBannerSlot?: React.ReactNode;
91
+ /** When provided, renders a tour-start icon button in the title bar. Null in Patch 1. */
92
+ onTourStart?: () => void;
91
93
  /**
92
94
  * Override className for the main content wrapper div.
93
95
  * When set, replaces the default `w-full px-4 py-6 …`
@@ -113,4 +115,4 @@ export interface AppLayoutShellProps {
113
115
  };
114
116
  };
115
117
  }
116
- export declare function AppLayoutShell({ children, navItems, sidebarItems, sidebarSections, sidebarPrimaryActions, sidebarTitle, hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle, suppressDashboardNav, hideSidebarToggle, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar, sidebarProfileLabels, eventBannerSlot, contentClassName, lightBackground, darkBackground, }: AppLayoutShellProps): import("react/jsx-runtime").JSX.Element;
118
+ export declare function AppLayoutShell({ children, navItems, sidebarItems, sidebarSections, sidebarPrimaryActions, sidebarTitle, hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle, suppressDashboardNav, hideSidebarToggle, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar, sidebarProfileLabels, eventBannerSlot, onTourStart, contentClassName, lightBackground, darkBackground, }: AppLayoutShellProps): import("react/jsx-runtime").JSX.Element;
@@ -104,7 +104,7 @@ function SidebarContent({ sidebarItems, sidebarSections, sidebarPrimaryActions,
104
104
  ].join(" "), children: action.label }, `${action.href}-${action.label}`))) })), isAuthenticated && (_jsx(CollapsibleNavGroup, { title: labels.sectionTitle, children: _jsxs(Ul, { spacing: "2xs", children: [_jsx(Li, { children: _jsx(TextLink, { href: profileHref, variant: "none", className: navItemClass, children: labels.profile }) }), userOrdersHref && (_jsx(Li, { children: _jsx(TextLink, { href: userOrdersHref, variant: "none", className: navItemClass, children: labels.orders }) })), userWishlistHref && (_jsx(Li, { children: _jsx(TextLink, { href: userWishlistHref, variant: "none", className: navItemClass, children: labels.wishlist }) })), userSettingsHref && (_jsx(Li, { children: _jsx(TextLink, { href: userSettingsHref, variant: "none", className: navItemClass, children: labels.settings }) }))] }) })), isAuthenticated && user?.stats && (_jsxs(Div, { layout: "grid", gap: "2", className: "grid-cols-2", children: [user.stats.totalOrders != null && (_jsxs(Div, { className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.totalOrders }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Orders" })] })), user.stats.reviewsCount != null && (_jsxs(Div, { className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.reviewsCount }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Reviews" })] })), user.stats.auctionsWon != null && (_jsxs(Div, { className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.auctionsWon }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Auctions Won" })] })), user.stats.itemsSold != null && (_jsxs(Div, { className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.itemsSold }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Items Sold" })] }))] })), isAuthenticated && isAdminOrSeller && (adminHref || resolvedStoreHref) && (_jsx(CollapsibleNavGroup, { title: labels.dashboardSectionTitle, children: _jsxs(Ul, { spacing: "2xs", children: [adminHref && role === "admin" && (_jsx(Li, { children: _jsx(TextLink, { href: adminHref, variant: "none", className: navItemClass, children: labels.adminDashboard }) })), resolvedStoreHref && isAdminOrSeller && (_jsx(Li, { children: _jsx(TextLink, { href: resolvedStoreHref, variant: "none", className: navItemClass, children: labels.storeDashboard }) }))] }) })), normalizedSections.map((section, sectionIndex) => (_jsx(CollapsibleSidebarSection, { section: section, navItemClass: navItemClass }, `sidebar-section-${sectionIndex}`))), (sidebarLocaleSlot || showThemeToggleInSidebar || (isAuthenticated && onLogout)) && (_jsxs(Stack, { border: "default", className: "border-t dark:border-slate-800", padding: "t-md", gap: "3", children: [sidebarLocaleSlot, showThemeToggleInSidebar && (_jsxs("button", { type: "button", onClick: toggleTheme, className: "flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm text-zinc-700 transition-colors hover:bg-primary-50 hover:text-primary-800 dark:text-zinc-300 dark:hover:bg-slate-800 dark:hover:text-secondary-300", children: [_jsx(Span, { "aria-hidden": "true", children: theme === "dark" ? "☀️" : "🌙" }), theme === "dark" ? "Light mode" : "Dark mode"] })), isAuthenticated && onLogout && (_jsx("button", { type: "button", onClick: () => { onLogout(); onAfterLogout(); }, className: CLS_LOGOUT_BTN, children: labels.logout }))] }))] }));
105
105
  }
106
106
  // ─── Main component ────────────────────────────────────────────────────────────
107
- export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarSections, sidebarPrimaryActions, sidebarTitle = "Navigation", hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle = false, suppressDashboardNav = false, hideSidebarToggle = false, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar = false, sidebarProfileLabels, eventBannerSlot, contentClassName, lightBackground = DEFAULT_LIGHT_BG, darkBackground = DEFAULT_DARK_BG, }) {
107
+ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarSections, sidebarPrimaryActions, sidebarTitle = "Navigation", hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle = false, suppressDashboardNav = false, hideSidebarToggle = false, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar = false, sidebarProfileLabels, eventBannerSlot, onTourStart, contentClassName, lightBackground = DEFAULT_LIGHT_BG, darkBackground = DEFAULT_DARK_BG, }) {
108
108
  // QueryClientProvider used to be created here so AppLayoutShell could stand
109
109
  // alone. The real cause of the 2026-06-10/11 "No QueryClient set" prod crash
110
110
  // turned out to be a peer-dep duplicate of @tanstack/query-core under
@@ -164,5 +164,5 @@ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarS
164
164
  opacity: darkBackground.overlay?.opacity ?? 0,
165
165
  },
166
166
  };
167
- return (_jsx(_Fragment, { children: _jsxs(Stack, { className: "min-h-screen w-full overflow-x-clip transition-colors duration-300", children: [_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: normalizedLightBackground, darkMode: normalizedDarkBackground }), _jsxs(Div, { ref: headerRef, className: "sticky top-0 z-50 w-full", children: [_jsx(TitleBar, { onToggleSidebar: handleTogglePublicSidebar, sidebarOpen: sidebarOpen, onSearchToggle: () => setSearchOpen((prev) => !prev), searchOpen: searchOpen, brandName: brandName, brandShortName: brandShortName, siteLogoUrl: siteLogoUrl, logoHref: logoHref, promotionsHref: promotionsHref, cartHref: cartHref, wishlistHref: wishlistHref, userId: userId, profileHref: profileHref, loginHref: loginHref, registerHref: registerHref, user: user, navSlot: titleBarNavSlot, notificationSlot: titleBarNotificationSlot, devSlot: titleBarDevSlot, promoStripText: titleBarPromoStripText, isDark: theme === "dark", onToggleTheme: showThemeToggle ? toggleTheme : undefined, onBeforeToggleDashboardNav: handleBeforeDashboardNavToggle, suppressDashboardNav: suppressDashboardNav, hideSidebarToggle: hideSidebarToggle }), _jsx(NavbarWithSettings, { navItems: navItems, hiddenNavItems: hiddenNavItems, permissions: authUser?.permissions }), searchOpen && (searchSlotRenderer ? searchSlotRenderer(() => setSearchOpen(false)) : searchSlot)] }), eventBannerSlot, _jsx(AutoBreadcrumbs, {}), _jsxs(Div, { layout: "flex", className: "relative w-full flex-1 overflow-x-clip", children: [_jsx(SidebarLayout, { isOpen: sidebarOpen, ariaLabel: "Secondary navigation", header: user ? (_jsx(SidebarUserHeader, { user: user, onClose: () => setSidebarOpen(false) })) : (_jsx(SidebarGuestHeader, { sidebarTitle: sidebarTitle, onClose: () => setSidebarOpen(false) })), onClose: () => setSidebarOpen(false), children: sidebarContent }), _jsx(Main, { id: "main-content", className: `w-full flex-1 flex flex-col ${hasBottomActions ? "mb-28" : "mb-16"} md:mb-0`, children: _jsx(Div, { padding: "y-lg", className: `flex-1 ${contentClassName ?? "w-full px-5 md:px-6 lg:px-8"}`, children: children }) })] }), _jsx(BackToTop, {}), _jsx(FooterLayout, { ...footer }), _jsx(BottomActions, {}), _jsx(BottomNavbar, { user: user, homeHref: homeHref, shopHref: shopHref, cartHref: cartHref, profileHref: profileHref, loginHref: loginHref, onSearchToggle: () => setSearchOpen((prev) => !prev), navItems: navItems, onMoreToggle: hasDashboardNav ? toggleDashboardNav : handleTogglePublicSidebar }), _jsx(UnsavedChangesModal, {})] }) }));
167
+ return (_jsx(_Fragment, { children: _jsxs(Stack, { className: "min-h-screen w-full overflow-x-clip transition-colors duration-300", children: [_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: normalizedLightBackground, darkMode: normalizedDarkBackground }), _jsxs(Div, { ref: headerRef, className: "sticky top-0 z-50 w-full", children: [_jsx(TitleBar, { onToggleSidebar: handleTogglePublicSidebar, sidebarOpen: sidebarOpen, onSearchToggle: () => setSearchOpen((prev) => !prev), searchOpen: searchOpen, brandName: brandName, brandShortName: brandShortName, siteLogoUrl: siteLogoUrl, logoHref: logoHref, promotionsHref: promotionsHref, cartHref: cartHref, wishlistHref: wishlistHref, userId: userId, profileHref: profileHref, loginHref: loginHref, registerHref: registerHref, user: user, navSlot: titleBarNavSlot, notificationSlot: titleBarNotificationSlot, devSlot: titleBarDevSlot, promoStripText: titleBarPromoStripText, isDark: theme === "dark", onToggleTheme: showThemeToggle ? toggleTheme : undefined, onTourStart: onTourStart, onBeforeToggleDashboardNav: handleBeforeDashboardNavToggle, suppressDashboardNav: suppressDashboardNav, hideSidebarToggle: hideSidebarToggle }), _jsx(NavbarWithSettings, { navItems: navItems, hiddenNavItems: hiddenNavItems, permissions: authUser?.permissions }), searchOpen && (searchSlotRenderer ? searchSlotRenderer(() => setSearchOpen(false)) : searchSlot)] }), eventBannerSlot, _jsx(AutoBreadcrumbs, {}), _jsxs(Div, { layout: "flex", className: "relative w-full flex-1 overflow-x-clip", children: [_jsx(SidebarLayout, { isOpen: sidebarOpen, ariaLabel: "Secondary navigation", header: user ? (_jsx(SidebarUserHeader, { user: user, onClose: () => setSidebarOpen(false) })) : (_jsx(SidebarGuestHeader, { sidebarTitle: sidebarTitle, onClose: () => setSidebarOpen(false) })), onClose: () => setSidebarOpen(false), children: sidebarContent }), _jsx(Main, { id: "main-content", className: `w-full flex-1 flex flex-col ${hasBottomActions ? "mb-28" : "mb-16"} md:mb-0`, children: _jsx(Div, { padding: "y-lg", className: `flex-1 ${contentClassName ?? "w-full px-5 md:px-6 lg:px-8"}`, children: children }) })] }), _jsx(BackToTop, {}), _jsx(FooterLayout, { ...footer }), _jsx(BottomActions, {}), _jsx(BottomNavbar, { user: user, homeHref: homeHref, shopHref: shopHref, cartHref: cartHref, profileHref: profileHref, loginHref: loginHref, onSearchToggle: () => setSearchOpen((prev) => !prev), navItems: navItems, onMoreToggle: hasDashboardNav ? toggleDashboardNav : handleTogglePublicSidebar }), _jsx(UnsavedChangesModal, {})] }) }));
168
168
  }
@@ -52,6 +52,8 @@ export interface TitleBarLayoutProps {
52
52
  onToggleDashboardNav?: () => void;
53
53
  /** Hide the public sidebar toggle button when nested layouts own navigation. */
54
54
  hideSidebarToggle?: boolean;
55
+ /** When provided, renders a tour-start icon button before the theme toggle. Null in Patch 1. */
56
+ onTourStart?: () => void;
55
57
  id?: string;
56
58
  className?: string;
57
59
  }
@@ -64,4 +66,4 @@ export interface TitleBarLayoutProps {
64
66
  *
65
67
  * Receives all domain data as props — zero domain imports.
66
68
  */
67
- export declare function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen: _searchOpen, brandName, brandShortName: _brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount, cartHref, cartCount, profileHref, unreadNotificationCount, notificationsHref, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark, onToggleTheme, hasDashboardNav, onToggleDashboardNav, hideSidebarToggle, id, className, }: TitleBarLayoutProps): import("react/jsx-runtime").JSX.Element;
69
+ export declare function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen: _searchOpen, brandName, brandShortName: _brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount, cartHref, cartCount, profileHref, unreadNotificationCount, notificationsHref, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark, onToggleTheme, onTourStart, hasDashboardNav, onToggleDashboardNav, hideSidebarToggle, id, className, }: TitleBarLayoutProps): import("react/jsx-runtime").JSX.Element;
@@ -14,10 +14,11 @@ const countBadge = "absolute -top-0.5 -right-0.5 flex items-center justify-cente
14
14
  *
15
15
  * Receives all domain data as props — zero domain imports.
16
16
  */
17
- export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen: _searchOpen, brandName, brandShortName: _brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount = 0, cartHref, cartCount = 0, profileHref, unreadNotificationCount = 0, notificationsHref, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark = false, onToggleTheme, hasDashboardNav, onToggleDashboardNav, hideSidebarToggle = false, id = "titlebar", className = "", }) {
17
+ export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen: _searchOpen, brandName, brandShortName: _brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount = 0, cartHref, cartCount = 0, profileHref, unreadNotificationCount = 0, notificationsHref, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark = false, onToggleTheme, onTourStart, hasDashboardNav, onToggleDashboardNav, hideSidebarToggle = false, id = "titlebar", className = "", }) {
18
18
  // ── Element builders ────────────────────────────────────────────────────────
19
19
  const promotionsEl = promotionsHref ? (_jsxs(Link, { href: promotionsHref, "aria-label": "Today's deals", className: "flex items-center gap-1 px-3 py-1 rounded-full text-xs font-bold bg-primary-100 text-primary-700 dark:bg-secondary-900/40 dark:text-secondary-400 hover:bg-primary-200 dark:hover:bg-secondary-900/60 transition-colors border border-primary-200/60 dark:border-secondary-700/40", children: [_jsx("svg", { className: "w-3 h-3", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { d: "M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.37.86.58 1.41.58.55 0 1.05-.21 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z" }) }), _jsx(Span, { className: "hidden sm:inline", children: "Today's Deals" })] })) : null;
20
20
  const themeBtn = onToggleTheme ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": isDark ? "Switch to light mode" : "Switch to dark mode", onClick: onToggleTheme, className: iconBtn, children: isDark ? (_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 3v1m0 16v1m8.66-9h-1M4.34 12h-1m15.07-6.07-.71.71M6.34 17.66l-.71.71m12.73 0-.71-.71M6.34 6.34l-.71-.71M12 5a7 7 0 1 0 0 14A7 7 0 0 0 12 5z" }) })) : (_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 12.79A9 9 0 1 1 11.21 3a7 7 0 1 0 9.79 9.79z" }) })) })) : null;
21
+ const tourBtn = onTourStart ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": "Start product tour", onClick: onTourStart, className: iconBtn, children: _jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" }) }) })) : null;
21
22
  const hamburgerBtn = !hideSidebarToggle ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": sidebarOpen ? "Close menu" : hasDashboardNav ? "Open dashboard navigation" : "Open menu", "aria-expanded": sidebarOpen, "aria-controls": "secondary-sidebar", onClick: hasDashboardNav && onToggleDashboardNav ? onToggleDashboardNav : onToggleSidebar, className: iconBtn, children: _jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: sidebarOpen ? (_jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" })) : (_jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 6h16M4 12h16M4 18h16" })) }) })) : null;
22
23
  // Compare is always lg+ (desktop-only feature, less critical on mobile)
23
24
  const compareEl = compareHref ? (_jsx(Link, { href: compareHref, "aria-label": "Compare items", className: `${iconBtn} hidden lg:flex`, children: _jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" }) }) })) : null;
@@ -34,5 +35,5 @@ export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, s
34
35
  // ── Render ───────────────────────────────────────────────────────────────────
35
36
  return (_jsxs(BlockHeader, { id: id, className: `sticky top-0 z-50 bg-white/95 dark:bg-slate-950/95 backdrop-blur-md border-b border-zinc-100 dark:border-slate-800 shadow-sm ${className}`, children: [promoStripText && (_jsx(Section, { color: "inverse", tone: "accent-banner", className: "text-xs text-center font-medium", padding: "y-2xs", children: promoStripText })), _jsxs(Div, { paddingX: "x-page", className: "container mx-auto max-w-[1920px]", children: [_jsxs(Row, { justify: "between", gap: "none", className: "relative h-14", children: [_jsx(Row, { gap: "3", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteLogo, { title: brandName, size: "md" }) }) }), siteLogoUrl ? (_jsx(Row, { className: "absolute inset-y-0 left-1/2 -translate-x-1/2", align: "center", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteLogo, { src: siteLogoUrl, title: brandName, size: "md" }) }) })) : (navSlot && _jsx(Div, { className: "hidden md:flex", children: navSlot })), _jsxs(Row, { gap: "xs", children: [devSlot, compareEl, notificationSlot, notificationsEl && _jsx(Div, { className: "hidden lg:flex", children: notificationsEl }), wishlistEl && _jsx(Div, { className: "hidden lg:flex", children: wishlistEl }), cartEl && _jsx(Div, { className: "hidden lg:flex", children: cartEl }), user
36
37
  ? profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })
37
- : authButtonsEl ?? (profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })), searchBtn, promotionsEl, themeBtn, hamburgerBtn] })] }), hasTb2 && (_jsxs(Row, { border: "subtle", as: "nav", "aria-label": "Account actions", justify: "end", gap: "xs", className: "flex lg:hidden h-10 border-t px-1", children: [notificationsEl, wishlistEl, cartEl, profileEl] }))] })] }));
38
+ : authButtonsEl ?? (profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })), searchBtn, promotionsEl, tourBtn, themeBtn, hamburgerBtn] })] }), hasTb2 && (_jsxs(Row, { border: "subtle", as: "nav", "aria-label": "Account actions", justify: "end", gap: "xs", className: "flex lg:hidden h-10 border-t px-1", children: [notificationsEl, wishlistEl, cartEl, profileEl] }))] })] }));
38
39
  }
@@ -37,6 +37,7 @@ export declare const PaymentStatusValues: {
37
37
  /** Runtime-accessible payment method values â€" use instead of bare string literals. */
38
38
  export declare const PaymentMethodValues: {
39
39
  readonly COD: "cod";
40
+ readonly CASH: "cash";
40
41
  readonly ONLINE: "online";
41
42
  readonly UPI_MANUAL: "upi_manual";
42
43
  readonly RAZORPAY: "razorpay";
@@ -209,6 +210,14 @@ export interface OrderDocument extends BaseDocument {
209
210
  pickedAt?: Date;
210
211
  /** Timestamp when the order was packed and ready for courier handoff. */
211
212
  packedAt?: Date;
213
+ /** Media slug for buyer-uploaded UPI/bank screenshot (via /api/media proxy). */
214
+ paymentProofUrl?: string;
215
+ /** UTR / transaction reference entered by the buyer. */
216
+ paymentTransactionId?: string;
217
+ /** MIME type detected at finalize step (image/jpeg, image/png, application/pdf). */
218
+ paymentProofMimeType?: string;
219
+ /** When the buyer submitted the proof. */
220
+ paymentProofUploadedAt?: Date;
212
221
  }
213
222
  export declare const ORDER_COLLECTION: "orders";
214
223
  export declare const ORDER_INDEXED_FIELDS: readonly ["userId", "productId", "storeId", "status", "paymentStatus", "payoutStatus", "shippingMethod", "orderDate", "createdAt", "assignedWorkerId", "pickedAt", "packedAt"];
@@ -31,6 +31,7 @@ export const PaymentStatusValues = {
31
31
  /** Runtime-accessible payment method values â€" use instead of bare string literals. */
32
32
  export const PaymentMethodValues = {
33
33
  COD: "cod",
34
+ CASH: "cash",
34
35
  ONLINE: "online",
35
36
  UPI_MANUAL: "upi_manual",
36
37
  RAZORPAY: "razorpay",
@@ -11,6 +11,7 @@ import { ImageUpload, MediaUploadField, MediaUploadList, } from "../../media";
11
11
  import { useMediaUpload } from "../../media";
12
12
  import { resolveDate } from "../../../utils/date.formatter";
13
13
  import { normalizeRichTextHtml } from "../../../utils/string.formatter";
14
+ import { useListingTypeFlags } from "../../../react/hooks/useListingTypeFlags";
14
15
  import { isAuctionListing, isPreOrderListing, isPrizeDrawListing, } from "../utils/listing-type";
15
16
  import { PrizeDrawItemsEditor } from "./PrizeDrawItemsEditor";
16
17
  export const PRODUCT_STATUS_OPTIONS = [
@@ -30,6 +31,7 @@ export function ProductForm({ product, onChange, isReadonly = false, renderDescr
30
31
  const t = useTranslations("adminProducts");
31
32
  const { upload } = useMediaUpload();
32
33
  const galleryIndexRef = useRef(0);
34
+ const listingTypeFlags = useListingTypeFlags();
33
35
  const update = (partial) => {
34
36
  onChange({ ...product, ...partial });
35
37
  };
@@ -133,7 +135,7 @@ export function ProductForm({ product, onChange, isReadonly = false, renderDescr
133
135
  ] })] }), _jsx(Checkbox, { label: t("formInsurance"), checked: !!product.insurance, onChange: (e) => update({
134
136
  insurance: e.target.checked,
135
137
  insuranceCost: e.target.checked ? product.insuranceCost || 0 : undefined,
136
- }), disabled: isReadonly }), product.insurance && (_jsxs(_Fragment, { children: [_jsx(Alert, { variant: "info", title: t("formInsuranceHelp"), children: t("formInsuranceHelp") }), _jsx(FormField, { name: "insuranceCost", label: t("formInsuranceCost"), type: "number", value: String(product.insuranceCost ?? ""), onChange: (value) => update({ insuranceCost: Number(value) }), disabled: isReadonly, placeholder: `${currencyPrefix}0` })] })), _jsx(Heading, { level: 4, className: "mt-4", children: t("sectionAuctionSettings") }), _jsx(Checkbox, { label: t("formIsAuction"), checked: isAuctionListing(product), onChange: (e) => update({ listingType: e.target.checked ? "auction" : "standard" }), disabled: isReadonly }), isAuctionListing(product) && (_jsxs(_Fragment, { children: [_jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "startingBid", label: t("formStartingBid"), type: "number", value: String(product.startingBid ?? ""), onChange: (value) => update({ startingBid: Number(value) }), disabled: isReadonly, placeholder: "0" }), _jsx(FormField, { name: "auctionEndDate", label: t("formAuctionEndDate"), type: "datetime-local", value: (() => {
138
+ }), disabled: isReadonly }), product.insurance && (_jsxs(_Fragment, { children: [_jsx(Alert, { variant: "info", title: t("formInsuranceHelp"), children: t("formInsuranceHelp") }), _jsx(FormField, { name: "insuranceCost", label: t("formInsuranceCost"), type: "number", value: String(product.insuranceCost ?? ""), onChange: (value) => update({ insuranceCost: Number(value) }), disabled: isReadonly, placeholder: `${currencyPrefix}0` })] })), listingTypeFlags.auction && (_jsxs(_Fragment, { children: [_jsx(Heading, { level: 4, className: "mt-4", children: t("sectionAuctionSettings") }), _jsx(Checkbox, { label: t("formIsAuction"), checked: isAuctionListing(product), onChange: (e) => update({ listingType: e.target.checked ? "auction" : "standard" }), disabled: isReadonly })] })), isAuctionListing(product) && (_jsxs(_Fragment, { children: [_jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "startingBid", label: t("formStartingBid"), type: "number", value: String(product.startingBid ?? ""), onChange: (value) => update({ startingBid: Number(value) }), disabled: isReadonly, placeholder: "0" }), _jsx(FormField, { name: "auctionEndDate", label: t("formAuctionEndDate"), type: "datetime-local", value: (() => {
137
139
  const d = resolveDate(product.auctionEndDate);
138
140
  if (!d)
139
141
  return "";
@@ -143,8 +145,8 @@ export function ProductForm({ product, onChange, isReadonly = false, renderDescr
143
145
  }), disabled: isReadonly, options: [
144
146
  { value: "winner", label: t("formAuctionShippingPaidByWinner") },
145
147
  { value: "seller", label: t("formAuctionShippingPaidBySeller") },
146
- ] }), _jsx(Text, { variant: "secondary", weight: "semibold", className: "mt-2", children: t("sectionAuctionAdvanced") }), _jsx(Checkbox, { label: t("formAutoExtendable"), checked: !!product.autoExtendable, onChange: (e) => update({ autoExtendable: e.target.checked }), disabled: isReadonly }), product.autoExtendable && (_jsxs(_Fragment, { children: [_jsx(Alert, { variant: "info", title: t("formAutoExtendableHelp"), children: t("formAutoExtendableHelp") }), _jsx(FormField, { name: "auctionExtensionMinutes", label: t("formAuctionExtensionMinutes"), type: "number", value: String(product.auctionExtensionMinutes ?? 5), onChange: (value) => update({ auctionExtensionMinutes: Number(value) || 5 }), disabled: isReadonly, placeholder: "5", helpText: t("formAuctionExtensionMinutesHelp") })] }))] })), _jsx(Heading, { level: 4, className: "mt-4", children: t("sectionPreOrderSettings") }), _jsx(Checkbox, { label: t("formIsPreOrder"), checked: isPreOrderListing(product), onChange: (e) => update({ listingType: e.target.checked ? "pre-order" : "standard" }), disabled: isReadonly }), _jsx(Heading, { level: 4, className: "mt-4", children: "Prize Draw Settings" }), _jsx(Checkbox, { label: "This is a prize-draw listing", checked: isPrizeDrawListing(product), onChange: (e) => update({ listingType: e.target.checked ? "prize-draw" : "standard" }), disabled: isReadonly ||
147
- (product.prizeDrawItems ?? []).some((it) => it.isWon) }), isPrizeDrawListing(product) && ((() => {
148
+ ] }), _jsx(Text, { variant: "secondary", weight: "semibold", className: "mt-2", children: t("sectionAuctionAdvanced") }), _jsx(Checkbox, { label: t("formAutoExtendable"), checked: !!product.autoExtendable, onChange: (e) => update({ autoExtendable: e.target.checked }), disabled: isReadonly }), product.autoExtendable && (_jsxs(_Fragment, { children: [_jsx(Alert, { variant: "info", title: t("formAutoExtendableHelp"), children: t("formAutoExtendableHelp") }), _jsx(FormField, { name: "auctionExtensionMinutes", label: t("formAuctionExtensionMinutes"), type: "number", value: String(product.auctionExtensionMinutes ?? 5), onChange: (value) => update({ auctionExtensionMinutes: Number(value) || 5 }), disabled: isReadonly, placeholder: "5", helpText: t("formAuctionExtensionMinutesHelp") })] }))] })), listingTypeFlags["pre-order"] && (_jsxs(_Fragment, { children: [_jsx(Heading, { level: 4, className: "mt-4", children: t("sectionPreOrderSettings") }), _jsx(Checkbox, { label: t("formIsPreOrder"), checked: isPreOrderListing(product), onChange: (e) => update({ listingType: e.target.checked ? "pre-order" : "standard" }), disabled: isReadonly })] })), listingTypeFlags["prize-draw"] && (_jsxs(_Fragment, { children: [_jsx(Heading, { level: 4, className: "mt-4", children: "Prize Draw Settings" }), _jsx(Checkbox, { label: "This is a prize-draw listing", checked: isPrizeDrawListing(product), onChange: (e) => update({ listingType: e.target.checked ? "prize-draw" : "standard" }), disabled: isReadonly ||
149
+ (product.prizeDrawItems ?? []).some((it) => it.isWon) })] })), isPrizeDrawListing(product) && ((() => {
148
150
  const prizeItems = (product.prizeDrawItems ?? []);
149
151
  const anyWon = prizeItems.some((it) => it.isWon);
150
152
  const lockedForReveal = anyWon;
@@ -127,6 +127,7 @@ export declare const DEFAULT_ROUTE_MAP: {
127
127
  readonly ORDER_CANCEL: (id: string) => string;
128
128
  readonly ORDER_TRACK: (id: string) => string;
129
129
  readonly ORDER_INVOICE: (id: string) => string;
130
+ readonly ORDER_PAYMENT: (id: string) => string;
130
131
  readonly NOTIFICATIONS: "/user/notifications";
131
132
  readonly MESSAGES: "/user/messages";
132
133
  /** Plan §10 — claimed-coupons wallet (Active / Expired / Used tabs). */
@@ -463,6 +464,7 @@ export declare const ROUTES: {
463
464
  readonly ORDER_CANCEL: (id: string) => string;
464
465
  readonly ORDER_TRACK: (id: string) => string;
465
466
  readonly ORDER_INVOICE: (id: string) => string;
467
+ readonly ORDER_PAYMENT: (id: string) => string;
466
468
  readonly NOTIFICATIONS: "/user/notifications";
467
469
  readonly MESSAGES: "/user/messages";
468
470
  /** Plan §10 — claimed-coupons wallet (Active / Expired / Used tabs). */
@@ -115,6 +115,7 @@ export const DEFAULT_ROUTE_MAP = {
115
115
  ORDER_CANCEL: (id) => `/user/orders/${id}/cancel`,
116
116
  ORDER_TRACK: (id) => `/user/orders/${id}/track`,
117
117
  ORDER_INVOICE: (id) => `/user/orders/${id}/invoice`,
118
+ ORDER_PAYMENT: (id) => `/user/orders/${id}/payment`,
118
119
  NOTIFICATIONS: "/user/notifications",
119
120
  MESSAGES: "/user/messages",
120
121
  /** Plan §10 — claimed-coupons wallet (Active / Expired / Used tabs). */
@@ -291,4 +291,58 @@ for (let i = _rawOrdersSeedData.length; i < 50; i++) {
291
291
  updatedAt: daysAgo(Math.max(0, daysBack - 10)),
292
292
  });
293
293
  }
294
- export const ordersSeedData = [..._rawOrdersSeedData, ...expandedOrders].slice(0, 50);
294
+ // P-1 MVP: two cash/UPI manual payment orders for demo purposes.
295
+ // cashOrderPendingProof: buyer submitted proof, admin has NOT yet verified.
296
+ const cashOrderPendingProof = {
297
+ id: "order-1-20260729-cash01",
298
+ productId: "product-dark-magician-lob-1st",
299
+ productTitle: "Dark Magician — LOB 1st Edition",
300
+ userId: "user-yugi-muto",
301
+ userName: "Yugi Muto",
302
+ userEmail: "yugi@duelkingdom.in",
303
+ storeId: "store-kaiba-corp-cards",
304
+ quantity: 1,
305
+ unitPrice: 499900,
306
+ totalPrice: 499900,
307
+ currency: "INR",
308
+ status: "pending",
309
+ paymentStatus: "pending",
310
+ paymentMethod: "cash",
311
+ paymentProofUrl: "/media/payment-proof-demo-pending.jpg",
312
+ paymentTransactionId: "UPI-DEMO-20260728-PROOF",
313
+ paymentProofMimeType: "image/jpeg",
314
+ paymentProofUploadedAt: daysAgo(1),
315
+ shippingAddress: "addr-yugi-home",
316
+ orderDate: daysAgo(2),
317
+ createdAt: daysAgo(2),
318
+ updatedAt: daysAgo(1),
319
+ };
320
+ const cashOrderVerified = {
321
+ id: "order-1-20260729-cash02",
322
+ productId: "product-hot-wheels-redline-vintage",
323
+ productTitle: "Hot Wheels Redline — Vintage",
324
+ userId: "user-yugi-muto",
325
+ userName: "Yugi Muto",
326
+ userEmail: "yugi@duelkingdom.in",
327
+ storeId: "store-diecast-depot",
328
+ quantity: 1,
329
+ unitPrice: 129900,
330
+ totalPrice: 129900,
331
+ currency: "INR",
332
+ status: "processing",
333
+ paymentStatus: "paid",
334
+ paymentMethod: "cash",
335
+ paymentProofUrl: "/media/payment-proof-demo-verified.jpg",
336
+ paymentTransactionId: "UPI-DEMO-20260727-YUGI",
337
+ paymentProofMimeType: "image/jpeg",
338
+ paymentProofUploadedAt: daysAgo(3),
339
+ shippingAddress: "addr-yugi-home",
340
+ orderDate: daysAgo(4),
341
+ createdAt: daysAgo(4),
342
+ updatedAt: daysAgo(2),
343
+ };
344
+ export const ordersSeedData = [
345
+ cashOrderPendingProof,
346
+ cashOrderVerified,
347
+ ...[..._rawOrdersSeedData, ...expandedOrders].slice(0, 48),
348
+ ];
@@ -374,16 +374,26 @@ export const siteSettingsSeedData = {
374
374
  smsVerification: true,
375
375
  translations: true,
376
376
  wishlists: true,
377
- auctions: true,
377
+ auctions: false,
378
378
  reviews: true,
379
- events: true,
380
- blog: true,
381
- coupons: true,
379
+ events: false,
380
+ blog: false,
381
+ coupons: false,
382
382
  notifications: true,
383
383
  sellerRegistration: true,
384
- preOrders: true,
384
+ preOrders: false,
385
385
  seedPanel: true,
386
386
  adminCheckoutBypass: false,
387
+ // P-1 MVP: only standard listing type active; others unlocked in later patches.
388
+ listingTypes: {
389
+ standard: true,
390
+ auction: false,
391
+ "pre-order": false,
392
+ "prize-draw": false,
393
+ classified: false,
394
+ "digital-code": false,
395
+ live: false,
396
+ },
387
397
  },
388
398
  credentials: {
389
399
  razorpayKeyId: "rzp_test_PLACEHOLDER",
package/dist/server.d.ts CHANGED
@@ -6,6 +6,7 @@ export { SERVER_ERRORS_COLLECTION, SERVER_ERROR_FIELDS, SERVER_ERROR_STACK_MAX_B
6
6
  export type { ServerErrorDocument, ServerErrorSource, } from "./features/server-errors/schemas/firestore";
7
7
  export { wrapAction, isOk, unwrap, } from "./_internal/shared/types/action-result";
8
8
  export type { ActionResult } from "./_internal/shared/types/action-result";
9
+ export { attachPaymentProofAction, adminVerifyPaymentAction, } from "./_internal/server/features/orders/actions";
9
10
  export { wrapJobHandler, wrapScheduleHandler, wrapTriggerHandler, wrapCallableHandler, } from "./_internal/server/jobs/core/wrapJobHandler";
10
11
  export { analyzeLogs } from "./_internal/server/features/maintenance/analyze";
11
12
  export type { AnalyzeOptions, AnalyzeReport, } from "./_internal/server/features/maintenance/analyze";
package/dist/server.js CHANGED
@@ -11,6 +11,8 @@ export { SERVER_ERRORS_COLLECTION, SERVER_ERROR_FIELDS, SERVER_ERROR_STACK_MAX_B
11
11
  // [SERVER-ONLY] ActionResult envelope + wrapAction helper for "use server" functions.
12
12
  // Mirrors the routeHandler envelope so client wrappers consume both surfaces uniformly.
13
13
  export { wrapAction, isOk, unwrap, } from "./_internal/shared/types/action-result";
14
+ // [SERVER-ONLY] P-1 manual payment proof server actions.
15
+ export { attachPaymentProofAction, adminVerifyPaymentAction, } from "./_internal/server/features/orders/actions";
14
16
  // [SERVER-ONLY] Cloud Function handler wrappers — persist exceptions to
15
17
  // serverErrors (source: "function") before re-throwing so retry semantics
16
18
  // are preserved. Apply at the consumer's runtime adapter boundary.
@@ -242,6 +242,12 @@ export type MediaFilenameContext = ({
242
242
  refundId: string;
243
243
  ext?: string;
244
244
  date?: Date;
245
+ } | {
246
+ type: "payment-proof";
247
+ orderId: string;
248
+ buyerName: string;
249
+ ext?: string;
250
+ date?: Date;
245
251
  };
246
252
  export declare function generateMediaFilename(ctx: MediaFilenameContext): string;
247
253
  export declare function validateMediaFilename(filename: string): boolean;
@@ -384,6 +384,15 @@ export function generateMediaFilename(ctx) {
384
384
  const ext = ctx.ext ?? "pdf";
385
385
  return `refund-proof-${ctx.orderId}-${ctx.refundId}-${y}${m}${day}.${ext}`;
386
386
  }
387
+ case "payment-proof": {
388
+ const d = ctx.date ?? new Date();
389
+ const y = d.getFullYear();
390
+ const m = String(d.getMonth() + 1).padStart(2, "0");
391
+ const day = String(d.getDate()).padStart(2, "0");
392
+ const ext = ctx.ext ?? "jpg";
393
+ const name = ctx.buyerName.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").slice(0, 20);
394
+ return `payment-proof-${ctx.orderId}-${name}-${y}${m}${day}.${ext}`;
395
+ }
387
396
  }
388
397
  }
389
398
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "3.1.7",
3
+ "version": "3.2.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"