notification
This commit is contained in:
parent
46979c6b9e
commit
88bf1cabb6
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"id": -1002593407119,
|
||||||
|
"title": "Bid histories dev",
|
||||||
|
"type": "supergroup",
|
||||||
|
"invite_link": "https://t.me/+CSBIA7mbyBhkM2Jl",
|
||||||
|
"permissions": {
|
||||||
|
"can_send_messages": true,
|
||||||
|
"can_send_media_messages": true,
|
||||||
|
"can_send_audios": true,
|
||||||
|
"can_send_documents": true,
|
||||||
|
"can_send_photos": true,
|
||||||
|
"can_send_videos": true,
|
||||||
|
"can_send_video_notes": true,
|
||||||
|
"can_send_voice_notes": true,
|
||||||
|
"can_send_polls": true,
|
||||||
|
"can_send_other_messages": true,
|
||||||
|
"can_add_web_page_previews": true,
|
||||||
|
"can_change_info": true,
|
||||||
|
"can_invite_users": true,
|
||||||
|
"can_pin_messages": true,
|
||||||
|
"can_manage_topics": true
|
||||||
|
},
|
||||||
|
"join_to_send_messages": true,
|
||||||
|
"max_reaction_count": 11,
|
||||||
|
"accent_color_id": 2
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"createdAt":1743133400720}
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
} from './system/routes/exclude-route';
|
} from './system/routes/exclude-route';
|
||||||
import { AuthorizationMiddleware } from './modules/admins/middlewares/authorization.middleware';
|
import { AuthorizationMiddleware } from './modules/admins/middlewares/authorization.middleware';
|
||||||
import { ClientAuthenticationMiddleware } from './modules/auth/middlewares/client-authentication.middleware';
|
import { ClientAuthenticationMiddleware } from './modules/auth/middlewares/client-authentication.middleware';
|
||||||
|
import { NotificationModule } from './modules/notification/notification.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -22,6 +23,7 @@ import { ClientAuthenticationMiddleware } from './modules/auth/middlewares/clien
|
||||||
AppValidatorsModule,
|
AppValidatorsModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
AdminsModule,
|
AdminsModule,
|
||||||
|
NotificationModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
}),
|
}),
|
||||||
|
EventEmitterModule.forRoot({
|
||||||
|
wildcard: true,
|
||||||
|
global: true,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppConfigsModule {}
|
export class AppConfigsModule {}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,18 @@ 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';
|
import { SendMessageHistoriesService } from '../services/send-message-histories.service';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { Constant } from '../utils/constant';
|
||||||
|
import {
|
||||||
|
existsSync,
|
||||||
|
mkdir,
|
||||||
|
mkdirSync,
|
||||||
|
readdirSync,
|
||||||
|
readFileSync,
|
||||||
|
statSync,
|
||||||
|
writeFile,
|
||||||
|
writeFileSync,
|
||||||
|
} from 'fs';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BotTelegramApi {
|
export class BotTelegramApi {
|
||||||
|
|
@ -64,6 +76,73 @@ export class BotTelegramApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createFolderPath(): Promise<string> {
|
||||||
|
const rootDir = process.cwd();
|
||||||
|
const folderPath = join(rootDir, `${Constant.BOT_TELEGRAM_PATH}`);
|
||||||
|
|
||||||
|
if (!existsSync(folderPath)) {
|
||||||
|
mkdirSync(folderPath, { recursive: true, mode: 0o777 });
|
||||||
|
|
||||||
|
// ✅ Lưu metadata lần đầu
|
||||||
|
const metadataPath = join(folderPath, 'metadata.json');
|
||||||
|
writeFileSync(
|
||||||
|
metadataPath,
|
||||||
|
JSON.stringify({ createdAt: Date.now() }),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return folderPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGroupInfo(
|
||||||
|
chatId: string = this.configService.get<string>('CHAT_ID'),
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const folderPath = await this.createFolderPath();
|
||||||
|
const metadataPath = join(folderPath, 'metadata.json');
|
||||||
|
const dataFilePath = join(folderPath, `group_${chatId}.json`);
|
||||||
|
|
||||||
|
// 10 minute
|
||||||
|
const TIME_TO_REFRESH_DATA = 10;
|
||||||
|
|
||||||
|
if (existsSync(metadataPath)) {
|
||||||
|
const metadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
|
||||||
|
const createdAt = metadata?.createdAt || 0;
|
||||||
|
const now = Date.now();
|
||||||
|
const diffMinutes = (now - createdAt) / 60000;
|
||||||
|
|
||||||
|
if (diffMinutes < TIME_TO_REFRESH_DATA && existsSync(dataFilePath)) {
|
||||||
|
return JSON.parse(readFileSync(dataFilePath, 'utf-8'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${this.apiUrl}/getChat`;
|
||||||
|
const { data } = await axios({
|
||||||
|
url,
|
||||||
|
params: { chat_id: chatId },
|
||||||
|
family: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data?.ok) {
|
||||||
|
writeFileSync(
|
||||||
|
dataFilePath,
|
||||||
|
JSON.stringify(data.result, null, 2),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
writeFileSync(
|
||||||
|
metadataPath,
|
||||||
|
JSON.stringify({ createdAt: Date.now() }),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.result;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error || error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async sendBidInfo(bid: Bid): Promise<boolean> {
|
async sendBidInfo(bid: Bid): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const text = this.formatBidMessage(bid);
|
const text = this.formatBidMessage(bid);
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import { AdminSendMessageHistoriesController } from './controllers/admin/admin-s
|
||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
import { AdminsModule } from '../admins/admins.module';
|
import { AdminsModule } from '../admins/admins.module';
|
||||||
import { AdminBidGateway } from './getways/admin-bid-getway';
|
import { AdminBidGateway } from './getways/admin-bid-getway';
|
||||||
|
import { NotificationModule } from '../notification/notification.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -35,11 +36,9 @@ import { AdminBidGateway } from './getways/admin-bid-getway';
|
||||||
WebBid,
|
WebBid,
|
||||||
SendMessageHistory,
|
SendMessageHistory,
|
||||||
]),
|
]),
|
||||||
EventEmitterModule.forRoot({
|
|
||||||
wildcard: true,
|
|
||||||
}),
|
|
||||||
// AuthModule,
|
// AuthModule,
|
||||||
AdminsModule,
|
AdminsModule,
|
||||||
|
NotificationModule,
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
BidsController,
|
BidsController,
|
||||||
|
|
@ -62,5 +61,6 @@ import { AdminBidGateway } from './getways/admin-bid-getway';
|
||||||
GraysApi,
|
GraysApi,
|
||||||
SendMessageHistoriesService,
|
SendMessageHistoriesService,
|
||||||
],
|
],
|
||||||
|
exports: [BotTelegramApi],
|
||||||
})
|
})
|
||||||
export class BidsModule {}
|
export class BidsModule {}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,16 @@
|
||||||
|
import { AdminsService } from '@/modules/admins/services/admins.service';
|
||||||
|
import { getWayMiddleware } from '@/modules/auth/middlewares/get-way.middleware';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import {
|
import {
|
||||||
|
OnGatewayConnection,
|
||||||
WebSocketGateway,
|
WebSocketGateway,
|
||||||
WebSocketServer,
|
WebSocketServer,
|
||||||
SubscribeMessage,
|
|
||||||
OnGatewayConnection,
|
|
||||||
} from '@nestjs/websockets';
|
} from '@nestjs/websockets';
|
||||||
import { Server, Socket } from 'socket.io';
|
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
||||||
import { BidsService } from '../services/bids.service';
|
|
||||||
import { WebBidsService } from '../services/web-bids.service';
|
|
||||||
import { plainToClass } from 'class-transformer';
|
import { plainToClass } from 'class-transformer';
|
||||||
|
import { Server, Socket } from 'socket.io';
|
||||||
import { WebBid } from '../entities/wed-bid.entity';
|
import { WebBid } from '../entities/wed-bid.entity';
|
||||||
import * as cookie from 'cookie';
|
import { WebBidsService } from '../services/web-bids.service';
|
||||||
import { Constant } from '@/modules/auth/ultils/constant';
|
|
||||||
import { getWayMiddleware } from '@/modules/auth/middlewares/get-way.middleware';
|
|
||||||
import { AdminsService } from '@/modules/admins/services/admins.service';
|
|
||||||
import { JwtService } from '@nestjs/jwt';
|
|
||||||
|
|
||||||
@WebSocketGateway({
|
@WebSocketGateway({
|
||||||
namespace: 'admin-bid-ws',
|
namespace: 'admin-bid-ws',
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ 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';
|
import { SendMessageHistoriesService } from './send-message-histories.service';
|
||||||
|
import { NotificationService } from '@/modules/notification/notification.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BidHistoriesService {
|
export class BidHistoriesService {
|
||||||
|
|
@ -23,6 +24,7 @@ export class BidHistoriesService {
|
||||||
readonly bidsRepo: Repository<Bid>,
|
readonly bidsRepo: Repository<Bid>,
|
||||||
private readonly botTelegramApi: BotTelegramApi,
|
private readonly botTelegramApi: BotTelegramApi,
|
||||||
readonly sendMessageHistoriesService: SendMessageHistoriesService,
|
readonly sendMessageHistoriesService: SendMessageHistoriesService,
|
||||||
|
private readonly notificationService: NotificationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async index() {
|
async index() {
|
||||||
|
|
@ -54,6 +56,9 @@ export class BidHistoriesService {
|
||||||
if (price + bid.plus_price > bid.max_price) {
|
if (price + bid.plus_price > bid.max_price) {
|
||||||
this.bidsRepo.update(bid_id, { status: 'out-bid' });
|
this.bidsRepo.update(bid_id, { status: 'out-bid' });
|
||||||
|
|
||||||
|
// send message event
|
||||||
|
this.notificationService.emitBidStatus({ ...bid, status: 'out-bid' });
|
||||||
|
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
AppResponse.toResponse(null, {
|
AppResponse.toResponse(null, {
|
||||||
message: 'Price is more than Max price ' + bid.max_price,
|
message: 'Price is more than Max price ' + bid.max_price,
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import { Bid } from '../entities/bid.entity';
|
||||||
import { ImageCompressionPipe } from '../pipes/image-compression-pipe';
|
import { ImageCompressionPipe } from '../pipes/image-compression-pipe';
|
||||||
import { Constant } from '../utils/constant';
|
import { Constant } from '../utils/constant';
|
||||||
import { WebBidsService } from './web-bids.service';
|
import { WebBidsService } from './web-bids.service';
|
||||||
|
import { NotificationService } from '@/modules/notification/notification.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BidsService {
|
export class BidsService {
|
||||||
|
|
@ -39,6 +40,7 @@ export class BidsService {
|
||||||
readonly bidHistoriesRepo: Repository<BidHistory>,
|
readonly bidHistoriesRepo: Repository<BidHistory>,
|
||||||
private readonly webBidsService: WebBidsService,
|
private readonly webBidsService: WebBidsService,
|
||||||
private eventEmitter: EventEmitter2,
|
private eventEmitter: EventEmitter2,
|
||||||
|
private notificationService: NotificationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async index(query: PaginateQuery) {
|
async index(query: PaginateQuery) {
|
||||||
|
|
@ -159,6 +161,9 @@ export class BidsService {
|
||||||
|
|
||||||
this.emitAllBidEvent();
|
this.emitAllBidEvent();
|
||||||
|
|
||||||
|
// send message event
|
||||||
|
this.notificationService.emitBidStatus({ ...bid, status: 'out-bid' });
|
||||||
|
|
||||||
return AppResponse.toResponse(true);
|
return AppResponse.toResponse(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,6 +188,9 @@ export class BidsService {
|
||||||
|
|
||||||
await this.bidsRepo.update(id, { status: 'biding' });
|
await this.bidsRepo.update(id, { status: 'biding' });
|
||||||
|
|
||||||
|
// send message event
|
||||||
|
this.notificationService.emitBidStatus({ ...bid, status: 'biding' });
|
||||||
|
|
||||||
this.emitAllBidEvent();
|
this.emitAllBidEvent();
|
||||||
return AppResponse.toResponse(true);
|
return AppResponse.toResponse(true);
|
||||||
}
|
}
|
||||||
|
|
@ -210,12 +218,6 @@ export class BidsService {
|
||||||
data.current_price >= bid.max_price + bid.plus_price ||
|
data.current_price >= bid.max_price + bid.plus_price ||
|
||||||
(bid.close_time && isTimeReached(bid.close_time))
|
(bid.close_time && isTimeReached(bid.close_time))
|
||||||
) {
|
) {
|
||||||
console.log({
|
|
||||||
a: data.current_price >= bid.max_price + bid.plus_price,
|
|
||||||
b: bid.close_time && !close_time,
|
|
||||||
c: bid.close_time && isTimeReached(bid.close_time),
|
|
||||||
});
|
|
||||||
|
|
||||||
bid.status = 'out-bid';
|
bid.status = 'out-bid';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -234,16 +236,26 @@ export class BidsService {
|
||||||
|
|
||||||
this.emitAllBidEvent();
|
this.emitAllBidEvent();
|
||||||
|
|
||||||
|
// send event message
|
||||||
|
if (result.status === 'out-bid') {
|
||||||
|
this.notificationService.emitBidStatus(result);
|
||||||
|
}
|
||||||
|
|
||||||
return AppResponse.toResponse(plainToClass(Bid, result));
|
return AppResponse.toResponse(plainToClass(Bid, result));
|
||||||
}
|
}
|
||||||
|
|
||||||
async outBid(id: Bid['id']) {
|
async outBid(id: Bid['id']) {
|
||||||
const result = await this.bidsRepo.update(id, { status: 'out-bid' });
|
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));
|
if (!result) throw new BadRequestException(AppResponse.toResponse(false));
|
||||||
|
|
||||||
await this.emitAllBidEvent();
|
await this.emitAllBidEvent();
|
||||||
|
|
||||||
|
// send message event
|
||||||
|
this.notificationService.emitBidStatus(bid);
|
||||||
|
|
||||||
return AppResponse.toResponse(true);
|
return AppResponse.toResponse(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -309,9 +321,25 @@ export class BidsService {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (lastHistory && lastHistory.price === data.current_price) {
|
if (lastHistory && lastHistory.price === data.current_price) {
|
||||||
|
if (bid.status !== 'win-bid') {
|
||||||
await this.bidsRepo.update(bid.id, { status: 'win-bid' });
|
await this.bidsRepo.update(bid.id, { status: 'win-bid' });
|
||||||
|
|
||||||
|
// send event message
|
||||||
|
this.notificationService.emitBidStatus({
|
||||||
|
...bid,
|
||||||
|
status: 'win-bid',
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (bid.status !== 'out-bid') {
|
||||||
await this.bidsRepo.update(bid.id, { status: 'out-bid' });
|
await this.bidsRepo.update(bid.id, { status: 'out-bid' });
|
||||||
|
|
||||||
|
// send event message
|
||||||
|
this.notificationService.emitBidStatus({
|
||||||
|
...bid,
|
||||||
|
status: 'out-bid',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emitAllBidEvent();
|
this.emitAllBidEvent();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
export class Constant {
|
export class Constant {
|
||||||
public static MEDIA_PATH = 'public';
|
public static MEDIA_PATH = 'public';
|
||||||
|
public static BOT_TELEGRAM_PATH = 'bot-data';
|
||||||
|
|
||||||
public static WORK_IMAGES_FOLDER = 'work-images';
|
public static WORK_IMAGES_FOLDER = 'work-images';
|
||||||
public static TMP_FOLDER = 'tmp';
|
public static TMP_FOLDER = 'tmp';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const NAME_EVENTS = {
|
||||||
|
BID_STATUS: 'notify.bid-status',
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export class CreateNotificationDto {}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { PartialType } from '@nestjs/mapped-types';
|
||||||
|
import { CreateNotificationDto } from './create-notification.dto';
|
||||||
|
|
||||||
|
export class UpdateNotificationDto extends PartialType(CreateNotificationDto) {}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { Timestamp } from './timestamp';
|
||||||
|
|
||||||
|
@Entity('notifications')
|
||||||
|
@Index(['message', 'raw_data'])
|
||||||
|
export class Notification extends Timestamp {
|
||||||
|
@PrimaryGeneratedColumn('increment')
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar' })
|
||||||
|
raw_data: string;
|
||||||
|
|
||||||
|
@Column({ default: null, nullable: true })
|
||||||
|
read_at: Date | null;
|
||||||
|
|
||||||
|
@Column({ type: 'text' })
|
||||||
|
send_to: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
export abstract class Timestamp {
|
||||||
|
@CreateDateColumn({ type: 'timestamp', name: 'created_at' })
|
||||||
|
created_at: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ type: 'timestamp', name: 'updated_at' })
|
||||||
|
updated_at: Date;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import { NAME_EVENTS } from '../constants';
|
||||||
|
import { Bid } from '@/modules/bids/entities/bid.entity';
|
||||||
|
import { Notification } from '../entities/notification.entity';
|
||||||
|
import { BotTelegramApi } from '@/modules/bids/apis/bot-telegram.api';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdminNotificationListener {
|
||||||
|
constructor(private readonly botTelegramApi: BotTelegramApi) {}
|
||||||
|
|
||||||
|
@OnEvent(NAME_EVENTS.BID_STATUS)
|
||||||
|
handleBidStatus({
|
||||||
|
bid,
|
||||||
|
notification,
|
||||||
|
}: {
|
||||||
|
bid: Bid;
|
||||||
|
notification: Notification;
|
||||||
|
}) {
|
||||||
|
if (JSON.parse(notification.send_to).length <= 0) return;
|
||||||
|
|
||||||
|
this.botTelegramApi.sendMessage(notification.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { NotificationController } from './notification.controller';
|
||||||
|
import { NotificationService } from './notification.service';
|
||||||
|
|
||||||
|
describe('NotificationController', () => {
|
||||||
|
let controller: NotificationController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [NotificationController],
|
||||||
|
providers: [NotificationService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<NotificationController>(NotificationController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { BotTelegramApi } from '../bids/apis/bot-telegram.api';
|
||||||
|
import { NotificationService } from './notification.service';
|
||||||
|
|
||||||
|
@Controller('admin/notifications')
|
||||||
|
export class NotificationController {
|
||||||
|
constructor(
|
||||||
|
private readonly notificationService: NotificationService,
|
||||||
|
private botTelegramApi: BotTelegramApi,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get('')
|
||||||
|
async test() {
|
||||||
|
return await this.botTelegramApi.getGroupInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { forwardRef, Module } from '@nestjs/common';
|
||||||
|
import { NotificationService } from './notification.service';
|
||||||
|
import { NotificationController } from './notification.controller';
|
||||||
|
import { BidsModule } from '../bids/bids.module';
|
||||||
|
import { Notification } from './entities/notification.entity';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AdminNotificationListener } from './listeners/admin-notification.listener';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
forwardRef(() => BidsModule),
|
||||||
|
TypeOrmModule.forFeature([Notification]),
|
||||||
|
],
|
||||||
|
controllers: [NotificationController],
|
||||||
|
providers: [NotificationService, AdminNotificationListener],
|
||||||
|
exports: [NotificationService],
|
||||||
|
})
|
||||||
|
export class NotificationModule {}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { NotificationService } from './notification.service';
|
||||||
|
|
||||||
|
describe('NotificationService', () => {
|
||||||
|
let service: NotificationService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [NotificationService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<NotificationService>(NotificationService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { Bid } from '../bids/entities/bid.entity';
|
||||||
|
import { NAME_EVENTS } from './constants';
|
||||||
|
import { BotTelegramApi } from '../bids/apis/bot-telegram.api';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Notification } from './entities/notification.entity';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { isTimeReached } from '@/ultils';
|
||||||
|
import {
|
||||||
|
FilterOperator,
|
||||||
|
FilterSuffix,
|
||||||
|
paginate,
|
||||||
|
PaginateQuery,
|
||||||
|
} from 'nestjs-paginate';
|
||||||
|
import { Column } from 'nestjs-paginate/lib/helper';
|
||||||
|
import AppResponse from '@/response/app-response';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class NotificationService {
|
||||||
|
constructor(
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
|
private readonly botTelegramApi: BotTelegramApi,
|
||||||
|
@InjectRepository(Notification)
|
||||||
|
readonly notificationRepo: Repository<Notification>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async index(query: PaginateQuery) {
|
||||||
|
const filterableColumns: {
|
||||||
|
[key in Column<Bid> | (string & {})]?:
|
||||||
|
| (FilterOperator | FilterSuffix)[]
|
||||||
|
| true;
|
||||||
|
} = {
|
||||||
|
id: true,
|
||||||
|
message: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
query.filter = AppResponse.processFilters(query.filter, filterableColumns);
|
||||||
|
|
||||||
|
const data = await paginate(query, this.notificationRepo, {
|
||||||
|
sortableColumns: ['id'],
|
||||||
|
searchableColumns: ['id'],
|
||||||
|
defaultLimit: 15,
|
||||||
|
filterableColumns,
|
||||||
|
defaultSortBy: [['id', 'DESC']],
|
||||||
|
maxLimit: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
return AppResponse.toPagination<Notification>(data, true, Notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
getBidStatusMessage(bid: Bid): string | null {
|
||||||
|
switch (bid.status) {
|
||||||
|
case 'biding':
|
||||||
|
return !bid.name ? null : `✅ The item has been activated. ${bid.name}`;
|
||||||
|
|
||||||
|
case 'out-bid':
|
||||||
|
if (isTimeReached(bid.close_time)) {
|
||||||
|
return `⏳ The auction for *${bid.name || 'this item'}* has ended.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemName = `*${bid.name || 'the item'}*`;
|
||||||
|
|
||||||
|
if (
|
||||||
|
bid.max_price + bid.plus_price <= bid.current_price ||
|
||||||
|
bid.reserve_price > bid.max_price + bid.plus_price
|
||||||
|
) {
|
||||||
|
return `💰 The current bid for ${itemName} has exceeded your maximum bid.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `🛑 The auction for ${itemName} has been canceled.`;
|
||||||
|
|
||||||
|
case 'win-bid':
|
||||||
|
return `🎉 Congratulations! You won the auction for ${itemName} at *${bid.current_price}*.`;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return '❓ Unknown auction status.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async emitBidStatus(bid: Bid, sendTo: boolean = true) {
|
||||||
|
const groupData = await this.botTelegramApi.getGroupInfo();
|
||||||
|
const sendToData = groupData && sendTo ? [groupData?.title || 'None'] : [];
|
||||||
|
|
||||||
|
const message = this.getBidStatusMessage(bid);
|
||||||
|
if (!message) return;
|
||||||
|
|
||||||
|
const notification = await this.notificationRepo.save({
|
||||||
|
message,
|
||||||
|
raw_data: JSON.stringify({
|
||||||
|
id: bid.id,
|
||||||
|
status: bid.status,
|
||||||
|
name: bid.name,
|
||||||
|
close_time: bid.close_time,
|
||||||
|
current_price: bid.current_price,
|
||||||
|
}),
|
||||||
|
send_to: JSON.stringify(sendToData),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventEmitter.emit(NAME_EVENTS.BID_STATUS, {
|
||||||
|
bid: {
|
||||||
|
...bid,
|
||||||
|
status: 'out-bid',
|
||||||
|
},
|
||||||
|
notification,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue