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

@@ -1,20 +0,0 @@
import { IsDateString, IsEmail, IsOptional, IsString } from "class-validator";
export class CreatePassengerDto {
@IsString()
fullName!: string;
@IsEmail()
email!: string;
@IsString()
phone!: string;
@IsOptional()
@IsString()
nationalId?: string;
@IsOptional()
@IsDateString()
dateOfBirth?: string;
}

View File

@@ -1,20 +0,0 @@
import { BaseEntity } from "@edr/api-common";
import { Column, Entity } from "typeorm";
@Entity({ name: "passengers" })
export class Passenger extends BaseEntity {
@Column({ name: "full_name", type: "varchar", length: 256 })
fullName!: string;
@Column({ name: "email", type: "varchar", length: 256, unique: true })
email!: string;
@Column({ name: "phone", type: "varchar", length: 32 })
phone!: string;
@Column({ name: "national_id", type: "varchar", length: 64, nullable: true })
nationalId?: string | null;
@Column({ name: "date_of_birth", type: "date", nullable: true })
dateOfBirth?: string | null;
}

View File

@@ -1,37 +1,19 @@
import {
Body,
Controller,
Get,
Param,
ParseUUIDPipe,
Post,
} from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { PassengersService } from './passengers.service';
import { CreateTravelerProfileDto, CreateSavedRouteDto } from './passengers.dto';
import { JwtGuard } from '../../common/jwt.guard';
import { CreatePassengerDto } from "./dto/create-passenger.dto";
import { PassengersService } from "./passengers.service";
@ApiTags("passengers")
// @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth
@Controller("passengers")
@ApiTags('Passenger')
@Controller('passengers')
@UseGuards(JwtGuard)
@ApiBearerAuth('JWT-auth')
export class PassengersController {
constructor(private readonly passengersService: PassengersService) {}
@Post()
@ApiOperation({ summary: "Register a new passenger" })
create(@Body() dto: CreatePassengerDto) {
return this.passengersService.create(dto);
}
@Get()
@ApiOperation({ summary: "List all passengers" })
findAll() {
return this.passengersService.findAll();
}
@Get(":id")
@ApiOperation({ summary: "Get a passenger by ID" })
findOne(@Param("id", ParseUUIDPipe) id: string) {
return this.passengersService.findById(id);
}
constructor(private service: PassengersService) {}
@Get(':id/profile') @ApiOperation({ summary: 'Get passenger profile' }) getProfile(@Param('id') id: string) { return this.service.getProfile(id); }
@Get(':id/stats') @ApiOperation({ summary: 'Get passenger stats' }) getStats(@Param('id') id: string) { return this.service.getStats(id); }
@Post('traveler-profiles') @ApiOperation({ summary: 'Add traveler profile (family member)' }) createTravelerProfile(@Body() dto: CreateTravelerProfileDto) { return this.service.createTravelerProfile(dto); }
@Get(':id/traveler-profiles') @ApiOperation({ summary: 'Get traveler profiles for passenger' }) getTravelerProfiles(@Param('id') id: string) { return this.service.getTravelerProfiles(id); }
@Post('saved-routes') @ApiOperation({ summary: 'Save a route' }) createSavedRoute(@Body() dto: CreateSavedRouteDto) { return this.service.createSavedRoute(dto); }
@Get(':id/saved-routes') @ApiOperation({ summary: 'Get saved routes' }) getSavedRoutes(@Param('id') id: string) { return this.service.getSavedRoutes(id); }
}

View File

@@ -0,0 +1,19 @@
import { IsString, IsOptional, IsDateString } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateTravelerProfileDto {
@ApiProperty() @IsString() passengerId: string;
@ApiProperty({ example: 'Sara Ketsela' }) @IsString() fullName: string;
@ApiProperty({ example: 'SPOUSE' }) @IsString() relationship: string;
@ApiPropertyOptional({ example: '1998-04-01' }) @IsOptional() @IsDateString() dateOfBirth?: string;
@ApiPropertyOptional({ example: 'ET-1234-5678' }) @IsOptional() @IsString() nationalId?: string;
@ApiPropertyOptional() @IsOptional() @IsString() notes?: string;
}
export class CreateSavedRouteDto {
@ApiProperty() @IsString() passengerId: string;
@ApiProperty() @IsString() fromStationId: string;
@ApiProperty() @IsString() toStationId: string;
@ApiProperty({ example: 'Addis Ababa' }) @IsString() fromName: string;
@ApiProperty({ example: 'Dire Dawa' }) @IsString() toName: string;
}

View File

