942 lines
26 KiB
TypeScript
942 lines
26 KiB
TypeScript
import { NotificationService } from '@/modules/notification/notification.service';
|
|
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 * as fs from 'fs';
|
|
import { existsSync, readdirSync } from 'fs';
|
|
import * as _ from 'lodash';
|
|
import { v4 as uuid } from 'uuid';
|
|
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,
|
|
isTimePassedByMinutes,
|
|
isTimeReached,
|
|
parseVideoFileName,
|
|
subtractMinutes,
|
|
} from 'src/ultils';
|
|
import { In, IsNull, Not, Repository } from 'typeorm';
|
|
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 { 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 { BidMetadata } from '../entities/bid-metadata.entity';
|
|
import { Bid } from '../entities/bid.entity';
|
|
import { ImageCompressionPipe } from '../pipes/image-compression-pipe';
|
|
import { Constant } from '../utils/constant';
|
|
import { Event } from '../utils/events';
|
|
import { BidMetadatasService } from './bid-metadatas.service';
|
|
import { WebBidsService } from './web-bids.service';
|
|
|
|
@Injectable()
|
|
export class BidsService {
|
|
constructor(
|
|
@InjectRepository(Bid)
|
|
readonly bidsRepo: Repository<Bid>,
|
|
@InjectRepository(BidHistory)
|
|
readonly bidHistoriesRepo: Repository<BidHistory>,
|
|
private readonly webBidsService: WebBidsService,
|
|
private readonly eventEmitter: EventEmitter2,
|
|
private readonly notificationService: NotificationService,
|
|
private readonly bidMetadatasService: BidMetadatasService,
|
|
) {}
|
|
|
|
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',
|
|
'close_time_ts',
|
|
'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,
|
|
metadata: 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, response?: (result: Bid) => any) {
|
|
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);
|
|
|
|
let metadata = BidMetadata.DEFAULT_META_DATA(webBid);
|
|
|
|
if (data.metadata) {
|
|
metadata = metadata.map((item) => {
|
|
const reqData = data.metadata.find(
|
|
(i) => i?.key_name === item.key_name,
|
|
);
|
|
|
|
if (!reqData) return { ...item };
|
|
|
|
return {
|
|
...item,
|
|
value:
|
|
item.key_name === BidMetadata.MODE_KEY
|
|
? JSON.stringify(reqData.value)
|
|
: reqData.value <= 0
|
|
? item.value
|
|
: String(reqData.value),
|
|
};
|
|
});
|
|
}
|
|
|
|
const result = await this.bidsRepo.save({
|
|
...data,
|
|
model,
|
|
web_bid: webBid,
|
|
metadata,
|
|
});
|
|
|
|
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),
|
|
);
|
|
}
|
|
|
|
async update(id: Bid['id'], { metadata = [], ...data }: UpdateBidDto) {
|
|
const prev = await this.bidsRepo.findOne({
|
|
where: { id },
|
|
relations: { web_bid: true, metadata: true },
|
|
});
|
|
|
|
if (!prev) {
|
|
throw new NotFoundException(
|
|
AppResponse.toResponse(false, {
|
|
message: 'Product not found',
|
|
status_code: HttpStatus.NOT_FOUND,
|
|
}),
|
|
);
|
|
}
|
|
|
|
if (metadata) {
|
|
await this.bidMetadatasService.bidMetadataRepo.upsert(
|
|
metadata.map((item) => {
|
|
return { ...item, bid: { id }, value: JSON.stringify(item.value) };
|
|
}),
|
|
['id', 'bid', 'key_name'],
|
|
);
|
|
}
|
|
|
|
let result = null;
|
|
if (!prev.close_time) {
|
|
// Trường hợp chưa có close_time => update đơn giản
|
|
result = await this.bidsRepo.update(id, {
|
|
...data,
|
|
});
|
|
} else {
|
|
// Trường hợp đã có close_time => kiểm tra arrival_offset_seconds có thay đổi không
|
|
const arrival_offset_seconds =
|
|
this.bidMetadatasService.getArrivalOffsetSecondsByMode(
|
|
metadata as BidMetadata[],
|
|
);
|
|
const prev_arrival_offset_seconds =
|
|
this.bidMetadatasService.getArrivalOffsetSecondsByMode(prev.metadata);
|
|
|
|
if (arrival_offset_seconds !== prev_arrival_offset_seconds) {
|
|
// Nếu offset thay đổi thì cần cập nhật lại start_bid_time
|
|
const start_bid_time = arrival_offset_seconds
|
|
? subtractMinutes(prev.close_time, arrival_offset_seconds / 60)
|
|
: prev.start_bid_time;
|
|
|
|
result = await this.bidsRepo.update(id, {
|
|
...data,
|
|
start_bid_time,
|
|
});
|
|
} else {
|
|
result = await this.bidsRepo.update(id, {
|
|
...data,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (!result) throw new BadRequestException(false);
|
|
|
|
await this.emitAllBidEvent();
|
|
|
|
return AppResponse.toResponse(true);
|
|
}
|
|
|
|
async getBidForClientUpdate(id: Bid['id']) {
|
|
return await this.bidsRepo.findOne({
|
|
where: { id },
|
|
relations: { histories: true, web_bid: true, metadata: true },
|
|
order: {
|
|
histories: {
|
|
price: 'DESC',
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
async toggle(id: Bid['id']) {
|
|
const bid = await this.bidsRepo.findOne({
|
|
where: { id },
|
|
relations: { web_bid: true },
|
|
});
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Workflow
|
|
* START
|
|
* |
|
|
* |--> Tìm bid theo id --> Không có? --> Throw 404
|
|
* |
|
|
* |--> Nếu chưa có thời gian bắt đầu --> Tính offset --> Gán start_time
|
|
* |
|
|
* |--> Nếu đã hết giờ:
|
|
* | |--> Nếu outbid --> Gán status 'out-bid'
|
|
* | |--> Else --> Gán status 'win-bid'
|
|
* |
|
|
* |--> Nếu chưa hết giờ:
|
|
* | |--> Nếu vượt giới hạn --> Gán status 'out-bid'
|
|
* |
|
|
* |--> Nếu close_time mới > cũ --> cập nhật
|
|
* |--> Nếu có model mới và chưa có model --> gán
|
|
* |
|
|
* |--> Gọi `save(...)` để lưu lại DB
|
|
* |--> Nếu có metadata --> gọi `upsert`
|
|
* |
|
|
* |--> Gửi sự kiện emitAllBidEvent
|
|
* |--> Nếu status là out-bid hoặc win-bid --> gửi notification
|
|
* |
|
|
* * |--> Trả response
|
|
* END
|
|
*/
|
|
async clientUpdate(
|
|
id: Bid['id'],
|
|
{ close_time, model, metadata, ...data }: ClientUpdateBidDto, // Nhận dữ liệu cập nhật
|
|
) {
|
|
// // Tìm kiếm phiên đấu giá trong database theo id
|
|
// let bid = await this.bidsRepo.findOne({
|
|
// where: { id },
|
|
// relations: { histories: true, web_bid: true, metadata: true },
|
|
// order: {
|
|
// histories: {
|
|
// price: 'DESC',
|
|
// },
|
|
// },
|
|
// });
|
|
|
|
let bid = await this.getBidForClientUpdate(id);
|
|
|
|
// 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 && data?.lot_id) {
|
|
// Tách lấy arrival_offset_seconds trong product nếu không có lấy trong web bid.
|
|
const arrival_offset_seconds =
|
|
this.bidMetadatasService.getArrivalOffsetSecondsByMode(bid.metadata) ||
|
|
bid.web_bid.arrival_offset_seconds;
|
|
|
|
// 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,
|
|
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ũ
|
|
if (
|
|
close_time &&
|
|
new Date(close_time).getTime() > new Date(bid.close_time).getTime()
|
|
) {
|
|
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);
|
|
}
|
|
|
|
// 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;
|
|
// bid.close_time_ts = new Date(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
|
|
});
|
|
|
|
if (metadata) {
|
|
await this.bidMetadatasService.upsert(metadata, bid);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// 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));
|
|
}
|
|
|
|
async outBid(id: Bid['id']) {
|
|
const result = await this.bidsRepo.update(id, { status: 'out-bid' });
|
|
|
|
const bid = await this.bidsRepo.findOne({
|
|
where: { id },
|
|
relations: { web_bid: true },
|
|
});
|
|
|
|
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 },
|
|
relations: { web_bid: true },
|
|
});
|
|
|
|
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 uploadRecord(id: Bid['id'], video: Express.Multer.File) {
|
|
if (!video) {
|
|
throw new BadRequestException(
|
|
AppResponse.toResponse(null, { message: 'File or Url is required' }),
|
|
);
|
|
}
|
|
|
|
const data: { filename: string } | undefined =
|
|
await new ImageCompressionPipe(
|
|
`${Constant.RECORD_FOLDER}`,
|
|
{},
|
|
{
|
|
unique_image_folder: false,
|
|
unique_name: false,
|
|
},
|
|
).transform(video);
|
|
|
|
if (!data)
|
|
throw new BadRequestException(
|
|
AppResponse.toResponse(null, {
|
|
message: "Can't create media",
|
|
status_code: HttpStatus.BAD_REQUEST,
|
|
}),
|
|
);
|
|
|
|
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 getRecord(name: string, res: Response) {
|
|
const rootDir = process.cwd();
|
|
const filePath = join(
|
|
rootDir,
|
|
`${Constant.MEDIA_PATH}/${Constant.RECORD_FOLDER}/${name}`,
|
|
);
|
|
|
|
if (!existsSync(filePath)) {
|
|
throw new NotFoundException(
|
|
AppResponse.toResponse(null, {
|
|
message: 'Folder not found',
|
|
status_code: HttpStatus.NOT_FOUND,
|
|
}),
|
|
);
|
|
}
|
|
|
|
const stat = fs.statSync(filePath);
|
|
const fileSize = stat.size;
|
|
|
|
res.writeHead(200, {
|
|
'Content-Type': 'video/mp4',
|
|
'Content-Length': fileSize,
|
|
});
|
|
|
|
const readStream = fs.createReadStream(filePath);
|
|
readStream.pipe(res);
|
|
}
|
|
|
|
async getRecords(id: Bid['id']) {
|
|
const rootDir = process.cwd();
|
|
const folderPath = join(
|
|
rootDir,
|
|
`${Constant.MEDIA_PATH}/${Constant.RECORD_FOLDER}`,
|
|
);
|
|
|
|
if (!existsSync(folderPath)) {
|
|
throw new NotFoundException(
|
|
AppResponse.toResponse(null, {
|
|
message: 'Folder not found',
|
|
status_code: HttpStatus.NOT_FOUND,
|
|
}),
|
|
);
|
|
}
|
|
|
|
const files = await fs.promises.readdir(folderPath);
|
|
|
|
const data = files
|
|
.map((item) => {
|
|
return parseVideoFileName(item);
|
|
})
|
|
.filter((i) => i.bid_id == id);
|
|
|
|
const sorted = _.orderBy(data, ['timestamp'], ['desc']);
|
|
|
|
return AppResponse.toResponse(sorted);
|
|
}
|
|
|
|
async deleteRecord(name: string) {
|
|
const rootDir = process.cwd();
|
|
const filePath = join(
|
|
rootDir,
|
|
`${Constant.MEDIA_PATH}/${Constant.RECORD_FOLDER}/${name}`,
|
|
);
|
|
|
|
if (!existsSync(filePath)) {
|
|
throw new NotFoundException(
|
|
AppResponse.toResponse(null, {
|
|
message: 'File not found',
|
|
status_code: HttpStatus.NOT_FOUND,
|
|
}),
|
|
);
|
|
}
|
|
|
|
await fs.promises.unlink(filePath);
|
|
|
|
return AppResponse.toResponse(true);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
async getNextBid(): Promise<Bid | null> {
|
|
const all = await this.bidsRepo.find({
|
|
where: { status: 'biding', close_time: Not(IsNull()) },
|
|
relations: { web_bid: true },
|
|
});
|
|
|
|
const now = Date.now();
|
|
|
|
let nextBid = null;
|
|
let minDiff = Infinity;
|
|
|
|
for (const bid of all) {
|
|
const time = Date.parse(bid.close_time);
|
|
if (!isNaN(time) && time >= now) {
|
|
const diff = time - now;
|
|
if (diff < minDiff) {
|
|
minDiff = diff;
|
|
nextBid = bid;
|
|
}
|
|
}
|
|
}
|
|
|
|
return nextBid;
|
|
}
|
|
|
|
async getBidByModel(model: string) {
|
|
const bid = await this.bidsRepo.findOne({
|
|
where: { model },
|
|
relations: { metadata: true, web_bid: true },
|
|
select: {
|
|
web_bid: { arrival_offset_seconds: true, early_tracking_seconds: true },
|
|
},
|
|
});
|
|
|
|
if (!bid)
|
|
return AppResponse.toResponse(null, {
|
|
status_code: HttpStatus.NOT_FOUND,
|
|
});
|
|
|
|
return AppResponse.toResponse(plainToClass(Bid, bid));
|
|
}
|
|
|
|
async hookAction(
|
|
{ id, type }: { id: Bid['id']; type: 'action' | 'api' },
|
|
data: any,
|
|
) {
|
|
const bid = await this.bidsRepo.findOne({
|
|
where: { id },
|
|
relations: { metadata: true },
|
|
});
|
|
|
|
if (!bid)
|
|
throw new NotFoundException(
|
|
AppResponse.toResponse(null, { message: 'Not foud bid' }),
|
|
);
|
|
|
|
if (type === 'api') {
|
|
const result = JSON.parse(data?.data || {});
|
|
|
|
result['timestamp'] = new Date().getTime();
|
|
result['uuid'] = uuid();
|
|
|
|
const prevDemoResponse = this.bidMetadatasService.getDemoResponse(
|
|
bid.metadata,
|
|
);
|
|
|
|
console.log({ prevDemoResponse, result });
|
|
|
|
await this.bidMetadatasService.upsert(
|
|
{
|
|
[BidMetadata.DEMO_RESPONSE]:
|
|
JSON.stringify([result, ...(prevDemoResponse || [])]) ||
|
|
JSON.stringify([]),
|
|
},
|
|
bid,
|
|
);
|
|
} else {
|
|
const recordUrl = data?.record_url || '';
|
|
|
|
await this.bidMetadatasService.upsert(
|
|
{
|
|
[BidMetadata.LATEST_RECROD_LINK]: JSON.stringify(recordUrl),
|
|
},
|
|
bid,
|
|
);
|
|
}
|
|
|
|
const latestBidData = await this.bidsRepo.findOne({
|
|
where: { id },
|
|
relations: { metadata: true },
|
|
});
|
|
|
|
this.eventEmitter.emit(Event.BID_DEMO, { bid: latestBidData, type });
|
|
|
|
return AppResponse.toResponse(data);
|
|
}
|
|
}
|