diff --git a/auto-bid-admin/index.html b/auto-bid-admin/index.html index 8ac7b8f..38abf8d 100644 --- a/auto-bid-admin/index.html +++ b/auto-bid-admin/index.html @@ -1,13 +1,13 @@ - - - - - Vite + React + TS - - -
- - + + + + + Bid System + + +
+ + diff --git a/auto-bid-server/src/modules/bids/controllers/client/bids.controller.ts b/auto-bid-server/src/modules/bids/controllers/client/bids.controller.ts index 3cd5366..0945b48 100644 --- a/auto-bid-server/src/modules/bids/controllers/client/bids.controller.ts +++ b/auto-bid-server/src/modules/bids/controllers/client/bids.controller.ts @@ -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), }); diff --git a/auto-bid-server/src/modules/bids/services/bid-metadatas.service.ts b/auto-bid-server/src/modules/bids/services/bid-metadatas.service.ts index 045c4a1..6de3ae1 100644 --- a/auto-bid-server/src/modules/bids/services/bid-metadatas.service.ts +++ b/auto-bid-server/src/modules/bids/services/bid-metadatas.service.ts @@ -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 } }); diff --git a/auto-bid-server/src/modules/bids/services/bids.service.ts b/auto-bid-server/src/modules/bids/services/bids.service.ts index 662ad68..66af446 100644 --- a/auto-bid-server/src/modules/bids/services/bids.service.ts +++ b/auto-bid-server/src/modules/bids/services/bids.service.ts @@ -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 ${bid.name}.`, + ], + }); + } + + // 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 ${bid.name}. 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)); } diff --git a/auto-bid-server/src/modules/bids/utils/events.ts b/auto-bid-server/src/modules/bids/utils/events.ts index 07bed77..3158cd6 100644 --- a/auto-bid-server/src/modules/bids/utils/events.ts +++ b/auto-bid-server/src/modules/bids/utils/events.ts @@ -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}`; diff --git a/auto-bid-server/src/modules/mails/services/mails.service.ts b/auto-bid-server/src/modules/mails/services/mails.service.ts index 28a27da..e711e7d 100644 --- a/auto-bid-server/src/modules/mails/services/mails.service.ts +++ b/auto-bid-server/src/modules/mails/services/mails.service.ts @@ -468,4 +468,68 @@ export class MailsService { `; } + + generateWarningEmailTemplate(title: string, messages: string[]): string { + const timestamp = new Date().toLocaleString(); // hoặc new Date().toISOString() + + return ` +
+

⚠️ ${title}

+ +

+ Sent at: ${timestamp} +

+
+ `; + } + + generateSuccessEmailTemplate(title: string, messages: string[]): string { + const timestamp = new Date().toLocaleString(); // hoặc new Date().toISOString() + + return ` +
+

✅ ${title}

+ +

+ Sent at: ${timestamp} +

+
+ `; + } + + generateErrorEmailTemplate(title: string, messages: string[]): string { + const timestamp = new Date().toLocaleString(); + + return ` +
+

❌ ${title}

+ +

+ Sent at: ${timestamp} +

+
+ `; + } + + generateInfoEmailTemplate(title: string, messages: string[]): string { + const timestamp = new Date().toLocaleString(); + + return ` +
+

ℹ️ ${title}

+ +

+ Sent at: ${timestamp} +

+
+ `; + } } diff --git a/auto-bid-server/src/modules/notification/listeners/admin-notification.listener.ts b/auto-bid-server/src/modules/notification/listeners/admin-notification.listener.ts index 5263f5d..4509201 100644 --- a/auto-bid-server/src/modules/notification/listeners/admin-notification.listener.ts +++ b/auto-bid-server/src/modules/notification/listeners/admin-notification.listener.ts @@ -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'), + }); + } } diff --git a/auto-bid-server/src/ultils/index.ts b/auto-bid-server/src/ultils/index.ts index f7d1dba..e7e070a 100644 --- a/auto-bid-server/src/ultils/index.ts +++ b/auto-bid-server/src/ultils/index.ts @@ -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); diff --git a/auto-bid-tool/models/allbids.com.au/allbids-api-bid.js b/auto-bid-tool/models/allbids.com.au/allbids-api-bid.js index 75f7382..c7dca06 100644 --- a/auto-bid-tool/models/allbids.com.au/allbids-api-bid.js +++ b/auto-bid-tool/models/allbids.com.au/allbids-api-bid.js @@ -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 ( diff --git a/auto-bid-tool/models/allbids.com.au/allbids-product-bid.js b/auto-bid-tool/models/allbids.com.au/allbids-product-bid.js index fcba512..1fbaaf5 100644 --- a/auto-bid-tool/models/allbids.com.au/allbids-product-bid.js +++ b/auto-bid-tool/models/allbids.com.au/allbids-product-bid.js @@ -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; }