update
This commit is contained in:
parent
fff5447ce1
commit
b1e34be9ce
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { generateNestParams, handleError, handleSuccess } from '.';
|
||||||
|
import axios from '../lib/axios';
|
||||||
|
|
||||||
|
const BASE_URL = 'send-message-histories';
|
||||||
|
|
||||||
|
export const getSendMessageHistories = async (params: Record<string, string | number>) => {
|
||||||
|
return await axios({
|
||||||
|
url: BASE_URL,
|
||||||
|
params: generateNestParams(params),
|
||||||
|
withCredentials: true,
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendMessageHistoryTest = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios({
|
||||||
|
url: `${BASE_URL}/send-test`,
|
||||||
|
withCredentials: true,
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
|
||||||
|
handleSuccess(data);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { ActionIcon, Badge, Box, Menu, Text, Tooltip } from '@mantine/core';
|
import { ActionIcon, Badge, Box, Menu, Text, Tooltip } from '@mantine/core';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { IconAd, IconAdOff, IconEdit, IconHistory, IconMenu, IconTrash } from '@tabler/icons-react';
|
import { IconAd, IconAdOff, IconEdit, IconHammer, IconHistory, IconMenu, IconTrash } from '@tabler/icons-react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useMemo, useRef, useState } from 'react';
|
import { useMemo, useRef, useState } from 'react';
|
||||||
import { deleteBid, deletesBid, getBids, toggleBid } from '../apis/bid';
|
import { deleteBid, deletesBid, getBids, toggleBid } from '../apis/bid';
|
||||||
|
|
@ -39,11 +39,7 @@ export default function Bids() {
|
||||||
title: 'Model',
|
title: 'Model',
|
||||||
typeFilter: 'text',
|
typeFilter: 'text',
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// key: 'quantity',
|
|
||||||
// title: 'Qty',
|
|
||||||
// typeFilter: 'number',
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
key: 'plus_price',
|
key: 'plus_price',
|
||||||
title: 'Plus price',
|
title: 'Plus price',
|
||||||
|
|
@ -253,7 +249,7 @@ export default function Bids() {
|
||||||
setClickData(row);
|
setClickData(row);
|
||||||
historiesGraysApiModel.open();
|
historiesGraysApiModel.open();
|
||||||
}}
|
}}
|
||||||
leftSection={<IconHistory size={14} />}
|
leftSection={<IconHammer size={14} />}
|
||||||
>
|
>
|
||||||
Bids
|
Bids
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Box, Title } from '@mantine/core';
|
import { Box, Text, Title } from '@mantine/core';
|
||||||
import io from 'socket.io-client';
|
import io from 'socket.io-client';
|
||||||
import { WorkingPage } from '../components/dashboard';
|
import { WorkingPage } from '../components/dashboard';
|
||||||
import { IBid, IWebBid } from '../system/type';
|
import { IBid, IWebBid } from '../system/type';
|
||||||
|
|
@ -71,9 +71,13 @@ export default function DashBoard() {
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<Box className="grid grid-cols-4 gap-4">
|
<Box className="grid grid-cols-4 gap-4">
|
||||||
{workingData.map((item, index) => (
|
{workingData.length > 0 && workingData.map((item, index) => <WorkingPage socket={socket} data={item} key={item.id + index} />)}
|
||||||
<WorkingPage socket={socket} data={item} key={item.id + index} />
|
|
||||||
))}
|
{workingData.length <= 0 && (
|
||||||
|
<Box className="flex items-center justify-center col-span-4">
|
||||||
|
<Text>No Pages</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { Box, Text } from '@mantine/core';
|
||||||
|
import { useMemo, useRef } from 'react';
|
||||||
|
import { getSendMessageHistories, sendMessageHistoryTest } from '../apis/send-message-histories';
|
||||||
|
import Table from '../lib/table/table';
|
||||||
|
import { IColumn, TRefTableFn } from '../lib/table/type';
|
||||||
|
import { ISendMessageHistory } from '../system/type';
|
||||||
|
import { formatTime } from '../utils';
|
||||||
|
|
||||||
|
export default function SendMessageHistories() {
|
||||||
|
const refTableFn: TRefTableFn<ISendMessageHistory> = useRef({});
|
||||||
|
|
||||||
|
const columns: IColumn<ISendMessageHistory>[] = [
|
||||||
|
{
|
||||||
|
key: 'id',
|
||||||
|
title: 'ID',
|
||||||
|
typeFilter: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'bid',
|
||||||
|
title: 'Product name',
|
||||||
|
typeFilter: 'none',
|
||||||
|
renderRow(row) {
|
||||||
|
return <Text>{row.bid?.name || 'None'}</Text>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'message',
|
||||||
|
title: 'Message',
|
||||||
|
typeFilter: 'text',
|
||||||
|
renderRow(row) {
|
||||||
|
return <Box className="max-w-[500px]" dangerouslySetInnerHTML={{ __html: row.message }}></Box>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'created_at',
|
||||||
|
title: 'Create at',
|
||||||
|
typeFilter: 'none',
|
||||||
|
renderRow(row) {
|
||||||
|
return <span className="text-sm">{formatTime(row.created_at)}</span>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const table = useMemo(() => {
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
actionsOptions={{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
key: 'send-test',
|
||||||
|
title: 'Send test',
|
||||||
|
callback: async () => {
|
||||||
|
await sendMessageHistoryTest();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
refTableFn={refTableFn}
|
||||||
|
striped
|
||||||
|
showLoading={true}
|
||||||
|
highlightOnHover
|
||||||
|
styleDefaultHead={{
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
width: 'fit-content',
|
||||||
|
}}
|
||||||
|
options={{
|
||||||
|
query: getSendMessageHistories,
|
||||||
|
pathToData: 'data.data',
|
||||||
|
keyOptions: {
|
||||||
|
last_page: 'lastPage',
|
||||||
|
per_page: 'perPage',
|
||||||
|
from: 'from',
|
||||||
|
to: 'to',
|
||||||
|
total: 'total',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
rows={[]}
|
||||||
|
withColumnBorders
|
||||||
|
showChooses={true}
|
||||||
|
withTableBorder
|
||||||
|
columns={columns}
|
||||||
|
rowKey="id"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <Box>{table}</Box>;
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
import { IconHammer, IconHome2, IconOutlet, IconPageBreak } from '@tabler/icons-react';
|
import { IconHammer, IconHome2, IconMessage, IconOutlet, IconPageBreak } from '@tabler/icons-react';
|
||||||
import { Bids, Dashboard, OutBidsLog } from '../pages';
|
import { Bids, Dashboard, OutBidsLog } from '../pages';
|
||||||
import WebBids from '../pages/web-bids';
|
import WebBids from '../pages/web-bids';
|
||||||
|
import SendMessageHistories from '../pages/send-message-histories';
|
||||||
export default class Links {
|
export default class Links {
|
||||||
public static DASHBOARD = '/dashboard';
|
public static DASHBOARD = '/dashboard';
|
||||||
public static BIDS = '/bids';
|
public static BIDS = '/bids';
|
||||||
public static WEBS = '/webs';
|
public static WEBS = '/webs';
|
||||||
public static OUT_BIDS_LOG = '/out-bids-log';
|
public static OUT_BIDS_LOG = '/out-bids-log';
|
||||||
|
public static SEND_MESSAGE_HISTORIES = '/send-message-histories';
|
||||||
|
|
||||||
public static HOME = '/';
|
public static HOME = '/';
|
||||||
public static LOGIN = '/login';
|
public static LOGIN = '/login';
|
||||||
|
|
@ -35,5 +37,11 @@ export default class Links {
|
||||||
icon: IconOutlet,
|
icon: IconOutlet,
|
||||||
element: OutBidsLog,
|
element: OutBidsLog,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: this.SEND_MESSAGE_HISTORIES,
|
||||||
|
title: 'Send message histories',
|
||||||
|
icon: IconMessage,
|
||||||
|
element: SendMessageHistories,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,3 +61,8 @@ export interface IPermission extends ITimestamp {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
export interface ISendMessageHistory extends ITimestamp {
|
||||||
|
id: number;
|
||||||
|
message: string;
|
||||||
|
bid: IBid;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import axios from 'axios';
|
||||||
import { escapeMarkdownV2 } from 'src/ultils';
|
import { escapeMarkdownV2 } from 'src/ultils';
|
||||||
import { Bid } from '../entities/bid.entity';
|
import { Bid } from '../entities/bid.entity';
|
||||||
import * as dayjs from 'dayjs';
|
import * as dayjs from 'dayjs';
|
||||||
|
import { SendMessageHistoriesService } from '../services/send-message-histories.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BotTelegramApi {
|
export class BotTelegramApi {
|
||||||
|
|
@ -63,12 +64,18 @@ export class BotTelegramApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendBidInfo(bid: Bid): Promise<void> {
|
async sendBidInfo(bid: Bid): Promise<boolean> {
|
||||||
const text = this.formatBidMessage(bid);
|
try {
|
||||||
|
const text = this.formatBidMessage(bid);
|
||||||
|
|
||||||
console.log(text);
|
await this.sendMessage(text, {
|
||||||
await this.sendMessage(text, {
|
parse_mode: 'HTML',
|
||||||
parse_mode: 'HTML',
|
});
|
||||||
});
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.log('SEND MESSAGE FAILURE');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,19 @@ import { OutBidLogsService } from './services/out-bid-logs.service';
|
||||||
import { WebBidsService } from './services/web-bids.service';
|
import { WebBidsService } from './services/web-bids.service';
|
||||||
import { BotTelegramApi } from './apis/bot-telegram.api';
|
import { BotTelegramApi } from './apis/bot-telegram.api';
|
||||||
import { GraysApi } from './apis/grays.api';
|
import { GraysApi } from './apis/grays.api';
|
||||||
|
import { SendMessageHistory } from './entities/send-message-histories.entity';
|
||||||
|
import { SendMessageHistoriesService } from './services/send-message-histories.service';
|
||||||
|
import { AdminSendMessageHistoriesController } from './controllers/admin/admin-send-message-histories.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Bid, BidHistory, OutBidLog, WebBid]),
|
TypeOrmModule.forFeature([
|
||||||
|
Bid,
|
||||||
|
BidHistory,
|
||||||
|
OutBidLog,
|
||||||
|
WebBid,
|
||||||
|
SendMessageHistory,
|
||||||
|
]),
|
||||||
EventEmitterModule.forRoot({
|
EventEmitterModule.forRoot({
|
||||||
wildcard: true,
|
wildcard: true,
|
||||||
}),
|
}),
|
||||||
|
|
@ -35,6 +44,7 @@ import { GraysApi } from './apis/grays.api';
|
||||||
AdminBidsController,
|
AdminBidsController,
|
||||||
AdminOutBidLogsController,
|
AdminOutBidLogsController,
|
||||||
AdminWebBidsController,
|
AdminWebBidsController,
|
||||||
|
AdminSendMessageHistoriesController,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
BidsService,
|
BidsService,
|
||||||
|
|
@ -44,6 +54,7 @@ import { GraysApi } from './apis/grays.api';
|
||||||
WebBidsService,
|
WebBidsService,
|
||||||
BotTelegramApi,
|
BotTelegramApi,
|
||||||
GraysApi,
|
GraysApi,
|
||||||
|
SendMessageHistoriesService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class BidsModule {}
|
export class BidsModule {}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Controller, Get, Post } from '@nestjs/common';
|
||||||
|
import { Paginate, PaginateQuery } from 'nestjs-paginate';
|
||||||
|
import { SendMessageHistoriesService } from '../../services/send-message-histories.service';
|
||||||
|
|
||||||
|
@Controller('admin/send-message-histories')
|
||||||
|
export class AdminSendMessageHistoriesController {
|
||||||
|
constructor(
|
||||||
|
private readonly sendMessageService: SendMessageHistoriesService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async index(@Paginate() query: PaginateQuery) {
|
||||||
|
return await this.sendMessageService.index(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('send-test')
|
||||||
|
async sendTest() {
|
||||||
|
return await this.sendMessageService.sendTestMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
import { Timestamp } from './timestamp';
|
import { Timestamp } from './timestamp';
|
||||||
import { BidHistory } from './bid-history.entity';
|
import { BidHistory } from './bid-history.entity';
|
||||||
import { WebBid } from './wed-bid.entity';
|
import { WebBid } from './wed-bid.entity';
|
||||||
|
import { SendMessageHistory } from './send-message-histories.entity';
|
||||||
|
|
||||||
@Entity('bids')
|
@Entity('bids')
|
||||||
export class Bid extends Timestamp {
|
export class Bid extends Timestamp {
|
||||||
|
|
@ -58,6 +59,11 @@ export class Bid extends Timestamp {
|
||||||
})
|
})
|
||||||
histories: BidHistory[];
|
histories: BidHistory[];
|
||||||
|
|
||||||
|
@OneToMany(() => SendMessageHistory, (sendMessage) => sendMessage.bid, {
|
||||||
|
cascade: true,
|
||||||
|
})
|
||||||
|
sendMessageHistories: SendMessageHistory[];
|
||||||
|
|
||||||
@ManyToOne(() => WebBid, (web) => web.children, { onDelete: 'CASCADE' })
|
@ManyToOne(() => WebBid, (web) => web.children, { onDelete: 'CASCADE' })
|
||||||
web_bid: WebBid;
|
web_bid: WebBid;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { Timestamp } from './timestamp';
|
||||||
|
import { Bid } from './bid.entity';
|
||||||
|
|
||||||
|
@Entity('send_message_histories')
|
||||||
|
export class SendMessageHistory extends Timestamp {
|
||||||
|
@PrimaryGeneratedColumn('increment')
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ default: null, nullable: true, type: 'text' })
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Bid, (bid) => bid.sendMessageHistories, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
bid: Bid;
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ import { BidHistory } from '../entities/bid-history.entity';
|
||||||
import { Bid } from '../entities/bid.entity';
|
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';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BidHistoriesService {
|
export class BidHistoriesService {
|
||||||
|
|
@ -21,6 +22,7 @@ export class BidHistoriesService {
|
||||||
@InjectRepository(Bid)
|
@InjectRepository(Bid)
|
||||||
readonly bidsRepo: Repository<Bid>,
|
readonly bidsRepo: Repository<Bid>,
|
||||||
private readonly botTelegramApi: BotTelegramApi,
|
private readonly botTelegramApi: BotTelegramApi,
|
||||||
|
readonly sendMessageHistoriesService: SendMessageHistoriesService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async index() {
|
async index() {
|
||||||
|
|
@ -72,7 +74,14 @@ export class BidHistoriesService {
|
||||||
this.bidsRepo.update(bid_id, { first_bid: false });
|
this.bidsRepo.update(bid_id, { first_bid: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.botTelegramApi.sendBidInfo({ ...bid, histories: response });
|
const botData = { ...bid, histories: response };
|
||||||
|
|
||||||
|
this.botTelegramApi.sendBidInfo(botData);
|
||||||
|
|
||||||
|
this.sendMessageHistoriesService.sendMessageRepo.save({
|
||||||
|
message: this.botTelegramApi.formatBidMessage(botData),
|
||||||
|
bid,
|
||||||
|
});
|
||||||
|
|
||||||
return AppResponse.toResponse(plainToClass(BidHistory, response));
|
return AppResponse.toResponse(plainToClass(BidHistory, response));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -205,9 +205,12 @@ export class BidsService {
|
||||||
bid.status = 'out-bid';
|
bid.status = 'out-bid';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Update ' + id);
|
||||||
|
|
||||||
const result = await this.bidsRepo.save({
|
const result = await this.bidsRepo.save({
|
||||||
...bid,
|
...bid,
|
||||||
...data,
|
...data,
|
||||||
|
updated_at: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.emitAllBidEvent();
|
this.emitAllBidEvent();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { OutBidLog } from '../entities/out-bid-log.entity';
|
||||||
|
import AppResponse from 'src/response/app-response';
|
||||||
|
import {
|
||||||
|
FilterOperator,
|
||||||
|
FilterSuffix,
|
||||||
|
paginate,
|
||||||
|
PaginateQuery,
|
||||||
|
} from 'nestjs-paginate';
|
||||||
|
import { Column } from 'nestjs-paginate/lib/helper';
|
||||||
|
import { CreateOutBidLogDto } from '../dto/out-bid-log/create-out-bid-log.dto';
|
||||||
|
import { SendMessageHistory } from '../entities/send-message-histories.entity';
|
||||||
|
import { BotTelegramApi } from '../apis/bot-telegram.api';
|
||||||
|
import { BidsService } from './bids.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SendMessageHistoriesService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(SendMessageHistory)
|
||||||
|
readonly sendMessageRepo: Repository<SendMessageHistory>,
|
||||||
|
private readonly botTelegramApi: BotTelegramApi,
|
||||||
|
private readonly bidsService: BidsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async index(query: PaginateQuery) {
|
||||||
|
const filterableColumns: {
|
||||||
|
[key in Column<SendMessageHistory> | (string & {})]?:
|
||||||
|
| (FilterOperator | FilterSuffix)[]
|
||||||
|
| true;
|
||||||
|
} = {
|
||||||
|
id: true,
|
||||||
|
message: true,
|
||||||
|
'bid.name': true,
|
||||||
|
'bid.lot_id': true,
|
||||||
|
};
|
||||||
|
|
||||||
|
query.filter = AppResponse.processFilters(query.filter, filterableColumns);
|
||||||
|
|
||||||
|
const data = await paginate(query, this.sendMessageRepo, {
|
||||||
|
sortableColumns: ['id', 'message', 'bid.name'],
|
||||||
|
searchableColumns: ['id', 'message', 'bid.name'],
|
||||||
|
defaultLimit: 15,
|
||||||
|
filterableColumns,
|
||||||
|
defaultSortBy: [['id', 'DESC']],
|
||||||
|
relations: {
|
||||||
|
bid: true,
|
||||||
|
},
|
||||||
|
maxLimit: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
return AppResponse.toPagination<SendMessageHistory>(
|
||||||
|
data,
|
||||||
|
true,
|
||||||
|
SendMessageHistory,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendTestMessage() {
|
||||||
|
const date = new Date().toUTCString();
|
||||||
|
|
||||||
|
const mockBid = this.bidsService.bidsRepo.create({
|
||||||
|
id: 12345,
|
||||||
|
max_price: 5000,
|
||||||
|
reserve_price: 1000,
|
||||||
|
current_price: 1200,
|
||||||
|
name: 'Laptop Gaming ASUS ROG',
|
||||||
|
quantity: 1,
|
||||||
|
url: 'https://example.com/product/12345',
|
||||||
|
model: 'ROG Strix G15',
|
||||||
|
lot_id: 'LOT-67890',
|
||||||
|
plus_price: 50,
|
||||||
|
close_time: date,
|
||||||
|
start_bid_time: date,
|
||||||
|
first_bid: false,
|
||||||
|
status: 'biding',
|
||||||
|
histories: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
price: 1000,
|
||||||
|
created_at: date,
|
||||||
|
updated_at: date,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
price: 1100,
|
||||||
|
created_at: date,
|
||||||
|
updated_at: date,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
price: 1200,
|
||||||
|
created_at: date,
|
||||||
|
updated_at: date,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
created_at: date,
|
||||||
|
updated_at: date,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await this.botTelegramApi.sendBidInfo(mockBid);
|
||||||
|
|
||||||
|
return AppResponse.toResponse(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
{
|
{
|
||||||
"AuctionOutBidLots": [
|
"AuctionOutBidLots": [
|
||||||
{
|
{
|
||||||
"Id": "23464489",
|
"Id": "23482715",
|
||||||
"Sku": "0002-2566250",
|
"Sku": "0045-10734155",
|
||||||
"Bid": "AU $59"
|
"Bid": "AU $50"
|
||||||
},
|
|
||||||
{
|
|
||||||
"Id": "23478598",
|
|
||||||
"Sku": "0153-3032503",
|
|
||||||
"Bid": "AU $12"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"NumOfLosingBids": 3
|
"NumOfLosingBids": 3
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { io } from 'socket.io-client';
|
import { io } from 'socket.io-client';
|
||||||
import { createApiBid, createBidProduct, deleteProfile } from './service/app-service.js';
|
import { createApiBid, createBidProduct, deleteProfile, shouldUpdateProductTab } from './service/app-service.js';
|
||||||
import browser from './system/browser.js';
|
import browser from './system/browser.js';
|
||||||
import configs from './system/config.js';
|
import configs from './system/config.js';
|
||||||
import { isTimeReached, safeClosePage } from './system/utils.js';
|
import { isTimeReached, safeClosePage } from './system/utils.js';
|
||||||
|
|
@ -48,6 +48,84 @@ const handleUpdateProductTabs = (data) => {
|
||||||
MANAGER_BIDS = newDataManager;
|
MANAGER_BIDS = newDataManager;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// const tracking = async () => {
|
||||||
|
// if (_INTERVAL_TRACKING_ID) {
|
||||||
|
// clearInterval(_INTERVAL_TRACKING_ID);
|
||||||
|
// _INTERVAL_TRACKING_ID = null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// _INTERVAL_TRACKING_ID = setInterval(async () => {
|
||||||
|
// const productTabs = _.flatMap(MANAGER_BIDS, 'children');
|
||||||
|
|
||||||
|
// for (const productTab of productTabs) {
|
||||||
|
// if (!productTab.parent_browser_context) {
|
||||||
|
// const parent = _.find(MANAGER_BIDS, { id: productTab.web_bid.id });
|
||||||
|
|
||||||
|
// productTab.parent_browser_context = parent.browser_context;
|
||||||
|
|
||||||
|
// if (!productTab.parent_browser_context) {
|
||||||
|
// console.log(`🔄 Waiting for parent process to start... (Product ID: ${productTab.id})`);
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!productTab.first_bid) {
|
||||||
|
// console.log(`🎯 Tracking out-bid event for Product ID: ${productTab.id}`);
|
||||||
|
|
||||||
|
// const updatedAt = new Date(productTab.updated_at).getTime();
|
||||||
|
// const now = Date.now();
|
||||||
|
|
||||||
|
// if (!productTab.page_context) {
|
||||||
|
// await productTab.puppeteer_connect();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (productTab.page_context.url() !== productTab.url) {
|
||||||
|
// await productTab.gotoLink();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (now - updatedAt < ONE_MINUTE) {
|
||||||
|
// console.log(`⏳ Product ID: ${productTab.id} was updated recently. Skipping update.`);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// await productTab.update();
|
||||||
|
// console.log(`🔄 Updating Product ID: ${productTab.id}...`);
|
||||||
|
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (productTab.start_bid_time && !isTimeReached(productTab.start_bid_time)) {
|
||||||
|
// console.log(`⏳ Not yet time to bid. Skipping Product ID: ${productTab.id}`);
|
||||||
|
|
||||||
|
// const updatedAt = new Date(productTab.updated_at).getTime();
|
||||||
|
// const now = Date.now();
|
||||||
|
|
||||||
|
// if (!productTab.page_context) {
|
||||||
|
// await productTab.puppeteer_connect();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (productTab.page_context.url() !== productTab.url) {
|
||||||
|
// await productTab.gotoLink();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (now - updatedAt < ONE_MINUTE) {
|
||||||
|
// console.log(`⏳ Product ID: ${productTab.id} was updated recently. Skipping update.`);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// await productTab.update();
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!productTab.page_context) {
|
||||||
|
// console.log(`🔌 Connecting to page for Product ID: ${productTab.id}`);
|
||||||
|
// await productTab.puppeteer_connect();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// console.log(`🚀 Executing action for Product ID: ${productTab.id}`);
|
||||||
|
// await productTab.action();
|
||||||
|
// }
|
||||||
|
// }, configs.AUTO_TRACKING_DELAY);
|
||||||
|
// };
|
||||||
|
|
||||||
const tracking = async () => {
|
const tracking = async () => {
|
||||||
if (_INTERVAL_TRACKING_ID) {
|
if (_INTERVAL_TRACKING_ID) {
|
||||||
clearInterval(_INTERVAL_TRACKING_ID);
|
clearInterval(_INTERVAL_TRACKING_ID);
|
||||||
|
|
@ -58,10 +136,10 @@ const tracking = async () => {
|
||||||
const productTabs = _.flatMap(MANAGER_BIDS, 'children');
|
const productTabs = _.flatMap(MANAGER_BIDS, 'children');
|
||||||
|
|
||||||
for (const productTab of productTabs) {
|
for (const productTab of productTabs) {
|
||||||
|
// Tìm parent context nếu chưa có
|
||||||
if (!productTab.parent_browser_context) {
|
if (!productTab.parent_browser_context) {
|
||||||
const parent = _.find(MANAGER_BIDS, { id: productTab.web_bid.id });
|
const parent = _.find(MANAGER_BIDS, { id: productTab.web_bid.id });
|
||||||
|
productTab.parent_browser_context = parent?.browser_context;
|
||||||
productTab.parent_browser_context = parent.browser_context;
|
|
||||||
|
|
||||||
if (!productTab.parent_browser_context) {
|
if (!productTab.parent_browser_context) {
|
||||||
console.log(`🔄 Waiting for parent process to start... (Product ID: ${productTab.id})`);
|
console.log(`🔄 Waiting for parent process to start... (Product ID: ${productTab.id})`);
|
||||||
|
|
@ -69,85 +147,43 @@ const tracking = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!productTab.first_bid) {
|
// Kết nối Puppeteer nếu chưa có page_context
|
||||||
console.log(`🎯 Tracking out-bid event for Product ID: ${productTab.id}`);
|
|
||||||
|
|
||||||
if (!productTab.page_context) {
|
|
||||||
console.log(`🔌 Establishing connection for Product ID: ${productTab.id}`);
|
|
||||||
await productTab.puppeteer_connect();
|
|
||||||
|
|
||||||
await productTab.handleTakeWorkSnapshot();
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (productTab.start_bid_time && !isTimeReached(productTab.start_bid_time)) {
|
|
||||||
console.log(`⏳ Not yet time to bid. Skipping Product ID: ${productTab.id}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!productTab.page_context) {
|
if (!productTab.page_context) {
|
||||||
console.log(`🔌 Connecting to page for Product ID: ${productTab.id}`);
|
console.log(`🔌 Connecting to page for Product ID: ${productTab.id}`);
|
||||||
await productTab.puppeteer_connect();
|
await productTab.puppeteer_connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nếu URL thay đổi, điều hướng đến URL mới
|
||||||
|
if (productTab.page_context.url() !== productTab.url) {
|
||||||
|
await productTab.gotoLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kiểm tra nếu cần cập nhật trước khi gọi update()
|
||||||
|
if (shouldUpdateProductTab(productTab)) {
|
||||||
|
console.log(`🔄 Updating Product ID: ${productTab.id}...`);
|
||||||
|
await productTab.update();
|
||||||
|
} else {
|
||||||
|
console.log(`⏳ Product ID: ${productTab.id} was updated recently. Skipping update.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nếu chưa có first_bid (trạng thái chưa đặt giá)
|
||||||
|
if (!productTab.first_bid) {
|
||||||
|
console.log(`🎯 Tracking out-bid event for Product ID: ${productTab.id}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nếu chưa đến giờ bid
|
||||||
|
if (productTab.start_bid_time && !isTimeReached(productTab.start_bid_time)) {
|
||||||
|
console.log(`⏳ Not yet time to bid. Skipping Product ID: ${productTab.id}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`🚀 Executing action for Product ID: ${productTab.id}`);
|
console.log(`🚀 Executing action for Product ID: ${productTab.id}`);
|
||||||
await productTab.action();
|
await productTab.action();
|
||||||
}
|
}
|
||||||
}, configs.AUTO_TRACKING_DELAY);
|
}, 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 () => {
|
const clearLazyTab = async () => {
|
||||||
if (_CLEAR_LAZY_TAB_ID) {
|
if (_CLEAR_LAZY_TAB_ID) {
|
||||||
clearInterval(_CLEAR_LAZY_TAB_ID);
|
clearInterval(_CLEAR_LAZY_TAB_ID);
|
||||||
|
|
|
||||||
|
|
@ -15,29 +15,29 @@ export class GrayApiBid extends ApiBid {
|
||||||
|
|
||||||
async polling(page) {
|
async polling(page) {
|
||||||
try {
|
try {
|
||||||
// // 🔥 Xóa tất cả event chặn request trước khi thêm mới
|
// 🔥 Xóa tất cả event chặn request trước khi thêm mới
|
||||||
// page.removeAllListeners('request');
|
page.removeAllListeners('request');
|
||||||
// await page.setRequestInterception(true);
|
await page.setRequestInterception(true);
|
||||||
|
|
||||||
// page.on('request', (request) => {
|
page.on('request', (request) => {
|
||||||
// if (request.url().includes('api/Notifications/GetOutBidLots')) {
|
if (request.url().includes('api/Notifications/GetOutBidLots')) {
|
||||||
// console.log('🚀 Fake response cho request:', request.url());
|
console.log('🚀 Fake response cho request:', request.url());
|
||||||
|
|
||||||
// const fakeData = fs.readFileSync('./data/fake-out-lots.json', 'utf8');
|
const fakeData = fs.readFileSync('./data/fake-out-lots.json', 'utf8');
|
||||||
|
|
||||||
// request.respond({
|
request.respond({
|
||||||
// status: 200,
|
status: 200,
|
||||||
// contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
// body: fakeData,
|
body: fakeData,
|
||||||
// });
|
});
|
||||||
// } else {
|
} else {
|
||||||
// try {
|
try {
|
||||||
// request.continue(); // ⚠️ Chỉ tiếp tục nếu request chưa bị chặn
|
request.continue(); // ⚠️ Chỉ tiếp tục nếu request chưa bị chặn
|
||||||
// } catch (error) {
|
} catch (error) {
|
||||||
// console.error('⚠️ Lỗi khi tiếp tục request:', error.message);
|
console.error('⚠️ Lỗi khi tiếp tục request:', error.message);
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// });
|
});
|
||||||
|
|
||||||
console.log('🔄 Starting polling process...');
|
console.log('🔄 Starting polling process...');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { outBid, pushPrice, updateBid, updateStatusByPrice } from '../../system/apis/bid.js';
|
import { outBid, pushPrice, updateBid, updateStatusByPrice } from '../../system/apis/bid.js';
|
||||||
import CONSTANTS from '../../system/constants.js';
|
import CONSTANTS from '../../system/constants.js';
|
||||||
import { delay, extractNumber, isNumber, isTimeReached, safeClosePage, takeSnapshot } from '../../system/utils.js';
|
import { delay, extractNumber, isNumber, isTimeReached, removeFalsyValues, safeClosePage, takeSnapshot } from '../../system/utils.js';
|
||||||
import { ProductBid } from '../product-bid.js';
|
import { ProductBid } from '../product-bid.js';
|
||||||
|
|
||||||
export class GraysProductBid extends ProductBid {
|
export class GraysProductBid extends ProductBid {
|
||||||
|
|
@ -17,7 +17,7 @@ export class GraysProductBid extends ProductBid {
|
||||||
if (!isNumber(price_value)) {
|
if (!isNumber(price_value)) {
|
||||||
console.log("Can't get PRICE_VALUE ❌");
|
console.log("Can't get PRICE_VALUE ❌");
|
||||||
await takeSnapshot(page, this, 'price-value-null');
|
await takeSnapshot(page, this, 'price-value-null');
|
||||||
await safeClosePage(this);
|
// await safeClosePage(this);
|
||||||
|
|
||||||
return { result: false, bid_price: 0 };
|
return { result: false, bid_price: 0 };
|
||||||
}
|
}
|
||||||
|
|
@ -27,7 +27,7 @@ export class GraysProductBid extends ProductBid {
|
||||||
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('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 safeClosePage(this);
|
// await safeClosePage(this);
|
||||||
|
|
||||||
await outBid(this.id);
|
await outBid(this.id);
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@ export class GraysProductBid extends ProductBid {
|
||||||
|
|
||||||
if (!response.status) {
|
if (!response.status) {
|
||||||
// await this.handleReturnProductPage(page);
|
// await this.handleReturnProductPage(page);
|
||||||
await safeClosePage(this);
|
// await safeClosePage(this);
|
||||||
return { result: false, bid_price: 0 };
|
return { result: false, bid_price: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,7 +97,7 @@ export class GraysProductBid extends ProductBid {
|
||||||
|
|
||||||
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(`Product is close ${close_time} ❌`);
|
||||||
await safeClosePage(this);
|
// await safeClosePage(this);
|
||||||
return { result: true, close_time };
|
return { result: true, close_time };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,7 +131,7 @@ export class GraysProductBid extends ProductBid {
|
||||||
console.log({ error: error.message });
|
console.log({ error: error.message });
|
||||||
console.log('❌ Timeout to loading');
|
console.log('❌ Timeout to loading');
|
||||||
await takeSnapshot(page, this, 'timeout to loading');
|
await takeSnapshot(page, this, 'timeout to loading');
|
||||||
await safeClosePage(this);
|
// await safeClosePage(this);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -142,7 +142,7 @@ export class GraysProductBid extends ProductBid {
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleUpdateBid({ lot_id, close_time, name, current_price, reserve_price }) {
|
async handleUpdateBid({ lot_id, close_time, name, current_price, reserve_price }) {
|
||||||
if (close_time && this.close_time == close_time) return;
|
// if (close_time && this.close_time == close_time) return;
|
||||||
|
|
||||||
const response = await updateBid(this.id, { lot_id, close_time, name, current_price, reserve_price: Number(reserve_price) || 0 });
|
const response = await updateBid(this.id, { lot_id, close_time, name, current_price, reserve_price: Number(reserve_price) || 0 });
|
||||||
|
|
||||||
|
|
@ -153,19 +153,75 @@ export class GraysProductBid extends ProductBid {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update = async () => {
|
||||||
|
// if (!this.page_context) return;
|
||||||
|
|
||||||
|
// const page = this.page_context;
|
||||||
|
|
||||||
|
// const close_time = await this.getCloseTime();
|
||||||
|
// const price_value = (await page.$eval('#priceValue', (el) => el.value)) || null;
|
||||||
|
// const lot_id = await page.$eval('#lotId', (el) => el.value);
|
||||||
|
// const name = (await page.$eval('#placebid-sticky > div:nth-child(2) > div > h3', (el) => el.innerText)) || null;
|
||||||
|
// 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}, Reserve price ${price_value}`);
|
||||||
|
|
||||||
|
// this.handleUpdateBid({ lot_id, reserve_price: price_value, close_time, name, current_price: current_price ? extractNumber(current_price) : null });
|
||||||
|
|
||||||
|
// return { price_value, lot_id, name, current_price };
|
||||||
|
// };
|
||||||
|
update = async () => {
|
||||||
|
if (!this.page_context) return;
|
||||||
|
|
||||||
|
const page = this.page_context;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const close_time = await this.getCloseTime();
|
||||||
|
|
||||||
|
// Chờ phần tử xuất hiện trước khi lấy giá trị
|
||||||
|
await page.waitForSelector('#priceValue', { timeout: 5000 }).catch(() => null);
|
||||||
|
const price_value = await page.$eval('#priceValue', (el) => el.value).catch(() => null);
|
||||||
|
|
||||||
|
await page.waitForSelector('#lotId', { timeout: 5000 }).catch(() => null);
|
||||||
|
const lot_id = await page.$eval('#lotId', (el) => el.value).catch(() => null);
|
||||||
|
|
||||||
|
await page.waitForSelector('#placebid-sticky > div:nth-child(2) > div > h3', { timeout: 5000 }).catch(() => null);
|
||||||
|
const name = await page.$eval('#placebid-sticky > div:nth-child(2) > div > h3', (el) => el.innerText).catch(() => null);
|
||||||
|
|
||||||
|
await page
|
||||||
|
.waitForSelector('#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span', { timeout: 5000 })
|
||||||
|
.catch(() => null);
|
||||||
|
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)
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
|
console.log(`📌 Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price: ${price_value}`);
|
||||||
|
|
||||||
|
const data = removeFalsyValues({
|
||||||
|
lot_id,
|
||||||
|
reserve_price: price_value,
|
||||||
|
close_time: String(close_time),
|
||||||
|
name,
|
||||||
|
current_price: current_price ? extractNumber(current_price) : null,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.handleUpdateBid(data);
|
||||||
|
|
||||||
|
return { price_value, lot_id, name, current_price };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`🚨 Error updating product info: ${error.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
action = async () => {
|
action = async () => {
|
||||||
try {
|
try {
|
||||||
const page = this.page_context;
|
const page = this.page_context;
|
||||||
console.log('🔄 Starting the bidding process...');
|
|
||||||
|
|
||||||
await page.goto(this.url, { waitUntil: 'networkidle2' });
|
await this.gotoLink();
|
||||||
console.log(`✅ Navigated to: ${this.url}`);
|
|
||||||
|
|
||||||
await page.bringToFront();
|
|
||||||
console.log('👀 Brought the tab to the foreground.');
|
|
||||||
|
|
||||||
await delay(1000);
|
await delay(1000);
|
||||||
// this.handleTakeWorkSnapshot();
|
|
||||||
|
|
||||||
const { close_time, ...isCloseProduct } = await this.isCloseProduct(page);
|
const { close_time, ...isCloseProduct } = await this.isCloseProduct(page);
|
||||||
if (isCloseProduct.result) {
|
if (isCloseProduct.result) {
|
||||||
|
|
@ -175,15 +231,9 @@ export class GraysProductBid extends ProductBid {
|
||||||
|
|
||||||
await delay(500);
|
await delay(500);
|
||||||
|
|
||||||
const price_value = (await page.$eval('#priceValue', (el) => el.value)) || null;
|
const { price_value } = await this.update();
|
||||||
const lot_id = await page.$eval('#lotId', (el) => el.value);
|
|
||||||
const name = (await page.$eval('#placebid-sticky > div:nth-child(2) > div > h3', (el) => el.innerText)) || null;
|
|
||||||
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}, Reserve price ${price_value}`);
|
if (!price_value) return;
|
||||||
|
|
||||||
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 });
|
const { result, bid_price } = await this.validate({ page, price_value });
|
||||||
|
|
||||||
|
|
@ -203,7 +253,7 @@ export class GraysProductBid extends ProductBid {
|
||||||
if (!resultPlaceBid) {
|
if (!resultPlaceBid) {
|
||||||
console.log('❌ Error occurred while placing the bid.');
|
console.log('❌ Error occurred while placing the bid.');
|
||||||
await takeSnapshot(page, this, 'place-bid-action');
|
await takeSnapshot(page, this, 'place-bid-action');
|
||||||
await safeClosePage(this);
|
// await safeClosePage(this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,7 +262,7 @@ export class GraysProductBid extends ProductBid {
|
||||||
await this.handleReturnProductPage(page);
|
await this.handleReturnProductPage(page);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`🚨 Error navigating the page: ${error.message}`);
|
console.error(`🚨 Error navigating the page: ${error.message}`);
|
||||||
safeClosePage(this);
|
// safeClosePage(this);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ export class ProductBid extends Bid {
|
||||||
current_price;
|
current_price;
|
||||||
name;
|
name;
|
||||||
reserve_price;
|
reserve_price;
|
||||||
|
update;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
url,
|
url,
|
||||||
|
|
@ -133,4 +134,15 @@ 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.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ 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 * as fs from 'fs';
|
||||||
|
|
||||||
|
const ONE_MINUTE = 60 * 1000;
|
||||||
|
|
||||||
export const handleCloseRemoveProduct = (data) => {
|
export const handleCloseRemoveProduct = (data) => {
|
||||||
if (!Array.isArray(data)) return;
|
if (!Array.isArray(data)) return;
|
||||||
|
|
||||||
|
|
@ -41,3 +44,9 @@ export const deleteProfile = (data) => {
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const shouldUpdateProductTab = (productTab) => {
|
||||||
|
const updatedAt = new Date(productTab.updated_at).getTime();
|
||||||
|
const now = Date.now();
|
||||||
|
return now - updatedAt >= ONE_MINUTE;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ export const updateStatusByPrice = async (id, current_price) => {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: 'bids/update-status/' + id,
|
url: 'bids/update-status/' + id,
|
||||||
data: {
|
data: {
|
||||||
current_price,
|
current_price: Number(current_price) | 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export const createOutBidLog = async (values) => {
|
||||||
|
|
||||||
return data.data;
|
return data.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('❌ ERROR IN SERVER (OUT BID LOG): ', error);
|
console.log('❌ ERROR IN SERVER (OUT BID LOG): ', error.message);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ puppeteer.use(StealthPlugin());
|
||||||
const browser = await puppeteer.launch({
|
const browser = await puppeteer.launch({
|
||||||
headless: process.env.ENVIRONMENT === 'prod' ? true : false,
|
headless: process.env.ENVIRONMENT === 'prod' ? true : false,
|
||||||
// userDataDir: CONSTANTS.PROFILE_PATH, // Thư mục lưu profile
|
// userDataDir: CONSTANTS.PROFILE_PATH, // Thư mục lưu profile
|
||||||
|
timeout: 60000,
|
||||||
args: [
|
args: [
|
||||||
'--no-sandbox',
|
'--no-sandbox',
|
||||||
'--disable-setuid-sandbox',
|
'--disable-setuid-sandbox',
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { updateStatusWork } from './apis/bid.js';
|
||||||
export const isNumber = (value) => !isNaN(value) && !isNaN(parseFloat(value));
|
export const isNumber = (value) => !isNaN(value) && !isNaN(parseFloat(value));
|
||||||
|
|
||||||
export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_IMAGE.ERRORS) => {
|
export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_IMAGE.ERRORS) => {
|
||||||
if (page.isClosed()) return;
|
if (!page || page.isClosed()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
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
|
||||||
|
|
@ -23,7 +23,19 @@ export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_
|
||||||
console.log(`📂 Save at folder: ${typeDir}`);
|
console.log(`📂 Save at folder: ${typeDir}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.waitForSelector('body', { visible: true, timeout: 5000 });
|
// await page.waitForSelector('body', { visible: true, timeout: 5000 });
|
||||||
|
// Kiểm tra có thể điều hướng trang không
|
||||||
|
const isPageResponsive = await page.evaluate(() => document.readyState === 'complete');
|
||||||
|
if (!isPageResponsive) {
|
||||||
|
console.log('🚫 Page is unresponsive, skipping snapshot.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chờ tối đa 15 giây, nếu không thấy thì bỏ qua
|
||||||
|
await page.waitForSelector('body', { visible: true, timeout: 15000 }).catch(() => {
|
||||||
|
console.log('⚠️ Body selector not found, skipping snapshot.');
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
// Chụp ảnh màn hình và lưu vào filePath
|
// Chụp ảnh màn hình và lưu vào filePath
|
||||||
await page.screenshot({ path: filePath, fullPage: true });
|
await page.screenshot({ path: filePath, fullPage: true });
|
||||||
|
|
@ -79,3 +91,12 @@ export const sanitizeFileName = (url) => {
|
||||||
export const getPathProfile = (origin_url) => {
|
export const getPathProfile = (origin_url) => {
|
||||||
return path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(origin_url) + '.json');
|
return path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(origin_url) + '.json');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function removeFalsyValues(obj, excludeKeys = []) {
|
||||||
|
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||||||
|
if (value || excludeKeys.includes(key)) {
|
||||||
|
acc[key] = value;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue