Deploy to Staging #9
			
				
			
		
		
		
	| 
						 | 
				
			
			@ -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';
 | 
			
		||||
import { AuthorizationMiddleware } from './modules/admins/middlewares/authorization.middleware';
 | 
			
		||||
import { ClientAuthenticationMiddleware } from './modules/auth/middlewares/client-authentication.middleware';
 | 
			
		||||
import { NotificationModule } from './modules/notification/notification.module';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [
 | 
			
		||||
| 
						 | 
				
			
			@ -22,6 +23,7 @@ import { ClientAuthenticationMiddleware } from './modules/auth/middlewares/clien
 | 
			
		|||
    AppValidatorsModule,
 | 
			
		||||
    AuthModule,
 | 
			
		||||
    AdminsModule,
 | 
			
		||||
    NotificationModule,
 | 
			
		||||
  ],
 | 
			
		||||
  controllers: [],
 | 
			
		||||
  providers: [],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,16 @@
 | 
			
		|||
import { Module } from '@nestjs/common';
 | 
			
		||||
import { ConfigModule } from '@nestjs/config';
 | 
			
		||||
import { EventEmitterModule } from '@nestjs/event-emitter';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [
 | 
			
		||||
    ConfigModule.forRoot({
 | 
			
		||||
      isGlobal: true,
 | 
			
		||||
    }),
 | 
			
		||||
    EventEmitterModule.forRoot({
 | 
			
		||||
      wildcard: true,
 | 
			
		||||
      global: true,
 | 
			
		||||
    }),
 | 
			
		||||
  ],
 | 
			
		||||
})
 | 
			
		||||
export class AppConfigsModule {}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,18 @@ import { escapeMarkdownV2 } from 'src/ultils';
 | 
			
		|||
import { Bid } from '../entities/bid.entity';
 | 
			
		||||
import * as dayjs from 'dayjs';
 | 
			
		||||
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()
 | 
			
		||||
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> {
 | 
			
		||||
    try {
 | 
			
		||||
      const text = this.formatBidMessage(bid);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,6 +25,7 @@ import { AdminSendMessageHistoriesController } from './controllers/admin/admin-s
 | 
			
		|||
import { AuthModule } from '../auth/auth.module';
 | 
			
		||||
import { AdminsModule } from '../admins/admins.module';
 | 
			
		||||
import { AdminBidGateway } from './getways/admin-bid-getway';
 | 
			
		||||
import { NotificationModule } from '../notification/notification.module';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [
 | 
			
		||||
| 
						 | 
				
			
			@ -35,11 +36,9 @@ import { AdminBidGateway } from './getways/admin-bid-getway';
 | 
			
		|||
      WebBid,
 | 
			
		||||
      SendMessageHistory,
 | 
			
		||||
    ]),
 | 
			
		||||
    EventEmitterModule.forRoot({
 | 
			
		||||
      wildcard: true,
 | 
			
		||||
    }),
 | 
			
		||||
    // AuthModule,
 | 
			
		||||
    AdminsModule,
 | 
			
		||||
    NotificationModule,
 | 
			
		||||
  ],
 | 
			
		||||
  controllers: [
 | 
			
		||||
    BidsController,
 | 
			
		||||
| 
						 | 
				
			
			@ -62,5 +61,6 @@ import { AdminBidGateway } from './getways/admin-bid-getway';
 | 
			
		|||
    GraysApi,
 | 
			
		||||
    SendMessageHistoriesService,
 | 
			
		||||
  ],
 | 
			
		||||
  exports: [BotTelegramApi],
 | 
			
		||||
})
 | 
			
		||||
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 {
 | 
			
		||||
  OnGatewayConnection,
 | 
			
		||||
  WebSocketGateway,
 | 
			
		||||
  WebSocketServer,
 | 
			
		||||
  SubscribeMessage,
 | 
			
		||||
  OnGatewayConnection,
 | 
			
		||||
} 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 { Server, Socket } from 'socket.io';
 | 
			
		||||
import { WebBid } from '../entities/wed-bid.entity';
 | 
			
		||||
import * as cookie from 'cookie';
 | 
			
		||||
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';
 | 
			
		||||
