bid-tool/auto-bid-server/src/modules/bids/services/bids.service.ts

528 lines
15 KiB
TypeScript

import {
BadRequestException,
HttpStatus,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectRepository } from '@nestjs/typeorm';
import { plainToClass } from 'class-transformer';
import { Response } from 'express';
import { existsSync, readdirSync } from 'fs';
import {
FilterOperator,
FilterSuffix,
paginate,
PaginateQuery,
} from 'nestjs-paginate';
import { Column } from 'nestjs-paginate/lib/helper';
import { join } from 'path';
import AppResponse from 'src/response/app-response';
import { extractModelId, isTimeReached, subtractMinutes } from 'src/ultils';
import { In, Repository } from 'typeorm';
import { ClientUpdateBidDto } from '../dto/bid/client-update-bid.dto';
import { CreateBidDto } from '../dto/bid/create-bid.dto';
import { UpdateBidDto } from '../dto/bid/update-bid.dto';
import { UpdateStatusByPriceDto } from '../dto/bid/update-status-by-price.dto';
import { BidHistory } from '../entities/bid-history.entity';
import { Bid } from '../entities/bid.entity';
import { ImageCompressionPipe } from '../pipes/image-compression-pipe';
import { Constant } from '../utils/constant';
import { WebBidsService } from './web-bids.service';
import { NotificationService } from '@/modules/notification/notification.service';
import { Event } from '../utils/events';
import _ from 'lodash';
import { ClientUpdateLoginStatusDto } from '../dto/bid/client-update-login-status.dto';
@Injectable()
export class BidsService {
constructor(
@InjectRepository(Bid)
readonly bidsRepo: Repository<Bid>,
@InjectRepository(BidHistory)
readonly bidHistoriesRepo: Repository<BidHistory>,
private readonly webBidsService: WebBidsService,
private eventEmitter: EventEmitter2,
private notificationService: NotificationService,
) {}
async index(query: PaginateQuery) {
const filterableColumns: {
[key in Column<Bid> | (string & {})]?:
| (FilterOperator | FilterSuffix)[]
| true;
} = {
id: true,
model: [FilterOperator.ILIKE],
lot_id: true,
close_time: true,
name: [FilterOperator.ILIKE],
status: true
};
query.filter = AppResponse.processFilters(query.filter, filterableColumns);
const data = await paginate(query, this.bidsRepo, {
sortableColumns: [
'id',
'close_time',
'first_bid',
'model',
'lot_id',
'max_price',
'status',
'name',
],
searchableColumns: ['id', 'status', 'model', 'lot_id', 'name'],
defaultLimit: 15,
filterableColumns,
defaultSortBy: [['id', 'DESC']],
maxLimit: 100,
relations: {
histories: true,
web_bid: true,
},
});
return AppResponse.toPagination<Bid>(data, true, Bid);
}
async clientIndex() {
const result = await this.bidsRepo.find({
relations: { histories: true },
where: { status: 'biding' },
});
return AppResponse.toResponse(result);
}
async emitAllBidEvent() {
await this.webBidsService.emitAllBidEvent();
}
async create(data: CreateBidDto) {
const model = extractModelId(data.url);
const prev = await this.bidsRepo.findOne({ where: { model: model } });
if (prev) {
throw new BadRequestException(
AppResponse.toResponse(null, { message: 'Product is already exits' }),
);
}
const webBid = await this.webBidsService.createByUrl(data.url);
const result = await this.bidsRepo.save({
...data,
model,
web_bid: webBid,
});
await this.emitAllBidEvent();
return AppResponse.toResponse(plainToClass(Bid, result));
}
async update(id: Bid['id'], data: UpdateBidDto) {
const prev = await this.bidsRepo.findOne({ where: { id } });
if (!prev) {
throw new NotFoundException(
AppResponse.toResponse(false, {
message: 'Product not found',
status_code: HttpStatus.NOT_FOUND,
}),
);
}
const result = await this.bidsRepo.update(id, {
...data,
// status:
// prev.max_price + prev.plus_price > data.max_price
// ? 'out-bid'
// : prev.status,
});
if (!result) throw new BadRequestException(false);
await this.emitAllBidEvent();
return AppResponse.toResponse(true);
}
async toggle(id: Bid['id']) {
const bid = await this.bidsRepo.findOne({ where: { id } });
if (!bid) {
throw new NotFoundException(
AppResponse.toResponse(false, {
message: 'Product not found',
status_code: HttpStatus.NOT_FOUND,
}),
);
}
if (bid.status === 'biding') {
await this.bidsRepo.update(id, { status: 'out-bid' });
this.emitAllBidEvent();
// send message event
this.notificationService.emitBidStatus({ ...bid, status: 'out-bid' });
return AppResponse.toResponse(true);
}
const lastHistory = await this.bidHistoriesRepo.findOne({
where: { bid: { id: bid.id } },
order: {
created_at: 'DESC',
},
});
if (lastHistory && lastHistory.price + bid.plus_price > bid.max_price) {
throw new BadRequestException(
AppResponse.toResponse(false, { message: 'Price is out of Max Price' }),
);
}
if (bid.close_time && isTimeReached(bid.close_time)) {
throw new BadRequestException(
AppResponse.toResponse(false, { message: 'Product was closed' }),
);
}
await this.bidsRepo.update(id, { status: 'biding' });
// send message event
this.notificationService.emitBidStatus({ ...bid, status: 'biding' });
this.emitAllBidEvent();
return AppResponse.toResponse(true);
}
async clientUpdate(
id: Bid['id'],
{ close_time, model, ...data }: ClientUpdateBidDto, // Nhận dữ liệu cập nhật
) {
// Tìm kiếm phiên đấu giá trong database theo id
const bid = await this.bidsRepo.findOne({
where: { id },
relations: { histories: true, web_bid: true },
order: {
histories: {
price: 'DESC',
},
},
});
// Nếu không tìm thấy phiên đấu giá, trả về lỗi 404
if (!bid)
throw new NotFoundException(
AppResponse.toResponse(null, {
message: 'Not found bid',
status_code: HttpStatus.NOT_FOUND,
}),
);
// Nếu phiên đấu giá chưa có thời gian bắt đầu và kết thúc
if (!bid.close_time && !bid.start_bid_time) {
// Thiết lập thời gian bắt đầu là 5 phút trước khi đóng
// bid.start_bid_time = new Date().toUTCString();
bid.start_bid_time = subtractMinutes(
close_time,
bid.web_bid.arrival_offset_seconds / 60,
);
}
// Kiểm tra nếu thời gian đóng bid đã đạt tới (tức phiên đấu giá đã kết thúc)
if (bid.close_time && isTimeReached(bid.close_time)) {
const bidHistoriesItem = bid.histories[0]; // Lấy lịch sử bid gần nhất (mới nhất)
if (
!bidHistoriesItem ||
// Nếu giá cuối cùng không phải là giá của người dùng và giá hiện tại vượt quá mức người dùng đặt + bước giá
(bidHistoriesItem.price !== data.current_price &&
data.current_price > bid.max_price + bid.plus_price)
) {
bid.status = 'out-bid'; // Người dùng đã bị outbid khi đấu giá kết thúc
} else {
bid.status = 'win-bid'; // Người dùng là người thắng nếu không bị outbid
}
} else {
// Nếu phiên đấu giá vẫn đang diễn ra và giá hiện tại vượt quá giới hạn đặt của người dùng
if (
data.current_price > bid.max_price + bid.plus_price ||
(!bid.histories.length &&
data.reserve_price > bid.max_price + bid.plus_price)
) {
bid.status = 'out-bid'; // Gán trạng thái là đã bị outbid
}
}
// Cập nhật thời gian kết thúc đấu giá nếu `close_time` mới lớn hơn `close_time` cũ
if (
close_time &&
new Date(close_time).getTime() > new Date(bid.close_time).getTime()
) {
bid.close_time = close_time;
}
// Nếu chưa có `model` nhưng dữ liệu mới có model, thì cập nhật model
if (model && !bid.model) {
bid.model = model;
}
// Lưu cập nhật vào database
const result = await this.bidsRepo.save({
...bid,
...data,
current_price: Math.max(data?.current_price || 0, bid.current_price),
updated_at: new Date(), // Cập nhật timestamp
});
// Phát sự kiện cập nhật toàn bộ danh sách đấu giá
this.emitAllBidEvent();
// send event message
// Nếu trạng thái của bid là 'out-bid', gửi thông báo
if (['out-bid', 'win-bid'].includes(result.status)) {
this.notificationService.emitBidStatus(result);
}
// Trả về kết quả cập nhật dưới dạng response chuẩn
return AppResponse.toResponse(plainToClass(Bid, result));
}
async outBid(id: Bid['id']) {
const result = await this.bidsRepo.update(id, { status: 'out-bid' });
const bid = await this.bidsRepo.findOne({ where: { id } });
if (!result) throw new BadRequestException(AppResponse.toResponse(false));
await this.emitAllBidEvent();
// send message event
this.notificationService.emitBidStatus(bid);
return AppResponse.toResponse(true);
}
async delete(id: Bid['id']) {
const bid = await this.bidsRepo.findOne({ where: { id } });
if (!bid)
throw new NotFoundException(
AppResponse.toResponse(false, {
message: 'Bid is not found',
status_code: HttpStatus.NOT_FOUND,
}),
);
await this.bidsRepo.delete({ id: bid.id });
if (bid.status === 'biding') {
this.emitAllBidEvent();
}
return AppResponse.toResponse(true, { message: 'Delete success !' });
}
async deletes(ids: Bid['id'][]) {
const result = await this.bidsRepo.delete({
id: In(ids),
});
if (!result.affected) {
throw new BadRequestException(
AppResponse.toResponse(false, {
message: 'No items have been deleted yet.',
status_code: HttpStatus.BAD_REQUEST,
}),
);
}
this.emitAllBidEvent();
return AppResponse.toResponse(true, { message: 'Delete success !' });
}
async updateStatusByPrice(id: Bid['id'], data: UpdateStatusByPriceDto) {
const bid = await this.bidsRepo.findOne({ where: { id } });
if (!bid)
throw new NotFoundException(
AppResponse.toResponse(false, {
message: 'Bid is not found',
status_code: HttpStatus.NOT_FOUND,
}),
);
if (!isTimeReached(bid.close_time)) {
throw new BadRequestException(
AppResponse.toResponse(false, { message: 'Product is opening' }),
);
}
const lastHistory = await this.bidHistoriesRepo.findOne({
where: { bid: { id: bid.id } },
order: { created_at: 'DESC' },
});
if (lastHistory && lastHistory.price === data.current_price) {
if (bid.status !== 'win-bid') {
await this.bidsRepo.update(bid.id, { status: 'win-bid' });
// send event message
this.notificationService.emitBidStatus({
...bid,
status: 'win-bid',
});
}
} else {
if (bid.status !== 'out-bid') {
await this.bidsRepo.update(bid.id, { status: 'out-bid' });
// send event message
this.notificationService.emitBidStatus({
...bid,
status: 'out-bid',
});
}
}
this.emitAllBidEvent();
return AppResponse.toResponse(true);
}
async updateStatusWork(
id: Bid['id'],
type: string,
image: Express.Multer.File,
) {
if (!image) {
throw new BadRequestException(
AppResponse.toResponse(null, { message: 'File or Url is required' }),
);
}
const data: { filename: string } | undefined =
await new ImageCompressionPipe(
`${Constant.WORK_IMAGES_FOLDER}/${type.replaceAll('_', '-').toLocaleLowerCase()}/${id}`,
{},
{
unique_image_folder: true,
unique_name: true,
},
).transform(image);
if (!data)
throw new BadRequestException(
AppResponse.toResponse(null, {
message: "Can't create media",
status_code: HttpStatus.BAD_REQUEST,
}),
);
// update time snapshot for API BID
if (type === 'API_BID') {
this.webBidsService.webBidRepo.update(id, { snapshot_at: new Date() });
}
this.eventEmitter.emit(`working`, {
status: 're-update',
id,
type,
filename: data.filename,
});
return AppResponse.toResponse(true);
}
async getStatusWorkingImage(
id: Bid['id'],
type: string,
name: string,
res: Response,
) {
const rootDir = process.cwd();
const folderPath = join(
rootDir,
`${Constant.MEDIA_PATH}/${Constant.WORK_IMAGES_FOLDER}/${type}/${id}`,
);
if (!existsSync(folderPath)) {
throw new NotFoundException(
AppResponse.toResponse(null, {
message: 'Folder not found',
status_code: HttpStatus.NOT_FOUND,
}),
);
}
let filePath: string;
if (name === Event.WORKING) {
const files = readdirSync(folderPath).filter((file) =>
/\.(jpg|jpeg|png|webp)$/i.test(file),
);
if (files.length === 0) {
throw new NotFoundException(
AppResponse.toResponse(null, {
message: 'No images found',
status_code: HttpStatus.NOT_FOUND,
}),
);
}
// Lấy ảnh đầu tiên trong danh sách
filePath = join(folderPath, files[0]);
} else {
// Nếu name khác 'working', lấy file cụ thể
filePath = join(folderPath, `${name}`);
if (!existsSync(filePath)) {
throw new NotFoundException(
AppResponse.toResponse(null, {
message: 'Image not found',
status_code: HttpStatus.NOT_FOUND,
}),
);
}
}
return res.sendFile(filePath);
}
async getImagesWorking(id: Bid['id'], type: string) {
const rootDir = process.cwd();
const folderPath = join(
rootDir,
`${Constant.MEDIA_PATH}/${Constant.WORK_IMAGES_FOLDER}/${type}/${id}`,
);
if (!existsSync(folderPath)) {
throw new NotFoundException(
AppResponse.toResponse(null, {
message: 'Folder not found',
status_code: HttpStatus.NOT_FOUND,
}),
);
}
// Lấy danh sách file trong folderPath
const files = readdirSync(folderPath);
return AppResponse.toResponse(files);
}
async emitLoginStatus(data: ClientUpdateLoginStatusDto) {
this.eventEmitter.emit(Event.statusLogin(data.data), data);
return AppResponse.toResponse(true);
}
}