480 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			480 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
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";
 | 
						|
 | 
						|
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);
 | 
						|
    }
 | 
						|
  };
 | 
						|
 | 
						|
  submitBid() {
 | 
						|
    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ả
 | 
						|
      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 || !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;
 | 
						|
      }
 | 
						|
 | 
						|
      console.log({ result });
 | 
						|
 | 
						|
      // 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(`🚨 [${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;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  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}`);
 | 
						|
    }
 | 
						|
  };
 | 
						|
}
 |