Deploy to Staging #9
			
				
			
		
		
		
	| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
import { generateNestParams, handleError, handleSuccess } from '.';
 | 
					import { generateNestParams, handleError, handleSuccess } from '.';
 | 
				
			||||||
import axios from '../lib/axios';
 | 
					import axios from '../lib/axios';
 | 
				
			||||||
import { IBid } from '../system/type';
 | 
					import { IBid, IWebBid } from '../system/type';
 | 
				
			||||||
import { removeFalsyValues } from '../utils';
 | 
					import { removeFalsyValues } from '../utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getBids = async (params: Record<string, string | number>) => {
 | 
					export const getBids = async (params: Record<string, string | number>) => {
 | 
				
			||||||
| 
						 | 
					@ -104,3 +104,17 @@ export const deletesBid = async (bids: IBid[]) => {
 | 
				
			||||||
        handleError(error);
 | 
					        handleError(error);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getImagesWorking = async (values: (IBid | IWebBid) & { type: string }) => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        const { data } = await axios({
 | 
				
			||||||
 | 
					            url: `bids/images-working/${values.type.toLocaleLowerCase().replace('_', '-')}/${values.id}`,
 | 
				
			||||||
 | 
					            withCredentials: true,
 | 
				
			||||||
 | 
					            method: 'GET',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return data;
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        handleError(error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,7 @@ import { useDisclosure } from '@mantine/hooks';
 | 
				
			||||||
import moment from 'moment';
 | 
					import moment from 'moment';
 | 
				
			||||||
import { useEffect, useState } from 'react';
 | 
					import { useEffect, useState } from 'react';
 | 
				
			||||||
import { Socket } from 'socket.io-client';
 | 
					import { Socket } from 'socket.io-client';
 | 
				
			||||||
 | 
					import { getImagesWorking } from '../../apis/bid';
 | 
				
			||||||
import { IBid, IWebBid } from '../../system/type';
 | 
					import { IBid, IWebBid } from '../../system/type';
 | 
				
			||||||
import ShowImageModal from './show-image-modal';
 | 
					import ShowImageModal from './show-image-modal';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,19 +17,27 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [opened, { open, close }] = useDisclosure(false);
 | 
					    const [opened, { open, close }] = useDisclosure(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [imageSrc, setImageSrc] = useState(`${import.meta.env.VITE_BASE_URL}bids/status-working/${data.type.replace('_', '-').toLowerCase()}/${data.id}/working`);
 | 
					    const [imageSrc, setImageSrc] = useState<string | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [lastUpdate, setLastUpdate] = useState(new Date());
 | 
					    const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function isIBid(obj: IBid | IWebBid): obj is IBid {
 | 
					    function isIBid(obj: IBid | IWebBid): obj is IBid {
 | 
				
			||||||
        return 'name' in obj;
 | 
					        return 'name' in obj;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const renderUrl = ({ type, id }: (IBid | IWebBid) & { type: string }, name: string) => {
 | 
				
			||||||
 | 
					        return `${import.meta.env.VITE_BASE_URL}bids/status-working/${type.replace('_', '-').toLowerCase()}/${id}/${name}`;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const extractTime = (filename: string) => {
 | 
				
			||||||
 | 
					        return Number(filename.split('-')[0]) || 0;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(() => {
 | 
					    useEffect(() => {
 | 
				
			||||||
        const updateImage = ({ type, id, filename }: { type: string; filename: string; id: IBid['id'] }) => {
 | 
					        const updateImage = ({ type, id, filename }: { type: string; filename: string; id: IBid['id'] }) => {
 | 
				
			||||||
            if (type == data.type && id == data.id) {
 | 
					            if (type == data.type && id == data.id) {
 | 
				
			||||||
                setLastUpdate(new Date());
 | 
					                setLastUpdate(new Date(extractTime(filename)));
 | 
				
			||||||
                setImageSrc(`${import.meta.env.VITE_BASE_URL}bids/status-working/${type.replace('_', '-').toLowerCase()}/${id}/${filename}`);
 | 
					                setImageSrc(renderUrl(data, filename));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,8 +46,23 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
 | 
				
			||||||
        return () => {
 | 
					        return () => {
 | 
				
			||||||
            socket.off('working', updateImage);
 | 
					            socket.off('working', updateImage);
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					        // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
    }, [socket, data.id, data.type]);
 | 
					    }, [socket, data.id, data.type]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        (async () => {
 | 
				
			||||||
 | 
					            const result = await getImagesWorking(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!result || !result.data) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const filename = result.data[0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            setImageSrc(renderUrl(data, filename));
 | 
				
			||||||
 | 
					            setLastUpdate(new Date(extractTime(filename)));
 | 
				
			||||||
 | 
					        })();
 | 
				
			||||||
 | 
					        // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					    }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <>
 | 
					        <>
 | 
				
			||||||
            <Box className="rounded-md overflow-hidden relative shadow-lg">
 | 
					            <Box className="rounded-md overflow-hidden relative shadow-lg">
 | 
				
			||||||
| 
						 | 
					@ -74,7 +98,7 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
 | 
				
			||||||
                </Box>
 | 
					                </Box>
 | 
				
			||||||
            </Box>
 | 
					            </Box>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <ShowImageModal src={imageSrc} fallbackSrc={fallbackSrc} opened={opened} onClose={close} />
 | 
					            <ShowImageModal src={imageSrc || fallbackSrc} fallbackSrc={fallbackSrc} opened={opened} onClose={close} />
 | 
				
			||||||
        </>
 | 
					        </>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,6 +24,11 @@ export default function Bids() {
 | 
				
			||||||
    const [openedBid, bidModal] = useDisclosure(false);
 | 
					    const [openedBid, bidModal] = useDisclosure(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const columns: IColumn<IBid>[] = [
 | 
					    const columns: IColumn<IBid>[] = [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            key: 'id',
 | 
				
			||||||
 | 
					            title: 'ID',
 | 
				
			||||||
 | 
					            typeFilter: 'number',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            key: 'name',
 | 
					            key: 'name',
 | 
				
			||||||
            title: 'Name',
 | 
					            title: 'Name',
 | 
				
			||||||
| 
						 | 
					@ -243,15 +248,17 @@ export default function Bids() {
 | 
				
			||||||
                                    >
 | 
					                                    >
 | 
				
			||||||
                                        Histories
 | 
					                                        Histories
 | 
				
			||||||
                                    </Menu.Item>
 | 
					                                    </Menu.Item>
 | 
				
			||||||
                                    <Menu.Item
 | 
					                                    {['https://www.grays.com'].includes(row?.web_bid.origin_url) && (
 | 
				
			||||||
                                        onClick={() => {
 | 
					                                        <Menu.Item
 | 
				
			||||||
                                            setClickData(row);
 | 
					                                            onClick={() => {
 | 
				
			||||||
                                            historiesGraysApiModel.open();
 | 
					                                                setClickData(row);
 | 
				
			||||||
                                        }}
 | 
					                                                historiesGraysApiModel.open();
 | 
				
			||||||
                                        leftSection={<IconHammer size={14} />}
 | 
					                                            }}
 | 
				
			||||||
                                    >
 | 
					                                            leftSection={<IconHammer size={14} />}
 | 
				
			||||||
                                        Bids
 | 
					                                        >
 | 
				
			||||||
                                    </Menu.Item>
 | 
					                                            Bids
 | 
				
			||||||
 | 
					                                        </Menu.Item>
 | 
				
			||||||
 | 
					                                    )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                                    <Menu.Item
 | 
					                                    <Menu.Item
 | 
				
			||||||
                                        disabled={row.status === 'win-bid'}
 | 
					                                        disabled={row.status === 'win-bid'}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,6 +34,7 @@ export interface IBid extends ITimestamp {
 | 
				
			||||||
    first_bid: boolean;
 | 
					    first_bid: boolean;
 | 
				
			||||||
    status: 'biding' | 'out-bid' | 'win-bid';
 | 
					    status: 'biding' | 'out-bid' | 'win-bid';
 | 
				
			||||||
    histories: IHistory[];
 | 
					    histories: IHistory[];
 | 
				
			||||||
 | 
					    web_bid: IWebBid;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IHistory extends ITimestamp {
 | 
					export interface IHistory extends ITimestamp {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "cSpell.words": ["LANGTONS", "LAWSONS"]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,178 @@
 | 
				
			||||||
 | 
					<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
 | 
				
			||||||
 | 
					<html>
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8">
 | 
				
			||||||
 | 
					<title>Callcentric Email</title>
 | 
				
			||||||
 | 
					<style type=3D"text/css" media=3D"all">
 | 
				
			||||||
 | 
					body {
 | 
				
			||||||
 | 
					        font-family: arial,helvetica,sans-serif;
 | 
				
			||||||
 | 
					        font-size: 14px;
 | 
				
			||||||
 | 
					        line-height: 22px;
 | 
				
			||||||
 | 
					        color: #444;
 | 
				
			||||||
 | 
					        margin: 0;
 | 
				
			||||||
 | 
					        padding: 20px 0;
 | 
				
			||||||
 | 
					        background-color: #e5e5e5;
 | 
				
			||||||
 | 
					        outline: 0;
 | 
				
			||||||
 | 
					        vertical-align: top;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					p {
 | 
				
			||||||
 | 
					        margin-bottom: 1.5em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					img {
 | 
				
			||||||
 | 
					        display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					a:link,
 | 
				
			||||||
 | 
					a:visited {
 | 
				
			||||||
 | 
					        color: #006699;
 | 
				
			||||||
 | 
					        text-decoration: underline;
 | 
				
			||||||
 | 
					        margin: 0px;
 | 
				
			||||||
 | 
					        padding: 0px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					a:hover {=20
 | 
				
			||||||
 | 
					        text-decoration: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					li {
 | 
				
			||||||
 | 
					        margin-bottom: 1em;
 | 
				
			||||||
 | 
					        line-height: 22px;
 | 
				
			||||||
 | 
					}=09
 | 
				
			||||||
 | 
					=2Efooter a {
 | 
				
			||||||
 | 
					        color: #aaaaaa;
 | 
				
			||||||
 | 
					        text-decoration: underline;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					=2Efooter a:hover {
 | 
				
			||||||
 | 
					        text-decoration: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					=2Eframe {
 | 
				
			||||||
 | 
					        width: 580px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#btn {
 | 
				
			||||||
 | 
					  background: #3498db;
 | 
				
			||||||
 | 
					  background-image: -webkit-linear-gradient(top, #3498db, #2980b9);
 | 
				
			||||||
 | 
					  background-image: -moz-linear-gradient(top, #3498db, #2980b9);
 | 
				
			||||||
 | 
					  background-image: -ms-linear-gradient(top, #3498db, #2980b9);
 | 
				
			||||||
 | 
					  background-image: -o-linear-gradient(top, #3498db, #2980b9);
 | 
				
			||||||
 | 
					  background-image: linear-gradient(to bottom, #3498db, #2980b9);
 | 
				
			||||||
 | 
					  -webkit-border-radius: 28;
 | 
				
			||||||
 | 
					  -moz-border-radius: 28;
 | 
				
			||||||
 | 
					  border-radius: 28px;
 | 
				
			||||||
 | 
					  font-family: Arial;
 | 
				
			||||||
 | 
					  color: #ffffff;
 | 
				
			||||||
 | 
					  font-size: 20px;
 | 
				
			||||||
 | 
					  padding: 10px 20px 10px 20px;
 | 
				
			||||||
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					  display: inline-block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#btn:hover {
 | 
				
			||||||
 | 
					  background: #3cb0fd;
 | 
				
			||||||
 | 
					  background-image: -webkit-linear-gradient(top, #3cb0fd, #3498db);
 | 
				
			||||||
 | 
					  background-image: -moz-linear-gradient(top, #3cb0fd, #3498db);
 | 
				
			||||||
 | 
					  background-image: -ms-linear-gradient(top, #3cb0fd, #3498db);
 | 
				
			||||||
 | 
					  background-image: -o-linear-gradient(top, #3cb0fd, #3498db);
 | 
				
			||||||
 | 
					  background-image: linear-gradient(to bottom, #3cb0fd, #3498db);
 | 
				
			||||||
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					=2Esms-box { font-family: arial,helvetica,sans-serif; font-size: 14px; padd=
 | 
				
			||||||
 | 
					ing:12px; border:1px solid #e0e0e0; background-color: #e0e0e0; -webkit-bord=
 | 
				
			||||||
 | 
					er-radius: 12px; -moz-border-radius: 12px; border-radius: 12px; word-wrap:b=
 | 
				
			||||||
 | 
					reak-word; display:inline-block; }
 | 
				
			||||||
 | 
					p { font-family: arial,helvetica,sans-serif; font-size: 14px; margin-bottom=
 | 
				
			||||||
 | 
					: 1.5em; line-height: 22px; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media only screen and (max-device-width: 480px) {
 | 
				
			||||||
 | 
					        .frame {
 | 
				
			||||||
 | 
					                width: 442px !important;
 | 
				
			||||||
 | 
					         }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        p {
 | 
				
			||||||
 | 
					                font-size: 22px !important;
 | 
				
			||||||
 | 
					                line-height: 28px !important;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        ul li {
 | 
				
			||||||
 | 
					                font-size: 22px !important;
 | 
				
			||||||
 | 
					                line-height: 28px !important;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .section_header{
 | 
				
			||||||
 | 
					                font-size: 28px !important;
 | 
				
			||||||
 | 
					                line-height: 30px !important;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        #btn {
 | 
				
			||||||
 | 
					                font-size: 28px !important;
 | 
				
			||||||
 | 
					                padding: 14px 24px 14px 24px !important;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body style=3D"font-family: Arial, sans-serif; font-size: 14px; margin-bott=
 | 
				
			||||||
 | 
					om: 1.5em; line-height: 22px; color: black; width: 100% !important; margin:=
 | 
				
			||||||
 | 
					 0; padding: 0px; background-color: #e5e5e5; outline: 0; vertical-align: to=
 | 
				
			||||||
 | 
					p;" bgcolor=3D"#e5e5e5">
 | 
				
			||||||
 | 
					<table width=3D"100%" cellpadding=3D"0" cellspacing=3D"0" border=3D"0"><tr>=
 | 
				
			||||||
 | 
					<td style=3D"padding: 0px 0px;" align=3D"center">
 | 
				
			||||||
 | 
					<table class=3D"head" width=3D"100%" bgcolor=3D"#c9c9c9" style=3D"backgroun=
 | 
				
			||||||
 | 
					d-color:#c9c9c9;">
 | 
				
			||||||
 | 
					        <tr><td align=3D"center" style=3D"color:#f3f3f3; padding:5px; font-size:12=
 | 
				
			||||||
 | 
					px;">Please do not respond to this automated message, you will not receive =
 | 
				
			||||||
 | 
					a reply</td></tr>
 | 
				
			||||||
 | 
					</table>
 | 
				
			||||||
 | 
					</td></tr><tr><td style=3D"padding: 0px 10px;" align=3D"center">
 | 
				
			||||||
 | 
					<table class=3D"frame" width=3D"580" cellpadding=3D"0" cellspacing=3D"0" bo=
 | 
				
			||||||
 | 
					rder=3D"0" align=3D"center"><tbody>
 | 
				
			||||||
 | 
					        <tr><td>
 | 
				
			||||||
 | 
					                <table width=3D"100%" cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
 | 
				
			||||||
 | 
					                        <tr><td style=3D"padding: 30px 0px 10px 0px; color: #444; text-align: le=
 | 
				
			||||||
 | 
					ft; vertical-align: middle;" align=3D"left"><a href=3D"https://www.callcent=
 | 
				
			||||||
 | 
					ric.com"><img src=3D"https://www.callcentric.com/img/callcentric_logo_250_t=
 | 
				
			||||||
 | 
					rans.png" width=3D"125" height=3D"33" alt=3D"Callcentric" border=3D"0"></a>=
 | 
				
			||||||
 | 
					</td></tr>
 | 
				
			||||||
 | 
					                </table>
 | 
				
			||||||
 | 
					        </td></tr>
 | 
				
			||||||
 | 
					        <tr><td style=3D"background-color: #ffffff; padding: 0px 0px 50px; border-=
 | 
				
			||||||
 | 
					radius: 5px 5px 0px 0px;">
 | 
				
			||||||
 | 
					                <table style=3D"padding-top: 40px;" width=3D"100%" cellpadding=3D"0" cell=
 | 
				
			||||||
 | 
					spacing=3D"0" border=3D"0"><tbody>
 | 
				
			||||||
 | 
					                        <tr><td class=3D"flex-column" style=3D"width: 40px;"></td>
 | 
				
			||||||
 | 
					                        <td style=3D"text-align: left;" align=3D"left">
 | 
				
			||||||
 | 
					<p>Dear Morgan Vu,</p><p>You have received an inbound text message from 223=
 | 
				
			||||||
 | 
					95 on your DID 17146893350:</p><div class=3D"sms-box">Your LANGTONS verific=
 | 
				
			||||||
 | 
					ation code is: 012152. This code will expire in 10 minutes.</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        </td><td class=3D"flex-column" style=3D"width: 40px;"></td></tr>
 | 
				
			||||||
 | 
					                </tbody></table>
 | 
				
			||||||
 | 
					        </td></tr>
 | 
				
			||||||
 | 
					        <tr><td align=3D"left" class=3D"footer" style=3D"background-color: #f2f2f2=
 | 
				
			||||||
 | 
					; color: #999999; padding: 0px 0px 40px; border-radius: 0px 0px 5px 5px;">
 | 
				
			||||||
 | 
					                <table border=3D"0" cellpadding=3D"0" cellspacing=3D"0" style=3D"padding-=
 | 
				
			||||||
 | 
					top: 40px;" width=3D"100%"><tbody>
 | 
				
			||||||
 | 
					                        <tr><td style=3D"width: 40px;"></td>
 | 
				
			||||||
 | 
					                                <td align=3D"left" style=3D"text-align: left; font-family:Arial, Helvet=
 | 
				
			||||||
 | 
					ica, sans-serif; font-size: 15px; line-height: 22px; color: #999999;">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Follow us on Twitter at:<br>
 | 
				
			||||||
 | 
					<a href=3D"https://twitter.com/Callcentric">https://twitter.com/Callcentric=
 | 
				
			||||||
 | 
					</a>
 | 
				
			||||||
 | 
					<br><br>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you have any questions/concerns, please contact our Customer Service Tea=
 | 
				
			||||||
 | 
					m via the following page:<br>
 | 
				
			||||||
 | 
					<a href=3D"https://www.callcentric.com/CustomerService">https://www.callcen=
 | 
				
			||||||
 | 
					tric.com/CustomerService</a><br><br>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For instructions on how to unsubscribe from our Newsletters/Service Announc=
 | 
				
			||||||
 | 
					ements please visit:<br>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<a href=3D"https://www.callcentric.com/faq/11/306">https://www.callcentric=
 | 
				
			||||||
 | 
					=2Ecom/faq/11/306</a><br><br>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<a href=3D"https://www.callcentric.com">www.callcentric.com</a><br><br>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                </td><td style=3D"width: 40px;"></td>
 | 
				
			||||||
 | 
					                        </tr>
 | 
				
			||||||
 | 
					                </tbody></table>
 | 
				
			||||||
 | 
					        </td></tr>
 | 
				
			||||||
 | 
					        <tr><td style=3D"padding: 20px 0; font-size: 15px; line-height: 25px; colo=
 | 
				
			||||||
 | 
					r: #999999; text-align: center;" align=3D"center"></td></tr>
 | 
				
			||||||
 | 
					</tbody></table></td></tr></table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "id": -1002593407119,
 | 
				
			||||||
 | 
					  "title": "Bid histories dev",
 | 
				
			||||||
 | 
					  "type": "supergroup",
 | 
				
			||||||
 | 
					  "invite_link": "https://t.me/+CSBIA7mbyBhkM2Jl",
 | 
				
			||||||
 | 
					  "permissions": {
 | 
				
			||||||
 | 
					    "can_send_messages": true,
 | 
				
			||||||
 | 
					    "can_send_media_messages": true,
 | 
				
			||||||
 | 
					    "can_send_audios": true,
 | 
				
			||||||
 | 
					    "can_send_documents": true,
 | 
				
			||||||
 | 
					    "can_send_photos": true,
 | 
				
			||||||
 | 
					    "can_send_videos": true,
 | 
				
			||||||
 | 
					    "can_send_video_notes": true,
 | 
				
			||||||
 | 
					    "can_send_voice_notes": true,
 | 
				
			||||||
 | 
					    "can_send_polls": true,
 | 
				
			||||||
 | 
					    "can_send_other_messages": true,
 | 
				
			||||||
 | 
					    "can_add_web_page_previews": true,
 | 
				
			||||||
 | 
					    "can_change_info": true,
 | 
				
			||||||
 | 
					    "can_invite_users": true,
 | 
				
			||||||
 | 
					    "can_pin_messages": true,
 | 
				
			||||||
 | 
					    "can_manage_topics": true
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "join_to_send_messages": true,
 | 
				
			||||||
 | 
					  "accepted_gift_types": {
 | 
				
			||||||
 | 
					    "unlimited_gifts": false,
 | 
				
			||||||
 | 
					    "limited_gifts": false,
 | 
				
			||||||
 | 
					    "unique_gifts": false,
 | 
				
			||||||
 | 
					    "premium_subscription": false
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "max_reaction_count": 11,
 | 
				
			||||||
 | 
					  "accent_color_id": 2
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					{"createdAt":1744420823691}
 | 
				
			||||||
| 
						 | 
					@ -27,12 +27,15 @@
 | 
				
			||||||
        "cookie": "^1.0.2",
 | 
					        "cookie": "^1.0.2",
 | 
				
			||||||
        "cookie-parser": "^1.4.7",
 | 
					        "cookie-parser": "^1.4.7",
 | 
				
			||||||
        "dayjs": "^1.11.13",
 | 
					        "dayjs": "^1.11.13",
 | 
				
			||||||
 | 
					        "imap": "^0.8.19",
 | 
				
			||||||
 | 
					        "lodash": "^4.17.21",
 | 
				
			||||||
        "multer": "^1.4.5-lts.1",
 | 
					        "multer": "^1.4.5-lts.1",
 | 
				
			||||||
        "mysql2": "^3.13.0",
 | 
					        "mysql2": "^3.13.0",
 | 
				
			||||||
        "nestjs-paginate": "^11.1.0",
 | 
					        "nestjs-paginate": "^11.1.0",
 | 
				
			||||||
        "reflect-metadata": "^0.2.0",
 | 
					        "reflect-metadata": "^0.2.0",
 | 
				
			||||||
        "rxjs": "^7.8.1",
 | 
					        "rxjs": "^7.8.1",
 | 
				
			||||||
        "sharp": "^0.33.5",
 | 
					        "sharp": "^0.33.5",
 | 
				
			||||||
 | 
					        "simple-imap": "^0.1.6",
 | 
				
			||||||
        "typeorm": "^0.3.21",
 | 
					        "typeorm": "^0.3.21",
 | 
				
			||||||
        "uuid": "^11.1.0"
 | 
					        "uuid": "^11.1.0"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
| 
						 | 
					@ -44,7 +47,9 @@
 | 
				
			||||||
        "@types/cookie": "^0.6.0",
 | 
					        "@types/cookie": "^0.6.0",
 | 
				
			||||||
        "@types/cookie-parser": "^1.4.8",
 | 
					        "@types/cookie-parser": "^1.4.8",
 | 
				
			||||||
        "@types/express": "^4.17.17",
 | 
					        "@types/express": "^4.17.17",
 | 
				
			||||||
 | 
					        "@types/imap": "^0.8.42",
 | 
				
			||||||
        "@types/jest": "^29.5.2",
 | 
					        "@types/jest": "^29.5.2",
 | 
				
			||||||
 | 
					        "@types/lodash": "^4.17.16",
 | 
				
			||||||
        "@types/multer": "^1.4.12",
 | 
					        "@types/multer": "^1.4.12",
 | 
				
			||||||
        "@types/node": "^20.3.1",
 | 
					        "@types/node": "^20.3.1",
 | 
				
			||||||
        "@types/supertest": "^6.0.0",
 | 
					        "@types/supertest": "^6.0.0",
 | 
				
			||||||
| 
						 | 
					@ -2931,6 +2936,16 @@
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "license": "MIT"
 | 
					      "license": "MIT"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@types/imap": {
 | 
				
			||||||
 | 
					      "version": "0.8.42",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@types/imap/-/imap-0.8.42.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-FusePG9Cp2GYN6OLow9xBCkjznFkAR7WCz0Fm+j1p/ER6C8V8P71DtjpSmwrZsS7zekCeqdTPHEk9N5OgPwcsg==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@types/node": "*"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/istanbul-lib-coverage": {
 | 
					    "node_modules/@types/istanbul-lib-coverage": {
 | 
				
			||||||
      "version": "2.0.6",
 | 
					      "version": "2.0.6",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
 | 
				
			||||||
| 
						 | 
					@ -2985,6 +3000,13 @@
 | 
				
			||||||
        "@types/node": "*"
 | 
					        "@types/node": "*"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@types/lodash": {
 | 
				
			||||||
 | 
					      "version": "4.17.16",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/methods": {
 | 
					    "node_modules/@types/methods": {
 | 
				
			||||||
      "version": "1.1.4",
 | 
					      "version": "1.1.4",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
 | 
				
			||||||
| 
						 | 
					@ -3558,6 +3580,12 @@
 | 
				
			||||||
        "node": ">=0.4.0"
 | 
					        "node": ">=0.4.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/addressparser": {
 | 
				
			||||||
 | 
					      "version": "1.0.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/addressparser/-/addressparser-1.0.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-aQX7AISOMM7HFE0iZ3+YnD07oIeJqWGVnJ+ZIKaBZAk03ftmVYVqsGas/rbXKR21n4D/hKCSHypvcyOkds/xzg==",
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/agent-base": {
 | 
					    "node_modules/agent-base": {
 | 
				
			||||||
      "version": "6.0.2",
 | 
					      "version": "6.0.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
 | 
				
			||||||
| 
						 | 
					@ -5189,6 +5217,27 @@
 | 
				
			||||||
        "node": ">= 0.8"
 | 
					        "node": ">= 0.8"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/encoding": {
 | 
				
			||||||
 | 
					      "version": "0.1.13",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "iconv-lite": "^0.6.2"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/encoding/node_modules/iconv-lite": {
 | 
				
			||||||
 | 
					      "version": "0.6.3",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "safer-buffer": ">= 2.1.2 < 3.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=0.10.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/engine.io": {
 | 
					    "node_modules/engine.io": {
 | 
				
			||||||
      "version": "6.6.4",
 | 
					      "version": "6.6.4",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
 | 
				
			||||||
| 
						 | 
					@ -6771,6 +6820,42 @@
 | 
				
			||||||
        "node": ">= 4"
 | 
					        "node": ">= 4"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/imap": {
 | 
				
			||||||
 | 
					      "version": "0.8.19",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/imap/-/imap-0.8.19.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-z5DxEA1uRnZG73UcPA4ES5NSCGnPuuouUx43OPX7KZx1yzq3N8/vx2mtXEShT5inxB3pRgnfG1hijfu7XN2YMw==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "readable-stream": "1.1.x",
 | 
				
			||||||
 | 
					        "utf7": ">=1.0.2"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=0.8.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/imap/node_modules/isarray": {
 | 
				
			||||||
 | 
					      "version": "0.0.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/imap/node_modules/readable-stream": {
 | 
				
			||||||
 | 
					      "version": "1.1.14",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "core-util-is": "~1.0.0",
 | 
				
			||||||
 | 
					        "inherits": "~2.0.1",
 | 
				
			||||||
 | 
					        "isarray": "0.0.1",
 | 
				
			||||||
 | 
					        "string_decoder": "~0.10.x"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/imap/node_modules/string_decoder": {
 | 
				
			||||||
 | 
					      "version": "0.10.31",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==",
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/import-fresh": {
 | 
					    "node_modules/import-fresh": {
 | 
				
			||||||
      "version": "3.3.1",
 | 
					      "version": "3.3.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
 | 
				
			||||||
| 
						 | 
					@ -8263,6 +8348,18 @@
 | 
				
			||||||
        "node": ">=12"
 | 
					        "node": ">=12"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/mailparser": {
 | 
				
			||||||
 | 
					      "version": "0.4.6",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-0.4.6.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-G9D9e+v0UKpG7jiANGNkMCinqcG+Q70GngQQm8WknkMEFLx7Cyp+vnoAJaOtVCR9dPuyVAKaUJlOgDE6HTwmSw==",
 | 
				
			||||||
 | 
					      "deprecated": "Mailparser versions older than v2.3.0 are deprecated",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "encoding": ">=0.1.4",
 | 
				
			||||||
 | 
					        "mime": "*",
 | 
				
			||||||
 | 
					        "mimelib": ">=0.2.17",
 | 
				
			||||||
 | 
					        "uue": "~1.0.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/make-dir": {
 | 
					    "node_modules/make-dir": {
 | 
				
			||||||
      "version": "4.0.0",
 | 
					      "version": "4.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
 | 
				
			||||||
| 
						 | 
					@ -8422,6 +8519,17 @@
 | 
				
			||||||
        "node": ">= 0.6"
 | 
					        "node": ">= 0.6"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/mimelib": {
 | 
				
			||||||
 | 
					      "version": "0.3.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/mimelib/-/mimelib-0.3.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-ipzNp6TBsNfD3hButGlPVlGmuCgybIM9SBf8YwIG+SYmBgtU0u8wjf+BSrJX0mvqtv59SLmwphw/XiCbkLWv7w==",
 | 
				
			||||||
 | 
					      "deprecated": "This project is unmaintained",
 | 
				
			||||||
 | 
					      "license": "EUPL-1.1",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "addressparser": "~1.0.1",
 | 
				
			||||||
 | 
					        "encoding": "~0.1.12"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/mimic-fn": {
 | 
					    "node_modules/mimic-fn": {
 | 
				
			||||||
      "version": "2.1.0",
 | 
					      "version": "2.1.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
 | 
				
			||||||
| 
						 | 
					@ -8509,6 +8617,14 @@
 | 
				
			||||||
        "mkdirp": "bin/cmd.js"
 | 
					        "mkdirp": "bin/cmd.js"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/moment": {
 | 
				
			||||||
 | 
					      "version": "2.8.3",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/moment/-/moment-2.8.3.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-BV9tebhfgcO0mT3desIQEM8vdFGOsqwfJ71a+mz/ECVa0uiokosBq3A3Vyk7XhyNUzzQECX/T/bpHQANemf5Vg==",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": "*"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/ms": {
 | 
					    "node_modules/ms": {
 | 
				
			||||||
      "version": "2.1.3",
 | 
					      "version": "2.1.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
 | 
				
			||||||
| 
						 | 
					@ -10128,6 +10244,64 @@
 | 
				
			||||||
        "url": "https://github.com/sponsors/isaacs"
 | 
					        "url": "https://github.com/sponsors/isaacs"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/simple-imap": {
 | 
				
			||||||
 | 
					      "version": "0.1.6",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/simple-imap/-/simple-imap-0.1.6.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-mdmaq1u/qEOa8Ow3ZPKv2KXZbV5oHfFewqgqi8FnxD0mQzMzMLtKcE3TEmI7qhp1FmRcdqBlYeUMtOt7nS7XkA==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "imap": "0.8.14",
 | 
				
			||||||
 | 
					        "mailparser": "0.4.6",
 | 
				
			||||||
 | 
					        "moment": "2.8.3"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=0.8.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/simple-imap/node_modules/imap": {
 | 
				
			||||||
 | 
					      "version": "0.8.14",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/imap/-/imap-0.8.14.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-8opGClGwiZ7/NBc3wofk7PbGkaLqggXx8W0D/Bc6YQdufl9CixmMBKm5rrn9gyelMVdMQVfpXp3D6gdXBgtAdg==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "readable-stream": "1.1.x",
 | 
				
			||||||
 | 
					        "utf7": "1.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=0.8.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/simple-imap/node_modules/isarray": {
 | 
				
			||||||
 | 
					      "version": "0.0.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/simple-imap/node_modules/readable-stream": {
 | 
				
			||||||
 | 
					      "version": "1.1.14",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "core-util-is": "~1.0.0",
 | 
				
			||||||
 | 
					        "inherits": "~2.0.1",
 | 
				
			||||||
 | 
					        "isarray": "0.0.1",
 | 
				
			||||||
 | 
					        "string_decoder": "~0.10.x"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/simple-imap/node_modules/string_decoder": {
 | 
				
			||||||
 | 
					      "version": "0.10.31",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==",
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/simple-imap/node_modules/utf7": {
 | 
				
			||||||
 | 
					      "version": "1.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/utf7/-/utf7-1.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-vMfAo9fTQeZG7W8PEOWa9g++rkHxYqBBrb22OWY+OMUnSFFtQURySIIJ2LstpF5JI6SzsGQNaglin2aLwA7pTA==",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": "*"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/simple-swizzle": {
 | 
					    "node_modules/simple-swizzle": {
 | 
				
			||||||
      "version": "0.2.2",
 | 
					      "version": "0.2.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
 | 
				
			||||||
| 
						 | 
					@ -11371,6 +11545,23 @@
 | 
				
			||||||
        "punycode": "^2.1.0"
 | 
					        "punycode": "^2.1.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/utf7": {
 | 
				
			||||||
 | 
					      "version": "1.0.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/utf7/-/utf7-1.0.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-qQrPtYLLLl12NF4DrM9CvfkxkYI97xOb5dsnGZHE3teFr0tWiEZ9UdgMPczv24vl708cYMpe6mGXGHrotIp3Bw==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "semver": "~5.3.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/utf7/node_modules/semver": {
 | 
				
			||||||
 | 
					      "version": "5.3.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-mfmm3/H9+67MCVix1h+IXTpDwL6710LyHuk7+cWC9T1mE0qz4iHhh6r4hU2wrIT9iTsAAC2XQRvfblL028cpLw==",
 | 
				
			||||||
 | 
					      "license": "ISC",
 | 
				
			||||||
 | 
					      "bin": {
 | 
				
			||||||
 | 
					        "semver": "bin/semver"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/util-deprecate": {
 | 
					    "node_modules/util-deprecate": {
 | 
				
			||||||
      "version": "1.0.2",
 | 
					      "version": "1.0.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
 | 
				
			||||||
| 
						 | 
					@ -11386,6 +11577,11 @@
 | 
				
			||||||
        "node": ">= 0.4.0"
 | 
					        "node": ">= 0.4.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/uue": {
 | 
				
			||||||
 | 
					      "version": "1.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/uue/-/uue-1.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-gEwvMeDsv0Q4SFsjhp/PmsXizTfEn3B8SXqtRiz5jE2nsRgEBCY5W5gmxOFrPVzbnDCELwOKyMnOEoL/W/TG7w=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/uuid": {
 | 
					    "node_modules/uuid": {
 | 
				
			||||||
      "version": "11.1.0",
 | 
					      "version": "11.1.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,12 +43,15 @@
 | 
				
			||||||
    "cookie": "^1.0.2",
 | 
					    "cookie": "^1.0.2",
 | 
				
			||||||
    "cookie-parser": "^1.4.7",
 | 
					    "cookie-parser": "^1.4.7",
 | 
				
			||||||
    "dayjs": "^1.11.13",
 | 
					    "dayjs": "^1.11.13",
 | 
				
			||||||
 | 
					    "imap": "^0.8.19",
 | 
				
			||||||
 | 
					    "lodash": "^4.17.21",
 | 
				
			||||||
    "multer": "^1.4.5-lts.1",
 | 
					    "multer": "^1.4.5-lts.1",
 | 
				
			||||||
    "mysql2": "^3.13.0",
 | 
					    "mysql2": "^3.13.0",
 | 
				
			||||||
    "nestjs-paginate": "^11.1.0",
 | 
					    "nestjs-paginate": "^11.1.0",
 | 
				
			||||||
    "reflect-metadata": "^0.2.0",
 | 
					    "reflect-metadata": "^0.2.0",
 | 
				
			||||||
    "rxjs": "^7.8.1",
 | 
					    "rxjs": "^7.8.1",
 | 
				
			||||||
    "sharp": "^0.33.5",
 | 
					    "sharp": "^0.33.5",
 | 
				
			||||||
 | 
					    "simple-imap": "^0.1.6",
 | 
				
			||||||
    "typeorm": "^0.3.21",
 | 
					    "typeorm": "^0.3.21",
 | 
				
			||||||
    "uuid": "^11.1.0"
 | 
					    "uuid": "^11.1.0"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -60,7 +63,9 @@
 | 
				
			||||||
    "@types/cookie": "^0.6.0",
 | 
					    "@types/cookie": "^0.6.0",
 | 
				
			||||||
    "@types/cookie-parser": "^1.4.8",
 | 
					    "@types/cookie-parser": "^1.4.8",
 | 
				
			||||||
    "@types/express": "^4.17.17",
 | 
					    "@types/express": "^4.17.17",
 | 
				
			||||||
 | 
					    "@types/imap": "^0.8.42",
 | 
				
			||||||
    "@types/jest": "^29.5.2",
 | 
					    "@types/jest": "^29.5.2",
 | 
				
			||||||
 | 
					    "@types/lodash": "^4.17.16",
 | 
				
			||||||
    "@types/multer": "^1.4.12",
 | 
					    "@types/multer": "^1.4.12",
 | 
				
			||||||
    "@types/node": "^20.3.1",
 | 
					    "@types/node": "^20.3.1",
 | 
				
			||||||
    "@types/supertest": "^6.0.0",
 | 
					    "@types/supertest": "^6.0.0",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,7 @@ import {
 | 
				
			||||||
} from './system/routes/exclude-route';
 | 
					} from './system/routes/exclude-route';
 | 
				
			||||||
import { AuthorizationMiddleware } from './modules/admins/middlewares/authorization.middleware';
 | 
					import { AuthorizationMiddleware } from './modules/admins/middlewares/authorization.middleware';
 | 
				
			||||||
import { ClientAuthenticationMiddleware } from './modules/auth/middlewares/client-authentication.middleware';
 | 
					import { ClientAuthenticationMiddleware } from './modules/auth/middlewares/client-authentication.middleware';
 | 
				
			||||||
 | 
					import { NotificationModule } from './modules/notification/notification.module';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
| 
						 | 
					@ -22,6 +23,7 @@ import { ClientAuthenticationMiddleware } from './modules/auth/middlewares/clien
 | 
				
			||||||
    AppValidatorsModule,
 | 
					    AppValidatorsModule,
 | 
				
			||||||
    AuthModule,
 | 
					    AuthModule,
 | 
				
			||||||
    AdminsModule,
 | 
					    AdminsModule,
 | 
				
			||||||
 | 
					    NotificationModule,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  controllers: [],
 | 
					  controllers: [],
 | 
				
			||||||
  providers: [],
 | 
					  providers: [],
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,11 +1,16 @@
 | 
				
			||||||
import { Module } from '@nestjs/common';
 | 
					import { Module } from '@nestjs/common';
 | 
				
			||||||
import { ConfigModule } from '@nestjs/config';
 | 
					import { ConfigModule } from '@nestjs/config';
 | 
				
			||||||
 | 
					import { EventEmitterModule } from '@nestjs/event-emitter';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
    ConfigModule.forRoot({
 | 
					    ConfigModule.forRoot({
 | 
				
			||||||
      isGlobal: true,
 | 
					      isGlobal: true,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
 | 
					    EventEmitterModule.forRoot({
 | 
				
			||||||
 | 
					      wildcard: true,
 | 
				
			||||||
 | 
					      global: true,
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class AppConfigsModule {}
 | 
					export class AppConfigsModule {}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,11 @@
 | 
				
			||||||
import { Injectable } from '@nestjs/common';
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
import { ConfigService } from '@nestjs/config';
 | 
					import { ConfigService } from '@nestjs/config';
 | 
				
			||||||
import axios from 'axios';
 | 
					import axios from 'axios';
 | 
				
			||||||
import { escapeMarkdownV2 } from 'src/ultils';
 | 
					 | 
				
			||||||
import { Bid } from '../entities/bid.entity';
 | 
					 | 
				
			||||||
import * as dayjs from 'dayjs';
 | 
					import * as dayjs from 'dayjs';
 | 
				
			||||||
import { SendMessageHistoriesService } from '../services/send-message-histories.service';
 | 
					import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
 | 
				
			||||||
 | 
					import { join } from 'path';
 | 
				
			||||||
 | 
					import { Bid } from '../entities/bid.entity';
 | 
				
			||||||
 | 
					import { Constant } from '../utils/constant';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class BotTelegramApi {
 | 
					export class BotTelegramApi {
 | 
				
			||||||
| 
						 | 
					@ -64,6 +65,73 @@ export class BotTelegramApi {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async createFolderPath(): Promise<string> {
 | 
				
			||||||
 | 
					    const rootDir = process.cwd();
 | 
				
			||||||
 | 
					    const folderPath = join(rootDir, `${Constant.BOT_TELEGRAM_PATH}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!existsSync(folderPath)) {
 | 
				
			||||||
 | 
					      mkdirSync(folderPath, { recursive: true, mode: 0o777 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // ✅ Lưu metadata lần đầu
 | 
				
			||||||
 | 
					      const metadataPath = join(folderPath, 'metadata.json');
 | 
				
			||||||
 | 
					      writeFileSync(
 | 
				
			||||||
 | 
					        metadataPath,
 | 
				
			||||||
 | 
					        JSON.stringify({ createdAt: Date.now() }),
 | 
				
			||||||
 | 
					        'utf-8',
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return folderPath;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async getGroupInfo(
 | 
				
			||||||
 | 
					    chatId: string = this.configService.get<string>('CHAT_ID'),
 | 
				
			||||||
 | 
					  ): Promise<any> {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const folderPath = await this.createFolderPath();
 | 
				
			||||||
 | 
					      const metadataPath = join(folderPath, 'metadata.json');
 | 
				
			||||||
 | 
					      const dataFilePath = join(folderPath, `group_${chatId}.json`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 10 minute
 | 
				
			||||||
 | 
					      const TIME_TO_REFRESH_DATA = 10;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (existsSync(metadataPath)) {
 | 
				
			||||||
 | 
					        const metadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
 | 
				
			||||||
 | 
					        const createdAt = metadata?.createdAt || 0;
 | 
				
			||||||
 | 
					        const now = Date.now();
 | 
				
			||||||
 | 
					        const diffMinutes = (now - createdAt) / 60000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (diffMinutes < TIME_TO_REFRESH_DATA && existsSync(dataFilePath)) {
 | 
				
			||||||
 | 
					          return JSON.parse(readFileSync(dataFilePath, 'utf-8'));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const url = `${this.apiUrl}/getChat`;
 | 
				
			||||||
 | 
					      const { data } = await axios({
 | 
				
			||||||
 | 
					        url,
 | 
				
			||||||
 | 
					        params: { chat_id: chatId },
 | 
				
			||||||
 | 
					        family: 4,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (data?.ok) {
 | 
				
			||||||
 | 
					        writeFileSync(
 | 
				
			||||||
 | 
					          dataFilePath,
 | 
				
			||||||
 | 
					          JSON.stringify(data.result, null, 2),
 | 
				
			||||||
 | 
					          'utf-8',
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        writeFileSync(
 | 
				
			||||||
 | 
					          metadataPath,
 | 
				
			||||||
 | 
					          JSON.stringify({ createdAt: Date.now() }),
 | 
				
			||||||
 | 
					          'utf-8',
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return data.result;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error(error || error.message);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async sendBidInfo(bid: Bid): Promise<boolean> {
 | 
					  async sendBidInfo(bid: Bid): Promise<boolean> {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const text = this.formatBidMessage(bid);
 | 
					      const text = this.formatBidMessage(bid);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,13 @@
 | 
				
			||||||
import { Module } from '@nestjs/common';
 | 
					import { Module } from '@nestjs/common';
 | 
				
			||||||
import { EventEmitterModule } from '@nestjs/event-emitter';
 | 
					 | 
				
			||||||
import { TypeOrmModule } from '@nestjs/typeorm';
 | 
					import { TypeOrmModule } from '@nestjs/typeorm';
 | 
				
			||||||
 | 
					import { AdminsModule } from '../admins/admins.module';
 | 
				
			||||||
 | 
					import { NotificationModule } from '../notification/notification.module';
 | 
				
			||||||
 | 
					import { BotTelegramApi } from './apis/bot-telegram.api';
 | 
				
			||||||
 | 
					import { GraysApi } from './apis/grays.api';
 | 
				
			||||||
import { AdminBidHistoriesController } from './controllers/admin/admin-bid-histories.controller';
 | 
					import { AdminBidHistoriesController } from './controllers/admin/admin-bid-histories.controller';
 | 
				
			||||||
import { AdminBidsController } from './controllers/admin/admin-bids.controller';
 | 
					import { AdminBidsController } from './controllers/admin/admin-bids.controller';
 | 
				
			||||||
import { AdminOutBidLogsController } from './controllers/admin/admin-out-bid-logs.controller';
 | 
					import { AdminOutBidLogsController } from './controllers/admin/admin-out-bid-logs.controller';
 | 
				
			||||||
 | 
					import { AdminSendMessageHistoriesController } from './controllers/admin/admin-send-message-histories.controller';
 | 
				
			||||||
import { AdminWebBidsController } from './controllers/admin/admin-web-bids.controller';
 | 
					import { AdminWebBidsController } from './controllers/admin/admin-web-bids.controller';
 | 
				
			||||||
import { BidHistoriesController } from './controllers/client/bid-histories.controller';
 | 
					import { BidHistoriesController } from './controllers/client/bid-histories.controller';
 | 
				
			||||||
import { BidsController } from './controllers/client/bids.controller';
 | 
					import { BidsController } from './controllers/client/bids.controller';
 | 
				
			||||||
| 
						 | 
					@ -11,20 +15,16 @@ import { OutBidLogsController } from './controllers/client/out-bid-logs.controll
 | 
				
			||||||
import { BidHistory } from './entities/bid-history.entity';
 | 
					import { BidHistory } from './entities/bid-history.entity';
 | 
				
			||||||
import { Bid } from './entities/bid.entity';
 | 
					import { Bid } from './entities/bid.entity';
 | 
				
			||||||
import { OutBidLog } from './entities/out-bid-log.entity';
 | 
					import { OutBidLog } from './entities/out-bid-log.entity';
 | 
				
			||||||
 | 
					import { SendMessageHistory } from './entities/send-message-histories.entity';
 | 
				
			||||||
import { WebBid } from './entities/wed-bid.entity';
 | 
					import { WebBid } from './entities/wed-bid.entity';
 | 
				
			||||||
 | 
					import { AdminBidGateway } from './getways/admin-bid-getway';
 | 
				
			||||||
import { BidGateway } from './getways/bid-getway';
 | 
					import { BidGateway } from './getways/bid-getway';
 | 
				
			||||||
import { BidHistoriesService } from './services/bid-histories.service';
 | 
					import { BidHistoriesService } from './services/bid-histories.service';
 | 
				
			||||||
import { BidsService } from './services/bids.service';
 | 
					import { BidsService } from './services/bids.service';
 | 
				
			||||||
 | 
					import { ImapService } from './services/mail/imap.service';
 | 
				
			||||||
import { OutBidLogsService } from './services/out-bid-logs.service';
 | 
					import { OutBidLogsService } from './services/out-bid-logs.service';
 | 
				
			||||||
import { WebBidsService } from './services/web-bids.service';
 | 
					 | 
				
			||||||
import { BotTelegramApi } from './apis/bot-telegram.api';
 | 
					 | 
				
			||||||
import { GraysApi } from './apis/grays.api';
 | 
					 | 
				
			||||||
import { SendMessageHistory } from './entities/send-message-histories.entity';
 | 
					 | 
				
			||||||
import { SendMessageHistoriesService } from './services/send-message-histories.service';
 | 
					import { SendMessageHistoriesService } from './services/send-message-histories.service';
 | 
				
			||||||
import { AdminSendMessageHistoriesController } from './controllers/admin/admin-send-message-histories.controller';
 | 
					import { WebBidsService } from './services/web-bids.service';
 | 
				
			||||||
import { AuthModule } from '../auth/auth.module';
 | 
					 | 
				
			||||||
import { AdminsModule } from '../admins/admins.module';
 | 
					 | 
				
			||||||
import { AdminBidGateway } from './getways/admin-bid-getway';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
| 
						 | 
					@ -35,11 +35,9 @@ import { AdminBidGateway } from './getways/admin-bid-getway';
 | 
				
			||||||
      WebBid,
 | 
					      WebBid,
 | 
				
			||||||
      SendMessageHistory,
 | 
					      SendMessageHistory,
 | 
				
			||||||
    ]),
 | 
					    ]),
 | 
				
			||||||
    EventEmitterModule.forRoot({
 | 
					 | 
				
			||||||
      wildcard: true,
 | 
					 | 
				
			||||||
    }),
 | 
					 | 
				
			||||||
    // AuthModule,
 | 
					    // AuthModule,
 | 
				
			||||||
    AdminsModule,
 | 
					    AdminsModule,
 | 
				
			||||||
 | 
					    NotificationModule,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  controllers: [
 | 
					  controllers: [
 | 
				
			||||||
    BidsController,
 | 
					    BidsController,
 | 
				
			||||||
| 
						 | 
					@ -61,6 +59,8 @@ import { AdminBidGateway } from './getways/admin-bid-getway';
 | 
				
			||||||
    BotTelegramApi,
 | 
					    BotTelegramApi,
 | 
				
			||||||
    GraysApi,
 | 
					    GraysApi,
 | 
				
			||||||
    SendMessageHistoriesService,
 | 
					    SendMessageHistoriesService,
 | 
				
			||||||
 | 
					    ImapService,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
 | 
					  exports: [BotTelegramApi, SendMessageHistoriesService, BidsService],
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class BidsModule {}
 | 
					export class BidsModule {}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -59,4 +59,12 @@ export class AdminBidsController {
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    return await this.bidsService.getStatusWorkingImage(id, type, name, res);
 | 
					    return await this.bidsService.getStatusWorkingImage(id, type, name, res);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Get('images-working/:type/:id')
 | 
				
			||||||
 | 
					  async getImagesWorking(
 | 
				
			||||||
 | 
					    @Param('id') id: Bid['id'],
 | 
				
			||||||
 | 
					    @Param('type') type: string,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    return await this.bidsService.getImagesWorking(id, type);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,22 +5,30 @@ import {
 | 
				
			||||||
  Param,
 | 
					  Param,
 | 
				
			||||||
  Post,
 | 
					  Post,
 | 
				
			||||||
  Put,
 | 
					  Put,
 | 
				
			||||||
  Res,
 | 
					 | 
				
			||||||
  UploadedFile,
 | 
					  UploadedFile,
 | 
				
			||||||
  UseInterceptors,
 | 
					  UseInterceptors,
 | 
				
			||||||
} from '@nestjs/common';
 | 
					} from '@nestjs/common';
 | 
				
			||||||
 | 
					import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
				
			||||||
 | 
					import { FileInterceptor } from '@nestjs/platform-express';
 | 
				
			||||||
 | 
					import { plainToClass } from 'class-transformer';
 | 
				
			||||||
 | 
					import { memoryStorage } from 'multer';
 | 
				
			||||||
import { ClientUpdateBidDto } from '../../dto/bid/client-update-bid.dto';
 | 
					import { ClientUpdateBidDto } from '../../dto/bid/client-update-bid.dto';
 | 
				
			||||||
import { CreateBidDto } from '../../dto/bid/create-bid.dto';
 | 
					import { CreateBidDto } from '../../dto/bid/create-bid.dto';
 | 
				
			||||||
import { Bid } from '../../entities/bid.entity';
 | 
					 | 
				
			||||||
import { BidsService } from '../../services/bids.service';
 | 
					 | 
				
			||||||
import { UpdateStatusByPriceDto } from '../../dto/bid/update-status-by-price.dto';
 | 
					import { UpdateStatusByPriceDto } from '../../dto/bid/update-status-by-price.dto';
 | 
				
			||||||
import { FileInterceptor } from '@nestjs/platform-express';
 | 
					import { Bid } from '../../entities/bid.entity';
 | 
				
			||||||
import { memoryStorage } from 'multer';
 | 
					import { WebBid } from '../../entities/wed-bid.entity';
 | 
				
			||||||
import { Response } from 'express';
 | 
					import { BidsService } from '../../services/bids.service';
 | 
				
			||||||
 | 
					import { WebBidsService } from '../../services/web-bids.service';
 | 
				
			||||||
 | 
					import { Event } from '../../utils/events';
 | 
				
			||||||
 | 
					import AppResponse from '@/response/app-response';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Controller('bids')
 | 
					@Controller('bids')
 | 
				
			||||||
export class BidsController {
 | 
					export class BidsController {
 | 
				
			||||||
  constructor(private readonly bidsService: BidsService) {}
 | 
					  constructor(
 | 
				
			||||||
 | 
					    private readonly bidsService: BidsService,
 | 
				
			||||||
 | 
					    private readonly eventEmitter: EventEmitter2,
 | 
				
			||||||
 | 
					    private readonly webBidService: WebBidsService,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Get()
 | 
					  @Get()
 | 
				
			||||||
  index() {
 | 
					  index() {
 | 
				
			||||||
| 
						 | 
					@ -59,4 +67,21 @@ export class BidsController {
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    return this.bidsService.updateStatusWork(id, type, image);
 | 
					    return this.bidsService.updateStatusWork(id, type, image);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Post('test')
 | 
				
			||||||
 | 
					  async test(@Body('code') code: string) {
 | 
				
			||||||
 | 
					    const webBid = await this.webBidService.webBidRepo.findOne({
 | 
				
			||||||
 | 
					      // where: { id: 9 },
 | 
				
			||||||
 | 
					      where: { id: 8 },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.eventEmitter.emit(Event.verifyCode(webBid), {
 | 
				
			||||||
 | 
					      code,
 | 
				
			||||||
 | 
					      // name: 'LAWSONS',
 | 
				
			||||||
 | 
					      name: 'LANGTONS',
 | 
				
			||||||
 | 
					      web_bid: plainToClass(WebBid, webBid),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toResponse({ code });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
import { Body, Controller, Post } from '@nestjs/common';
 | 
					import { Body, Controller, Post } from '@nestjs/common';
 | 
				
			||||||
import { CreateOutBidLogsDto } from '../../dto/out-bid-log/create-out-bid-logs.dto';
 | 
					import { CreateOutBidLogsDto } from '../../dto/out-bid-log/create-out-bid-logs.dto';
 | 
				
			||||||
import { OutBidLogsService } from '../../services/out-bid-logs.service';
 | 
					import { OutBidLogsService } from '../../services/out-bid-logs.service';
 | 
				
			||||||
 | 
					import { CreateOutBidLogDto } from '../../dto/out-bid-log/create-out-bid-log.dto';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Controller('out-bid-logs')
 | 
					@Controller('out-bid-logs')
 | 
				
			||||||
export class OutBidLogsController {
 | 
					export class OutBidLogsController {
 | 
				
			||||||
| 
						 | 
					@ -10,4 +11,9 @@ export class OutBidLogsController {
 | 
				
			||||||
  create(@Body() data: CreateOutBidLogsDto) {
 | 
					  create(@Body() data: CreateOutBidLogsDto) {
 | 
				
			||||||
    return this.outBidLogsService.create(data.data);
 | 
					    return this.outBidLogsService.create(data.data);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Post('simple')
 | 
				
			||||||
 | 
					  createSimple(@Body() data: CreateOutBidLogDto) {
 | 
				
			||||||
 | 
					    return this.outBidLogsService.createSimple(data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,10 @@ export class ClientUpdateBidDto {
 | 
				
			||||||
  @IsOptional()
 | 
					  @IsOptional()
 | 
				
			||||||
  lot_id: string;
 | 
					  lot_id: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  model: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @IsOptional()
 | 
					  @IsOptional()
 | 
				
			||||||
  @IsString()
 | 
					  @IsString()
 | 
				
			||||||
  close_time: string | null;
 | 
					  close_time: string | null;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,6 +16,6 @@ export class OutBidLog extends Timestamp {
 | 
				
			||||||
  @Column()
 | 
					  @Column()
 | 
				
			||||||
  out_price: number;
 | 
					  out_price: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Column({ default: null, nullable: true })
 | 
					  @Column({ default: null, nullable: true, type: 'text' })
 | 
				
			||||||
  raw_data: string;
 | 
					  raw_data: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,6 +10,9 @@ export class SendMessageHistory extends Timestamp {
 | 
				
			||||||
  @Column({ default: null, nullable: true, type: 'text' })
 | 
					  @Column({ default: null, nullable: true, type: 'text' })
 | 
				
			||||||
  message: string;
 | 
					  message: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ default: 'biding' })
 | 
				
			||||||
 | 
					  type: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @ManyToOne(() => Bid, (bid) => bid.sendMessageHistories, {
 | 
					  @ManyToOne(() => Bid, (bid) => bid.sendMessageHistories, {
 | 
				
			||||||
    onDelete: 'CASCADE',
 | 
					    onDelete: 'CASCADE',
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,20 +1,20 @@
 | 
				
			||||||
 | 
					import { AdminsService } from '@/modules/admins/services/admins.service';
 | 
				
			||||||
 | 
					import { getWayMiddleware } from '@/modules/auth/middlewares/get-way.middleware';
 | 
				
			||||||
 | 
					import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
				
			||||||
 | 
					import { JwtService } from '@nestjs/jwt';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
 | 
					  OnGatewayConnection,
 | 
				
			||||||
  WebSocketGateway,
 | 
					  WebSocketGateway,
 | 
				
			||||||
  WebSocketServer,
 | 
					  WebSocketServer,
 | 
				
			||||||
  SubscribeMessage,
 | 
					 | 
				
			||||||
  OnGatewayConnection,
 | 
					 | 
				
			||||||
} from '@nestjs/websockets';
 | 
					} from '@nestjs/websockets';
 | 
				
			||||||
import { Server, Socket } from 'socket.io';
 | 
					 | 
				
			||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
					 | 
				
			||||||
import { BidsService } from '../services/bids.service';
 | 
					 | 
				
			||||||
import { WebBidsService } from '../services/web-bids.service';
 | 
					 | 
				
			||||||
import { plainToClass } from 'class-transformer';
 | 
					import { plainToClass } from 'class-transformer';
 | 
				
			||||||
 | 
					import { Server, Socket } from 'socket.io';
 | 
				
			||||||
import { WebBid } from '../entities/wed-bid.entity';
 | 
					import { WebBid } from '../entities/wed-bid.entity';
 | 
				
			||||||
import * as cookie from 'cookie';
 | 
					import { WebBidsService } from '../services/web-bids.service';
 | 
				
			||||||
import { Constant } from '@/modules/auth/ultils/constant';
 | 
					import * as Imap from 'imap';
 | 
				
			||||||
import { getWayMiddleware } from '@/modules/auth/middlewares/get-way.middleware';
 | 
					import { ImapService } from '../services/mail/imap.service';
 | 
				
			||||||
import { AdminsService } from '@/modules/admins/services/admins.service';
 | 
					import { ConfigService } from '@nestjs/config';
 | 
				
			||||||
import { JwtService } from '@nestjs/jwt';
 | 
					import { Event } from '../utils/events';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@WebSocketGateway({
 | 
					@WebSocketGateway({
 | 
				
			||||||
  namespace: 'admin-bid-ws',
 | 
					  namespace: 'admin-bid-ws',
 | 
				
			||||||
| 
						 | 
					@ -33,16 +33,22 @@ export class AdminBidGateway implements OnGatewayConnection {
 | 
				
			||||||
    private readonly jwtService: JwtService,
 | 
					    private readonly jwtService: JwtService,
 | 
				
			||||||
    private webBidsService: WebBidsService,
 | 
					    private webBidsService: WebBidsService,
 | 
				
			||||||
    private readonly adminService: AdminsService,
 | 
					    private readonly adminService: AdminsService,
 | 
				
			||||||
  ) {}
 | 
					    private readonly imapService: ImapService,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    this.imapService.server = this.server;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async onModuleInit() {
 | 
					  async onModuleInit() {
 | 
				
			||||||
    this.eventEmitter.on('bids.updated', (data) => {
 | 
					    this.eventEmitter.on(Event.BIDS_UPDATED, (data) => {
 | 
				
			||||||
      this.server.emit('adminBidsUpdated', plainToClass(WebBid, data));
 | 
					      this.server.emit(Event.ADMIN_BIDS_UPDATED, plainToClass(WebBid, data));
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.eventEmitter.on('working', (data) => {
 | 
					    this.eventEmitter.on(Event.WORKING, (data) => {
 | 
				
			||||||
      this.server.emit('working', data);
 | 
					      this.server.emit(Event.WORKING, data);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // IMAP
 | 
				
			||||||
 | 
					    this.imapService.connectIMAP();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async handleConnection(client: Socket) {
 | 
					  async handleConnection(client: Socket) {
 | 
				
			||||||
| 
						 | 
					@ -51,7 +57,8 @@ export class AdminBidGateway implements OnGatewayConnection {
 | 
				
			||||||
    console.log(`📢 Client connected: ${client.id}`);
 | 
					    console.log(`📢 Client connected: ${client.id}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const data = await this.webBidsService.getDataClient();
 | 
					    const data = await this.webBidsService.getDataClient();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Gửi dữ liệu bids ngay khi client kết nối
 | 
					    // Gửi dữ liệu bids ngay khi client kết nối
 | 
				
			||||||
    client.emit('adminBidsUpdated', plainToClass(WebBid, data));
 | 
					    client.emit(Event.ADMIN_BIDS_UPDATED, plainToClass(WebBid, data));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,8 @@ import {
 | 
				
			||||||
import { Server, Socket } from 'socket.io';
 | 
					import { Server, Socket } from 'socket.io';
 | 
				
			||||||
import { BidsService } from '../services/bids.service';
 | 
					import { BidsService } from '../services/bids.service';
 | 
				
			||||||
import { WebBidsService } from '../services/web-bids.service';
 | 
					import { WebBidsService } from '../services/web-bids.service';
 | 
				
			||||||
 | 
					import { Event } from '../utils/events';
 | 
				
			||||||
 | 
					import { WebBid } from '../entities/wed-bid.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@WebSocketGateway({
 | 
					@WebSocketGateway({
 | 
				
			||||||
  namespace: 'bid-ws',
 | 
					  namespace: 'bid-ws',
 | 
				
			||||||
| 
						 | 
					@ -29,13 +31,24 @@ export class BidGateway implements OnGatewayConnection {
 | 
				
			||||||
  ) {}
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async onModuleInit() {
 | 
					  async onModuleInit() {
 | 
				
			||||||
    this.eventEmitter.on('bids.updated', (data) => {
 | 
					    this.eventEmitter.on(Event.BIDS_UPDATED, (data) => {
 | 
				
			||||||
      this.server.emit('bidsUpdated', data);
 | 
					      this.server.emit(Event.BIDS_UPDATED, data);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.eventEmitter.on('web.updated', (data) => {
 | 
					    this.eventEmitter.on(Event.WEB_UPDATED, (data) => {
 | 
				
			||||||
      this.server.emit('webUpdated', data);
 | 
					      this.server.emit(Event.WEB_UPDATED, data);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.eventEmitter.onAny(
 | 
				
			||||||
 | 
					      (
 | 
				
			||||||
 | 
					        event: string,
 | 
				
			||||||
 | 
					        payload: { name: string; code: string; web_bid: WebBid },
 | 
				
			||||||
 | 
					      ) => {
 | 
				
			||||||
 | 
					        if (!event.startsWith(Event.VERIFY_CODE)) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.server.emit(Event.verifyCode(payload.web_bid), payload);
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async handleConnection(client: Socket) {
 | 
					  async handleConnection(client: Socket) {
 | 
				
			||||||
| 
						 | 
					@ -45,6 +58,6 @@ export class BidGateway implements OnGatewayConnection {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const data = await this.webBidsService.getDataClient();
 | 
					    const data = await this.webBidsService.getDataClient();
 | 
				
			||||||
    // Gửi dữ liệu bids ngay khi client kết nối
 | 
					    // Gửi dữ liệu bids ngay khi client kết nối
 | 
				
			||||||
    client.emit('bidsUpdated', data);
 | 
					    client.emit(Event.BIDS_UPDATED, data);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,16 +13,21 @@ import { Bid } from '../entities/bid.entity';
 | 
				
			||||||
import { CreateBidHistoryDto } from '../dto/bid-history/create-bid-history.dto';
 | 
					import { CreateBidHistoryDto } from '../dto/bid-history/create-bid-history.dto';
 | 
				
			||||||
import { BotTelegramApi } from '../apis/bot-telegram.api';
 | 
					import { BotTelegramApi } from '../apis/bot-telegram.api';
 | 
				
			||||||
import { SendMessageHistoriesService } from './send-message-histories.service';
 | 
					import { SendMessageHistoriesService } from './send-message-histories.service';
 | 
				
			||||||
 | 
					import { NotificationService } from '@/modules/notification/notification.service';
 | 
				
			||||||
 | 
					import { isTimeReached } from '@/ultils';
 | 
				
			||||||
 | 
					import { BidsService } from './bids.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class BidHistoriesService {
 | 
					export class BidHistoriesService {
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    @InjectRepository(BidHistory)
 | 
					    @InjectRepository(BidHistory)
 | 
				
			||||||
    readonly bidHistoriesRepo: Repository<BidHistory>,
 | 
					    readonly bidHistoriesRepo: Repository<BidHistory>,
 | 
				
			||||||
    @InjectRepository(Bid)
 | 
					    // @InjectRepository(Bid)
 | 
				
			||||||
    readonly bidsRepo: Repository<Bid>,
 | 
					    // readonly bidsRepo: Repository<Bid>,
 | 
				
			||||||
    private readonly botTelegramApi: BotTelegramApi,
 | 
					    private readonly botTelegramApi: BotTelegramApi,
 | 
				
			||||||
    readonly sendMessageHistoriesService: SendMessageHistoriesService,
 | 
					    readonly sendMessageHistoriesService: SendMessageHistoriesService,
 | 
				
			||||||
 | 
					    private readonly notificationService: NotificationService,
 | 
				
			||||||
 | 
					    private readonly bidsService: BidsService,
 | 
				
			||||||
  ) {}
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async index() {
 | 
					  async index() {
 | 
				
			||||||
| 
						 | 
					@ -30,8 +35,12 @@ export class BidHistoriesService {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async create({ price, bid_id }: CreateBidHistoryDto) {
 | 
					  async create({ price, bid_id }: CreateBidHistoryDto) {
 | 
				
			||||||
    const bid = await this.bidsRepo.findOne({ where: { id: bid_id } });
 | 
					    // Tìm thông tin bid từ database
 | 
				
			||||||
 | 
					    const bid = await this.bidsService.bidsRepo.findOne({
 | 
				
			||||||
 | 
					      where: { id: bid_id },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Nếu không tìm thấy bid, trả về lỗi 404
 | 
				
			||||||
    if (!bid)
 | 
					    if (!bid)
 | 
				
			||||||
      throw new NotFoundException(
 | 
					      throw new NotFoundException(
 | 
				
			||||||
        AppResponse.toResponse(null, {
 | 
					        AppResponse.toResponse(null, {
 | 
				
			||||||
| 
						 | 
					@ -40,20 +49,34 @@ export class BidHistoriesService {
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Lấy lịch sử đặt giá cao nhất trước đó của bid hiện tại
 | 
				
			||||||
    const lastHistory = await this.bidHistoriesRepo.findOne({
 | 
					    const lastHistory = await this.bidHistoriesRepo.findOne({
 | 
				
			||||||
      where: { bid: { id: bid_id } },
 | 
					      where: { bid: { id: bid_id } },
 | 
				
			||||||
      order: { price: 'desc' },
 | 
					      order: { price: 'desc' },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Nếu đã có lịch sử và giá mới giống với giá cao nhất hiện tại
 | 
				
			||||||
    if (lastHistory && lastHistory.price === price) {
 | 
					    if (lastHistory && lastHistory.price === price) {
 | 
				
			||||||
 | 
					      // Nếu đã hết thời gian đấu giá, cập nhật trạng thái là 'win-bid'
 | 
				
			||||||
 | 
					      if (isTimeReached(bid.close_time)) {
 | 
				
			||||||
 | 
					        this.bidsService.bidsRepo.update(bid_id, { status: 'win-bid' });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Ném lỗi không cho đặt giá trùng lặp
 | 
				
			||||||
      throw new BadRequestException(
 | 
					      throw new BadRequestException(
 | 
				
			||||||
        AppResponse.toResponse(null, { message: 'Duplicate place bid' }),
 | 
					        AppResponse.toResponse(null, { message: 'Duplicate place bid' }),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Nếu tổng giá vượt quá mức tối đa cho phép
 | 
				
			||||||
    if (price + bid.plus_price > bid.max_price) {
 | 
					    if (price + bid.plus_price > bid.max_price) {
 | 
				
			||||||
      this.bidsRepo.update(bid_id, { status: 'out-bid' });
 | 
					      // Cập nhật trạng thái bid là 'out-bid'
 | 
				
			||||||
 | 
					      this.bidsService.bidsRepo.update(bid_id, { status: 'out-bid' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Gửi thông báo trạng thái mới qua service thông báo
 | 
				
			||||||
 | 
					      this.notificationService.emitBidStatus({ ...bid, status: 'out-bid' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Ném lỗi không cho đặt giá vượt mức
 | 
				
			||||||
      throw new BadRequestException(
 | 
					      throw new BadRequestException(
 | 
				
			||||||
        AppResponse.toResponse(null, {
 | 
					        AppResponse.toResponse(null, {
 | 
				
			||||||
          message: 'Price is more than Max price ' + bid.max_price,
 | 
					          message: 'Price is more than Max price ' + bid.max_price,
 | 
				
			||||||
| 
						 | 
					@ -61,8 +84,10 @@ export class BidHistoriesService {
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Lưu lịch sử đặt giá mới
 | 
				
			||||||
    await this.bidHistoriesRepo.save({ bid, price });
 | 
					    await this.bidHistoriesRepo.save({ bid, price });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Lấy danh sách tất cả lịch sử đặt giá theo thứ tự mới nhất
 | 
				
			||||||
    const response = await this.bidHistoriesRepo.find({
 | 
					    const response = await this.bidHistoriesRepo.find({
 | 
				
			||||||
      where: { bid: { id: bid_id } },
 | 
					      where: { bid: { id: bid_id } },
 | 
				
			||||||
      order: {
 | 
					      order: {
 | 
				
			||||||
| 
						 | 
					@ -70,19 +95,29 @@ export class BidHistoriesService {
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Nếu đây là lần đặt giá đầu tiên, cập nhật cờ `first_bid` thành false
 | 
				
			||||||
    if (response.length === 1) {
 | 
					    if (response.length === 1) {
 | 
				
			||||||
      this.bidsRepo.update(bid_id, { first_bid: false });
 | 
					      this.bidsService.bidsRepo.update(bid_id, { first_bid: false });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Gửi thông tin bid đến bot telegram
 | 
				
			||||||
    const botData = { ...bid, histories: response };
 | 
					    const botData = { ...bid, histories: response };
 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.botTelegramApi.sendBidInfo(botData);
 | 
					    this.botTelegramApi.sendBidInfo(botData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Lưu message đã gửi để theo dõi
 | 
				
			||||||
    this.sendMessageHistoriesService.sendMessageRepo.save({
 | 
					    this.sendMessageHistoriesService.sendMessageRepo.save({
 | 
				
			||||||
      message: this.botTelegramApi.formatBidMessage(botData),
 | 
					      message: this.botTelegramApi.formatBidMessage(botData),
 | 
				
			||||||
      bid,
 | 
					      bid,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Kiểm tra nếu trạng thái bid thay đổi sau khi lưu, phát sự kiện cập nhật tất cả bid
 | 
				
			||||||
 | 
					    const bidUpdated = await this.bidsService.bidsRepo.findOne({
 | 
				
			||||||
 | 
					      where: { id: bid_id },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.bidsService.emitAllBidEvent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Trả về danh sách lịch sử đặt giá đã cập nhật
 | 
				
			||||||
    return AppResponse.toResponse(plainToClass(BidHistory, response));
 | 
					    return AppResponse.toResponse(plainToClass(BidHistory, response));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,6 +29,8 @@ import { Bid } from '../entities/bid.entity';
 | 
				
			||||||
import { ImageCompressionPipe } from '../pipes/image-compression-pipe';
 | 
					import { ImageCompressionPipe } from '../pipes/image-compression-pipe';
 | 
				
			||||||
import { Constant } from '../utils/constant';
 | 
					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 { Event } from '../utils/events';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class BidsService {
 | 
					export class BidsService {
 | 
				
			||||||
| 
						 | 
					@ -39,6 +41,7 @@ export class BidsService {
 | 
				
			||||||
    readonly bidHistoriesRepo: Repository<BidHistory>,
 | 
					    readonly bidHistoriesRepo: Repository<BidHistory>,
 | 
				
			||||||
    private readonly webBidsService: WebBidsService,
 | 
					    private readonly webBidsService: WebBidsService,
 | 
				
			||||||
    private eventEmitter: EventEmitter2,
 | 
					    private eventEmitter: EventEmitter2,
 | 
				
			||||||
 | 
					    private notificationService: NotificationService,
 | 
				
			||||||
  ) {}
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async index(query: PaginateQuery) {
 | 
					  async index(query: PaginateQuery) {
 | 
				
			||||||
| 
						 | 
					@ -74,6 +77,7 @@ export class BidsService {
 | 
				
			||||||
      maxLimit: 100,
 | 
					      maxLimit: 100,
 | 
				
			||||||
      relations: {
 | 
					      relations: {
 | 
				
			||||||
        histories: true,
 | 
					        histories: true,
 | 
				
			||||||
 | 
					        web_bid: true,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -97,6 +101,7 @@ export class BidsService {
 | 
				
			||||||
    const model = extractModelId(data.url);
 | 
					    const model = extractModelId(data.url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const prev = await this.bidsRepo.findOne({ where: { model: model } });
 | 
					    const prev = await this.bidsRepo.findOne({ where: { model: model } });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (prev) {
 | 
					    if (prev) {
 | 
				
			||||||
      throw new BadRequestException(
 | 
					      throw new BadRequestException(
 | 
				
			||||||
        AppResponse.toResponse(null, { message: 'Product is already exits' }),
 | 
					        AppResponse.toResponse(null, { message: 'Product is already exits' }),
 | 
				
			||||||
| 
						 | 
					@ -159,6 +164,9 @@ export class BidsService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.emitAllBidEvent();
 | 
					      this.emitAllBidEvent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // send message event
 | 
				
			||||||
 | 
					      this.notificationService.emitBidStatus({ ...bid, status: 'out-bid' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return AppResponse.toResponse(true);
 | 
					      return AppResponse.toResponse(true);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -183,16 +191,29 @@ export class BidsService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await this.bidsRepo.update(id, { status: 'biding' });
 | 
					    await this.bidsRepo.update(id, { status: 'biding' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // send message event
 | 
				
			||||||
 | 
					    this.notificationService.emitBidStatus({ ...bid, status: 'biding' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.emitAllBidEvent();
 | 
					    this.emitAllBidEvent();
 | 
				
			||||||
    return AppResponse.toResponse(true);
 | 
					    return AppResponse.toResponse(true);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async clientUpdate(
 | 
					  async clientUpdate(
 | 
				
			||||||
    id: Bid['id'],
 | 
					    id: Bid['id'],
 | 
				
			||||||
    { close_time, ...data }: ClientUpdateBidDto,
 | 
					    { close_time, model, ...data }: ClientUpdateBidDto, // Nhận dữ liệu cập nhật
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    const bid = await this.bidsRepo.findOne({ where: { id } });
 | 
					    // Tìm kiếm phiên đấu giá trong database theo id
 | 
				
			||||||
 | 
					    const bid = await this.bidsRepo.findOne({
 | 
				
			||||||
 | 
					      where: { id },
 | 
				
			||||||
 | 
					      relations: { histories: true },
 | 
				
			||||||
 | 
					      order: {
 | 
				
			||||||
 | 
					        histories: {
 | 
				
			||||||
 | 
					          price: 'DESC',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Nếu không tìm thấy phiên đấu giá, trả về lỗi 404
 | 
				
			||||||
    if (!bid)
 | 
					    if (!bid)
 | 
				
			||||||
      throw new NotFoundException(
 | 
					      throw new NotFoundException(
 | 
				
			||||||
        AppResponse.toResponse(null, {
 | 
					        AppResponse.toResponse(null, {
 | 
				
			||||||
| 
						 | 
					@ -201,24 +222,35 @@ export class BidsService {
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Nếu phiên đấu giá chưa có thời gian bắt đầu và kết thúc
 | 
				
			||||||
    if (!bid.close_time && !bid.start_bid_time) {
 | 
					    if (!bid.close_time && !bid.start_bid_time) {
 | 
				
			||||||
 | 
					      // Thiết lập thời gian bắt đầu là 5 phút trước khi đóng
 | 
				
			||||||
      // bid.start_bid_time = new Date().toUTCString();
 | 
					      // bid.start_bid_time = new Date().toUTCString();
 | 
				
			||||||
      bid.start_bid_time = subtractMinutes(close_time, 5);
 | 
					      bid.start_bid_time = subtractMinutes(close_time, 5);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (
 | 
					    // Kiểm tra nếu thời gian đóng bid đã đạt tới (tức phiên đấu giá đã kết thúc)
 | 
				
			||||||
      data.current_price >= bid.max_price + bid.plus_price ||
 | 
					    if (bid.close_time && isTimeReached(bid.close_time)) {
 | 
				
			||||||
      (bid.close_time && isTimeReached(bid.close_time))
 | 
					      const bidHistoriesItem = bid.histories[0]; // Lấy lịch sử bid gần nhất (mới nhất)
 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
      console.log({
 | 
					 | 
				
			||||||
        a: data.current_price >= bid.max_price + bid.plus_price,
 | 
					 | 
				
			||||||
        b: bid.close_time && !close_time,
 | 
					 | 
				
			||||||
        c: bid.close_time && isTimeReached(bid.close_time),
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      bid.status = 'out-bid';
 | 
					      if (
 | 
				
			||||||
 | 
					        !bidHistoriesItem ||
 | 
				
			||||||
 | 
					        // Nếu giá cuối cùng không phải là giá của người dùng và giá hiện tại vượt quá mức người dùng đặt + bước giá
 | 
				
			||||||
 | 
					        (bidHistoriesItem.price !== data.current_price &&
 | 
				
			||||||
 | 
					          data.current_price > bid.max_price + bid.plus_price)
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        bid.status = 'out-bid'; // Người dùng đã bị outbid khi đấu giá kết thúc
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        bid.status = 'win-bid'; // Người dùng là người thắng nếu không bị outbid
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } 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
 | 
				
			||||||
 | 
					      if (data.current_price > bid.max_price + bid.plus_price) {
 | 
				
			||||||
 | 
					        bid.status = 'out-bid'; // Gán trạng thái là đã bị outbid
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Cập nhật thời gian kết thúc đấu giá nếu `close_time` mới lớn hơn `close_time` cũ
 | 
				
			||||||
    if (
 | 
					    if (
 | 
				
			||||||
      close_time &&
 | 
					      close_time &&
 | 
				
			||||||
      new Date(close_time).getTime() > new Date(bid.close_time).getTime()
 | 
					      new Date(close_time).getTime() > new Date(bid.close_time).getTime()
 | 
				
			||||||
| 
						 | 
					@ -226,24 +258,43 @@ export class BidsService {
 | 
				
			||||||
      bid.close_time = close_time;
 | 
					      bid.close_time = close_time;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Nếu chưa có `model` nhưng dữ liệu mới có model, thì cập nhật model
 | 
				
			||||||
 | 
					    if (model && !bid.model) {
 | 
				
			||||||
 | 
					      bid.model = model;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Lưu cập nhật vào database
 | 
				
			||||||
    const result = await this.bidsRepo.save({
 | 
					    const result = await this.bidsRepo.save({
 | 
				
			||||||
      ...bid,
 | 
					      ...bid,
 | 
				
			||||||
      ...data,
 | 
					      ...data,
 | 
				
			||||||
      updated_at: new Date(),
 | 
					      updated_at: new Date(), // Cập nhật timestamp
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Phát sự kiện cập nhật toàn bộ danh sách đấu giá
 | 
				
			||||||
    this.emitAllBidEvent();
 | 
					    this.emitAllBidEvent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // send event message
 | 
				
			||||||
 | 
					    // Nếu trạng thái của bid là 'out-bid', gửi thông báo
 | 
				
			||||||
 | 
					    if (['out-bid', 'win-bid'].includes(result.status)) {
 | 
				
			||||||
 | 
					      this.notificationService.emitBidStatus(result);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Trả về kết quả cập nhật dưới dạng response chuẩn
 | 
				
			||||||
    return AppResponse.toResponse(plainToClass(Bid, result));
 | 
					    return AppResponse.toResponse(plainToClass(Bid, result));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async outBid(id: Bid['id']) {
 | 
					  async outBid(id: Bid['id']) {
 | 
				
			||||||
    const result = await this.bidsRepo.update(id, { status: 'out-bid' });
 | 
					    const result = await this.bidsRepo.update(id, { status: 'out-bid' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const bid = await this.bidsRepo.findOne({ where: { id } });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!result) throw new BadRequestException(AppResponse.toResponse(false));
 | 
					    if (!result) throw new BadRequestException(AppResponse.toResponse(false));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await this.emitAllBidEvent();
 | 
					    await this.emitAllBidEvent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // send message event
 | 
				
			||||||
 | 
					    this.notificationService.emitBidStatus(bid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppResponse.toResponse(true);
 | 
					    return AppResponse.toResponse(true);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -309,9 +360,25 @@ export class BidsService {
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (lastHistory && lastHistory.price === data.current_price) {
 | 
					    if (lastHistory && lastHistory.price === data.current_price) {
 | 
				
			||||||
      await this.bidsRepo.update(bid.id, { status: 'win-bid' });
 | 
					      if (bid.status !== 'win-bid') {
 | 
				
			||||||
 | 
					        await this.bidsRepo.update(bid.id, { status: 'win-bid' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // send event message
 | 
				
			||||||
 | 
					        this.notificationService.emitBidStatus({
 | 
				
			||||||
 | 
					          ...bid,
 | 
				
			||||||
 | 
					          status: 'win-bid',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      await this.bidsRepo.update(bid.id, { status: 'out-bid' });
 | 
					      if (bid.status !== 'out-bid') {
 | 
				
			||||||
 | 
					        await this.bidsRepo.update(bid.id, { status: 'out-bid' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // send event message
 | 
				
			||||||
 | 
					        this.notificationService.emitBidStatus({
 | 
				
			||||||
 | 
					          ...bid,
 | 
				
			||||||
 | 
					          status: 'out-bid',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.emitAllBidEvent();
 | 
					    this.emitAllBidEvent();
 | 
				
			||||||
| 
						 | 
					@ -380,7 +447,7 @@ export class BidsService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let filePath: string;
 | 
					    let filePath: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (name === 'working') {
 | 
					    if (name === Event.WORKING) {
 | 
				
			||||||
      const files = readdirSync(folderPath).filter((file) =>
 | 
					      const files = readdirSync(folderPath).filter((file) =>
 | 
				
			||||||
        /\.(jpg|jpeg|png|webp)$/i.test(file),
 | 
					        /\.(jpg|jpeg|png|webp)$/i.test(file),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
| 
						 | 
					@ -412,4 +479,26 @@ export class BidsService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return res.sendFile(filePath);
 | 
					    return res.sendFile(filePath);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async getImagesWorking(id: Bid['id'], type: string) {
 | 
				
			||||||
 | 
					    const rootDir = process.cwd();
 | 
				
			||||||
 | 
					    const folderPath = join(
 | 
				
			||||||
 | 
					      rootDir,
 | 
				
			||||||
 | 
					      `${Constant.MEDIA_PATH}/${Constant.WORK_IMAGES_FOLDER}/${type}/${id}`,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!existsSync(folderPath)) {
 | 
				
			||||||
 | 
					      throw new NotFoundException(
 | 
				
			||||||
 | 
					        AppResponse.toResponse(null, {
 | 
				
			||||||
 | 
					          message: 'Folder not found',
 | 
				
			||||||
 | 
					          status_code: HttpStatus.NOT_FOUND,
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Lấy danh sách file trong folderPath
 | 
				
			||||||
 | 
					    const files = readdirSync(folderPath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toResponse(files);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,133 @@
 | 
				
			||||||
 | 
					import { extractVerifyCodeLANGTONS, verifyCode } from '@/ultils';
 | 
				
			||||||
 | 
					import { ConfigService } from '@nestjs/config';
 | 
				
			||||||
 | 
					import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
				
			||||||
 | 
					import { WebSocketGateway } from '@nestjs/websockets';
 | 
				
			||||||
 | 
					import * as Imap from 'imap';
 | 
				
			||||||
 | 
					import { Server, Socket } from 'socket.io';
 | 
				
			||||||
 | 
					import { WebBidsService } from '../web-bids.service';
 | 
				
			||||||
 | 
					import { Like } from 'typeorm';
 | 
				
			||||||
 | 
					import { Event } from '../../utils/events';
 | 
				
			||||||
 | 
					import { plainToClass } from 'class-transformer';
 | 
				
			||||||
 | 
					import { WebBid } from '../../entities/wed-bid.entity';
 | 
				
			||||||
 | 
					@WebSocketGateway({
 | 
				
			||||||
 | 
					  namespace: 'admin-bid-ws',
 | 
				
			||||||
 | 
					  cors: {
 | 
				
			||||||
 | 
					    origin: '*',
 | 
				
			||||||
 | 
					    methods: ['GET', 'POST'],
 | 
				
			||||||
 | 
					    credentials: true,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class ImapService {
 | 
				
			||||||
 | 
					  private imap: Imap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  server: Server;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    private readonly configService: ConfigService,
 | 
				
			||||||
 | 
					    private readonly webBidsService: WebBidsService,
 | 
				
			||||||
 | 
					    private eventEmitter: EventEmitter2,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    this.imap = new Imap({
 | 
				
			||||||
 | 
					      user: this.configService.get<string>('MAIL_USER'),
 | 
				
			||||||
 | 
					      password: this.configService.get<string>('MAIL_PASSWORD'),
 | 
				
			||||||
 | 
					      host: this.configService.get<string>('MAIL_SERVER'),
 | 
				
			||||||
 | 
					      port: 993,
 | 
				
			||||||
 | 
					      tls: true,
 | 
				
			||||||
 | 
					      tlsOptions: { rejectUnauthorized: false },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log(this.configService.get<string>('MAIL_USER'));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async connectIMAP() {
 | 
				
			||||||
 | 
					    this.imap.once('ready', () => {
 | 
				
			||||||
 | 
					      console.log('📥 IMAP Connected. Listening for new emails...');
 | 
				
			||||||
 | 
					      this.openInbox();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.imap.on('error', (err) => {
 | 
				
			||||||
 | 
					      console.error('❌ IMAP Error:', err);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.imap.on('end', () => {
 | 
				
			||||||
 | 
					      console.log('📴 IMAP Connection Ended');
 | 
				
			||||||
 | 
					      setTimeout(() => this.connectIMAP(), 5000);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.imap.connect();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private openInbox() {
 | 
				
			||||||
 | 
					    this.imap.openBox('INBOX', false, (err, box) => {
 | 
				
			||||||
 | 
					      if (err) {
 | 
				
			||||||
 | 
					        console.error('❌ Error opening inbox:', err);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      console.log('📬 INBOX opened, waiting for new emails...');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Lắng nghe email mới
 | 
				
			||||||
 | 
					      this.imap.on('mail', (numNewMsgs: number) => {
 | 
				
			||||||
 | 
					        console.log(`📩 New email received! (${numNewMsgs} new messages)`);
 | 
				
			||||||
 | 
					        this.fetchLatestEmail();
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private fetchLatestEmail() {
 | 
				
			||||||
 | 
					    this.imap.search(['UNSEEN'], (err, results) => {
 | 
				
			||||||
 | 
					      if (err || !results.length) {
 | 
				
			||||||
 | 
					        console.log('📭 No new unread emails.');
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const fetchOptions = { bodies: '', struct: true };
 | 
				
			||||||
 | 
					      const f = this.imap.fetch(results.slice(-1), fetchOptions); // Lấy email mới nhất
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      f.on('message', (msg, seqno) => {
 | 
				
			||||||
 | 
					        console.log(`📥 Fetching email #${seqno}`);
 | 
				
			||||||
 | 
					        let emailContent = '';
 | 
				
			||||||
 | 
					        let header = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        msg.on('body', (stream) => {
 | 
				
			||||||
 | 
					          stream.on('data', (chunk) => {
 | 
				
			||||||
 | 
					            emailContent += chunk.toString();
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          stream.on('end', async () => {
 | 
				
			||||||
 | 
					            const result = verifyCode(emailContent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!result) {
 | 
				
			||||||
 | 
					              throw new Error('fetchLatestEmail: Name or Code is empty');
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const { code, name } = result;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const webBid = await this.webBidsService.webBidRepo.findOne({
 | 
				
			||||||
 | 
					              where: { origin_url: Like(`%${name.toLocaleLowerCase()}%`) },
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!webBid) {
 | 
				
			||||||
 | 
					              throw new Error('Not found web bid');
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // send message event
 | 
				
			||||||
 | 
					            this.eventEmitter.emit(Event.verifyCode(webBid), {
 | 
				
			||||||
 | 
					              code,
 | 
				
			||||||
 | 
					              name,
 | 
				
			||||||
 | 
					              web_bid: plainToClass(WebBid, webBid),
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      f.on('error', (err) => {
 | 
				
			||||||
 | 
					        console.error('❌ Fetch error:', err);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      f.on('end', () => {
 | 
				
			||||||
 | 
					        console.log('✅ Done fetching emails.');
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -53,11 +53,30 @@ export class OutBidLogsService {
 | 
				
			||||||
        skipUpdateIfNoValuesChanged: true,
 | 
					        skipUpdateIfNoValuesChanged: true,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!result) throw new BadRequestException(false);
 | 
					      if (!result) throw new BadRequestException(AppResponse.toResponse(false));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return AppResponse.toResponse(true);
 | 
					      return AppResponse.toResponse(true);
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      throw new BadRequestException(false);
 | 
					      throw new BadRequestException(AppResponse.toResponse(false));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async createSimple({ model, lot_id, ...data }: CreateOutBidLogDto) {
 | 
				
			||||||
 | 
					    const prev = await this.outbidLogRepo.findOne({
 | 
				
			||||||
 | 
					      where: { lot_id: lot_id, model: model },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!prev) {
 | 
				
			||||||
 | 
					      const result = await this.outbidLogRepo.save({ ...data, lot_id, model });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return AppResponse.toResponse(!!result);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const result = await this.outbidLogRepo.update(
 | 
				
			||||||
 | 
					      { model: model, lot_id: lot_id },
 | 
				
			||||||
 | 
					      { ...data },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toResponse(!!result);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,6 +21,7 @@ import { extractDomain } from 'src/ultils';
 | 
				
			||||||
import { Bid } from '../entities/bid.entity';
 | 
					import { Bid } from '../entities/bid.entity';
 | 
				
			||||||
import { CreateWebBidDto } from '../dto/web-bid/create-web-bid';
 | 
					import { CreateWebBidDto } from '../dto/web-bid/create-web-bid';
 | 
				
			||||||
import { UpdateWebBidDto } from '../dto/web-bid/update-web-bid';
 | 
					import { UpdateWebBidDto } from '../dto/web-bid/update-web-bid';
 | 
				
			||||||
 | 
					import { Event } from '../utils/events';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class WebBidsService {
 | 
					export class WebBidsService {
 | 
				
			||||||
| 
						 | 
					@ -73,13 +74,16 @@ export class WebBidsService {
 | 
				
			||||||
  async emitAllBidEvent() {
 | 
					  async emitAllBidEvent() {
 | 
				
			||||||
    const data = await this.getDataClient();
 | 
					    const data = await this.getDataClient();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.eventEmitter.emit('bids.updated', data);
 | 
					    this.eventEmitter.emit(Event.BIDS_UPDATED, data);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async emitAccountUpdate(id: WebBid['id']) {
 | 
					  async emitAccountUpdate(id: WebBid['id']) {
 | 
				
			||||||
    const data = await this.webBidRepo.findOne({ where: { id } });
 | 
					    const data = await this.webBidRepo.findOne({
 | 
				
			||||||
 | 
					      where: { id, children: { status: 'biding' } },
 | 
				
			||||||
 | 
					      relations: { children: true },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.eventEmitter.emit('web.updated', data || null);
 | 
					    this.eventEmitter.emit(Event.WEB_UPDATED, data || null);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async createByUrl(url: string) {
 | 
					  async createByUrl(url: string) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,7 @@
 | 
				
			||||||
export class Constant {
 | 
					export class Constant {
 | 
				
			||||||
  public static MEDIA_PATH = 'public';
 | 
					  public static MEDIA_PATH = 'public';
 | 
				
			||||||
 | 
					  public static BOT_TELEGRAM_PATH = 'bot-data';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public static WORK_IMAGES_FOLDER = 'work-images';
 | 
					  public static WORK_IMAGES_FOLDER = 'work-images';
 | 
				
			||||||
  public static TMP_FOLDER = 'tmp';
 | 
					  public static TMP_FOLDER = 'tmp';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					import { WebBid } from '../entities/wed-bid.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Event {
 | 
				
			||||||
 | 
					  public static VERIFY_CODE = 'verify-code';
 | 
				
			||||||
 | 
					  public static WORKING = 'working';
 | 
				
			||||||
 | 
					  public static BIDS_UPDATED = 'bidsUpdated';
 | 
				
			||||||
 | 
					  public static ADMIN_BIDS_UPDATED = 'adminBidsUpdated';
 | 
				
			||||||
 | 
					  public static WEB_UPDATED = 'webUpdated';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public static verifyCode(data: WebBid) {
 | 
				
			||||||
 | 
					    return `${this.VERIFY_CODE}.${data.origin_url}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					export const NAME_EVENTS = {
 | 
				
			||||||
 | 
					  BID_STATUS: 'notify.bid-status',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,31 @@
 | 
				
			||||||
 | 
					import { BotTelegramApi } from '@/modules/bids/apis/bot-telegram.api';
 | 
				
			||||||
 | 
					import { Body, Controller, Get, Post } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { SendMessageDto } from '../dto/send-message.dto';
 | 
				
			||||||
 | 
					import { NotificationService } from '../notification.service';
 | 
				
			||||||
 | 
					import { BidsService } from '@/modules/bids/services/bids.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Controller('notifications')
 | 
				
			||||||
 | 
					export class ClientNotificationController {
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    private botTelegramApi: BotTelegramApi,
 | 
				
			||||||
 | 
					    private readonly notifyService: NotificationService,
 | 
				
			||||||
 | 
					    private readonly bidsService: BidsService,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Post('send-messages')
 | 
				
			||||||
 | 
					  async sendMessage(@Body() data: SendMessageDto) {
 | 
				
			||||||
 | 
					    return await this.botTelegramApi.sendMessage(data.text, data.options);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Post('test')
 | 
				
			||||||
 | 
					  async test() {
 | 
				
			||||||
 | 
					    const bid = await this.bidsService.bidsRepo.findOne({
 | 
				
			||||||
 | 
					      where: { lot_id: '26077023' },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return await this.notifyService.emitBidStatus({
 | 
				
			||||||
 | 
					      ...bid,
 | 
				
			||||||
 | 
					      status: 'win-bid',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					import { Controller, Get } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { BotTelegramApi } from '../../bids/apis/bot-telegram.api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Controller('admin/notifications')
 | 
				
			||||||
 | 
					export class NotificationController {
 | 
				
			||||||
 | 
					  constructor(private botTelegramApi: BotTelegramApi) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Get('')
 | 
				
			||||||
 | 
					  async test() {
 | 
				
			||||||
 | 
					    return await this.botTelegramApi.getGroupInfo();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					export class CreateNotificationDto {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					import { IsObject, IsString } from 'class-validator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class SendMessageDto {
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  text: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsObject()
 | 
				
			||||||
 | 
					  options: Record<string, string>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					import { PartialType } from '@nestjs/mapped-types';
 | 
				
			||||||
 | 
					import { CreateNotificationDto } from './create-notification.dto';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class UpdateNotificationDto extends PartialType(CreateNotificationDto) {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
 | 
				
			||||||
 | 
					import { Timestamp } from './timestamp';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Entity('notifications')
 | 
				
			||||||
 | 
					@Index(['message', 'raw_data'])
 | 
				
			||||||
 | 
					export class Notification extends Timestamp {
 | 
				
			||||||
 | 
					  @PrimaryGeneratedColumn('increment')
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column()
 | 
				
			||||||
 | 
					  message: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ type: 'varchar' })
 | 
				
			||||||
 | 
					  raw_data: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ default: null, nullable: true })
 | 
				
			||||||
 | 
					  read_at: Date | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ type: 'text' })
 | 
				
			||||||
 | 
					  send_to: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					import { CreateDateColumn, UpdateDateColumn } from 'typeorm';
 | 
				
			||||||
 | 
					export abstract class Timestamp {
 | 
				
			||||||
 | 
					  @CreateDateColumn({ type: 'timestamp', name: 'created_at' })
 | 
				
			||||||
 | 
					  created_at: Date;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @UpdateDateColumn({ type: 'timestamp', name: 'updated_at' })
 | 
				
			||||||
 | 
					  updated_at: Date;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,24 @@
 | 
				
			||||||
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { OnEvent } from '@nestjs/event-emitter';
 | 
				
			||||||
 | 
					import { NAME_EVENTS } from '../constants';
 | 
				
			||||||
 | 
					import { Bid } from '@/modules/bids/entities/bid.entity';
 | 
				
			||||||
 | 
					import { Notification } from '../entities/notification.entity';
 | 
				
			||||||
 | 
					import { BotTelegramApi } from '@/modules/bids/apis/bot-telegram.api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class AdminNotificationListener {
 | 
				
			||||||
 | 
					  constructor(private readonly botTelegramApi: BotTelegramApi) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @OnEvent(NAME_EVENTS.BID_STATUS)
 | 
				
			||||||
 | 
					  handleBidStatus({
 | 
				
			||||||
 | 
					    bid,
 | 
				
			||||||
 | 
					    notification,
 | 
				
			||||||
 | 
					  }: {
 | 
				
			||||||
 | 
					    bid: Bid;
 | 
				
			||||||
 | 
					    notification: Notification;
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    if (JSON.parse(notification.send_to).length <= 0) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.botTelegramApi.sendMessage(notification.message);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,20 @@
 | 
				
			||||||
 | 
					import { Test, TestingModule } from '@nestjs/testing';
 | 
				
			||||||
 | 
					import { NotificationController } from './controllers/notification.controller';
 | 
				
			||||||
 | 
					import { NotificationService } from './notification.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('NotificationController', () => {
 | 
				
			||||||
 | 
					  let controller: NotificationController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    const module: TestingModule = await Test.createTestingModule({
 | 
				
			||||||
 | 
					      controllers: [NotificationController],
 | 
				
			||||||
 | 
					      providers: [NotificationService],
 | 
				
			||||||
 | 
					    }).compile();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    controller = module.get<NotificationController>(NotificationController);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should be defined', () => {
 | 
				
			||||||
 | 
					    expect(controller).toBeDefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					import { forwardRef, Module } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { TypeOrmModule } from '@nestjs/typeorm';
 | 
				
			||||||
 | 
					import { BidsModule } from '../bids/bids.module';
 | 
				
			||||||
 | 
					import { ClientNotificationController } from './controllers/client-notification.controller';
 | 
				
			||||||
 | 
					import { NotificationController } from './controllers/notification.controller';
 | 
				
			||||||
 | 
					import { Notification } from './entities/notification.entity';
 | 
				
			||||||
 | 
					import { AdminNotificationListener } from './listeners/admin-notification.listener';
 | 
				
			||||||
 | 
					import { NotificationService } from './notification.service';
 | 
				
			||||||
 | 
					import { SendMessageHistoriesService } from '../bids/services/send-message-histories.service';
 | 
				
			||||||
 | 
					import { SendMessageHistory } from '../bids/entities/send-message-histories.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Module({
 | 
				
			||||||
 | 
					  imports: [
 | 
				
			||||||
 | 
					    forwardRef(() => BidsModule),
 | 
				
			||||||
 | 
					    TypeOrmModule.forFeature([Notification, SendMessageHistory]),
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  controllers: [NotificationController, ClientNotificationController],
 | 
				
			||||||
 | 
					  providers: [NotificationService, AdminNotificationListener],
 | 
				
			||||||
 | 
					  exports: [NotificationService],
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class NotificationModule {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,18 @@
 | 
				
			||||||
 | 
					import { Test, TestingModule } from '@nestjs/testing';
 | 
				
			||||||
 | 
					import { NotificationService } from './notification.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('NotificationService', () => {
 | 
				
			||||||
 | 
					  let service: NotificationService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    const module: TestingModule = await Test.createTestingModule({
 | 
				
			||||||
 | 
					      providers: [NotificationService],
 | 
				
			||||||
 | 
					    }).compile();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    service = module.get<NotificationService>(NotificationService);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should be defined', () => {
 | 
				
			||||||
 | 
					    expect(service).toBeDefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,126 @@
 | 
				
			||||||
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
				
			||||||
 | 
					import { Bid } from '../bids/entities/bid.entity';
 | 
				
			||||||
 | 
					import { NAME_EVENTS } from './constants';
 | 
				
			||||||
 | 
					import { BotTelegramApi } from '../bids/apis/bot-telegram.api';
 | 
				
			||||||
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
 | 
					import { Notification } from './entities/notification.entity';
 | 
				
			||||||
 | 
					import { LEGAL_TCP_SOCKET_OPTIONS, Repository } from 'typeorm';
 | 
				
			||||||
 | 
					import { isTimeReached } from '@/ultils';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  FilterOperator,
 | 
				
			||||||
 | 
					  FilterSuffix,
 | 
				
			||||||
 | 
					  paginate,
 | 
				
			||||||
 | 
					  PaginateQuery,
 | 
				
			||||||
 | 
					} from 'nestjs-paginate';
 | 
				
			||||||
 | 
					import { Column } from 'nestjs-paginate/lib/helper';
 | 
				
			||||||
 | 
					import AppResponse from '@/response/app-response';
 | 
				
			||||||
 | 
					import { SendMessageHistoriesService } from '../bids/services/send-message-histories.service';
 | 
				
			||||||
 | 
					import { SendMessageHistory } from '../bids/entities/send-message-histories.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class NotificationService {
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    private eventEmitter: EventEmitter2,
 | 
				
			||||||
 | 
					    private readonly botTelegramApi: BotTelegramApi,
 | 
				
			||||||
 | 
					    @InjectRepository(Notification)
 | 
				
			||||||
 | 
					    readonly notificationRepo: Repository<Notification>,
 | 
				
			||||||
 | 
					    @InjectRepository(SendMessageHistory)
 | 
				
			||||||
 | 
					    readonly sendMessageRepo: Repository<SendMessageHistory>,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async index(query: PaginateQuery) {
 | 
				
			||||||
 | 
					    const filterableColumns: {
 | 
				
			||||||
 | 
					      [key in Column<Bid> | (string & {})]?:
 | 
				
			||||||
 | 
					        | (FilterOperator | FilterSuffix)[]
 | 
				
			||||||
 | 
					        | true;
 | 
				
			||||||
 | 
					    } = {
 | 
				
			||||||
 | 
					      id: true,
 | 
				
			||||||
 | 
					      message: true,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    query.filter = AppResponse.processFilters(query.filter, filterableColumns);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const data = await paginate(query, this.notificationRepo, {
 | 
				
			||||||
 | 
					      sortableColumns: ['id'],
 | 
				
			||||||
 | 
					      searchableColumns: ['id'],
 | 
				
			||||||
 | 
					      defaultLimit: 15,
 | 
				
			||||||
 | 
					      filterableColumns,
 | 
				
			||||||
 | 
					      defaultSortBy: [['id', 'DESC']],
 | 
				
			||||||
 | 
					      maxLimit: 100,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toPagination<Notification>(data, true, Notification);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getBidStatusMessage(bid: Bid): string | null {
 | 
				
			||||||
 | 
					    const itemName = `*${bid.name || 'the item'}*`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    switch (bid.status) {
 | 
				
			||||||
 | 
					      case 'biding':
 | 
				
			||||||
 | 
					        return !bid.name ? null : `✅ The item has been activated. ${bid.name}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      case 'out-bid':
 | 
				
			||||||
 | 
					        if (isTimeReached(bid.close_time)) {
 | 
				
			||||||
 | 
					          return `⏳ The auction for *${bid.name || 'this item'}* has ended.`;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					          bid.max_price + bid.plus_price <= bid.current_price ||
 | 
				
			||||||
 | 
					          bid.reserve_price > bid.max_price + bid.plus_price
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					          return `💰 The current bid for ${itemName} has exceeded your maximum bid.`;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return `🛑 The auction for ${itemName} has been canceled.`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      case 'win-bid':
 | 
				
			||||||
 | 
					        return `🎉 Congratulations! You won the auction for ${itemName} at *${bid.current_price}*.`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return '❓ Unknown auction status.';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async emitBidStatus(bid: Bid, sendTo: boolean = true) {
 | 
				
			||||||
 | 
					    const groupData = await this.botTelegramApi.getGroupInfo();
 | 
				
			||||||
 | 
					    const sendToData = groupData && sendTo ? [groupData?.title || 'None'] : [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const message = this.getBidStatusMessage(bid);
 | 
				
			||||||
 | 
					    if (!message) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (bid.status === 'win-bid') {
 | 
				
			||||||
 | 
					      const prevSendMessage = await this.sendMessageRepo.findOne({
 | 
				
			||||||
 | 
					        where: { bid: { id: bid.id }, type: 'win-bid' },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (prevSendMessage) return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const notification = await this.notificationRepo.save({
 | 
				
			||||||
 | 
					      message,
 | 
				
			||||||
 | 
					      raw_data: JSON.stringify({
 | 
				
			||||||
 | 
					        id: bid.id,
 | 
				
			||||||
 | 
					        status: bid.status,
 | 
				
			||||||
 | 
					        name: bid.name,
 | 
				
			||||||
 | 
					        close_time: bid.close_time,
 | 
				
			||||||
 | 
					        current_price: bid.current_price,
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					      send_to: JSON.stringify(sendToData),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await this.sendMessageRepo.save({
 | 
				
			||||||
 | 
					      bid: { id: bid.id },
 | 
				
			||||||
 | 
					      message: notification.message,
 | 
				
			||||||
 | 
					      type: bid.status,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.eventEmitter.emit(NAME_EVENTS.BID_STATUS, {
 | 
				
			||||||
 | 
					      bid: {
 | 
				
			||||||
 | 
					        ...bid,
 | 
				
			||||||
 | 
					        status: 'out-bid',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      notification,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,18 @@
 | 
				
			||||||
export function extractModelId(url: string): string | null {
 | 
					export function extractModelId(url: string): string | null {
 | 
				
			||||||
  const match = url.match(/\/lot\/([\d-]+)\//);
 | 
					  switch (extractDomain(url)) {
 | 
				
			||||||
  return match ? match[1] : null;
 | 
					    case 'https://www.grays.com': {
 | 
				
			||||||
 | 
					      const match = url.match(/\/lot\/([\d-]+)\//);
 | 
				
			||||||
 | 
					      return match ? match[1] : null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    case 'https://www.langtons.com.au': {
 | 
				
			||||||
 | 
					      const match = url.match(/auc-var-\d+/);
 | 
				
			||||||
 | 
					      return match[0];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    case 'https://www.lawsons.com.au': {
 | 
				
			||||||
 | 
					      const match = url.split('_');
 | 
				
			||||||
 | 
					      return match ? match[1] : null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function subtractMinutes(timeStr: string, minutes: number) {
 | 
					export function subtractMinutes(timeStr: string, minutes: number) {
 | 
				
			||||||
| 
						 | 
					@ -28,3 +40,60 @@ export function extractDomain(url: string): string | null {
 | 
				
			||||||
export function escapeMarkdownV2(text: string) {
 | 
					export function escapeMarkdownV2(text: string) {
 | 
				
			||||||
  return text.replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, '\\$&');
 | 
					  return text.replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, '\\$&');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function extractVerifyCodeLANGTONS(content: string) {
 | 
				
			||||||
 | 
					  const match = content.match(/Your\s+(\w+)\s+verific.*?code\s+is:\s+(\d+)/i);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (match) {
 | 
				
			||||||
 | 
					    const name = match[1]; // LANGTONS
 | 
				
			||||||
 | 
					    const code = match[2]; // 012152
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      name,
 | 
				
			||||||
 | 
					      code,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function extractVerifyCodeLAWSONS(content: string) {
 | 
				
			||||||
 | 
					  if (!content) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Loại bỏ các ký tự `=` không cần thiết do email encoding (quoted-printable)
 | 
				
			||||||
 | 
					  content = content.replace(/=\r\n/g, '').replace(/=3D/g, '=');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Regex để tìm mã xác minh (6 chữ số)
 | 
				
			||||||
 | 
					  const codeMatch = content.match(/Verification code:\s*(\d{6})/);
 | 
				
			||||||
 | 
					  const code = codeMatch ? codeMatch[1] : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Regex để tìm tên (trong đoạn "Lawsons | Sydney, AU")
 | 
				
			||||||
 | 
					  const nameMatch = content.match(/([\w\s]+)\s*\|\s*[\w\s,]+/);
 | 
				
			||||||
 | 
					  const name = nameMatch ? nameMatch[1].trim() : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Nếu không tìm thấy cả hai giá trị, trả về null
 | 
				
			||||||
 | 
					  if (!code && !name) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return { code, name };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function verifyCode(content: string) {
 | 
				
			||||||
 | 
					  // Kiểm tra mã xác minh từ LANGTONS
 | 
				
			||||||
 | 
					  const langtonsResult = extractVerifyCodeLANGTONS(content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (langtonsResult) {
 | 
				
			||||||
 | 
					    return langtonsResult;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Nếu không có kết quả, tiếp tục kiểm tra mã xác minh từ LAWSONS
 | 
				
			||||||
 | 
					  const lawsonsResult = extractVerifyCodeLAWSONS(content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (lawsonsResult) {
 | 
				
			||||||
 | 
					    return lawsonsResult; // Trả về kết quả đúng thay vì langtonsResult
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Nếu không tìm thấy mã xác minh, trả về null
 | 
				
			||||||
 | 
					  return null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    "cSpell.words": ["Langtons", "Lawsons"]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,29 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    "action": "Auction-LotsData",
 | 
				
			||||||
 | 
					    "queryString": "auctionBpa=18&auctionCatalogPrice=16&auctionId=7402&auctionUnitQuantity=1&includeQuickViewLotData=true&lotIds=26033882&pid=auc-var-26033882&productName=VICKERY%20WINES%20Riesling%2C%20Eden%20Valley%202016%20Bottle",
 | 
				
			||||||
 | 
					    "locale": "en_AU",
 | 
				
			||||||
 | 
					    "csrf": {
 | 
				
			||||||
 | 
					        "tokenName": "csrf_token",
 | 
				
			||||||
 | 
					        "token": "I0GEkb97Y1mbVWixUo2f7sXj4EChyVDMO-CT3I6vV90VEfRJFLio5eBbartpUOaiJUZci22TbjoAnR-UVxaIJ2Umj51lWikd3oUxsTGM_ObrtgFiQXQqxJjY4D98BWf7_tI4uAu-kG6uttjdwArabsiJMx6qDGG0wvH2FLBQ8fpK9IjIBXo="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "success": true,
 | 
				
			||||||
 | 
					    "lotData": {
 | 
				
			||||||
 | 
					        "lotId": "26033882",
 | 
				
			||||||
 | 
					        "currentMaxBid": 17,
 | 
				
			||||||
 | 
					        "minimumBid": 18,
 | 
				
			||||||
 | 
					        "numberOfBidders": 2,
 | 
				
			||||||
 | 
					        "numberOfWatchers": 1,
 | 
				
			||||||
 | 
					        "hasMetReserve": true,
 | 
				
			||||||
 | 
					        "bidStatus": "Winning",
 | 
				
			||||||
 | 
					        "timestamp": 49568786,
 | 
				
			||||||
 | 
					        "myBid": "20"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "productBPA": "18.00",
 | 
				
			||||||
 | 
					    "priceIncBPA": "$18.88",
 | 
				
			||||||
 | 
					    "auctionPrice": "$16.00",
 | 
				
			||||||
 | 
					    "totalAuctionPrice": "$16.00",
 | 
				
			||||||
 | 
					    "lotId": "26033882",
 | 
				
			||||||
 | 
					    "pid": "auc-var-26033882",
 | 
				
			||||||
 | 
					    "unitQuantity": "1",
 | 
				
			||||||
 | 
					    "auctionBpa": "18"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -7,6 +7,8 @@ import browser from './system/browser.js';
 | 
				
			||||||
import configs from './system/config.js';
 | 
					import configs from './system/config.js';
 | 
				
			||||||
import { delay, isTimeReached, safeClosePage } from './system/utils.js';
 | 
					import { delay, isTimeReached, safeClosePage } from './system/utils.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					global.IS_CLEANING = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let MANAGER_BIDS = [];
 | 
					let MANAGER_BIDS = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const activeTasks = new Set();
 | 
					const activeTasks = new Set();
 | 
				
			||||||
| 
						 | 
					@ -129,6 +131,11 @@ const tracking = async () => {
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const clearLazyTab = async () => {
 | 
					const clearLazyTab = async () => {
 | 
				
			||||||
 | 
					    if (!global.IS_CLEANING) {
 | 
				
			||||||
 | 
					        console.log('🚀 Cleaning flag is OFF. Proceeding with operation.');
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!browser) {
 | 
					    if (!browser) {
 | 
				
			||||||
        console.warn('⚠️ Browser is not available or disconnected.');
 | 
					        console.warn('⚠️ Browser is not available or disconnected.');
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
| 
						 | 
					@ -203,6 +210,9 @@ const workTracking = async () => {
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // set socket on global app
 | 
				
			||||||
 | 
					    global.socket = socket;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // listen connect
 | 
					    // listen connect
 | 
				
			||||||
    socket.on('connect', () => {
 | 
					    socket.on('connect', () => {
 | 
				
			||||||
        console.log('✅ Connected to WebSocket server');
 | 
					        console.log('✅ Connected to WebSocket server');
 | 
				
			||||||
| 
						 | 
					@ -229,11 +239,16 @@ const workTracking = async () => {
 | 
				
			||||||
        if (isDeleted) {
 | 
					        if (isDeleted) {
 | 
				
			||||||
            console.log('✅ Profile deleted successfully!');
 | 
					            console.log('✅ Profile deleted successfully!');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const tabs = MANAGER_BIDS.filter((item) => item.url === data.url || item?.web_bid.url === data.url);
 | 
					            const tab = MANAGER_BIDS.find((item) => item.url === data.url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (tabs.length <= 0) return;
 | 
					            if (!tab) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await Promise.all(tabs.map((tab) => safeClosePage(tab)));
 | 
					            global.IS_CLEANING = false;
 | 
				
			||||||
 | 
					            await Promise.all(tab.children.map((tab) => safeClosePage(tab)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await safeClosePage(tab);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            global.IS_CLEANING = true;
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            console.log('⚠️ No profile found to delete.');
 | 
					            console.log('⚠️ No profile found to delete.');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -39,44 +39,43 @@ export class GrayApiBid extends ApiBid {
 | 
				
			||||||
            //     }
 | 
					            //     }
 | 
				
			||||||
            // });
 | 
					            // });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            console.log('🔄 Starting polling process...');
 | 
					            console.log(`🔄 [${this.id}] Starting polling process...`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await page.evaluateHandle(
 | 
					            await page.evaluateHandle(
 | 
				
			||||||
                (apiUrl, interval) => {
 | 
					                (apiUrl, interval, bidId) => {
 | 
				
			||||||
                    if (window._autoBidPollingStarted) {
 | 
					                    if (window._autoBidPollingStarted) {
 | 
				
			||||||
                        console.log('✅ Polling is already running. Skipping initialization.');
 | 
					                        console.log(`✅ [${bidId}] Polling is already running. Skipping initialization.`);
 | 
				
			||||||
                        return;
 | 
					                        return;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    console.log('🚀 Initializing polling...');
 | 
					                    console.log(`🚀 [${bidId}] Initializing polling...`);
 | 
				
			||||||
                    window._autoBidPollingStarted = true;
 | 
					                    window._autoBidPollingStarted = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    function sendRequest() {
 | 
					                    function sendRequest() {
 | 
				
			||||||
                        console.log('📡 Sending request to track out-bid lots...');
 | 
					                        console.log(`📡 [${bidId}] Sending request to track out-bid lots...`);
 | 
				
			||||||
                        fetch(apiUrl, {
 | 
					                        fetch(apiUrl, {
 | 
				
			||||||
                            method: 'POST',
 | 
					                            method: 'POST',
 | 
				
			||||||
                            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
 | 
					                            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
 | 
				
			||||||
                            body: JSON.stringify({ timeStamp: new Date().getTime() }),
 | 
					                            body: JSON.stringify({ timeStamp: new Date().getTime() }),
 | 
				
			||||||
                        })
 | 
					                        })
 | 
				
			||||||
                            .then((response) => console.log(`✅ Response received: ${response.status}`))
 | 
					                            .then((response) => console.log(`✅ [${bidId}] Response received: ${response.status}`))
 | 
				
			||||||
                            .catch((err) => console.error('⚠️ Request error:', err));
 | 
					                            .catch((err) => console.error(`⚠️ [${bidId}] Request error:`, err));
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    window._pollingInterval = setInterval(sendRequest, interval);
 | 
					                    window._pollingInterval = setInterval(sendRequest, interval);
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                configs.WEB_CONFIGS.GRAYS.API_CALL_TO_TRACKING,
 | 
					                configs.WEB_CONFIGS.GRAYS.API_CALL_TO_TRACKING,
 | 
				
			||||||
                configs.WEB_CONFIGS.GRAYS.AUTO_CALL_API_TO_TRACKING,
 | 
					                configs.WEB_CONFIGS.GRAYS.AUTO_CALL_API_TO_TRACKING,
 | 
				
			||||||
 | 
					                this.id,
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
 | 
					 | 
				
			||||||
            console.log('✅ Polling successfully started!');
 | 
					 | 
				
			||||||
        } catch (error) {
 | 
					        } catch (error) {
 | 
				
			||||||
            if (error.message.includes('Execution context was destroyed')) {
 | 
					            if (error.message.includes('Execution context was destroyed')) {
 | 
				
			||||||
                console.log('⚠️ Page reload detected, restarting polling...');
 | 
					                console.log(`⚠️ [${this.id}] Page reload detected, restarting polling...`);
 | 
				
			||||||
                await page.waitForNavigation({ waitUntil: 'networkidle2' }).catch(() => {});
 | 
					                await page.waitForNavigation({ waitUntil: 'networkidle2' }).catch(() => {});
 | 
				
			||||||
                return await this.polling(page);
 | 
					                return await this.polling(page);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            console.error('🚨 Unexpected polling error:', error);
 | 
					            console.error(`🚨 [${this.id}] Unexpected polling error:`, error);
 | 
				
			||||||
            throw error;
 | 
					            throw error;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -110,7 +109,7 @@ export class GrayApiBid extends ApiBid {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const product_tab of handleChildren) {
 | 
					        for (const product_tab of handleChildren) {
 | 
				
			||||||
            if (!isTimeReached(product_tab.start_bid_time)) {
 | 
					            if (!isTimeReached(product_tab.start_bid_time)) {
 | 
				
			||||||
                console.log("❌ It's not time yet ID: " + product_tab.id + ' continue waiting...');
 | 
					                console.log(`❌ [${this.id}] It's not time yet ID: ${product_tab.id} continue waiting...`);
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -129,17 +128,20 @@ export class GrayApiBid extends ApiBid {
 | 
				
			||||||
    async handleLogin() {
 | 
					    async handleLogin() {
 | 
				
			||||||
        const page = this.page_context;
 | 
					        const page = this.page_context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        global.IS_CLEANING = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const filePath = getPathProfile(this.origin_url);
 | 
					        const filePath = getPathProfile(this.origin_url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // 🔍 Check if already logged in (login input should not be visible)
 | 
					        // 🔍 Check if already logged in (login input should not be visible)
 | 
				
			||||||
        if (!(await page.$('input[name="username"]')) || fs.existsSync(filePath)) {
 | 
					        if (!(await page.$('input[name="username"]')) || fs.existsSync(filePath)) {
 | 
				
			||||||
            console.log('✅ Already logged in, skipping login.');
 | 
					            console.log(`✅ [${this.id}] Already logged in, skipping login.`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            global.IS_CLEANING = true;
 | 
				
			||||||
            this.retry_login = 0; // Reset retry count
 | 
					            this.retry_login = 0; // Reset retry count
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        console.log('🔑 Starting login process...');
 | 
					        console.log(`🔑 [${this.id}] Starting login process...`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            await page.type('input[name="username"]', this.username, { delay: 100 });
 | 
					            await page.type('input[name="username"]', this.username, { delay: 100 });
 | 
				
			||||||
| 
						 | 
					@ -152,18 +154,18 @@ export class GrayApiBid extends ApiBid {
 | 
				
			||||||
            ]);
 | 
					            ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!(await page.$('input[name="username"]'))) {
 | 
					            if (!(await page.$('input[name="username"]'))) {
 | 
				
			||||||
                console.log('✅ Login successful!');
 | 
					                console.log(`✅ [${this.id}] Login successful!`);
 | 
				
			||||||
                this.retry_login = 0; // Reset retry count after success
 | 
					                this.retry_login = 0; // Reset retry count after success
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            throw new Error('Login failed, login input is still visible.');
 | 
					            throw new Error('Login failed, login input is still visible.');
 | 
				
			||||||
        } catch (error) {
 | 
					        } catch (error) {
 | 
				
			||||||
            console.log(`⚠️ Login error: ${error.message}. Retrying attempt ${this.retry_login + 1} ❌`);
 | 
					            console.log(`⚠️ [${this.id}] Login error: ${error.message}. Retrying attempt ${this.retry_login + 1} ❌`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            this.retry_login++;
 | 
					            this.retry_login++;
 | 
				
			||||||
            if (this.retry_login > this.retry_login_count) {
 | 
					            if (this.retry_login > this.retry_login_count) {
 | 
				
			||||||
                console.log('🚨 Maximum login attempts reached. Stopping login process.');
 | 
					                console.log(`🚨 [${this.id}] Maximum login attempts reached. Stopping login process.`);
 | 
				
			||||||
                safeClosePage(this);
 | 
					                safeClosePage(this);
 | 
				
			||||||
                this.retry_login = 0; // Reset retry count
 | 
					                this.retry_login = 0; // Reset retry count
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
| 
						 | 
					@ -177,6 +179,8 @@ export class GrayApiBid extends ApiBid {
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return await this.action(); // Retry login
 | 
					            return await this.action(); // Retry login
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					            global.IS_CLEANING = true;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -185,43 +189,38 @@ export class GrayApiBid extends ApiBid {
 | 
				
			||||||
            const page = this.page_context;
 | 
					            const page = this.page_context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await page.goto(this.url, { waitUntil: 'networkidle2' });
 | 
					            await page.goto(this.url, { waitUntil: 'networkidle2' });
 | 
				
			||||||
 | 
					            console.log(`🌍 [${this.id}] Navigated to URL: ${this.url}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await page.bringToFront();
 | 
					            await page.bringToFront();
 | 
				
			||||||
 | 
					            console.log(`🎯 [${this.id}] Brought page to front.`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Set userAgent
 | 
					            // Set userAgent
 | 
				
			||||||
            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');
 | 
					            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}] UserAgent set.`);
 | 
				
			||||||
            // await takeSnapshot(this.page_context, this, 'working', CONSTANTS.TYPE_IMAGE.WORK);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            page.on('response', async (response) => {
 | 
					            page.on('response', async (response) => {
 | 
				
			||||||
                if (response.request().url().includes('api/Notifications/GetOutBidLots')) {
 | 
					                if (response.request().url().includes('api/Notifications/GetOutBidLots')) {
 | 
				
			||||||
                    console.log('🚀 API POST:', response.url());
 | 
					                    console.log(`🚀 [${this.id}] API POST detected: ${response.url()}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    try {
 | 
					                    try {
 | 
				
			||||||
                        const responseBody = await response.json();
 | 
					                        const responseBody = await response.json();
 | 
				
			||||||
 | 
					 | 
				
			||||||
                        await this.listen_out_bids(responseBody.AuctionOutBidLots || []);
 | 
					                        await this.listen_out_bids(responseBody.AuctionOutBidLots || []);
 | 
				
			||||||
                    } catch (error) {
 | 
					                    } catch (error) {
 | 
				
			||||||
                        console.error('❌ Error get response', error?.message);
 | 
					                        console.error(`❌ [${this.id}] Error processing response:`, error?.message);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            page.on('load', async () => {
 | 
					            page.on('load', async () => {
 | 
				
			||||||
                console.log('🔄 The page has reloaded, restarting polling...');
 | 
					                console.log(`🔄 [${this.id}] Page has reloaded, restarting polling...`);
 | 
				
			||||||
 | 
					 | 
				
			||||||
                // await takeSnapshot(this.page_context, this, 'working', CONSTANTS.TYPE_IMAGE.WORK);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                await this.polling(page);
 | 
					                await this.polling(page);
 | 
				
			||||||
 | 
					 | 
				
			||||||
                await this.handleLogin();
 | 
					                await this.handleLogin();
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await this.polling(page); // Call when fist load
 | 
					            await this.polling(page); // Call when first load
 | 
				
			||||||
 | 
					 | 
				
			||||||
            await this.handleLogin();
 | 
					            await this.handleLogin();
 | 
				
			||||||
        } catch (error) {
 | 
					        } catch (error) {
 | 
				
			||||||
            console.log(error.message);
 | 
					            console.log(`❌ [${this.id}] Action error: ${error.message}`);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,12 +10,12 @@ export class GraysProductBid extends ProductBid {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async validate({ page, price_value }) {
 | 
					    async validate({ page, price_value }) {
 | 
				
			||||||
        if (!this.start_bid_time || !isTimeReached(this.start_bid_time)) {
 | 
					        if (!this.start_bid_time || !isTimeReached(this.start_bid_time)) {
 | 
				
			||||||
            console.log("It's not time yet ❌");
 | 
					            console.log(`❌ [${this.id}] It's not time yet`);
 | 
				
			||||||
            return { result: false, bid_price: 0 };
 | 
					            return { result: false, bid_price: 0 };
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!isNumber(price_value)) {
 | 
					        if (!isNumber(price_value)) {
 | 
				
			||||||
            console.log("Can't get PRICE_VALUE ❌");
 | 
					            console.log(`❌ [${this.id}] Can't get PRICE_VALUE`);
 | 
				
			||||||
            await takeSnapshot(page, this, 'price-value-null');
 | 
					            await takeSnapshot(page, this, 'price-value-null');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return { result: false, bid_price: 0 };
 | 
					            return { result: false, bid_price: 0 };
 | 
				
			||||||
| 
						 | 
					@ -24,7 +24,7 @@ export class GraysProductBid extends ProductBid {
 | 
				
			||||||
        const bid_price = this.plus_price + Number(price_value);
 | 
					        const bid_price = this.plus_price + Number(price_value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (bid_price > this.max_price) {
 | 
					        if (bid_price > this.max_price) {
 | 
				
			||||||
            console.log('PRICE BID is more than MAX_VALUE => STOP BID THIS PRODUCT ❌');
 | 
					            console.log(`❌ ${this.id} PRICE BID is more than MAX_VALUE => STOP BID THIS PRODUCT`);
 | 
				
			||||||
            await takeSnapshot(page, this, 'price-bid-more-than');
 | 
					            await takeSnapshot(page, this, 'price-bid-more-than');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await outBid(this.id);
 | 
					            await outBid(this.id);
 | 
				
			||||||
| 
						 | 
					@ -92,7 +92,7 @@ export class GraysProductBid extends ProductBid {
 | 
				
			||||||
        await delay(500);
 | 
					        await delay(500);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!close_time || new Date(close_time).getTime() <= new Date().getTime()) {
 | 
					        if (!close_time || new Date(close_time).getTime() <= new Date().getTime()) {
 | 
				
			||||||
            console.log(`Product is close ${close_time} ❌`);
 | 
					            console.log(`❌ [${this.id}] Product is close ${close_time}`);
 | 
				
			||||||
            return { result: true, close_time };
 | 
					            return { result: true, close_time };
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -123,7 +123,7 @@ export class GraysProductBid extends ProductBid {
 | 
				
			||||||
            await takeSnapshot(page, this, 'bid-success', CONSTANTS.TYPE_IMAGE.SUCCESS);
 | 
					            await takeSnapshot(page, this, 'bid-success', CONSTANTS.TYPE_IMAGE.SUCCESS);
 | 
				
			||||||
            return true;
 | 
					            return true;
 | 
				
			||||||
        } catch (error) {
 | 
					        } catch (error) {
 | 
				
			||||||
            console.log('❌ Timeout to loading');
 | 
					            console.log(`❌ [${this.id}] Timeout to loading`);
 | 
				
			||||||
            await takeSnapshot(page, this, 'timeout to loading');
 | 
					            await takeSnapshot(page, this, 'timeout to loading');
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -169,7 +169,7 @@ export class GraysProductBid extends ProductBid {
 | 
				
			||||||
                .$eval('#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span', (el) => el.innerText)
 | 
					                .$eval('#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span', (el) => el.innerText)
 | 
				
			||||||
                .catch(() => null);
 | 
					                .catch(() => null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            console.log(`📌 Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price: ${price_value}`);
 | 
					            console.log(`📌 [${this.id}] Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price: ${price_value}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const data = removeFalsyValues(
 | 
					            const data = removeFalsyValues(
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
| 
						 | 
					@ -196,54 +196,50 @@ export class GraysProductBid extends ProductBid {
 | 
				
			||||||
            const page = this.page_context;
 | 
					            const page = this.page_context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await this.gotoLink();
 | 
					            await this.gotoLink();
 | 
				
			||||||
 | 
					            console.log(`🌍 [${this.id}] Navigated to link.`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await delay(1000);
 | 
					            await delay(1000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const { close_time, ...isCloseProduct } = await this.isCloseProduct();
 | 
					            const { close_time, ...isCloseProduct } = await this.isCloseProduct();
 | 
				
			||||||
            if (isCloseProduct.result) {
 | 
					            if (isCloseProduct.result) {
 | 
				
			||||||
                console.log('❌ The product is closed, cannot place a bid.');
 | 
					                console.log(`❌ [${this.id}] The product is closed, cannot place a bid.`);
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await delay(500);
 | 
					            await delay(500);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const { price_value } = await this.update();
 | 
					            const { price_value } = await this.update();
 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (!price_value) return;
 | 
					            if (!price_value) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const { result, bid_price } = await this.validate({ page, price_value });
 | 
					            const { result, bid_price } = await this.validate({ page, price_value });
 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (!result) {
 | 
					            if (!result) {
 | 
				
			||||||
                console.log('❌ Validation failed. Unable to proceed with bidding.');
 | 
					                console.log(`❌ [${this.id}] Validation failed. Unable to proceed with bidding.`);
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const bidHistoriesItem = _.maxBy(this.histories, 'price');
 | 
					            const bidHistoriesItem = _.maxBy(this.histories, 'price');
 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (bidHistoriesItem && bidHistoriesItem.price === this.current_price) {
 | 
					            if (bidHistoriesItem && bidHistoriesItem.price === this.current_price) {
 | 
				
			||||||
                console.log(`🔄 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;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (price_value != bid_price) {
 | 
					            if (price_value != bid_price) {
 | 
				
			||||||
                console.log(`✍️ Updating bid price from ${price_value} → ${bid_price}`);
 | 
					                console.log(`✍️ [${this.id}] Updating bid price from ${price_value} → ${bid_price}`);
 | 
				
			||||||
                await this.handleWritePrice(page, bid_price);
 | 
					                await this.handleWritePrice(page, bid_price);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            console.log('🚀 Placing the bid...');
 | 
					            console.log(`🚀 [${this.id}] Placing the bid...`);
 | 
				
			||||||
            const resultPlaceBid = await this.placeBid(page);
 | 
					            const resultPlaceBid = await this.placeBid(page);
 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (!resultPlaceBid) {
 | 
					            if (!resultPlaceBid) {
 | 
				
			||||||
                console.log('❌ Error occurred while placing the bid.');
 | 
					                console.log(`❌ [${this.id}] Error occurred while placing the bid.`);
 | 
				
			||||||
                await takeSnapshot(page, this, 'place-bid-action');
 | 
					                await takeSnapshot(page, this, 'place-bid-action');
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            console.log(`✅ Bid placed successfully! 🏆 Bid Price: ${bid_price}, Closing Time: ${close_time}`);
 | 
					            console.log(`✅ [${this.id}] Bid placed successfully! 🏆 Bid Price: ${bid_price}, Closing Time: ${close_time}`);
 | 
				
			||||||
 | 
					 | 
				
			||||||
            await this.handleReturnProductPage(page);
 | 
					            await this.handleReturnProductPage(page);
 | 
				
			||||||
        } catch (error) {
 | 
					        } catch (error) {
 | 
				
			||||||
            console.error(`🚨 Error navigating the page: ${error.message}`);
 | 
					            console.error(`🚨 [${this.id}] Error navigating the page: ${error.message}`);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,225 @@
 | 
				
			||||||
 | 
					import fs from 'fs';
 | 
				
			||||||
 | 
					import configs from '../../system/config.js';
 | 
				
			||||||
 | 
					import { getPathProfile, safeClosePage } from '../../system/utils.js';
 | 
				
			||||||
 | 
					import { ApiBid } from '../api-bid.js';
 | 
				
			||||||
 | 
					import _ from 'lodash';
 | 
				
			||||||
 | 
					import { updateStatusByPrice } from '../../system/apis/bid.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class LangtonsApiBid extends ApiBid {
 | 
				
			||||||
 | 
					    reloadInterval = null;
 | 
				
			||||||
 | 
					    constructor({ ...prev }) {
 | 
				
			||||||
 | 
					        super(prev);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    waitVerifyData = async () =>
 | 
				
			||||||
 | 
					        new Promise((rev, rej) => {
 | 
				
			||||||
 | 
					            // Tạo timeout để reject sau 1 phút nếu không có phản hồi
 | 
				
			||||||
 | 
					            const timeout = setTimeout(() => {
 | 
				
			||||||
 | 
					                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.`));
 | 
				
			||||||
 | 
					            }, 60 * 1000); // 60 giây
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            global.socket.on(`verify-code.${this.origin_url}`, async (data) => {
 | 
				
			||||||
 | 
					                console.log(`📢 [${this.id}] VERIFY CODE:`, data);
 | 
				
			||||||
 | 
					                clearTimeout(timeout); // Hủy timeout vì đã nhận được mã
 | 
				
			||||||
 | 
					                global.socket.off(`verify-code.${this.origin_url}`); // Xóa listener tránh lặp lại
 | 
				
			||||||
 | 
					                rev(data); // Resolve với dữ liệu nhận được
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async isLogin() {
 | 
				
			||||||
 | 
					        if (!this.page_context) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const filePath = getPathProfile(this.origin_url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return !(await this.page_context.$('input[name="loginEmail"]')) && fs.existsSync(filePath);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async handleLogin() {
 | 
				
			||||||
 | 
					        const page = this.page_context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        global.IS_CLEANING = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const filePath = getPathProfile(this.origin_url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await page.waitForNavigation({ waitUntil: 'domcontentloaded' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 🛠 Check if already logged in (login input should not be visible or profile exists)
 | 
				
			||||||
 | 
					        if (!(await page.$('input[name="loginEmail"]')) && fs.existsSync(filePath)) {
 | 
				
			||||||
 | 
					            console.log(`✅ [${this.id}] Already logged in, skipping login process.`);
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (fs.existsSync(filePath)) {
 | 
				
			||||||
 | 
					            console.log(`🗑 [${this.id}] Deleting existing file: ${filePath}`);
 | 
				
			||||||
 | 
					            fs.unlinkSync(filePath);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const children = this.children.filter((item) => item.page_context);
 | 
				
			||||||
 | 
					        console.log(`🔍 [${this.id}] Found ${children.length} child pages to close.`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (children.length > 0) {
 | 
				
			||||||
 | 
					            console.log(`🛑 [${this.id}] Closing child pages...`);
 | 
				
			||||||
 | 
					            await Promise.all(
 | 
				
			||||||
 | 
					                children.map((item) => {
 | 
				
			||||||
 | 
					                    console.log(`➡ [${this.id}] Closing child page with context: ${item.page_context}`);
 | 
				
			||||||
 | 
					                    return safeClosePage(item);
 | 
				
			||||||
 | 
					                }),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            console.log(`➡ [${this.id}] Closing main page context: ${this.page_context}`);
 | 
				
			||||||
 | 
					            await safeClosePage(this);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log(`🔑 [${this.id}] Starting login process...`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // ⌨ Enter email
 | 
				
			||||||
 | 
					            console.log(`✍ [${this.id}] Entering email:`, this.username);
 | 
				
			||||||
 | 
					            await page.type('input[name="loginEmail"]', this.username, { delay: 100 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // ⌨ Enter password
 | 
				
			||||||
 | 
					            console.log(`✍ [${this.id}] Entering password...`);
 | 
				
			||||||
 | 
					            await page.type('input[name="loginPassword"]', this.password, { delay: 150 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // ✅ Click the "Remember Me" checkbox
 | 
				
			||||||
 | 
					            console.log(`🔘 [${this.id}] Clicking the "Remember Me" checkbox`);
 | 
				
			||||||
 | 
					            await page.click('#rememberMe', { delay: 80 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 🚀 Click the login button
 | 
				
			||||||
 | 
					            console.log(`🔘 [${this.id}] Clicking the "Login" button`);
 | 
				
			||||||
 | 
					            await page.click('#loginFormSubmitButton', { delay: 92 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // ⏳ Wait for navigation after login
 | 
				
			||||||
 | 
					            console.log(`⏳ [${this.id}] Waiting for navigation after login...`);
 | 
				
			||||||
 | 
					            await page.waitForNavigation({ timeout: 8000, waitUntil: 'domcontentloaded' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            console.log(`🌍 [${this.id}] Current page after login:`, page.url());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 📢 Listen for verification code event
 | 
				
			||||||
 | 
					            console.log(`👂 [${this.id}] Listening for event: verify-code.${this.origin_url}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // ⏳ Wait for verification code from socket event
 | 
				
			||||||
 | 
					            const { name, code } = await this.waitVerifyData();
 | 
				
			||||||
 | 
					            console.log(`✅ [${this.id}] Verification code received:`, { name, code });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // ⌨ Enter verification code
 | 
				
			||||||
 | 
					            console.log(`✍ [${this.id}] Entering verification code...`);
 | 
				
			||||||
 | 
					            await page.type('#code', code, { delay: 120 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 🚀 Click the verification confirmation button
 | 
				
			||||||
 | 
					            console.log(`🔘 [${this.id}] Clicking the verification confirmation button`);
 | 
				
			||||||
 | 
					            await page.click('.btn.btn-block.btn-primary', { delay: 90 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // ⏳ Wait for navigation after verification
 | 
				
			||||||
 | 
					            console.log(`⏳ [${this.id}] Waiting for navigation after verification...`);
 | 
				
			||||||
 | 
					            await page.waitForNavigation({ timeout: 8000, waitUntil: 'domcontentloaded' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 📂 Save session context to avoid re-login
 | 
				
			||||||
 | 
					            await this.saveContext();
 | 
				
			||||||
 | 
					            console.log(`✅ [${this.id}] Login successful!`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await page.goto(this.url);
 | 
				
			||||||
 | 
					            console.log(`✅ [${this.id}] Navigation successful!`);
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            console.error(`❌ [${this.id}] Error during login process:`, error.message);
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					            global.IS_CLEANING = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async getWonList() {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            await page.waitForSelector('.row.account-product-list', { timeout: 30000 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const items = await page.evaluate(() => {
 | 
				
			||||||
 | 
					                return Array.from(document.querySelectorAll('.row.account-product-list')).map((item) => item.getAttribute('data-lotid') || null);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return _.compact(items);
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            return [];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async handleUpdateWonItem() {
 | 
				
			||||||
 | 
					        console.log(`🔄 [${this.id}] Starting to update the won list...`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Lấy danh sách các lot_id thắng
 | 
				
			||||||
 | 
					        const items = await this.getWonList();
 | 
				
			||||||
 | 
					        console.log(`📌 [${this.id}] List of won lot_ids:`, items);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Nếu không có item nào, thoát ra
 | 
				
			||||||
 | 
					        if (items.length === 0) {
 | 
				
			||||||
 | 
					            console.log(`⚠️ [${this.id}] No items to update.`);
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Lọc danh sách `this.children` chỉ giữ lại những item có trong danh sách thắng
 | 
				
			||||||
 | 
					        const result = _.filter(this.children, (item) => _.includes(items, item.lot_id));
 | 
				
			||||||
 | 
					        console.log(`✅ [${this.id}] ${result.length} items need to be updated:`, result);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Gọi API updateStatusByPrice cho mỗi item và đợi tất cả hoàn thành
 | 
				
			||||||
 | 
					        const responses = await Promise.allSettled(result.map((i) => updateStatusByPrice(i.id, i.current_price)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Log kết quả của mỗi request
 | 
				
			||||||
 | 
					        responses.forEach((response, index) => {
 | 
				
			||||||
 | 
					            if (response.status === 'fulfilled') {
 | 
				
			||||||
 | 
					                console.log(`✔️ [${this.id}] Successfully updated:`, result[index]);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                console.error(`❌ [${this.id}] Update failed:`, result[index], response.reason);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log(`🏁 [${this.id}] Finished updating the won list.`);
 | 
				
			||||||
 | 
					        return responses;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    action = async () => {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            const page = this.page_context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            page.on('response', async (response) => {
 | 
				
			||||||
 | 
					                const request = response.request();
 | 
				
			||||||
 | 
					                if (request.redirectChain().length > 0) {
 | 
				
			||||||
 | 
					                    if (response.url().includes(configs.WEB_CONFIGS.LANGTONS.LOGIN_URL)) {
 | 
				
			||||||
 | 
					                        await this.handleLogin();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await page.goto(this.url, { waitUntil: 'networkidle2' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await page.bringToFront();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Set userAgent
 | 
				
			||||||
 | 
					            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');
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            console.log('Error [action]: ', error.message);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    listen_events = async () => {
 | 
				
			||||||
 | 
					        if (this.page_context) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await this.puppeteer_connect();
 | 
				
			||||||
 | 
					        await this.action();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.reloadInterval = setInterval(async () => {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                if (this.page_context && !this.page_context.isClosed()) {
 | 
				
			||||||
 | 
					                    console.log(`🔄 [${this.id}] Reloading page...`);
 | 
				
			||||||
 | 
					                    await this.page_context.reload({ waitUntil: 'networkidle2' });
 | 
				
			||||||
 | 
					                    console.log(`✅ [${this.id}] Page reloaded successfully.`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // this.handleUpdateWonItem();
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    console.log(`❌ [${this.id}] Page context is closed. Stopping reload.`);
 | 
				
			||||||
 | 
					                    clearInterval(this.reloadInterval);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } catch (error) {
 | 
				
			||||||
 | 
					                console.error(`🚨 [${this.id}] Error reloading page:`, error.message);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }, 60000);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,401 @@
 | 
				
			||||||
 | 
					import _ from 'lodash';
 | 
				
			||||||
 | 
					import { outBid, pushPrice, updateBid } from '../../system/apis/bid.js';
 | 
				
			||||||
 | 
					import { sendMessage } from '../../system/apis/notification.js';
 | 
				
			||||||
 | 
					import { createOutBidLog } from '../../system/apis/out-bid-log.js';
 | 
				
			||||||
 | 
					import configs from '../../system/config.js';
 | 
				
			||||||
 | 
					import CONSTANTS from '../../system/constants.js';
 | 
				
			||||||
 | 
					import { convertAETtoUTC, isTimeReached, removeFalsyValues, takeSnapshot } from '../../system/utils.js';
 | 
				
			||||||
 | 
					import { ProductBid } from '../product-bid.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class LangtonsProductBid extends ProductBid {
 | 
				
			||||||
 | 
					    constructor({ ...prev }) {
 | 
				
			||||||
 | 
					        super(prev);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Hàm lấy thời gian kết thúc từ trang web
 | 
				
			||||||
 | 
					    async getCloseTime() {
 | 
				
			||||||
 | 
					        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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await this.page_context.waitForSelector('.site-timezone', { timeout: 2000 });
 | 
				
			||||||
 | 
					            const time = await this.page_context.evaluate(() => {
 | 
				
			||||||
 | 
					                const el = document.querySelector('.site-timezone');
 | 
				
			||||||
 | 
					                return el ? el.innerText : null;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return time ? convertAETtoUTC(time) : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // return new Date(Date.now() + 6 * 60 * 1000).toUTCString();
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            // Nếu có lỗi xảy ra trong quá trình lấy thời gian, trả về null
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async waitForApiResponse(timeout = 10000) {
 | 
				
			||||||
 | 
					        if (!this.page_context) {
 | 
				
			||||||
 | 
					            console.error(`❌ [${this.id}] Error: page_context is undefined.`);
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return new Promise((resolve) => {
 | 
				
			||||||
 | 
					            const onResponse = async (response) => {
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    if (!response || !response.request().url().includes(configs.WEB_CONFIGS.LANGTONS.API_CALL_TO_TRACKING)) {
 | 
				
			||||||
 | 
					                        return;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    clearTimeout(timer); // Hủy timeout nếu có phản hồi
 | 
				
			||||||
 | 
					                    this.page_context.off('response', onResponse); // Gỡ bỏ listener
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    const data = await response.json();
 | 
				
			||||||
 | 
					                    resolve(data);
 | 
				
			||||||
 | 
					                } catch (error) {
 | 
				
			||||||
 | 
					                    console.error(`❌ [${this.id}] Error while parsing response:`, error?.message);
 | 
				
			||||||
 | 
					                    resolve(null);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const timer = setTimeout(() => {
 | 
				
			||||||
 | 
					                console.log(`⏳ [${this.id}] Timeout: No response received within 10s`);
 | 
				
			||||||
 | 
					                this.page_context.off('response', onResponse); // Gỡ bỏ listener khi timeout
 | 
				
			||||||
 | 
					                resolve(null);
 | 
				
			||||||
 | 
					            }, timeout);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.page_context.on('response', onResponse);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async getName() {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            if (!this.page_context) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await this.page_context.waitForSelector('.product-name', { timeout: 3000 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return await this.page_context.evaluate(() => {
 | 
				
			||||||
 | 
					                const el = document.querySelector('.product-name');
 | 
				
			||||||
 | 
					                return el ? el.innerText : null;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async handleUpdateBid({ lot_id, close_time, name, current_price, reserve_price, model }) {
 | 
				
			||||||
 | 
					        const response = await updateBid(this.id, { lot_id, close_time, name, current_price, reserve_price: Number(reserve_price) || 0, model });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (response) {
 | 
				
			||||||
 | 
					            this.lot_id = response.lot_id;
 | 
				
			||||||
 | 
					            this.close_time = response.close_time;
 | 
				
			||||||
 | 
					            this.start_bid_time = response.start_bid_time;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    update = async () => {
 | 
				
			||||||
 | 
					        if (!this.page_context) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log(`🔄 [${this.id}] Call update for ID: ${this.id}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 📌 Lấy thời gian kết thúc đấu giá từ giao diện
 | 
				
			||||||
 | 
					        const close_time = await this.getCloseTime();
 | 
				
			||||||
 | 
					        console.log(`⏳ [${this.id}] Retrieved close time: ${close_time}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 📌 Lấy tên sản phẩm hoặc thông tin liên quan
 | 
				
			||||||
 | 
					        const name = await this.getName();
 | 
				
			||||||
 | 
					        console.log(`📌 [${this.id}] Retrieved name: ${name}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 📌 Chờ phản hồi API từ trang, tối đa 10 giây
 | 
				
			||||||
 | 
					        const result = await this.waitForApiResponse();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 📌 Nếu không có dữ liệu trả về thì dừng
 | 
				
			||||||
 | 
					        if (!result) {
 | 
				
			||||||
 | 
					            console.log(`⚠️ [${this.id}] No valid data received, skipping update.`);
 | 
				
			||||||
 | 
					            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(
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                model: result?.pid || null,
 | 
				
			||||||
 | 
					                lot_id: result?.lotId || null,
 | 
				
			||||||
 | 
					                reserve_price: result.lotData?.minimumBid || null,
 | 
				
			||||||
 | 
					                current_price: result.lotData?.currentMaxBid || null,
 | 
				
			||||||
 | 
					                // close_time: close_time && !this.close_time ? String(close_time) : null,
 | 
				
			||||||
 | 
					                close_time: close_time ? String(close_time) : null,
 | 
				
			||||||
 | 
					                name,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            // [],
 | 
				
			||||||
 | 
					            ['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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log('✅ Update successful!');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return { ...response, name, close_time };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async getContinueShopButton() {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            if (!this.page_context) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await this.page_context.waitForSelector('.btn.btn-block.btn-primary.error.continue-shopping', { timeout: 3000 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return await this.page_context.evaluate(() => {
 | 
				
			||||||
 | 
					                const el = document.querySelector('.btn.btn-block.btn-primary.error.continue-shopping');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return el;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async handlePlaceBid() {
 | 
				
			||||||
 | 
					        if (!this.page_context) {
 | 
				
			||||||
 | 
					            console.log(`⚠️ [${this.id}] No page context found, aborting bid process.`);
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const page = this.page_context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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...`);
 | 
				
			||||||
 | 
					            global[`IS_PLACE_BID-${this.id}`] = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const continueShopBtn = await this.getContinueShopButton();
 | 
				
			||||||
 | 
					            if (continueShopBtn) {
 | 
				
			||||||
 | 
					                console.log(`⚠️ [${this.id}] Outbid detected, calling outBid function.`);
 | 
				
			||||||
 | 
					                await outBid(this.id);
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Kiểm tra nếu giá hiện tại lớn hơn giá tối đa cộng thêm giá cộng thêm
 | 
				
			||||||
 | 
					            if (this.current_price > this.max_price + this.plus_price) {
 | 
				
			||||||
 | 
					                console.log(`⚠️ [${this.id}] Outbid bid`); // Ghi log cảnh báo nếu giá hiện tại vượt quá mức tối đa cho phép
 | 
				
			||||||
 | 
					                return; // Dừng hàm nếu giá đã vượt qua giới hạn
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Kiểm tra thời gian bid
 | 
				
			||||||
 | 
					            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;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Đợi phản hồi từ API
 | 
				
			||||||
 | 
					            const response = await this.waitForApiResponse();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Kiểm tra nếu phản hồi không tồn tại hoặc nếu giá đấu của người dùng bằng với giá tối đa hiện tại
 | 
				
			||||||
 | 
					            if (!response || (response?.lotData?.myBid && response.lotData.myBid == this.max_price) || response?.lotData?.minimumBid > this.max_price) {
 | 
				
			||||||
 | 
					                console.log(`⚠️ [${this.id}] No response or myBid equals max_price:`, response); // Ghi log nếu không có phản hồi hoặc giá đấu của người dùng bằng giá tối đa
 | 
				
			||||||
 | 
					                return; // Nếu không có phản hồi hoặc giá đấu bằng giá tối đa thì dừng hàm
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Kiểm tra nếu dữ liệu trong response có tồn tại và trạng thái đấu giá (bidStatus) không phải là 'None'
 | 
				
			||||||
 | 
					            if (response.lotData && response.lotData?.bidStatus !== 'None' && this.max_price == response?.lotData.myBid) {
 | 
				
			||||||
 | 
					                console.log(`✔️ [${this.id}] Bid status is not 'None'. Current bid status:`, response.lotData?.bidStatus); // Ghi log nếu trạng thái đấu giá không phải 'None'
 | 
				
			||||||
 | 
					                return; // Nếu trạng thái đấu giá không phải là 'None', dừng hàm
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const bidHistoriesItem = _.maxBy(this.histories, 'price');
 | 
				
			||||||
 | 
					            console.log(`📜 [${this.id}] Current bid history:`, this.histories);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            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})`);
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            console.log(`💰 [${this.id}] Placing a bid with amount: ${this.reserve_price}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 📌 Làm rỗng ô input trước khi nhập giá đấu
 | 
				
			||||||
 | 
					            await page.evaluate(() => {
 | 
				
			||||||
 | 
					                document.querySelector('#place-bid').value = '';
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            console.log(`📝 [${this.id}] Cleared bid input field.`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 📌 Nhập giá đấu vào ô input
 | 
				
			||||||
 | 
					            await page.type('#place-bid', String(this.max_price), { delay: 800 });
 | 
				
			||||||
 | 
					            console.log(`✅ [${this.id}] Entered bid amount: ${this.max_price}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 📌 Lấy giá trị thực tế từ ô input sau khi nhập
 | 
				
			||||||
 | 
					            const bidValue = await page.evaluate(() => document.querySelector('#place-bid').value);
 | 
				
			||||||
 | 
					            console.log(`🔍 Entered bid value: ${bidValue}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 📌 Kiểm tra nếu giá trị nhập vào không khớp với giá trị mong muốn
 | 
				
			||||||
 | 
					            if (!bidValue || bidValue !== String(this.max_price)) {
 | 
				
			||||||
 | 
					                console.log(`❌ Incorrect bid amount! Received: ${bidValue}`);
 | 
				
			||||||
 | 
					                return; // Dừng thực hiện nếu giá trị nhập sai
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 📌 Nhấn nút "Place Bid"
 | 
				
			||||||
 | 
					            await page.click('.place-bid-submit .btn.btn-primary.btn-block.place-bid-btn', { delay: 5000 });
 | 
				
			||||||
 | 
					            console.log(`🖱️ [${this.id}] Clicked "Place Bid" button.`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            console.log(`📩 [${this.id}] Bid submitted, waiting for navigation...`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 📌 Chờ trang load lại để cập nhật trạng thái đấu giá
 | 
				
			||||||
 | 
					            await page.waitForNavigation({ timeout: 8000, waitUntil: 'domcontentloaded' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            console.log(`🔄 [${this.id}] Page reloaded, checking bid status...`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const { lotData } = await this.waitForApiResponse();
 | 
				
			||||||
 | 
					            console.log(`📡 [${this.id}] API Response received:`, lotData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 📌 Kiểm tra trạng thái đấu giá từ API
 | 
				
			||||||
 | 
					            if (lotData?.myBid == this.max_price) {
 | 
				
			||||||
 | 
					                console.log(`📸 [${this.id}] Taking bid success snapshot...`);
 | 
				
			||||||
 | 
					                await takeSnapshot(page, this, 'bid-success', CONSTANTS.TYPE_IMAGE.SUCCESS);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                sendMessage(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                console.log(`✅ [${this.id}] Bid placed successfully!`);
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            console.log(`⚠️ [${this.id}] Bid action completed, but status is still "None".`);
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            console.log(`🚨 [${this.id}] Error placing bid: ${error.message}`);
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					            console.log(`🔚 [${this.id}] Resetting bid flag.`);
 | 
				
			||||||
 | 
					            global[`IS_PLACE_BID-${this.id}`] = false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async handleCreateLogsOnServer(data) {
 | 
				
			||||||
 | 
					        const values = data.map((item) => {
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                model: item.pid,
 | 
				
			||||||
 | 
					                lot_id: item.lotId,
 | 
				
			||||||
 | 
					                out_price: item.lotData.minimumBid || 0,
 | 
				
			||||||
 | 
					                raw_data: JSON.stringify(item),
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await createOutBidLog(values);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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...`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // // 🔥 Xóa tất cả event chặn request trước khi thêm mới
 | 
				
			||||||
 | 
					            // page.removeAllListeners('request');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // await page.setRequestInterception(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // page.on('request', (request) => {
 | 
				
			||||||
 | 
					            //     if (request.url().includes(configs.WEB_CONFIGS.LANGTONS.API_CALL_TO_TRACKING)) {
 | 
				
			||||||
 | 
					            //         console.log('🚀 Fake response cho request:', request.url());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            //         const fakeData = fs.readFileSync('./data/fake-out-lot-langtons.json', 'utf8');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            //         request.respond({
 | 
				
			||||||
 | 
					            //             status: 200,
 | 
				
			||||||
 | 
					            //             contentType: 'application/json',
 | 
				
			||||||
 | 
					            //             body: fakeData,
 | 
				
			||||||
 | 
					            //         });
 | 
				
			||||||
 | 
					            //     } else {
 | 
				
			||||||
 | 
					            //         try {
 | 
				
			||||||
 | 
					            //             request.continue(); // ⚠️ Chỉ tiếp tục nếu request chưa bị chặn
 | 
				
			||||||
 | 
					            //         } catch (error) {
 | 
				
			||||||
 | 
					            //             console.error('⚠️ Lỗi khi tiếp tục request:', error.message);
 | 
				
			||||||
 | 
					            //         }
 | 
				
			||||||
 | 
					            //     }
 | 
				
			||||||
 | 
					            // });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const onResponse = async (response) => {
 | 
				
			||||||
 | 
					                const url = response?.request()?.url();
 | 
				
			||||||
 | 
					                if (!url || !url.includes(configs.WEB_CONFIGS.LANGTONS.API_CALL_TO_TRACKING)) {
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    const { lotData, ...prev } = await response.json();
 | 
				
			||||||
 | 
					                    console.log(`📜 [${this.id}] Received lotData:`, lotData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (!lotData || lotData.lotId !== this.lot_id) {
 | 
				
			||||||
 | 
					                        console.log(`⚠️ [${this.id}] Ignored response for lotId: ${lotData?.lotId}`);
 | 
				
			||||||
 | 
					                        return;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    console.log(`🔍 [${this.id}] Checking bid status...`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (['Outbid'].includes(lotData?.bidStatus)) {
 | 
				
			||||||
 | 
					                        console.log(`⚠️ [${this.id}] Outbid detected, attempting to place a new bid...`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        this.handleCreateLogsOnServer([{ lotData, ...prev }]);
 | 
				
			||||||
 | 
					                    } else if (['Winning'].includes(lotData?.bidStatus)) {
 | 
				
			||||||
 | 
					                        const bidHistoriesItem = _.maxBy(this.histories, 'price');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if (!bidHistoriesItem || bidHistoriesItem?.price != lotData?.currentMaxBid) {
 | 
				
			||||||
 | 
					                            pushPrice({
 | 
				
			||||||
 | 
					                                bid_id: this.id,
 | 
				
			||||||
 | 
					                                price: lotData?.currentMaxBid,
 | 
				
			||||||
 | 
					                            });
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (lotData.myBid && this.max_price && this.max_price != lotData.myBid) {
 | 
				
			||||||
 | 
					                        this.handlePlaceBid();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } 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);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,260 @@
 | 
				
			||||||
 | 
					import fs from 'fs';
 | 
				
			||||||
 | 
					import configs from '../../system/config.js';
 | 
				
			||||||
 | 
					import { delay, getPathProfile, safeClosePage } from '../../system/utils.js';
 | 
				
			||||||
 | 
					import { ApiBid } from '../api-bid.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class LawsonsApiBid extends ApiBid {
 | 
				
			||||||
 | 
					    reloadInterval = null;
 | 
				
			||||||
 | 
					    constructor({ ...prev }) {
 | 
				
			||||||
 | 
					        super(prev);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    waitVerifyData = async () =>
 | 
				
			||||||
 | 
					        new Promise((rev, rej) => {
 | 
				
			||||||
 | 
					            // Tạo timeout để reject sau 1 phút nếu không có phản hồi
 | 
				
			||||||
 | 
					            const timeout = setTimeout(() => {
 | 
				
			||||||
 | 
					                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.`));
 | 
				
			||||||
 | 
					            }, 60 * 1000); // 60 giây
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            global.socket.on(`verify-code.${this.origin_url}`, async (data) => {
 | 
				
			||||||
 | 
					                console.log(`📢 [${this.id}] VERIFY CODE:`, data);
 | 
				
			||||||
 | 
					                clearTimeout(timeout); // Hủy timeout vì đã nhận được mã
 | 
				
			||||||
 | 
					                global.socket.off(`verify-code.${this.origin_url}`); // Xóa listener tránh lặp lại
 | 
				
			||||||
 | 
					                rev(data); // Resolve với dữ liệu nhận được
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async isLogin() {
 | 
				
			||||||
 | 
					        if (!this.page_context) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const filePath = getPathProfile(this.origin_url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return !(await this.page_context.$('#emailLogin')) && fs.existsSync(filePath);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    waitVerifyData = async () =>
 | 
				
			||||||
 | 
					        new Promise((rev, rej) => {
 | 
				
			||||||
 | 
					            // Tạo timeout để reject sau 1 phút nếu không có phản hồi
 | 
				
			||||||
 | 
					            const timeout = setTimeout(() => {
 | 
				
			||||||
 | 
					                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.`));
 | 
				
			||||||
 | 
					            }, 60 * 1000); // 60 giây
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            global.socket.on(`verify-code.${this.origin_url}`, async (data) => {
 | 
				
			||||||
 | 
					                console.log(`📢 [${this.id}] VERIFY CODE:`, data);
 | 
				
			||||||
 | 
					                clearTimeout(timeout); // Hủy timeout vì đã nhận được mã
 | 
				
			||||||
 | 
					                global.socket.off(`verify-code.${this.origin_url}`); // Xóa listener tránh lặp lại
 | 
				
			||||||
 | 
					                rev(data); // Resolve với dữ liệu nhận được
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async enterOTP(otp) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // Selector cho tất cả các input OTP
 | 
				
			||||||
 | 
					            const inputSelector = '.MuiDialog-container .container input';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Chờ cho các input OTP xuất hiện
 | 
				
			||||||
 | 
					            await this.page_context.waitForSelector(inputSelector, { timeout: 8000 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Lấy tất cả các input OTP
 | 
				
			||||||
 | 
					            const inputs = await this.page_context.$$(inputSelector);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Kiểm tra nếu có đúng 6 trường input
 | 
				
			||||||
 | 
					            if (inputs.length === 6 && otp.length === 6) {
 | 
				
			||||||
 | 
					                // Nhập mỗi ký tự của OTP vào các input tương ứng
 | 
				
			||||||
 | 
					                for (let i = 0; i < 6; i++) {
 | 
				
			||||||
 | 
					                    await inputs[i].type(otp[i], { delay: 100 });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                console.log(`✅ OTP entered successfully: ${otp}`);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                console.error('❌ Invalid OTP or input fields count');
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            console.error('❌ Error entering OTP:', error);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async waitToTwoVerify() {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            if (!this.page_context) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Selector của các phần tử trên trang
 | 
				
			||||||
 | 
					            const button = '.form-input-wrapper.form-group > .btn.btn-primary'; // Nút để tiếp tục quá trình xác minh
 | 
				
			||||||
 | 
					            const remember = '.PrivateSwitchBase-input'; // Checkbox "Remember me"
 | 
				
			||||||
 | 
					            const continueButton =
 | 
				
			||||||
 | 
					                '.MuiButtonBase-root.MuiButton-root.MuiButton-contained.MuiButton-containedPrimary.MuiButton-sizeMedium.MuiButton-containedSizeMedium.MuiButton-colorPrimary.MuiButton-root'; // Nút "Continue"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Chờ cho nút xác minh xuất hiện
 | 
				
			||||||
 | 
					            console.log(`🔎 [${this.id}] Waiting for the button with selector: ${button}`);
 | 
				
			||||||
 | 
					            await this.page_context.waitForSelector(button, { timeout: 8000 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            console.log(`✅ [${this.id}] Button found, clicking the first button.`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Lấy phần tử của nút và log nội dung của nó
 | 
				
			||||||
 | 
					            const firstButton = await this.page_context.$(button); // Lấy phần tử đầu tiên
 | 
				
			||||||
 | 
					            const buttonContent = await firstButton.evaluate((el) => el.textContent); // Lấy nội dung của nút
 | 
				
			||||||
 | 
					            console.log(`🔎 [${this.id}] Button content: ${buttonContent}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Chờ 2s cho button sẵn sàn
 | 
				
			||||||
 | 
					            await delay(2000);
 | 
				
			||||||
 | 
					            // Click vào nút xác minh
 | 
				
			||||||
 | 
					            await firstButton.click();
 | 
				
			||||||
 | 
					            console.log(`✅ [${this.id}] Button clicked.`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Nhận mã OTP để nhập vào form
 | 
				
			||||||
 | 
					            const { name, code } = await this.waitVerifyData();
 | 
				
			||||||
 | 
					            console.log(`🔎 [${this.id}] Waiting for OTP input, received code: ${code}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Nhập mã OTP vào form
 | 
				
			||||||
 | 
					            await this.enterOTP(code);
 | 
				
			||||||
 | 
					            console.log(`✅ [${this.id}] OTP entered successfully.`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Chờ cho checkbox "Remember me" xuất hiện
 | 
				
			||||||
 | 
					            await this.page_context.waitForSelector(remember, { timeout: 8000 });
 | 
				
			||||||
 | 
					            console.log(`🔎 [${this.id}] Waiting for remember me checkbox with selector: ${remember}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Click vào checkbox "Remember me"
 | 
				
			||||||
 | 
					            await this.page_context.click(remember, { delay: 92 });
 | 
				
			||||||
 | 
					            console.log(`✅ [${this.id}] Remember me checkbox clicked.`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Chờ cho nút "Continue" xuất hiện
 | 
				
			||||||
 | 
					            await this.page_context.waitForSelector(continueButton, { timeout: 8000 });
 | 
				
			||||||
 | 
					            console.log(`🔎 [${this.id}] Waiting for continue button with selector: ${continueButton}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Click vào nút "Continue"
 | 
				
			||||||
 | 
					            await this.page_context.click(continueButton, { delay: 100 });
 | 
				
			||||||
 | 
					            console.log(`✅ [${this.id}] Continue button clicked.`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Chờ cho trang tải hoàn tất sau khi click "Continue"
 | 
				
			||||||
 | 
					            await this.page_context.waitForNavigation({ waitUntil: 'domcontentloaded' });
 | 
				
			||||||
 | 
					            console.log(`✅ [${this.id}] Navigation completed.`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            console.error(`❌ [${this.id}] Error:`, error);
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async handleLogin() {
 | 
				
			||||||
 | 
					        const page = this.page_context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        global.IS_CLEANING = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const filePath = getPathProfile(this.origin_url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await page.waitForNavigation({ waitUntil: 'domcontentloaded' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 🛠 Check if already logged in (login input should not be visible or profile exists)
 | 
				
			||||||
 | 
					        if (!(await page.$('#emailLogin')) && fs.existsSync(filePath)) {
 | 
				
			||||||
 | 
					            console.log(`✅ [${this.id}] Already logged in, skipping login process.`);
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (fs.existsSync(filePath)) {
 | 
				
			||||||
 | 
					            console.log(`🗑 [${this.id}] Deleting existing file: ${filePath}`);
 | 
				
			||||||
 | 
					            fs.unlinkSync(filePath);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const children = this.children.filter((item) => item.page_context);
 | 
				
			||||||
 | 
					        console.log(`🔍 [${this.id}] Found ${children.length} child pages to close.`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (children.length > 0) {
 | 
				
			||||||
 | 
					            console.log(`🛑 [${this.id}] Closing child pages...`);
 | 
				
			||||||
 | 
					            await Promise.all(
 | 
				
			||||||
 | 
					                children.map((item) => {
 | 
				
			||||||
 | 
					                    console.log(`➡ [${this.id}] Closing child page with context: ${item.page_context}`);
 | 
				
			||||||
 | 
					                    return safeClosePage(item);
 | 
				
			||||||
 | 
					                }),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            console.log(`➡ [${this.id}] Closing main page context: ${this.page_context}`);
 | 
				
			||||||
 | 
					            await safeClosePage(this);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log(`🔑 [${this.id}] Starting login process...`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // ⌨ Enter email
 | 
				
			||||||
 | 
					            console.log(`✍ [${this.id}] Entering email:`, this.username);
 | 
				
			||||||
 | 
					            await page.type('#emailLogin', this.username, { delay: 100 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // ⌨ Enter password
 | 
				
			||||||
 | 
					            console.log(`✍ [${this.id}] Entering password...`);
 | 
				
			||||||
 | 
					            await page.type('#passwordLogin', this.password, { delay: 150 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 🚀 Click the login button
 | 
				
			||||||
 | 
					            console.log(`🔘 [${this.id}] Clicking the "Login" button`);
 | 
				
			||||||
 | 
					            await page.click('#signInBtn', { delay: 92 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const result = await this.waitToTwoVerify();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // ⏳ Wait for navigation after login
 | 
				
			||||||
 | 
					            if (!result) {
 | 
				
			||||||
 | 
					                console.log(`⏳ [${this.id}] Waiting for navigation after login...`);
 | 
				
			||||||
 | 
					                await page.waitForNavigation({ timeout: 8000, waitUntil: 'domcontentloaded' });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (this.page_context.url() == this.url) {
 | 
				
			||||||
 | 
					                // 📂 Save session context to avoid re-login
 | 
				
			||||||
 | 
					                await this.saveContext();
 | 
				
			||||||
 | 
					                console.log(`✅ [${this.id}] Login successful!`);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                console.log(`❌ [${this.id}] Login Failure!`);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            console.error(`❌ [${this.id}] Error during login process:`, error.message);
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					            global.IS_CLEANING = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    action = async () => {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            const page = this.page_context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            page.on('response', async (response) => {
 | 
				
			||||||
 | 
					                const request = response.request();
 | 
				
			||||||
 | 
					                if (request.redirectChain().length > 0) {
 | 
				
			||||||
 | 
					                    if (response.url().includes(configs.WEB_CONFIGS.LAWSONS.LOGIN_URL)) {
 | 
				
			||||||
 | 
					                        await this.handleLogin();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await page.goto(this.url, { waitUntil: 'networkidle2' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await page.bringToFront();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Set userAgent
 | 
				
			||||||
 | 
					            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');
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            console.log('Error [action]: ', error.message);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    listen_events = async () => {
 | 
				
			||||||
 | 
					        if (this.page_context) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await this.puppeteer_connect();
 | 
				
			||||||
 | 
					        await this.action();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.reloadInterval = setInterval(async () => {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                if (this.page_context && !this.page_context.isClosed()) {
 | 
				
			||||||
 | 
					                    console.log(`🔄 [${this.id}] Reloading page...`);
 | 
				
			||||||
 | 
					                    await this.page_context.reload({ waitUntil: 'networkidle2' });
 | 
				
			||||||
 | 
					                    console.log(`✅ [${this.id}] Page reloaded successfully.`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // this.handleUpdateWonItem();
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    console.log(`❌ [${this.id}] Page context is closed. Stopping reload.`);
 | 
				
			||||||
 | 
					                    clearInterval(this.reloadInterval);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } catch (error) {
 | 
				
			||||||
 | 
					                console.error(`🚨 [${this.id}] Error reloading page:`, error.message);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }, 60000);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,114 @@
 | 
				
			||||||
 | 
					import { updateBid } from '../../system/apis/bid.js';
 | 
				
			||||||
 | 
					import configs from '../../system/config.js';
 | 
				
			||||||
 | 
					import { extractPriceNumber, 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 {
 | 
				
			||||||
 | 
					            // 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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await this.page_context.waitForSelector('.select-dropdown-value.text-truncate', { timeout: 2000 });
 | 
				
			||||||
 | 
					            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);
 | 
				
			||||||
 | 
					            // Nếu có lỗi xảy ra trong quá trình lấy thời gian, trả về null
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    update = async () => {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            if (!this.page_context) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            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,
 | 
				
			||||||
 | 
					                    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);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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 };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            console.error(`🚨 [${this.id}] Error navigating the page: ${error}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -135,16 +135,6 @@ export class ProductBid extends Bid {
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // async gotoLink() {
 | 
					 | 
				
			||||||
    //     const page = this.page_context;
 | 
					 | 
				
			||||||
    //     console.log('🔄 Starting the bidding process...');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    //     await page.goto(this.url, { waitUntil: 'networkidle2' });
 | 
					 | 
				
			||||||
    //     console.log(`✅ Navigated to: ${this.url}`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    //     await page.bringToFront();
 | 
					 | 
				
			||||||
    //     console.log('👀 Brought the tab to the foreground.');
 | 
					 | 
				
			||||||
    // }
 | 
					 | 
				
			||||||
    async gotoLink() {
 | 
					    async gotoLink() {
 | 
				
			||||||
        const page = this.page_context;
 | 
					        const page = this.page_context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -160,6 +150,7 @@ export class ProductBid extends Bid {
 | 
				
			||||||
            console.log(`✅ Navigated to: ${this.url}`);
 | 
					            console.log(`✅ Navigated to: ${this.url}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await page.bringToFront();
 | 
					            await page.bringToFront();
 | 
				
			||||||
 | 
					            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('👀 Brought the tab to the foreground.');
 | 
					            console.log('👀 Brought the tab to the foreground.');
 | 
				
			||||||
        } catch (error) {
 | 
					        } catch (error) {
 | 
				
			||||||
            console.error('❌ Error during navigation:', error);
 | 
					            console.error('❌ Error during navigation:', error);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,13 +1,16 @@
 | 
				
			||||||
 | 
					import * as fs from 'fs';
 | 
				
			||||||
import path from 'path';
 | 
					import path from 'path';
 | 
				
			||||||
import { GrayApiBid } from '../models/grays.com/grays-api-bid.js';
 | 
					import { GrayApiBid } from '../models/grays.com/grays-api-bid.js';
 | 
				
			||||||
import { GraysProductBid } from '../models/grays.com/grays-product-bid.js';
 | 
					import { GraysProductBid } from '../models/grays.com/grays-product-bid.js';
 | 
				
			||||||
 | 
					import { LangtonsApiBid } from '../models/langtons.com.au/langtons-api-bid.js';
 | 
				
			||||||
 | 
					import { LangtonsProductBid } from '../models/langtons.com.au/langtons-product-bid.js';
 | 
				
			||||||
import configs from '../system/config.js';
 | 
					import configs from '../system/config.js';
 | 
				
			||||||
import CONSTANTS from '../system/constants.js';
 | 
					import CONSTANTS from '../system/constants.js';
 | 
				
			||||||
import { sanitizeFileName } from '../system/utils.js';
 | 
					import { sanitizeFileName } from '../system/utils.js';
 | 
				
			||||||
import * as fs from 'fs';
 | 
					import { LawsonsApiBid } from '../models/lawsons.com.au/lawsons-api-bid.js';
 | 
				
			||||||
import _ from 'lodash';
 | 
					import { LawsonsProductBid } from '../models/lawsons.com.au/lawsons-product-bid.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ONE_MINUTE = 60 * 1000;
 | 
					const TIME = 30 * 1000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const handleCloseRemoveProduct = (data) => {
 | 
					export const handleCloseRemoveProduct = (data) => {
 | 
				
			||||||
    if (!Array.isArray(data)) return;
 | 
					    if (!Array.isArray(data)) return;
 | 
				
			||||||
| 
						 | 
					@ -24,6 +27,12 @@ export const createBidProduct = (web, data) => {
 | 
				
			||||||
        case configs.WEB_URLS.GRAYS: {
 | 
					        case configs.WEB_URLS.GRAYS: {
 | 
				
			||||||
            return new GraysProductBid({ ...data });
 | 
					            return new GraysProductBid({ ...data });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        case configs.WEB_URLS.LANGTONS: {
 | 
				
			||||||
 | 
					            return new LangtonsProductBid({ ...data });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case configs.WEB_URLS.LAWSONS: {
 | 
				
			||||||
 | 
					            return new LawsonsProductBid({ ...data });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -32,6 +41,12 @@ export const createApiBid = (web) => {
 | 
				
			||||||
        case configs.WEB_URLS.GRAYS: {
 | 
					        case configs.WEB_URLS.GRAYS: {
 | 
				
			||||||
            return new GrayApiBid({ ...web });
 | 
					            return new GrayApiBid({ ...web });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        case configs.WEB_URLS.LANGTONS: {
 | 
				
			||||||
 | 
					            return new LangtonsApiBid({ ...web });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case configs.WEB_URLS.LAWSONS: {
 | 
				
			||||||
 | 
					            return new LawsonsApiBid({ ...web });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,5 +64,6 @@ export const deleteProfile = (data) => {
 | 
				
			||||||
export const shouldUpdateProductTab = (productTab) => {
 | 
					export const shouldUpdateProductTab = (productTab) => {
 | 
				
			||||||
    const updatedAt = new Date(productTab.updated_at).getTime();
 | 
					    const updatedAt = new Date(productTab.updated_at).getTime();
 | 
				
			||||||
    const now = Date.now();
 | 
					    const now = Date.now();
 | 
				
			||||||
    return now - updatedAt >= ONE_MINUTE;
 | 
					
 | 
				
			||||||
 | 
					    return now - updatedAt >= TIME;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,7 +14,7 @@ export const getBids = async () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return data.data;
 | 
					        return data.data;
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
        console.log('❌ ERROR IN SERVER: ', error);
 | 
					        console.log('❌ ERROR IN SERVER (GET BIDS): ', error);
 | 
				
			||||||
        return [];
 | 
					        return [];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -34,7 +34,7 @@ export const updateBid = async (id, values) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return data.data;
 | 
					        return data.data;
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
        console.log('❌ ERROR IN SERVER: (UPDATE BID) ', error.response);
 | 
					        console.log('❌ ERROR IN SERVER: (UPDATE BID) ', error);
 | 
				
			||||||
        return null;
 | 
					        return null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -73,7 +73,7 @@ export const pushPrice = async (values) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return { status: true, data: data.data };
 | 
					        return { status: true, data: data.data };
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
        console.log('❌ ERROR IN SERVER: ', error);
 | 
					        console.log('❌ ERROR IN SERVER (PUSH PRICE): ', error.response.data);
 | 
				
			||||||
        return { status: false, data: [] };
 | 
					        return { status: false, data: [] };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -95,7 +95,10 @@ export const updateStatusByPrice = async (id, current_price) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return { status: true, data: data.data };
 | 
					        return { status: true, data: data.data };
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
        console.log('❌ ERROR IN SERVER:(UPDATE STATUS BY PRICE) ', error.message);
 | 
					        console.log('❌ ERROR IN SERVER:(UPDATE STATUS BY PRICE) ', {
 | 
				
			||||||
 | 
					            // response: error.response,
 | 
				
			||||||
 | 
					            message: error.message,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
        return { status: false, data: [] };
 | 
					        return { status: false, data: [] };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					import axios from '../axios.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const sendMessage = async (values) => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        const { data } = await axios({
 | 
				
			||||||
 | 
					            method: 'POST',
 | 
				
			||||||
 | 
					            url: 'notifications/send-messages',
 | 
				
			||||||
 | 
					            data: values,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!data || !data?.data) {
 | 
				
			||||||
 | 
					            console.log('❌ UPDATE FAILURE (UPDATE Noti)');
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return data.data;
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        console.log('❌ ERROR IN SERVER: (UPDATE Noti) ', error);
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -4,12 +4,29 @@ const configs = {
 | 
				
			||||||
    SOCKET_URL: process.env.SOCKET_URL,
 | 
					    SOCKET_URL: process.env.SOCKET_URL,
 | 
				
			||||||
    WEB_URLS: {
 | 
					    WEB_URLS: {
 | 
				
			||||||
        GRAYS: `https://www.grays.com`,
 | 
					        GRAYS: `https://www.grays.com`,
 | 
				
			||||||
 | 
					        LANGTONS: `https://www.langtons.com.au`,
 | 
				
			||||||
 | 
					        LAWSONS: `https://www.lawsons.com.au`,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    WEB_CONFIGS: {
 | 
					    WEB_CONFIGS: {
 | 
				
			||||||
        GRAYS: {
 | 
					        GRAYS: {
 | 
				
			||||||
            AUTO_CALL_API_TO_TRACKING: 3000,
 | 
					            AUTO_CALL_API_TO_TRACKING: 3000,
 | 
				
			||||||
            API_CALL_TO_TRACKING: 'https://www.grays.com/api/Notifications/GetOutBidLots',
 | 
					            API_CALL_TO_TRACKING: 'https://www.grays.com/api/Notifications/GetOutBidLots',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        LANGTONS: {
 | 
				
			||||||
 | 
					            AUTO_CALL_API_TO_TRACKING: 5000,
 | 
				
			||||||
 | 
					            LOGIN_URL: 'https://www.langtons.com.au/account/login',
 | 
				
			||||||
 | 
					            API_CALL_TO_TRACKING: 'https://www.langtons.com.au/on/demandware.store/Sites-langtons-Site/en_AU/Auction-LotsData',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        LAWSONS: {
 | 
				
			||||||
 | 
					            LOGIN_URL: 'https://www.lawsons.com.au/login?redirectUrl=/my-account/current-bids',
 | 
				
			||||||
 | 
					            // API_CALL_TO_TRACKING: 'https://www.langtons.com.au/on/demandware.store/Sites-langtons-Site/en_AU/Auction-LotsData',
 | 
				
			||||||
 | 
					            API_DETAIL_INFO: (model) => {
 | 
				
			||||||
 | 
					                return `https://www.lawsons.com.au/api/auctions/lot/v2/liveInfo/${model}`;
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            API_DETAIL_PRODUCT: (model) => {
 | 
				
			||||||
 | 
					                return `https://www.lawsons.com.au/api/auctions/lot/${model}`;
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,6 @@ export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_
 | 
				
			||||||
    if (!page || page.isClosed()) return;
 | 
					    if (!page || page.isClosed()) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
        global.IS_CLEANING = false;
 | 
					 | 
				
			||||||
        const baseDir = path.join(CONSTANTS.ERROR_IMAGES_PATH, item.type, String(item.id)); // Thư mục theo lot_id
 | 
					        const baseDir = path.join(CONSTANTS.ERROR_IMAGES_PATH, item.type, String(item.id)); // Thư mục theo lot_id
 | 
				
			||||||
        const typeDir = path.join(baseDir, type); // Thư mục con theo type
 | 
					        const typeDir = path.join(baseDir, type); // Thư mục con theo type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -101,3 +100,81 @@ export function removeFalsyValues(obj, excludeKeys = []) {
 | 
				
			||||||
        return acc;
 | 
					        return acc;
 | 
				
			||||||
    }, {});
 | 
					    }, {});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const enableAutoBidMessage = (data) => {
 | 
				
			||||||
 | 
					    return `
 | 
				
			||||||
 | 
					        <b>⭉ Activate Auto Bid</b><br>
 | 
				
			||||||
 | 
					    📌 Product: <b>${data.name}</b><br>
 | 
				
			||||||
 | 
					    🔗 Link: <a href="${data.url}">Click here</a><br>
 | 
				
			||||||
 | 
					    💰 Max Price: <b>$${data.max_price}</b><br>
 | 
				
			||||||
 | 
					    🌐 Platform: <a href="${data.web_bid.origin_url}">Langtons</a>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function convertAETtoUTC(dateString) {
 | 
				
			||||||
 | 
					    // Bảng ánh xạ tên tháng sang số (0-11, theo chuẩn JavaScript)
 | 
				
			||||||
 | 
					    const monthMap = {
 | 
				
			||||||
 | 
					        Jan: 0,
 | 
				
			||||||
 | 
					        Feb: 1,
 | 
				
			||||||
 | 
					        Mar: 2,
 | 
				
			||||||
 | 
					        Apr: 3,
 | 
				
			||||||
 | 
					        May: 4,
 | 
				
			||||||
 | 
					        Jun: 5,
 | 
				
			||||||
 | 
					        Jul: 6,
 | 
				
			||||||
 | 
					        Aug: 7,
 | 
				
			||||||
 | 
					        Sep: 8,
 | 
				
			||||||
 | 
					        Oct: 9,
 | 
				
			||||||
 | 
					        Nov: 10,
 | 
				
			||||||
 | 
					        Dec: 11,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Tách chuỗi đầu vào
 | 
				
			||||||
 | 
					    const parts = dateString.match(/(\w+)\s(\d+)\s(\w+)\s(\d+),\s(\d+)\s(PM|AM)\sAET/);
 | 
				
			||||||
 | 
					    if (!parts) {
 | 
				
			||||||
 | 
					        throw new Error("Error format: 'Sun 6 Apr 2025, 9 PM AET'");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const [, , day, month, year, hour, period] = parts;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Chuyển đổi giờ sang định dạng 24h
 | 
				
			||||||
 | 
					    let hours = parseInt(hour, 10);
 | 
				
			||||||
 | 
					    if (period === 'PM' && hours !== 12) hours += 12;
 | 
				
			||||||
 | 
					    if (period === 'AM' && hours === 12) hours = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Tạo đối tượng Date ban đầu (chưa điều chỉnh múi giờ)
 | 
				
			||||||
 | 
					    const date = new Date(Date.UTC(parseInt(year, 10), monthMap[month], parseInt(day, 10), hours, 0, 0));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Hàm kiểm tra DST cho AET
 | 
				
			||||||
 | 
					    function isDST(date) {
 | 
				
			||||||
 | 
					        const year = date.getUTCFullYear();
 | 
				
			||||||
 | 
					        const month = date.getUTCMonth();
 | 
				
			||||||
 | 
					        const day = date.getUTCDate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // DST bắt đầu: Chủ nhật đầu tiên của tháng 10 (2:00 AM AEST -> 3:00 AM AEDT)
 | 
				
			||||||
 | 
					        const dstStart = new Date(Date.UTC(year, 9, 1, 0, 0, 0)); // 1/10
 | 
				
			||||||
 | 
					        dstStart.setUTCDate(1 + ((7 - dstStart.getUTCDay()) % 7)); // Chủ nhật đầu tiên
 | 
				
			||||||
 | 
					        const dstStartTime = dstStart.getTime() + 2 * 60 * 60 * 1000; // 2:00 AM UTC+10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // DST kết thúc: Chủ nhật đầu tiên của tháng 4 (3:00 AM AEDT -> 2:00 AM AEST)
 | 
				
			||||||
 | 
					        const dstEnd = new Date(Date.UTC(year, 3, 1, 0, 0, 0)); // 1/4
 | 
				
			||||||
 | 
					        dstEnd.setUTCDate(1 + ((7 - dstEnd.getUTCDay()) % 7)); // Chủ nhật đầu tiên
 | 
				
			||||||
 | 
					        const dstEndTime = dstEnd.getTime() + 3 * 60 * 60 * 1000; // 3:00 AM UTC+11
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const currentTime = date.getTime() + 10 * 60 * 60 * 1000; // Thời gian AET (giả định ban đầu UTC+10)
 | 
				
			||||||
 | 
					        return currentTime >= dstStartTime && currentTime < dstEndTime;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Xác định offset dựa trên DST
 | 
				
			||||||
 | 
					    const offset = isDST(date) ? 11 : 10; // UTC+11 nếu DST, UTC+10 nếu không
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Điều chỉnh thời gian về UTC
 | 
				
			||||||
 | 
					    const utcDate = new Date(date.getTime() - offset * 60 * 60 * 1000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Trả về chuỗi UTC
 | 
				
			||||||
 | 
					    return utcDate.toUTCString();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function extractPriceNumber(priceString) {
 | 
				
			||||||
 | 
					    const cleaned = priceString.replace(/[^\d.]/g, '');
 | 
				
			||||||
 | 
					    return parseFloat(cleaned);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue