From db988a8392b30747c21f6b78eef0254ccb6b3e40 Mon Sep 17 00:00:00 2001 From: Omowumi Balogun Date: Sat, 22 Feb 2025 20:05:37 +0100 Subject: [PATCH] Created the power up infrastrucutre --- backend/src/app.module.ts | 3 +- .../src/power-ups/dtos/create-power-up.dto.ts | 17 ++ .../power-ups/dtos/purchase-power-up.dto.ts | 7 + .../src/power-ups/dtos/update-power-up.dto.ts | 21 ++ .../entities/power-up-purchase.entity.ts | 24 ++ .../src/power-ups/entities/power-up.entity.ts | 19 ++ .../power-up-validation.middleware.ts | 18 ++ backend/src/power-ups/power-up.controller.ts | 36 +++ backend/src/power-ups/power-up.module.ts | 15 ++ backend/src/power-ups/power-up.service.ts | 227 ++++++++++++++++++ 10 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 backend/src/power-ups/dtos/create-power-up.dto.ts create mode 100644 backend/src/power-ups/dtos/purchase-power-up.dto.ts create mode 100644 backend/src/power-ups/dtos/update-power-up.dto.ts create mode 100644 backend/src/power-ups/entities/power-up-purchase.entity.ts create mode 100644 backend/src/power-ups/entities/power-up.entity.ts create mode 100644 backend/src/power-ups/power-up-validation.middleware.ts create mode 100644 backend/src/power-ups/power-up.controller.ts create mode 100644 backend/src/power-ups/power-up.module.ts create mode 100644 backend/src/power-ups/power-up.service.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 1176db71..3a953035 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -18,6 +18,7 @@ import { GlobalInterceptor } from './interceptors/global.interceptor'; import { SongsModule } from './songs/songs.module'; import { ScoringModule } from './scoring/scoring.module'; import { ChatRoomModule } from './chat-room/chat-room.module'; +import { PowerUpModule } from './power-ups/power-up.module'; @Module({ imports: [ @@ -39,7 +40,7 @@ import { ChatRoomModule } from './chat-room/chat-room.module'; SongsModule, ChatRoomModule, ScoringModule, - + PowerUpModule, ], controllers: [AppController], providers: [ diff --git a/backend/src/power-ups/dtos/create-power-up.dto.ts b/backend/src/power-ups/dtos/create-power-up.dto.ts new file mode 100644 index 00000000..f116e653 --- /dev/null +++ b/backend/src/power-ups/dtos/create-power-up.dto.ts @@ -0,0 +1,17 @@ +import { IsString, IsNumber, IsPositive } from 'class-validator'; + +export class CreatePowerUpDto { + @IsString() + name: string; + + @IsString() + description: string; + + @IsNumber() + @IsPositive() + duration: number; + + @IsNumber() + @IsPositive() + price: number; +} diff --git a/backend/src/power-ups/dtos/purchase-power-up.dto.ts b/backend/src/power-ups/dtos/purchase-power-up.dto.ts new file mode 100644 index 00000000..ea00c94a --- /dev/null +++ b/backend/src/power-ups/dtos/purchase-power-up.dto.ts @@ -0,0 +1,7 @@ +import { IsNumber, IsPositive } from 'class-validator'; + +export class PurchasePowerUpDto { + @IsNumber() + @IsPositive() + powerUpId: number; +} diff --git a/backend/src/power-ups/dtos/update-power-up.dto.ts b/backend/src/power-ups/dtos/update-power-up.dto.ts new file mode 100644 index 00000000..caa9404b --- /dev/null +++ b/backend/src/power-ups/dtos/update-power-up.dto.ts @@ -0,0 +1,21 @@ +import { IsString, IsNumber, IsPositive, IsOptional } from 'class-validator'; + +export class UpdatePowerUpDto { + @IsOptional() + @IsString() + name?: string; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @IsNumber() + @IsPositive() + duration?: number; + + @IsOptional() + @IsNumber() + @IsPositive() + price?: number; +} diff --git a/backend/src/power-ups/entities/power-up-purchase.entity.ts b/backend/src/power-ups/entities/power-up-purchase.entity.ts new file mode 100644 index 00000000..67107b5b --- /dev/null +++ b/backend/src/power-ups/entities/power-up-purchase.entity.ts @@ -0,0 +1,24 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; +import { PowerUp } from './power-up.entity'; +import { User } from '../users/user.entity'; + +@Entity() +export class PowerUpPurchase { + @PrimaryGeneratedColumn() + id: number; + + @ManyToOne(() => PowerUp) + powerUp: PowerUp; + + @ManyToOne(() => User) + user: User; + + @Column() + purchaseDate: Date; + + @Column() + expirationDate: Date; + + @Column({ default: false }) + isUsed: boolean; +} diff --git a/backend/src/power-ups/entities/power-up.entity.ts b/backend/src/power-ups/entities/power-up.entity.ts new file mode 100644 index 00000000..a41e6b2f --- /dev/null +++ b/backend/src/power-ups/entities/power-up.entity.ts @@ -0,0 +1,19 @@ +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; + +@Entity() +export class PowerUp { + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @Column() + description: string; + + @Column() + duration: number; + + @Column() + price: number; +} diff --git a/backend/src/power-ups/power-up-validation.middleware.ts b/backend/src/power-ups/power-up-validation.middleware.ts new file mode 100644 index 00000000..9937ea13 --- /dev/null +++ b/backend/src/power-ups/power-up-validation.middleware.ts @@ -0,0 +1,18 @@ +import { Injectable, type NestMiddleware } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; +import { PowerUpService } from './power-up.service'; + +@Injectable() +export class PowerUpValidationMiddleware implements NestMiddleware { + constructor(private readonly powerUpService: PowerUpService) {} + + async use(req: Request, res: Response, next: NextFunction) { + const user = req.user; + const activePowerUps = await this.powerUpService.getActivePowerUps(user); + + // Add active power-ups to the request object for use in controllers + req['activePowerUps'] = activePowerUps; + + next(); + } +} diff --git a/backend/src/power-ups/power-up.controller.ts b/backend/src/power-ups/power-up.controller.ts new file mode 100644 index 00000000..88c33728 --- /dev/null +++ b/backend/src/power-ups/power-up.controller.ts @@ -0,0 +1,36 @@ +import { Controller, Post, Body, Get, UseGuards } from '@nestjs/common'; +import { PowerUpService } from './power-up.service'; +import { CreatePowerUpDto } from './dto/create-power-up.dto'; +import { PurchasePowerUpDto } from './dto/purchase-power-up.dto'; +import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { User } from '../users/user.decorator'; + +@Controller('power-ups') +export class PowerUpController { + constructor(private readonly powerUpService: PowerUpService) {} + + @Post() + @UseGuards(JwtAuthGuard) + async createPowerUp(@Body() createPowerUpDto: CreatePowerUpDto) { + return this.powerUpService.createPowerUp(createPowerUpDto); + } + + @Post('purchase') + @UseGuards(JwtAuthGuard) + async purchasePowerUp(@User() user, @Body() purchaseDto: PurchasePowerUpDto) { + return this.powerUpService.purchasePowerUp(user, purchaseDto); + } + + @Get('active') + @UseGuards(JwtAuthGuard) + async getActivePowerUps(@User() user) { + return this.powerUpService.getActivePowerUps(user); + } + + @Post('use') + @UseGuards(JwtAuthGuard) + async usePowerUp(@User() user, @Body('powerUpId') powerUpId: number) { + await this.powerUpService.usePowerUp(user, powerUpId); + return { message: 'Power-up used successfully' }; + } +} diff --git a/backend/src/power-ups/power-up.module.ts b/backend/src/power-ups/power-up.module.ts new file mode 100644 index 00000000..4a3bf871 --- /dev/null +++ b/backend/src/power-ups/power-up.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { PowerUpService } from './power-up.service'; +import { PowerUpController } from './power-up.controller'; +import { PowerUp } from './entities/power-up.entity'; +import { PowerUpPurchase } from './entities/power-up-purchase.entity'; +import { PowerUpValidationMiddleware } from './power-up-validation.middleware'; + +@Module({ + imports: [TypeOrmModule.forFeature([PowerUp, PowerUpPurchase])], + providers: [PowerUpService, PowerUpValidationMiddleware], + controllers: [PowerUpController], + exports: [PowerUpService, PowerUpValidationMiddleware], +}) +export class PowerUpModule {} diff --git a/backend/src/power-ups/power-up.service.ts b/backend/src/power-ups/power-up.service.ts new file mode 100644 index 00000000..d4b6bb77 --- /dev/null +++ b/backend/src/power-ups/power-up.service.ts @@ -0,0 +1,227 @@ +import { + Injectable, + NotFoundException, + BadRequestException, + InternalServerErrorException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, FindOneOptions } from 'typeorm'; +import { PowerUp } from './entities/power-up.entity'; +import { PowerUpPurchase } from './entities/power-up-purchase.entity'; +import { CreatePowerUpDto } from './dtos/create-power-up.dto'; +import { UpdatePowerUpDto } from './dto/update-power-up.dto'; +import { PurchasePowerUpDto } from './dto/purchase-power-up.dto'; +import { User } from '../users/user.entity'; + +@Injectable() +export class PowerUpService { + constructor( + @InjectRepository(PowerUp) + private powerUpRepository: Repository, + @InjectRepository(PowerUpPurchase) + private powerUpPurchaseRepository: Repository, + ) {} + + async createPowerUp(createPowerUpDto: CreatePowerUpDto): Promise { + try { + const powerUp = this.powerUpRepository.create(createPowerUpDto); + return await this.powerUpRepository.save(powerUp); + } catch (error) { + throw new InternalServerErrorException('Failed to create power-up'); + } + } + + async getAllPowerUps(): Promise { + try { + return await this.powerUpRepository.find(); + } catch (error) { + throw new InternalServerErrorException('Failed to retrieve power-ups'); + } + } + + async getPowerUpById(id: number): Promise { + const powerUp = await this.powerUpRepository.findOne({ + where: { id }, + } as FindOneOptions); + if (!powerUp) { + throw new NotFoundException(`Power-up with ID ${id} not found`); + } + return powerUp; + } + + async updatePowerUp( + id: number, + updatePowerUpDto: UpdatePowerUpDto, + ): Promise { + const powerUp = await this.getPowerUpById(id); + Object.assign(powerUp, updatePowerUpDto); + try { + return await this.powerUpRepository.save(powerUp); + } catch (error) { + throw new InternalServerErrorException('Failed to update power-up'); + } + } + + async deletePowerUp(id: number): Promise { + const result = await this.powerUpRepository.delete(id); + if (result.affected === 0) { + throw new NotFoundException(`Power-up with ID ${id} not found`); + } + } + + async purchasePowerUp( + user: User, + purchaseDto: PurchasePowerUpDto, + ): Promise { + const powerUp = await this.getPowerUpById(purchaseDto.powerUpId); + + const purchase = new PowerUpPurchase(); + purchase.powerUp = powerUp; + purchase.user = user; + purchase.purchaseDate = new Date(); + purchase.expirationDate = new Date( + Date.now() + powerUp.duration * 60 * 60 * 1000, + ); // Convert hours to milliseconds + + try { + return await this.powerUpPurchaseRepository.save(purchase); + } catch (error) { + throw new InternalServerErrorException('Failed to purchase power-up'); + } + } + + async getActivePowerUps(user: User): Promise { + try { + return await this.powerUpPurchaseRepository.find({ + where: { + user: { id: user.id }, + isUsed: false, + expirationDate: { $gt: new Date() }, + }, + relations: ['powerUp'], + }); + } catch (error) { + throw new InternalServerErrorException( + 'Failed to retrieve active power-ups', + ); + } + } + + async usePowerUp(user: User, powerUpId: number): Promise { + const purchase = await this.powerUpPurchaseRepository.findOne({ + where: { + user: { id: user.id }, + powerUp: { id: powerUpId }, + isUsed: false, + expirationDate: { $gt: new Date() }, + }, + } as FindOneOptions); + + if (!purchase) { + throw new NotFoundException('Active power-up not found'); + } + + purchase.isUsed = true; + try { + await this.powerUpPurchaseRepository.save(purchase); + } catch (error) { + throw new InternalServerErrorException('Failed to use power-up'); + } + } + + async getPowerUpPurchaseHistory(user: User): Promise { + try { + return await this.powerUpPurchaseRepository.find({ + where: { user: { id: user.id } }, + relations: ['powerUp'], + order: { purchaseDate: 'DESC' }, + }); + } catch (error) { + throw new InternalServerErrorException( + 'Failed to retrieve purchase history', + ); + } + } + + async checkPowerUpAvailability( + user: User, + powerUpId: number, + ): Promise { + const activePowerUp = await this.powerUpPurchaseRepository.findOne({ + where: { + user: { id: user.id }, + powerUp: { id: powerUpId }, + isUsed: false, + expirationDate: { $gt: new Date() }, + }, + } as FindOneOptions); + + return !!activePowerUp; + } + + async extendPowerUpDuration( + purchaseId: number, + extensionHours: number, + ): Promise { + const purchase = await this.powerUpPurchaseRepository.findOne({ + where: { id: purchaseId }, + } as FindOneOptions); + if (!purchase) { + throw new NotFoundException( + `Power-up purchase with ID ${purchaseId} not found`, + ); + } + + if (purchase.isUsed || purchase.expirationDate < new Date()) { + throw new BadRequestException( + 'Cannot extend an expired or used power-up', + ); + } + + purchase.expirationDate = new Date( + purchase.expirationDate.getTime() + extensionHours * 60 * 60 * 1000, + ); + + try { + return await this.powerUpPurchaseRepository.save(purchase); + } catch (error) { + throw new InternalServerErrorException( + 'Failed to extend power-up duration', + ); + } + } + + async getPowerUpStats(): Promise { + try { + const totalPowerUps = await this.powerUpRepository.count(); + const totalPurchases = await this.powerUpPurchaseRepository.count(); + const activePurchases = await this.powerUpPurchaseRepository.count({ + where: { + isUsed: false, + expirationDate: { $gt: new Date() }, + }, + }); + + const popularPowerUps = await this.powerUpPurchaseRepository + .createQueryBuilder('purchase') + .select('powerUp.name', 'name') + .addSelect('COUNT(*)', 'count') + .innerJoin('purchase.powerUp', 'powerUp') + .groupBy('powerUp.id') + .orderBy('count', 'DESC') + .limit(5) + .getRawMany(); + + return { + totalPowerUps, + totalPurchases, + activePurchases, + popularPowerUps, + }; + } catch (error) { + throw new InternalServerErrorException( + 'Failed to retrieve power-up statistics', + ); + } + } +}