update
This commit is contained in:
parent
efd8cd1fe8
commit
ec9b9506a6
|
|
@ -0,0 +1,17 @@
|
|||
import { handleError } from '.';
|
||||
import axios from '../lib/axios';
|
||||
import { IBid } from '../system/type';
|
||||
|
||||
export const getDetailBidHistories = async (lot_id: IBid['lot_id']) => {
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: `bid-histories/detail/${lot_id}`,
|
||||
withCredentials: true,
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
|
@ -32,14 +32,14 @@ export const createBid = async (bid: Omit<IBid, 'id' | 'created_at' | 'updated_a
|
|||
};
|
||||
|
||||
export const updateBid = async (bid: Partial<IBid>) => {
|
||||
const { step_price, max_price, quantity } = removeFalsyValues(bid);
|
||||
const { plus_price, max_price, quantity } = removeFalsyValues(bid, ['plus_price']);
|
||||
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: 'bids/' + bid.id,
|
||||
withCredentials: true,
|
||||
method: 'PUT',
|
||||
data: { step_price, max_price, quantity },
|
||||
data: { plus_price, max_price, quantity },
|
||||
});
|
||||
|
||||
handleSuccess(data);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export interface IBidModelProps extends ModalProps {
|
|||
const schema = {
|
||||
url: z.string({ message: 'Url is required' }).url('Invalid url format'),
|
||||
max_price: z.number({ message: 'Max price is required' }).min(1, 'Max price must be at least 1'),
|
||||
step_price: z.number().min(0, 'Step price must be at least 1').optional(),
|
||||
plus_price: z.number().min(0, 'Plus price must be at least 1').optional(),
|
||||
quantity: z.number().min(1, 'Quantity must be at least 1').optional(),
|
||||
};
|
||||
|
||||
|
|
@ -50,9 +50,9 @@ export default function BidModal({ data, onUpdated, ...props }: IBidModelProps)
|
|||
},
|
||||
});
|
||||
} else {
|
||||
const { url, max_price, step_price } = values;
|
||||
const { url, max_price, plus_price } = values;
|
||||
|
||||
const result = await createBid({ url, max_price, step_price } as IBid);
|
||||
const result = await createBid({ url, max_price, plus_price } as IBid);
|
||||
|
||||
if (!result) return;
|
||||
|
||||
|
|
@ -92,9 +92,10 @@ export default function BidModal({ data, onUpdated, ...props }: IBidModelProps)
|
|||
centered
|
||||
>
|
||||
<form onSubmit={form.onSubmit(handleSubmit)} className="grid grid-cols-2 gap-2.5">
|
||||
{data && data.name && <TextInput className="col-span-2" readOnly={!!data} size="sm" label="Name" value={data.name} />}
|
||||
<TextInput className="col-span-2" readOnly={!!data} size="sm" label="Url" {...form.getInputProps('url')} />
|
||||
<NumberInput className="col-span-2" size="sm" label="Max price" {...form.getInputProps('max_price')} />
|
||||
<NumberInput size="sm" label="Step price" {...form.getInputProps('step_price')} />
|
||||
<NumberInput size="sm" label="Plus price" {...form.getInputProps('plus_price')} />
|
||||
<NumberInput size="sm" label="Quantity" {...form.getInputProps('quantity')} />
|
||||
|
||||
<Button disabled={_.isEqual(form.getValues(), prevData.current)} className="col-span-2" type="submit" fullWidth size="sm" mt="md">
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export { default as ShowHistoriesModal } from './show-histories-modal';
|
||||
export { default as ShowHistoriesBidGraysApiModal } from './show-histories-bid-grays-api-modal';
|
||||
export { default as BidModal } from './bid-modal';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Modal, ModalProps, Table } from '@mantine/core';
|
||||
import axios from 'axios';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { extractNumber } from '../../lib/table/ultils';
|
||||
import { IBid } from '../../system/type';
|
||||
import { formatTime } from '../../utils';
|
||||
import { getDetailBidHistories } from '../../apis/bid-histories';
|
||||
|
||||
export interface IShowHistoriesBidGraysApiModalProps extends ModalProps {
|
||||
data: IBid | null;
|
||||
onUpdated?: () => void;
|
||||
}
|
||||
|
||||
export default function ShowHistoriesBidGraysApiModal({ data, onUpdated, ...props }: IShowHistoriesBidGraysApiModalProps) {
|
||||
const [histories, setHistories] = useState<Record<string, string>[]>([]);
|
||||
|
||||
const rows = useMemo(() => {
|
||||
return histories.map((element) => (
|
||||
<Table.Tr key={element.LotId}>
|
||||
<Table.Td>{`${element['UserInitials']} - ${element['UserShortAddress']}`}</Table.Td>
|
||||
<Table.Td>{formatTime(new Date(extractNumber(element['OriginalDate']) || 0).toUTCString(), 'HH:mm:ss DD/MM/YYYY')}</Table.Td>
|
||||
<Table.Td>{`AU $${element['Price']}`}</Table.Td>
|
||||
<Table.Td>{`${element['Quantity']}`}</Table.Td>
|
||||
<Table.Td>{`${element['WinningQuantity']}`}</Table.Td>
|
||||
</Table.Tr>
|
||||
));
|
||||
}, [histories]);
|
||||
|
||||
const handleCallApi = useCallback(async () => {
|
||||
if (!data?.lot_id) {
|
||||
setHistories([]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await getDetailBidHistories(data?.lot_id);
|
||||
|
||||
if (response.data && response.data) {
|
||||
setHistories(response.data);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
handleCallApi();
|
||||
}, [handleCallApi]);
|
||||
|
||||
return (
|
||||
<Modal {...props} size="xl" title={<span className="text-xl font-bold">BIDDING HISTORY</span>} centered>
|
||||
<Table striped highlightOnHover withTableBorder withColumnBorders>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Bidding Details</Table.Th>
|
||||
<Table.Th>Bid Time</Table.Th>
|
||||
<Table.Th>Bid Price</Table.Th>
|
||||
<Table.Th>Bid Qty</Table.Th>
|
||||
<Table.Th>Win Qty</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{histories.length <= 0 ? (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={5} className="text-center">
|
||||
None
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
) : (
|
||||
rows
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ export interface IShowHistoriesModalModalProps extends ModalProps {
|
|||
onUpdated?: () => void;
|
||||
}
|
||||
|
||||
export default function ConfigModal({ data, onUpdated, ...props }: IShowHistoriesModalModalProps) {
|
||||
export default function ShowHistoriesModal({ data, onUpdated, ...props }: IShowHistoriesModalModalProps) {
|
||||
const rows = data?.histories.map((element) => (
|
||||
<Table.Tr key={element.id}>
|
||||
<Table.Td>{element.id}</Table.Td>
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import { Box, Checkbox, MantineStyleProp, Table as MTable, TableProps as MTableP
|
|||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import DOMPurify from 'dompurify';
|
||||
import React, { ChangeEvent, CSSProperties, ReactNode, useCallback, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import TableActions, { ITableActionsProps } from './action';
|
||||
import Filter from './filter';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { IChooseOptions, IColumn, IColumnStyle, IDataFilter, IFilterItemProps, IOptions, ITableFilter, ITableShort, TableChildProps, TKeyPagination, TRefTableFn } from './type';
|
||||
import { defaultPathToData, defaultPrefixShort, defaultStyleHightlight, flowShort, getParamsData as getParamsFromURL, searchKey } from './ultils';
|
||||
|
||||
|
|
@ -639,6 +639,7 @@ const Table = <R extends Record<string, any>>({
|
|||
)}
|
||||
</div>
|
||||
|
||||
<MTable.ScrollContainer minWidth={500} type="scrollarea">
|
||||
<MTable {...props}>
|
||||
<MTable.Thead {...thead}>
|
||||
<MTable.Tr {...trhead}>
|
||||
|
|
@ -769,6 +770,7 @@ const Table = <R extends Record<string, any>>({
|
|||
))}
|
||||
</MTable.Tbody>
|
||||
</MTable>
|
||||
</MTable.ScrollContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -77,3 +77,8 @@ export const removeFalsy = (data: { [key: string]: string | number }) => {
|
|||
return prev;
|
||||
}, {} as { [key: string]: string | number });
|
||||
};
|
||||
|
||||
export function extractNumber(str: string) {
|
||||
const match = str.match(/\d+(\.\d+)?/);
|
||||
return match ? parseFloat(match[0]) : null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { IconAd, IconAdOff, IconEdit, IconHistory, IconMenu, IconTrash } from '@
|
|||
import _ from 'lodash';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { deleteBid, deletesBid, getBids, toggleBid } from '../apis/bid';
|
||||
import { BidModal, ShowHistoriesModal } from '../components/bid';
|
||||
import { BidModal, ShowHistoriesBidGraysApiModal, ShowHistoriesModal } from '../components/bid';
|
||||
import Table from '../lib/table/table';
|
||||
import { IColumn, TRefTableFn } from '../lib/table/type';
|
||||
import { useConfirmStore } from '../lib/zustand/use-confirm';
|
||||
|
|
@ -20,13 +20,14 @@ export default function Bids() {
|
|||
const { setConfirm } = useConfirmStore();
|
||||
|
||||
const [openedHistories, historiesModel] = useDisclosure(false);
|
||||
const [openedHistoriesGraysApi, historiesGraysApiModel] = useDisclosure(false);
|
||||
const [openedBid, bidModal] = useDisclosure(false);
|
||||
|
||||
const columns: IColumn<IBid>[] = [
|
||||
{
|
||||
key: 'id',
|
||||
title: 'ID',
|
||||
typeFilter: 'number',
|
||||
key: 'name',
|
||||
title: 'Name',
|
||||
typeFilter: 'text',
|
||||
},
|
||||
{
|
||||
key: 'lot_id',
|
||||
|
|
@ -38,14 +39,14 @@ export default function Bids() {
|
|||
title: 'Model',
|
||||
typeFilter: 'text',
|
||||
},
|
||||
// {
|
||||
// key: 'quantity',
|
||||
// title: 'Qty',
|
||||
// typeFilter: 'number',
|
||||
// },
|
||||
{
|
||||
key: 'quantity',
|
||||
title: 'Qty',
|
||||
typeFilter: 'number',
|
||||
},
|
||||
{
|
||||
key: 'step_price',
|
||||
title: 'Step price',
|
||||
key: 'plus_price',
|
||||
title: 'Plus price',
|
||||
typeFilter: 'number',
|
||||
},
|
||||
{
|
||||
|
|
@ -58,6 +59,11 @@ export default function Bids() {
|
|||
title: 'Current price',
|
||||
typeFilter: 'number',
|
||||
},
|
||||
{
|
||||
key: 'reserve_price',
|
||||
title: 'Reserve price',
|
||||
typeFilter: 'number',
|
||||
},
|
||||
{
|
||||
key: 'histories',
|
||||
title: 'Current bid',
|
||||
|
|
@ -107,14 +113,14 @@ export default function Bids() {
|
|||
},
|
||||
},
|
||||
|
||||
{
|
||||
key: 'updated_at',
|
||||
title: 'Update at',
|
||||
typeFilter: 'none',
|
||||
renderRow(row) {
|
||||
return <span className="text-sm">{formatTime(row.updated_at)}</span>;
|
||||
},
|
||||
},
|
||||
// {
|
||||
// key: 'updated_at',
|
||||
// title: 'Update at',
|
||||
// typeFilter: 'none',
|
||||
// renderRow(row) {
|
||||
// return <span className="text-sm">{formatTime(row.updated_at)}</span>;
|
||||
// },
|
||||
// },
|
||||
];
|
||||
|
||||
const handleDelete = (bid: IBid) => {
|
||||
|
|
@ -242,6 +248,15 @@ export default function Bids() {
|
|||
>
|
||||
Histories
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
setClickData(row);
|
||||
historiesGraysApiModel.open();
|
||||
}}
|
||||
leftSection={<IconHistory size={14} />}
|
||||
>
|
||||
Bids
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
disabled={row.status === 'win-bid'}
|
||||
|
|
@ -293,6 +308,23 @@ export default function Bids() {
|
|||
}}
|
||||
data={clickData}
|
||||
/>
|
||||
|
||||
<ShowHistoriesBidGraysApiModal
|
||||
onUpdated={() => {
|
||||
if (refTableFn.current?.fetchData) {
|
||||
refTableFn.current.fetchData();
|
||||
}
|
||||
|
||||
setClickData(null);
|
||||
}}
|
||||
opened={openedHistoriesGraysApi}
|
||||
onClose={() => {
|
||||
historiesGraysApiModel.close();
|
||||
|
||||
setClickData(null);
|
||||
}}
|
||||
data={clickData}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@ import io from 'socket.io-client';
|
|||
import { WorkingPage } from '../components/dashboard';
|
||||
import { IBid, IWebBid } from '../system/type';
|
||||
|
||||
// Dùng singleton để giữ kết nối WebSocket khi chuyển trang
|
||||
const socket = io(import.meta.env.VITE_SOCKET_URL, {
|
||||
autoConnect: true, // Tránh tự động kết nối khi import file
|
||||
transports: ['websocket'], // Chỉ dùng WebSocket để giảm độ trễ
|
||||
autoConnect: true,
|
||||
transports: ['websocket'],
|
||||
});
|
||||
|
||||
export default function DashBoard() {
|
||||
|
|
|
|||
|
|
@ -16,13 +16,14 @@ export interface ITimestamp {
|
|||
export interface IBid extends ITimestamp {
|
||||
id: number;
|
||||
max_price: number;
|
||||
reserve_price: number;
|
||||
current_price: number;
|
||||
name: string | null;
|
||||
quantity: number;
|
||||
url: string;
|
||||
model: string;
|
||||
lot_id: string;
|
||||
step_price: number;
|
||||
plus_price: number;
|
||||
close_time: string | null;
|
||||
start_bid_time: string | null;
|
||||
first_bid: boolean;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
"axios": "^1.8.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.13.0",
|
||||
"nestjs-paginate": "^11.1.0",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
"axios": "^1.8.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.13.0",
|
||||
"nestjs-paginate": "^11.1.0",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import axios from 'axios';
|
||||
import { escapeMarkdownV2 } from 'src/ultils';
|
||||
import { Bid } from '../entities/bid.entity';
|
||||
import * as dayjs from 'dayjs';
|
||||
|
||||
@Injectable()
|
||||
export class BotTelegramApi {
|
||||
private readonly botToken: string;
|
||||
private readonly apiUrl: string;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.botToken = this.configService.get<string>('TELEGRAM_BOT_TOKEN');
|
||||
this.apiUrl = `https://api.telegram.org/bot${this.botToken}`;
|
||||
}
|
||||
|
||||
formatBidMessage(bid: Bid) {
|
||||
return `<b>📢 New Bid Alert!</b>\n
|
||||
<b>📌 Product:</b> ${bid.name ? `<b>${bid.name}</b>` : '_No name_'}\n
|
||||
<b>💰 Current Price:</b> <code>${bid.current_price.toLocaleString()}${bid.plus_price ? ` (+${bid.plus_price.toLocaleString()})` : ''}</code>\n
|
||||
<b>🏆 Max Price:</b> <code>${bid.max_price.toLocaleString()}</code>\n
|
||||
<b>🔖 Starting Price:</b> <code>${bid.reserve_price.toLocaleString()}</code>\n\n
|
||||
<b>📦 Model:</b> <code>${bid.model || 'None'}</code>\n
|
||||
<b>🔢 Quantity:</b> <code>${bid.quantity}</code>\n
|
||||
<b>🆔 Lot ID:</b> <code>${bid.lot_id ? bid.lot_id.toString() : 'None'}</code>\n\n
|
||||
<b>⏳ Closing Time:</b> ${bid.close_time ? `<code>${dayjs(bid.close_time).format('HH:mm:ss DD/MM/YYYY')}</code>` : '_Not available_'}\n
|
||||
<b>🚀 Start Time:</b> ${bid.start_bid_time ? `<code>${dayjs(bid.start_bid_time).format('HH:mm:ss DD/MM/YYYY')}</code>` : '_Not available_'}\n\n
|
||||
<b>📈 Bid History:</b>\n${
|
||||
bid.histories.length > 0
|
||||
? bid.histories
|
||||
.map(
|
||||
(h, index) =>
|
||||
` ${index + 1}. 💵 <code>${index === 0 ? '🔴' : ''}${h.price.toLocaleString()}</code> 🕒 <code>${dayjs(h.updated_at).format('HH:mm:ss DD/MM/YYYY')}</code>`,
|
||||
)
|
||||
.join('\n')
|
||||
: '_No bid history yet_'
|
||||
}\n\n
|
||||
<b>📝 Status:</b> <code>${bid.status.replace('-', ' ')}</code>\n\n
|
||||
⬇️ <b>View Details:</b> <a href="${bid.url}">Click here</a>`;
|
||||
}
|
||||
|
||||
async sendMessage(
|
||||
text: string,
|
||||
bonusBody: Record<string, any> = {},
|
||||
chatId: string = this.configService.get<string>('CHAT_ID'),
|
||||
): Promise<void> {
|
||||
try {
|
||||
const url = `${this.apiUrl}/sendMessage`;
|
||||
const response = await axios.post(
|
||||
url,
|
||||
{
|
||||
chat_id: chatId,
|
||||
text: text,
|
||||
...bonusBody,
|
||||
},
|
||||
{
|
||||
family: 4,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error || error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async sendBidInfo(bid: Bid): Promise<void> {
|
||||
const text = this.formatBidMessage(bid);
|
||||
|
||||
console.log(text);
|
||||
await this.sendMessage(text, {
|
||||
parse_mode: 'HTML',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import axios from 'axios';
|
||||
import AppResponse from 'src/response/app-response';
|
||||
import { Bid } from '../entities/bid.entity';
|
||||
|
||||
@Injectable()
|
||||
export class GraysApi {
|
||||
async getHistoriesBid(lot_id: Bid['lot_id']) {
|
||||
try {
|
||||
const response = await axios({
|
||||
url: `https://www.grays.com/api/LotInfo/GetBiddingHistory?lotId=${lot_id}¤cyCode=AUD`,
|
||||
});
|
||||
|
||||
if (response.data && response.data?.Bids) {
|
||||
return AppResponse.toResponse(response.data.Bids);
|
||||
}
|
||||
} catch (error) {
|
||||
return AppResponse.toResponse([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,8 @@ import { BidHistoriesService } from './services/bid-histories.service';
|
|||
import { BidsService } from './services/bids.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';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -40,6 +42,8 @@ import { WebBidsService } from './services/web-bids.service';
|
|||
BidGateway,
|
||||
OutBidLogsService,
|
||||
WebBidsService,
|
||||
BotTelegramApi,
|
||||
GraysApi,
|
||||
],
|
||||
})
|
||||
export class BidsModule {}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,25 @@
|
|||
import { Body, Controller, Get, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { BidsService } from '../../services/bids.service';
|
||||
import { CreateBidDto } from '../../dto/bid/create-bid.dto';
|
||||
import { BidHistoriesService } from '../../services/bid-histories.service';
|
||||
import { CreateBidHistoryDto } from '../../dto/bid-history/create-bid-history.dto';
|
||||
import { Bid } from '../../entities/bid.entity';
|
||||
import { GraysApi } from '../../apis/grays.api';
|
||||
|
||||
@Controller('admin/bid-histories')
|
||||
export class AdminBidHistoriesController {
|
||||
constructor(private readonly bidHistoriesService: BidHistoriesService) {}
|
||||
constructor(
|
||||
private readonly bidHistoriesService: BidHistoriesService,
|
||||
private readonly graysApi: GraysApi,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
create(@Body() data: CreateBidHistoryDto) {
|
||||
return this.bidHistoriesService.create(data);
|
||||
}
|
||||
|
||||
@Get('detail/:lot_id')
|
||||
async getBidHistories(@Param('lot_id') lot_id: Bid['lot_id']) {
|
||||
return await this.graysApi.getHistoriesBid(lot_id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export class BidHistoriesController {
|
|||
constructor(private readonly bidHistoriesService: BidHistoriesService) {}
|
||||
|
||||
@Post()
|
||||
create(@Body() data: CreateBidHistoryDto) {
|
||||
return this.bidHistoriesService.create(data);
|
||||
async create(@Body() data: CreateBidHistoryDto) {
|
||||
return await this.bidHistoriesService.create(data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,4 +15,8 @@ export class ClientUpdateBidDto {
|
|||
@IsNumber()
|
||||
@IsOptional()
|
||||
current_price: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
reserve_price: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,5 +20,5 @@ export class CreateBidDto {
|
|||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
step_price: number;
|
||||
plus_price: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,5 +11,5 @@ export class UpdateBidDto {
|
|||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
step_price: number;
|
||||
plus_price: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,10 @@ export class Bid extends Timestamp {
|
|||
lot_id: string;
|
||||
|
||||
@Column({ default: 0 })
|
||||
step_price: number;
|
||||
plus_price: number;
|
||||
|
||||
@Column({ default: 0 })
|
||||
reserve_price: number;
|
||||
|
||||
@Column({ default: null, nullable: true })
|
||||
name: string;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||
import { Timestamp } from './timestamp';
|
||||
|
||||
@Entity('out-bid-logs')
|
||||
@Unique(['model', 'out_price'])
|
||||
export class OutBidLog extends Timestamp {
|
||||
@PrimaryGeneratedColumn('increment')
|
||||
id: number;
|
||||
|
||||
@Column({ unique: true, default: null, nullable: true })
|
||||
@Column({ default: null, nullable: true })
|
||||
model: string | null;
|
||||
|
||||
@Column({ default: null, nullable: true })
|
||||
|
|
|
|||
|
|
@ -28,7 +28,11 @@ export class BidGateway implements OnGatewayConnection {
|
|||
|
||||
async onModuleInit() {
|
||||
this.eventEmitter.on('bids.updated', (data) => {
|
||||
this.server.emit('bidsUpdated', data); // Gửi sự kiện WebSocket đến client
|
||||
this.server.emit('bidsUpdated', data);
|
||||
});
|
||||
|
||||
this.eventEmitter.on('web.updated', (data) => {
|
||||
this.server.emit('webUpdated', data);
|
||||
});
|
||||
|
||||
this.eventEmitter.on('working', (data) => {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { Repository } from 'typeorm';
|
|||
import { BidHistory } from '../entities/bid-history.entity';
|
||||
import { Bid } from '../entities/bid.entity';
|
||||
import { CreateBidHistoryDto } from '../dto/bid-history/create-bid-history.dto';
|
||||
import { BotTelegramApi } from '../apis/bot-telegram.api';
|
||||
|
||||
@Injectable()
|
||||
export class BidHistoriesService {
|
||||
|
|
@ -19,6 +20,7 @@ export class BidHistoriesService {
|
|||
readonly bidHistoriesRepo: Repository<BidHistory>,
|
||||
@InjectRepository(Bid)
|
||||
readonly bidsRepo: Repository<Bid>,
|
||||
private readonly botTelegramApi: BotTelegramApi,
|
||||
) {}
|
||||
|
||||
async index() {
|
||||
|
|
@ -47,7 +49,7 @@ export class BidHistoriesService {
|
|||
);
|
||||
}
|
||||
|
||||
if (price + bid.step_price > bid.max_price) {
|
||||
if (price + bid.plus_price > bid.max_price) {
|
||||
this.bidsRepo.update(bid_id, { status: 'out-bid' });
|
||||
|
||||
throw new BadRequestException(
|
||||
|
|
@ -70,6 +72,8 @@ export class BidHistoriesService {
|
|||
this.bidsRepo.update(bid_id, { first_bid: false });
|
||||
}
|
||||
|
||||
this.botTelegramApi.sendBidInfo({ ...bid, histories: response });
|
||||
|
||||
return AppResponse.toResponse(plainToClass(BidHistory, response));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,10 +127,10 @@ export class BidsService {
|
|||
|
||||
const result = await this.bidsRepo.update(id, {
|
||||
...data,
|
||||
status:
|
||||
prev.max_price + prev.step_price > data.max_price
|
||||
? 'out-bid'
|
||||
: prev.status,
|
||||
// status:
|
||||
// prev.max_price + prev.plus_price > data.max_price
|
||||
// ? 'out-bid'
|
||||
// : prev.status,
|
||||
});
|
||||
|
||||
if (!result) throw new BadRequestException(false);
|
||||
|
|
@ -167,7 +167,7 @@ export class BidsService {
|
|||
},
|
||||
});
|
||||
|
||||
if (lastHistory && lastHistory.price + bid.step_price >= bid.max_price) {
|
||||
if (lastHistory && lastHistory.price + bid.plus_price >= bid.max_price) {
|
||||
throw new BadRequestException(
|
||||
AppResponse.toResponse(false, { message: 'Price is out of Max Price' }),
|
||||
);
|
||||
|
|
@ -175,7 +175,7 @@ export class BidsService {
|
|||
|
||||
if (bid.close_time && isTimeReached(bid.close_time)) {
|
||||
throw new BadRequestException(
|
||||
AppResponse.toResponse(false, { message: 'Close was closed' }),
|
||||
AppResponse.toResponse(false, { message: 'Product was closed' }),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -201,7 +201,7 @@ export class BidsService {
|
|||
bid.start_bid_time = subtractMinutes(data.close_time, 5);
|
||||
}
|
||||
|
||||
if (data.current_price > bid.max_price + bid.step_price) {
|
||||
if (data.current_price > bid.max_price + bid.plus_price) {
|
||||
bid.status = 'out-bid';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,13 +47,17 @@ export class OutBidLogsService {
|
|||
}
|
||||
|
||||
async create(data: CreateOutBidLogDto[]) {
|
||||
try {
|
||||
const result = await this.outbidLogRepo.upsert(data, {
|
||||
conflictPaths: ['model', 'id'],
|
||||
conflictPaths: ['model', 'lot_id'],
|
||||
skipUpdateIfNoValuesChanged: true,
|
||||
});
|
||||
|
||||
if (!result) throw new BadRequestException(false);
|
||||
|
||||
return AppResponse.toResponse(true);
|
||||
} catch (error) {
|
||||
throw new BadRequestException(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,12 @@ export class WebBidsService {
|
|||
this.eventEmitter.emit('bids.updated', data);
|
||||
}
|
||||
|
||||
async emitAccountUpdate(id: WebBid['id']) {
|
||||
const data = await this.webBidRepo.findOne({ where: { id } });
|
||||
|
||||
this.eventEmitter.emit('web.updated', data || null);
|
||||
}
|
||||
|
||||
async createByUrl(url: string) {
|
||||
const originUrl = extractDomain(url);
|
||||
|
||||
|
|
@ -131,6 +137,9 @@ export class WebBidsService {
|
|||
|
||||
if (!result) throw new BadRequestException(false);
|
||||
|
||||
if (data.password !== prev.password || data.username !== prev.username) {
|
||||
this.emitAccountUpdate(prev.id);
|
||||
}
|
||||
this.emitAllBidEvent();
|
||||
|
||||
return AppResponse.toResponse(true);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||
database: configService.get<string>('DB_NAME'),
|
||||
charset: 'utf8mb4_unicode_ci',
|
||||
entities: ['dist/**/*.entity{.ts,.js}'],
|
||||
synchronize: true,
|
||||
synchronize:
|
||||
configService.get<string>('ENVIRONMENT') === 'prod' ? false : true,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -24,3 +24,7 @@ export function extractDomain(url: string): string | null {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function escapeMarkdownV2(text: string) {
|
||||
return text.replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, '\\$&');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import 'dotenv/config';
|
||||
import _ from 'lodash';
|
||||
import { io } from 'socket.io-client';
|
||||
import { GrayApiBid } from './models/grays.com/grays-api-bid.js';
|
||||
import { GraysProductBid } from './models/grays.com/grays-product-bid.js';
|
||||
import { createApiBid, createBidProduct, deleteProfile } from './service/app-service.js';
|
||||
import browser from './system/browser.js';
|
||||
import configs from './system/config.js';
|
||||
import { isTimeReached, safeClosePage } from './system/utils.js';
|
||||
import browser from './system/browser.js';
|
||||
|
||||
let MANAGER_BIDS = [];
|
||||
|
||||
|
|
@ -12,31 +12,7 @@ let _INTERVAL_TRACKING_ID = null;
|
|||
let _CLEAR_LAZY_TAB_ID = null;
|
||||
let _WORK_TRACKING_ID = null;
|
||||
|
||||
const handleCloseRemoveProduct = (data) => {
|
||||
if (!Array.isArray(data)) return;
|
||||
|
||||
data.forEach(async (item) => {
|
||||
if (item.page_context) {
|
||||
safeClosePage(item);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const createBidProduct = (web, data) => {
|
||||
switch (web.origin_url) {
|
||||
case configs.WEB_URLS.GRAYS: {
|
||||
return new GraysProductBid({ ...data });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createApiBid = (web) => {
|
||||
switch (web.origin_url) {
|
||||
case configs.WEB_URLS.GRAYS: {
|
||||
return new GrayApiBid({ ...web });
|
||||
}
|
||||
}
|
||||
};
|
||||
global.IS_CLEANING = false;
|
||||
|
||||
const handleUpdateProductTabs = (data) => {
|
||||
if (!Array.isArray(data)) {
|
||||
|
|
@ -75,6 +51,7 @@ const handleUpdateProductTabs = (data) => {
|
|||
const tracking = async () => {
|
||||
if (_INTERVAL_TRACKING_ID) {
|
||||
clearInterval(_INTERVAL_TRACKING_ID);
|
||||
_INTERVAL_TRACKING_ID = null;
|
||||
}
|
||||
|
||||
_INTERVAL_TRACKING_ID = setInterval(async () => {
|
||||
|
|
@ -121,42 +98,129 @@ const tracking = async () => {
|
|||
}, configs.AUTO_TRACKING_DELAY);
|
||||
};
|
||||
|
||||
// const clearLazyTab = async () => {
|
||||
// if (_CLEAR_LAZY_TAB_ID) {
|
||||
// clearInterval(_CLEAR_LAZY_TAB_ID);
|
||||
// _CLEAR_LAZY_TAB_ID = null; // Reset tránh memory leak
|
||||
// }
|
||||
|
||||
// try {
|
||||
// _CLEAR_LAZY_TAB_ID = setInterval(async () => {
|
||||
// if (!browser) {
|
||||
// console.warn('⚠️ Browser is not available.');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// try {
|
||||
// const pages = await browser.pages();
|
||||
|
||||
// // Lấy danh sách URL từ flattenedArray
|
||||
// const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [
|
||||
// // item.url,
|
||||
// ...item.children.map((child) => child.url),
|
||||
// ]).filter(Boolean); // Lọc bỏ null hoặc undefined
|
||||
|
||||
// console.log(
|
||||
// '🔍 Page URLs:',
|
||||
// pages.map((page) => page.url()),
|
||||
// );
|
||||
|
||||
// for (const page of pages) {
|
||||
// const pageUrl = page.url();
|
||||
|
||||
// if (!activeUrls.includes(pageUrl)) {
|
||||
// if (!page.isClosed()) {
|
||||
// try {
|
||||
// await page.close();
|
||||
// console.log(`🛑 Closing unused tab: ${pageUrl}`);
|
||||
// } catch (err) {
|
||||
// console.warn(`⚠️ Error closing tab ${pageUrl}:`, err.message);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } catch (err) {
|
||||
// console.error('❌ Error in clearLazyTab:', err.message);
|
||||
// }
|
||||
// }, configs.AUTO_TRACKING_CLEANING);
|
||||
// } catch (error) {
|
||||
// console.log('CLEAR LAZY TAB: ', error.message);
|
||||
// }
|
||||
// };
|
||||
|
||||
const clearLazyTab = async () => {
|
||||
if (_CLEAR_LAZY_TAB_ID) {
|
||||
clearInterval(_CLEAR_LAZY_TAB_ID);
|
||||
_CLEAR_LAZY_TAB_ID = null;
|
||||
}
|
||||
|
||||
try {
|
||||
_CLEAR_LAZY_TAB_ID = setInterval(async () => {
|
||||
if (!global.IS_CLEANING) return;
|
||||
|
||||
if (!browser) {
|
||||
console.warn('⚠️ Browser is not available or disconnected.');
|
||||
clearInterval(_CLEAR_LAZY_TAB_ID);
|
||||
_CLEAR_LAZY_TAB_ID = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const pages = await browser.pages();
|
||||
|
||||
// Lấy danh sách URL từ flattenedArray
|
||||
const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [item.url, ...item.children.map((child) => child.url)]).filter(Boolean); // Lọc bỏ null hoặc undefined
|
||||
|
||||
console.log({ activeUrls });
|
||||
console.log(
|
||||
'🔍 Page URLs:',
|
||||
pages.map((page) => page.url()),
|
||||
);
|
||||
|
||||
for (const page of pages) {
|
||||
const pageUrl = page.url();
|
||||
|
||||
// 🔥 Bỏ qua tab 'about:blank' hoặc tab không có URL
|
||||
if (!pageUrl || pageUrl === 'about:blank') continue;
|
||||
|
||||
if (!activeUrls.includes(pageUrl)) {
|
||||
console.log(`Closing unused tab: ${pageUrl}`);
|
||||
if (!page.isClosed() && browser.isConnected()) {
|
||||
try {
|
||||
await page.close();
|
||||
console.log(`🛑 Closing unused tab: ${pageUrl}`);
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ Error closing tab ${pageUrl}:`, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ Error in clearLazyTab:', err.message);
|
||||
}
|
||||
}, configs.AUTO_TRACKING_CLEANING);
|
||||
} catch (error) {
|
||||
console.log('CLEAR LAZY TAB ERROR: ', error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const workTracking = () => {
|
||||
try {
|
||||
if (_WORK_TRACKING_ID) {
|
||||
clearInterval(_WORK_TRACKING_ID);
|
||||
_WORK_TRACKING_ID = null;
|
||||
}
|
||||
|
||||
_WORK_TRACKING_ID = setInterval(() => {
|
||||
const activeData = _.flatMap(MANAGER_BIDS, (item) => [item, ...item.children]);
|
||||
|
||||
for (const item of activeData) {
|
||||
if (item.page_context && !item.page_context.isClosed()) {
|
||||
item.handleTakeWorkSnapshot();
|
||||
}
|
||||
}
|
||||
}, 10000);
|
||||
} catch (error) {
|
||||
console.log('Loi oi day');
|
||||
}
|
||||
};
|
||||
|
||||
(async () => {
|
||||
|
|
@ -180,6 +244,26 @@ const workTracking = () => {
|
|||
await Promise.all(MANAGER_BIDS.map((apiBid) => apiBid.listen_events()));
|
||||
});
|
||||
|
||||
socket.on('webUpdated', async (data) => {
|
||||
console.log('📢 Account was updated:', data);
|
||||
|
||||
const isDeleted = deleteProfile(data);
|
||||
|
||||
if (isDeleted) {
|
||||
console.log('✅ Profile deleted successfully!');
|
||||
|
||||
const tabs = MANAGER_BIDS.filter((item) => item.url === data.url || item?.web_bid.url === data.url);
|
||||
|
||||
if (tabs.length <= 0) return;
|
||||
|
||||
await Promise.all(tabs.map((tab) => safeClosePage(tab)));
|
||||
|
||||
await Promise.all(MANAGER_BIDS.map((apiBid) => apiBid.listen_events()));
|
||||
} else {
|
||||
console.log('⚠️ No profile found to delete.');
|
||||
}
|
||||
});
|
||||
|
||||
// AUTO TRACKING
|
||||
tracking();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import * as fs from 'fs';
|
||||
import path from 'path';
|
||||
import BID_TYPE from '../system/bid-type.js';
|
||||
import browser from '../system/browser.js';
|
||||
import CONSTANTS from '../system/constants.js';
|
||||
import { sanitizeFileName } from '../system/utils.js';
|
||||
import { Account } from './account.js';
|
||||
import { getPathProfile, sanitizeFileName } from '../system/utils.js';
|
||||
import { Bid } from './bid.js';
|
||||
import * as fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export class ApiBid extends Bid {
|
||||
id;
|
||||
|
|
@ -93,7 +92,7 @@ export class ApiBid extends Bid {
|
|||
async restoreContext() {
|
||||
if (!this.browser_context || !this.page_context) return;
|
||||
|
||||
const filePath = path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(this.origin_url) + '.json');
|
||||
const filePath = getPathProfile(this.origin_url);
|
||||
|
||||
if (!fs.existsSync(filePath)) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import path from 'path';
|
||||
import { createOutBidLog } from '../../system/apis/out-bid-log.js';
|
||||
import configs from '../../system/config.js';
|
||||
import CONSTANTS from '../../system/constants.js';
|
||||
import { delay, extractNumber, isTimeReached, safeClosePage, takeSnapshot } from '../../system/utils.js';
|
||||
import { delay, extractNumber, getPathProfile, isTimeReached, safeClosePage } from '../../system/utils.js';
|
||||
import { ApiBid } from '../api-bid.js';
|
||||
import * as fs from 'fs';
|
||||
import fs from 'fs';
|
||||
|
||||
export class GrayApiBid extends ApiBid {
|
||||
retry_login = 0;
|
||||
|
|
@ -129,20 +129,25 @@ export class GrayApiBid extends ApiBid {
|
|||
async handleLogin() {
|
||||
const page = this.page_context;
|
||||
|
||||
const filePath = getPathProfile(this.origin_url);
|
||||
|
||||
// 🔍 Check if already logged in (login input should not be visible)
|
||||
if (!(await page.$('input[name="username"]'))) {
|
||||
if (!(await page.$('input[name="username"]')) || fs.existsSync(filePath)) {
|
||||
console.log('✅ Already logged in, skipping login.');
|
||||
global.IS_CLEANING = true;
|
||||
|
||||
this.retry_login = 0; // Reset retry count
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔑 Starting login process...');
|
||||
global.IS_CLEANING = false;
|
||||
|
||||
try {
|
||||
await page.type('input[name="username"]', this.username, { delay: 100 });
|
||||
await page.type('input[name="password"]', this.password, { delay: 150 });
|
||||
await page.click('#loginButton');
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
page.waitForNavigation({ timeout: 8000, waitUntil: 'domcontentloaded' }),
|
||||
page.waitForFunction(() => !document.querySelector('input[name="username"]'), { timeout: 8000 }), // Check if login input disappears
|
||||
|
|
@ -151,6 +156,7 @@ export class GrayApiBid extends ApiBid {
|
|||
if (!(await page.$('input[name="username"]'))) {
|
||||
console.log('✅ Login successful!');
|
||||
this.retry_login = 0; // Reset retry count after success
|
||||
global.IS_CLEANING = true;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export class GraysProductBid extends ProductBid {
|
|||
return { result: false, bid_price: 0 };
|
||||
}
|
||||
|
||||
const bid_price = this.step_price + Number(price_value);
|
||||
const bid_price = this.plus_price + Number(price_value);
|
||||
|
||||
if (bid_price > this.max_price) {
|
||||
console.log('PRICE BID is more than MAX_VALUE => STOP BID THIS PRODUCT ❌');
|
||||
|
|
@ -141,10 +141,10 @@ export class GraysProductBid extends ProductBid {
|
|||
await delay(1000);
|
||||
}
|
||||
|
||||
async handleUpdateBid({ lot_id, close_time, name, current_price }) {
|
||||
async handleUpdateBid({ lot_id, close_time, name, current_price, reserve_price }) {
|
||||
if (close_time && this.close_time == close_time) return;
|
||||
|
||||
const response = await updateBid(this.id, { lot_id, close_time, name, current_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;
|
||||
|
|
@ -181,9 +181,9 @@ export class GraysProductBid extends ProductBid {
|
|||
const current_price =
|
||||
(await page.$eval('#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span', (el) => el.innerText)) || null;
|
||||
|
||||
console.log(`📌 Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}`);
|
||||
console.log(`📌 Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price ${price_value}`);
|
||||
|
||||
this.handleUpdateBid({ lot_id, close_time, name, current_price: current_price ? extractNumber(current_price) : null });
|
||||
this.handleUpdateBid({ lot_id, reserve_price: price_value, close_time, name, current_price: current_price ? extractNumber(current_price) : null });
|
||||
|
||||
const { result, bid_price } = await this.validate({ page, price_value });
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import path from 'path';
|
|||
import BID_TYPE from '../system/bid-type.js';
|
||||
import browser from '../system/browser.js';
|
||||
import CONSTANTS from '../system/constants.js';
|
||||
import { delay, sanitizeFileName } from '../system/utils.js';
|
||||
import { delay, getPathProfile, sanitizeFileName } from '../system/utils.js';
|
||||
import { Bid } from './bid.js';
|
||||
|
||||
export class ProductBid extends Bid {
|
||||
|
|
@ -11,7 +11,7 @@ export class ProductBid extends Bid {
|
|||
max_price;
|
||||
model;
|
||||
lot_id;
|
||||
step_price;
|
||||
plus_price;
|
||||
close_time;
|
||||
first_bid;
|
||||
quantity;
|
||||
|
|
@ -23,11 +23,12 @@ export class ProductBid extends Bid {
|
|||
web_bid;
|
||||
current_price;
|
||||
name;
|
||||
reserve_price;
|
||||
|
||||
constructor({
|
||||
url,
|
||||
max_price,
|
||||
step_price,
|
||||
plus_price,
|
||||
model,
|
||||
first_bid = false,
|
||||
id,
|
||||
|
|
@ -40,12 +41,13 @@ export class ProductBid extends Bid {
|
|||
start_bid_time,
|
||||
web_bid,
|
||||
current_price,
|
||||
reserve_price,
|
||||
name,
|
||||
}) {
|
||||
super(BID_TYPE.PRODUCT_TAB, url);
|
||||
this.max_price = max_price || 0;
|
||||
this.model = model;
|
||||
this.step_price = step_price || 0;
|
||||
this.plus_price = plus_price || 0;
|
||||
this.first_bid = first_bid;
|
||||
this.id = id;
|
||||
this.created_at = created_at;
|
||||
|
|
@ -58,12 +60,13 @@ export class ProductBid extends Bid {
|
|||
this.web_bid = web_bid;
|
||||
this.current_price = current_price;
|
||||
this.name = name;
|
||||
this.reserve_price = reserve_price;
|
||||
}
|
||||
|
||||
setNewData({
|
||||
url,
|
||||
max_price,
|
||||
step_price,
|
||||
plus_price,
|
||||
model,
|
||||
first_bid = false,
|
||||
id,
|
||||
|
|
@ -76,11 +79,12 @@ export class ProductBid extends Bid {
|
|||
start_bid_time,
|
||||
web_bid,
|
||||
current_price,
|
||||
reserve_price,
|
||||
name,
|
||||
}) {
|
||||
this.max_price = max_price || 0;
|
||||
this.model = model;
|
||||
this.step_price = step_price || 0;
|
||||
this.plus_price = plus_price || 0;
|
||||
this.first_bid = first_bid;
|
||||
this.id = id;
|
||||
this.created_at = created_at;
|
||||
|
|
@ -94,6 +98,7 @@ export class ProductBid extends Bid {
|
|||
this.url = url;
|
||||
this.current_price = current_price;
|
||||
this.name = name;
|
||||
this.reserve_price = reserve_price;
|
||||
}
|
||||
|
||||
puppeteer_connect = async () => {
|
||||
|
|
@ -107,7 +112,7 @@ export class ProductBid extends Bid {
|
|||
const statusInit = await this.restoreContext(context);
|
||||
|
||||
if (!statusInit) {
|
||||
console.log(`⚠️ Restore failed. Restart count`);
|
||||
console.log(`⚠️ Restore failed.`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -117,7 +122,7 @@ export class ProductBid extends Bid {
|
|||
};
|
||||
|
||||
async restoreContext(context) {
|
||||
const filePath = path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(this.web_bid.origin_url) + '.json');
|
||||
const filePath = getPathProfile(this.web_bid.origin_url);
|
||||
|
||||
if (!fs.existsSync(filePath)) return false;
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.2",
|
||||
"dotenv": "^16.4.7",
|
||||
"lodash": "^4.17.21",
|
||||
"puppeteer": "^24.4.0",
|
||||
"puppeteer-extra": "^3.3.6",
|
||||
|
|
@ -480,6 +481,18 @@
|
|||
"integrity": "sha512-yRtvFD8Oyk7C9Os3GmnFZLu53yAfsnyw1s+mLmHHUK0GQEc9zthHWvS1r67Zqzm5t7v56PILHIVZ7kmFMaL2yQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.7",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"description": "",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.2",
|
||||
"dotenv": "^16.4.7",
|
||||
"lodash": "^4.17.21",
|
||||
"puppeteer": "^24.4.0",
|
||||
"puppeteer-extra": "^3.3.6",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import path from 'path';
|
||||
import { GrayApiBid } from '../models/grays.com/grays-api-bid.js';
|
||||
import { GraysProductBid } from '../models/grays.com/grays-product-bid.js';
|
||||
import configs from '../system/config.js';
|
||||
import CONSTANTS from '../system/constants.js';
|
||||
import { sanitizeFileName } from '../system/utils.js';
|
||||
import * as fs from 'fs';
|
||||
export const handleCloseRemoveProduct = (data) => {
|
||||
if (!Array.isArray(data)) return;
|
||||
|
||||
data.forEach(async (item) => {
|
||||
if (item.page_context) {
|
||||
safeClosePage(item);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const createBidProduct = (web, data) => {
|
||||
switch (web.origin_url) {
|
||||
case configs.WEB_URLS.GRAYS: {
|
||||
return new GraysProductBid({ ...data });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const createApiBid = (web) => {
|
||||
switch (web.origin_url) {
|
||||
case configs.WEB_URLS.GRAYS: {
|
||||
return new GrayApiBid({ ...web });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteProfile = (data) => {
|
||||
const filePath = path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(data.origin_url) + '.json');
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
|
@ -28,13 +28,13 @@ export const updateBid = async (id, values) => {
|
|||
});
|
||||
|
||||
if (!data || !data?.data) {
|
||||
console.log('❌ UPDATE FAILURE');
|
||||
console.log('❌ UPDATE FAILURE (UPDATE BID)');
|
||||
return null;
|
||||
}
|
||||
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
console.log('❌ ERROR IN SERVER: ', error);
|
||||
console.log('❌ ERROR IN SERVER: (UPDATE BID) ', error.response);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import ax from 'axios';
|
||||
|
||||
const axios = ax.create({
|
||||
baseURL: 'http://localhost:4000/api/v1/',
|
||||
// baseURL: 'http://172.18.2.125/api/v1/',
|
||||
baseURL: process.env.BASE_URL,
|
||||
});
|
||||
|
||||
export default axios;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
|||
|
||||
puppeteer.use(StealthPlugin());
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false, // Để false để quan sát quá trình login
|
||||
headless: process.env.ENVIRONMENT === 'prod' ? true : false,
|
||||
// userDataDir: CONSTANTS.PROFILE_PATH, // Thư mục lưu profile
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
const configs = {
|
||||
AUTO_TRACKING_DELAY: 5000,
|
||||
AUTO_TRACKING_CLEANING: 10000,
|
||||
SOCKET_URL: 'http://localhost:4000',
|
||||
SOCKET_URL: process.env.SOCKET_URL,
|
||||
WEB_URLS: {
|
||||
GRAYS: `https://www.grays.com`,
|
||||
},
|
||||
WEB_CONFIGS: {
|
||||
GRAYS: {
|
||||
AUTO_CALL_API_TO_TRACKING: 10000,
|
||||
AUTO_CALL_API_TO_TRACKING: 3000,
|
||||
API_CALL_TO_TRACKING: 'https://www.grays.com/api/Notifications/GetOutBidLots',
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_
|
|||
console.log(`📂 Save at folder: ${typeDir}`);
|
||||
}
|
||||
|
||||
await page.waitForSelector('body', { visible: true });
|
||||
await page.waitForSelector('body', { visible: true, timeout: 5000 });
|
||||
|
||||
// Chụp ảnh màn hình và lưu vào filePath
|
||||
await page.screenshot({ path: filePath, fullPage: true });
|
||||
|
|
@ -42,6 +42,7 @@ export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_
|
|||
export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
export async function safeClosePage(item) {
|
||||
try {
|
||||
const page = item.page_context;
|
||||
|
||||
if (!page?.isClosed() && page?.close) {
|
||||
|
|
@ -49,6 +50,12 @@ export async function safeClosePage(item) {
|
|||
}
|
||||
|
||||
item.page_context = undefined;
|
||||
if (item?.page_context) {
|
||||
item.page_context = undefined;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Can't close item: " + item.id);
|
||||
}
|
||||
}
|
||||
|
||||
export function isTimeReached(targetTime) {
|
||||
|
|
@ -68,3 +75,7 @@ export function extractNumber(str) {
|
|||
export const sanitizeFileName = (url) => {
|
||||
return url.replace(/[:\/]/g, '_');
|
||||
};
|
||||
|
||||
export const getPathProfile = (origin_url) => {
|
||||
return path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(origin_url) + '.json');
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue