/** * Pattern configuration for the unified auction bidding screen/card. * * Forward and Reverse bidding were 90%-duplicated screens that drifted apart. * Every behavioral difference between them is captured HERE, explicitly, so * the screen and card contain zero `if (pattern === ...)` logic and a bidding * fix can never again land in one pattern but not the other. * * The stepper is modelled as a bounded number line per pattern: * - Forward: bid UP. floor = current bid (never undercut it), no ceiling. * - Reverse: bid DOWN. ceiling = current bid (never exceed it), floor = EMD. * `stepUp`/`stepDown` return the next value already clamped to [floor, ceil], * and `canIncrease`/`canDecrease` are derived from them, so the buttons can * never overshoot a bound and can never be "enabled but do nothing". */ import { FORWARD_AUCTION_ITEM, REVERSE_AUCTION_ITEM } from '../../Services/ApiCall/ApiUrls'; export type AuctionPattern = 'Forward' | 'Reverse'; export type BiddingFlags = { oneTimeBid: boolean; rebid: boolean; }; export type BiddingPatternConfig = { pattern: AuctionPattern; /** Endpoint returning this pattern's auction items. */ itemsUrl: string; /** Opening amount — the backend uses a different field per pattern. */ startValue: (item: any) => number; /** Stepper step size — always a positive number (see resolveStep). */ step: (item: any) => number; stepLabel: 'Increment' | 'Decrement'; stepPrefix: '+' | '-'; /** Lowest value the working bid may reach. */ floor: (item: any) => number; /** Highest value the working bid may reach. */ ceil: (item: any) => number; /** Next value after pressing +/-, already clamped into [floor, ceil]. */ stepUp: (bidValue: number, item: any) => number; stepDown: (bidValue: number, item: any) => number; /** True only when the corresponding button would actually move the value. */ canIncrease: (bidValue: number, item: any) => boolean; canDecrease: (bidValue: number, item: any) => boolean; /** Gate applied while typing a manual (one-time/rebid) amount. */ acceptManualInput: (value: number, item: any) => boolean; /** Submit-time validation for manual bids; returns an error message or null. */ validateManualBid: (value: number, item: any) => string | null; /** Reverse renders the manual input inline within the stepper row. */ inlineManualInput: boolean; /** Whether the collapsible body opens at full height. */ isBodyFull: (flags: BiddingFlags, item: any) => boolean; /** Whether the Confirm Bid button is rendered. */ showSubmit: (flags: BiddingFlags, item: any) => boolean; }; /** * Amounts arrive from the backend as strings (e.g. auctionFees: "400"), so any * bare `>`/`<`/`+` against them silently string-compares or concatenates. Every * numeric read below goes through `num` so steps, bounds and comparisons are * always real arithmetic; a missing/non-numeric field degrades to 0. */ const num = (v: any): number => { const n = Number(v); return Number.isFinite(n) ? n : 0; }; /** First positive numeric value among the given keys, else 0. */ const firstPositive = (item: any, keys: string[]): number => { for (const k of keys) { const n = num(item?.[k]); if (n > 0) return n; } return 0; }; // The bid-variation amount can arrive under any of these keys depending on the // endpoint; whichever is present and positive is the authoritative step. const STEP_KEYS = [ 'bidVariationValue', 'bidVariation', 'variationValue', 'bidVariationFigure', 'incrementValue', 'decrementValue', ]; /** * The +/- step for an item: * - the configured bid-variation amount when the backend provides one, * - otherwise a flat ₹1, so a misconfigured auction (variation 0/absent) can * never leave the stepper frozen and the user can still nudge the bid. */ const resolveStep = (item: any): number => { const configured = firstPositive(item, STEP_KEYS); return configured > 0 ? configured : 1; }; const clamp = (v: number, lo: number, hi: number): number => Math.min(Math.max(v, lo), hi); const forward: BiddingPatternConfig = { pattern: 'Forward', itemsUrl: FORWARD_AUCTION_ITEM, startValue: item => num(item?.auctionStartValueFigure), step: item => resolveStep(item), stepLabel: 'Increment', stepPrefix: '+', // Forward: outbid upward. Never below the current bid; no upper limit. floor: item => num(item?.bidAmount), ceil: () => Number.POSITIVE_INFINITY, stepUp: (bidValue, item) => clamp(num(bidValue) + forward.step(item), forward.floor(item), forward.ceil(item)), stepDown: (bidValue, item) => clamp(num(bidValue) - forward.step(item), forward.floor(item), forward.ceil(item)), canIncrease: (bidValue, item) => forward.stepUp(bidValue, item) > num(bidValue), canDecrease: (bidValue, item) => forward.stepDown(bidValue, item) < num(bidValue), acceptManualInput: () => true, validateManualBid: (value, item) => num(value) < num(item?.bidAmount) ? 'Enter a value greater than the current bid value' : null, inlineManualInput: false, isBodyFull: (flags, item) => !flags.rebid && item.contractorId == undefined, showSubmit: (flags, item) => !flags.rebid && item.contractorId == undefined, }; const reverse: BiddingPatternConfig = { pattern: 'Reverse', itemsUrl: REVERSE_AUCTION_ITEM, startValue: item => num(item?.auctionStartValue), step: item => resolveStep(item), stepLabel: 'Decrement', stepPrefix: '-', // Reverse: undercut downward. Never above the current bid; never below EMD. floor: item => num(item?.emdAmount), ceil: item => num(item?.bidAmount) || num(item?.auctionStartValue), stepUp: (bidValue, item) => clamp(num(bidValue) + reverse.step(item), reverse.floor(item), reverse.ceil(item)), stepDown: (bidValue, item) => clamp(num(bidValue) - reverse.step(item), reverse.floor(item), reverse.ceil(item)), canIncrease: (bidValue, item) => reverse.stepUp(bidValue, item) > num(bidValue), canDecrease: (bidValue, item) => reverse.stepDown(bidValue, item) < num(bidValue), acceptManualInput: (value, item) => num(value) >= 0 && num(value) <= num(item?.auctionStartValue), validateManualBid: () => null, inlineManualInput: true, isBodyFull: () => true, showSubmit: () => true, }; export const getBiddingConfig = (pattern: string | undefined): BiddingPatternConfig => pattern === 'Reverse' ? reverse : forward;