@mohasinac/appkit 4.12.0 → 4.12.2

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 (68) hide show
  1. package/dist/_internal/server/features/auctions/service.d.ts +7 -1
  2. package/dist/_internal/server/features/auctions/service.js +13 -5
  3. package/dist/_internal/server/features/checkout/actions.d.ts +0 -15
  4. package/dist/_internal/server/features/checkout/actions.js +7 -3
  5. package/dist/_internal/server/features/orders/actions.js +5 -3
  6. package/dist/_internal/server/functions/firestore.d.ts +1 -2
  7. package/dist/_internal/server/functions/firestore.js +1 -9
  8. package/dist/_internal/server/jobs/core/index.d.ts +0 -1
  9. package/dist/_internal/server/jobs/core/index.js +0 -1
  10. package/dist/_internal/server/jobs/core/offerExpiry.js +33 -1
  11. package/dist/_internal/server/jobs/handlers/index.d.ts +1 -2
  12. package/dist/_internal/server/jobs/handlers/index.js +1 -2
  13. package/dist/_internal/shared/features/auctions/config.d.ts +34 -0
  14. package/dist/_internal/shared/features/auctions/config.js +47 -3
  15. package/dist/_internal/shared/listing-types/auction/config.js +1 -1
  16. package/dist/_internal/shared/listing-types/digital-code/config.js +1 -1
  17. package/dist/_internal/shared/listing-types/live/config.js +1 -1
  18. package/dist/_internal/shared/listing-types/pre-order/config.js +1 -1
  19. package/dist/features/account/components/NotificationBell.js +1 -1
  20. package/dist/features/admin/components/AdminNotificationsView.js +4 -1
  21. package/dist/features/admin/components/AdminOrderEditorView.d.ts +7 -1
  22. package/dist/features/admin/components/AdminOrderEditorView.js +16 -4
  23. package/dist/features/admin/components/AdminOrdersView.js +2 -1
  24. package/dist/features/auctions/actions/bid-actions.js +71 -9
  25. package/dist/features/auctions/components/AuctionDetailPageView.js +8 -2
  26. package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -3
  27. package/dist/features/auctions/components/PlaceBidFormClient.js +89 -14
  28. package/dist/features/auctions/schemas/bid-input.d.ts +19 -6
  29. package/dist/features/auctions/schemas/bid-input.js +19 -7
  30. package/dist/features/auth/repository/user.repository.js +6 -0
  31. package/dist/features/faq/components/FAQHelpfulButtons.js +2 -2
  32. package/dist/features/filters/FilterFacetSection.js +1 -1
  33. package/dist/features/homepage/components/WhatsAppCommunitySection.js +1 -1
  34. package/dist/features/layout/BackToTop.js +1 -1
  35. package/dist/features/layout/BottomActions.js +1 -1
  36. package/dist/features/layout/FooterLayout.js +2 -2
  37. package/dist/features/layout/TitleBarLayout.js +1 -1
  38. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -3
  39. package/dist/features/products/components/MarketplaceBundleCard.js +2 -2
  40. package/dist/features/products/components/MarketplacePrizeDrawCard.js +2 -2
  41. package/dist/features/products/components/PrizeDrawCollage.js +1 -1
  42. package/dist/features/products/components/PrizeDrawItemsEditor.js +1 -1
  43. package/dist/features/products/components/ProductDetailPageView.js +1 -1
  44. package/dist/features/products/components/ProductGrid.js +3 -3
  45. package/dist/features/products/repository/products.repository.d.ts +0 -4
  46. package/dist/features/products/repository/products.repository.js +0 -15
  47. package/dist/features/promotions/repository/coupons.repository.d.ts +12 -0
  48. package/dist/features/promotions/repository/coupons.repository.js +12 -0
  49. package/dist/features/seller/components/SellerSidebar.js +1 -1
  50. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +279 -0
  51. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +1 -1
  52. package/dist/jobs.d.ts +1 -1
  53. package/dist/jobs.js +1 -1
  54. package/dist/seed/homepage-sections-seed-data.js +5 -1
  55. package/dist/seed/site-settings-seed-data.js +18 -0
  56. package/dist/styles.css +55 -1
  57. package/dist/tailwind-utilities.css +1 -1
  58. package/dist/tokens/color-pairs.js +1 -0
  59. package/dist/tokens/tokens.css +33 -0
  60. package/dist/ui/components/Button.style.css +21 -0
  61. package/dist/ui/components/ImageLightbox.js +7 -4
  62. package/dist/ui/components/surface-tokens.d.ts +7 -0
  63. package/dist/ui/components/surface-tokens.js +7 -0
  64. package/package.json +1 -1
  65. package/dist/_internal/server/jobs/core/onBidPlaced.d.ts +0 -15
  66. package/dist/_internal/server/jobs/core/onBidPlaced.js +0 -63
  67. package/dist/_internal/server/jobs/handlers/onBidPlaced.d.ts +0 -3
  68. package/dist/_internal/server/jobs/handlers/onBidPlaced.js +0 -9
@@ -2,7 +2,13 @@ import { type BidIncrementTier } from "../../../shared/features/auctions/config"
2
2
  import type { ProductDocument } from "../../../shared/features/products/types";
3
3
  /** Assert an auction exists, is published, and is still active. Returns the product. */
4
4
  export declare function assertAuctionActive(auctionId: string): Promise<ProductDocument>;
5
- /** Compute the minimum valid bid given the current state and the admin's tier table. */
5
+ /**
6
+ * Compute the minimum valid bid given the current state and the admin's tier table.
7
+ *
8
+ * Delegates to the shared `resolveMinBid` so this path can't drift from
9
+ * `placeBid`'s — including the no-bids case, where the starting bid is itself
10
+ * acceptable rather than needing to be beaten by an increment.
11
+ */
6
12
  export declare function computeMinBid(product: ProductDocument, tiers: BidIncrementTier[]): number;
7
13
  /** Assert a bid amount is valid against the current auction state. */
8
14
  export declare function assertBidAmount(product: ProductDocument, amount: number, tiers: BidIncrementTier[]): void;
@@ -1,7 +1,7 @@
1
1
  import { productRepository } from "../../../../repositories";
2
2
  import { isAuctionListing } from "../../../../features/products/utils/listing-type";
3
3
  import { AuctionNotFoundError, AuctionEndedError, BidTooLowError, BidOnOwnAuctionError, } from "../../../shared/features/auctions/errors";
