mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 00:38:11 +00:00
Boarding, payment methods, journey direction on seat hold, and more updates
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { DeleteOperationException } from './delete-operation.exception';
|
||||
|
||||
@Catch(DeleteOperationException, HttpException)
|
||||
export class DeleteExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: DeleteOperationException | HttpException, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const status = exception.getStatus?.() || HttpStatus.BAD_REQUEST;
|
||||
|
||||
if (exception instanceof DeleteOperationException) {
|
||||
// Format the response specifically for delete operations
|
||||
response.status(status).json({
|
||||
statusCode: status,
|
||||
error: 'Delete Operation Failed',
|
||||
message: exception.message,
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'DELETE_CONSTRAINT_VIOLATION',
|
||||
userFriendly: true,
|
||||
details: {
|
||||
canRetry: true,
|
||||
action: 'RESOLVE_DEPENDENCIES',
|
||||
hint: 'Please resolve the listed dependencies and try again.'
|
||||
}
|
||||
});
|
||||
} else if (exception instanceof HttpException) {
|
||||
// Handle other HTTP exceptions normally
|
||||
const exceptionResponse = exception.getResponse();
|
||||
|
||||
response.status(status).json({
|
||||
statusCode: status,
|
||||
timestamp: new Date().toISOString(),
|
||||
...(typeof exceptionResponse === 'object'
|
||||
? exceptionResponse
|
||||
: { message: exceptionResponse }
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
export interface DeleteConstraint {
|
||||
entityName: string;
|
||||
count: number;
|
||||
action: 'delete' | 'reassign' | 'cancel' | 'complete';
|
||||
}
|
||||
|
||||
export class DeleteOperationException extends BadRequestException {
|
||||
constructor(
|
||||
entityType: string,
|
||||
entityName: string,
|
||||
constraints: DeleteConstraint[]
|
||||
) {
|
||||
const message = DeleteOperationException.buildUserFriendlyMessage(
|
||||
entityType,
|
||||
entityName,
|
||||
constraints
|
||||
);
|
||||
super(message);
|
||||
}
|
||||
|
||||
private static buildUserFriendlyMessage(
|
||||
entityType: string,
|
||||
entityName: string,
|
||||
constraints: DeleteConstraint[]
|
||||
): string {
|
||||
const baseMessage = `Cannot delete ${entityType.toLowerCase()} "${entityName}".`;
|
||||
|
||||
if (constraints.length === 0) {
|
||||
return `${baseMessage} Unknown constraint violation.`;
|
||||
}
|
||||
|
||||
const constraintMessages = constraints.map(constraint => {
|
||||
const { entityName: constraintEntity, count, action } = constraint;
|
||||
|
||||
const entityDisplayName = count === 1
|
||||
? constraintEntity.toLowerCase()
|
||||
: `${constraintEntity.toLowerCase()}s`;
|
||||
|
||||
const actionText = this.getActionText(action, count);
|
||||
|
||||
return `• ${count} ${entityDisplayName} ${count === 1 ? 'is' : 'are'} still ${this.getStatusText(constraintEntity)}. Please ${actionText} first.`;
|
||||
});
|
||||
|
||||
return [
|
||||
baseMessage,
|
||||
'',
|
||||
'The following dependencies must be resolved:',
|
||||
...constraintMessages,
|
||||
'',
|
||||
'Once all dependencies are resolved, you can retry the deletion.'
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
private static getActionText(action: string, count: number): string {
|
||||
const actions: Record<string, string> = {
|
||||
delete: count === 1 ? 'delete it' : 'delete them',
|
||||
reassign: count === 1 ? 'reassign it' : 'reassign them',
|
||||
cancel: count === 1 ? 'cancel it' : 'cancel them',
|
||||
complete: count === 1 ? 'complete it' : 'complete them'
|
||||
};
|
||||
return actions[action] || (count === 1 ? 'resolve it' : 'resolve them');
|
||||
}
|
||||
|
||||
private static getStatusText(entityName: string): string {
|
||||
const statusTexts: Record<string, string> = {
|
||||
booking: 'active',
|
||||
schedule: 'in use',
|
||||
coach: 'assigned',
|
||||
seat: 'occupied or blocked',
|
||||
'seat class': 'in use by fare rules',
|
||||
'coach type': 'in use by coaches or seat classes',
|
||||
train: 'scheduled',
|
||||
ticket: 'issued',
|
||||
'payment record': 'linked',
|
||||
'fare rule': 'active',
|
||||
route: 'in use by schedules',
|
||||
passenger: 'active with bookings or accounts',
|
||||
promotion: 'active',
|
||||
station: 'in use by routes'
|
||||
};
|
||||
return statusTexts[entityName.toLowerCase()] || 'in use';
|
||||
}
|
||||
}
|
||||
2
apps/edr-passenger-api/src/common/exceptions/index.ts
Normal file
2
apps/edr-passenger-api/src/common/exceptions/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { DeleteOperationException, DeleteConstraint } from './delete-operation.exception';
|
||||
export { DeleteExceptionFilter } from './delete-exception.filter';
|
||||
33
apps/edr-passenger-api/src/common/iam.guard.ts
Normal file
33
apps/edr-passenger-api/src/common/iam.guard.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class IamGuard implements CanActivate {
|
||||
canActivate(
|
||||
context: ExecutionContext,
|
||||
): boolean | Promise<boolean> | Observable<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const authHeader = request.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
throw new UnauthorizedException('No IAM token provided');
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
// TODO: Implement actual IAM token validation
|
||||
// For now, just check if token exists
|
||||
if (!token) {
|
||||
throw new UnauthorizedException('Invalid IAM token');
|
||||
}
|
||||
|
||||
// Add user info to request for downstream usage
|
||||
request.user = {
|
||||
id: 'iam-user-id',
|
||||
roles: ['AGENT'],
|
||||
permissions: []
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
137
apps/edr-passenger-api/src/common/utils/timezone.utils.ts
Normal file
137
apps/edr-passenger-api/src/common/utils/timezone.utils.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Timezone Utility for Ethiopian Railway
|
||||
*
|
||||
* All dates/times in the system are stored and handled in Ethiopian Time (EAT - UTC+3).
|
||||
* This utility ensures consistent date handling across the application.
|
||||
*
|
||||
* IMPORTANT: The application timezone is set to 'Africa/Addis_Ababa' in main.ts
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse a date string or Date object ensuring it's treated as Ethiopian time (EAT - UTC+3)
|
||||
*
|
||||
* @param dateInput - ISO string, date string, or Date object
|
||||
* @returns Date object in Ethiopian time
|
||||
*
|
||||
* @example
|
||||
* parseEthiopianTime('2026-06-15T08:00:00') // Treats as 08:00 EAT, not UTC
|
||||
* parseEthiopianTime('2026-06-15') // Treats as midnight EAT
|
||||
*/
|
||||
export function parseEthiopianTime(dateInput: string | Date): Date {
|
||||
if (dateInput instanceof Date) {
|
||||
return dateInput;
|
||||
}
|
||||
|
||||
// Parse as local time (EAT) since TZ is set to Africa/Addis_Ababa
|
||||
return new Date(dateInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the start of day (00:00:00) in Ethiopian time
|
||||
*
|
||||
* @param date - Date object or date string
|
||||
* @returns Date object set to midnight EAT
|
||||
*/
|
||||
export function startOfDayEAT(date: Date | string): Date {
|
||||
const d = typeof date === 'string' ? new Date(date) : new Date(date);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the end of day (23:59:59.999) in Ethiopian time
|
||||
*
|
||||
* @param date - Date object or date string
|
||||
* @returns Date object set to end of day EAT
|
||||
*/
|
||||
export function endOfDayEAT(date: Date | string): Date {
|
||||
const d = typeof date === 'string' ? new Date(date) : new Date(date);
|
||||
d.setHours(23, 59, 59, 999);
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the start of the next day in Ethiopian time
|
||||
*
|
||||
* @param date - Date object or date string
|
||||
* @returns Date object set to midnight of next day EAT
|
||||
*/
|
||||
export function startOfNextDayEAT(date: Date | string): Date {
|
||||
const d = startOfDayEAT(date);
|
||||
d.setDate(d.getDate() + 1);
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date for display in Ethiopian time
|
||||
*
|
||||
* @param date - Date object
|
||||
* @param options - Intl.DateTimeFormatOptions
|
||||
* @returns Formatted date string
|
||||
*/
|
||||
export function formatEthiopianTime(
|
||||
date: Date,
|
||||
options: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
}
|
||||
): string {
|
||||
return new Intl.DateTimeFormat('en-ET', {
|
||||
...options,
|
||||
timeZone: 'Africa/Addis_Ababa',
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add minutes to a date
|
||||
*
|
||||
* @param date - Date object
|
||||
* @param minutes - Number of minutes to add
|
||||
* @returns New Date object
|
||||
*/
|
||||
export function addMinutes(date: Date, minutes: number): Date {
|
||||
return new Date(date.getTime() + minutes * 60_000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add hours to a date
|
||||
*
|
||||
* @param date - Date object
|
||||
* @param hours - Number of hours to add
|
||||
* @returns New Date object
|
||||
*/
|
||||
export function addHours(date: Date, hours: number): Date {
|
||||
return new Date(date.getTime() + hours * 60 * 60_000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add days to a date
|
||||
*
|
||||
* @param date - Date object
|
||||
* @param days - Number of days to add
|
||||
* @returns New Date object
|
||||
*/
|
||||
export function addDays(date: Date, days: number): Date {
|
||||
const result = new Date(date);
|
||||
result.setDate(result.getDate() + days);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two dates are on the same day (Ethiopian time)
|
||||
*
|
||||
* @param date1 - First date
|
||||
* @param date2 - Second date
|
||||
* @returns true if both dates are on the same calendar day in EAT
|
||||
*/
|
||||
export function isSameDayEAT(date1: Date, date2: Date): boolean {
|
||||
return (
|
||||
date1.getFullYear() === date2.getFullYear() &&
|
||||
date1.getMonth() === date2.getMonth() &&
|
||||
date1.getDate() === date2.getDate()
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user