Initial commit of edr-passenger-api alpha version

This commit is contained in:
Stephanos A
2026-05-13 16:58:49 +03:00
parent 199a3eba11
commit 39ba561d8f
113 changed files with 3602 additions and 1035 deletions

View File

@@ -0,0 +1,24 @@
import { Controller, Get, Param, Patch, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { NotificationsService } from './notifications.service';
import { JwtGuard } from '../../common/jwt.guard';
@ApiTags('Notifications')
@Controller('notifications')
@UseGuards(JwtGuard)
@ApiBearerAuth('JWT-auth')
export class NotificationsController {
constructor(private service: NotificationsService) {}
@Get(':passengerId')
@ApiOperation({ summary: 'Get notifications for passenger' })
getForPassenger(@Param('passengerId') id: string) { return this.service.getForPassenger(id); }
@Patch(':id/read')
@ApiOperation({ summary: 'Mark notification as read' })
markRead(@Param('id') id: string) { return this.service.markRead(id); }
@Patch(':passengerId/read-all')
@ApiOperation({ summary: 'Mark all notifications as read' })
markAllRead(@Param('passengerId') id: string) { return this.service.markAllRead(id); }
}

View File

@@ -0,0 +1,19 @@
import { IsString, IsEnum, IsOptional } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export enum NotificationCategoryEnum {
BOOKING = 'BOOKING',
PAYMENT = 'PAYMENT',
DISRUPTION = 'DISRUPTION',
PROMOTION = 'PROMOTION',
SYSTEM = 'SYSTEM',
}
export class SendNotificationDto {
@ApiProperty() @IsString() passengerId: string;
@ApiProperty({ example: 'Platform Change' }) @IsString() title: string;
@ApiProperty({ example: 'Your train departs from Platform 3' }) @IsString() body: string;
@ApiProperty({ enum: NotificationCategoryEnum }) @IsEnum(NotificationCategoryEnum) category: NotificationCategoryEnum;
@ApiPropertyOptional({ example: 'edr://tickets/tkt_01' }) @IsOptional() @IsString() deepLink?: string;
@ApiPropertyOptional() @IsOptional() metadata?: Record<string, any>;
}

View File

@@ -1,9 +1,6 @@
import { Module } from "@nestjs/common";
import { Module } from '@nestjs/common';
import { NotificationsController } from './notifications.controller';
import { NotificationsService } from './notifications.service';
import { NotificationsService } from "./notifications.service";
@Module({
providers: [NotificationsService],
exports: [NotificationsService],
})
@Module({ controllers: [NotificationsController], providers: [NotificationsService], exports: [NotificationsService] })
export class NotificationsModule {}

View File

@@ -1,14 +1,45 @@
import { Injectable, Logger } from "@nestjs/common";
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { PrismaService } from '../../common/prisma.service';
import { SendNotificationDto, NotificationCategoryEnum } from './notifications.dto';
import * as sgMail from '@sendgrid/mail';
@Injectable()
export class NotificationsService {
private readonly logger = new Logger(NotificationsService.name);
/**
* Dispatch a notification to a passenger (booking confirmation, schedule change, etc.).
* TODO: wire to email/SMS provider via a mailer service.
*/
async send(recipient: string, subject: string, body: string): Promise<void> {
this.logger.log(`[notify] ${recipient} :: ${subject} :: ${body}`);
constructor(private prisma: PrismaService) {
if (process.env.SENDGRID_API_KEY) sgMail.setApiKey(process.env.SENDGRID_API_KEY);
}
}
private sanitize(value: string): string {
return value.replace(/[\r\n]/g, ' ').replace(/[<>&"']/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&#x27;' }[c] ?? c));
}
async send(dto: SendNotificationDto) {
const notification = await this.prisma.notification.create({ data: { passengerId: dto.passengerId, title: dto.title, body: dto.body, category: dto.category as any, deepLink: dto.deepLink, metadata: dto.metadata } });
const passenger = await this.prisma.passenger.findUnique({ where: { id: dto.passengerId }, include: { user: true } });
if (passenger?.user) await this.sendEmail(passenger.user.email, this.sanitize(dto.title), this.sanitize(dto.body));
return notification;
}
getForPassenger(passengerId: string) { return this.prisma.notification.findMany({ where: { passengerId }, orderBy: { createdAt: 'desc' }, take: 50 }); }
markRead(id: string) { return this.prisma.notification.update({ where: { id }, data: { read: true } }); }
async markAllRead(passengerId: string) { await this.prisma.notification.updateMany({ where: { passengerId, read: false }, data: { read: true } }); return { updated: true }; }
@OnEvent('booking.created')
async onBookingCreated(payload: any) {
await this.send({ passengerId: payload.booking.passengerId, title: 'Booking Created', body: `Booking ${payload.booking.bookingRef} created. Complete payment within 15 minutes.`, category: NotificationCategoryEnum.BOOKING, deepLink: `edr://bookings/${payload.booking.bookingRef}`, metadata: { bookingRef: payload.booking.bookingRef } });
}
@OnEvent('payment.succeeded')
async onPaymentSucceeded(payload: any) {
await this.send({ passengerId: payload.booking.passengerId, title: 'Payment Successful', body: `Your ticket for ${payload.booking.bookingRef} is confirmed. Have a great journey!`, category: NotificationCategoryEnum.PAYMENT, deepLink: `edr://tickets/${payload.booking.bookingRef}`, metadata: { bookingRef: payload.booking.bookingRef } });
}
private async sendEmail(to: string, subject: string, text: string) {
if (!process.env.SENDGRID_API_KEY) { console.log(`[EMAIL] To: ${to} | Subject: ${subject}`); return; }
try { await sgMail.send({ to, from: process.env.SENDGRID_FROM_EMAIL || 'noreply@edr-platform.com', subject, text }); }
catch (e) { console.error('[EMAIL] Send error:', String(e instanceof Error ? e.message : e).replace(/[\r\n<>&"']/g, ' ')); }
}
}