import _ from "lodash"; import { pushPrice, updateBid } from "../../system/apis/bid.js"; import configs from "../../system/config.js"; import { delay, isTimeReached, removeFalsyValues } from "../../system/utils.js"; import { ProductBid } from "../product-bid.js"; import { sendMessage } from "../../system/apis/notification.js"; import axios from "../../system/axios.js"; export class PicklesProductBid extends ProductBid { constructor({ ...prev }) { super(prev); } async handleUpdateBid({ lot_id, close_time, name, current_price, reserve_price, }) { const response = await updateBid(this.id, { lot_id, close_time, name, current_price, reserve_price: Number(reserve_price) || 0, }); if (response) { this.lot_id = response.lot_id; this.close_time = response.close_time; this.start_bid_time = response.start_bid_time; } } fetchFromPage = async (url) => { return await this.page_context.evaluate(async (url) => { try { const res = await fetch(url, { method: "GET", headers: { "Content-Type": "application/json", }, }); return await res.json(); } catch (err) { return { error: err.message }; } }, url); }; detailData = async () => { return await this.fetchFromPage( configs.WEB_CONFIGS.PICKLES.API_DETAIL_PRODUCT(this.model) ); }; getName = async () => { if (!this.page_context) return null; try { return await this.page_context.$eval( "#pd-ph-header > div:first-child > div > div:nth-child(2) > div > h1", (el) => el.textContent ); } catch (error) { console.log( "%cmodels/pickles.com.au/pickles-product-bid.js:60 error.message", "color: #007acc;", error.message ); return null; } }; update = async () => { try { if (!this.page_context) return; const result = await this.detailData(); if (!result || !result[0]) return; const { item, bidding } = result[0]; const name = await this.getName(); console.log( "%cmodels/pickles.com.au/pickles-product-bid.js:83 item", "color: #007acc;", bidding ); // 📌 Loại bỏ các giá trị không hợp lệ và bổ sung thông tin cần thiết const data = removeFalsyValues( { lot_id: String(item?.id) || null, reserve_price: bidding?.minimumBidAmount || null, current_price: bidding?.currentActualBid || null, close_time: new Date(item?.itemBidEndTimestamp).toUTCString() || null, name: name || null, }, ["close_time"] ); console.log(`🚀 [${this.id}] Processed data ready for update`); // 📌 Gửi dữ liệu cập nhật lên hệ thống await this.handleUpdateBid(data); await this.page_context.reload({ waitUntil: "networkidle2" }); await this.page_context.waitForNavigation({ timeout: 8000, waitUntil: "domcontentloaded", }); } catch (error) { console.log("Error Update", error.message); } }; handlePlaceBidLive() { return new Promise(async (resolve, reject) => { if (!this.page_context || !this.lot_id) { console.log(`[${this.id}] Page context or model is missing.`); reject("Context is not define"); return; } try { console.log(`💰 [${this.id}] Prepared Bid Amount: ${this.max_price}`); const result = await this.page_context.evaluate( async (bidAmount, lotRef, url) => { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ itemId: lotRef, bidValues: { activity: "BID", maxBid: bidAmount, // giá trị tối đa của sản phẩm roundedMaxBid: bidAmount, // giá trị tối đa của sản phẩm submittedBuyNowValue: null, }, buyerFeeCalculated: false, buyerFees: null, dashboardRedirectUrl: null, itemTitle: null, productLine: null, registrationRequired: false, totalAmount: null, updateDetailsRequired: null, }), }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); }, this.max_price + this.plus_price, this.lot_id, configs.WEB_CONFIGS.PICKLES.API_CHECKOUT ); console.log("🧾 API Bid Result:", { bid_amount: this.max_price + this.plus_price, result, }); if (!result?.confirmationRequest) reject("Api call failure"); resolve(result); } catch (err) { console.log(`[${this.id}] Failed to submit bid: ${err.message}`); reject(err); } }); } async handlePlaceBid() { // Kiểm tra xem có page context không, nếu không có thì kết thúc quá trình đấu giá if (!this.page_context) { console.log( `⚠️ [${this.id}] No page context found, aborting bid process.` ); return; } const page = this.page_context; // Kiểm tra xem đấu giá đã đang diễn ra chưa. Nếu có thì không thực hiện nữa if (global[`IS_PLACE_BID-${this.id}`]) { console.log(`⚠️ [${this.id}] Bid is already in progress, skipping.`); return; } try { console.log(`🔄 [${this.id}] Starting bid process...`); // Đánh dấu rằng đang thực hiện quá trình đấu giá để tránh đấu lại global[`IS_PLACE_BID-${this.id}`] = true; // Kiểm tra xem giá hiện tại có vượt qua mức giá tối đa chưa if (this.current_price > this.max_price + this.plus_price) { console.log(`⚠️ [${this.id}] Outbid bid`); return; // Nếu giá hiện tại vượt quá mức giá tối đa thì dừng lại } // Kiểm tra thời gian đấu giá if (this.start_bid_time && !isTimeReached(this.start_bid_time)) { console.log( `⏳ [${this.id}] Not yet time to bid. Skipping Product: ${ this.name || "None" }` ); return; // Nếu chưa đến giờ đấu giá thì bỏ qua } // Đợi lấy thông tin API để kiểm tra tình trạng đấu giá hiện tại const response = await this.detailData(); if (!response) { console.log(`[${this.id}] Can't get info data`); return; } const { bidding } = response[0]; console.log( "%cmodels/pickles.com.au/pickles-product-bid.js:157 response", "color: #007acc;", bidding ); // // Kiểm tra nếu có lý do nào khiến không thể tiếp tục đấu giá const shouldStop = !response || bidding?.currentActualBid > this.max_price + this.plus_price || (bidding?.userItemBidStatus && bidding?.userItemBidStatus?.type !== "OUTBID") || !bidding?.minimumBidAmount || bidding.minimumBidAmount > this.max_price + this.plus_price; if (shouldStop) { console.log(`⚠️ [${this.id}] Stop bidding:`, { reservePrice: bidding?.minimumBidAmount, currentBidAmount: response?.currentBidAmount, maxBidAmount: response?.maxBidAmount, }); return; // Nếu gặp điều kiện dừng thì không thực hiện đấu giá } // Tìm bid history lớn nhất từ các lịch sử đấu giá của item const bidHistoriesItem = _.maxBy(this.histories, "price"); console.log(`📜 [${this.id}] Current bid history:`, this.histories); // Kiểm tra xem đã bid rồi chưa. Nếu đã bid rồi thì bỏ qua if ( bidHistoriesItem && bidHistoriesItem?.price == this.current_price && this.max_price + this.plus_price == response?.maxBidAmount ) { console.log( `🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem?.price})` ); return; } if (this.reserve_price <= 0) { console.log(`[${this.reserve_price}]`); return; } console.log( `===============Start call to submit [${this.id}] ================` ); // waiting 2s await delay(2000); // Nếu chưa bid, thực hiện đặt giá console.log( `💰 [${this.id}] Placing a bid with amount: ${this.max_price}` ); // Gửi bid qua API và nhận kết quả let result = null; if (this.isSandbox()) { result = await this.handleCallActionSanbox(); } else { result = await this.handlePlaceBidLive(); } console.log({ result }); // Nếu không có kết quả (lỗi khi gửi bid) thì dừng lại if (!result || !result?.confirmationRequest) { console.log( "%cmodels/pickles.com.au/pickles-product-bid.js:289 Error when call plance bid", "color: #007acc;", "Error when call plance bid" ); return; } // Gửi thông báo đã đấu giá thành công // sendMessage(this); pushPrice({ bid_id: this.id, price: result?.yourBid || 0, }); await this.page_context.evaluate(() => location.reload()); console.log(`✅ [${this.id}] Bid placed successfully!`); } catch (error) { // Nếu có lỗi xảy ra trong quá trình đấu giá, log lại lỗi console.log(error); console.log(`🚨 [${this.id}] Error placing bid: ${error.message}`); } finally { // Đảm bảo luôn reset trạng thái đấu giá sau khi hoàn thành console.log(`🔚 [${this.id}] Resetting bid flag.`); global[`IS_PLACE_BID-${this.id}`] = false; } } async handlePlaceBidSanbox() { return new Promise(async (resolve, reject) => { if (!this.page_context || !this.lot_id) { console.log(`[${this.id}] Page context or model is missing.`); reject("Context is not define"); return; } try { console.log(`💰 [${this.id}] Prepared Bid Amount: ${this.max_price}`); const result = await this.page_context.evaluate( async (lotRef, url) => { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ itemId: lotRef, bidValues: { activity: "BID", maxBid: 0, // giá trị tối đa của sản phẩm roundedMaxBid: 0, // giá trị tối đa của sản phẩm submittedBuyNowValue: null, }, buyerFeeCalculated: false, buyerFees: null, dashboardRedirectUrl: null, itemTitle: null, productLine: null, registrationRequired: false, totalAmount: null, updateDetailsRequired: null, }), }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); }, this.lot_id, configs.WEB_CONFIGS.PICKLES.API_CHECKOUT ); console.log("🧾 API Bid Result:", { bid_amount: this.max_price + this.plus_price, result, }); resolve(result); } catch (err) { console.log(`[${this.id}] Failed to submit bid: ${err.message}`); reject(err); } }); } isOutBid = async () => { try { // Chờ tối đa 10s cho element xuất hiện const element = await page.waitForSelector( '[data-testid="pd-pbsb-bit-status-banner"]', { timeout: 10000, // 10s visible: true, // chỉ accept khi element hiện ra } ); if (!element) return false; // không có thì return false // Lấy innerHTML của element const innerHTML = await page.evaluate((el) => el.innerHTML, element); console.log( "%cmodels/pickles.com.au/pickles-product-bid.js:339 {in}", "color: #007acc;", { innerHTML } ); // Kiểm tra có từ "outbid" không return innerHTML.includes("outbid"); } catch (error) { // Nếu lỗi (timeout hoặc gì đó) => return false return false; } }; async trackingOutbid() { if (!this.page_context) return; if (global[`TRACKING_PROCRESS_${this.id}`]) { console.log(`🔄 [${this.id}] Removing previous response listeners...`); clearInterval(global[`TRACKING_PROCRESS_${this.id}`]); } try { global[`TRACKING_PROCRESS_${this.id}`] = setInterval(async () => { try { const result = await this.detailData(); if (!result) return; console.log(`📈 [${this.id}] Bid data: `, result); const { item, bidding } = result[0]; console.log( `📊 [${this.id}] API Info - minimumBidAmount: ${bidding?.minimumBidAmount}, currentActualBid: ${bidding?.currentActualBid}` ); // Lấy giá reverse (giá thấp nhất cần để thắng đấu giá) const reversePrice = bidding?.currentActualBid; const currentBidAmount = bidding?.currentActualBid; const maxBidAmount = bidding?.buyerCurrentBid?.maximumBid; console.log(`💰 [${this.id}] Current reverse price: ${reversePrice}`); // Tìm ra lịch sử đấu giá có giá cao nhất trong this.histories const bidHistoriesItem = _.maxBy(this.histories, "price"); console.log( `📈 [${this.id}] Highest local bid: ${ bidHistoriesItem?.price ?? "N/A" }` ); if (!this.close_time || !this.lot_id || !this.current_price) return; // Nếu chưa từng đặt giá và có giá tối đa (maxBidAmount), thì push giá đó vào histories if ( (!bidHistoriesItem && maxBidAmount) || (bidHistoriesItem?.price != currentBidAmount && currentBidAmount == maxBidAmount) ) { console.log( `🆕 [${this.id}] No previous bid found. Placing initial bid at ${maxBidAmount}.` ); pushPrice({ bid_id: this.id, price: Number(currentBidAmount), }); } // Nếu giá hiện tại cao hơn giá mình đã đặt, và reversePrice vẫn trong giới hạn cho phép, và đang bị outbid thì sẽ đặt giá tiếp if ( reversePrice <= this.max_price + this.plus_price && currentBidAmount <= this.max_price + this.plus_price && this.max_price != maxBidAmount && this.histories.length > 0 && bidding?.currentMaximumBid !== this.max_price + this.plus_price ) { console.log( `⚠️ [${this.id}] Outbid detected. Reverse price acceptable. Placing a new bid...` ); await this.handlePlaceBid(); } else { console.log(`✅ [${this.id}] No bid needed. Conditions not met.`); } } catch (error) { console.error(`🚨 [${this.id}] Error parsing API response:`, error); } }, 5000); console.log(`✅ [${this.id}] Navigation setup complete.`); } catch (error) { console.error(`❌ [${this.id}] Error during navigationnnnn:`, error); } } async gotoLink() { const page = this.page_context; if (page.isClosed()) { console.error(`❌ [${this.id}] Page has been closed, cannot navigate.`); return; } console.log(`🔄 [${this.id}] Starting the bidding process...`); try { console.log(`🌐 [${this.id}] Navigating to: ${this.url} ...`); await page.goto(this.url, { waitUntil: "networkidle2" }); console.log(`✅ [${this.id}] Successfully navigated to: ${this.url}`); console.log(`🖥️ [${this.id}] Bringing tab to the foreground...`); await page.bringToFront(); console.log(`🛠️ [${this.id}] Setting custom user agent...`); await page.setUserAgent( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" ); console.log(`🎯 [${this.id}] Listening for API responses...`); // tracking out bid this.trackingOutbid(); } catch (error) { console.error(`❌ [${this.id}] Error during navigation:`, error); } } action = async () => { try { const page = this.page_context; // 📌 Kiểm tra nếu trang chưa tải đúng URL thì điều hướng đến URL mục tiêu if (!page.url() || !page.url().includes(this.url)) { console.log(`🔄 [${this.id}] Navigating to target URL: ${this.url}`); await this.gotoLink(); } await this.handlePlaceBid(); } catch (error) { console.error(`🚨 [${this.id}] Error navigating the page: ${error}`); } }; async handleCallActionSanbox() { try { const result = await this.handlePlaceBidSanbox(); await axios({ url: this.ACTION_URL({ type: "api" }), data: { id: this.id, data: JSON.stringify(result), }, method: "POST", }); if (global[`TRACKING_PROCRESS_${this.id}`]) { clearInterval(global[`TRACKING_PROCRESS_${this.id}`]); } await this.close(); return result; } catch (error) { console.log("a:", error); } } }