@@ -1,15 +1,6 @@
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Module } from '@nestjs/common';
import { PassengersController } from './passengers.controller';
import { PassengersService } from './passengers.service';
import { Passenger } from "./entities/passenger.entity";
import { PassengersController } from "./passengers.controller";
import { PassengersRepository } from "./passengers.repository";
import { PassengersService } from "./passengers.service";
@Module({
imports: [TypeOrmModule.forFeature([Passenger])],
controllers: [PassengersController],
providers: [PassengersService, PassengersRepository],
exports: [PassengersService],
})
@Module({ controllers: [PassengersController], providers: [PassengersService] })
export class PassengersModule {}

View File

@@ -1,21 +0,0 @@
import { BaseRepository } from "@edr/api-common";
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Passenger } from "./entities/passenger.entity";
@Injectable()
export class PassengersRepository extends BaseRepository<Passenger> {
constructor(
@InjectRepository(Passenger)
repository: Repository<Passenger>,
) {
super(repository);
}
/** Find a passenger by their unique email. */
findByEmail(email: string): Promise<Passenger | null> {
return this.repository.findOne({ where: { email } });
}
}

View File

@@ -1,29 +1,57 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { CreatePassengerDto } from "./dto/create-passenger.dto";
import { Passenger } from "./entities/passenger.entity";
import { PassengersRepository } from "./passengers.repository";
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../../common/prisma.service';
import { CreateTravelerProfileDto, CreateSavedRouteDto } from './passengers.dto';
@Injectable()
export class PassengersService {
constructor(private readonly passengersRepository: PassengersRepository) {}
constructor(private prisma: PrismaService) {}
/** Register a new passenger. */
create(dto: CreatePassengerDto): Promise<Passenger> {
return this.passengersRepository.create(dto);
async getProfile(passengerId: string) {
const p = await this.prisma.passenger.findUnique({
where: { id: passengerId },
include: {
user: { select: { fullName: true, email: true, phone: true } },
bookings: { orderBy: { createdAt: 'desc' }, take: 10, include: { trip: { include: { originStation: true, destinationStation: true, service: true } }, seats: { include: { seat: { include: { coach: true } } } } } },
loyalty: true, wallet: true, travelerProfiles: true, savedRoutes: true,
},
});
if (!p) throw new NotFoundException('Passenger not found');
return {
id: p.id,
fullName: p.user.fullName,
email: p.user.email,
phone: p.user.phone,
createdAt: p.createdAt,
bookings: p.bookings.map((b) => ({
id: b.id, bookingRef: b.bookingRef, status: b.status, totalFare: b.totalMinor / 100, createdAt: b.createdAt,
trip: {
number: b.trip.service.number,
origin: { id: b.trip.originStation.id, name: b.trip.originStation.name, code: b.trip.originStation.code, city: b.trip.originStation.city },
destination: { id: b.trip.destinationStation.id, name: b.trip.destinationStation.name, code: b.trip.destinationStation.code, city: b.trip.destinationStation.city },
departureAt: b.trip.departureAt,
},
passengers: b.seats.map((bs) => ({ fullName: bs.passengerName, seat: { number: bs.seat.label, coach: bs.seat.coach.label, class: bs.seat.coach.serviceClass } })),
})),
};
}
/** List every passenger (alphabetical). */
findAll(): Promise<Passenger[]> {
return this.passengersRepository.findAll({ order: { fullName: "ASC" } });
async getStats(passengerId: string) {
const [totalTrips, totalSpendResult, loyalty] = await Promise.all([
this.prisma.booking.count({ where: { passengerId, status: 'COMPLETED' } }),
this.prisma.booking.aggregate({ where: { passengerId, status: 'COMPLETED' }, _sum: { totalMinor: true } }),
this.prisma.loyaltyAccount.findUnique({ where: { passengerId } }),
]);
const totalSpend = (totalSpendResult._sum.totalMinor ?? 0) / 100;
return { totalTrips, totalSpend, loyaltyPoints: loyalty?.pointsBalance ?? 0, co2Saved: totalTrips * 6 };
}
/** Get a single passenger by ID. */
async findById(id: string): Promise<Passenger> {
const passenger = await this.passengersRepository.findById(id);
if (!passenger) {
throw new NotFoundException(`Passenger ${id} not found`);
}
return passenger;
createTravelerProfile(dto: CreateTravelerProfileDto) {
return this.prisma.travelerProfile.create({ data: { ...dto, dateOfBirth: dto.dateOfBirth ? new Date(dto.dateOfBirth) : null } });
}
getTravelerProfiles(passengerId: string) { return this.prisma.travelerProfile.findMany({ where: { passengerId } }); }
createSavedRoute(dto: CreateSavedRouteDto) { return this.prisma.savedRoute.create({ data: dto }); }
getSavedRoutes(passengerId: string) { return this.prisma.savedRoute.findMany({ where: { passengerId }, orderBy: { tripCount: 'desc' } }); }
}