@mohasinac/appkit 4.12.0 → 4.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/server/features/auctions/service.d.ts +7 -1
- package/dist/_internal/server/features/auctions/service.js +13 -5
- package/dist/_internal/server/features/checkout/actions.d.ts +0 -15
- package/dist/_internal/server/features/checkout/actions.js +7 -3
- package/dist/_internal/server/features/orders/actions.js +5 -3
- package/dist/_internal/server/functions/firestore.d.ts +1 -2
- package/dist/_internal/server/functions/firestore.js +1 -9
- package/dist/_internal/server/jobs/core/index.d.ts +0 -1
- package/dist/_internal/server/jobs/core/index.js +0 -1
- package/dist/_internal/server/jobs/core/offerExpiry.js +33 -1
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -2
- package/dist/_internal/server/jobs/handlers/index.js +1 -2
- package/dist/_internal/shared/features/auctions/config.d.ts +34 -0
- package/dist/_internal/shared/features/auctions/config.js +47 -3
- package/dist/_internal/shared/listing-types/auction/config.js +1 -1
- package/dist/_internal/shared/listing-types/digital-code/config.js +1 -1
- package/dist/_internal/shared/listing-types/live/config.js +1 -1
- package/dist/_internal/shared/listing-types/pre-order/config.js +1 -1
- package/dist/features/account/components/NotificationBell.js +1 -1
- package/dist/features/admin/components/AdminNotificationsView.js +4 -1
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +7 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +16 -4
- package/dist/features/admin/components/AdminOrdersView.js +2 -1
- package/dist/features/auctions/actions/bid-actions.js +71 -9
- package/dist/features/auctions/components/AuctionDetailPageView.js +8 -2
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -3
- package/dist/features/auctions/components/PlaceBidFormClient.js +89 -14
- package/dist/features/auctions/schemas/bid-input.d.ts +19 -6
- package/dist/features/auctions/schemas/bid-input.js +19 -7
- package/dist/features/auth/repository/user.repository.js +6 -0
- package/dist/features/faq/components/FAQHelpfulButtons.js +2 -2
- package/dist/features/filters/FilterFacetSection.js +1 -1
- package/dist/features/homepage/components/WhatsAppCommunitySection.js +1 -1
- package/dist/features/layout/BackToTop.js +1 -1
- package/dist/features/layout/BottomActions.js +1 -1
- package/dist/features/layout/FooterLayout.js +2 -2
- package/dist/features/layout/TitleBarLayout.js +1 -1
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -3
- package/dist/features/products/components/MarketplaceBundleCard.js +2 -2
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +2 -2
- package/dist/features/products/components/PrizeDrawCollage.js +1 -1
- package/dist/features/products/components/PrizeDrawItemsEditor.js +1 -1
- package/dist/features/products/components/ProductDetailPageView.js +1 -1
- package/dist/features/products/components/ProductGrid.js +3 -3
- package/dist/features/products/repository/products.repository.d.ts +0 -4
- package/dist/features/products/repository/products.repository.js +0 -15
- package/dist/features/promotions/repository/coupons.repository.d.ts +12 -0
- package/dist/features/promotions/repository/coupons.repository.js +12 -0
- package/dist/features/seller/components/SellerSidebar.js +1 -1
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +273 -0
- package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +1 -1
- package/dist/jobs.d.ts +1 -1
- package/dist/jobs.js +1 -1
- package/dist/seed/homepage-sections-seed-data.js +5 -1
- package/dist/seed/site-settings-seed-data.js +18 -0
- package/dist/styles.css +55 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/color-pairs.js +1 -0
- package/dist/tokens/tokens.css +33 -0
- package/dist/ui/components/Button.style.css +21 -0
- package/dist/ui/components/ImageLightbox.js +7 -4
- package/dist/ui/components/surface-tokens.d.ts +7 -0
- package/dist/ui/components/surface-tokens.js +7 -0
- package/package.json +1 -1
- package/dist/_internal/server/jobs/core/onBidPlaced.d.ts +0 -15
- package/dist/_internal/server/jobs/core/onBidPlaced.js +0 -63
- package/dist/_internal/server/jobs/handlers/onBidPlaced.d.ts +0 -3
- 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
|
-
/**
|
|
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,
|
|
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
|
-
/**
|
|
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
|
|
22
|
-
const
|
|
23
|
-
|
|
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
|
-
|
|
1143
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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").
|
|
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 {
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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" })),
|
|
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
|
}
|