368 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			368 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
import _ from 'lodash';
 | 
						|
import { pushPrice, updateBid } from '../../system/apis/bid.js';
 | 
						|
import { sendMessage } from '../../system/apis/notification.js';
 | 
						|
import configs from '../../system/config.js';
 | 
						|
import { delay, extractPriceNumber, isTimeReached, removeFalsyValues } from '../../system/utils.js';
 | 
						|
import { ProductBid } from '../product-bid.js';
 | 
						|
 | 
						|
export class LawsonsProductBid 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;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    async getReversePrice() {
 | 
						|
        try {
 | 
						|
            if (!this.page_context) return null;
 | 
						|
 | 
						|
            await this.page_context.waitForSelector('.select-dropdown-value.text-truncate', { timeout: 4000 });
 | 
						|
            const price = await this.page_context.evaluate(() => {
 | 
						|
                const el = document.querySelector('.select-dropdown-value.text-truncate');
 | 
						|
                return el ? el.innerText : null;
 | 
						|
            });
 | 
						|
 | 
						|
            return price ? extractPriceNumber(price) : null;
 | 
						|
        } catch (error) {
 | 
						|
            console.log(error.message);
 | 
						|
            return null;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    update = async () => {
 | 
						|
        try {
 | 
						|
            if (!this.page_context) return;
 | 
						|
 | 
						|
            // if (this.updated_at) {
 | 
						|
            //     await this.page_context.reload({ waitUntil: 'networkidle0' });
 | 
						|
            // }
 | 
						|
 | 
						|
            const result = await this.waitApiInfo();
 | 
						|
 | 
						|
            const reservePrice = await this.getReversePrice();
 | 
						|
 | 
						|
            console.log({ reservePrice });
 | 
						|
            if (!result) return;
 | 
						|
 | 
						|
            // 📌 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(result?.itemView.lotId) || null,
 | 
						|
                    reserve_price: reservePrice,
 | 
						|
                    current_price: result?.currentBidAmount || null,
 | 
						|
                    close_time: new Date(result.endTime).toUTCString() || null,
 | 
						|
                    // close_time: this.close_time ? null : new Date(Date.now() + 5 * 60 * 1000).toUTCString(), //test
 | 
						|
                    name: result?.itemView?.title || 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);
 | 
						|
        } catch (error) {
 | 
						|
            console.log('Error Update', error.message);
 | 
						|
        }
 | 
						|
    };
 | 
						|
 | 
						|
    // Hàm con để fetch trong context trình duyệt
 | 
						|
    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);
 | 
						|
    };
 | 
						|
 | 
						|
    submitBid() {
 | 
						|
        return new Promise(async (resolve, reject) => {
 | 
						|
            if (!this.page_context || !this.model) {
 | 
						|
                console.log(`[${this.id}] Page context or model is missing.`);
 | 
						|
                reject(null);
 | 
						|
                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({
 | 
						|
                                bidAmount,
 | 
						|
                                lotRef,
 | 
						|
                                v2: true,
 | 
						|
                            }),
 | 
						|
                        });
 | 
						|
 | 
						|
                        if (!response.ok) {
 | 
						|
                            throw new Error(`HTTP ${response.status}`);
 | 
						|
                        }
 | 
						|
 | 
						|
                        return await response.json();
 | 
						|
                    },
 | 
						|
                    this.max_price,
 | 
						|
                    this.model,
 | 
						|
                    configs.WEB_CONFIGS.LAWSONS.API_CHECKOUT,
 | 
						|
                );
 | 
						|
 | 
						|
                console.log('🧾 API Bid Result:', {
 | 
						|
                    bid_amount: this.max_price,
 | 
						|
                    result,
 | 
						|
                });
 | 
						|
 | 
						|
                if (!result?.data?.orderBidResponse?.success) reject(null);
 | 
						|
 | 
						|
                resolve(result);
 | 
						|
            } catch (err) {
 | 
						|
                console.log(`[${this.id}] Failed to submit bid: ${err.message}`);
 | 
						|
                reject(null);
 | 
						|
            }
 | 
						|
        });
 | 
						|
    }
 | 
						|
 | 
						|
    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.waitApiInfo();
 | 
						|
 | 
						|
            // Lấy giá reserve price để kiểm tra
 | 
						|
            const reservePrice = await this.getReversePrice();
 | 
						|
 | 
						|
            // Kiểm tra nếu có lý do nào khiến không thể tiếp tục đấu giá
 | 
						|
            const shouldStop =
 | 
						|
                !response ||
 | 
						|
                response?.currentBidAmount > this.max_price + this.plus_price ||
 | 
						|
                response.isOutBid != true ||
 | 
						|
                !reservePrice ||
 | 
						|
                reservePrice > this.max_price + this.plus_price;
 | 
						|
 | 
						|
            if (shouldStop) {
 | 
						|
                console.log(`⚠️ [${this.id}] Stop bidding:`, { reservePrice, 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}] ================`);
 | 
						|
 | 
						|
            await delay(20000);
 | 
						|
 | 
						|
            // 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ả
 | 
						|
            const result = await this.submitBid();
 | 
						|
 | 
						|
            // Nếu không có kết quả (lỗi khi gửi bid) thì dừng lại
 | 
						|
            if (!result) return;
 | 
						|
 | 
						|
            console.log({ result });
 | 
						|
 | 
						|
            // Gửi thông báo đã đấu giá thành công
 | 
						|
            sendMessage(this);
 | 
						|
 | 
						|
            await this.page_context.reload({ waitUntil: 'networkidle0' });
 | 
						|
 | 
						|
            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(`🚨 [${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 waitApiInfo() {
 | 
						|
        if (!this.page_context) {
 | 
						|
            console.error(`❌ [${this.id}] Error: page_context is undefined.`);
 | 
						|
            return null;
 | 
						|
        }
 | 
						|
 | 
						|
        const infoUrl = configs.WEB_CONFIGS.LAWSONS.API_DETAIL_INFO(this.model);
 | 
						|
        const detailUrl = configs.WEB_CONFIGS.LAWSONS.API_DETAIL_PRODUCT(this.model);
 | 
						|
 | 
						|
        const [info, detailData] = await Promise.all([this.fetchFromPage(infoUrl), this.fetchFromPage(detailUrl)]);
 | 
						|
 | 
						|
        return { ...info, ...detailData };
 | 
						|
    }
 | 
						|
 | 
						|
    async trackingOutbid() {
 | 
						|
        if (!this.page_context) return;
 | 
						|
 | 
						|
        try {
 | 
						|
            const onResponse = async (response) => {
 | 
						|
                const url = response?.request()?.url();
 | 
						|
                if (!url || !url.includes(configs.WEB_CONFIGS.LAWSONS.API_DETAIL_INFO(this.model))) {
 | 
						|
                    return;
 | 
						|
                }
 | 
						|
 | 
						|
                try {
 | 
						|
                    const result = await response.json();
 | 
						|
 | 
						|
                    if (!result) return;
 | 
						|
 | 
						|
                    console.log(`📈 [${this.id}] Bid data: `, result);
 | 
						|
 | 
						|
                    const { maxBidAmount, currentBidAmount, isOutBid } = result;
 | 
						|
 | 
						|
                    console.log(`📊 [${this.id}] API Info - maxBidAmount: ${maxBidAmount}, currentBidAmount: ${currentBidAmount}, isOutBid: ${isOutBid}`);
 | 
						|
 | 
						|
                    // Lấy giá reverse (giá thấp nhất cần để thắng đấu giá)
 | 
						|
                    const reversePrice = await this.getReversePrice();
 | 
						|
                    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: 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 && isOutBid && currentBidAmount <= this.max_price + this.plus_price && this.max_price != maxBidAmount) {
 | 
						|
                        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.`);
 | 
						|
                    }
 | 
						|
 | 
						|
                    if (new Date(this.updated_at).getTime() > Date.now() - 120 * 1000) {
 | 
						|
                        await this.page_context.reload({ waitUntil: 'networkidle0' });
 | 
						|
                    }
 | 
						|
                } catch (error) {
 | 
						|
                    console.error(`🚨 [${this.id}] Error parsing API response:`, error);
 | 
						|
                }
 | 
						|
            };
 | 
						|
 | 
						|
            console.log(`🔄 [${this.id}] Removing previous response listeners...`);
 | 
						|
            this.page_context.off('response', onResponse);
 | 
						|
 | 
						|
            console.log(`📡 [${this.id}] Attaching new response listener...`);
 | 
						|
            this.page_context.on('response', onResponse);
 | 
						|
 | 
						|
            console.log(`✅ [${this.id}] Navigation setup complete.`);
 | 
						|
        } catch (error) {
 | 
						|
            console.error(`❌ [${this.id}] Error during navigation:`, 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}`);
 | 
						|
        }
 | 
						|
    };
 | 
						|
}
 |