update all bid

This commit is contained in:
Admin 2025-07-02 15:55:53 +07:00
parent 52167a360b
commit 41e174f545
10 changed files with 289 additions and 41 deletions

View File

@ -1,13 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="public/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
<head>
<meta charset="UTF-8" />
<link rel="icon" href="public/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bid System</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -1,3 +1,4 @@
import AppResponse from '@/response/app-response';
import {
Body,
Controller,
@ -12,8 +13,10 @@ import {
import { EventEmitter2 } from '@nestjs/event-emitter';
import { FileInterceptor } from '@nestjs/platform-express';
import { plainToClass } from 'class-transformer';
import { diskStorage, memoryStorage } from 'multer';
import { memoryStorage } from 'multer';
import { ClientInfoUpdateBidDto } from '../../dto/bid/client-info-update-bid.dto';
import { ClientUpdateBidDto } from '../../dto/bid/client-update-bid.dto';
import { ClientUpdateLoginStatusDto } from '../../dto/bid/client-update-login-status.dto';
import { CreateBidDto } from '../../dto/bid/create-bid.dto';
import { UpdateStatusByPriceDto } from '../../dto/bid/update-status-by-price.dto';
import { Bid } from '../../entities/bid.entity';
@ -21,11 +24,6 @@ import { WebBid } from '../../entities/wed-bid.entity';
import { BidsService } from '../../services/bids.service';
import { WebBidsService } from '../../services/web-bids.service';
import { Event } from '../../utils/events';
import AppResponse from '@/response/app-response';
import { ClientUpdateLoginStatusDto } from '../../dto/bid/client-update-login-status.dto';
import { UpdateBidDto } from '../../dto/bid/update-bid.dto';
import { ClientInfoUpdateBidDto } from '../../dto/bid/client-info-update-bid.dto';
import path from 'path';
@Controller('bids')
export class BidsController {
@ -100,14 +98,14 @@ export class BidsController {
@Post('test')
async test(@Body('code') code: string) {
const webBid = await this.webBidService.webBidRepo.findOne({
where: { id: 4 },
// where: { id: 1 },
// where: { id: 4 },
where: { id: 1 },
});
this.eventEmitter.emit(Event.verifyCode(webBid), {
code,
name: 'LAWSONS',
// name: 'LANGTONS',
// name: 'LAWSONS',
name: 'LANGTONS',
web_bid: plainToClass(WebBid, webBid),
});

View File

@ -109,6 +109,10 @@ export class BidMetadatasService {
return this.getArrivalOffsetSecondsSandbox(metadatas);
};
isSandbox(metadatas: BidMetadata[]) {
return this.getMode(metadatas) !== 'live';
}
async delete(id: Bid['id']) {
const bid = await this.bidMetadataRepo.findOne({ where: { id } });

View File

@ -24,6 +24,7 @@ import { join } from 'path';
import AppResponse from 'src/response/app-response';
import {
extractModelId,
isTimePassedByMinutes,
isTimeReached,
parseVideoFileName,
subtractMinutes,
@ -156,6 +157,36 @@ export class BidsService {
await this.emitAllBidEvent();
const warnings = [];
if (!webBid.username || !webBid.password) {
// Add warning message
warnings.push(
`Account setup for ${webBid.origin_url} website is not yet complete.`,
);
}
if (!webBid.active) {
// Add warning message
warnings.push(
`${webBid.origin_url} is disabled. Please enable to continue.`,
);
}
if (warnings.length) {
// Send event warning
this.eventEmitter.emit(Event.SEND_WARNING, {
title: `System Warning: Abnormal Data Detected`,
messages: warnings,
});
}
// Send event success
this.eventEmitter.emit(Event.SEND_SUCCESS, {
title: 'Successfully Added Product',
messages: [`Successfully added product to bid list: ${data.url}`],
});
return AppResponse.toResponse(
response ? response(result) : plainToClass(Bid, result),
);
@ -356,6 +387,12 @@ export class BidsService {
close_time,
arrival_offset_seconds / 60,
);
// update
await this.bidsRepo.update(
{ id },
{ start_bid_time: bid.start_bid_time },
);
}
// Cập nhật thời gian kết thúc đấu giá nếu `close_time` mới lớn hơn `close_time` cũ
@ -365,6 +402,13 @@ export class BidsService {
) {
bid.close_time = close_time;
bid.close_time_ts = new Date(close_time);
// update
await this.bidsRepo.update(
{ id },
{ close_time: bid.close_time, close_time_ts: bid.close_time_ts },
);
bid = await this.getBidForClientUpdate(id);
}
@ -384,7 +428,6 @@ export class BidsService {
}
} else {
// Nếu phiên đấu giá vẫn đang diễn ra và giá hiện tại vượt quá giới hạn đặt của người dùng
if (
data.current_price > bid.max_price + bid.plus_price ||
(!bid.histories.length &&
@ -429,6 +472,30 @@ export class BidsService {
this.notificationService.emitBidStatus(result);
}
// Send INFO
if (bid.current_price < result.current_price && bid.histories.length) {
this.eventEmitter.emit(Event.SEND_INFO, {
title: 'New Higher Bid Detected',
messages: [
`Another user just placed a higher bid of ${result.current_price} on <a href="${bid.url}">${bid.name}</a>.`,
],
});
}
// Send error when bidding fail
if (
isTimePassedByMinutes(result.start_bid_time, 1) &&
!bid.histories.length &&
!this.bidMetadatasService.isSandbox(bid.metadata)
) {
this.eventEmitter.emit(Event.SEND_ERROR, {
title: 'Bidding Error Detected',
messages: [
`An error occurred while placing a bid on <a href="${bid.url}">${bid.name}</a>. Please check the system.`,
],
});
}
// Trả về kết quả cập nhật dưới dạng response chuẩn
return AppResponse.toResponse(plainToClass(Bid, result));
}

View File

@ -10,6 +10,10 @@ export class Event {
public static BID_SUBMITED = 'bid-submited';
public static BID_STATUS = 'bid-status';
public static BID_DEMO = 'bid-demo';
public static SEND_WARNING = 'send-warning';
public static SEND_SUCCESS = 'send-success';
public static SEND_ERROR = 'send-error';
public static SEND_INFO = 'send-info';
public static verifyCode(data: WebBid) {
return `${this.VERIFY_CODE}.${data.origin_url}`;

View File

@ -468,4 +468,68 @@ export class MailsService {
</div>
`;
}
generateWarningEmailTemplate(title: string, messages: string[]): string {
const timestamp = new Date().toLocaleString(); // hoặc new Date().toISOString()
return `
<div style="font-family: Arial, sans-serif; padding: 20px; background-color: #fffbe6; color: #333;">
<h2 style="color: #d9534f;"> ${title}</h2>
<ul>
${messages.map((msg) => `<li>${msg}</li>`).join('')}
</ul>
<p style="margin-top: 20px; font-size: 12px; color: #999;">
Sent at: ${timestamp}
</p>
</div>
`;
}
generateSuccessEmailTemplate(title: string, messages: string[]): string {
const timestamp = new Date().toLocaleString(); // hoặc new Date().toISOString()
return `
<div style="font-family: Arial, sans-serif; padding: 20px; background-color: #e6fff2; color: #333;">
<h2 style="color: #28a745;"> ${title}</h2>
<ul>
${messages.map((msg) => `<li>${msg}</li>`).join('')}
</ul>
<p style="margin-top: 20px; font-size: 12px; color: #999;">
Sent at: ${timestamp}
</p>
</div>
`;
}
generateErrorEmailTemplate(title: string, messages: string[]): string {
const timestamp = new Date().toLocaleString();
return `
<div style="font-family: Arial, sans-serif; padding: 20px; background-color: #fdecea; color: #333;">
<h2 style="color: #c82333;"> ${title}</h2>
<ul>
${messages.map((msg) => `<li>${msg}</li>`).join('')}
</ul>
<p style="margin-top: 20px; font-size: 12px; color: #999;">
Sent at: ${timestamp}
</p>
</div>
`;
}
generateInfoEmailTemplate(title: string, messages: string[]): string {
const timestamp = new Date().toLocaleString();
return `
<div style="font-family: Arial, sans-serif; padding: 20px; background-color: #e7f3fe; color: #333;">
<h2 style="color: #31708f;"> ${title}</h2>
<ul>
${messages.map((msg) => `<li>${msg}</li>`).join('')}
</ul>
<p style="margin-top: 20px; font-size: 12px; color: #999;">
Sent at: ${timestamp}
</p>
</div>
`;
}
}

View File

@ -69,4 +69,98 @@ export class AdminNotificationListener {
moment(new Date()).format('YYYY-MM-DD HH:mm'),
});
}
@OnEvent(Event.SEND_WARNING)
async handleSendWarning({
title,
messages,
}: {
title: string;
messages: string[];
}) {
const mails =
(await this.configsSerice.getConfig('MAIL_SCRAP_REPORT')).value || '';
const html = this.mailsService.generateWarningEmailTemplate(
title,
messages,
);
this.mailsService.sendHtmlMailJob({
to: mails,
html: html,
subject:
'[WARNING] Auto Auctions System ' +
moment(new Date()).format('YYYY-MM-DD HH:mm'),
});
}
@OnEvent(Event.SEND_SUCCESS)
async handleSendSuccess({
title,
messages,
}: {
title: string;
messages: string[];
}) {
const mails =
(await this.configsSerice.getConfig('MAIL_SCRAP_REPORT')).value || '';
const html = this.mailsService.generateSuccessEmailTemplate(
title,
messages,
);
this.mailsService.sendHtmlMailJob({
to: mails,
html: html,
subject:
'[SUCCESS] Auto Auctions System ' +
moment(new Date()).format('YYYY-MM-DD HH:mm'),
});
}
@OnEvent(Event.SEND_ERROR)
async handleSendError({
title,
messages,
}: {
title: string;
messages: string[];
}) {
const mails =
(await this.configsSerice.getConfig('MAIL_SCRAP_REPORT')).value || '';
const html = this.mailsService.generateErrorEmailTemplate(title, messages);
this.mailsService.sendHtmlMailJob({
to: mails,
html: html,
subject:
'[ERROR] Auto Auctions System ' +
moment(new Date()).format('YYYY-MM-DD HH:mm'),
});
}
@OnEvent(Event.SEND_INFO)
async handleSendInfo({
title,
messages,
}: {
title: string;
messages: string[];
}) {
const mails =
(await this.configsSerice.getConfig('MAIL_SCRAP_REPORT')).value || '';
const html = this.mailsService.generateInfoEmailTemplate(title, messages);
this.mailsService.sendHtmlMailJob({
to: mails,
html: html,
subject:
'[INFO] Auto Auctions System ' +
moment(new Date()).format('YYYY-MM-DD HH:mm'),
});
}
}

View File

@ -65,6 +65,19 @@ export function isTimeReached(targetTime: string) {
return now >= targetDate;
}
export function isTimePassedByMinutes(
targetTime: string,
minutes: number,
): boolean {
const targetDate = new Date(targetTime);
const now = new Date();
const diffMs = now.getTime() - targetDate.getTime(); // hiệu số milliseconds
const diffMinutes = diffMs / (1000 * 60);
return diffMinutes >= minutes;
}
export function extractDomain(url: string): string | null {
try {
const parsedUrl = new URL(url);

View File

@ -16,12 +16,6 @@ export class AllbidsApiBid extends ApiBid {
const currentUrl = await this.page_context.url();
console.log({
filePath,
currentUrl,
a: currentUrl.includes(configs.WEB_URLS.ALLBIDS.LOGIN_URL),
});
if (currentUrl.includes(configs.WEB_URLS.ALLBIDS.LOGIN_URL)) return false;
return (

View File

@ -33,7 +33,11 @@ export class AllbidsProductBid extends ProductBid {
for (let i = 0; i < elements.length; i++) {
try {
const scope = angular.element(elements[i]).scope();
if (scope?.auction) {
if (
scope?.auction &&
scope?.auction.aucID === this.model &&
scope?.auction?.aucBidIncrement
) {
data = scope.auction;
break;
}
@ -134,7 +138,7 @@ export class AllbidsProductBid extends ProductBid {
const response = await this.page_context.evaluate(
async (aucID, bidAmount, submitUrl) => {
try {
const url = `${submitUrl}?aucID=${aucID}&bidAmount=${bidAmount}&bidType=maximum`;
const url = `${submitUrl}?aucID=${aucID}&bidAmount=${bidAmount}`;
const res = await fetch(url, {
method: "POST",
@ -153,7 +157,7 @@ export class AllbidsProductBid extends ProductBid {
},
this.model,
this.max_price,
configs.WEB_URLS.ALLBIDS.PLACE_BID
configs.WEB_CONFIGS.ALLBIDS.PLACE_BID
);
return response;
@ -183,7 +187,7 @@ export class AllbidsProductBid extends ProductBid {
}
},
this.model,
configs.WEB_URLS.ALLBIDS.PLACE_BID
configs.WEB_CONFIGS.ALLBIDS.PLACE_BID
);
return response;
@ -329,26 +333,32 @@ export class AllbidsProductBid extends ProductBid {
await this.page_context.reload({ waitUntil: "networkidle0" });
const { aucUserMaxBid } = await this.waitForApiResponse();
console.log(`📡 [${this.id}] API Response received:`, { aucUserMaxBid });
// const { aucUserMaxBid } = await this.waitForApiResponse();
console.log(`📡 [${this.id}] API Response received:`, {
aucUserMaxBid: this.max_price,
});
// 📌 Kiểm tra trạng thái đấu giá từ API
if (aucUserMaxBid == this.max_price) {
if (
data?.bidResult?.result ||
data?.bidResult?.bidAmount == this.max_price
) {
console.log(`📸 [${this.id}] Taking bid success snapshot...`);
await takeSnapshot(
page,
this,
"bid-success",
CONSTANTS.TYPE_IMAGE.SUCCESS
);
// sendMessage(this);
pushPrice({
bid_id: this.id,
price: aucUserMaxBid,
price: this.max_price,
});
await takeSnapshot(
this.page_context,
this,
"bid-success",
CONSTANTS.TYPE_IMAGE.SUCCESS
);
console.log(`✅ [${this.id}] Bid placed successfully!`);
return;
}