mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 08:20:58 +00:00
Merge branch 'dev' of github.com:Tria-plc/edr-platform into freight/feature/user_management_UI
This commit is contained in:
@@ -28,9 +28,8 @@ MINIO_ACCESS_KEY=minioadmin
|
|||||||
MINIO_SECRET_KEY=minioadmin
|
MINIO_SECRET_KEY=minioadmin
|
||||||
MINIO_BUCKET=edr-dev
|
MINIO_BUCKET=edr-dev
|
||||||
|
|
||||||
# CORS
|
# CORS — comma-separated list of allowed origins (add more, comma-separated)
|
||||||
FRONTEND_URL=http://localhost:5174
|
CORS_ORIGINS=http://localhost:5174,http://localhost:5184
|
||||||
BACK_OFFICE_URL=http://localhost:5184
|
|
||||||
|
|
||||||
# JWT (legacy passenger auth — being replaced by IAM)
|
# JWT (legacy passenger auth — being replaced by IAM)
|
||||||
# REQUIRED in production — use a random 32+ character string (e.g. openssl rand -hex 32)
|
# REQUIRED in production — use a random 32+ character string (e.g. openssl rand -hex 32)
|
||||||
|
|||||||
@@ -1,79 +1,72 @@
|
|||||||
import {Logger, Module, OnApplicationBootstrap} from '@nestjs/common';
|
import { Logger, Module, OnApplicationBootstrap } from "@nestjs/common";
|
||||||
import { ThrottlerModule } from '@nestjs/throttler';
|
import { APP_FILTER } from "@nestjs/core";
|
||||||
import { DynamicThrottlerGuard } from './common/dynamic-throttler.guard';
|
import { ConfigModule, ConfigService } from "@nestjs/config";
|
||||||
import { APP_GUARD, APP_FILTER } from '@nestjs/core';
|
import { ScheduleModule } from "@nestjs/schedule";
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { EventEmitterModule } from "@nestjs/event-emitter";
|
||||||
import { ScheduleModule } from '@nestjs/schedule';
|
import { TypeOrmModule, TypeOrmModuleOptions } from "@nestjs/typeorm";
|
||||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
import { IamModule as TriaIamModule } from "@tria-plc/iamapi-common/iam.module";
|
||||||
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
|
import { DataSeeder } from "@tria-plc/iamapi-common/db/seed/seeder";
|
||||||
import { IamModule as TriaIamModule } from '@tria-plc/iamapi-common/iam.module';
|
import { SharedAuthModule } from "@tria-plc/api-common/modules/auth/shared-auth.module";
|
||||||
import { DataSeeder } from '@tria-plc/iamapi-common/db/seed/seeder';
|
|
||||||
import { SharedAuthModule } from '@tria-plc/api-common/modules/auth/shared-auth.module';
|
|
||||||
import {
|
import {
|
||||||
EDR_PASSENGER_APPLICATION,
|
EDR_PASSENGER_APPLICATION,
|
||||||
EDR_PASSENGER_PERMISSIONS,
|
EDR_PASSENGER_PERMISSIONS,
|
||||||
} from './seed/edr-passenger.seed';
|
} from "./seed/edr-passenger.seed";
|
||||||
import { EdrPassengerOrgSeeder } from './seed/edr-passenger-org.seeder';
|
import { EdrPassengerOrgSeeder } from "./seed/edr-passenger-org.seeder";
|
||||||
import { PassengerStaffUsersSeeder } from './seed/passenger-staff-users.seeder';
|
import { PassengerStaffUsersSeeder } from "./seed/passenger-staff-users.seeder";
|
||||||
import { PrismaModule } from './common/prisma.module';
|
import { PrismaModule } from "./common/prisma.module";
|
||||||
import { AuditModule } from './common/audit.module';
|
import { AuditModule } from "./common/audit.module";
|
||||||
import { I18nModule } from './common/i18n/i18n.module';
|
import { I18nModule } from "./common/i18n/i18n.module";
|
||||||
import { LocaleMiddleware } from './common/i18n/locale.middleware';
|
import { LocaleMiddleware } from "./common/i18n/locale.middleware";
|
||||||
import { DeleteExceptionFilter } from './common/exceptions/delete-exception.filter';
|
import { DeleteExceptionFilter } from "./common/exceptions/delete-exception.filter";
|
||||||
import appConfig from './config/app.config';
|
import appConfig from "./config/app.config";
|
||||||
import dbConfig from './config/database.config';
|
import dbConfig from "./config/database.config";
|
||||||
import iamDatabaseConfig from './config/iam-database.config';
|
import iamDatabaseConfig from "./config/iam-database.config";
|
||||||
import telebirrConfig from './config/telebirr.config';
|
import telebirrConfig from "./config/telebirr.config";
|
||||||
import cbeConfig from './config/cbe.config';
|
import cbeConfig from "./config/cbe.config";
|
||||||
import ebirrConfig from './config/ebirr.config';
|
import ebirrConfig from "./config/ebirr.config";
|
||||||
import cardConfig from './config/card.config';
|
import cardConfig from "./config/card.config";
|
||||||
import waafiConfig from './config/waafi.config';
|
import waafiConfig from "./config/waafi.config";
|
||||||
import faydaConfig from './config/fayda.config';
|
import faydaConfig from "./config/fayda.config";
|
||||||
import rabbitmqConfig from './config/rabbitmq.config';
|
import rabbitmqConfig from "./config/rabbitmq.config";
|
||||||
import { AuthModule } from './modules/auth/auth.module';
|
import { AuthModule } from "./modules/auth/auth.module";
|
||||||
import { StationsModule } from './modules/stations/stations.module';
|
import { StationsModule } from "./modules/stations/stations.module";
|
||||||
import { FleetModule } from './modules/fleet/fleet.module';
|
import { FleetModule } from "./modules/fleet/fleet.module";
|
||||||
import { SchedulesModule } from './modules/schedules/schedules.module';
|
import { SchedulesModule } from "./modules/schedules/schedules.module";
|
||||||
import { SearchModule } from './modules/search/search.module';
|
import { SearchModule } from "./modules/search/search.module";
|
||||||
import { SeatsModule } from './modules/seats/seats.module';
|
import { SeatsModule } from "./modules/seats/seats.module";
|
||||||
import { BookingsModule } from './modules/bookings/bookings.module';
|
import { BookingsModule } from "./modules/bookings/bookings.module";
|
||||||
import { PaymentsModule } from './modules/payments/payments.module';
|
import { PaymentsModule } from "./modules/payments/payments.module";
|
||||||
import { TicketsModule } from './modules/tickets/tickets.module';
|
import { TicketsModule } from "./modules/tickets/tickets.module";
|
||||||
import { PassengersModule } from './modules/passengers/passengers.module';
|
import { PassengersModule } from "./modules/passengers/passengers.module";
|
||||||
import { NotificationsModule } from './modules/notifications/notifications.module';
|
import { NotificationsModule } from "./modules/notifications/notifications.module";
|
||||||
import { LoyaltyModule } from './modules/loyalty/loyalty.module';
|
import { LoyaltyModule } from "./modules/loyalty/loyalty.module";
|
||||||
import { WalletModule } from './modules/wallet/wallet.module';
|
import { WalletModule } from "./modules/wallet/wallet.module";
|
||||||
import { PromosModule } from './modules/promos/promos.module';
|
import { PromosModule } from "./modules/promos/promos.module";
|
||||||
import { LiveModule } from './modules/live/live.module';
|
import { LiveModule } from "./modules/live/live.module";
|
||||||
import { SupportModule } from './modules/support/support.module';
|
import { SupportModule } from "./modules/support/support.module";
|
||||||
import { DashboardModule } from './modules/dashboard/dashboard.module';
|
import { DashboardModule } from "./modules/dashboard/dashboard.module";
|
||||||
import { SegmentsModule } from './modules/segments/segments.module';
|
import { SegmentsModule } from "./modules/segments/segments.module";
|
||||||
import { AgentsModule } from './modules/agents/agents.module';
|
import { AgentsModule } from "./modules/agents/agents.module";
|
||||||
import { ReportsModule } from './modules/reports/reports.module';
|
import { ReportsModule } from "./modules/reports/reports.module";
|
||||||
import { FraudModule } from './modules/fraud/fraud.module';
|
import { FraudModule } from "./modules/fraud/fraud.module";
|
||||||
import { SeatClassesModule } from './modules/seat-classes/seat-classes.module';
|
import { SeatClassesModule } from "./modules/seat-classes/seat-classes.module";
|
||||||
import { FareEngineModule } from './modules/fare-engine/fare-engine.module';
|
import { FareEngineModule } from "./modules/fare-engine/fare-engine.module";
|
||||||
import { VerifaydaModule } from './modules/verifayda/verifayda.module';
|
import { VerifaydaModule } from "./modules/verifayda/verifayda.module";
|
||||||
import { AuditModuleFeature } from './modules/audit/audit.module';
|
import { AuditModuleFeature } from "./modules/audit/audit.module";
|
||||||
import { CurrenciesModule } from './modules/currencies/currencies.module';
|
import { CurrenciesModule } from "./modules/currencies/currencies.module";
|
||||||
import { SystemConfigModule } from './modules/system-config/system-config.module';
|
import { SystemConfigModule } from "./modules/system-config/system-config.module";
|
||||||
import { PackagesModule } from './modules/packages/packages.module';
|
import { PackagesModule } from "./modules/packages/packages.module";
|
||||||
import { ExcessBaggageModule } from './modules/excess-baggage/excess-baggage.module';
|
import { ExcessBaggageModule } from "./modules/excess-baggage/excess-baggage.module";
|
||||||
import { HealthModule } from './modules/health/health.module';
|
import { HealthModule } from "./modules/health/health.module";
|
||||||
import { TasksModule } from './modules/tasks/tasks.module';
|
import { TasksModule } from "./modules/tasks/tasks.module";
|
||||||
import { AppReleasesModule } from './modules/app-releases/app-releases.module';
|
import { AppReleasesModule } from "./modules/app-releases/app-releases.module";
|
||||||
import { ConfigurableFareModule } from './modules/configurable-fare/configurable-fare.module';
|
import { ConfigurableFareModule } from "./modules/configurable-fare/configurable-fare.module";
|
||||||
import { SegmentFareSeeder } from './seed/segment-fare.seeder';
|
import { SegmentFareSeeder } from "./seed/segment-fare.seeder";
|
||||||
|
|
||||||
import { EOtpType } from "@tria-plc/iamapi-common";
|
import { EOtpType } from "@tria-plc/iamapi-common";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ThrottlerModule.forRoot([
|
|
||||||
{ name: 'auth', ttl: 60_000, limit: 5 },
|
|
||||||
{ name: 'strict', ttl: 60_000, limit: 20 },
|
|
||||||
{ name: 'default', ttl: 60_000, limit: 100 },
|
|
||||||
]),
|
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
load: [
|
load: [
|
||||||
@@ -94,27 +87,27 @@ import { EOtpType } from "@tria-plc/iamapi-common";
|
|||||||
TypeOrmModule.forRootAsync({
|
TypeOrmModule.forRootAsync({
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
useFactory: (config: ConfigService): TypeOrmModuleOptions =>
|
useFactory: (config: ConfigService): TypeOrmModuleOptions =>
|
||||||
config.get<TypeOrmModuleOptions>('iamDatabase')!,
|
config.get<TypeOrmModuleOptions>("iamDatabase")!,
|
||||||
}),
|
}),
|
||||||
TriaIamModule.forRoot({
|
TriaIamModule.forRoot({
|
||||||
applications: [EDR_PASSENGER_APPLICATION],
|
applications: [EDR_PASSENGER_APPLICATION],
|
||||||
permissions: EDR_PASSENGER_PERMISSIONS,
|
permissions: EDR_PASSENGER_PERMISSIONS,
|
||||||
otpMessages: {
|
otpMessages: {
|
||||||
[EOtpType.MFA_LOGIN]: ({ otp }) =>
|
[EOtpType.MFA_LOGIN]: ({ otp }) =>
|
||||||
`Your EDR Passenger login code is ${otp}. It will expire in 5 minutes.`,
|
`Your EDR Passenger login code is ${otp}. It will expire in 5 minutes.`,
|
||||||
[EOtpType.VERIFY_PHONE_NUMBER]: ({ otp }) =>
|
[EOtpType.VERIFY_PHONE_NUMBER]: ({ otp }) =>
|
||||||
`Your EDR Passenger phone verification code is ${otp}. It will expire in 5 minutes.`,
|
`Your EDR Passenger phone verification code is ${otp}. It will expire in 5 minutes.`,
|
||||||
[EOtpType.RESET_PASSWORD]: ({ route }) =>
|
[EOtpType.RESET_PASSWORD]: ({ route }) =>
|
||||||
`Reset your EDR Passenger password using this link: ${route}`,
|
`Reset your EDR Passenger password using this link: ${route}`,
|
||||||
[EOtpType.SET_PASSWORD]: ({ route }) =>
|
[EOtpType.SET_PASSWORD]: ({ route }) =>
|
||||||
`Set your EDR Passenger password using this link: ${route}`,
|
`Set your EDR Passenger password using this link: ${route}`,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
SharedAuthModule,
|
SharedAuthModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
AuditModule,
|
AuditModule,
|
||||||
I18nModule,
|
I18nModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
StationsModule,
|
StationsModule,
|
||||||
FleetModule,
|
FleetModule,
|
||||||
SchedulesModule,
|
SchedulesModule,
|
||||||
@@ -149,9 +142,7 @@ import { EOtpType } from "@tria-plc/iamapi-common";
|
|||||||
ConfigurableFareModule,
|
ConfigurableFareModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: APP_GUARD, useClass: DynamicThrottlerGuard },
|
|
||||||
{ provide: APP_FILTER, useClass: DeleteExceptionFilter },
|
{ provide: APP_FILTER, useClass: DeleteExceptionFilter },
|
||||||
DynamicThrottlerGuard,
|
|
||||||
EdrPassengerOrgSeeder,
|
EdrPassengerOrgSeeder,
|
||||||
PassengerStaffUsersSeeder,
|
PassengerStaffUsersSeeder,
|
||||||
SegmentFareSeeder,
|
SegmentFareSeeder,
|
||||||
@@ -170,22 +161,34 @@ export class AppModule implements OnApplicationBootstrap {
|
|||||||
try {
|
try {
|
||||||
await this.seeder.run();
|
await this.seeder.run();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error('[DataSeeder] Seed failed (non-fatal):', (err as Error).message);
|
this.logger.error(
|
||||||
|
"[DataSeeder] Seed failed (non-fatal):",
|
||||||
|
(err as Error).message,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await this.edrPassengerOrgSeeder.run();
|
await this.edrPassengerOrgSeeder.run();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error('[EdrPassengerOrgSeeder] Seed failed (non-fatal):', (err as Error).message);
|
this.logger.error(
|
||||||
|
"[EdrPassengerOrgSeeder] Seed failed (non-fatal):",
|
||||||
|
(err as Error).message,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await this.passengerStaffUsersSeeder.run();
|
await this.passengerStaffUsersSeeder.run();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error('[PassengerStaffUsersSeeder] Seed failed (non-fatal):', (err as Error).message);
|
this.logger.error(
|
||||||
|
"[PassengerStaffUsersSeeder] Seed failed (non-fatal):",
|
||||||
|
(err as Error).message,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await this.segmentFareSeeder.run();
|
await this.segmentFareSeeder.run();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error('[SegmentFareSeeder] Seed failed (non-fatal):', (err as Error).message);
|
this.logger.error(
|
||||||
|
"[SegmentFareSeeder] Seed failed (non-fatal):",
|
||||||
|
(err as Error).message,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
import { Injectable, ExecutionContext, Inject } from '@nestjs/common';
|
|
||||||
import { Reflector } from '@nestjs/core';
|
|
||||||
import { ThrottlerGuard, ThrottlerStorage, getOptionsToken, getStorageToken } from '@nestjs/throttler';
|
|
||||||
import { SystemConfigService, CONFIG_KEYS } from '../modules/system-config/system-config.service';
|
|
||||||
|
|
||||||
// Route-prefix → throttler tier mapping.
|
|
||||||
// Evaluated in order; first match wins.
|
|
||||||
const ROUTE_TIERS: Array<{ prefix: string; tier: 'auth' | 'strict' | 'default' }> = [
|
|
||||||
{ prefix: '/auth', tier: 'auth' },
|
|
||||||
{ prefix: '/fayda/verification',tier: 'auth' },
|
|
||||||
{ prefix: '/bookings', tier: 'strict' },
|
|
||||||
{ prefix: '/passengers', tier: 'strict' },
|
|
||||||
{ prefix: '/payments', tier: 'strict' },
|
|
||||||
{ prefix: '/wallet', tier: 'strict' },
|
|
||||||
];
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class DynamicThrottlerGuard extends ThrottlerGuard {
|
|
||||||
constructor(
|
|
||||||
@Inject(getOptionsToken()) options: any,
|
|
||||||
@Inject(getStorageToken()) storageService: ThrottlerStorage,
|
|
||||||
reflector: Reflector,
|
|
||||||
private readonly systemConfig: SystemConfigService,
|
|
||||||
) {
|
|
||||||
super(options, storageService, reflector);
|
|
||||||
}
|
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
||||||
if (context.getType() !== 'http') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [authLimit, authTtl, strictLimit, strictTtl, defaultLimit, defaultTtl] =
|
|
||||||
await Promise.all([
|
|
||||||
this.systemConfig.getNumber(CONFIG_KEYS.THROTTLE_AUTH_LIMIT),
|
|
||||||
this.systemConfig.getNumber(CONFIG_KEYS.THROTTLE_AUTH_TTL_MS),
|
|
||||||
this.systemConfig.getNumber(CONFIG_KEYS.THROTTLE_STRICT_LIMIT),
|
|
||||||
this.systemConfig.getNumber(CONFIG_KEYS.THROTTLE_STRICT_TTL_MS),
|
|
||||||
this.systemConfig.getNumber(CONFIG_KEYS.THROTTLE_DEFAULT_LIMIT),
|
|
||||||
this.systemConfig.getNumber(CONFIG_KEYS.THROTTLE_DEFAULT_TTL_MS),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const url: string = context.switchToHttp().getRequest<{ url: string }>().url ?? '';
|
|
||||||
const matched = ROUTE_TIERS.find(({ prefix }) => url.startsWith(prefix));
|
|
||||||
const tier = matched?.tier ?? 'default';
|
|
||||||
|
|
||||||
if (tier === 'auth') {
|
|
||||||
this.throttlers = [{ name: 'auth', ttl: authTtl, limit: authLimit }];
|
|
||||||
} else if (tier === 'strict') {
|
|
||||||
this.throttlers = [{ name: 'strict', ttl: strictTtl, limit: strictLimit }];
|
|
||||||
} else {
|
|
||||||
this.throttlers = [{ name: 'default', ttl: defaultTtl, limit: defaultLimit }];
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.canActivate(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -33,11 +33,16 @@ async function bootstrap() {
|
|||||||
// version-neutral at their existing paths (e.g. /search, /bookings) — unchanged for the frontend.
|
// version-neutral at their existing paths (e.g. /search, /bookings) — unchanged for the frontend.
|
||||||
app.enableVersioning({ type: VersioningType.URI });
|
app.enableVersioning({ type: VersioningType.URI });
|
||||||
|
|
||||||
|
// Allowed CORS origins come from a single comma-separated env var (CORS_ORIGINS),
|
||||||
|
// e.g. "https://portal.edr.et,https://backoffice.edr.et". Whitespace around each
|
||||||
|
// entry is trimmed and empties are dropped. Falls back to the local dev ports.
|
||||||
|
const corsOrigins = (process.env.CORS_ORIGINS ?? "http://localhost:5174,http://localhost:5184")
|
||||||
|
.split(",")
|
||||||
|
.map((origin) => origin.trim())
|
||||||
|
.filter((origin) => origin.length > 0);
|
||||||
|
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: [
|
origin: corsOrigins,
|
||||||
process.env.PORTAL_URL ?? "http://localhost:5174",
|
|
||||||
process.env.BACK_OFFICE_URL ?? "http://localhost:5184",
|
|
||||||
],
|
|
||||||
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||||
allowedHeaders: ['Content-Type', 'Authorization', 'Accept-Language', 'X-Request-ID'],
|
allowedHeaders: ['Content-Type', 'Authorization', 'Accept-Language', 'X-Request-ID'],
|
||||||
credentials: true,
|
credentials: true,
|
||||||
|
|||||||
@@ -1,155 +1,231 @@
|
|||||||
import { Body, Controller, Post, HttpCode, HttpStatus, UseGuards, Get, Patch, Delete, Param, Request, Query, UnauthorizedException } from '@nestjs/common';
|
import {
|
||||||
import { ApiTags, ApiOperation, ApiResponse, ApiBody, ApiBearerAuth } from '@nestjs/swagger';
|
Body,
|
||||||
import { Throttle, SkipThrottle } from '@nestjs/throttler';
|
Controller,
|
||||||
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
|
Post,
|
||||||
import { PassengerAuthService } from './passenger-auth.service';
|
HttpCode,
|
||||||
import { RegisterDto, LoginDto, ResendRegistrationCodeDto, FaydaRequestPasswordSetupDto, FaydaVerifyAndLoginDto } from './auth.dto';
|
HttpStatus,
|
||||||
import { JwtGuard } from '../../common/jwt.guard';
|
UseGuards,
|
||||||
|
Get,
|
||||||
|
Patch,
|
||||||
|
Delete,
|
||||||
|
Param,
|
||||||
|
Request,
|
||||||
|
Query,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import {
|
||||||
|
ApiTags,
|
||||||
|
ApiOperation,
|
||||||
|
ApiResponse,
|
||||||
|
ApiBody,
|
||||||
|
ApiBearerAuth,
|
||||||
|
} from "@nestjs/swagger";
|
||||||
|
import { IsPublic } from "@tria-plc/api-common/modules/auth/decorators/public.decorator";
|
||||||
|
import { PassengerAuthService } from "./passenger-auth.service";
|
||||||
|
import {
|
||||||
|
RegisterDto,
|
||||||
|
LoginDto,
|
||||||
|
ResendRegistrationCodeDto,
|
||||||
|
FaydaRequestPasswordSetupDto,
|
||||||
|
FaydaVerifyAndLoginDto,
|
||||||
|
} from "./auth.dto";
|
||||||
|
import { JwtGuard } from "../../common/jwt.guard";
|
||||||
|
|
||||||
@ApiTags('Passenger Auth')
|
@ApiTags("Passenger Auth")
|
||||||
@Controller('auth')
|
@Controller("auth")
|
||||||
@Throttle({ auth: { limit: 5, ttl: 60_000 } })
|
// @Throttle({ auth: { limit: 5, ttl: 60_000 } })
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(private passengerAuthService: PassengerAuthService) {}
|
constructor(private passengerAuthService: PassengerAuthService) {}
|
||||||
|
|
||||||
@Post('register')
|
@Post("register")
|
||||||
@IsPublic()
|
@IsPublic()
|
||||||
@ApiOperation({ summary: 'Register new passenger account (sends SMS verification code)' })
|
@ApiOperation({
|
||||||
|
summary: "Register new passenger account (sends SMS verification code)",
|
||||||
|
})
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 201,
|
status: 201,
|
||||||
description:
|
description:
|
||||||
'Account created as pending. A verification code is sent via SMS — complete signup via PATCH /v1/auth/set-password.',
|
"Account created as pending. A verification code is sent via SMS — complete signup via PATCH /v1/auth/set-password.",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 409,
|
||||||
|
description: "Email or phone already registered",
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 409, description: 'Email or phone already registered' })
|
|
||||||
@ApiBody({ type: RegisterDto })
|
@ApiBody({ type: RegisterDto })
|
||||||
register(@Request() req: any, @Body() dto: RegisterDto) {
|
register(@Request() req: any, @Body() dto: RegisterDto) {
|
||||||
return this.passengerAuthService.register(dto, req);
|
return this.passengerAuthService.register(dto, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('register/resend-code')
|
@Post("register/resend-code")
|
||||||
@IsPublic()
|
@IsPublic()
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: 'Resend the registration verification code for a pending account' })
|
@ApiOperation({
|
||||||
@ApiResponse({ status: 200, description: 'Verification code re-sent if the account is pending.' })
|
summary: "Resend the registration verification code for a pending account",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: "Verification code re-sent if the account is pending.",
|
||||||
|
})
|
||||||
@ApiBody({ type: ResendRegistrationCodeDto })
|
@ApiBody({ type: ResendRegistrationCodeDto })
|
||||||
resendRegistrationCode(@Request() req: any, @Body() dto: ResendRegistrationCodeDto) {
|
resendRegistrationCode(
|
||||||
|
@Request() req: any,
|
||||||
|
@Body() dto: ResendRegistrationCodeDto,
|
||||||
|
) {
|
||||||
return this.passengerAuthService.resendRegistrationCode(dto, req);
|
return this.passengerAuthService.resendRegistrationCode(dto, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('login')
|
@Post("login")
|
||||||
@IsPublic()
|
@IsPublic()
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: 'Login with email and password' })
|
@ApiOperation({ summary: "Login with email and password" })
|
||||||
@ApiResponse({ status: 200, description: 'Login successful. Returns token + passengerId.' })
|
@ApiResponse({
|
||||||
@ApiResponse({ status: 401, description: 'Invalid credentials' })
|
status: 200,
|
||||||
|
description: "Login successful. Returns token + passengerId.",
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 401, description: "Invalid credentials" })
|
||||||
@ApiBody({ type: LoginDto })
|
@ApiBody({ type: LoginDto })
|
||||||
login(@Request() req: any, @Body() dto: LoginDto) {
|
login(@Request() req: any, @Body() dto: LoginDto) {
|
||||||
return this.passengerAuthService.login(dto, req);
|
return this.passengerAuthService.login(dto, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('logout')
|
@Post("logout")
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@ApiOperation({ summary: 'Logout current user' })
|
@ApiOperation({ summary: "Logout current user" })
|
||||||
@ApiResponse({ status: 200, description: 'Logout successful' })
|
@ApiResponse({ status: 200, description: "Logout successful" })
|
||||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
@ApiResponse({ status: 401, description: "Unauthorized" })
|
||||||
logout(@Request() req: any) {
|
logout(@Request() req: any) {
|
||||||
if (!req.user?.id) throw new UnauthorizedException('User not authenticated');
|
if (!req.user?.id)
|
||||||
|
throw new UnauthorizedException("User not authenticated");
|
||||||
return this.passengerAuthService.logout(req.user, req);
|
return this.passengerAuthService.logout(req.user, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('me')
|
@Get("me")
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@ApiOperation({ summary: '[DEV] Inspect raw JWT payload — shows full req.user from JwtGuard' })
|
@ApiOperation({
|
||||||
@ApiResponse({ status: 200, description: 'Returns the full req.user object set by JwtGuard' })
|
summary:
|
||||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
"[DEV] Inspect raw JWT payload — shows full req.user from JwtGuard",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: "Returns the full req.user object set by JwtGuard",
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 401, description: "Unauthorized" })
|
||||||
getMe(@Request() req: any) {
|
getMe(@Request() req: any) {
|
||||||
return { user: req.user };
|
return { user: req.user };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('profile')
|
@Get("profile")
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@ApiOperation({ summary: 'Get current user profile' })
|
@ApiOperation({ summary: "Get current user profile" })
|
||||||
@ApiResponse({ status: 200, description: 'User profile retrieved successfully' })
|
@ApiResponse({
|
||||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
status: 200,
|
||||||
|
description: "User profile retrieved successfully",
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 401, description: "Unauthorized" })
|
||||||
getProfile(@Request() req: any) {
|
getProfile(@Request() req: any) {
|
||||||
const userId = req.user?.id;
|
const userId = req.user?.id;
|
||||||
if (!userId) throw new UnauthorizedException('User not authenticated');
|
if (!userId) throw new UnauthorizedException("User not authenticated");
|
||||||
return this.passengerAuthService.getProfile(userId);
|
return this.passengerAuthService.getProfile(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: admin user management endpoints — implement when admin module is ready
|
// TODO: admin user management endpoints — implement when admin module is ready
|
||||||
|
|
||||||
@Get('users')
|
@Get("users")
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@ApiOperation({ summary: 'List all users (admin)' })
|
@ApiOperation({ summary: "List all users (admin)" })
|
||||||
listUsers(
|
listUsers(
|
||||||
@Query('search') search?: string,
|
@Query("search") search?: string,
|
||||||
@Query('role') role?: string,
|
@Query("role") role?: string,
|
||||||
@Query('status') status?: string,
|
@Query("status") status?: string,
|
||||||
@Query('page') page?: string,
|
@Query("page") page?: string,
|
||||||
@Query('pageSize') pageSize?: string,
|
@Query("pageSize") pageSize?: string,
|
||||||
) {
|
) {
|
||||||
return this.passengerAuthService.listUsers({
|
return this.passengerAuthService.listUsers({
|
||||||
search, role, status,
|
search,
|
||||||
|
role,
|
||||||
|
status,
|
||||||
page: page ? +page : 1,
|
page: page ? +page : 1,
|
||||||
pageSize: pageSize ? +pageSize : 20,
|
pageSize: pageSize ? +pageSize : 20,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('users')
|
@Post("users")
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@ApiOperation({ summary: 'Create user (admin)' })
|
@ApiOperation({ summary: "Create user (admin)" })
|
||||||
createUser(@Body() body: any) {
|
createUser(@Body() body: any) {
|
||||||
return this.passengerAuthService.createUser(body);
|
return this.passengerAuthService.createUser(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('users/:id')
|
@Patch("users/:id")
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@ApiOperation({ summary: 'Update user (admin)' })
|
@ApiOperation({ summary: "Update user (admin)" })
|
||||||
updateUser(@Param('id') id: string, @Body() body: any) {
|
updateUser(@Param("id") id: string, @Body() body: any) {
|
||||||
return this.passengerAuthService.updateUser(id, body);
|
return this.passengerAuthService.updateUser(id, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete('users/:id')
|
@Delete("users/:id")
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@ApiOperation({ summary: 'Delete user (admin)' })
|
@ApiOperation({ summary: "Delete user (admin)" })
|
||||||
deleteUser(@Param('id') id: string) {
|
deleteUser(@Param("id") id: string) {
|
||||||
return this.passengerAuthService.deleteUser(id);
|
return this.passengerAuthService.deleteUser(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('users/:id/reset-password')
|
@Post("users/:id/reset-password")
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@ApiOperation({ summary: 'Reset user password (admin)' })
|
@ApiOperation({ summary: "Reset user password (admin)" })
|
||||||
resetPassword(@Param('id') id: string, @Body() body: { tempPassword: string }) {
|
resetPassword(
|
||||||
|
@Param("id") id: string,
|
||||||
|
@Body() body: { tempPassword: string },
|
||||||
|
) {
|
||||||
return this.passengerAuthService.resetUserPassword(id, body.tempPassword);
|
return this.passengerAuthService.resetUserPassword(id, body.tempPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('fayda/request-password-setup')
|
@Post("fayda/request-password-setup")
|
||||||
@IsPublic()
|
@IsPublic()
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: 'Send OTP to phone for Fayda-verified account password setup' })
|
@ApiOperation({
|
||||||
@ApiResponse({ status: 200, description: 'OTP sent to registered phone number' })
|
summary: "Send OTP to phone for Fayda-verified account password setup",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: "OTP sent to registered phone number",
|
||||||
|
})
|
||||||
@ApiBody({ type: FaydaRequestPasswordSetupDto })
|
@ApiBody({ type: FaydaRequestPasswordSetupDto })
|
||||||
requestFaydaPasswordSetup(@Body() dto: FaydaRequestPasswordSetupDto, @Request() req: any) {
|
requestFaydaPasswordSetup(
|
||||||
return this.passengerAuthService.requestFaydaPasswordSetup(dto.phoneNumber, req);
|
@Body() dto: FaydaRequestPasswordSetupDto,
|
||||||
|
@Request() req: any,
|
||||||
|
) {
|
||||||
|
return this.passengerAuthService.requestFaydaPasswordSetup(
|
||||||
|
dto.phoneNumber,
|
||||||
|
req,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('fayda/verify-and-login')
|
@Post("fayda/verify-and-login")
|
||||||
@IsPublic()
|
@IsPublic()
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: 'Verify OTP and receive session token for Fayda-verified account' })
|
@ApiOperation({
|
||||||
@ApiResponse({ status: 200, description: 'Returns token + requiresPassword flag. Use token with POST /v1/auth/set-fayda-password.' })
|
summary: "Verify OTP and receive session token for Fayda-verified account",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description:
|
||||||
|
"Returns token + requiresPassword flag. Use token with POST /v1/auth/set-fayda-password.",
|
||||||
|
})
|
||||||
@ApiBody({ type: FaydaVerifyAndLoginDto })
|
@ApiBody({ type: FaydaVerifyAndLoginDto })
|
||||||
verifyFaydaAndLogin(@Body() dto: FaydaVerifyAndLoginDto) {
|
verifyFaydaAndLogin(@Body() dto: FaydaVerifyAndLoginDto) {
|
||||||
return this.passengerAuthService.verifyFaydaAndLogin(dto.phoneNumber, dto.otp);
|
return this.passengerAuthService.verifyFaydaAndLogin(
|
||||||
|
dto.phoneNumber,
|
||||||
|
dto.otp,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,84 @@
|
|||||||
import { Body, Controller, Delete, Get, Param, Post, Patch, UseGuards, Query, Req, SetMetadata, BadRequestException, UnauthorizedException } from '@nestjs/common';
|
import {
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse, ApiQuery, ApiBody } from '@nestjs/swagger';
|
Body,
|
||||||
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
|
Controller,
|
||||||
import { Throttle } from '@nestjs/throttler';
|
Delete,
|
||||||
import { BookingsService } from './bookings.service';
|
Get,
|
||||||
import { GuestBookingService } from './guest-booking.service';
|
Param,
|
||||||
import { CreateBookingDto, ModifyBookingDto, CancelBookingDto } from './bookings.dto';
|
Post,
|
||||||
import { CreateGuestBookingDto, GetSavedPassengersDto } from './guest-booking.dto';
|
Patch,
|
||||||
import { JwtGuard } from '../../common/jwt.guard';
|
UseGuards,
|
||||||
import { PassengerAdmin } from '../../common/passenger-guards';
|
Query,
|
||||||
|
Req,
|
||||||
|
SetMetadata,
|
||||||
|
BadRequestException,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import {
|
||||||
|
ApiTags,
|
||||||
|
ApiOperation,
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiResponse,
|
||||||
|
ApiQuery,
|
||||||
|
ApiBody,
|
||||||
|
} from "@nestjs/swagger";
|
||||||
|
import { IsPublic } from "@tria-plc/api-common/modules/auth/decorators/public.decorator";
|
||||||
|
import { BookingsService } from "./bookings.service";
|
||||||
|
import { GuestBookingService } from "./guest-booking.service";
|
||||||
|
import {
|
||||||
|
CreateBookingDto,
|
||||||
|
ModifyBookingDto,
|
||||||
|
CancelBookingDto,
|
||||||
|
} from "./bookings.dto";
|
||||||
|
import {
|
||||||
|
CreateGuestBookingDto,
|
||||||
|
GetSavedPassengersDto,
|
||||||
|
} from "./guest-booking.dto";
|
||||||
|
import { JwtGuard } from "../../common/jwt.guard";
|
||||||
|
import { PassengerAdmin } from "../../common/passenger-guards";
|
||||||
|
|
||||||
@ApiTags('Booking')
|
@ApiTags("Booking")
|
||||||
@Controller('bookings')
|
@Controller("bookings")
|
||||||
@Throttle({ strict: { limit: 20, ttl: 60_000 } })
|
// @Throttle({ strict: { limit: 20, ttl: 60_000 } })
|
||||||
export class BookingsController {
|
export class BookingsController {
|
||||||
constructor(
|
constructor(
|
||||||
private service: BookingsService,
|
private service: BookingsService,
|
||||||
private guestService: GuestBookingService,
|
private guestService: GuestBookingService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get('my')
|
@Get("my")
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Get logged-in user\'s booking history',
|
summary: "Get logged-in user's booking history",
|
||||||
description: 'Returns all bookings for the authenticated user with schedule and payment details'
|
description:
|
||||||
|
"Returns all bookings for the authenticated user with schedule and payment details",
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: "search",
|
||||||
|
required: false,
|
||||||
|
description: "Search by booking reference or station names",
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: "status",
|
||||||
|
required: false,
|
||||||
|
description: "Filter by booking status",
|
||||||
|
})
|
||||||
|
@ApiQuery({ name: "page", required: false, description: "Page number" })
|
||||||
|
@ApiQuery({
|
||||||
|
name: "pageSize",
|
||||||
|
required: false,
|
||||||
|
description: "Items per page",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: "List of user bookings with schedule and passenger details",
|
||||||
})
|
})
|
||||||
@ApiQuery({ name: 'search', required: false, description: 'Search by booking reference or station names' })
|
|
||||||
@ApiQuery({ name: 'status', required: false, description: 'Filter by booking status' })
|
|
||||||
@ApiQuery({ name: 'page', required: false, description: 'Page number' })
|
|
||||||
@ApiQuery({ name: 'pageSize', required: false, description: 'Items per page' })
|
|
||||||
@ApiResponse({ status: 200, description: 'List of user bookings with schedule and passenger details' })
|
|
||||||
getMyBookings(
|
getMyBookings(
|
||||||
@Req() req: any,
|
@Req() req: any,
|
||||||
@Query('search') search?: string,
|
@Query("search") search?: string,
|
||||||
@Query('status') status?: string,
|
@Query("status") status?: string,
|
||||||
@Query('page') page?: string,
|
@Query("page") page?: string,
|
||||||
@Query('pageSize') pageSize?: string,
|
@Query("pageSize") pageSize?: string,
|
||||||
) {
|
) {
|
||||||
const iamUserId = req.user?.id;
|
const iamUserId = req.user?.id;
|
||||||
if (!iamUserId) throw new UnauthorizedException();
|
if (!iamUserId) throw new UnauthorizedException();
|
||||||
@@ -43,81 +86,103 @@ export class BookingsController {
|
|||||||
search,
|
search,
|
||||||
status,
|
status,
|
||||||
page: page ? parseInt(page) : 1,
|
page: page ? parseInt(page) : 1,
|
||||||
pageSize: pageSize ? parseInt(pageSize) : 20
|
pageSize: pageSize ? parseInt(pageSize) : 20,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('by-device')
|
@Get("by-device")
|
||||||
@SetMetadata('isPublic', true)
|
@SetMetadata("isPublic", true)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Get bookings by device ID',
|
summary: "Get bookings by device ID",
|
||||||
description: 'Returns all bookings associated with a device ID (for guest users). Includes saved passenger details and booking history.'
|
description:
|
||||||
|
"Returns all bookings associated with a device ID (for guest users). Includes saved passenger details and booking history.",
|
||||||
})
|
})
|
||||||
@ApiQuery({ name: 'deviceId', required: true, description: 'Device identifier' })
|
@ApiQuery({
|
||||||
@ApiQuery({ name: 'search', required: false, description: 'Search by booking reference or station names' })
|
name: "deviceId",
|
||||||
@ApiQuery({ name: 'status', required: false, description: 'Filter by booking status' })
|
required: true,
|
||||||
@ApiQuery({ name: 'page', required: false, description: 'Page number' })
|
description: "Device identifier",
|
||||||
@ApiQuery({ name: 'pageSize', required: false, description: 'Items per page' })
|
})
|
||||||
@ApiResponse({ status: 200, description: 'List of guest bookings and saved passengers for device' })
|
@ApiQuery({
|
||||||
@ApiResponse({ status: 400, description: 'Device ID is required' })
|
name: "search",
|
||||||
|
required: false,
|
||||||
|
description: "Search by booking reference or station names",
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: "status",
|
||||||
|
required: false,
|
||||||
|
description: "Filter by booking status",
|
||||||
|
})
|
||||||
|
@ApiQuery({ name: "page", required: false, description: "Page number" })
|
||||||
|
@ApiQuery({
|
||||||
|
name: "pageSize",
|
||||||
|
required: false,
|
||||||
|
description: "Items per page",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: "List of guest bookings and saved passengers for device",
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 400, description: "Device ID is required" })
|
||||||
getByDevice(
|
getByDevice(
|
||||||
@Query('deviceId') deviceId?: string,
|
@Query("deviceId") deviceId?: string,
|
||||||
@Query('search') search?: string,
|
@Query("search") search?: string,
|
||||||
@Query('status') status?: string,
|
@Query("status") status?: string,
|
||||||
@Query('page') page?: string,
|
@Query("page") page?: string,
|
||||||
@Query('pageSize') pageSize?: string,
|
@Query("pageSize") pageSize?: string,
|
||||||
) {
|
) {
|
||||||
if (!deviceId) throw new BadRequestException('Device ID is required');
|
if (!deviceId) throw new BadRequestException("Device ID is required");
|
||||||
return this.service.findByDeviceId(deviceId, {
|
return this.service.findByDeviceId(deviceId, {
|
||||||
search,
|
search,
|
||||||
status,
|
status,
|
||||||
page: page ? parseInt(page) : 1,
|
page: page ? parseInt(page) : 1,
|
||||||
pageSize: pageSize ? parseInt(pageSize) : 20
|
pageSize: pageSize ? parseInt(pageSize) : 20,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@SetMetadata('isPublic', true)
|
@SetMetadata("isPublic", true)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
description: 'Returns paginated list of bookings. Use `returnLegStatus=OUTBOUND_ONLY` to find round-trip no-shows on the return leg, `INBOUND_ONLY` for passengers who only used the return leg, `BOTH_USED` for fully completed round-trips, and `NEITHER_USED` for confirmed but not yet boarded.'
|
description:
|
||||||
|
"Returns paginated list of bookings. Use `returnLegStatus=OUTBOUND_ONLY` to find round-trip no-shows on the return leg, `INBOUND_ONLY` for passengers who only used the return leg, `BOTH_USED` for fully completed round-trips, and `NEITHER_USED` for confirmed but not yet boarded.",
|
||||||
})
|
})
|
||||||
@ApiQuery({ name: 'search', required: false })
|
@ApiQuery({ name: "search", required: false })
|
||||||
@ApiQuery({ name: 'status', required: false })
|
@ApiQuery({ name: "status", required: false })
|
||||||
@ApiQuery({ name: 'returnLegStatus', required: false })
|
@ApiQuery({ name: "returnLegStatus", required: false })
|
||||||
@ApiQuery({ name: 'bookingType', required: false })
|
@ApiQuery({ name: "bookingType", required: false })
|
||||||
@ApiQuery({ name: 'paymentStatus', required: false })
|
@ApiQuery({ name: "paymentStatus", required: false })
|
||||||
@ApiQuery({ name: 'dateFrom', required: false })
|
@ApiQuery({ name: "dateFrom", required: false })
|
||||||
@ApiQuery({ name: 'dateTo', required: false })
|
@ApiQuery({ name: "dateTo", required: false })
|
||||||
@ApiQuery({ name: 'page', required: false })
|
@ApiQuery({ name: "page", required: false })
|
||||||
@ApiQuery({ name: 'pageSize', required: false })
|
@ApiQuery({ name: "pageSize", required: false })
|
||||||
findAll(
|
findAll(
|
||||||
@Query('search') search?: string,
|
@Query("search") search?: string,
|
||||||
@Query('status') status?: string,
|
@Query("status") status?: string,
|
||||||
@Query('returnLegStatus') returnLegStatus?: string,
|
@Query("returnLegStatus") returnLegStatus?: string,
|
||||||
@Query('bookingType') bookingType?: string,
|
@Query("bookingType") bookingType?: string,
|
||||||
@Query('paymentStatus') paymentStatus?: string,
|
@Query("paymentStatus") paymentStatus?: string,
|
||||||
@Query('dateFrom') dateFrom?: string,
|
@Query("dateFrom") dateFrom?: string,
|
||||||
@Query('dateTo') dateTo?: string,
|
@Query("dateTo") dateTo?: string,
|
||||||
@Query('page') page?: string,
|
@Query("page") page?: string,
|
||||||
@Query('pageSize') pageSize?: string,
|
@Query("pageSize") pageSize?: string,
|
||||||
) {
|
) {
|
||||||
return this.service.findAll({
|
return this.service.findAll({
|
||||||
search,
|
search,
|
||||||
status,
|
status,
|
||||||
returnLegStatus,
|
returnLegStatus,
|
||||||
bookingType,
|
bookingType,
|
||||||
paymentStatus,
|
paymentStatus,
|
||||||
dateFrom,
|
dateFrom,
|
||||||
dateTo,
|
dateTo,
|
||||||
page: page ? parseInt(page) : 1,
|
page: page ? parseInt(page) : 1,
|
||||||
pageSize: pageSize ? parseInt(pageSize) : 20
|
pageSize: pageSize ? parseInt(pageSize) : 20,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('guest')
|
@Post("guest")
|
||||||
@SetMetadata('isPublic', true)
|
@SetMetadata("isPublic", true)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Create guest booking — ONE_WAY | ROUND_TRIP | TRANSIT | ROUND_TRIP_TRANSIT (no login required)',
|
summary:
|
||||||
|
"Create guest booking — ONE_WAY | ROUND_TRIP | TRANSIT | ROUND_TRIP_TRANSIT (no login required)",
|
||||||
description: `Creates a booking without requiring login. Supports all four booking types.
|
description: `Creates a booking without requiring login. Supports all four booking types.
|
||||||
|
|
||||||
**bookingType: ONE_WAY (default)**
|
**bookingType: ONE_WAY (default)**
|
||||||
@@ -138,152 +203,170 @@ export class BookingsController {
|
|||||||
|
|
||||||
**Optional account creation:** set createAccount=true with password — creates account from first passenger details, loyalty + wallet initialised.
|
**Optional account creation:** set createAccount=true with password — creates account from first passenger details, loyalty + wallet initialised.
|
||||||
|
|
||||||
**Verifayda:** Ethiopian nationals verified; international passengers require passportNumber + passportCountry.`
|
**Verifayda:** Ethiopian nationals verified; international passengers require passportNumber + passportCountry.`,
|
||||||
})
|
})
|
||||||
@ApiBody({
|
@ApiBody({
|
||||||
type: CreateGuestBookingDto,
|
type: CreateGuestBookingDto,
|
||||||
examples: {
|
examples: {
|
||||||
ONE_WAY: {
|
ONE_WAY: {
|
||||||
summary: 'ONE_WAY — single direct journey (guest)',
|
summary: "ONE_WAY — single direct journey (guest)",
|
||||||
value: {
|
value: {
|
||||||
scheduleId: 'schedule-uuid',
|
scheduleId: "schedule-uuid",
|
||||||
holdId: 'hold-uuid',
|
holdId: "hold-uuid",
|
||||||
originStationId: 'station-uuid',
|
originStationId: "station-uuid",
|
||||||
destinationStationId: 'station-uuid',
|
destinationStationId: "station-uuid",
|
||||||
seatClassId: 'seat-class-uuid',
|
seatClassId: "seat-class-uuid",
|
||||||
bookingType: 'ONE_WAY',
|
bookingType: "ONE_WAY",
|
||||||
displayCurrency: 'ETB',
|
displayCurrency: "ETB",
|
||||||
passengers: [{
|
passengers: [
|
||||||
seatId: 'seat-uuid',
|
{
|
||||||
passengerName: 'Abebe Kebede',
|
seatId: "seat-uuid",
|
||||||
dateOfBirth: '1990-05-15',
|
passengerName: "Abebe Kebede",
|
||||||
idDocumentType: 'NATIONAL_ID',
|
dateOfBirth: "1990-05-15",
|
||||||
idDocumentNumber: 'ET123456789',
|
idDocumentType: "NATIONAL_ID",
|
||||||
nationality: 'Ethiopian',
|
idDocumentNumber: "ET123456789",
|
||||||
phone: '+251911234567',
|
nationality: "Ethiopian",
|
||||||
email: 'abebe@email.com',
|
phone: "+251911234567",
|
||||||
}],
|
email: "abebe@email.com",
|
||||||
|
},
|
||||||
|
],
|
||||||
savePassengerDetails: true,
|
savePassengerDetails: true,
|
||||||
deviceId: 'device-uuid-123',
|
deviceId: "device-uuid-123",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ROUND_TRIP: {
|
ROUND_TRIP: {
|
||||||
summary: 'ROUND_TRIP — outbound + return, single PNR (guest)',
|
summary: "ROUND_TRIP — outbound + return, single PNR (guest)",
|
||||||
value: {
|
value: {
|
||||||
scheduleId: 'outbound-schedule-uuid',
|
scheduleId: "outbound-schedule-uuid",
|
||||||
holdId: 'outbound-hold-uuid',
|
holdId: "outbound-hold-uuid",
|
||||||
originStationId: 'addis-station-uuid',
|
originStationId: "addis-station-uuid",
|
||||||
destinationStationId: 'djibouti-station-uuid',
|
destinationStationId: "djibouti-station-uuid",
|
||||||
seatClassId: 'seat-class-uuid',
|
seatClassId: "seat-class-uuid",
|
||||||
bookingType: 'ROUND_TRIP',
|
bookingType: "ROUND_TRIP",
|
||||||
returnScheduleId: 'return-schedule-uuid',
|
returnScheduleId: "return-schedule-uuid",
|
||||||
returnHoldId: 'return-hold-uuid',
|
returnHoldId: "return-hold-uuid",
|
||||||
returnOriginStationId: 'djibouti-station-uuid',
|
returnOriginStationId: "djibouti-station-uuid",
|
||||||
returnDestinationStationId: 'addis-station-uuid',
|
returnDestinationStationId: "addis-station-uuid",
|
||||||
returnSeatClassId: 'seat-class-uuid',
|
returnSeatClassId: "seat-class-uuid",
|
||||||
displayCurrency: 'ETB',
|
displayCurrency: "ETB",
|
||||||
passengers: [{
|
passengers: [
|
||||||
seatId: 'outbound-seat-uuid',
|
{
|
||||||
returnSeatId: 'return-seat-uuid',
|
seatId: "outbound-seat-uuid",
|
||||||
passengerName: 'Abebe Kebede',
|
returnSeatId: "return-seat-uuid",
|
||||||
dateOfBirth: '1990-05-15',
|
passengerName: "Abebe Kebede",
|
||||||
idDocumentType: 'NATIONAL_ID',
|
dateOfBirth: "1990-05-15",
|
||||||
idDocumentNumber: 'ET123456789',
|
idDocumentType: "NATIONAL_ID",
|
||||||
nationality: 'Ethiopian',
|
idDocumentNumber: "ET123456789",
|
||||||
phone: '+251911234567',
|
nationality: "Ethiopian",
|
||||||
}],
|
phone: "+251911234567",
|
||||||
|
},
|
||||||
|
],
|
||||||
savePassengerDetails: true,
|
savePassengerDetails: true,
|
||||||
deviceId: 'device-uuid-123',
|
deviceId: "device-uuid-123",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
TRANSIT: {
|
TRANSIT: {
|
||||||
summary: 'TRANSIT — connecting train, single PNR (guest)',
|
summary: "TRANSIT — connecting train, single PNR (guest)",
|
||||||
value: {
|
value: {
|
||||||
scheduleId: 'leg1-schedule-uuid',
|
scheduleId: "leg1-schedule-uuid",
|
||||||
holdId: 'leg1-hold-uuid',
|
holdId: "leg1-hold-uuid",
|
||||||
originStationId: 'addis-station-uuid',
|
originStationId: "addis-station-uuid",
|
||||||
destinationStationId: 'diredawa-station-uuid',
|
destinationStationId: "diredawa-station-uuid",
|
||||||
seatClassId: 'seat-class-uuid',
|
seatClassId: "seat-class-uuid",
|
||||||
bookingType: 'TRANSIT',
|
bookingType: "TRANSIT",
|
||||||
leg2ScheduleId: 'leg2-schedule-uuid',
|
leg2ScheduleId: "leg2-schedule-uuid",
|
||||||
leg2HoldId: 'leg2-hold-uuid',
|
leg2HoldId: "leg2-hold-uuid",
|
||||||
transitStationId: 'diredawa-station-uuid',
|
transitStationId: "diredawa-station-uuid",
|
||||||
leg2DestinationStationId: 'djibouti-station-uuid',
|
leg2DestinationStationId: "djibouti-station-uuid",
|
||||||
displayCurrency: 'ETB',
|
displayCurrency: "ETB",
|
||||||
passengers: [{
|
passengers: [
|
||||||
seatId: 'leg1-seat-uuid',
|
{
|
||||||
leg2SeatId: 'leg2-seat-uuid',
|
seatId: "leg1-seat-uuid",
|
||||||
passengerName: 'Abebe Kebede',
|
leg2SeatId: "leg2-seat-uuid",
|
||||||
dateOfBirth: '1990-05-15',
|
passengerName: "Abebe Kebede",
|
||||||
idDocumentType: 'NATIONAL_ID',
|
dateOfBirth: "1990-05-15",
|
||||||
idDocumentNumber: 'ET123456789',
|
idDocumentType: "NATIONAL_ID",
|
||||||
nationality: 'Ethiopian',
|
idDocumentNumber: "ET123456789",
|
||||||
phone: '+251911234567',
|
nationality: "Ethiopian",
|
||||||
}],
|
phone: "+251911234567",
|
||||||
deviceId: 'device-uuid-123',
|
},
|
||||||
|
],
|
||||||
|
deviceId: "device-uuid-123",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ROUND_TRIP_TRANSIT: {
|
ROUND_TRIP_TRANSIT: {
|
||||||
summary: 'ROUND_TRIP_TRANSIT — round trip via connecting trains, 4 holds (guest)',
|
summary:
|
||||||
|
"ROUND_TRIP_TRANSIT — round trip via connecting trains, 4 holds (guest)",
|
||||||
value: {
|
value: {
|
||||||
scheduleId: 'ob-leg1-schedule-uuid',
|
scheduleId: "ob-leg1-schedule-uuid",
|
||||||
holdId: 'ob-leg1-hold-uuid',
|
holdId: "ob-leg1-hold-uuid",
|
||||||
originStationId: 'addis-station-uuid',
|
originStationId: "addis-station-uuid",
|
||||||
destinationStationId: 'diredawa-station-uuid',
|
destinationStationId: "diredawa-station-uuid",
|
||||||
seatClassId: 'seat-class-uuid',
|
seatClassId: "seat-class-uuid",
|
||||||
bookingType: 'ROUND_TRIP_TRANSIT',
|
bookingType: "ROUND_TRIP_TRANSIT",
|
||||||
leg2ScheduleId: 'ob-leg2-schedule-uuid',
|
leg2ScheduleId: "ob-leg2-schedule-uuid",
|
||||||
leg2HoldId: 'ob-leg2-hold-uuid',
|
leg2HoldId: "ob-leg2-hold-uuid",
|
||||||
transitStationId: 'diredawa-station-uuid',
|
transitStationId: "diredawa-station-uuid",
|
||||||
leg2DestinationStationId: 'djibouti-station-uuid',
|
leg2DestinationStationId: "djibouti-station-uuid",
|
||||||
returnScheduleId: 'ret-leg1-schedule-uuid',
|
returnScheduleId: "ret-leg1-schedule-uuid",
|
||||||
returnHoldId: 'ret-leg1-hold-uuid',
|
returnHoldId: "ret-leg1-hold-uuid",
|
||||||
returnOriginStationId: 'djibouti-station-uuid',
|
returnOriginStationId: "djibouti-station-uuid",
|
||||||
returnDestinationStationId: 'diredawa-station-uuid',
|
returnDestinationStationId: "diredawa-station-uuid",
|
||||||
returnLeg2ScheduleId: 'ret-leg2-schedule-uuid',
|
returnLeg2ScheduleId: "ret-leg2-schedule-uuid",
|
||||||
returnLeg2HoldId: 'ret-leg2-hold-uuid',
|
returnLeg2HoldId: "ret-leg2-hold-uuid",
|
||||||
returnTransitStationId: 'diredawa-station-uuid',
|
returnTransitStationId: "diredawa-station-uuid",
|
||||||
returnLeg2DestinationStationId: 'addis-station-uuid',
|
returnLeg2DestinationStationId: "addis-station-uuid",
|
||||||
displayCurrency: 'ETB',
|
displayCurrency: "ETB",
|
||||||
passengers: [{
|
passengers: [
|
||||||
seatId: 'ob-leg1-seat-uuid',
|
{
|
||||||
leg2SeatId: 'ob-leg2-seat-uuid',
|
seatId: "ob-leg1-seat-uuid",
|
||||||
returnSeatId: 'ret-leg1-seat-uuid',
|
leg2SeatId: "ob-leg2-seat-uuid",
|
||||||
returnLeg2SeatId: 'ret-leg2-seat-uuid',
|
returnSeatId: "ret-leg1-seat-uuid",
|
||||||
passengerName: 'Abebe Kebede',
|
returnLeg2SeatId: "ret-leg2-seat-uuid",
|
||||||
dateOfBirth: '1990-05-15',
|
passengerName: "Abebe Kebede",
|
||||||
idDocumentType: 'NATIONAL_ID',
|
dateOfBirth: "1990-05-15",
|
||||||
idDocumentNumber: 'ET123456789',
|
idDocumentType: "NATIONAL_ID",
|
||||||
nationality: 'Ethiopian',
|
idDocumentNumber: "ET123456789",
|
||||||
phone: '+251911234567',
|
nationality: "Ethiopian",
|
||||||
}],
|
phone: "+251911234567",
|
||||||
deviceId: 'device-uuid-123',
|
},
|
||||||
|
],
|
||||||
|
deviceId: "device-uuid-123",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 201, description: 'Booking created successfully with fareBreakdown' })
|
@ApiResponse({
|
||||||
@ApiResponse({ status: 400, description: 'Missing required seat IDs for bookingType, or Verifayda verification failed' })
|
status: 201,
|
||||||
|
description: "Booking created successfully with fareBreakdown",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 400,
|
||||||
|
description:
|
||||||
|
"Missing required seat IDs for bookingType, or Verifayda verification failed",
|
||||||
|
})
|
||||||
createGuest(@Req() req: any, @Body() dto: CreateGuestBookingDto) {
|
createGuest(@Req() req: any, @Body() dto: CreateGuestBookingDto) {
|
||||||
return this.guestService.createGuestBooking(dto, req);
|
return this.guestService.createGuestBooking(dto, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('saved-passengers')
|
@Get("saved-passengers")
|
||||||
@SetMetadata('isPublic', true)
|
@SetMetadata("isPublic", true)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Get saved passenger profiles',
|
summary: "Get saved passenger profiles",
|
||||||
description: 'Retrieve saved passenger details by userId (if logged in) or deviceId (for guest users)'
|
description:
|
||||||
|
"Retrieve saved passenger details by userId (if logged in) or deviceId (for guest users)",
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 200, description: 'List of saved passenger profiles' })
|
@ApiResponse({ status: 200, description: "List of saved passenger profiles" })
|
||||||
getSavedPassengers(@Query() query: GetSavedPassengersDto) {
|
getSavedPassengers(@Query() query: GetSavedPassengersDto) {
|
||||||
return this.guestService.getSavedPassengers(undefined, query.deviceId);
|
return this.guestService.getSavedPassengers(undefined, query.deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Create booking — ONE_WAY | ROUND_TRIP | TRANSIT | ROUND_TRIP_TRANSIT',
|
summary:
|
||||||
|
"Create booking — ONE_WAY | ROUND_TRIP | TRANSIT | ROUND_TRIP_TRANSIT",
|
||||||
description: `Creates a booking for a logged-in passenger. bookingType controls which fields are required.
|
description: `Creates a booking for a logged-in passenger. bookingType controls which fields are required.
|
||||||
|
|
||||||
**ONE_WAY**
|
**ONE_WAY**
|
||||||
@@ -306,164 +389,193 @@ export class BookingsController {
|
|||||||
|
|
||||||
**Age-Based Pricing (all types)**
|
**Age-Based Pricing (all types)**
|
||||||
- ADULT (≥5 years): full fare per leg
|
- ADULT (≥5 years): full fare per leg
|
||||||
- CHILD (<5 years): first child FREE per booking, subsequent children full fare`
|
- CHILD (<5 years): first child FREE per booking, subsequent children full fare`,
|
||||||
})
|
})
|
||||||
@ApiBody({
|
@ApiBody({
|
||||||
type: CreateBookingDto,
|
type: CreateBookingDto,
|
||||||
examples: {
|
examples: {
|
||||||
ONE_WAY: {
|
ONE_WAY: {
|
||||||
summary: 'ONE_WAY — single direct journey',
|
summary: "ONE_WAY — single direct journey",
|
||||||
value: {
|
value: {
|
||||||
passengerId: 'passenger-uuid',
|
passengerId: "passenger-uuid",
|
||||||
scheduleId: 'schedule-uuid',
|
scheduleId: "schedule-uuid",
|
||||||
holdId: 'hold-uuid',
|
holdId: "hold-uuid",
|
||||||
originStationId: 'station-uuid',
|
originStationId: "station-uuid",
|
||||||
destinationStationId: 'station-uuid',
|
destinationStationId: "station-uuid",
|
||||||
seatClassId: 'seat-class-uuid',
|
seatClassId: "seat-class-uuid",
|
||||||
bookingType: 'ONE_WAY',
|
bookingType: "ONE_WAY",
|
||||||
displayCurrency: 'ETB',
|
displayCurrency: "ETB",
|
||||||
passengers: [{
|
passengers: [
|
||||||
seatId: 'seat-uuid',
|
{
|
||||||
passengerName: 'Abebe Kebede',
|
seatId: "seat-uuid",
|
||||||
dateOfBirth: '1990-05-15',
|
passengerName: "Abebe Kebede",
|
||||||
idDocumentType: 'NATIONAL_ID',
|
dateOfBirth: "1990-05-15",
|
||||||
idDocumentNumber: 'ET123456789',
|
idDocumentType: "NATIONAL_ID",
|
||||||
nationality: 'Ethiopian',
|
idDocumentNumber: "ET123456789",
|
||||||
}],
|
nationality: "Ethiopian",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ROUND_TRIP: {
|
ROUND_TRIP: {
|
||||||
summary: 'ROUND_TRIP — outbound + return, single PNR',
|
summary: "ROUND_TRIP — outbound + return, single PNR",
|
||||||
value: {
|
value: {
|
||||||
passengerId: 'passenger-uuid',
|
passengerId: "passenger-uuid",
|
||||||
scheduleId: 'outbound-schedule-uuid',
|
scheduleId: "outbound-schedule-uuid",
|
||||||
holdId: 'outbound-hold-uuid',
|
holdId: "outbound-hold-uuid",
|
||||||
originStationId: 'addis-station-uuid',
|
originStationId: "addis-station-uuid",
|
||||||
destinationStationId: 'djibouti-station-uuid',
|
destinationStationId: "djibouti-station-uuid",
|
||||||
seatClassId: 'seat-class-uuid',
|
seatClassId: "seat-class-uuid",
|
||||||
bookingType: 'ROUND_TRIP',
|
bookingType: "ROUND_TRIP",
|
||||||
returnScheduleId: 'return-schedule-uuid',
|
returnScheduleId: "return-schedule-uuid",
|
||||||
returnHoldId: 'return-hold-uuid',
|
returnHoldId: "return-hold-uuid",
|
||||||
returnOriginStationId: 'djibouti-station-uuid',
|
returnOriginStationId: "djibouti-station-uuid",
|
||||||
returnDestinationStationId: 'addis-station-uuid',
|
returnDestinationStationId: "addis-station-uuid",
|
||||||
returnSeatClassId: 'seat-class-uuid',
|
returnSeatClassId: "seat-class-uuid",
|
||||||
displayCurrency: 'ETB',
|
displayCurrency: "ETB",
|
||||||
passengers: [{
|
passengers: [
|
||||||
seatId: 'outbound-seat-uuid',
|
{
|
||||||
returnSeatId: 'return-seat-uuid',
|
seatId: "outbound-seat-uuid",
|
||||||
passengerName: 'Abebe Kebede',
|
returnSeatId: "return-seat-uuid",
|
||||||
dateOfBirth: '1990-05-15',
|
passengerName: "Abebe Kebede",
|
||||||
idDocumentType: 'NATIONAL_ID',
|
dateOfBirth: "1990-05-15",
|
||||||
idDocumentNumber: 'ET123456789',
|
idDocumentType: "NATIONAL_ID",
|
||||||
nationality: 'Ethiopian',
|
idDocumentNumber: "ET123456789",
|
||||||
}],
|
nationality: "Ethiopian",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
TRANSIT: {
|
TRANSIT: {
|
||||||
summary: 'TRANSIT — connecting train, single PNR',
|
summary: "TRANSIT — connecting train, single PNR",
|
||||||
value: {
|
value: {
|
||||||
passengerId: 'passenger-uuid',
|
passengerId: "passenger-uuid",
|
||||||
scheduleId: 'leg1-schedule-uuid',
|
scheduleId: "leg1-schedule-uuid",
|
||||||
holdId: 'leg1-hold-uuid',
|
holdId: "leg1-hold-uuid",
|
||||||
originStationId: 'addis-station-uuid',
|
originStationId: "addis-station-uuid",
|
||||||
destinationStationId: 'diredawa-station-uuid',
|
destinationStationId: "diredawa-station-uuid",
|
||||||
seatClassId: 'seat-class-uuid',
|
seatClassId: "seat-class-uuid",
|
||||||
bookingType: 'TRANSIT',
|
bookingType: "TRANSIT",
|
||||||
leg2ScheduleId: 'leg2-schedule-uuid',
|
leg2ScheduleId: "leg2-schedule-uuid",
|
||||||
leg2HoldId: 'leg2-hold-uuid',
|
leg2HoldId: "leg2-hold-uuid",
|
||||||
transitStationId: 'diredawa-station-uuid',
|
transitStationId: "diredawa-station-uuid",
|
||||||
leg2DestinationStationId: 'djibouti-station-uuid',
|
leg2DestinationStationId: "djibouti-station-uuid",
|
||||||
displayCurrency: 'ETB',
|
displayCurrency: "ETB",
|
||||||
passengers: [{
|
passengers: [
|
||||||
seatId: 'leg1-seat-uuid',
|
{
|
||||||
leg2SeatId: 'leg2-seat-uuid',
|
seatId: "leg1-seat-uuid",
|
||||||
passengerName: 'Abebe Kebede',
|
leg2SeatId: "leg2-seat-uuid",
|
||||||
dateOfBirth: '1990-05-15',
|
passengerName: "Abebe Kebede",
|
||||||
idDocumentType: 'NATIONAL_ID',
|
dateOfBirth: "1990-05-15",
|
||||||
idDocumentNumber: 'ET123456789',
|
idDocumentType: "NATIONAL_ID",
|
||||||
nationality: 'Ethiopian',
|
idDocumentNumber: "ET123456789",
|
||||||
}],
|
nationality: "Ethiopian",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ROUND_TRIP_TRANSIT: {
|
ROUND_TRIP_TRANSIT: {
|
||||||
summary: 'ROUND_TRIP_TRANSIT — round trip via connecting trains, 4 holds',
|
summary:
|
||||||
|
"ROUND_TRIP_TRANSIT — round trip via connecting trains, 4 holds",
|
||||||
value: {
|
value: {
|
||||||
passengerId: 'passenger-uuid',
|
passengerId: "passenger-uuid",
|
||||||
scheduleId: 'ob-leg1-schedule-uuid',
|
scheduleId: "ob-leg1-schedule-uuid",
|
||||||
holdId: 'ob-leg1-hold-uuid',
|
holdId: "ob-leg1-hold-uuid",
|
||||||
originStationId: 'addis-station-uuid',
|
originStationId: "addis-station-uuid",
|
||||||
destinationStationId: 'diredawa-station-uuid',
|
destinationStationId: "diredawa-station-uuid",
|
||||||
seatClassId: 'seat-class-uuid',
|
seatClassId: "seat-class-uuid",
|
||||||
bookingType: 'ROUND_TRIP_TRANSIT',
|
bookingType: "ROUND_TRIP_TRANSIT",
|
||||||
leg2ScheduleId: 'ob-leg2-schedule-uuid',
|
leg2ScheduleId: "ob-leg2-schedule-uuid",
|
||||||
leg2HoldId: 'ob-leg2-hold-uuid',
|
leg2HoldId: "ob-leg2-hold-uuid",
|
||||||
transitStationId: 'diredawa-station-uuid',
|
transitStationId: "diredawa-station-uuid",
|
||||||
leg2DestinationStationId: 'djibouti-station-uuid',
|
leg2DestinationStationId: "djibouti-station-uuid",
|
||||||
returnScheduleId: 'ret-leg1-schedule-uuid',
|
returnScheduleId: "ret-leg1-schedule-uuid",
|
||||||
returnHoldId: 'ret-leg1-hold-uuid',
|
returnHoldId: "ret-leg1-hold-uuid",
|
||||||
returnOriginStationId: 'djibouti-station-uuid',
|
returnOriginStationId: "djibouti-station-uuid",
|
||||||
returnDestinationStationId: 'diredawa-station-uuid',
|
returnDestinationStationId: "diredawa-station-uuid",
|
||||||
returnLeg2ScheduleId: 'ret-leg2-schedule-uuid',
|
returnLeg2ScheduleId: "ret-leg2-schedule-uuid",
|
||||||
returnLeg2HoldId: 'ret-leg2-hold-uuid',
|
returnLeg2HoldId: "ret-leg2-hold-uuid",
|
||||||
returnTransitStationId: 'diredawa-station-uuid',
|
returnTransitStationId: "diredawa-station-uuid",
|
||||||
returnLeg2DestinationStationId: 'addis-station-uuid',
|
returnLeg2DestinationStationId: "addis-station-uuid",
|
||||||
displayCurrency: 'ETB',
|
displayCurrency: "ETB",
|
||||||
passengers: [{
|
passengers: [
|
||||||
seatId: 'ob-leg1-seat-uuid',
|
{
|
||||||
leg2SeatId: 'ob-leg2-seat-uuid',
|
seatId: "ob-leg1-seat-uuid",
|
||||||
returnSeatId: 'ret-leg1-seat-uuid',
|
leg2SeatId: "ob-leg2-seat-uuid",
|
||||||
returnLeg2SeatId: 'ret-leg2-seat-uuid',
|
returnSeatId: "ret-leg1-seat-uuid",
|
||||||
passengerName: 'Abebe Kebede',
|
returnLeg2SeatId: "ret-leg2-seat-uuid",
|
||||||
dateOfBirth: '1990-05-15',
|
passengerName: "Abebe Kebede",
|
||||||
idDocumentType: 'NATIONAL_ID',
|
dateOfBirth: "1990-05-15",
|
||||||
idDocumentNumber: 'ET123456789',
|
idDocumentType: "NATIONAL_ID",
|
||||||
nationality: 'Ethiopian',
|
idDocumentNumber: "ET123456789",
|
||||||
}],
|
nationality: "Ethiopian",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 201, description: 'Booking created with fare breakdown' })
|
@ApiResponse({
|
||||||
@ApiResponse({ status: 400, description: 'Missing required fields for bookingType, or Verifayda verification failed' })
|
status: 201,
|
||||||
@ApiResponse({ status: 404, description: 'Schedule or seat hold not found' })
|
description: "Booking created with fare breakdown",
|
||||||
create(@Body() dto: CreateBookingDto) {
|
|
||||||
return this.service.create(dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get(':id/usage')
|
|
||||||
@SetMetadata('isPublic', true)
|
|
||||||
@ApiOperation({
|
|
||||||
description: 'Returns list of modules/data that reference this booking'
|
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 200, description: 'Usage information retrieved' })
|
@ApiResponse({
|
||||||
@ApiResponse({ status: 404, description: 'Booking not found' })
|
status: 400,
|
||||||
checkUsage(@Param('id') id: string) {
|
description:
|
||||||
|
"Missing required fields for bookingType, or Verifayda verification failed",
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 404, description: "Schedule or seat hold not found" })
|
||||||
|
create(@Body() dto: CreateBookingDto) {
|
||||||
|
return this.service.create(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(":id/usage")
|
||||||
|
@SetMetadata("isPublic", true)
|
||||||
|
@ApiOperation({
|
||||||
|
description: "Returns list of modules/data that reference this booking",
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: "Usage information retrieved" })
|
||||||
|
@ApiResponse({ status: 404, description: "Booking not found" })
|
||||||
|
checkUsage(@Param("id") id: string) {
|
||||||
return this.service.checkBookingUsage(id);
|
return this.service.checkBookingUsage(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('by-phone')
|
@Get("by-phone")
|
||||||
@SetMetadata('isPublic', true)
|
@SetMetadata("isPublic", true)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Find bookings by phone number (no auth required)',
|
summary: "Find bookings by phone number (no auth required)",
|
||||||
description: `Returns all bookings where the contact phone matches the provided number.
|
description: `Returns all bookings where the contact phone matches the provided number.
|
||||||
Accepts Ethiopian local format (09XXXXXXXX) and international format (+251XXXXXXXXX).
|
Accepts Ethiopian local format (09XXXXXXXX) and international format (+251XXXXXXXXX).
|
||||||
Results are ordered most-recent first. Use the returned \`bookingRef\` to open booking detail.`
|
Results are ordered most-recent first. Use the returned \`bookingRef\` to open booking detail.`,
|
||||||
})
|
})
|
||||||
@ApiQuery({ name: 'phone', required: true, description: 'Phone number in local (09…) or international (+251…) format' })
|
@ApiQuery({
|
||||||
@ApiQuery({ name: 'status', required: false, description: 'Filter by booking status' })
|
name: "phone",
|
||||||
@ApiQuery({ name: 'page', required: false })
|
required: true,
|
||||||
@ApiQuery({ name: 'pageSize', required: false })
|
description: "Phone number in local (09…) or international (+251…) format",
|
||||||
@ApiResponse({ status: 200, description: 'Paginated list of bookings for this phone number' })
|
})
|
||||||
@ApiResponse({ status: 400, description: 'Phone number missing or invalid' })
|
@ApiQuery({
|
||||||
|
name: "status",
|
||||||
|
required: false,
|
||||||
|
description: "Filter by booking status",
|
||||||
|
})
|
||||||
|
@ApiQuery({ name: "page", required: false })
|
||||||
|
@ApiQuery({ name: "pageSize", required: false })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: "Paginated list of bookings for this phone number",
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 400, description: "Phone number missing or invalid" })
|
||||||
findByPhone(
|
findByPhone(
|
||||||
@Query('phone') phone?: string,
|
@Query("phone") phone?: string,
|
||||||
@Query('status') status?: string,
|
@Query("status") status?: string,
|
||||||
@Query('page') page?: string,
|
@Query("page") page?: string,
|
||||||
@Query('pageSize') pageSize?: string,
|
@Query("pageSize") pageSize?: string,
|
||||||
) {
|
) {
|
||||||
if (!phone?.trim()) throw new BadRequestException('Phone number is required');
|
if (!phone?.trim())
|
||||||
const digits = phone.replace(/[^\d]/g, '');
|
throw new BadRequestException("Phone number is required");
|
||||||
if (digits.length < 7) throw new BadRequestException('Phone number is too short');
|
const digits = phone.replace(/[^\d]/g, "");
|
||||||
|
if (digits.length < 7)
|
||||||
|
throw new BadRequestException("Phone number is too short");
|
||||||
return this.service.findByPhone(phone.trim(), {
|
return this.service.findByPhone(phone.trim(), {
|
||||||
status,
|
status,
|
||||||
page: page ? parseInt(page) : 1,
|
page: page ? parseInt(page) : 1,
|
||||||
@@ -471,65 +583,82 @@ Results are ordered most-recent first. Use the returned \`bookingRef\` to open b
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':bookingRef')
|
@Get(":bookingRef")
|
||||||
@SetMetadata('isPublic', true)
|
@SetMetadata("isPublic", true)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Get booking details by reference (no auth required)',
|
summary: "Get booking details by reference (no auth required)",
|
||||||
description: 'Returns booking with passenger categories, Verifayda verification status, and multi-currency amounts. Works for both guest and authenticated bookings.'
|
description:
|
||||||
|
"Returns booking with passenger categories, Verifayda verification status, and multi-currency amounts. Works for both guest and authenticated bookings.",
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 200, description: 'Booking details with adult/child counts and currency conversion' })
|
@ApiResponse({
|
||||||
@ApiResponse({ status: 404, description: 'Booking not found' })
|
status: 200,
|
||||||
getByRef(@Param('bookingRef') ref: string) {
|
description:
|
||||||
return this.service.getByRef(ref);
|
"Booking details with adult/child counts and currency conversion",
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 404, description: "Booking not found" })
|
||||||
|
getByRef(@Param("bookingRef") ref: string) {
|
||||||
|
return this.service.getByRef(ref);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':bookingRef/modify')
|
@Patch(":bookingRef/modify")
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Modify booking seats or trip',
|
summary: "Modify booking seats or trip",
|
||||||
description: 'Allows modification of confirmed bookings before departure'
|
description: "Allows modification of confirmed bookings before departure",
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: "Booking modified successfully" })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 400,
|
||||||
|
description: "Cannot modify cancelled or past bookings",
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 200, description: 'Booking modified successfully' })
|
|
||||||
@ApiResponse({ status: 400, description: 'Cannot modify cancelled or past bookings' })
|
|
||||||
modify(@Body() dto: ModifyBookingDto) {
|
modify(@Body() dto: ModifyBookingDto) {
|
||||||
return this.service.modify(dto);
|
return this.service.modify(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(":id")
|
||||||
@PassengerAdmin()
|
@PassengerAdmin()
|
||||||
@ApiBearerAuth('IAM-auth')
|
@ApiBearerAuth("IAM-auth")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
description: 'Permanently deletes a booking record'
|
description: "Permanently deletes a booking record",
|
||||||
})
|
})
|
||||||
@ApiQuery({ name: 'cascade', required: false, type: Boolean, description: 'Force delete with all related data' })
|
@ApiQuery({
|
||||||
@ApiResponse({ status: 200, description: 'Booking deleted successfully' })
|
name: "cascade",
|
||||||
@ApiResponse({ status: 404, description: 'Booking not found' })
|
required: false,
|
||||||
delete(@Param('id') id: string, @Query('cascade') cascade?: string) {
|
type: Boolean,
|
||||||
return this.service.delete(id, cascade === 'true');
|
description: "Force delete with all related data",
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: "Booking deleted successfully" })
|
||||||
|
@ApiResponse({ status: 404, description: "Booking not found" })
|
||||||
|
delete(@Param("id") id: string, @Query("cascade") cascade?: string) {
|
||||||
|
return this.service.delete(id, cascade === "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(":id")
|
||||||
@SetMetadata('isPublic', true)
|
@SetMetadata("isPublic", true)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
description: 'Updates booking information for admin/agent operations'
|
description: "Updates booking information for admin/agent operations",
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 200, description: 'Booking updated successfully' })
|
@ApiResponse({ status: 200, description: "Booking updated successfully" })
|
||||||
@ApiResponse({ status: 404, description: 'Booking not found' })
|
@ApiResponse({ status: 404, description: "Booking not found" })
|
||||||
update(@Param('id') id: string, @Body() dto: any) {
|
update(@Param("id") id: string, @Body() dto: any) {
|
||||||
return this.service.update(id, dto);
|
return this.service.update(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':bookingRef')
|
@Delete(":bookingRef")
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Cancel booking with refund',
|
summary: "Cancel booking with refund",
|
||||||
description: 'Cancels booking and processes refund (80% for confirmed bookings)'
|
description:
|
||||||
|
"Cancels booking and processes refund (80% for confirmed bookings)",
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 200, description: 'Booking cancelled with refund amount' })
|
@ApiResponse({
|
||||||
@ApiResponse({ status: 400, description: 'Booking already cancelled' })
|
status: 200,
|
||||||
cancel(@Param('bookingRef') ref: string, @Body() dto: CancelBookingDto) {
|
description: "Booking cancelled with refund amount",
|
||||||
return this.service.cancel(ref, dto.reason);
|
})
|
||||||
|
@ApiResponse({ status: 400, description: "Booking already cancelled" })
|
||||||
|
cancel(@Param("bookingRef") ref: string, @Body() dto: CancelBookingDto) {
|
||||||
|
return this.service.cancel(ref, dto.reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { Controller, Get, HttpStatus, Res } from '@nestjs/common';
|
import { Controller, Get, HttpStatus, Res } from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||||
import { SkipThrottle } from '@nestjs/throttler';
|
|
||||||
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
|
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
|
||||||
import { PrismaService } from '../../common/prisma.service';
|
import { PrismaService } from '../../common/prisma.service';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
|
|
||||||
@ApiTags('Health')
|
@ApiTags('Health')
|
||||||
@Controller('health')
|
@Controller('health')
|
||||||
@SkipThrottle()
|
|
||||||
export class HealthController {
|
export class HealthController {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,41 @@
|
|||||||
import { Body, Controller, Get, Param, Post, UseGuards, Query, Request, UnauthorizedException, Patch, Delete, SetMetadata } from '@nestjs/common';
|
import {
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse, ApiQuery } from '@nestjs/swagger';
|
Body,
|
||||||
import { SkipThrottle, Throttle } from '@nestjs/throttler';
|
Controller,
|
||||||
import { PassengersService } from './passengers.service';
|
Get,
|
||||||
import { CreateTravelerProfileDto, CreateSavedRouteDto, VerifyFaydaDto, SavePassengersDto, RegisterPassengerDto } from './passengers.dto';
|
Param,
|
||||||
import { JwtGuard } from '../../common/jwt.guard';
|
Post,
|
||||||
import { PassengerAdmin } from '../../common/passenger-guards';
|
UseGuards,
|
||||||
import { VerifaydaService } from '../verifayda/verifayda.service';
|
Query,
|
||||||
import { OptionalJwtGuard } from '../verifayda/optional-jwt.guard';
|
Request,
|
||||||
import { PrismaService } from '../../common/prisma.service';
|
UnauthorizedException,
|
||||||
|
Patch,
|
||||||
|
Delete,
|
||||||
|
SetMetadata,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import {
|
||||||
|
ApiTags,
|
||||||
|
ApiOperation,
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiResponse,
|
||||||
|
ApiQuery,
|
||||||
|
} from "@nestjs/swagger";
|
||||||
|
import { PassengersService } from "./passengers.service";
|
||||||
|
import {
|
||||||
|
CreateTravelerProfileDto,
|
||||||
|
CreateSavedRouteDto,
|
||||||
|
VerifyFaydaDto,
|
||||||
|
SavePassengersDto,
|
||||||
|
RegisterPassengerDto,
|
||||||
|
} from "./passengers.dto";
|
||||||
|
import { JwtGuard } from "../../common/jwt.guard";
|
||||||
|
import { PassengerAdmin } from "../../common/passenger-guards";
|
||||||
|
import { VerifaydaService } from "../verifayda/verifayda.service";
|
||||||
|
import { OptionalJwtGuard } from "../verifayda/optional-jwt.guard";
|
||||||
|
import { PrismaService } from "../../common/prisma.service";
|
||||||
|
|
||||||
@ApiTags('Passengers')
|
@ApiTags("Passengers")
|
||||||
@Controller('passengers')
|
@Controller("passengers")
|
||||||
@Throttle({ strict: { limit: 20, ttl: 60_000 } })
|
// @Throttle({ strict: { limit: 20, ttl: 60_000 } })
|
||||||
export class PassengersController {
|
export class PassengersController {
|
||||||
constructor(
|
constructor(
|
||||||
private service: PassengersService,
|
private service: PassengersService,
|
||||||
@@ -20,9 +44,9 @@ export class PassengersController {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@SetMetadata('isPublic', true)
|
@SetMetadata("isPublic", true)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'List all travelers with filters (Admin/Agent)',
|
summary: "List all travelers with filters (Admin/Agent)",
|
||||||
description: `**Returns paginated list of all travelers in the system**
|
description: `**Returns paginated list of all travelers in the system**
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -54,90 +78,114 @@ export class PassengersController {
|
|||||||
- **faydaVerified**: Whether verified via Verifayda
|
- **faydaVerified**: Whether verified via Verifayda
|
||||||
- **loyaltyTier/loyaltyPoints**: If linked to user account
|
- **loyaltyTier/loyaltyPoints**: If linked to user account
|
||||||
- **totalBookings**: Number of bookings
|
- **totalBookings**: Number of bookings
|
||||||
- **createdAt**: When traveler was first added to system`
|
- **createdAt**: When traveler was first added to system`,
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: "search",
|
||||||
|
required: false,
|
||||||
|
description: "Search by name, email, or phone",
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: "gender",
|
||||||
|
required: false,
|
||||||
|
description: "Filter by gender (Male, Female, Other)",
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: "dateFrom",
|
||||||
|
required: false,
|
||||||
|
description: "Filter by creation date from (YYYY-MM-DD)",
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: "dateTo",
|
||||||
|
required: false,
|
||||||
|
description: "Filter by creation date to (YYYY-MM-DD)",
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: "page",
|
||||||
|
required: false,
|
||||||
|
description: "Page number (default: 1)",
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: "pageSize",
|
||||||
|
required: false,
|
||||||
|
description: "Items per page (default: 20)",
|
||||||
})
|
})
|
||||||
@ApiQuery({ name: 'search', required: false, description: 'Search by name, email, or phone' })
|
|
||||||
@ApiQuery({ name: 'gender', required: false, description: 'Filter by gender (Male, Female, Other)' })
|
|
||||||
@ApiQuery({ name: 'dateFrom', required: false, description: 'Filter by creation date from (YYYY-MM-DD)' })
|
|
||||||
@ApiQuery({ name: 'dateTo', required: false, description: 'Filter by creation date to (YYYY-MM-DD)' })
|
|
||||||
@ApiQuery({ name: 'page', required: false, description: 'Page number (default: 1)' })
|
|
||||||
@ApiQuery({ name: 'pageSize', required: false, description: 'Items per page (default: 20)' })
|
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
description: 'Travelers retrieved successfully',
|
description: "Travelers retrieved successfully",
|
||||||
schema: {
|
schema: {
|
||||||
example: {
|
example: {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 'uuid-123',
|
id: "uuid-123",
|
||||||
fullName: 'Abebe Kebede',
|
fullName: "Abebe Kebede",
|
||||||
email: 'abebe@example.com',
|
email: "abebe@example.com",
|
||||||
phone: '+251911234567',
|
phone: "+251911234567",
|
||||||
gender: 'Male',
|
gender: "Male",
|
||||||
dateOfBirth: '1985-03-15',
|
dateOfBirth: "1985-03-15",
|
||||||
nationality: 'Ethiopian',
|
nationality: "Ethiopian",
|
||||||
faydaVerified: true,
|
faydaVerified: true,
|
||||||
loyaltyTier: 'SILVER',
|
loyaltyTier: "SILVER",
|
||||||
loyaltyPoints: 1500,
|
loyaltyPoints: 1500,
|
||||||
totalBookings: 5,
|
totalBookings: 5,
|
||||||
createdAt: '2024-01-10T12:00:00.000Z'
|
createdAt: "2024-01-10T12:00:00.000Z",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
meta: {
|
meta: {
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
total: 150,
|
total: 150,
|
||||||
totalPages: 8
|
totalPages: 8,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
description: 'Travelers retrieved successfully',
|
description: "Travelers retrieved successfully",
|
||||||
schema: {
|
schema: {
|
||||||
example: {
|
example: {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 'uuid-123',
|
id: "uuid-123",
|
||||||
fullName: 'Abebe Kebede',
|
fullName: "Abebe Kebede",
|
||||||
email: 'abebe@example.com',
|
email: "abebe@example.com",
|
||||||
phone: '+251911234567',
|
phone: "+251911234567",
|
||||||
gender: 'Male',
|
gender: "Male",
|
||||||
dateOfBirth: '1985-03-15',
|
dateOfBirth: "1985-03-15",
|
||||||
nationality: 'Ethiopian',
|
nationality: "Ethiopian",
|
||||||
nationalityCode: 'ET',
|
nationalityCode: "ET",
|
||||||
faydaVerified: true,
|
faydaVerified: true,
|
||||||
faydaVerifiedAt: '2024-01-15T10:30:00.000Z',
|
faydaVerifiedAt: "2024-01-15T10:30:00.000Z",
|
||||||
passportNumber: null,
|
passportNumber: null,
|
||||||
passportCountry: null,
|
passportCountry: null,
|
||||||
passportExpiryDate: null,
|
passportExpiryDate: null,
|
||||||
idDocumentType: 'NATIONAL_ID',
|
idDocumentType: "NATIONAL_ID",
|
||||||
verified: true,
|
verified: true,
|
||||||
lastLoginAt: '2024-01-20T08:15:00.000Z',
|
lastLoginAt: "2024-01-20T08:15:00.000Z",
|
||||||
role: 'PASSENGER',
|
role: "PASSENGER",
|
||||||
loyalty: {
|
loyalty: {
|
||||||
tier: 'SILVER',
|
tier: "SILVER",
|
||||||
pointsBalance: 1500,
|
pointsBalance: 1500,
|
||||||
lifetimePoints: 3000
|
lifetimePoints: 3000,
|
||||||
},
|
},
|
||||||
wallet: {
|
wallet: {
|
||||||
balanceMinor: 50000,
|
balanceMinor: 50000,
|
||||||
currency: 'ETB'
|
currency: "ETB",
|
||||||
},
|
},
|
||||||
loyaltyTier: 'SILVER',
|
loyaltyTier: "SILVER",
|
||||||
loyaltyPoints: 1500,
|
loyaltyPoints: 1500,
|
||||||
totalBookings: 5,
|
totalBookings: 5,
|
||||||
createdAt: '2024-01-10T12:00:00.000Z'
|
createdAt: "2024-01-10T12:00:00.000Z",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'uuid-456',
|
id: "uuid-456",
|
||||||
fullName: 'Sara Ketsela',
|
fullName: "Sara Ketsela",
|
||||||
email: null,
|
email: null,
|
||||||
phone: null,
|
phone: null,
|
||||||
gender: 'Female',
|
gender: "Female",
|
||||||
dateOfBirth: '1990-08-22',
|
dateOfBirth: "1990-08-22",
|
||||||
nationality: 'Ethiopian',
|
nationality: "Ethiopian",
|
||||||
nationalityCode: null,
|
nationalityCode: null,
|
||||||
faydaVerified: false,
|
faydaVerified: false,
|
||||||
faydaVerifiedAt: null,
|
faydaVerifiedAt: null,
|
||||||
@@ -150,54 +198,59 @@ export class PassengersController {
|
|||||||
role: null,
|
role: null,
|
||||||
loyalty: null,
|
loyalty: null,
|
||||||
wallet: null,
|
wallet: null,
|
||||||
loyaltyTier: 'BRONZE',
|
loyaltyTier: "BRONZE",
|
||||||
loyaltyPoints: 0,
|
loyaltyPoints: 0,
|
||||||
totalBookings: 1,
|
totalBookings: 1,
|
||||||
createdAt: '2024-01-18T14:30:00.000Z'
|
createdAt: "2024-01-18T14:30:00.000Z",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
meta: {
|
meta: {
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
total: 150,
|
total: 150,
|
||||||
totalPages: 8
|
totalPages: 8,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
findAll(
|
findAll(
|
||||||
@Query('search') search?: string,
|
@Query("search") search?: string,
|
||||||
@Query('gender') gender?: string,
|
@Query("gender") gender?: string,
|
||||||
@Query('dateFrom') dateFrom?: string,
|
@Query("dateFrom") dateFrom?: string,
|
||||||
@Query('dateTo') dateTo?: string,
|
@Query("dateTo") dateTo?: string,
|
||||||
@Query('page') page?: string,
|
@Query("page") page?: string,
|
||||||
@Query('pageSize') pageSize?: string,
|
@Query("pageSize") pageSize?: string,
|
||||||
) {
|
) {
|
||||||
return this.service.findAll({
|
return this.service.findAll({
|
||||||
search,
|
search,
|
||||||
gender,
|
gender,
|
||||||
dateFrom,
|
dateFrom,
|
||||||
dateTo,
|
dateTo,
|
||||||
page: page ? parseInt(page) : 1,
|
page: page ? parseInt(page) : 1,
|
||||||
pageSize: pageSize ? parseInt(pageSize) : 20
|
pageSize: pageSize ? parseInt(pageSize) : 20,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('me')
|
@Get("me")
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Get current passenger profile',
|
summary: "Get current passenger profile",
|
||||||
description: 'Returns complete profile for authenticated passenger including passport details and verification status. Returns null if no passenger profile exists.'
|
description:
|
||||||
|
"Returns complete profile for authenticated passenger including passport details and verification status. Returns null if no passenger profile exists.",
|
||||||
})
|
})
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
description: 'Passenger profile retrieved successfully or null if not found'
|
description:
|
||||||
|
"Passenger profile retrieved successfully or null if not found",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 401,
|
||||||
|
description: "Unauthorized - Invalid or missing token",
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing token' })
|
|
||||||
async getMe(@Request() req: any) {
|
async getMe(@Request() req: any) {
|
||||||
if (!req.user || !req.user.id) {
|
if (!req.user || !req.user.id) {
|
||||||
throw new UnauthorizedException('User not authenticated');
|
throw new UnauthorizedException("User not authenticated");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -211,26 +264,26 @@ export class PassengersController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/profile')
|
@Get(":id/profile")
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@ApiOperation({ summary: 'Get passenger profile' })
|
@ApiOperation({ summary: "Get passenger profile" })
|
||||||
getProfile(@Param('id') id: string) {
|
getProfile(@Param("id") id: string) {
|
||||||
return this.service.getProfile(id);
|
return this.service.getProfile(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/stats')
|
@Get(":id/stats")
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@ApiOperation({ summary: 'Get passenger stats' })
|
@ApiOperation({ summary: "Get passenger stats" })
|
||||||
getStats(@Param('id') id: string) {
|
getStats(@Param("id") id: string) {
|
||||||
return this.service.getStats(id);
|
return this.service.getStats(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('verify-fayda')
|
@Post("verify-fayda")
|
||||||
@SetMetadata('isPublic', true)
|
@SetMetadata("isPublic", true)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Verify Ethiopian national ID via Verifayda 2.0',
|
summary: "Verify Ethiopian national ID via Verifayda 2.0",
|
||||||
description: `**Standalone endpoint for pre-verification of Ethiopian national IDs**
|
description: `**Standalone endpoint for pre-verification of Ethiopian national IDs**
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -277,32 +330,35 @@ Pre-verify national ID to auto-fill passenger registration form before submissio
|
|||||||
- **Public endpoint** (no authentication required)
|
- **Public endpoint** (no authentication required)
|
||||||
- Can be called before login/registration`,
|
- Can be called before login/registration`,
|
||||||
})
|
})
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
description: 'Verification successful with passenger data',
|
description: "Verification successful with passenger data",
|
||||||
schema: {
|
schema: {
|
||||||
example: {
|
example: {
|
||||||
verified: true,
|
verified: true,
|
||||||
passengerData: {
|
passengerData: {
|
||||||
fullName: 'Abebe Kebede',
|
fullName: "Abebe Kebede",
|
||||||
dateOfBirth: '1985-03-15T00:00:00.000Z',
|
dateOfBirth: "1985-03-15T00:00:00.000Z",
|
||||||
gender: 'Male',
|
gender: "Male",
|
||||||
nationality: 'Ethiopian'
|
nationality: "Ethiopian",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 400,
|
||||||
|
description: "Verification failed or Verifayda disabled",
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 400, description: 'Verification failed or Verifayda disabled' })
|
|
||||||
verifyFayda(@Body() dto: VerifyFaydaDto) {
|
verifyFayda(@Body() dto: VerifyFaydaDto) {
|
||||||
return this.verifaydaService.verifyNationalId(dto.nationalId);
|
return this.verifaydaService.verifyNationalId(dto.nationalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('register')
|
@Post("register")
|
||||||
@SetMetadata('isPublic', true)
|
@SetMetadata("isPublic", true)
|
||||||
@UseGuards(OptionalJwtGuard)
|
@UseGuards(OptionalJwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Universal passenger registration endpoint',
|
summary: "Universal passenger registration endpoint",
|
||||||
description: `**Single endpoint for all passenger registration scenarios**
|
description: `**Single endpoint for all passenger registration scenarios**
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -357,45 +413,45 @@ The API automatically detects:
|
|||||||
### Replaces
|
### Replaces
|
||||||
- Manual verification + save flows`,
|
- Manual verification + save flows`,
|
||||||
})
|
})
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 201,
|
status: 201,
|
||||||
description: 'Passenger registered successfully',
|
description: "Passenger registered successfully",
|
||||||
schema: {
|
schema: {
|
||||||
example: {
|
example: {
|
||||||
id: 'uuid-123',
|
id: "uuid-123",
|
||||||
passengerName: 'Abebe Kebede',
|
passengerName: "Abebe Kebede",
|
||||||
dateOfBirth: '1985-03-15T00:00:00.000Z',
|
dateOfBirth: "1985-03-15T00:00:00.000Z",
|
||||||
nationality: 'Ethiopian',
|
nationality: "Ethiopian",
|
||||||
verified: true,
|
verified: true,
|
||||||
linked: false,
|
linked: false,
|
||||||
message: 'Passenger details saved for guest booking'
|
message: "Passenger details saved for guest booking",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 400,
|
status: 400,
|
||||||
description: 'Validation error or verification failed',
|
description: "Validation error or verification failed",
|
||||||
schema: {
|
schema: {
|
||||||
example: {
|
example: {
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: 'Validation failed',
|
message: "Validation failed",
|
||||||
error: 'Bad Request'
|
error: "Bad Request",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 401,
|
status: 401,
|
||||||
description: 'Invalid JWT token (only if token provided but invalid)'
|
description: "Invalid JWT token (only if token provided but invalid)",
|
||||||
})
|
})
|
||||||
registerPassenger(@Body() dto: RegisterPassengerDto, @Request() req: any) {
|
registerPassenger(@Body() dto: RegisterPassengerDto, @Request() req: any) {
|
||||||
const userId = req.user?.id;
|
const userId = req.user?.id;
|
||||||
return this.service.registerPassenger({ ...dto, userId });
|
return this.service.registerPassenger({ ...dto, userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('save-details')
|
@Post("save-details")
|
||||||
@SetMetadata('isPublic', true)
|
@SetMetadata("isPublic", true)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Bulk save passenger details from booking flow',
|
summary: "Bulk save passenger details from booking flow",
|
||||||
description: `**Endpoint for saving multiple passengers in a single booking**
|
description: `**Endpoint for saving multiple passengers in a single booking**
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -427,105 +483,117 @@ Save all passenger details for a multi-passenger booking before proceeding to se
|
|||||||
### Response
|
### Response
|
||||||
Returns saved passenger details with generated IDs and confirmation.`,
|
Returns saved passenger details with generated IDs and confirmation.`,
|
||||||
})
|
})
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 201,
|
status: 201,
|
||||||
description: 'All passenger details saved successfully',
|
description: "All passenger details saved successfully",
|
||||||
schema: {
|
schema: {
|
||||||
example: {
|
example: {
|
||||||
count: 2,
|
count: 2,
|
||||||
passengerIds: ['uuid-1', 'uuid-2'],
|
passengerIds: ["uuid-1", "uuid-2"],
|
||||||
passengers: [
|
passengers: [
|
||||||
{
|
{
|
||||||
id: 'uuid-1',
|
id: "uuid-1",
|
||||||
passengerName: 'Abebe Kebede',
|
passengerName: "Abebe Kebede",
|
||||||
dateOfBirth: '1985-03-15T00:00:00.000Z',
|
dateOfBirth: "1985-03-15T00:00:00.000Z",
|
||||||
nationality: 'Ethiopian',
|
nationality: "Ethiopian",
|
||||||
nationalId: 'ET123456789'
|
nationalId: "ET123456789",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'uuid-2',
|
id: "uuid-2",
|
||||||
passengerName: 'Sara Ketsela',
|
passengerName: "Sara Ketsela",
|
||||||
dateOfBirth: '1990-08-22T00:00:00.000Z',
|
dateOfBirth: "1990-08-22T00:00:00.000Z",
|
||||||
nationality: 'Ethiopian',
|
nationality: "Ethiopian",
|
||||||
nationalId: 'ET987654321'
|
nationalId: "ET987654321",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
message: 'Passenger details saved successfully'
|
message: "Passenger details saved successfully",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 400,
|
||||||
|
description: "Validation error - passengers array required",
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 400, description: 'Validation error - passengers array required' })
|
|
||||||
savePassengers(@Body() dto: SavePassengersDto) {
|
savePassengers(@Body() dto: SavePassengersDto) {
|
||||||
return this.service.savePassengers(dto.passengers, dto.userId, dto.deviceId);
|
return this.service.savePassengers(
|
||||||
|
dto.passengers,
|
||||||
|
dto.userId,
|
||||||
|
dto.deviceId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('traveler-profiles')
|
@Post("traveler-profiles")
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@ApiOperation({ summary: 'Add traveler profile (family member)' })
|
@ApiOperation({ summary: "Add traveler profile (family member)" })
|
||||||
createTravelerProfile(@Body() dto: CreateTravelerProfileDto) {
|
createTravelerProfile(@Body() dto: CreateTravelerProfileDto) {
|
||||||
return this.service.createTravelerProfile(dto);
|
return this.service.createTravelerProfile(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/traveler-profiles')
|
@Get(":id/traveler-profiles")
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@ApiOperation({ summary: 'Get traveler profiles for passenger' })
|
@ApiOperation({ summary: "Get traveler profiles for passenger" })
|
||||||
getTravelerProfiles(@Param('id') id: string) {
|
getTravelerProfiles(@Param("id") id: string) {
|
||||||
return this.service.getTravelerProfiles(id);
|
return this.service.getTravelerProfiles(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('saved-routes')
|
@Post("saved-routes")
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@ApiOperation({ summary: 'Save a route' })
|
@ApiOperation({ summary: "Save a route" })
|
||||||
createSavedRoute(@Body() dto: CreateSavedRouteDto) {
|
createSavedRoute(@Body() dto: CreateSavedRouteDto) {
|
||||||
return this.service.createSavedRoute(dto);
|
return this.service.createSavedRoute(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/saved-routes')
|
@Get(":id/saved-routes")
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@ApiOperation({ summary: 'Get saved routes' })
|
@ApiOperation({ summary: "Get saved routes" })
|
||||||
getSavedRoutes(@Param('id') id: string) {
|
getSavedRoutes(@Param("id") id: string) {
|
||||||
return this.service.getSavedRoutes(id);
|
return this.service.getSavedRoutes(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(":id")
|
||||||
@SetMetadata('isPublic', true)
|
@SetMetadata("isPublic", true)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Update passenger details',
|
summary: "Update passenger details",
|
||||||
description: 'Updates passenger information for admin/agent operations'
|
description: "Updates passenger information for admin/agent operations",
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 200, description: 'Passenger updated successfully' })
|
@ApiResponse({ status: 200, description: "Passenger updated successfully" })
|
||||||
@ApiResponse({ status: 404, description: 'Passenger not found' })
|
@ApiResponse({ status: 404, description: "Passenger not found" })
|
||||||
updatePassenger(@Param('id') id: string, @Body() dto: any) {
|
updatePassenger(@Param("id") id: string, @Body() dto: any) {
|
||||||
return this.service.updatePassenger(id, dto);
|
return this.service.updatePassenger(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(":id")
|
||||||
@PassengerAdmin()
|
@PassengerAdmin()
|
||||||
@ApiBearerAuth('IAM-auth')
|
@ApiBearerAuth("IAM-auth")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Delete passenger (admin only)',
|
summary: "Delete passenger (admin only)",
|
||||||
description: 'Permanently deletes a passenger record and associated data'
|
description: "Permanently deletes a passenger record and associated data",
|
||||||
})
|
})
|
||||||
@ApiQuery({ name: 'cascade', required: false, type: Boolean, description: 'Force delete with all related bookings and data' })
|
@ApiQuery({
|
||||||
@ApiResponse({ status: 200, description: 'Passenger deleted successfully' })
|
name: "cascade",
|
||||||
@ApiResponse({ status: 404, description: 'Passenger not found' })
|
required: false,
|
||||||
deletePassenger(@Param('id') id: string, @Query('cascade') cascade?: string) {
|
type: Boolean,
|
||||||
return this.service.deletePassenger(id, cascade === 'true');
|
description: "Force delete with all related bookings and data",
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: "Passenger deleted successfully" })
|
||||||
|
@ApiResponse({ status: 404, description: "Passenger not found" })
|
||||||
|
deletePassenger(@Param("id") id: string, @Query("cascade") cascade?: string) {
|
||||||
|
return this.service.deletePassenger(id, cascade === "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/usage')
|
@Get(":id/usage")
|
||||||
@SetMetadata('isPublic', true)
|
@SetMetadata("isPublic", true)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Check if passenger is in use',
|
summary: "Check if passenger is in use",
|
||||||
description: 'Returns list of modules/data that reference this passenger'
|
description: "Returns list of modules/data that reference this passenger",
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 200, description: 'Usage information retrieved' })
|
@ApiResponse({ status: 200, description: "Usage information retrieved" })
|
||||||
@ApiResponse({ status: 404, description: 'Passenger not found' })
|
@ApiResponse({ status: 404, description: "Passenger not found" })
|
||||||
checkUsage(@Param('id') id: string) {
|
checkUsage(@Param("id") id: string) {
|
||||||
return this.service.checkPassengerUsage(id);
|
return this.service.checkPassengerUsage(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
UseGuards,
|
UseGuards,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
import { SkipThrottle } from "@nestjs/throttler";
|
|
||||||
import { ServiceAuthGuard } from "../../common/guards/service-auth.guard";
|
import { ServiceAuthGuard } from "../../common/guards/service-auth.guard";
|
||||||
import { PaymentEventDto, MarkPaidResponseDto } from "./internal-payments.dto";
|
import { PaymentEventDto, MarkPaidResponseDto } from "./internal-payments.dto";
|
||||||
import { PaymentsService } from "./payments.service";
|
import { PaymentsService } from "./payments.service";
|
||||||
@@ -21,7 +20,6 @@ import { PaymentsService } from "./payments.service";
|
|||||||
@ApiTags("Internal Payments")
|
@ApiTags("Internal Payments")
|
||||||
@UseGuards(ServiceAuthGuard)
|
@UseGuards(ServiceAuthGuard)
|
||||||
@Controller("internal/payments")
|
@Controller("internal/payments")
|
||||||
@SkipThrottle()
|
|
||||||
export class InternalPaymentsController {
|
export class InternalPaymentsController {
|
||||||
constructor(private readonly paymentsService: PaymentsService) {}
|
constructor(private readonly paymentsService: PaymentsService) {}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
ApiProduces,
|
ApiProduces,
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
|
|
||||||
import { SkipThrottle, Throttle } from "@nestjs/throttler";
|
|
||||||
import { Response } from "express";
|
import { Response } from "express";
|
||||||
import { PaymentsService } from "./payments.service";
|
import { PaymentsService } from "./payments.service";
|
||||||
import {
|
import {
|
||||||
@@ -41,7 +40,7 @@ import { PASSENGER_PERMS } from "../../seed/passenger-permissions.registry";
|
|||||||
|
|
||||||
@ApiTags("Payment")
|
@ApiTags("Payment")
|
||||||
@Controller("payments")
|
@Controller("payments")
|
||||||
@Throttle({ strict: { limit: 20, ttl: 60_000 } })
|
// @Throttle({ strict: { limit: 20, ttl: 60_000 } })
|
||||||
export class PaymentsController {
|
export class PaymentsController {
|
||||||
constructor(private service: PaymentsService) {}
|
constructor(private service: PaymentsService) {}
|
||||||
|
|
||||||
@@ -79,7 +78,7 @@ export class PaymentsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("initiate")
|
@Post("initiate")
|
||||||
@SetMetadata('isPublic', true)
|
@SetMetadata("isPublic", true)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Initiate payment with nationality-based payment methods",
|
summary: "Initiate payment with nationality-based payment methods",
|
||||||
description: `Initiates payment for a booking with support for multiple payment providers:\n\n**Ethiopian Payment Methods:**\n- TELEBIRR - Ethiopia's leading mobile money\n- CBE_BIRR - Commercial Bank of Ethiopia\n- EBIRR - Electronic payment gateway\n\n**Djiboutian Payment Methods:**\n- WAAFI - Djibouti's mobile money service\n\n**International Payment Methods:**\n- CARD - Visa, Mastercard\n- WALLET - Internal wallet balance\n\n**Multi-Currency:**\n- All transactions processed in ETB\n- Display amounts in ETB, DJF, or USD\n- Real-time exchange rate conversion`,
|
description: `Initiates payment for a booking with support for multiple payment providers:\n\n**Ethiopian Payment Methods:**\n- TELEBIRR - Ethiopia's leading mobile money\n- CBE_BIRR - Commercial Bank of Ethiopia\n- EBIRR - Electronic payment gateway\n\n**Djiboutian Payment Methods:**\n- WAAFI - Djibouti's mobile money service\n\n**International Payment Methods:**\n- CARD - Visa, Mastercard\n- WALLET - Internal wallet balance\n\n**Multi-Currency:**\n- All transactions processed in ETB\n- Display amounts in ETB, DJF, or USD\n- Real-time exchange rate conversion`,
|
||||||
@@ -89,14 +88,14 @@ export class PaymentsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get("intents/:bookingId")
|
@Get("intents/:bookingId")
|
||||||
@SetMetadata('isPublic', true)
|
@SetMetadata("isPublic", true)
|
||||||
@ApiOperation({ summary: "Get payment intent status for a booking" })
|
@ApiOperation({ summary: "Get payment intent status for a booking" })
|
||||||
getIntent(@Param("bookingId") bookingId: string) {
|
getIntent(@Param("bookingId") bookingId: string) {
|
||||||
return this.service.getIntentByBookingId(bookingId);
|
return this.service.getIntentByBookingId(bookingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(":bookingId/confirm")
|
@Post(":bookingId/confirm")
|
||||||
@SetMetadata('isPublic', true)
|
@SetMetadata("isPublic", true)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Confirm an OTP-debit payment (CAC Bank)",
|
summary: "Confirm an OTP-debit payment (CAC Bank)",
|
||||||
description:
|
description:
|
||||||
@@ -111,7 +110,7 @@ export class PaymentsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get("waafi/return")
|
@Get("waafi/return")
|
||||||
@SetMetadata('isPublic', true)
|
@SetMetadata("isPublic", true)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary:
|
summary:
|
||||||
"DEMO ONLY — confirm a Waafi payment from the browser-return params and return JSON for the " +
|
"DEMO ONLY — confirm a Waafi payment from the browser-return params and return JSON for the " +
|
||||||
@@ -142,7 +141,10 @@ export class PaymentsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post(":bookingId/force-confirm")
|
@Post(":bookingId/force-confirm")
|
||||||
@PassengerStaff([PASSENGER_PERMS.payments.manageMethods, PASSENGER_PERMS.admin])
|
@PassengerStaff([
|
||||||
|
PASSENGER_PERMS.payments.manageMethods,
|
||||||
|
PASSENGER_PERMS.admin,
|
||||||
|
])
|
||||||
@ApiBearerAuth("IAM-auth")
|
@ApiBearerAuth("IAM-auth")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Force-confirm payment & generate ticket (back-office only)",
|
summary: "Force-confirm payment & generate ticket (back-office only)",
|
||||||
@@ -150,12 +152,18 @@ export class PaymentsController {
|
|||||||
"Marks the payment as SUCCEEDED, confirms the booking, and generates the ticket. " +
|
"Marks the payment as SUCCEEDED, confirms the booking, and generates the ticket. " +
|
||||||
"Use when a vendor payment completed but the webhook was never delivered. Idempotent.",
|
"Use when a vendor payment completed but the webhook was never delivered. Idempotent.",
|
||||||
})
|
})
|
||||||
forceConfirm(@Param("bookingId") bookingId: string, @Body() dto: ForceConfirmDto) {
|
forceConfirm(
|
||||||
|
@Param("bookingId") bookingId: string,
|
||||||
|
@Body() dto: ForceConfirmDto,
|
||||||
|
) {
|
||||||
return this.service.forceConfirmPayment(bookingId, dto);
|
return this.service.forceConfirmPayment(bookingId, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("methods")
|
@Post("methods")
|
||||||
@PassengerStaff([PASSENGER_PERMS.payments.manageMethods, PASSENGER_PERMS.admin])
|
@PassengerStaff([
|
||||||
|
PASSENGER_PERMS.payments.manageMethods,
|
||||||
|
PASSENGER_PERMS.admin,
|
||||||
|
])
|
||||||
@ApiBearerAuth("IAM-auth")
|
@ApiBearerAuth("IAM-auth")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Add a payment system to the platform catalog (admin only)",
|
summary: "Add a payment system to the platform catalog (admin only)",
|
||||||
@@ -165,17 +173,23 @@ export class PaymentsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch("methods/:id")
|
@Patch("methods/:id")
|
||||||
@PassengerStaff([PASSENGER_PERMS.payments.manageMethods, PASSENGER_PERMS.admin])
|
@PassengerStaff([
|
||||||
|
PASSENGER_PERMS.payments.manageMethods,
|
||||||
|
PASSENGER_PERMS.admin,
|
||||||
|
])
|
||||||
@ApiBearerAuth("IAM-auth")
|
@ApiBearerAuth("IAM-auth")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Update a payment method configuration (admin only)",
|
summary: "Update a payment method configuration (admin only)",
|
||||||
})
|
})
|
||||||
updateMethod(@Param("id") id: string, @Body() dto: Partial<AddPaymentMethodDto>) {
|
updateMethod(
|
||||||
|
@Param("id") id: string,
|
||||||
|
@Body() dto: Partial<AddPaymentMethodDto>,
|
||||||
|
) {
|
||||||
return this.service.updatePaymentMethod(id, dto);
|
return this.service.updatePaymentMethod(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("methods")
|
@Get("methods")
|
||||||
@SetMetadata('isPublic', true)
|
@SetMetadata("isPublic", true)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "List payment systems supported by the platform",
|
summary: "List payment systems supported by the platform",
|
||||||
description:
|
description:
|
||||||
@@ -183,14 +197,12 @@ export class PaymentsController {
|
|||||||
})
|
})
|
||||||
@ApiQuery({ name: "region", enum: PaymentRegionEnum, required: false })
|
@ApiQuery({ name: "region", enum: PaymentRegionEnum, required: false })
|
||||||
@ApiOkResponse({ type: [SupportedPaymentMethodDto] })
|
@ApiOkResponse({ type: [SupportedPaymentMethodDto] })
|
||||||
getMethods(
|
getMethods(@Query("region") region?: PaymentRegionEnum) {
|
||||||
@Query("region") region?: PaymentRegionEnum,
|
|
||||||
) {
|
|
||||||
return this.service.getSupportedPaymentMethods(region);
|
return this.service.getSupportedPaymentMethods(region);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("booking-amount")
|
@Get("booking-amount")
|
||||||
@SetMetadata('isPublic', true)
|
@SetMetadata("isPublic", true)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get booking amount in a specific currency",
|
summary: "Get booking amount in a specific currency",
|
||||||
description:
|
description:
|
||||||
@@ -199,7 +211,12 @@ export class PaymentsController {
|
|||||||
"Amounts are returned in major currency units (e.g. 162.50 DJF, not centimes).",
|
"Amounts are returned in major currency units (e.g. 162.50 DJF, not centimes).",
|
||||||
})
|
})
|
||||||
@ApiQuery({ name: "bookingId", required: true, description: "Booking UUID" })
|
@ApiQuery({ name: "bookingId", required: true, description: "Booking UUID" })
|
||||||
@ApiQuery({ name: "currency", required: true, example: "DJF", description: "Target currency: ETB, DJF, or USD" })
|
@ApiQuery({
|
||||||
|
name: "currency",
|
||||||
|
required: true,
|
||||||
|
example: "DJF",
|
||||||
|
description: "Target currency: ETB, DJF, or USD",
|
||||||
|
})
|
||||||
@ApiOkResponse({ type: BookingAmountResponseDto })
|
@ApiOkResponse({ type: BookingAmountResponseDto })
|
||||||
getBookingAmount(
|
getBookingAmount(
|
||||||
@Query("bookingId") bookingId: string,
|
@Query("bookingId") bookingId: string,
|
||||||
@@ -209,7 +226,7 @@ export class PaymentsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get("checkout")
|
@Get("checkout")
|
||||||
@SetMetadata('isPublic', true)
|
@SetMetadata("isPublic", true)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Browser checkout redirect",
|
summary: "Browser checkout redirect",
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Body, Controller, Get, Patch, SetMetadata, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Get, Patch, SetMetadata, UseGuards } from '@nestjs/common';
|
||||||
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
||||||
import { SkipThrottle } from '@nestjs/throttler';
|
|
||||||
import { SystemConfigService } from './system-config.service';
|
import { SystemConfigService } from './system-config.service';
|
||||||
import { IamGuard } from '../../common/iam-adapter';
|
import { IamGuard } from '../../common/iam-adapter';
|
||||||
import { Roles } from '../../common/roles.decorator';
|
import { Roles } from '../../common/roles.decorator';
|
||||||
@@ -12,7 +11,6 @@ export class SystemConfigController {
|
|||||||
|
|
||||||
@Get('fayda-status')
|
@Get('fayda-status')
|
||||||
@SetMetadata('isPublic', true)
|
@SetMetadata('isPublic', true)
|
||||||
@SkipThrottle()
|
|
||||||
@ApiOperation({ summary: 'Get Fayda verification enabled status (public)' })
|
@ApiOperation({ summary: 'Get Fayda verification enabled status (public)' })
|
||||||
getFaydaStatus() {
|
getFaydaStatus() {
|
||||||
const enabled = process.env.VERIFAYDA_ENABLED !== 'false';
|
const enabled = process.env.VERIFAYDA_ENABLED !== 'false';
|
||||||
|
|||||||
@@ -8,25 +8,24 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
Req,
|
Req,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from "@nestjs/common";
|
||||||
import {
|
import {
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
ApiOkResponse,
|
ApiOkResponse,
|
||||||
ApiOperation,
|
ApiOperation,
|
||||||
ApiTags,
|
ApiTags,
|
||||||
} from '@nestjs/swagger';
|
} from "@nestjs/swagger";
|
||||||
import { Throttle } from '@nestjs/throttler';
|
import type { TCurrentUser } from "@tria-plc/api-common/modules/auth/types/current-user.type";
|
||||||
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
|
import { IsPublic } from "@tria-plc/api-common/modules/auth/decorators/public.decorator";
|
||||||
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
|
import { JwtGuard } from "../../common/jwt.guard";
|
||||||
import { JwtGuard } from '../../common/jwt.guard';
|
import { OptionalJwtGuard } from "./optional-jwt.guard";
|
||||||
import { OptionalJwtGuard } from './optional-jwt.guard';
|
|
||||||
import {
|
import {
|
||||||
CompleteVerificationResultDto,
|
CompleteVerificationResultDto,
|
||||||
StartVerificationDto,
|
StartVerificationDto,
|
||||||
VerifaydaCallbackDto,
|
VerifaydaCallbackDto,
|
||||||
VerificationStatusDto,
|
VerificationStatusDto,
|
||||||
} from './verifayda.dto';
|
} from "./verifayda.dto";
|
||||||
import { VerifaydaService } from './verifayda.service';
|
import { VerifaydaService } from "./verifayda.service";
|
||||||
|
|
||||||
/** Minimal slices of the Express req we touch (avoids a hard dependency on
|
/** Minimal slices of the Express req we touch (avoids a hard dependency on
|
||||||
* `@types/express`, which isn't resolved in this package). */
|
* `@types/express`, which isn't resolved in this package). */
|
||||||
@@ -37,19 +36,19 @@ interface RequestWithUser {
|
|||||||
user: TCurrentUser;
|
user: TCurrentUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiTags('Fayda Verification')
|
@ApiTags("Fayda Verification")
|
||||||
@Controller('fayda/verification')
|
@Controller("fayda/verification")
|
||||||
@Throttle({ auth: { limit: 5, ttl: 60_000 } })
|
// @Throttle({ auth: { limit: 5, ttl: 60_000 } })
|
||||||
export class VerifaydaController {
|
export class VerifaydaController {
|
||||||
constructor(private readonly service: VerifaydaService) {}
|
constructor(private readonly service: VerifaydaService) {}
|
||||||
|
|
||||||
@Post('start')
|
@Post("start")
|
||||||
@IsPublic()
|
@IsPublic()
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@UseGuards(OptionalJwtGuard)
|
@UseGuards(OptionalJwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Start a VeriFayda 2.0 verification session',
|
summary: "Start a VeriFayda 2.0 verification session",
|
||||||
description: `Creates a verification session and returns the eSignet authorize URL the frontend should send the user to.
|
description: `Creates a verification session and returns the eSignet authorize URL the frontend should send the user to.
|
||||||
|
|
||||||
- Works for **logged-in users** and **guests**. If a valid bearer token is present, the verification is tied to that user.
|
- Works for **logged-in users** and **guests**. If a valid bearer token is present, the verification is tied to that user.
|
||||||
@@ -58,11 +57,11 @@ export class VerifaydaController {
|
|||||||
- The returned \`authorizationUrl\` already carries the PKCE \`code_challenge\`, CSRF \`state\`, requested \`claims\`, and \`code_challenge_method=S256\`. The frontend simply navigates to it (full page or popup).`,
|
- The returned \`authorizationUrl\` already carries the PKCE \`code_challenge\`, CSRF \`state\`, requested \`claims\`, and \`code_challenge_method=S256\`. The frontend simply navigates to it (full page or popup).`,
|
||||||
})
|
})
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
description: 'Authorize URL the frontend should redirect the user to.',
|
description: "Authorize URL the frontend should redirect the user to.",
|
||||||
schema: {
|
schema: {
|
||||||
example: {
|
example: {
|
||||||
authorizationUrl:
|
authorizationUrl:
|
||||||
'https://esignet.example.com/authorize?client_id=...&state=...&code_challenge=...',
|
"https://esignet.example.com/authorize?client_id=...&state=...&code_challenge=...",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -71,18 +70,19 @@ export class VerifaydaController {
|
|||||||
@Req() req: RequestWithOptionalUser,
|
@Req() req: RequestWithOptionalUser,
|
||||||
): Promise<{ authorizationUrl: string }> {
|
): Promise<{ authorizationUrl: string }> {
|
||||||
const authorizationUrl = await this.service.startVerification({
|
const authorizationUrl = await this.service.startVerification({
|
||||||
purpose: dto.purpose ?? 'VERIFY',
|
purpose: dto.purpose ?? "VERIFY",
|
||||||
platform: dto.platform ?? 'WEB',
|
platform: dto.platform ?? "WEB",
|
||||||
userId: req.user?.id,
|
userId: req.user?.id,
|
||||||
wantsPasswordSetup: dto.wantsPasswordSetup ?? false,
|
wantsPasswordSetup: dto.wantsPasswordSetup ?? false,
|
||||||
});
|
});
|
||||||
return { authorizationUrl };
|
return { authorizationUrl };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('complete')
|
@Get("complete")
|
||||||
@IsPublic()
|
@IsPublic()
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Complete a verification (Fayda redirect / client callback lands here)',
|
summary:
|
||||||
|
"Complete a verification (Fayda redirect / client callback lands here)",
|
||||||
description: `This is the registered Fayda \`redirect_uri\`. Fayda redirects the browser here with \`?code&state\``,
|
description: `This is the registered Fayda \`redirect_uri\`. Fayda redirects the browser here with \`?code&state\``,
|
||||||
})
|
})
|
||||||
@ApiOkResponse({ type: CompleteVerificationResultDto })
|
@ApiOkResponse({ type: CompleteVerificationResultDto })
|
||||||
@@ -92,18 +92,16 @@ export class VerifaydaController {
|
|||||||
return this.service.completeVerification(dto);
|
return this.service.completeVerification(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('status')
|
@Get("status")
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get the current user's Fayda verification status",
|
summary: "Get the current user's Fayda verification status",
|
||||||
description:
|
description:
|
||||||
'Returns whether the authenticated user has linked a verified Fayda identity to their account, when, and the name on file.',
|
"Returns whether the authenticated user has linked a verified Fayda identity to their account, when, and the name on file.",
|
||||||
})
|
})
|
||||||
@ApiOkResponse({ type: VerificationStatusDto })
|
@ApiOkResponse({ type: VerificationStatusDto })
|
||||||
async status(
|
async status(@Req() req: RequestWithUser): Promise<VerificationStatusDto> {
|
||||||
@Req() req: RequestWithUser,
|
|
||||||
): Promise<VerificationStatusDto> {
|
|
||||||
return this.service.getVerificationStatus(req.user.id);
|
return this.service.getVerificationStatus(req.user.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,46 @@
|
|||||||
import { Body, Controller, Get, Param, Post, Delete, UseGuards, SetMetadata, Query } from '@nestjs/common';
|
import {
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
Body,
|
||||||
import { Throttle } from '@nestjs/throttler';
|
Controller,
|
||||||
import { WalletService } from './wallet.service';
|
Get,
|
||||||
import { JwtGuard } from '../../common/jwt.guard';
|
Param,
|
||||||
|
Post,
|
||||||
|
Delete,
|
||||||
|
UseGuards,
|
||||||
|
SetMetadata,
|
||||||
|
Query,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ApiTags, ApiOperation, ApiBearerAuth } from "@nestjs/swagger";
|
||||||
|
import { WalletService } from "./wallet.service";
|
||||||
|
import { JwtGuard } from "../../common/jwt.guard";
|
||||||
|
|
||||||
@ApiTags('Wallet')
|
@ApiTags("Wallet")
|
||||||
@Controller('wallet')
|
@Controller("wallet")
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
@ApiBearerAuth('JWT-auth')
|
@ApiBearerAuth("JWT-auth")
|
||||||
@Throttle({ strict: { limit: 20, ttl: 60_000 } })
|
// @Throttle({ strict: { limit: 20, ttl: 60_000 } })
|
||||||
export class WalletController {
|
export class WalletController {
|
||||||
constructor(private service: WalletService) {}
|
constructor(private service: WalletService) {}
|
||||||
@Get('accounts') @SetMetadata('isPublic', true) @ApiOperation({ summary: 'List all wallet accounts' }) getAccounts(@Query() q: any) { return this.service.getAccounts(q); }
|
@Get("accounts")
|
||||||
@Get(':passengerId') @ApiOperation({ summary: 'Get wallet balance and ledger' }) getWallet(@Param('passengerId') id: string) { return this.service.getWallet(id); }
|
@SetMetadata("isPublic", true)
|
||||||
@Post(':passengerId/topup') @ApiOperation({ summary: 'Top up wallet' }) topUp(@Param('passengerId') id: string, @Body('amountMinor') amount: number) { return this.service.topUp(id, amount); }
|
@ApiOperation({ summary: "List all wallet accounts" })
|
||||||
@Delete('accounts/:id') @SetMetadata('isPublic', true) @ApiOperation({ summary: 'Delete wallet account' }) deleteAccount(@Param('id') id: string) { return this.service.deleteAccount(id); }
|
getAccounts(@Query() q: any) {
|
||||||
|
return this.service.getAccounts(q);
|
||||||
|
}
|
||||||
|
@Get(":passengerId")
|
||||||
|
@ApiOperation({ summary: "Get wallet balance and ledger" })
|
||||||
|
getWallet(@Param("passengerId") id: string) {
|
||||||
|
return this.service.getWallet(id);
|
||||||
|
}
|
||||||
|
@Post(":passengerId/topup") @ApiOperation({ summary: "Top up wallet" }) topUp(
|
||||||
|
@Param("passengerId") id: string,
|
||||||
|
@Body("amountMinor") amount: number,
|
||||||
|
) {
|
||||||
|
return this.service.topUp(id, amount);
|
||||||
|
}
|
||||||
|
@Delete("accounts/:id")
|
||||||
|
@SetMetadata("isPublic", true)
|
||||||
|
@ApiOperation({ summary: "Delete wallet account" })
|
||||||
|
deleteAccount(@Param("id") id: string) {
|
||||||
|
return this.service.deleteAccount(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ const navigationSections: { title: string; items: NavItem[] }[] = [
|
|||||||
title: 'Customer Services',
|
title: 'Customer Services',
|
||||||
items: [
|
items: [
|
||||||
// { name: 'Loyalty Program', href: '/loyalty', icon: Gift, permission: PERMS.passengers.view },
|
// { name: 'Loyalty Program', href: '/loyalty', icon: Gift, permission: PERMS.passengers.view },
|
||||||
// { name: 'Support Center', href: '/support', icon: MessageSquare, permission: PERMS.bookings.view },
|
{ name: 'Support Center', href: '/support', icon: MessageSquare, permission: PERMS.bookings.view },
|
||||||
{ name: 'Notifications', href: '/notifications', icon: Bell, permission: PERMS.notifications.send },
|
{ name: 'Notifications', href: '/notifications', icon: Bell, permission: PERMS.notifications.send },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1166,6 +1166,14 @@ function PassengersForm() {
|
|||||||
Finish verifying Passenger {(verifyingIndex ?? 0) + 1} first
|
Finish verifying Passenger {(verifyingIndex ?? 0) + 1} first
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{/* <button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleForm(index)}
|
||||||
|
disabled={isVerifyingThis}
|
||||||
|
className="text-sm text-gray-500 dark:text-gray-400 hover:underline mt-3 block mx-auto disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Skip for now
|
||||||
|
</button> */}
|
||||||
</div>
|
</div>
|
||||||
) : showManualEntryLink ? (
|
) : showManualEntryLink ? (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
|
|||||||
@@ -12,24 +12,23 @@
|
|||||||
# Server layout (one file per service):
|
# Server layout (one file per service):
|
||||||
# /home/user/environmen/<project>/<branch-slug>/freight-api.env
|
# /home/user/environmen/<project>/<branch-slug>/freight-api.env
|
||||||
# /home/user/environmen/<project>/<branch-slug>/freight-portal.env
|
# /home/user/environmen/<project>/<branch-slug>/freight-portal.env
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
DEPLOY_USER="${DEPLOY_USER:-tria}"
|
DEPLOY_USER="${DEPLOY_USER:-tria}"
|
||||||
BRANCH="${BRANCH:?BRANCH is required}"
|
BRANCH="${BRANCH:?BRANCH is required}"
|
||||||
BRANCH_SLUG="${BRANCH_SLUG:-$(echo "${BRANCH}" | tr "[:upper:]" "[:lower:]" | sed -E "s/[^a-z0-9]+/-/g; s/^-+//; s/-+$//")}"
|
BRANCH_SLUG="${BRANCH_SLUG:-$(echo "${BRANCH}" | tr "[:upper:]" "[:lower:]" | sed -E "s/[^a-z0-9]+/-/g; s/^-+//; s/-+$//")}"
|
||||||
ENV_ROOT="${ENV_ROOT:-/home/${DEPLOY_USER}/environment/edr/${BRANCH_SLUG}/${PROJECT:?PROJECT is required}}"
|
ENV_ROOT="${ENV_ROOT:-/home/${DEPLOY_USER}/environment/edr/${BRANCH_SLUG}/${PROJECT:?PROJECT is required}}"
|
||||||
CI_ENV_FILE="${CI_ENV_FILE:?CI_ENV_FILE is required (e.g. \${WORKSPACE}/.ci-env/<service>.env)}"
|
CI_ENV_FILE="${CI_ENV_FILE:?CI_ENV_FILE is required (e.g. \${WORKSPACE}/.ci-env/<service>.env)}"
|
||||||
|
|
||||||
if [[ ! -d "${ENV_ROOT}" ]]; then
|
if [[ ! -d "${ENV_ROOT}" ]]; then
|
||||||
echo "Environment directory not found: ${ENV_ROOT}" >&2
|
echo "Environment directory not found: ${ENV_ROOT}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "Using environment directory: ${ENV_ROOT}"
|
echo "Using environment directory: ${ENV_ROOT}"
|
||||||
|
|
||||||
mkdir -p "$(dirname "${CI_ENV_FILE}")"
|
mkdir -p "$(dirname "${CI_ENV_FILE}")"
|
||||||
|
if [[ -d "${CI_ENV_FILE}" ]]; then
|
||||||
|
echo "Removing stale directory at ${CI_ENV_FILE}" >&2
|
||||||
|
rm -rf "${CI_ENV_FILE}"
|
||||||
|
fi
|
||||||
: > "${CI_ENV_FILE}"
|
: > "${CI_ENV_FILE}"
|
||||||
|
|
||||||
declare -A SERVICE_ENV_TARGET=(
|
declare -A SERVICE_ENV_TARGET=(
|
||||||
["freight-api"]="apps/edr-freight-api/.env"
|
["freight-api"]="apps/edr-freight-api/.env"
|
||||||
["freight-portal"]="apps/edr-freight-web/portal/.env"
|
["freight-portal"]="apps/edr-freight-web/portal/.env"
|
||||||
@@ -39,36 +38,29 @@ declare -A SERVICE_ENV_TARGET=(
|
|||||||
["passenger-backoffice"]="apps/edr-passenger-web/backoffice/.env"
|
["passenger-backoffice"]="apps/edr-passenger-web/backoffice/.env"
|
||||||
["payment-api"]="apps/edr-payment-api/.env"
|
["payment-api"]="apps/edr-payment-api/.env"
|
||||||
)
|
)
|
||||||
|
|
||||||
for service in "$@"; do
|
for service in "$@"; do
|
||||||
src="${ENV_ROOT}/${service}.env"
|
src="${ENV_ROOT}/${service}.env"
|
||||||
dest="${SERVICE_ENV_TARGET[${service}]:-}"
|
dest="${SERVICE_ENV_TARGET[${service}]:-}"
|
||||||
|
|
||||||
if [[ -z "${dest}" ]]; then
|
if [[ -z "${dest}" ]]; then
|
||||||
echo "Unknown service: ${service}" >&2
|
echo "Unknown service: ${service}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -f "${src}" ]]; then
|
if [[ ! -f "${src}" ]]; then
|
||||||
echo "Missing env file: ${src}" >&2
|
echo "Missing env file: ${src}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$(dirname "${dest}")"
|
mkdir -p "$(dirname "${dest}")"
|
||||||
cp "${src}" "${dest}"
|
cp "${src}" "${dest}"
|
||||||
echo "Synced ${src} -> ${dest}"
|
echo "Synced ${src} -> ${dest}"
|
||||||
|
|
||||||
port_value=$(sed -n -E 's/^[[:space:]]*PORT[[:space:]]*=[[:space:]]*"?([^"#]+)"?[[:space:]]*(#.*)?$/\1/p' "${src}" | head -n1 | tr -d '[:space:]')
|
port_value=$(sed -n -E 's/^[[:space:]]*PORT[[:space:]]*=[[:space:]]*"?([^"#]+)"?[[:space:]]*(#.*)?$/\1/p' "${src}" | head -n1 | tr -d '[:space:]')
|
||||||
if [[ -z "${port_value}" ]]; then
|
if [[ -z "${port_value}" ]]; then
|
||||||
echo "Missing required PORT in env file: ${src}" >&2
|
echo "Missing required PORT in env file: ${src}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
service_var=$(echo "${service}" | tr '[:lower:]-' '[:upper:]_')
|
service_var=$(echo "${service}" | tr '[:lower:]-' '[:upper:]_')
|
||||||
echo "${service_var}_PORT=${port_value}" >> "${CI_ENV_FILE}"
|
echo "${service_var}_PORT=${port_value}" >> "${CI_ENV_FILE}"
|
||||||
echo "Exported ${service_var}_PORT from ${src}"
|
echo "Exported ${service_var}_PORT from ${src}"
|
||||||
|
|
||||||
# Forward NEXT_PUBLIC_* and VITE_* vars so docker compose build can inject them as build args.
|
# Forward NEXT_PUBLIC_* and VITE_* vars so docker compose build can inject them as build args.
|
||||||
grep -E '^[[:space:]]*(NEXT_PUBLIC_|VITE_)[A-Za-z0-9_]+=' "${src}" \
|
grep -E '^[[:space:]]*(NEXT_PUBLIC_|VITE_)[A-Za-z0-9_]+=' "${src}" \
|
||||||
| sed -E 's/^[[:space:]]*//' >> "${CI_ENV_FILE}" || true
|
| sed -E 's/^[[:space:]]*//' >> "${CI_ENV_FILE}" || true
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ declare -A SERVICE_ENV_TARGET=(
|
|||||||
["freight-api"]="apps/edr-freight-api/.env"
|
["freight-api"]="apps/edr-freight-api/.env"
|
||||||
["freight-portal"]="apps/edr-freight-web/portal/.env"
|
["freight-portal"]="apps/edr-freight-web/portal/.env"
|
||||||
["freight-backoffice"]="apps/edr-freight-web/backoffice/.env"
|
["freight-backoffice"]="apps/edr-freight-web/backoffice/.env"
|
||||||
|
["gps-tracker"]="apps/edr-gps-tracker/.env"
|
||||||
["passenger-api"]="apps/edr-passenger-api/.env"
|
["passenger-api"]="apps/edr-passenger-api/.env"
|
||||||
["passenger-portal"]="apps/edr-passenger-web/portal/.env"
|
["passenger-portal"]="apps/edr-passenger-web/portal/.env"
|
||||||
["passenger-backoffice"]="apps/edr-passenger-web/backoffice/.env"
|
["passenger-backoffice"]="apps/edr-passenger-web/backoffice/.env"
|
||||||
@@ -65,4 +66,4 @@ for service in "$@"; do
|
|||||||
grep -E '^[[:space:]]*(NEXT_PUBLIC_|VITE_)[A-Za-z0-9_]+=' "${src}" \
|
grep -E '^[[:space:]]*(NEXT_PUBLIC_|VITE_)[A-Za-z0-9_]+=' "${src}" \
|
||||||
| sed -E 's/^[[:space:]]*//' >> "${GITHUB_ENV}" || true
|
| sed -E 's/^[[:space:]]*//' >> "${GITHUB_ENV}" || true
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
Reference in New Issue
Block a user