4
- import { AUCTION_SNIPING_WINDOW_SECONDS, AUCTION_DEFAULT_EXTENSION_MINUTES, resolveMinBidIncrement, } from "../../../shared/features/auctions/config";
4
+ import { AUCTION_SNIPING_WINDOW_SECONDS, AUCTION_DEFAULT_EXTENSION_MINUTES, resolveMinBid, } from "../../../shared/features/auctions/config";
5
5
  /** Assert an auction exists, is published, and is still active. Returns the product. */
6
6
  export async function assertAuctionActive(auctionId) {
7
7
  const product = await productRepository.findByIdOrSlug(auctionId).catch(() => null);
@@ -16,11 +16,19 @@ export async function assertAuctionActive(auctionId) {
16
16
  throw new AuctionEndedError(auctionId);
17
17
  return product;
18
18
  }
19
- /** Compute the minimum valid bid given the current state and the admin's tier table. */
19
+ /**
20
+ * Compute the minimum valid bid given the current state and the admin's tier table.
21
+ *
22
+ * Delegates to the shared `resolveMinBid` so this path can't drift from
23
+ * `placeBid`'s — including the no-bids case, where the starting bid is itself
24
+ * acceptable rather than needing to be beaten by an increment.
25
+ */
20
26
  export function computeMinBid(product, tiers) {
21
- const current = product.currentBid ?? product.startingBid ?? 0;
22
- const increment = resolveMinBidIncrement(current, tiers, product.minBidIncrement);
23
- return current + increment;
27
+ const hasBids = (product.currentBid ?? 0) > 0 || (product.bidCount ?? 0) > 0;
28
+ const current = hasBids
29
+ ? (product.currentBid ?? product.startingBid ?? 0)
30
+ : (product.startingBid ?? 0);
31
+ return resolveMinBid(current, tiers, product.minBidIncrement, { hasBids });
24
32
  }
25
33
  /** Assert a bid amount is valid against the current auction state. */
26
34
  export function assertBidAmount(product, amount, tiers) {
@@ -24,14 +24,6 @@ export interface CreateCheckoutOrderInput {
24
24
  * omits the field.
25
25
  */
26
26
  outOfStockPolicy?: OutOfStockPolicy;
27
- /** Buyer opted into the ₹10 WhatsApp order-updates addon at checkout. Defaults false (unchecked). Only actually charged/honored when siteSettings.commissions.whatsappNotifyFeeEnabled is also true. */
28
- whatsappNotifyAddon?: boolean;
29
- /** Buyer opted into gift wrap at checkout. Defaults false (unchecked). Only actually charged/honored when siteSettings.commissions.giftWrapFeeEnabled is also true. */
30
- giftWrapAddon?: boolean;
31
- /** Optional gift message, only meaningful when giftWrapAddon is true. */
32
- giftWrapMessage?: string;
33
- /** Buyer opted into shipment protection at checkout. Defaults false (unchecked). Only actually charged/honored when siteSettings.commissions.shipmentProtectionFeeEnabled is also true. */
34
- shipmentProtectionAddon?: boolean;
35
27
  }
36
28
  /**
37
29
  * Exported so `/api/payment/create-order` (pre-charge amount) and the
@@ -146,12 +138,5 @@ export interface VerifyAndPlaceRazorpayOrderInput {
146
138
  * on the Razorpay path.
147
139
  */
148
140
  outOfStockPolicy?: OutOfStockPolicy;
149
- /** Buyer opted into the ₹10 WhatsApp order-updates addon at checkout — must match the value sent to /api/payment/create-order so the pre-charged amount and the placed order agree. */
150
- whatsappNotifyAddon?: boolean;
151
- /** Buyer opted into gift wrap at checkout — must match the value sent to /api/payment/create-order. */
152
- giftWrapAddon?: boolean;
153
- giftWrapMessage?: string;
154
- /** Buyer opted into shipment protection at checkout — must match the value sent to /api/payment/create-order. */
155
- shipmentProtectionAddon?: boolean;
156
141
  }
157
142
  export declare function verifyAndPlaceRazorpayOrderAction(input: VerifyAndPlaceRazorpayOrderInput): Promise<CheckoutOrderResult>;
@@ -1139,8 +1139,12 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
1139
1139
  // here — the only path that charged the commission at all, and it charged a
1140
1140
  // different (larger, multiplied-per-store) number than the cap now allows.
1141
1141
  // Both the amount and its allocation are now decided once for the checkout.
1142
- const { platformFee: rawPlatformFee = 0, gstOnFee = 0 } = platformFeeByStore.get(firstItem.storeId) ?? {};
1143
- const platformFee = rawPlatformFee + gstOnFee;
1142
+ // Kept as two separate values, exactly as the COD/manual path does. Folding the
1143
+ // GST into `platformFee` made `OrderDocument.platformFee` mean "fee" on one path
1144
+ // and "fee + GST" on the other, so no revenue rollup could read it without
1145
+ // knowing which path created the row. The buyer pays the same either way —
1146
+ // both components are still added to orderTotal below.
1147
+ const { platformFee = 0, gstOnFee: platformFeeGst = 0 } = platformFeeByStore.get(firstItem.storeId) ?? {};
1144
1148
  // Add-ons are this store's choice, read off the cart doc — previously one
1145
1149
  // cart-wide fee was passed in and billed against every group.
1146
1150
  const commissionRates = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
@@ -1149,7 +1153,7 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
1149
1153
  const giftWrapFee = computeGiftWrapFee(addons.giftWrapAddon ?? false, commissionRates);
1150
1154
  const giftWrapMessage = addons.giftWrapMessage;
1151
1155
  const shipmentProtectionFee = computeShipmentProtectionFee(groupTotal, addons.shipmentProtectionAddon ?? false, commissionRates);
1152
- const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee + whatsappNotifyFee + giftWrapFee + shipmentProtectionFee + platformFee;
1156
+ const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee + whatsappNotifyFee + giftWrapFee + shipmentProtectionFee + platformFee + platformFeeGst;
1153
1157
  // P-8 GST — deliberately NOT wired into this Razorpay-verify path. The
1154
1158
  // amount-mismatch check above (expectedPaymentAmountRs) compares against
1155
1159
  // what the buyer already paid via the Razorpay order created earlier in
@@ -11,7 +11,7 @@ import { sendNotification } from "../../../../features/admin/actions/notificatio
11
11
  import { restoreStockForOrder } from "../checkout/stock-restore";
12
12
  import { getAdminDb } from "../../../../providers/db-firebase";
13
13
  import { enqueueJob } from "../../../../features/jobs/actions/enqueue-job";
14
- import { PAYMENT_WINDOW_MS, PAYMENT_FRAUD_REJECTED_REASON } from "../../../../features/orders/constants/payment-window";
14
+ import { PAYMENT_WINDOW_MS, PAYMENT_FRAUD_REJECTED_REASON, isManualPaymentMethod } from "../../../../features/orders/constants/payment-window";
15
15
  import { normalizeError } from "../../../../errors/normalize";
16
16
  import { serverLogger } from "../../../../monitoring";
17
17
  export async function createOrderAction(input) {
@@ -131,8 +131,10 @@ export async function attachPaymentProofAction(orderId, proof) {
131
131
  throw new OrderNotFoundError(orderId);
132
132
  if (!isAdminUser(user) && order.userId !== user.uid)
133
133
  throw new OrderOwnershipError(orderId);
134
- const pm = order.paymentMethod ?? "";
135
- if (pm !== "cash" && pm !== "upi_manual" && pm !== "emi") {
134
+ // Use the shared predicate rather than re-inlining the method list — this
135
+ // was the second of the two sites that had drifted from it (the other being
136
+ // AdminOrderEditorView, which was missing `emi` outright).
137
+ if (!isManualPaymentMethod(order.paymentMethod ?? "")) {
136
138
  throw new ValidationError("Payment proof can only be attached to cash, UPI, or EMI orders");
137
139
  }
138
140
  if (order.paymentProofUrl) {
@@ -6,7 +6,6 @@
6
6
  * The path patterns mirror the original `functions/src/index.ts` declarations
7
7
  * to preserve identical deploy behavior.
8
8
  */
9
- export declare const onBidPlaced: import("./types").DocumentTriggerFunctionDefinition<null, import("../jobs").NewBid>;
10
9
  export declare const onOrderCreate: import("./types").DocumentTriggerFunctionDefinition<null, import("../jobs").NewOrder>;
11
10
  export declare const onOrderStatusChange: import("./types").DocumentTriggerFunctionDefinition<import("../jobs").OrderBefore, import("../jobs").OrderAfter>;
12
11
  export declare const onProductWrite: import("./types").DocumentTriggerFunctionDefinition<import("../jobs").ProductDoc, import("../jobs").ProductDoc>;
@@ -51,7 +50,7 @@ export declare const onCatalogueSubmittedForApproval: import("./types").Document
51
50
  [x: string]: import("../../..").JsonValue;
52
51
  }>;
53
52
  export declare const onJobCreated: import("./types").DocumentTriggerFunctionDefinition<null, import("../../../server").JobDocument>;
54
- export declare const FIRESTORE_TRIGGER_FUNCTIONS: readonly [import("./types").DocumentTriggerFunctionDefinition<null, import("../jobs").NewBid>, import("./types").DocumentTriggerFunctionDefinition<null, import("../jobs").NewOrder>, import("./types").DocumentTriggerFunctionDefinition<import("../jobs").OrderBefore, import("../jobs").OrderAfter>, import("./types").DocumentTriggerFunctionDefinition<import("../jobs").ProductDoc, import("../jobs").ProductDoc>, import("./types").DocumentTriggerFunctionDefinition<{
53
+ export declare const FIRESTORE_TRIGGER_FUNCTIONS: readonly [import("./types").DocumentTriggerFunctionDefinition<null, import("../jobs").NewOrder>, import("./types").DocumentTriggerFunctionDefinition<import("../jobs").OrderBefore, import("../jobs").OrderAfter>, import("./types").DocumentTriggerFunctionDefinition<import("../jobs").ProductDoc, import("../jobs").ProductDoc>, import("./types").DocumentTriggerFunctionDefinition<{
55
54
  [x: string]: import("../../..").JsonValue;
56
55
  }, {
57
56
  [x: string]: import("../../..").JsonValue;
@@ -6,16 +6,9 @@
6
6
  * The path patterns mirror the original `functions/src/index.ts` declarations
7
7
  * to preserve identical deploy behavior.
8
8
  */
9
- import { onBidPlacedHandler, onCategoryWriteHandler, onOrderCreateHandler, onOrderStatusChangeHandler, onProductStockChangeHandler, onProductWriteHandler, onReviewWriteHandler, onScamReportCreateHandler, onScamReportUpdateHandler, onStoreWriteHandler, onSupportTicketCreateHandler, onSupportTicketUpdateHandler, onUserBanChangeHandler, onShipmentItemWriteHandler, onShipmentLotWriteHandler, onShipmentHeaderWriteHandler, onShipmentDeletedHandler, onCatalogueSubmittedForApprovalHandler, onJobCreatedHandler, onPrizeDrawPaymentConfirmedHandler, prizeDrawSoldOutRevealHandler, } from "../jobs/handlers";
9
+ import { onCategoryWriteHandler, onOrderCreateHandler, onOrderStatusChangeHandler, onProductStockChangeHandler, onProductWriteHandler, onReviewWriteHandler, onScamReportCreateHandler, onScamReportUpdateHandler, onStoreWriteHandler, onSupportTicketCreateHandler, onSupportTicketUpdateHandler, onUserBanChangeHandler, onShipmentItemWriteHandler, onShipmentLotWriteHandler, onShipmentHeaderWriteHandler, onShipmentDeletedHandler, onCatalogueSubmittedForApprovalHandler, onJobCreatedHandler, onPrizeDrawPaymentConfirmedHandler, prizeDrawSoldOutRevealHandler, } from "../jobs/handlers";
10
10
  import { defineFunction } from "./define";
11
11
  const REGION = "asia-south1";
12
- export const onBidPlaced = defineFunction({
13
- name: "onBidPlaced",
14
- description: "Side-effects on bid creation (notifications, outbid emails).",
15
- trigger: { kind: "documentCreated", pathPattern: "bids/{bidId}" },
16
- handler: onBidPlacedHandler,
17
- options: { region: REGION },
18
- });
19
12
  export const onOrderCreate = defineFunction({
20
13
  name: "onOrderCreate",
21
14
  description: "Side-effects on order creation (notify store, decrement stock).",
@@ -164,7 +157,6 @@ export const onJobCreated = defineFunction({
164
157
  options: { region: REGION, timeoutSeconds: 300, memory: "512MiB" },
165
158
  });
166
159
  export const FIRESTORE_TRIGGER_FUNCTIONS = [
167
- onBidPlaced,
168
160
  onOrderCreate,
169
161
  onOrderStatusChange,
170
162
  onProductWrite,
@@ -36,7 +36,6 @@ export { runStoreAnalytics, type StoreAnalyticsInput, type StoreAnalyticsResult,
36
36
  export { runAdminAnalytics, type AdminAnalyticsResult } from "./adminAnalytics";
37
37
  export { runTriggerEventRaffle, type TriggerEventRaffleInput, type TriggerEventRaffleResult, } from "./triggerEventRaffle";
38
38
  export { runAssignSpinPrize, type AssignSpinPrizeInput, type AssignSpinPrizeResult, } from "./assignSpinPrize";
39
- export { handleBidPlaced, type HandleBidPlacedInput, type NewBid } from "./onBidPlaced";
40
39
  export { handleOrderCreate, type HandleOrderCreateInput, type NewOrder, } from "./onOrderCreate";
41
40
  export { handleOrderStatusChange, type HandleOrderStatusChangeInput, type OrderAfter, type OrderBefore, } from "./onOrderStatusChange";
42
41
  export { handleProductWrite, type HandleProductWriteInput, type ProductDoc, } from "./onProductWrite";
@@ -39,7 +39,6 @@ export { runAdminAnalytics } from "./adminAnalytics";
39
39
  export { runTriggerEventRaffle, } from "./triggerEventRaffle";
40
40
  export { runAssignSpinPrize, } from "./assignSpinPrize";
41
41
  // Trigger reactors — call with the meaningful payload, no Firestore event needed
42
- export { handleBidPlaced } from "./onBidPlaced";
43
42
  export { handleOrderCreate, } from "./onOrderCreate";
44
43
  export { handleOrderStatusChange, } from "./onOrderStatusChange";
45
44
  export { handleProductWrite, } from "./onProductWrite";
@@ -1,5 +1,5 @@
1
1
  import { normalizeError } from "../../../../errors/normalize";
2
- import { bidRepository, cartRepository, offerRepository } from "../../../../repositories";
2
+ import { bidRepository, cartRepository, offerRepository, storeRepository } from "../../../../repositories";
3
3
  import { sendNotification } from "../../../../features/admin/actions/notification-actions";
4
4
  export async function runOfferExpiry(ctx) {
5
5
  ctx.logger.info("Starting offer expiry sweep");
@@ -141,6 +141,37 @@ async function lapseUnpaidAuctionWins(ctx) {
141
141
  if (auctionLines.length === 0)
142
142
  return;
143
143
  ctx.logger.info(`Found ${auctionLines.length} unpaid auction win(s) past deadline`);
144
+ /**
145
+ * Tell the seller their item is unsold again so they can relist it or approach
146
+ * the runner-up. Only the buyer was ever notified, so to a seller a forfeited
147
+ * win looked like a completed sale that simply never paid out.
148
+ *
149
+ * Best-effort by design: the forfeiture itself (cart line cleared, bid marked
150
+ * forfeited) is already committed by the time this runs, and a missing store or
151
+ * a notification failure must not roll that back or abort the sweep.
152
+ */
153
+ async function notifySellerOfForfeitedWin(item, jobCtx) {
154
+ try {
155
+ const store = item.storeId ? await storeRepository.findById(item.storeId) : null;
156
+ if (!store?.ownerId)
157
+ return;
158
+ await sendNotification({
159
+ userId: store.ownerId,
160
+ type: "auction_ended",
161
+ priority: "normal",
162
+ title: "Auction win forfeited — item unsold",
163
+ message: `The winning bidder for "${item.productTitle}" did not pay within the deadline, so the win was forfeited. You can relist the item or approach the next highest bidder.`,
164
+ relatedId: item.auctionId ?? item.productId,
165
+ relatedType: "product",
166
+ });
167
+ }
168
+ catch (sellerErr) {
169
+ void normalizeError(sellerErr);
170
+ jobCtx.logger.warn(`Failed to notify seller of forfeited win ${item.bidId}`, {
171
+ error: sellerErr instanceof Error ? sellerErr.message : String(sellerErr),
172
+ });
173
+ }
174
+ }
144
175
  for (const { userId, item } of auctionLines) {
145
176
  try {
146
177
  await cartRepository.removeItemsByBidId(userId, item.bidId);
@@ -154,6 +185,7 @@ async function lapseUnpaidAuctionWins(ctx) {
154
185
  relatedId: item.auctionId ?? item.productId,
155
186
  relatedType: "product",
156
187
  });
188
+ await notifySellerOfForfeitedWin(item, ctx);
157
189
  }
158
190
  catch (err) {
159
191
  void normalizeError(err);
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * S4 batch 1: promotions, onOrderCreate, onOrderStatusChange,
6
6
  * auctionSettlement, autoPayoutEligibility, couponExpiry, offerExpiry
7
- * S5 batch 2: onReviewWrite, onBidPlaced, cartPrune, notificationPrune,
7
+ * S5 batch 2: onReviewWrite, cartPrune, notificationPrune,
8
8
  * dailyDataCleanup, countersReconcile, cleanupRtdbEvents
9
9
  */
10
10
  export { couponExpiryHandler } from "./couponExpiry";
@@ -18,7 +18,6 @@ export { autoPayoutEligibilityHandler } from "./autoPayoutEligibility";
18
18
  export { countersReconcileHandler } from "./countersReconcile";
19
19
  export { onOrderCreateHandler } from "./onOrderCreate";
20
20
  export { onOrderStatusChangeHandler } from "./onOrderStatusChange";
21
- export { onBidPlacedHandler } from "./onBidPlaced";
22
21
  export { onJobCreatedHandler } from "./onJobCreated";
23
22
  export { onReviewWriteHandler } from "./onReviewWrite";
24
23
  export { promotionsHandler, type PromotionsCallableResult } from "./promotions";
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * S4 batch 1: promotions, onOrderCreate, onOrderStatusChange,
6
6
  * auctionSettlement, autoPayoutEligibility, couponExpiry, offerExpiry
7
- * S5 batch 2: onReviewWrite, onBidPlaced, cartPrune, notificationPrune,
7
+ * S5 batch 2: onReviewWrite, cartPrune, notificationPrune,
8
8
  * dailyDataCleanup, countersReconcile, cleanupRtdbEvents
9
9
  */
10
10
  export { couponExpiryHandler } from "./couponExpiry";
@@ -18,7 +18,6 @@ export { autoPayoutEligibilityHandler } from "./autoPayoutEligibility";
18
18
  export { countersReconcileHandler } from "./countersReconcile";
19
19
  export { onOrderCreateHandler } from "./onOrderCreate";
20
20
  export { onOrderStatusChangeHandler } from "./onOrderStatusChange";
21
- export { onBidPlacedHandler } from "./onBidPlaced";
22
21
  export { onJobCreatedHandler } from "./onJobCreated";
23
22
  export { onReviewWriteHandler } from "./onReviewWrite";
24
23
  export { promotionsHandler } from "./promotions";
@@ -24,3 +24,37 @@ export declare function resolveTieredBidIncrement(currentBidAmount: number, tier
24
24
  * MORE than the platform tier, but can never undercut it.
25
25
  */
26
26
  export declare function resolveMinBidIncrement(currentBidAmount: number, tiers: BidIncrementTier[], override?: number | null): number;
27
+ export interface ResolveMinBidOptions {
28
+ /**
29
+ * Whether the auction already has at least one bid. Defaults to `true`.
30
+ *
31
+ * When `false` the starting bid is itself acceptable — a seller's opening
32
+ * price is a price, not a floor that must be beaten. Requiring
33
+ * `startingBid + increment` for the very first bid (which is what this
34
+ * function did unconditionally before) meant a ₹100 auction could never be
35
+ * opened at ₹100, only at ₹110, contradicting both the "Starting bid ₹100"
36
+ * label shown next to the field and the eBay convention buyers expect.
37
+ */
38
+ hasBids?: boolean;
39
+ }
40
+ /**
41
+ * The single definition of "the lowest amount this auction will accept next".
42
+ *
43
+ * Both the bid form and `placeBid` must agree on this exactly, or the form
44
+ * seeds an amount the server then rejects. Deriving it in two places is the
45
+ * drift shape Root Cause #30 describes, so both call this instead.
46
+ *
47
+ * `currentBidAmount` is the auction's live visible price — for an auction with
48
+ * no bids yet, callers pass `startingBid` (mirroring `placeBid`'s `baseBid`)
49
+ * together with `{ hasBids: false }`.
50
+ */
51
+ export declare function resolveMinBid(currentBidAmount: number, tiers: BidIncrementTier[], override?: number | null, opts?: ResolveMinBidOptions): number;
52
+ /**
53
+ * Preset step multipliers offered by the bid form, as multiples of the
54
+ * effective minimum increment. With a ₹100 increment this renders
55
+ * +₹100 / +₹500 / +₹1,000; with a ₹1,000 increment, +₹1,000 / +₹5,000 /
56
+ * +₹10,000. Never hardcode the rupee amounts at the call site — the whole
57
+ * point is that the steps track the tier.
58
+ */
59
+ export declare const BID_PRESET_MULTIPLIERS: readonly [1, 5, 10];
60
+ export type BidPresetMultiplier = (typeof BID_PRESET_MULTIPLIERS)[number];
@@ -12,6 +12,23 @@ export const DEFAULT_AUCTION_BID_INCREMENT_TIERS = [
12
12
  { upTo: 10000, increment: 500 },
13
13
  { upTo: null, increment: 1000 },
14
14
  ];
15
+ /**
16
+ * Drop malformed rows and fall back to the seed table when nothing usable is
17
+ * left.
18
+ *
19
+ * An empty/missing tier array does NOT mean "this auction has no increment
20
+ * rule" — it means the `siteSettings/global` singleton predates
21
+ * `auctionConfig.bidIncrementTiers`, or an admin cleared every row. Every
22
+ * caller reaches this via `settings.auctionConfig?.bidIncrementTiers ?? []`,
23
+ * so before this guard existed that case fell through to the ₹1
24
+ * `AUCTION_MIN_BID_INCREMENT` last-resort constant, which made the effective
25
+ * minimum step ₹1 on every auction on the site — that is what rendered the
26
+ * bid presets as "+₹1 / +₹5 / +₹10" instead of tier-derived steps.
27
+ */
28
+ function usableTiers(tiers) {
29
+ const valid = (tiers ?? []).filter((t) => t && Number.isFinite(t.increment) && t.increment > 0);
30
+ return valid.length > 0 ? valid : DEFAULT_AUCTION_BID_INCREMENT_TIERS;
31
+ }
15
32
  /**
16
33
  * Resolve the tiered minimum bid increment for a given current bid amount.
17
34
  * Tier lookup is inclusive of each band's upper bound — e.g. with the
@@ -19,11 +36,12 @@ export const DEFAULT_AUCTION_BID_INCREMENT_TIERS = [
19
36
  * increment (the 100-1000 tier), not the 200 increment of the next band.
20
37
  */
21
38
  export function resolveTieredBidIncrement(currentBidAmount, tiers) {
22
- for (const tier of tiers) {
39
+ const table = usableTiers(tiers);
40
+ for (const tier of table) {
23
41
  if (tier.upTo === null || currentBidAmount <= tier.upTo)
24
42
  return tier.increment;
25
43
  }
26
- return tiers[tiers.length - 1]?.increment ?? AUCTION_MIN_BID_INCREMENT;
44
+ return table[table.length - 1]?.increment ?? AUCTION_MIN_BID_INCREMENT;
27
45
  }
28
46
  /**
29
47
  * Effective minimum bid increment = max(admin tier floor, per-listing
@@ -32,5 +50,31 @@ export function resolveTieredBidIncrement(currentBidAmount, tiers) {
32
50
  */
33
51
  export function resolveMinBidIncrement(currentBidAmount, tiers, override) {
34
52
  const tierValue = resolveTieredBidIncrement(currentBidAmount, tiers);
35
- return typeof override === "number" && override > tierValue ? override : tierValue;
53
+ return typeof override === "number" && Number.isFinite(override) && override > tierValue
54
+ ? override
55
+ : tierValue;
56
+ }
57
+ /**
58
+ * The single definition of "the lowest amount this auction will accept next".
59
+ *
60
+ * Both the bid form and `placeBid` must agree on this exactly, or the form
61
+ * seeds an amount the server then rejects. Deriving it in two places is the
62
+ * drift shape Root Cause #30 describes, so both call this instead.
63
+ *
64
+ * `currentBidAmount` is the auction's live visible price — for an auction with
65
+ * no bids yet, callers pass `startingBid` (mirroring `placeBid`'s `baseBid`)
66
+ * together with `{ hasBids: false }`.
67
+ */
68
+ export function resolveMinBid(currentBidAmount, tiers, override, opts) {
69
+ if (opts?.hasBids === false)
70
+ return currentBidAmount;
71
+ return currentBidAmount + resolveMinBidIncrement(currentBidAmount, tiers, override);
36
72
  }
73
+ /**
74
+ * Preset step multipliers offered by the bid form, as multiples of the
75
+ * effective minimum increment. With a ₹100 increment this renders
76
+ * +₹100 / +₹500 / +₹1,000; with a ₹1,000 increment, +₹1,000 / +₹5,000 /
77
+ * +₹10,000. Never hardcode the rupee amounts at the call site — the whole
78
+ * point is that the steps track the tier.
79
+ */
80
+ export const BID_PRESET_MULTIPLIERS = [1, 5, 10];
@@ -9,7 +9,7 @@ export const config = {
9
9
  slugPrefix: "auction-",
10
10
  cartLine: "blocked",
11
11
  detailRoute: (idOrSlug) => String(ROUTES.PUBLIC.AUCTION_DETAIL(idOrSlug)),
12
- badge: { label: "Auction", className: "bg-warning-surface text-white" },
12
+ badge: { label: "Auction", className: "bg-warning-solid text-warning-on-solid" },
13
13
  priceLabel: "Suggested Retail Price (₹)",
14
14
  typeLabel: "Auction",
15
15
  showsStockQuantity: false,
@@ -9,7 +9,7 @@ export const config = {
9
9
  slugPrefix: "digitalcode-",
10
10
  cartLine: "single-product",
11
11
  detailRoute: (idOrSlug) => String(ROUTES.PUBLIC.DIGITAL_CODE_DETAIL(idOrSlug)),
12
- badge: { label: "Digital Code", className: "bg-success-surface text-success" },
12
+ badge: { label: "Digital Code", className: "bg-success-solid text-success-on-solid" },
13
13
  priceLabel: "Price per Code (₹)",
14
14
  typeLabel: "Digital Code",
15
15
  showsStockQuantity: false,
@@ -9,7 +9,7 @@ export const config = {
9
9
  slugPrefix: "live-",
10
10
  cartLine: "single-product",
11
11
  detailRoute: (idOrSlug) => String(ROUTES.PUBLIC.LIVE_DETAIL(idOrSlug)),
12
- badge: { label: "Live Item", className: "bg-danger-surface text-error" },
12
+ badge: { label: "Live Item", className: "bg-error-solid text-error-on-solid" },
13
13
  priceLabel: "Price (₹)",
14
14
  typeLabel: "Live Item",
15
15
  showsStockQuantity: true,
@@ -9,7 +9,7 @@ export const config = {
9
9
  slugPrefix: "preorder-",
10
10
  cartLine: "single-product",
11
11
  detailRoute: (idOrSlug) => String(ROUTES.PUBLIC.PRE_ORDER_DETAIL(idOrSlug)),
12
- badge: { label: "Pre-Order", className: "bg-info-surface text-white" },
12
+ badge: { label: "Pre-Order", className: "bg-info-solid text-info-on-solid" },
13
13
  priceLabel: "Pre-Order Price (₹)",
14
14
  typeLabel: "Pre-Order",
15
15
  showsStockQuantity: false,
@@ -10,7 +10,7 @@ const __O = {
10
10
  hidden: "overflow-hidden",
11
11
  yAuto: "overflow-y-auto",
12
12
  };
13
- const CLS_UNREAD_BADGE = "absolute -top-1 -right-1 bg-error-surface text-white min-w-[20px] h-5 px-[var(--appkit-space-1-5)] flex items-center justify-center rounded-full shadow-md";
13
+ const CLS_UNREAD_BADGE = "absolute -top-1 -right-1 bg-error-solid text-error-on-solid min-w-[20px] h-5 px-[var(--appkit-space-1-5)] flex items-center justify-center rounded-full shadow-md";
14
14
  const CLS_UNREAD_PILL = "ml-2 bg-error-surface text-error dark:bg-error-surface dark:text-error px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] rounded-full";
15
15
  const DEFAULT_ICONS = {
16
16
  order_placed: "🛍️",
@@ -82,7 +82,10 @@ export function AdminNotificationsView({ children, ...props }) {
82
82
  const config = {
83
83
  portal: "admin",
84
84
  title: "Notifications",
85
- searchPlaceholder: "Search by title or user ID",
85
+ // Title search is not expressible here — `title` is not in the repository's
86
+ // SIEVE_FIELDS and Sieve→Firestore cannot OR across two fields. The route
87
+ // treats `q` as an exact userId lookup, so the placeholder says exactly that.
88
+ searchPlaceholder: "Search by user ID",
86
89
  emptyLabel: "No notifications found",
87
90
  filterKeys: ["type", "readState"],
88
91
  defaultSort: sortBy("createdAt", "DESC"),
@@ -24,6 +24,12 @@ export interface AdminOrderEditorViewProps {
24
24
  paymentUpiMismatch?: boolean;
25
25
  buyerMarkedPaid?: boolean;
26
26
  buyerFraudAgreementAccepted?: boolean;
27
+ /**
28
+ * Which way a prior review went, if any. Without this the panel cannot tell
29
+ * "awaiting verification" from "already rejected" and re-offers live
30
+ * Verify/Reject buttons on a decided order.
31
+ */
32
+ paymentReviewOutcome?: string;
27
33
  /**
28
34
  * Paid add-ons + applied coupon for this order. Operational, not decorative:
29
35
  * `whatsappNotifyAddon` is who a status change should message, and the
@@ -31,4 +37,4 @@ export interface AdminOrderEditorViewProps {
31
37
  */
32
38
  addons?: OrderAddonBadgesOrder;
33
39
  }
34
- export declare function AdminOrderEditorView({ open, onClose, orderId, orderLabel, currentStatus, items, paymentProofUrl, paymentTransactionId, paymentMethod, paymentStatus, displayedUpiId, buyerReportedUpiId, paymentUpiMismatch, buyerMarkedPaid, buyerFraudAgreementAccepted, addons, }: AdminOrderEditorViewProps): React.JSX.Element;
40
+ export declare function AdminOrderEditorView({ open, onClose, orderId, orderLabel, currentStatus, items, paymentProofUrl, paymentTransactionId, paymentMethod, paymentStatus, displayedUpiId, buyerReportedUpiId, paymentUpiMismatch, buyerMarkedPaid, buyerFraudAgreementAccepted, paymentReviewOutcome, addons, }: AdminOrderEditorViewProps): React.JSX.Element;
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { OrderAddonBadges } from "../../orders/components/OrderAddonBadges";
4
+ import { isManualPaymentMethod } from "../../orders/constants/payment-window";
4
5
  import { useApiMutation } from "@mohasinac/appkit/client";
5
6
  import React, { useState } from "react";
6
7
  import { normalizeError } from "../../../errors/normalize";
@@ -30,7 +31,7 @@ const CARRIER_OPTIONS = [
30
31
  { label: "Other", value: "Other" },
31
32
  ];
32
33
  // --- Component ---------------------------------------------------------------
33
- export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, currentStatus, items, paymentProofUrl, paymentTransactionId, paymentMethod, paymentStatus, displayedUpiId, buyerReportedUpiId, paymentUpiMismatch, buyerMarkedPaid, buyerFraudAgreementAccepted, addons, }) {
34
+ export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, currentStatus, items, paymentProofUrl, paymentTransactionId, paymentMethod, paymentStatus, displayedUpiId, buyerReportedUpiId, paymentUpiMismatch, buyerMarkedPaid, buyerFraudAgreementAccepted, paymentReviewOutcome, addons, }) {
34
35
  const queryClient = useQueryClient();
35
36
  const { showToast } = useToast();
36
37
  const [status, setStatus] = React.useState(currentStatus ?? "pending");
@@ -42,8 +43,19 @@ export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, curre
42
43
  const [reviewNote, setReviewNote] = React.useState("");
43
44
  const [isRequestingReupload, setIsRequestingReupload] = useState(false);
44
45
  const [isRejectingFraud, setIsRejectingFraud] = useState(false);
45
- const isCashOrUpi = paymentMethod === "cash" || paymentMethod === "upi_manual";
46
- const needsVerification = isCashOrUpi && paymentStatus === "pending";
46
+ // This used to inline `paymentMethod === "cash" || === "upi_manual"`, which
47
+ // omitted `emi` so the whole proof panel never rendered for an EMI order and
48
+ // an admin had no way to verify one, even though the list row said "Awaiting
49
+ // verification". Use the shared predicate, which is the single source of truth
50
+ // for the manual-payment set.
51
+ const isManualPayment = isManualPaymentMethod(paymentMethod ?? "");
52
+ const isVerified = paymentStatus === "paid";
53
+ const isRejected = paymentReviewOutcome === "rejected_fraud";
54
+ const isReuploadRequested = paymentReviewOutcome === "reupload_requested";
55
+ // Only an undecided, unpaid manual order is actionable. Gating on
56
+ // `paymentStatus === "pending"` alone re-offered live Verify/Reject buttons on
57
+ // an order that had already been rejected or sent back for re-upload.
58
+ const needsVerification = isManualPayment && !isVerified && !isRejected && Boolean(paymentProofUrl);
47
59
  React.useEffect(() => {
48
60
  if (open) {
49
61
  setStatus(currentStatus ?? "pending");
@@ -137,5 +149,5 @@ export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, curre
137
149
  return (_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: orderLabel ? `Order: ${orderLabel}` : "Update Order", children: _jsxs(Form, { onSubmit: (e) => {
138
150
  e.preventDefault();
139
151
  saveMutation.mutate();
140
- }, spacing: "md", padding: "md", children: [addons && _jsx(OrderAddonBadges, { order: addons, variant: "detail" }), items && items.length > 0 && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Label, { size: "sm", weight: "medium", color: "primary", children: ["Items (", items.length, ")"] }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: items.map((item, i) => (_jsxs(Div, { layout: "flex", align: "center", gap: "3", padding: "sm", children: [_jsx(Div, { className: "h-10 w-10 shrink-0", rounded: "md", overflow: "hidden", children: _jsx(MediaImage, { src: item.image, alt: item.title, size: "thumbnail" }) }), _jsxs(Div, { className: "min-w-0 flex-1", children: [_jsx(Text, { size: "sm", className: "truncate", weight: "medium", children: item.title }), _jsxs(Text, { size: "xs", color: "muted", children: ["Qty: ", item.quantity, " \u00D7 ", toCurrency(item.unitPrice)] })] }), _jsx(Text, { size: "sm", className: "shrink-0", weight: "semibold", children: toCurrency(item.totalPrice) })] }, item.productId || i))) })] })), _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" })] }), (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(Div, { border: "default", rounded: "lg", overflow: "hidden", children: _jsx(MediaImage, { src: paymentProofUrl, alt: "Payment screenshot", size: "card" }) }), paymentTransactionId && (_jsxs(Text, { size: "xs", color: "muted", children: ["UTR: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: paymentTransactionId })] })), needsVerification && (_jsxs(Stack, { gap: "xs", children: [(displayedUpiId || buyerReportedUpiId) && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "xs", color: "muted", children: ["Expected UPI: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: displayedUpiId || "—" }), " · ", "Buyer reported: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerReportedUpiId || "—" })] }), paymentUpiMismatch && (_jsx(Div, { rounded: "lg", padding: "inlineSm", className: "border border-error/20", surface: "danger-surface", children: _jsx(Text, { size: "xs", className: "text-error", weight: "semibold", children: "UPI mismatch \u2014 buyer-reported ID doesn't match the one shown for this order." }) }))] })), _jsxs(Text, { size: "xs", color: "muted", children: ["Buyer marked as paid: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerMarkedPaid ? "Yes" : "No" }), " · ", "Fraud agreement accepted: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerFraudAgreementAccepted ? "Yes" : "No" })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Review note (required for re-upload / reject)" }), _jsx(Textarea, { value: reviewNote, onChange: (e) => setReviewNote(e.target.value), rows: 2, placeholder: "e.g. Screenshot is blurry \u2014 amount not readable / UPI ID doesn't match order" })] }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["verify-payment"], onClick: handleVerifyPayment, isLoading: isVerifyingPayment, disabled: isVerifyingPayment, variant: "primary", className: "w-full" }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["request-payment-reupload"], onClick: handleRequestReupload, isLoading: isRequestingReupload, disabled: isRequestingReupload || !reviewNote.trim(), variant: "secondary", className: "w-full" }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["reject-payment-fraud"], onClick: handleRejectFraud, isLoading: isRejectingFraud, disabled: isRejectingFraud || !reviewNote.trim(), variant: "danger", className: "w-full" })] })), !needsVerification && paymentStatus === "paid" && (_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" })] })] }) }));
152
+ }, spacing: "md", padding: "md", children: [addons && _jsx(OrderAddonBadges, { order: addons, variant: "detail" }), items && items.length > 0 && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Label, { size: "sm", weight: "medium", color: "primary", children: ["Items (", items.length, ")"] }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: items.map((item, i) => (_jsxs(Div, { layout: "flex", align: "center", gap: "3", padding: "sm", children: [_jsx(Div, { className: "h-10 w-10 shrink-0", rounded: "md", overflow: "hidden", children: _jsx(MediaImage, { src: item.image, alt: item.title, size: "thumbnail" }) }), _jsxs(Div, { className: "min-w-0 flex-1", children: [_jsx(Text, { size: "sm", className: "truncate", weight: "medium", children: item.title }), _jsxs(Text, { size: "xs", color: "muted", children: ["Qty: ", item.quantity, " \u00D7 ", toCurrency(item.unitPrice)] })] }), _jsx(Text, { size: "sm", className: "shrink-0", weight: "semibold", children: toCurrency(item.totalPrice) })] }, item.productId || i))) })] })), _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" })] }), (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" })), isManualPayment && (_jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Payment Proof" }), isRejected && (_jsx(Text, { size: "xs", className: "text-error", weight: "medium", children: "Payment rejected as fraud \u2014 no further action available here." })), isReuploadRequested && !isVerified && (_jsx(Text, { size: "xs", className: "text-warning", weight: "medium", children: "Re-upload requested \u2014 waiting for the buyer to submit a new proof." })), paymentProofUrl ? (_jsxs(Stack, { gap: "xs", children: [_jsx(Div, { border: "default", rounded: "lg", overflow: "hidden", children: _jsx(MediaImage, { src: paymentProofUrl, alt: "Payment screenshot", size: "card" }) }), paymentTransactionId && (_jsxs(Text, { size: "xs", color: "muted", children: ["UTR: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: paymentTransactionId })] })), needsVerification && (_jsxs(Stack, { gap: "xs", children: [(displayedUpiId || buyerReportedUpiId) && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "xs", color: "muted", children: ["Expected UPI: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: displayedUpiId || "—" }), " · ", "Buyer reported: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerReportedUpiId || "—" })] }), paymentUpiMismatch && (_jsx(Div, { rounded: "lg", padding: "inlineSm", className: "border border-error/20", surface: "danger-surface", children: _jsx(Text, { size: "xs", className: "text-error", weight: "semibold", children: "UPI mismatch \u2014 buyer-reported ID doesn't match the one shown for this order." }) }))] })), _jsxs(Text, { size: "xs", color: "muted", children: ["Buyer marked as paid: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerMarkedPaid ? "Yes" : "No" }), " · ", "Fraud agreement accepted: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerFraudAgreementAccepted ? "Yes" : "No" })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Review note (required for re-upload / reject)" }), _jsx(Textarea, { value: reviewNote, onChange: (e) => setReviewNote(e.target.value), rows: 2, placeholder: "e.g. Screenshot is blurry \u2014 amount not readable / UPI ID doesn't match order" })] }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["verify-payment"], onClick: handleVerifyPayment, isLoading: isVerifyingPayment, disabled: isVerifyingPayment, variant: "primary", className: "w-full" }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["request-payment-reupload"], onClick: handleRequestReupload, isLoading: isRequestingReupload, disabled: isRequestingReupload || !reviewNote.trim(), variant: "secondary", className: "w-full" }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["reject-payment-fraud"], onClick: handleRejectFraud, isLoading: isRejectingFraud, disabled: isRejectingFraud || !reviewNote.trim(), variant: "danger", className: "w-full" })] })), isVerified && (_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" })] })] }) }));
141
153
  }