import { WebBidsService } from '../services/web-bids.service';
 | 
			
		||||
 | 
			
		||||
@WebSocketGateway({
 | 
			
		||||
  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 { BotTelegramApi } from '../apis/bot-telegram.api';
 | 
			
		||||
import { SendMessageHistoriesService } from './send-message-histories.service';
 | 
			
		||||
import { NotificationService } from '@/modules/notification/notification.service';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class BidHistoriesService {
 | 
			
		||||
| 
						 | 
				
			
			@ -23,6 +24,7 @@ export class BidHistoriesService {
 | 
			
		|||
    readonly bidsRepo: Repository<Bid>,
 | 
			
		||||
    private readonly botTelegramApi: BotTelegramApi,
 | 
			
		||||
    readonly sendMessageHistoriesService: SendMessageHistoriesService,
 | 
			
		||||
    private readonly notificationService: NotificationService,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  async index() {
 | 
			
		||||
| 
						 | 
				
			
			@ -54,6 +56,9 @@ export class BidHistoriesService {
 | 
			
		|||
    if (price + bid.plus_price > bid.max_price) {
 | 
			
		||||
      this.bidsRepo.update(bid_id, { status: 'out-bid' });
 | 
			
		||||
 | 
			
		||||
      // send message event
 | 
			
		||||
      this.notificationService.emitBidStatus({ ...bid, status: 'out-bid' });
 | 
			
		||||
 | 
			
		||||
      throw new BadRequestException(
 | 
			
		||||
        AppResponse.toResponse(null, {
 | 
			
		||||
          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 { Constant } from '../utils/constant';
 | 
			
		||||
import { WebBidsService } from './web-bids.service';
 | 
			
		||||
import { NotificationService } from '@/modules/notification/notification.service';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class BidsService {
 | 
			
		||||
| 
						 | 
				
			
			@ -39,6 +40,7 @@ export class BidsService {
 | 
			
		|||
    readonly bidHistoriesRepo: Repository<BidHistory>,
 | 
			
		||||
    private readonly webBidsService: WebBidsService,
 | 
			
		||||
    private eventEmitter: EventEmitter2,
 | 
			
		||||
    private notificationService: NotificationService,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  async index(query: PaginateQuery) {
 | 
			
		||||
| 
						 | 
				
			
			@ -159,6 +161,9 @@ export class BidsService {
 | 
			
		|||
 | 
			
		||||
      this.emitAllBidEvent();
 | 
			
		||||
 | 
			
		||||
      // send message event
 | 
			
		||||
      this.notificationService.emitBidStatus({ ...bid, status: 'out-bid' });
 | 
			
		||||
 | 
			
		||||
      return AppResponse.toResponse(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -183,6 +188,9 @@ export class BidsService {
 | 
			
		|||
 | 
			
		||||
    await this.bidsRepo.update(id, { status: 'biding' });
 | 
			
		||||
 | 
			
		||||
    // send message event
 | 
			
		||||
    this.notificationService.emitBidStatus({ ...bid, status: 'biding' });
 | 
			
		||||
 | 
			
		||||
    this.emitAllBidEvent();
 | 
			
		||||
    return AppResponse.toResponse(true);
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -210,12 +218,6 @@ export class BidsService {
 | 
			
		|||
      data.current_price >= bid.max_price + bid.plus_price ||
 | 
			
		||||
      (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';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -234,16 +236,26 @@ export class BidsService {
 | 
			
		|||
 | 
			
		||||
    this.emitAllBidEvent();
 | 
			
		||||
 | 
			
		||||
    // send event message
 | 
			
		||||
    if (result.status === 'out-bid') {
 | 
			
		||||
      this.notificationService.emitBidStatus(result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -309,9 +321,25 @@ export class BidsService {
 | 
			
		|||
    });
 | 
			
		||||
 | 
			
		||||
    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();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,7 @@
 | 
			
		|||
export class Constant {
 | 
			
		||||
  public static MEDIA_PATH = 'public';
 | 
			
		||||
  public static BOT_TELEGRAM_PATH = 'bot-data';
 | 
			
		||||
 | 
			
		||||
  public static WORK_IMAGES_FOLDER = 'work-images';
 | 
			
		||||
  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