mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 23:28:11 +00:00
Seatmap rendering and other updates
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Param, Post, UseGuards, Query, Patch, Delete } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { PromosService } from './promos.service';
|
||||
import { CreatePromotionDto } from './promos.dto';
|
||||
@@ -8,7 +8,66 @@ import { JwtGuard } from '../../common/jwt.guard';
|
||||
@Controller('promos')
|
||||
export class PromosController {
|
||||
constructor(private service: PromosService) {}
|
||||
@Get() @ApiOperation({ summary: 'Get active promotions' }) getActive() { return this.service.getActive(); }
|
||||
@Get('validate/:code') @ApiOperation({ summary: 'Validate a promo code' }) validate(@Param('code') code: string) { return this.service.validate(code); }
|
||||
@Post() @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Create promotion (admin)' }) create(@Body() dto: CreatePromotionDto) { return this.service.create(dto); }
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get active promotions' })
|
||||
getActive() {
|
||||
return this.service.getActive();
|
||||
}
|
||||
|
||||
@Get('all')
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: 'Get all promos with filters (admin)' })
|
||||
getAll(
|
||||
@Query('search') search?: string,
|
||||
@Query('active') active?: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
) {
|
||||
return this.service.getAll({
|
||||
search,
|
||||
active: active === 'true' ? true : active === 'false' ? false : undefined,
|
||||
page: page ? parseInt(page) : 1,
|
||||
pageSize: pageSize ? parseInt(pageSize) : 10,
|
||||
});
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: 'Get promo by ID' })
|
||||
getById(@Param('id') id: string) {
|
||||
return this.service.getById(id);
|
||||
}
|
||||
|
||||
@Get('validate/:code')
|
||||
@ApiOperation({ summary: 'Validate a promo code' })
|
||||
validate(@Param('code') code: string) {
|
||||
return this.service.validate(code);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: 'Create promotion (admin)' })
|
||||
create(@Body() dto: CreatePromotionDto) {
|
||||
return this.service.create(dto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: 'Update promo (admin)' })
|
||||
update(@Param('id') id: string, @Body() dto: Partial<CreatePromotionDto>) {
|
||||
return this.service.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: 'Delete promo (admin)' })
|
||||
delete(@Param('id') id: string) {
|
||||
return this.service.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,46 @@
|
||||
import { IsString, IsOptional, IsInt } from 'class-validator';
|
||||
import { IsString, IsOptional, IsInt, IsBoolean } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class CreatePromotionDto {
|
||||
@ApiProperty({ example: 'Weekend Special' }) @IsString() title: string;
|
||||
@ApiPropertyOptional({ example: '15% off all routes' }) @IsOptional() @IsString() subtitle?: string;
|
||||
@ApiProperty({ example: 'WEEKEND15' }) @IsString() code: string;
|
||||
@ApiPropertyOptional({ example: 15 }) @IsOptional() @IsInt() percentOff?: number;
|
||||
@ApiPropertyOptional({ example: 5000 }) @IsOptional() @IsInt() amountOffMinor?: number;
|
||||
@ApiProperty({ example: '2026-12-31T23:59:59Z' }) @IsString() validUntil: string;
|
||||
@ApiPropertyOptional({ example: 'Book Now' }) @IsOptional() @IsString() ctaLabel?: string;
|
||||
@ApiPropertyOptional({ example: 'edr://search' }) @IsOptional() @IsString() deepLink?: string;
|
||||
@ApiProperty({ example: 'SUMMER2024' })
|
||||
@IsString()
|
||||
code: string;
|
||||
|
||||
@ApiProperty({ example: 'Summer Discount' })
|
||||
@IsString()
|
||||
title: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Get 15% off' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
subtitle?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 15 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
percentOff?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 5000 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
amountOffMinor?: number;
|
||||
|
||||
@ApiProperty({ example: '2026-12-31T23:59:59Z' })
|
||||
@IsString()
|
||||
validUntil: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Book Now' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
ctaLabel?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'edr://search' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
deepLink?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,190 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../../common/prisma.service';
|
||||
import { CreatePromotionDto } from './promos.dto';
|
||||
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
|
||||
|
||||
@Injectable()
|
||||
export class PromosService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
getActive() { return this.prisma.promotion.findMany({ where: { active: true, validUntil: { gte: new Date() } }, orderBy: { createdAt: 'desc' } }); }
|
||||
getActive() {
|
||||
return this.prisma.promotion.findMany({
|
||||
where: { active: true, validUntil: { gte: new Date() } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
async getAll(filters: { search?: string; active?: boolean; page?: number; pageSize?: number }) {
|
||||
const { search, active, page = 1, pageSize = 10 } = filters;
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
const where: any = {};
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ code: { contains: search, mode: 'insensitive' } },
|
||||
{ title: { contains: search, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
if (active !== undefined) {
|
||||
where.active = active;
|
||||
}
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.promotion.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: pageSize,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
}),
|
||||
this.prisma.promotion.count({ where }),
|
||||
]);
|
||||
|
||||
return { items: this.formatItems(items), total, page, pageSize };
|
||||
}
|
||||
|
||||
async getById(id: string) {
|
||||
const promo = await this.prisma.promotion.findUnique({ where: { id } });
|
||||
if (!promo) throw new NotFoundException('Promo not found');
|
||||
return this.formatItem(promo);
|
||||
}
|
||||
|
||||
async validate(code: string) {
|
||||
const promo = await this.prisma.promotion.findUnique({ where: { code } });
|
||||
if (!promo || !promo.active || promo.validUntil < new Date()) return { applicable: false, message: 'Promo code invalid or expired' };
|
||||
return { code: promo.code, percentOff: promo.percentOff, amountOffMinor: promo.amountOffMinor, validUntil: promo.validUntil, applicable: true, message: promo.percentOff ? `${promo.percentOff}% off` : `ETB ${((promo.amountOffMinor ?? 0) / 100).toFixed(2)} off` };
|
||||
if (!promo || !promo.active || promo.validUntil < new Date())
|
||||
return { applicable: false, message: 'Promo code invalid or expired' };
|
||||
return {
|
||||
code: promo.code,
|
||||
percentOff: promo.percentOff,
|
||||
amountOffMinor: promo.amountOffMinor,
|
||||
validUntil: promo.validUntil,
|
||||
applicable: true,
|
||||
message: promo.percentOff
|
||||
? `${promo.percentOff}% off`
|
||||
: `ETB ${((promo.amountOffMinor ?? 0) / 100).toFixed(2)} off`,
|
||||
};
|
||||
}
|
||||
|
||||
create(dto: CreatePromotionDto) {
|
||||
return this.prisma.promotion.create({ data: { ...dto, validUntil: new Date(dto.validUntil) } });
|
||||
async create(dto: CreatePromotionDto & { discountType?: string; discountValue?: number }) {
|
||||
try {
|
||||
// Map frontend fields to database fields
|
||||
let percentOff: number | undefined;
|
||||
let amountOffMinor: number | undefined;
|
||||
|
||||
if (dto.discountType && dto.discountValue !== undefined) {
|
||||
if (dto.discountType === 'PERCENTAGE') {
|
||||
percentOff = dto.discountValue;
|
||||
} else if (dto.discountType === 'FIXED') {
|
||||
amountOffMinor = dto.discountValue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to direct fields
|
||||
percentOff = dto.percentOff;
|
||||
amountOffMinor = dto.amountOffMinor;
|
||||
}
|
||||
|
||||
const promo = await this.prisma.promotion.create({
|
||||
data: {
|
||||
code: dto.code,
|
||||
title: dto.title,
|
||||
subtitle: dto.subtitle,
|
||||
percentOff,
|
||||
amountOffMinor,
|
||||
validUntil: new Date(dto.validUntil),
|
||||
ctaLabel: dto.ctaLabel,
|
||||
deepLink: dto.deepLink,
|
||||
active: dto.active ?? true,
|
||||
},
|
||||
});
|
||||
return this.formatItem(promo);
|
||||
} catch (error) {
|
||||
if (error instanceof PrismaClientKnownRequestError) {
|
||||
if (error.code === 'P2002') {
|
||||
const field = (error.meta?.target as string[])?.[0];
|
||||
throw new BadRequestException(
|
||||
`A promo code with this ${field} already exists. Please use a different ${field}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: string, dto: Partial<CreatePromotionDto> & { discountType?: string; discountValue?: number }) {
|
||||
const promo = await this.prisma.promotion.findUnique({ where: { id } });
|
||||
if (!promo) throw new NotFoundException('Promo not found');
|
||||
|
||||
const updateData: any = {};
|
||||
|
||||
// Map frontend fields to database fields
|
||||
if (dto.discountType && dto.discountValue !== undefined) {
|
||||
// Clear existing discount fields
|
||||
updateData.percentOff = null;
|
||||
updateData.amountOffMinor = null;
|
||||
|
||||
if (dto.discountType === 'PERCENTAGE') {
|
||||
updateData.percentOff = dto.discountValue;
|
||||
} else if (dto.discountType === 'FIXED') {
|
||||
updateData.amountOffMinor = dto.discountValue;
|
||||
}
|
||||
} else {
|
||||
// Only include fields that are explicitly provided
|
||||
if (dto.percentOff !== undefined) updateData.percentOff = dto.percentOff;
|
||||
if (dto.amountOffMinor !== undefined) updateData.amountOffMinor = dto.amountOffMinor;
|
||||
}
|
||||
|
||||
if (dto.title !== undefined) updateData.title = dto.title;
|
||||
if (dto.subtitle !== undefined) updateData.subtitle = dto.subtitle;
|
||||
if (dto.ctaLabel !== undefined) updateData.ctaLabel = dto.ctaLabel;
|
||||
if (dto.deepLink !== undefined) updateData.deepLink = dto.deepLink;
|
||||
if (dto.active !== undefined) updateData.active = dto.active;
|
||||
if (dto.validUntil !== undefined) updateData.validUntil = new Date(dto.validUntil);
|
||||
|
||||
// Don't allow updating code - it's immutable after creation
|
||||
|
||||
try {
|
||||
const updated = await this.prisma.promotion.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
return this.formatItem(updated);
|
||||
} catch (error) {
|
||||
if (error instanceof PrismaClientKnownRequestError && error.code === 'P2002') {
|
||||
const field = (error.meta?.target as string[])?.[0];
|
||||
throw new BadRequestException(
|
||||
`A promo code with this ${field} already exists. Please use a different ${field}.`,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id: string) {
|
||||
const promo = await this.prisma.promotion.findUnique({ where: { id } });
|
||||
if (!promo) throw new NotFoundException('Promo not found');
|
||||
return this.prisma.promotion.delete({ where: { id } });
|
||||
}
|
||||
|
||||
private formatItem(promo: any) {
|
||||
return {
|
||||
id: promo.id,
|
||||
code: promo.code,
|
||||
title: promo.title,
|
||||
discountType: promo.percentOff ? 'PERCENTAGE' : 'FIXED',
|
||||
discountValue: promo.percentOff || promo.amountOffMinor || 0,
|
||||
maxDiscount: undefined,
|
||||
minBookingAmount: undefined,
|
||||
maxUsagePerUser: undefined,
|
||||
totalUsageLimit: undefined,
|
||||
usageCount: 0,
|
||||
validFrom: promo.createdAt,
|
||||
validUntil: promo.validUntil,
|
||||
isActive: promo.active,
|
||||
createdAt: promo.createdAt,
|
||||
updatedAt: promo.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
private formatItems(promos: any[]) {
|
||||
return promos.map((promo) => this.formatItem(promo));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user