@@ -122,6 +122,7 @@ export function AdminOrdersView({ children, ...props }) {
122
122
  paymentUpiMismatch: Boolean(item.paymentUpiMismatch),
123
123
  buyerMarkedPaid: Boolean(item.buyerMarkedPaid),
124
124
  buyerFraudAgreementAccepted: Boolean(item.buyerFraudAgreementAccepted),
125
+ paymentReviewOutcome: toStringValue(item.paymentReviewOutcome, "") || undefined,
125
126
  // Straight off the order document — the admin list route returns
126
127
  // raw docs, so these need no serializer entry (cf. Root Cause #38).
127
128
  addons: {
@@ -211,5 +212,5 @@ export function AdminOrdersView({ children, ...props }) {
211
212
  ] })),
212
213
  renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsxs(_Fragment, { children: [_jsx(FilterChipGroup, { label: "Status", tabs: ADMIN_ORDER_STATUS_TABS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id, paymentReview: "" })) }), _jsx(FilterChipGroup, { label: "Manual payment", tabs: ADMIN_ORDER_PAYMENT_REVIEW_TABS, value: pendingFilters.paymentReview ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, paymentReview: id, status: "" })) })] })),
213
214
  };
214
- return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(AdminOrderEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), orderId: selectedRow?.id, orderLabel: selectedRow?.primary, currentStatus: selectedRow?.status, items: selectedRow?.items, paymentProofUrl: selectedRow?.paymentProofUrl, paymentTransactionId: selectedRow?.paymentTransactionId, paymentMethod: selectedRow?.paymentMethod, paymentStatus: selectedRow?.paymentStatus, displayedUpiId: selectedRow?.displayedUpiId, buyerReportedUpiId: selectedRow?.buyerReportedUpiId, paymentUpiMismatch: selectedRow?.paymentUpiMismatch, buyerMarkedPaid: selectedRow?.buyerMarkedPaid, buyerFraudAgreementAccepted: selectedRow?.buyerFraudAgreementAccepted, addons: selectedRow?.addons })] }));
215
+ return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(AdminOrderEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), orderId: selectedRow?.id, orderLabel: selectedRow?.primary, currentStatus: selectedRow?.status, items: selectedRow?.items, paymentProofUrl: selectedRow?.paymentProofUrl, paymentTransactionId: selectedRow?.paymentTransactionId, paymentMethod: selectedRow?.paymentMethod, paymentStatus: selectedRow?.paymentStatus, displayedUpiId: selectedRow?.displayedUpiId, buyerReportedUpiId: selectedRow?.buyerReportedUpiId, paymentUpiMismatch: selectedRow?.paymentUpiMismatch, buyerMarkedPaid: selectedRow?.buyerMarkedPaid, buyerFraudAgreementAccepted: selectedRow?.buyerFraudAgreementAccepted, paymentReviewOutcome: selectedRow?.paymentReviewOutcome, addons: selectedRow?.addons })] }));
215
216
  }