Deploy to staing #12
			
				
			
		
		
		
	| 
						 | 
					@ -0,0 +1,36 @@
 | 
				
			||||||
 | 
					import { handleError, handleSuccess } from '.';
 | 
				
			||||||
 | 
					import axios from '../lib/axios';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const BASE_URL = 'dashboards';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const resetTool = async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        const { data } = await axios({
 | 
				
			||||||
 | 
					            url: `${BASE_URL}/reset-tool`,
 | 
				
			||||||
 | 
					            withCredentials: true,
 | 
				
			||||||
 | 
					            method: 'POST',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        handleSuccess(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return data;
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        handleError(error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const shutdownTool = async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        const { data } = await axios({
 | 
				
			||||||
 | 
					            url: `${BASE_URL}/shutdown-tool`,
 | 
				
			||||||
 | 
					            withCredentials: true,
 | 
				
			||||||
 | 
					            method: 'POST',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        handleSuccess(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return data;
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        handleError(error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -696,7 +696,7 @@ const Table = <R extends Record<string, any>>({
 | 
				
			||||||
                                            {chooseOptions?.renderBody ? (
 | 
					                                            {chooseOptions?.renderBody ? (
 | 
				
			||||||
                                                chooseOptions.renderBody(chooses, row, (e) => handleChooseSingle(e, row))
 | 
					                                                chooseOptions.renderBody(chooses, row, (e) => handleChooseSingle(e, row))
 | 
				
			||||||
                                            ) : (
 | 
					                                            ) : (
 | 
				
			||||||
                                                <Box style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
 | 
					                                                <Box onClick={(e) => e.stopPropagation()} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
 | 
				
			||||||
                                                    <Checkbox
 | 
					                                                    <Checkbox
 | 
				
			||||||
                                                        classNames={{
 | 
					                                                        classNames={{
 | 
				
			||||||
                                                            input: 'cursor-pointer',
 | 
					                                                            input: 'cursor-pointer',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,13 @@
 | 
				
			||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
 | 
					/* eslint-disable @typescript-eslint/no-explicit-any */
 | 
				
			||||||
import { Box, Text, Title } from '@mantine/core';
 | 
					import { Box, Button, LoadingOverlay, Text, Title } from '@mantine/core';
 | 
				
			||||||
import { useEffect, useRef, useState } from 'react';
 | 
					import { useEffect, useRef, useState } from 'react';
 | 
				
			||||||
import io from 'socket.io-client';
 | 
					import io from 'socket.io-client';
 | 
				
			||||||
import { WorkingPage } from '../components/dashboard';
 | 
					import { WorkingPage } from '../components/dashboard';
 | 
				
			||||||
import { IBid, IWebBid } from '../system/type';
 | 
					import { IBid, IWebBid } from '../system/type';
 | 
				
			||||||
import { checkStatus } from '../apis/auth';
 | 
					import { checkStatus } from '../apis/auth';
 | 
				
			||||||
 | 
					import { IconPower, IconRestore } from '@tabler/icons-react';
 | 
				
			||||||
 | 
					import { useConfirmStore } from '../lib/zustand/use-confirm';
 | 
				
			||||||
 | 
					import { resetTool, shutdownTool } from '../apis/dashboard';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const socket = io(`${import.meta.env.VITE_SOCKET_URL}/admin-bid-ws`, {
 | 
					const socket = io(`${import.meta.env.VITE_SOCKET_URL}/admin-bid-ws`, {
 | 
				
			||||||
    autoConnect: true,
 | 
					    autoConnect: true,
 | 
				
			||||||
| 
						 | 
					@ -13,6 +16,9 @@ const socket = io(`${import.meta.env.VITE_SOCKET_URL}/admin-bid-ws`, {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function DashBoard() {
 | 
					export default function DashBoard() {
 | 
				
			||||||
    const [workingData, setWorkingData] = useState<(IWebBid & { type: string })[] | (IBid & { type: string })[]>([]);
 | 
					    const [workingData, setWorkingData] = useState<(IWebBid & { type: string })[] | (IBid & { type: string })[]>([]);
 | 
				
			||||||
 | 
					    const { setConfirm } = useConfirmStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const [loading, setLoading] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const RETRY_CONNECT = useRef(2);
 | 
					    const RETRY_CONNECT = useRef(2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -69,12 +75,47 @@ export default function DashBoard() {
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    }, []);
 | 
					    }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleResetTool = () => {
 | 
				
			||||||
 | 
					        setConfirm({
 | 
				
			||||||
 | 
					            handleOk: async () => {
 | 
				
			||||||
 | 
					                setLoading(true);
 | 
				
			||||||
 | 
					                await resetTool();
 | 
				
			||||||
 | 
					                setLoading(false);
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            title: 'Confirm tool reset',
 | 
				
			||||||
 | 
					            message: 'Are you sure you want to reset this tool? All current processes will be stopped and restarted.',
 | 
				
			||||||
 | 
					            okButton: { value: 'Ok', color: 'blue' },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleShutdownTool = () => {
 | 
				
			||||||
 | 
					        setConfirm({
 | 
				
			||||||
 | 
					            handleOk: async () => {
 | 
				
			||||||
 | 
					                setLoading(true);
 | 
				
			||||||
 | 
					                await shutdownTool();
 | 
				
			||||||
 | 
					                setLoading(false);
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            title: 'Confirm tool shutdown',
 | 
				
			||||||
 | 
					            message: 'Are you sure you want to shut down this tool? All running processes will be stopped and the tool will go offline.',
 | 
				
			||||||
 | 
					            okButton: { value: 'Ok', color: 'blue' },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <Box>
 | 
					        <Box>
 | 
				
			||||||
 | 
					            <Box className="flex items-center justify-between">
 | 
				
			||||||
                <Title order={2} mb="md">
 | 
					                <Title order={2} mb="md">
 | 
				
			||||||
                    Admin Dashboard
 | 
					                    Admin Dashboard
 | 
				
			||||||
                </Title>
 | 
					                </Title>
 | 
				
			||||||
 | 
					                <Box className="flex gap-2">
 | 
				
			||||||
 | 
					                    <Button onClick={handleResetTool} leftSection={<IconRestore size={16} />} size="xs">
 | 
				
			||||||
 | 
					                        Reset tool
 | 
				
			||||||
 | 
					                    </Button>
 | 
				
			||||||
 | 
					                    <Button onClick={handleShutdownTool} leftSection={<IconPower size={16} />} color="red" size="xs">
 | 
				
			||||||
 | 
					                        Shutdown tool
 | 
				
			||||||
 | 
					                    </Button>
 | 
				
			||||||
 | 
					                </Box>
 | 
				
			||||||
 | 
					            </Box>
 | 
				
			||||||
            <Box className="grid grid-cols-4 gap-4">
 | 
					            <Box className="grid grid-cols-4 gap-4">
 | 
				
			||||||
                {workingData.length > 0 && workingData.map((item, index) => <WorkingPage socket={socket} data={item} key={item.id + index} />)}
 | 
					                {workingData.length > 0 && workingData.map((item, index) => <WorkingPage socket={socket} data={item} key={item.id + index} />)}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -84,6 +125,8 @@ export default function DashBoard() {
 | 
				
			||||||
                    </Box>
 | 
					                    </Box>
 | 
				
			||||||
                )}
 | 
					                )}
 | 
				
			||||||
            </Box>
 | 
					            </Box>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <LoadingOverlay visible={loading} />
 | 
				
			||||||
        </Box>
 | 
					        </Box>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1 +1 @@
 | 
				
			||||||
{"createdAt":1744420823691}
 | 
					{"createdAt":1744861741554}
 | 
				
			||||||
| 
						 | 
					@ -25,6 +25,8 @@ import { ImapService } from './services/mail/imap.service';
 | 
				
			||||||
import { OutBidLogsService } from './services/out-bid-logs.service';
 | 
					import { OutBidLogsService } from './services/out-bid-logs.service';
 | 
				
			||||||
import { SendMessageHistoriesService } from './services/send-message-histories.service';
 | 
					import { SendMessageHistoriesService } from './services/send-message-histories.service';
 | 
				
			||||||
import { WebBidsService } from './services/web-bids.service';
 | 
					import { WebBidsService } from './services/web-bids.service';
 | 
				
			||||||
 | 
					import { DashboardService } from './services/dashboard.service';
 | 
				
			||||||
 | 
					import { AdminDashboardController } from './controllers/admin/admin-dashboard.controller';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
| 
						 | 
					@ -48,6 +50,7 @@ import { WebBidsService } from './services/web-bids.service';
 | 
				
			||||||
    AdminOutBidLogsController,
 | 
					    AdminOutBidLogsController,
 | 
				
			||||||
    AdminWebBidsController,
 | 
					    AdminWebBidsController,
 | 
				
			||||||
    AdminSendMessageHistoriesController,
 | 
					    AdminSendMessageHistoriesController,
 | 
				
			||||||
 | 
					    AdminDashboardController,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  providers: [
 | 
					  providers: [
 | 
				
			||||||
    BidsService,
 | 
					    BidsService,
 | 
				
			||||||
| 
						 | 
					@ -60,6 +63,7 @@ import { WebBidsService } from './services/web-bids.service';
 | 
				
			||||||
    GraysApi,
 | 
					    GraysApi,
 | 
				
			||||||
    SendMessageHistoriesService,
 | 
					    SendMessageHistoriesService,
 | 
				
			||||||
    ImapService,
 | 
					    ImapService,
 | 
				
			||||||
 | 
					    DashboardService,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  exports: [BotTelegramApi, SendMessageHistoriesService, BidsService],
 | 
					  exports: [BotTelegramApi, SendMessageHistoriesService, BidsService],
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,17 @@
 | 
				
			||||||
 | 
					import { Controller, Post } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { DashboardService } from '../../services/dashboard.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Controller('admin/dashboards')
 | 
				
			||||||
 | 
					export class AdminDashboardController {
 | 
				
			||||||
 | 
					  constructor(private readonly dashboardService: DashboardService) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Post('reset-tool')
 | 
				
			||||||
 | 
					  async resetTool() {
 | 
				
			||||||
 | 
					    return await this.dashboardService.resetTool();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Post('shutdown-tool')
 | 
				
			||||||
 | 
					  async shutdownTool() {
 | 
				
			||||||
 | 
					    return await this.dashboardService.shutdownTool();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -31,6 +31,7 @@ import { Constant } from '../utils/constant';
 | 
				
			||||||
import { WebBidsService } from './web-bids.service';
 | 
					import { WebBidsService } from './web-bids.service';
 | 
				
			||||||
import { NotificationService } from '@/modules/notification/notification.service';
 | 
					import { NotificationService } from '@/modules/notification/notification.service';
 | 
				
			||||||
import { Event } from '../utils/events';
 | 
					import { Event } from '../utils/events';
 | 
				
			||||||
 | 
					import _ from 'lodash';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class BidsService {
 | 
					export class BidsService {
 | 
				
			||||||
| 
						 | 
					@ -177,7 +178,7 @@ export class BidsService {
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (lastHistory && lastHistory.price + bid.plus_price >= bid.max_price) {
 | 
					    if (lastHistory && lastHistory.price + bid.plus_price > bid.max_price) {
 | 
				
			||||||
      throw new BadRequestException(
 | 
					      throw new BadRequestException(
 | 
				
			||||||
        AppResponse.toResponse(false, { message: 'Price is out of Max Price' }),
 | 
					        AppResponse.toResponse(false, { message: 'Price is out of Max Price' }),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
| 
						 | 
					@ -205,7 +206,7 @@ export class BidsService {
 | 
				
			||||||
    // Tìm kiếm phiên đấu giá trong database theo id
 | 
					    // Tìm kiếm phiên đấu giá trong database theo id
 | 
				
			||||||
    const bid = await this.bidsRepo.findOne({
 | 
					    const bid = await this.bidsRepo.findOne({
 | 
				
			||||||
      where: { id },
 | 
					      where: { id },
 | 
				
			||||||
      relations: { histories: true },
 | 
					      relations: { histories: true, web_bid: true },
 | 
				
			||||||
      order: {
 | 
					      order: {
 | 
				
			||||||
        histories: {
 | 
					        histories: {
 | 
				
			||||||
          price: 'DESC',
 | 
					          price: 'DESC',
 | 
				
			||||||
| 
						 | 
					@ -245,7 +246,12 @@ export class BidsService {
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      // Nếu phiên đấu giá vẫn đang diễn ra và giá hiện tại vượt quá giới hạn đặt của người dùng
 | 
					      // Nếu phiên đấu giá vẫn đang diễn ra và giá hiện tại vượt quá giới hạn đặt của người dùng
 | 
				
			||||||
      if (data.current_price > bid.max_price + bid.plus_price) {
 | 
					
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        data.current_price > bid.max_price + bid.plus_price ||
 | 
				
			||||||
 | 
					        (!bid.histories.length &&
 | 
				
			||||||
 | 
					          data.reserve_price > bid.max_price + bid.plus_price)
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
        bid.status = 'out-bid'; // Gán trạng thái là đã bị outbid
 | 
					        bid.status = 'out-bid'; // Gán trạng thái là đã bị outbid
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -267,6 +273,7 @@ export class BidsService {
 | 
				
			||||||
    const result = await this.bidsRepo.save({
 | 
					    const result = await this.bidsRepo.save({
 | 
				
			||||||
      ...bid,
 | 
					      ...bid,
 | 
				
			||||||
      ...data,
 | 
					      ...data,
 | 
				
			||||||
 | 
					      current_price: Math.max(data.current_price, bid.current_price),
 | 
				
			||||||
      updated_at: new Date(), // Cập nhật timestamp
 | 
					      updated_at: new Date(), // Cập nhật timestamp
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,97 @@
 | 
				
			||||||
 | 
					import AppResponse from '@/response/app-response';
 | 
				
			||||||
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { exec } from 'child_process';
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class DashboardService {
 | 
				
			||||||
 | 
					  constructor() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private readonly tool_name = 'auto-bid-tool';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async resetToolByName(toolName: string): Promise<string> {
 | 
				
			||||||
 | 
					    return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					      // Lấy danh sách process đang chạy
 | 
				
			||||||
 | 
					      exec('pm2 jlist', (error, stdout, stderr) => {
 | 
				
			||||||
 | 
					        if (error) {
 | 
				
			||||||
 | 
					          return reject(`Error get list process: ${stderr}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          const processList = JSON.parse(stdout);
 | 
				
			||||||
 | 
					          const targetProcess = processList.find(
 | 
				
			||||||
 | 
					            (proc: any) => proc.name === toolName,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (!targetProcess) {
 | 
				
			||||||
 | 
					            return reject(`Not found process for name "${toolName}"`);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          const pmId = targetProcess.pm_id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // Restart process theo pm_id
 | 
				
			||||||
 | 
					          exec(`pm2 restart ${pmId}`, (err, out, errOut) => {
 | 
				
			||||||
 | 
					            if (err) {
 | 
				
			||||||
 | 
					              return reject(`Error restart process: ${errOut}`);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            resolve(`Restarted process "${toolName}" with pm_id ${pmId}`);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        } catch (parseErr) {
 | 
				
			||||||
 | 
					          reject(`Error parse JSON output: ${parseErr}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async shutdownToolByName(toolName: string): Promise<string> {
 | 
				
			||||||
 | 
					    return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					      // Lấy danh sách process đang chạy
 | 
				
			||||||
 | 
					      exec('pm2 jlist', (error, stdout, stderr) => {
 | 
				
			||||||
 | 
					        if (error) {
 | 
				
			||||||
 | 
					          return reject(`Error get list process: ${stderr}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          const processList = JSON.parse(stdout);
 | 
				
			||||||
 | 
					          const targetProcess = processList.find(
 | 
				
			||||||
 | 
					            (proc: any) => proc.name === toolName,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (!targetProcess) {
 | 
				
			||||||
 | 
					            return reject(`Not found process for name "${toolName}"`);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          const pmId = targetProcess.pm_id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // Restart process theo pm_id
 | 
				
			||||||
 | 
					          exec(`pm2 stop ${pmId}`, (err, out, errOut) => {
 | 
				
			||||||
 | 
					            if (err) {
 | 
				
			||||||
 | 
					              return reject(`Error restart process: ${errOut}`);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            resolve(`Restarted process "${toolName}" with pm_id ${pmId}`);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        } catch (parseErr) {
 | 
				
			||||||
 | 
					          reject(`Error parse JSON output: ${parseErr}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async resetTool() {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      await this.resetToolByName(this.tool_name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return AppResponse.toResponse(true);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      return AppResponse.toResponse(false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async shutdownTool() {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      await this.shutdownToolByName(this.tool_name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return AppResponse.toResponse(true);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      return AppResponse.toResponse(false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					module.exports = {
 | 
				
			||||||
 | 
					    apps: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            name: 'auto-bid-tool',
 | 
				
			||||||
 | 
					            script: './index.js',
 | 
				
			||||||
 | 
					            instances: 1,
 | 
				
			||||||
 | 
					            exec_mode: 'fork',
 | 
				
			||||||
 | 
					            watch: false,
 | 
				
			||||||
 | 
					            log_date_format: 'YYYY-MM-DD HH:mm:ss',
 | 
				
			||||||
 | 
					            output: './logs/out.log',
 | 
				
			||||||
 | 
					            error: './logs/error.log',
 | 
				
			||||||
 | 
					            merge_logs: true,
 | 
				
			||||||
 | 
					            max_memory_restart: '12G',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -161,7 +161,21 @@ const clearLazyTab = async () => {
 | 
				
			||||||
            if (!activeUrls.includes(pageUrl)) {
 | 
					            if (!activeUrls.includes(pageUrl)) {
 | 
				
			||||||
                if (!page.isClosed() && browser.isConnected()) {
 | 
					                if (!page.isClosed() && browser.isConnected()) {
 | 
				
			||||||
                    try {
 | 
					                    try {
 | 
				
			||||||
 | 
					                        const bidData = MANAGER_BIDS.filter((item) => item.page_context)
 | 
				
			||||||
 | 
					                            .map((i) => ({
 | 
				
			||||||
 | 
					                                current_url: i.page_context.url(),
 | 
				
			||||||
 | 
					                                data: i,
 | 
				
			||||||
 | 
					                            }))
 | 
				
			||||||
 | 
					                            .find((j) => j.current_url === pageUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        console.log(bidData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if (bidData && bidData.data) {
 | 
				
			||||||
 | 
					                            await safeClosePage(bidData.data);
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
                            await page.close();
 | 
					                            await page.close();
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        console.log(`🛑 Closing unused tab: ${pageUrl}`);
 | 
					                        console.log(`🛑 Closing unused tab: ${pageUrl}`);
 | 
				
			||||||
                    } catch (err) {
 | 
					                    } catch (err) {
 | 
				
			||||||
                        console.warn(`⚠️ Error closing tab ${pageUrl}:, err.message`);
 | 
					                        console.warn(`⚠️ Error closing tab ${pageUrl}:, err.message`);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -123,8 +123,10 @@ export class LangtonsProductBid extends ProductBid {
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                model: result?.pid || null,
 | 
					                model: result?.pid || null,
 | 
				
			||||||
                lot_id: result?.lotId || null,
 | 
					                lot_id: result?.lotId || null,
 | 
				
			||||||
                reserve_price: result.lotData?.minimumBid || null,
 | 
					                reserve_price: 21, //test
 | 
				
			||||||
                current_price: result.lotData?.currentMaxBid || null,
 | 
					                // reserve_price: result.lotData?.minimumBid || null,
 | 
				
			||||||
 | 
					                // current_price: result.lotData?.currentMaxBid || null,
 | 
				
			||||||
 | 
					                current_price: 20, // test
 | 
				
			||||||
                // close_time: close_time && !this.close_time ? String(close_time) : null,
 | 
					                // close_time: close_time && !this.close_time ? String(close_time) : null,
 | 
				
			||||||
                close_time: close_time ? String(close_time) : null,
 | 
					                close_time: close_time ? String(close_time) : null,
 | 
				
			||||||
                name,
 | 
					                name,
 | 
				
			||||||
| 
						 | 
					@ -212,7 +214,7 @@ export class LangtonsProductBid extends ProductBid {
 | 
				
			||||||
            const bidHistoriesItem = _.maxBy(this.histories, 'price');
 | 
					            const bidHistoriesItem = _.maxBy(this.histories, 'price');
 | 
				
			||||||
            console.log(`📜 [${this.id}] Current bid history:`, this.histories);
 | 
					            console.log(`📜 [${this.id}] Current bid history:`, this.histories);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (bidHistoriesItem && bidHistoriesItem.price === this.current_price && this.max_price == response?.lotData.myBid) {
 | 
					            if (bidHistoriesItem && bidHistoriesItem?.price === this.current_price && this.max_price == response?.lotData.myBid) {
 | 
				
			||||||
                console.log(`🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem.price})`);
 | 
					                console.log(`🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem.price})`);
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
| 
						 | 
					@ -370,7 +372,7 @@ export class LangtonsProductBid extends ProductBid {
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (lotData.myBid && this.max_price && this.max_price != lotData.myBid) {
 | 
					                    if (lotData.myBid && this.max_price && this.max_price != lotData.myBid) {
 | 
				
			||||||
                        // this.handlePlaceBid();
 | 
					                        this.handlePlaceBid();
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                } catch (error) {
 | 
					                } catch (error) {
 | 
				
			||||||
                    console.error(`🚨 [${this.id}] Error parsing API response:`, error);
 | 
					                    console.error(`🚨 [${this.id}] Error parsing API response:`, error);
 | 
				
			||||||
| 
						 | 
					@ -399,7 +401,7 @@ export class LangtonsProductBid extends ProductBid {
 | 
				
			||||||
                await this.gotoLink();
 | 
					                await this.gotoLink();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // await this.handlePlaceBid();
 | 
					            await this.handlePlaceBid();
 | 
				
			||||||
        } catch (error) {
 | 
					        } catch (error) {
 | 
				
			||||||
            console.error(`🚨 [${this.id}] Error navigating the page: ${error}`);
 | 
					            console.error(`🚨 [${this.id}] Error navigating the page: ${error}`);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,8 +14,8 @@ export class LawsonsApiBid extends ApiBid {
 | 
				
			||||||
            // Tạo timeout để reject sau 1 phút nếu không có phản hồi
 | 
					            // Tạo timeout để reject sau 1 phút nếu không có phản hồi
 | 
				
			||||||
            const timeout = setTimeout(() => {
 | 
					            const timeout = setTimeout(() => {
 | 
				
			||||||
                global.socket.off(`verify-code.${this.origin_url}`); // Xóa listener tránh rò rỉ bộ nhớ
 | 
					                global.socket.off(`verify-code.${this.origin_url}`); // Xóa listener tránh rò rỉ bộ nhớ
 | 
				
			||||||
                rej(new Error(`[${this.id}] Timeout: No verification code received within 1 minute.`));
 | 
					                rej(new Error(`[${this.id}] Timeout: No verification code received within 2 minute.`));
 | 
				
			||||||
            }, 60 * 1000); // 60 giây
 | 
					            }, 120 * 1000); // 60 giây
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            global.socket.on(`verify-code.${this.origin_url}`, async (data) => {
 | 
					            global.socket.on(`verify-code.${this.origin_url}`, async (data) => {
 | 
				
			||||||
                console.log(`📢 [${this.id}] VERIFY CODE:`, data);
 | 
					                console.log(`📢 [${this.id}] VERIFY CODE:`, data);
 | 
				
			||||||
| 
						 | 
					@ -246,8 +246,6 @@ export class LawsonsApiBid extends ApiBid {
 | 
				
			||||||
                    console.log(`🔄 [${this.id}] Reloading page...`);
 | 
					                    console.log(`🔄 [${this.id}] Reloading page...`);
 | 
				
			||||||
                    await this.page_context.reload({ waitUntil: 'networkidle2' });
 | 
					                    await this.page_context.reload({ waitUntil: 'networkidle2' });
 | 
				
			||||||
                    console.log(`✅ [${this.id}] Page reloaded successfully.`);
 | 
					                    console.log(`✅ [${this.id}] Page reloaded successfully.`);
 | 
				
			||||||
 | 
					 | 
				
			||||||
                    // this.handleUpdateWonItem();
 | 
					 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    console.log(`❌ [${this.id}] Page context is closed. Stopping reload.`);
 | 
					                    console.log(`❌ [${this.id}] Page context is closed. Stopping reload.`);
 | 
				
			||||||
                    clearInterval(this.reloadInterval);
 | 
					                    clearInterval(this.reloadInterval);
 | 
				
			||||||
| 
						 | 
					@ -255,6 +253,6 @@ export class LawsonsApiBid extends ApiBid {
 | 
				
			||||||
            } catch (error) {
 | 
					            } catch (error) {
 | 
				
			||||||
                console.error(`🚨 [${this.id}] Error reloading page:`, error.message);
 | 
					                console.error(`🚨 [${this.id}] Error reloading page:`, error.message);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }, 60000);
 | 
					        }, 60000); // 1p reload
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,8 @@
 | 
				
			||||||
import { updateBid } from '../../system/apis/bid.js';
 | 
					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 configs from '../../system/config.js';
 | 
				
			||||||
import { extractPriceNumber, removeFalsyValues } from '../../system/utils.js';
 | 
					import { delay, extractPriceNumber, isTimeReached, removeFalsyValues } from '../../system/utils.js';
 | 
				
			||||||
import { ProductBid } from '../product-bid.js';
 | 
					import { ProductBid } from '../product-bid.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class LawsonsProductBid extends ProductBid {
 | 
					export class LawsonsProductBid extends ProductBid {
 | 
				
			||||||
| 
						 | 
					@ -20,10 +22,9 @@ export class LawsonsProductBid extends ProductBid {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async getReversePrice() {
 | 
					    async getReversePrice() {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            // Kiểm tra xem có context của trang web không, nếu không thì trả về null
 | 
					 | 
				
			||||||
            if (!this.page_context) return null;
 | 
					            if (!this.page_context) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await this.page_context.waitForSelector('.select-dropdown-value.text-truncate', { timeout: 2000 });
 | 
					            await this.page_context.waitForSelector('.select-dropdown-value.text-truncate', { timeout: 4000 });
 | 
				
			||||||
            const price = await this.page_context.evaluate(() => {
 | 
					            const price = await this.page_context.evaluate(() => {
 | 
				
			||||||
                const el = document.querySelector('.select-dropdown-value.text-truncate');
 | 
					                const el = document.querySelector('.select-dropdown-value.text-truncate');
 | 
				
			||||||
                return el ? el.innerText : null;
 | 
					                return el ? el.innerText : null;
 | 
				
			||||||
| 
						 | 
					@ -31,8 +32,7 @@ export class LawsonsProductBid extends ProductBid {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return price ? extractPriceNumber(price) : null;
 | 
					            return price ? extractPriceNumber(price) : null;
 | 
				
			||||||
        } catch (error) {
 | 
					        } catch (error) {
 | 
				
			||||||
            console.log(error);
 | 
					            console.log(error.message);
 | 
				
			||||||
            // Nếu có lỗi xảy ra trong quá trình lấy thời gian, trả về null
 | 
					 | 
				
			||||||
            return null;
 | 
					            return null;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -41,6 +41,10 @@ export class LawsonsProductBid extends ProductBid {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            if (!this.page_context) return;
 | 
					            if (!this.page_context) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // if (this.updated_at) {
 | 
				
			||||||
 | 
					            //     await this.page_context.reload({ waitUntil: 'networkidle0' });
 | 
				
			||||||
 | 
					            // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const result = await this.waitApiInfo();
 | 
					            const result = await this.waitApiInfo();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const reservePrice = await this.getReversePrice();
 | 
					            const reservePrice = await this.getReversePrice();
 | 
				
			||||||
| 
						 | 
					@ -55,8 +59,10 @@ export class LawsonsProductBid extends ProductBid {
 | 
				
			||||||
                    reserve_price: reservePrice,
 | 
					                    reserve_price: reservePrice,
 | 
				
			||||||
                    current_price: result?.currentBidAmount || null,
 | 
					                    current_price: result?.currentBidAmount || null,
 | 
				
			||||||
                    close_time: new Date(result.endTime).toUTCString() || 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,
 | 
					                    name: result?.itemView?.title || null,
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
 | 
					                // [],
 | 
				
			||||||
                ['close_time'],
 | 
					                ['close_time'],
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -84,6 +90,153 @@ export class LawsonsProductBid extends ProductBid {
 | 
				
			||||||
        }, url);
 | 
					        }, 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() {
 | 
					    async waitApiInfo() {
 | 
				
			||||||
        if (!this.page_context) {
 | 
					        if (!this.page_context) {
 | 
				
			||||||
            console.error(`❌ [${this.id}] Error: page_context is undefined.`);
 | 
					            console.error(`❌ [${this.id}] Error: page_context is undefined.`);
 | 
				
			||||||
| 
						 | 
					@ -98,6 +251,104 @@ export class LawsonsProductBid extends ProductBid {
 | 
				
			||||||
        return { ...info, ...detailData };
 | 
					        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 () => {
 | 
					    action = async () => {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            const page = this.page_context;
 | 
					            const page = this.page_context;
 | 
				
			||||||
| 
						 | 
					@ -107,6 +358,8 @@ export class LawsonsProductBid extends ProductBid {
 | 
				
			||||||
                console.log(`🔄 [${this.id}] Navigating to target URL: ${this.url}`);
 | 
					                console.log(`🔄 [${this.id}] Navigating to target URL: ${this.url}`);
 | 
				
			||||||
                await this.gotoLink();
 | 
					                await this.gotoLink();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await this.handlePlaceBid();
 | 
				
			||||||
        } catch (error) {
 | 
					        } catch (error) {
 | 
				
			||||||
            console.error(`🚨 [${this.id}] Error navigating the page: ${error}`);
 | 
					            console.error(`🚨 [${this.id}] Error navigating the page: ${error}`);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,6 +26,7 @@ const configs = {
 | 
				
			||||||
            API_DETAIL_PRODUCT: (model) => {
 | 
					            API_DETAIL_PRODUCT: (model) => {
 | 
				
			||||||
                return `https://www.lawsons.com.au/api/auctions/lot/${model}`;
 | 
					                return `https://www.lawsons.com.au/api/auctions/lot/${model}`;
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					            API_CHECKOUT: 'https://www.lawsons.com.au/app/orderBid',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue