439 lines
14 KiB
JavaScript
439 lines
14 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(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) 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}`);
|
|
}
|
|
};
|
|
}
|