Merge pull request 'azeem' (#23) from azeem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/23
This commit is contained in:
@@ -14,6 +14,33 @@ CREATE SCHEMA IF NOT EXISTS masters_action_builder;
|
||||
CREATE SCHEMA IF NOT EXISTS masters_lookup;
|
||||
CREATE SCHEMA IF NOT EXISTS cohort;
|
||||
CREATE SCHEMA IF NOT EXISTS policy_engine;
|
||||
CREATE SCHEMA IF NOT EXISTS audit;
|
||||
CREATE SCHEMA IF NOT EXISTS recovery_incident;
|
||||
|
||||
-- ─── Audit Logs ──────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit.tbl_audit_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id VARCHAR(255) NOT NULL,
|
||||
module VARCHAR(100) NOT NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
entity_id VARCHAR(255),
|
||||
entity_label VARCHAR(255),
|
||||
before JSONB,
|
||||
after JSONB,
|
||||
performed_by VARCHAR(255),
|
||||
ip_address VARCHAR(50),
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_tenant ON audit.tbl_audit_logs (tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_module ON audit.tbl_audit_logs (module);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit.tbl_audit_logs (action);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit.tbl_audit_logs (entity_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_createdat ON audit.tbl_audit_logs (created_at DESC);
|
||||
|
||||
|
||||
|
||||
-- ─── Enum Types ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
-- PostgreSQL 14+
|
||||
-- =============================================================================
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS masters_lookup;
|
||||
CREATE SCHEMA IF NOT EXISTS masters_rule_engine;
|
||||
CREATE SCHEMA IF NOT EXISTS masters_action_builder;
|
||||
|
||||
-- 1. Action Categories
|
||||
INSERT INTO masters_action_builder.tbl_action_categories (code, name, "displayOrder", "isActive")
|
||||
SELECT code, name, displayOrder, true FROM (VALUES
|
||||
@@ -224,6 +228,7 @@ SELECT label, value, true FROM (VALUES
|
||||
) AS t(label, value)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_jurisdictions WHERE masters_lookup.tbl_jurisdictions.value = t.value);
|
||||
|
||||
|
||||
-- 12. Rule Categories (Business Rule Categories)
|
||||
DELETE FROM masters_rule_engine.tbl_rules_categories
|
||||
WHERE code NOT IN (
|
||||
@@ -695,5 +700,161 @@ ON CONFLICT (code) DO UPDATE SET
|
||||
"operatorType" = EXCLUDED."operatorType",
|
||||
"displayOrder" = EXCLUDED."displayOrder";
|
||||
|
||||
-- 38. Baggage Types
|
||||
INSERT INTO masters_lookup.tbl_baggage_types (label, value, "isActive")
|
||||
SELECT label, value, true FROM (VALUES
|
||||
('Checked Bag', 'checked-bag'),
|
||||
('Cabin Bag', 'cabin-bag'),
|
||||
('Oversized Baggage', 'oversized-baggage'),
|
||||
('Sports Equipment', 'sports-equipment'),
|
||||
('Special Baggage', 'special-baggage')
|
||||
) AS t(label, value)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_baggage_types WHERE masters_lookup.tbl_baggage_types.value = t.value);
|
||||
|
||||
-- 39. Compensation Rules
|
||||
INSERT INTO masters_lookup.tbl_compensation_rules (label, value, "isActive")
|
||||
SELECT label, value, true FROM (VALUES
|
||||
('EU261', 'eu261'),
|
||||
('UK261', 'uk261'),
|
||||
('APPR', 'appr'),
|
||||
('DGCA', 'dgca'),
|
||||
('DOT', 'dot'),
|
||||
('Airline Policy', 'airline-policy')
|
||||
) AS t(label, value)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_compensation_rules WHERE masters_lookup.tbl_compensation_rules.value = t.value);
|
||||
|
||||
-- 40. Approval Levels
|
||||
INSERT INTO masters_lookup.tbl_approval_levels (label, value, "isActive")
|
||||
SELECT label, value, true FROM (VALUES
|
||||
('Supervisor', 'supervisor'),
|
||||
('Manager', 'manager'),
|
||||
('Director', 'director')
|
||||
) AS t(label, value)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_approval_levels WHERE masters_lookup.tbl_approval_levels.value = t.value);
|
||||
|
||||
-- 41. Eligible Fare Types
|
||||
INSERT INTO masters_lookup.tbl_eligible_fare_types (label, value, "isActive")
|
||||
SELECT label, value, true FROM (VALUES
|
||||
('Published Fares', 'published-fares'),
|
||||
('Promo Fares', 'promo-fares')
|
||||
) AS t(label, value)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_eligible_fare_types WHERE masters_lookup.tbl_eligible_fare_types.value = t.value);
|
||||
|
||||
-- 42. Applicable Products
|
||||
INSERT INTO masters_lookup.tbl_applicable_products (label, value, "isActive")
|
||||
SELECT label, value, true FROM (VALUES
|
||||
('Flights', 'flights'),
|
||||
('Ancillary', 'ancillary'),
|
||||
('Lounge', 'lounge')
|
||||
) AS t(label, value)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_applicable_products WHERE masters_lookup.tbl_applicable_products.value = t.value);
|
||||
|
||||
-- 43. Meal Types
|
||||
INSERT INTO masters_lookup.tbl_meal_types (label, value, "isActive")
|
||||
SELECT label, value, true FROM (VALUES
|
||||
('Breakfast', 'breakfast'),
|
||||
('Lunch', 'lunch'),
|
||||
('Dinner', 'dinner'),
|
||||
('Snack', 'snack'),
|
||||
('Any', 'any')
|
||||
) AS t(label, value)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_meal_types WHERE masters_lookup.tbl_meal_types.value = t.value);
|
||||
|
||||
-- 44. Airport Restrictions
|
||||
INSERT INTO masters_lookup.tbl_airport_restrictions (label, value, "isActive")
|
||||
SELECT label, value, true FROM (VALUES
|
||||
('Departure', 'departure'),
|
||||
('Transit', 'transit'),
|
||||
('Arrival', 'arrival'),
|
||||
('Any Airport', 'any-airport')
|
||||
) AS t(label, value)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_airport_restrictions WHERE masters_lookup.tbl_airport_restrictions.value = t.value);
|
||||
|
||||
-- 45. Destination Types
|
||||
INSERT INTO masters_lookup.tbl_destination_types (label, value, "isActive")
|
||||
SELECT label, value, true FROM (VALUES
|
||||
('Home', 'home'),
|
||||
('Hotel', 'hotel'),
|
||||
('Airport', 'airport'),
|
||||
('City Center', 'city-center')
|
||||
) AS t(label, value)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_destination_types WHERE masters_lookup.tbl_destination_types.value = t.value);
|
||||
|
||||
-- 46. Vehicle Categories
|
||||
INSERT INTO masters_lookup.tbl_vehicle_categories (label, value, "isActive")
|
||||
SELECT label, value, true FROM (VALUES
|
||||
('Executive Sedan', 'executive-sedan'),
|
||||
('Luxury SUV', 'luxury-suv'),
|
||||
('Premium Van', 'premium-van')
|
||||
) AS t(label, value)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_vehicle_categories WHERE masters_lookup.tbl_vehicle_categories.value = t.value);
|
||||
|
||||
-- 47. Cabin Preferences
|
||||
INSERT INTO masters_lookup.tbl_cabin_preferences (label, value, "isActive")
|
||||
SELECT label, value, true FROM (VALUES
|
||||
('Same Cabin', 'same-cabin'),
|
||||
('Upgrade if Available', 'upgrade-if-available'),
|
||||
('Lowest Available', 'lowest-available')
|
||||
) AS t(label, value)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_cabin_preferences WHERE masters_lookup.tbl_cabin_preferences.value = t.value);
|
||||
|
||||
-- 48. Airline Preferences
|
||||
INSERT INTO masters_lookup.tbl_airline_preferences (label, value, "isActive")
|
||||
SELECT label, value, true FROM (VALUES
|
||||
('Same Airline', 'same-airline'),
|
||||
('Alliance Airline', 'alliance-airline'),
|
||||
('Any Partner', 'any-partner')
|
||||
) AS t(label, value)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_airline_preferences WHERE masters_lookup.tbl_airline_preferences.value = t.value);
|
||||
|
||||
-- 49. Meal Categories
|
||||
INSERT INTO masters_lookup.tbl_meal_categories (label, value, "isActive")
|
||||
SELECT label, value, true FROM (VALUES
|
||||
('Vegetarian', 'vegetarian'),
|
||||
('Non-Vegetarian', 'non-vegetarian'),
|
||||
('Vegan', 'vegan'),
|
||||
('Child Meal', 'child-meal')
|
||||
) AS t(label, value)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_meal_categories WHERE masters_lookup.tbl_meal_categories.value = t.value);
|
||||
|
||||
-- 50. Seat Categories
|
||||
INSERT INTO masters_lookup.tbl_seat_categories (label, value, "isActive")
|
||||
SELECT label, value, true FROM (VALUES
|
||||
('Standard', 'standard'),
|
||||
('Preferred', 'preferred'),
|
||||
('Exit Row', 'exit-row'),
|
||||
('Extra Legroom', 'extra-legroom')
|
||||
) AS t(label, value)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_seat_categories WHERE masters_lookup.tbl_seat_categories.value = t.value);
|
||||
|
||||
-- 51. Compensation Bases
|
||||
INSERT INTO masters_lookup.tbl_compensation_bases (label, value, "isActive")
|
||||
SELECT label, value, true FROM (VALUES
|
||||
('Regulatory Rule', 'regulatory-rule'),
|
||||
('Airline Policy', 'airline-policy'),
|
||||
('Percentage Difference', 'percentage-difference')
|
||||
) AS t(label, value)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_compensation_bases WHERE masters_lookup.tbl_compensation_bases.value = t.value);
|
||||
|
||||
-- 52. Tax Types
|
||||
INSERT INTO masters_lookup.tbl_tax_types (label, value, "isActive")
|
||||
SELECT label, value, true FROM (VALUES
|
||||
('Airport Tax', 'airport-tax'),
|
||||
('Fuel Surcharge', 'fuel-surcharge'),
|
||||
('Government Tax', 'government-tax'),
|
||||
('Service Tax', 'service-tax'),
|
||||
('VAT', 'vat'),
|
||||
('GST', 'gst'),
|
||||
('Airport Development Fee', 'airport-development-fee')
|
||||
) AS t(label, value)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_tax_types WHERE masters_lookup.tbl_tax_types.value = t.value);
|
||||
|
||||
-- 53. Room Types
|
||||
INSERT INTO masters_lookup.tbl_room_types (label, value, "isActive")
|
||||
SELECT label, value, true FROM (VALUES
|
||||
('Single', 'single'),
|
||||
('Double', 'double'),
|
||||
('Twin', 'twin'),
|
||||
('Family', 'family')
|
||||
) AS t(label, value)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_room_types WHERE masters_lookup.tbl_room_types.value = t.value);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { DatabaseModule } from './database/database.module';
|
||||
import { HealthModule } from './modules/health/health.module';
|
||||
@@ -8,6 +9,8 @@ import { TenantModule } from './modules/tenant/tenant.module';
|
||||
import { TenantMiddleware } from './common/tenant/tenant.middleware';
|
||||
import { PolicyEngineModule } from './modules/policy-engine/policy-engine.module';
|
||||
import { RecoveryIncidentModule } from './modules/recovery-incident/recovery-incident.module';
|
||||
import { AuditLogModule } from './modules/audit-log/audit-log.module';
|
||||
import { AuditInterceptor } from './common/interceptors/audit.interceptor';
|
||||
|
||||
const env = process.env.NODE_ENV;
|
||||
const envFilePath = env ? [`.env.${env}`, '.env.local', '.env'] : ['.env.local', '.env'];
|
||||
@@ -25,6 +28,13 @@ const envFilePath = env ? [`.env.${env}`, '.env.local', '.env'] : ['.env.local',
|
||||
CohortModule,
|
||||
PolicyEngineModule,
|
||||
RecoveryIncidentModule,
|
||||
AuditLogModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: AuditInterceptor,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
CallHandler,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { Request } from 'express';
|
||||
import { AuditLogService } from '../../modules/audit-log/audit-log.service';
|
||||
|
||||
// Methods that mutate data — only these are audited
|
||||
const MUTABLE_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];
|
||||
|
||||
// Map: [HTTP method, URL pattern] → { module, action }
|
||||
// The interceptor only handles CREATE (POST) since UPDATE/DELETE
|
||||
// need before-snapshots which are captured directly in each service.
|
||||
const MODULE_MAP: Array<{ pattern: RegExp; module: string }> = [
|
||||
{ pattern: /policy-engine/, module: 'policy-engine' },
|
||||
{ pattern: /recovery-incidents/, module: 'recovery-incident' },
|
||||
{ pattern: /cohort/, module: 'cohort' },
|
||||
{ pattern: /master-data/, module: 'master-data' },
|
||||
];
|
||||
|
||||
function resolveModule(url: string): string {
|
||||
for (const entry of MODULE_MAP) {
|
||||
if (entry.pattern.test(url)) return entry.module;
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function resolveAction(method: string, url: string): string {
|
||||
if (method === 'POST') return 'CREATE';
|
||||
if (method === 'DELETE') return 'DELETE';
|
||||
if (method === 'PATCH' && url.includes('/status')) return 'STATUS_CHANGE';
|
||||
if (method === 'PATCH' || method === 'PUT') return 'UPDATE';
|
||||
return method;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuditInterceptor implements NestInterceptor {
|
||||
private readonly logger = new Logger(AuditInterceptor.name);
|
||||
|
||||
constructor(private readonly auditLogService: AuditLogService) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
const req = context.switchToHttp().getRequest<Request>();
|
||||
const { method, ip, headers } = req;
|
||||
const url = req.originalUrl || req.url;
|
||||
|
||||
// Only intercept CREATE (POST) — UPDATE/DELETE handled in services
|
||||
if (!MUTABLE_METHODS.includes(method) || method !== 'POST') {
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
const module = resolveModule(url);
|
||||
const action = resolveAction(method, url);
|
||||
const userAgent = headers['user-agent'] ?? '';
|
||||
const tenantId = (headers['x-tenant-id'] as string) ?? 'unknown';
|
||||
|
||||
this.logger.debug(`Intercepted ${method} ${url} -> Module: ${module}, Action: ${action}`);
|
||||
|
||||
return next.handle().pipe(
|
||||
tap({
|
||||
next: async (responseData: any) => {
|
||||
this.logger.debug(`Writing audit log for ${module} ${action}...`);
|
||||
try {
|
||||
await this.auditLogService.log({
|
||||
module,
|
||||
action,
|
||||
entityId: responseData?.id ?? undefined,
|
||||
entityLabel: responseData?.name ?? responseData?.recoveryCode ?? responseData?.flightNumber ?? undefined,
|
||||
before: undefined, // no before on CREATE
|
||||
after: responseData ?? undefined,
|
||||
performedBy: tenantId,
|
||||
ipAddress: ip,
|
||||
userAgent: String(userAgent),
|
||||
});
|
||||
} catch (err) {
|
||||
// Never let audit failure break the main request
|
||||
this.logger.warn(`Audit log failed for ${method} ${url}: ${err}`);
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "cohort";`);
|
||||
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "policy_engine";`);
|
||||
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "recovery_incident";`);
|
||||
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "audit";`);
|
||||
|
||||
// Run synchronization manually if it was enabled
|
||||
if (options.synchronize) {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Query,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import { AuditLogService } from './audit-log.service';
|
||||
|
||||
@ApiTags('Audit Logs')
|
||||
@Controller('audit-logs')
|
||||
export class AuditLogController {
|
||||
constructor(private readonly auditLogService: AuditLogService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get paginated audit logs with optional filters' })
|
||||
findAll(
|
||||
@Query('page') page: string = '1',
|
||||
@Query('limit') limit: string = '20',
|
||||
@Query('module') module?: string,
|
||||
@Query('action') action?: string,
|
||||
@Query('entityId') entityId?: string,
|
||||
@Query('dateFrom') dateFrom?: string,
|
||||
@Query('dateTo') dateTo?: string,
|
||||
) {
|
||||
return this.auditLogService.findAll({
|
||||
page: Number(page),
|
||||
limit: Number(limit),
|
||||
module,
|
||||
action,
|
||||
entityId,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
});
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get a single audit log entry by ID' })
|
||||
async findOne(@Param('id') id: string) {
|
||||
const log = await this.auditLogService.findOne(id);
|
||||
if (!log) {
|
||||
throw new NotFoundException(`Audit log entry ${id} not found`);
|
||||
}
|
||||
return log;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuditLog } from './entities/audit-log.entity';
|
||||
import { AuditLogService } from './audit-log.service';
|
||||
import { AuditLogController } from './audit-log.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([AuditLog])],
|
||||
controllers: [AuditLogController],
|
||||
providers: [AuditLogService],
|
||||
exports: [AuditLogService], // exported so other modules can inject it
|
||||
})
|
||||
export class AuditLogModule {}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, FindOptionsWhere, Between, MoreThanOrEqual, LessThanOrEqual } from 'typeorm';
|
||||
import { AuditLog } from './entities/audit-log.entity';
|
||||
import { CreateAuditLogDto } from './dto/create-audit-log.dto';
|
||||
import { getTenantIdOrNull } from '../../common/tenant/tenant.context';
|
||||
|
||||
@Injectable()
|
||||
export class AuditLogService {
|
||||
constructor(
|
||||
@InjectRepository(AuditLog)
|
||||
private readonly auditLogRepository: Repository<AuditLog>,
|
||||
) {}
|
||||
|
||||
// ─── Write a single audit entry ──────────────────────────────────────────
|
||||
// Called directly from services (for UPDATE/DELETE with before snapshots)
|
||||
// and from the HTTP interceptor (for CREATE).
|
||||
async log(dto: CreateAuditLogDto): Promise<void> {
|
||||
const tenantId = getTenantIdOrNull() ?? dto.performedBy ?? 'system';
|
||||
const entry = this.auditLogRepository.create({
|
||||
...dto,
|
||||
tenantId,
|
||||
performedBy: dto.performedBy ?? tenantId,
|
||||
});
|
||||
await this.auditLogRepository.save(entry);
|
||||
}
|
||||
|
||||
// ─── Paginated query with filters ────────────────────────────────────────
|
||||
async findAll(options: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
module?: string;
|
||||
action?: string;
|
||||
entityId?: string;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
}) {
|
||||
const { page = 1, limit = 20, module, action, entityId, dateFrom, dateTo } = options;
|
||||
const skip = (page - 1) * limit;
|
||||
const tenantId = getTenantIdOrNull();
|
||||
|
||||
const where: FindOptionsWhere<AuditLog> = {};
|
||||
|
||||
if (tenantId) where.tenantId = tenantId;
|
||||
if (module) where.module = module;
|
||||
if (action) where.action = action;
|
||||
if (entityId) where.entityId = entityId;
|
||||
|
||||
if (dateFrom && dateTo) {
|
||||
where.createdAt = Between(new Date(dateFrom), new Date(dateTo)) as any;
|
||||
} else if (dateFrom) {
|
||||
where.createdAt = MoreThanOrEqual(new Date(dateFrom)) as any;
|
||||
} else if (dateTo) {
|
||||
where.createdAt = LessThanOrEqual(new Date(dateTo)) as any;
|
||||
}
|
||||
|
||||
const [data, total] = await this.auditLogRepository.findAndCount({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Single entry lookup ──────────────────────────────────────────────────
|
||||
async findOne(id: string): Promise<AuditLog | null> {
|
||||
const tenantId = getTenantIdOrNull();
|
||||
const where: FindOptionsWhere<AuditLog> = { id };
|
||||
if (tenantId) where.tenantId = tenantId;
|
||||
return this.auditLogRepository.findOne({ where });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { IsString, IsNotEmpty, IsOptional, IsObject } from 'class-validator';
|
||||
|
||||
export class CreateAuditLogDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
module: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
action: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
entityId?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
entityLabel?: string;
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
before?: Record<string, any>;
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
after?: Record<string, any>;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
performedBy?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
ipAddress?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
userAgent?: string;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tbl_audit_logs', schema: 'audit' })
|
||||
export class AuditLog {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ name: 'tenant_id', type: 'varchar', length: 255 })
|
||||
tenantId!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 100 })
|
||||
module!: string; // 'policy-engine' | 'recovery-incident' | 'cohort' | 'master-data'
|
||||
|
||||
@Column({ type: 'varchar', length: 50 })
|
||||
action!: string; // 'CREATE' | 'UPDATE' | 'DELETE' | 'STATUS_CHANGE'
|
||||
|
||||
@Column({ name: 'entity_id', type: 'varchar', length: 255, nullable: true })
|
||||
entityId?: string;
|
||||
|
||||
@Column({ name: 'entity_label', type: 'varchar', length: 255, nullable: true })
|
||||
entityLabel?: string; // human-readable name (policy name, flight #, etc.)
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
before?: Record<string, any>; // snapshot before change (null for CREATE)
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
after?: Record<string, any>; // snapshot after change (null for DELETE)
|
||||
|
||||
@Column({ name: 'performed_by', type: 'varchar', length: 255, nullable: true })
|
||||
performedBy?: string; // tenantId for now; will be userId when auth is added
|
||||
|
||||
@Column({ name: 'ip_address', type: 'varchar', length: 50, nullable: true })
|
||||
ipAddress?: string;
|
||||
|
||||
@Column({ name: 'user_agent', type: 'text', nullable: true })
|
||||
userAgent?: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CohortController } from './cohort.controller';
|
||||
import { CohortService } from './cohort.service';
|
||||
import { Cohort } from './cohort.entity';
|
||||
import { AuditLogModule } from '../audit-log/audit-log.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Cohort])],
|
||||
imports: [TypeOrmModule.forFeature([Cohort]), AuditLogModule],
|
||||
controllers: [CohortController],
|
||||
providers: [CohortService],
|
||||
exports: [CohortService],
|
||||
|
||||
@@ -5,12 +5,14 @@ import { getTenantId } from '../../common/tenant/tenant.context';
|
||||
import { Cohort } from './cohort.entity';
|
||||
import { CreateCohortDto } from './dto/create-cohort.dto';
|
||||
import { UpdateCohortDto } from './dto/update-cohort.dto';
|
||||
import { AuditLogService } from '../audit-log/audit-log.service';
|
||||
|
||||
@Injectable()
|
||||
export class CohortService {
|
||||
constructor(
|
||||
@InjectRepository(Cohort)
|
||||
private readonly cohortRepository: Repository<Cohort>,
|
||||
private readonly auditLogService: AuditLogService,
|
||||
) {}
|
||||
|
||||
private mapDtoToEntity(dto: CreateCohortDto | UpdateCohortDto) {
|
||||
@@ -96,19 +98,56 @@ export class CohortService {
|
||||
|
||||
async update(id: string, updateCohortDto: UpdateCohortDto): Promise<Cohort> {
|
||||
const cohort = await this.findOne(id);
|
||||
const beforeSnapshot = { ...cohort };
|
||||
const mappedData = this.mapDtoToEntity(updateCohortDto);
|
||||
Object.assign(cohort, mappedData);
|
||||
return this.cohortRepository.save(cohort);
|
||||
const after = await this.cohortRepository.save(cohort);
|
||||
|
||||
await this.auditLogService.log({
|
||||
module: 'cohort',
|
||||
action: 'UPDATE',
|
||||
entityId: id,
|
||||
entityLabel: cohort.name,
|
||||
before: beforeSnapshot,
|
||||
after: { ...after },
|
||||
performedBy: getTenantId(),
|
||||
});
|
||||
|
||||
return after;
|
||||
}
|
||||
|
||||
async updateStatus(id: string, status: string): Promise<Cohort> {
|
||||
const cohort = await this.findOne(id);
|
||||
const beforeStatus = cohort.status;
|
||||
cohort.status = status;
|
||||
return this.cohortRepository.save(cohort);
|
||||
const after = await this.cohortRepository.save(cohort);
|
||||
|
||||
await this.auditLogService.log({
|
||||
module: 'cohort',
|
||||
action: 'STATUS_CHANGE',
|
||||
entityId: id,
|
||||
entityLabel: cohort.name,
|
||||
before: { status: beforeStatus },
|
||||
after: { status },
|
||||
performedBy: getTenantId(),
|
||||
});
|
||||
|
||||
return after;
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const cohort = await this.findOne(id);
|
||||
const beforeSnapshot = { ...cohort };
|
||||
await this.cohortRepository.remove(cohort);
|
||||
|
||||
await this.auditLogService.log({
|
||||
module: 'cohort',
|
||||
action: 'DELETE',
|
||||
entityId: id,
|
||||
entityLabel: beforeSnapshot.name,
|
||||
before: beforeSnapshot,
|
||||
after: undefined,
|
||||
performedBy: getTenantId(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tbl_airline_preferences', schema: 'masters_lookup' })
|
||||
export class AirlinePreference {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
label!: string;
|
||||
|
||||
@Column()
|
||||
value!: string;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tbl_airport_restrictions', schema: 'masters_lookup' })
|
||||
export class AirportRestriction {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
label!: string;
|
||||
|
||||
@Column()
|
||||
value!: string;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tbl_applicable_products', schema: 'masters_lookup' })
|
||||
export class ApplicableProduct {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
label!: string;
|
||||
|
||||
@Column()
|
||||
value!: string;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tbl_approval_levels', schema: 'masters_lookup' })
|
||||
export class ApprovalLevel {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
label!: string;
|
||||
|
||||
@Column()
|
||||
value!: string;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tbl_baggage_types', schema: 'masters_lookup' })
|
||||
export class BaggageType {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
label!: string;
|
||||
|
||||
@Column()
|
||||
value!: string;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tbl_cabin_preferences', schema: 'masters_lookup' })
|
||||
export class CabinPreference {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
label!: string;
|
||||
|
||||
@Column()
|
||||
value!: string;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tbl_compensation_bases', schema: 'masters_lookup' })
|
||||
export class CompensationBasis {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
label!: string;
|
||||
|
||||
@Column()
|
||||
value!: string;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tbl_compensation_rules', schema: 'masters_lookup' })
|
||||
export class CompensationRule {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
label!: string;
|
||||
|
||||
@Column()
|
||||
value!: string;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tbl_destination_types', schema: 'masters_lookup' })
|
||||
export class DestinationType {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
label!: string;
|
||||
|
||||
@Column()
|
||||
value!: string;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tbl_eligible_fare_types', schema: 'masters_lookup' })
|
||||
export class EligibleFareType {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
label!: string;
|
||||
|
||||
@Column()
|
||||
value!: string;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tbl_meal_categories', schema: 'masters_lookup' })
|
||||
export class MealCategory {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
label!: string;
|
||||
|
||||
@Column()
|
||||
value!: string;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tbl_meal_types', schema: 'masters_lookup' })
|
||||
export class MealType {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
label!: string;
|
||||
|
||||
@Column()
|
||||
value!: string;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tbl_room_types', schema: 'masters_lookup' })
|
||||
export class RoomType {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
label!: string;
|
||||
|
||||
@Column()
|
||||
value!: string;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tbl_seat_categories', schema: 'masters_lookup' })
|
||||
export class SeatCategory {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
label!: string;
|
||||
|
||||
@Column()
|
||||
value!: string;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tbl_tax_types', schema: 'masters_lookup' })
|
||||
export class TaxType {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
label!: string;
|
||||
|
||||
@Column()
|
||||
value!: string;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tbl_vehicle_categories', schema: 'masters_lookup' })
|
||||
export class VehicleCategory {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
label!: string;
|
||||
|
||||
@Column()
|
||||
value!: string;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -44,6 +44,22 @@ import { RefundBasis } from './entities/refund-basis.entity';
|
||||
import { Currency } from './entities/currency.entity';
|
||||
import { RefundMethod } from './entities/refund-method.entity';
|
||||
import { AmountType } from './entities/amount-type.entity';
|
||||
import { BaggageType } from './entities/baggage-type.entity';
|
||||
import { CompensationRule } from './entities/compensation-rule.entity';
|
||||
import { ApprovalLevel } from './entities/approval-level.entity';
|
||||
import { EligibleFareType } from './entities/eligible-fare-type.entity';
|
||||
import { ApplicableProduct } from './entities/applicable-product.entity';
|
||||
import { MealType } from './entities/meal-type.entity';
|
||||
import { AirportRestriction } from './entities/airport-restriction.entity';
|
||||
import { DestinationType } from './entities/destination-type.entity';
|
||||
import { VehicleCategory } from './entities/vehicle-category.entity';
|
||||
import { CabinPreference } from './entities/cabin-preference.entity';
|
||||
import { AirlinePreference } from './entities/airline-preference.entity';
|
||||
import { MealCategory } from './entities/meal-category.entity';
|
||||
import { SeatCategory } from './entities/seat-category.entity';
|
||||
import { CompensationBasis } from './entities/compensation-basis.entity';
|
||||
import { TaxType } from './entities/tax-type.entity';
|
||||
import { RoomType } from './entities/room-type.entity';
|
||||
|
||||
import { ConditionGroup } from './entities/condition-group.entity';
|
||||
import { ConditionField } from './entities/condition-field.entity';
|
||||
@@ -92,6 +108,22 @@ import { RuleCategoryGroup } from './entities/rule-category-group.entity';
|
||||
Currency,
|
||||
RefundMethod,
|
||||
AmountType,
|
||||
BaggageType,
|
||||
CompensationRule,
|
||||
ApprovalLevel,
|
||||
EligibleFareType,
|
||||
ApplicableProduct,
|
||||
MealType,
|
||||
AirportRestriction,
|
||||
DestinationType,
|
||||
VehicleCategory,
|
||||
CabinPreference,
|
||||
AirlinePreference,
|
||||
MealCategory,
|
||||
SeatCategory,
|
||||
CompensationBasis,
|
||||
TaxType,
|
||||
RoomType,
|
||||
ConditionGroup,
|
||||
ConditionField,
|
||||
RuleCategoryGroup,
|
||||
|
||||
@@ -45,6 +45,22 @@ import { RefundBasis } from './entities/refund-basis.entity';
|
||||
import { Currency } from './entities/currency.entity';
|
||||
import { RefundMethod } from './entities/refund-method.entity';
|
||||
import { AmountType } from './entities/amount-type.entity';
|
||||
import { BaggageType } from './entities/baggage-type.entity';
|
||||
import { CompensationRule } from './entities/compensation-rule.entity';
|
||||
import { ApprovalLevel } from './entities/approval-level.entity';
|
||||
import { EligibleFareType } from './entities/eligible-fare-type.entity';
|
||||
import { ApplicableProduct } from './entities/applicable-product.entity';
|
||||
import { MealType } from './entities/meal-type.entity';
|
||||
import { AirportRestriction } from './entities/airport-restriction.entity';
|
||||
import { DestinationType } from './entities/destination-type.entity';
|
||||
import { VehicleCategory } from './entities/vehicle-category.entity';
|
||||
import { CabinPreference } from './entities/cabin-preference.entity';
|
||||
import { AirlinePreference } from './entities/airline-preference.entity';
|
||||
import { MealCategory } from './entities/meal-category.entity';
|
||||
import { SeatCategory } from './entities/seat-category.entity';
|
||||
import { CompensationBasis } from './entities/compensation-basis.entity';
|
||||
import { TaxType } from './entities/tax-type.entity';
|
||||
import { RoomType } from './entities/room-type.entity';
|
||||
import { ConditionGroup } from './entities/condition-group.entity';
|
||||
import { ConditionField } from './entities/condition-field.entity';
|
||||
import { RuleCategoryGroup } from './entities/rule-category-group.entity';
|
||||
@@ -104,6 +120,22 @@ export class MasterDataService implements OnModuleInit {
|
||||
@InjectRepository(Currency) private currencyRepo: Repository<Currency>,
|
||||
@InjectRepository(RefundMethod) private refundMethodRepo: Repository<RefundMethod>,
|
||||
@InjectRepository(AmountType) private amountTypeRepo: Repository<AmountType>,
|
||||
@InjectRepository(BaggageType) private baggageTypeRepo: Repository<BaggageType>,
|
||||
@InjectRepository(CompensationRule) private compensationRuleRepo: Repository<CompensationRule>,
|
||||
@InjectRepository(ApprovalLevel) private approvalLevelRepo: Repository<ApprovalLevel>,
|
||||
@InjectRepository(EligibleFareType) private eligibleFareTypeRepo: Repository<EligibleFareType>,
|
||||
@InjectRepository(ApplicableProduct) private applicableProductRepo: Repository<ApplicableProduct>,
|
||||
@InjectRepository(MealType) private mealTypeRepo: Repository<MealType>,
|
||||
@InjectRepository(AirportRestriction) private airportRestrictionRepo: Repository<AirportRestriction>,
|
||||
@InjectRepository(DestinationType) private destinationTypeRepo: Repository<DestinationType>,
|
||||
@InjectRepository(VehicleCategory) private vehicleCategoryRepo: Repository<VehicleCategory>,
|
||||
@InjectRepository(CabinPreference) private cabinPreferenceRepo: Repository<CabinPreference>,
|
||||
@InjectRepository(AirlinePreference) private airlinePreferenceRepo: Repository<AirlinePreference>,
|
||||
@InjectRepository(MealCategory) private mealCategoryRepo: Repository<MealCategory>,
|
||||
@InjectRepository(SeatCategory) private seatCategoryRepo: Repository<SeatCategory>,
|
||||
@InjectRepository(CompensationBasis) private compensationBasisRepo: Repository<CompensationBasis>,
|
||||
@InjectRepository(TaxType) private taxTypeRepo: Repository<TaxType>,
|
||||
@InjectRepository(RoomType) private roomTypeRepo: Repository<RoomType>,
|
||||
@InjectRepository(ConditionGroup) private conditionGroupRepo: Repository<ConditionGroup>,
|
||||
@InjectRepository(ConditionField) private conditionFieldRepo: Repository<ConditionField>,
|
||||
@InjectRepository(RuleCategoryGroup) private ruleCategoryGroupRepo: Repository<RuleCategoryGroup>,
|
||||
@@ -258,6 +290,85 @@ export class MasterDataService implements OnModuleInit {
|
||||
case 'TBL_AMOUNT_TYPES':
|
||||
return this.amountTypeRepo;
|
||||
|
||||
case 'BAGGAGE_TYPE':
|
||||
case 'BAGGAGE_TYPES':
|
||||
case 'TBL_BAGGAGE_TYPES':
|
||||
return this.baggageTypeRepo;
|
||||
|
||||
case 'COMPENSATION_RULE':
|
||||
case 'COMPENSATION_RULES':
|
||||
case 'TBL_COMPENSATION_RULES':
|
||||
return this.compensationRuleRepo;
|
||||
|
||||
case 'APPROVAL_LEVEL':
|
||||
case 'APPROVAL_LEVELS':
|
||||
case 'TBL_APPROVAL_LEVELS':
|
||||
return this.approvalLevelRepo;
|
||||
|
||||
case 'ELIGIBLE_FARE_TYPE':
|
||||
case 'ELIGIBLE_FARE_TYPES':
|
||||
case 'TBL_ELIGIBLE_FARE_TYPES':
|
||||
return this.eligibleFareTypeRepo;
|
||||
|
||||
case 'APPLICABLE_PRODUCT':
|
||||
case 'APPLICABLE_PRODUCTS':
|
||||
case 'TBL_APPLICABLE_PRODUCTS':
|
||||
return this.applicableProductRepo;
|
||||
|
||||
case 'MEAL_TYPE':
|
||||
case 'MEAL_TYPES':
|
||||
case 'TBL_MEAL_TYPES':
|
||||
return this.mealTypeRepo;
|
||||
|
||||
case 'AIRPORT_RESTRICTION':
|
||||
case 'AIRPORT_RESTRICTIONS':
|
||||
case 'TBL_AIRPORT_RESTRICTIONS':
|
||||
return this.airportRestrictionRepo;
|
||||
|
||||
case 'DESTINATION_TYPE':
|
||||
case 'DESTINATION_TYPES':
|
||||
case 'TBL_DESTINATION_TYPES':
|
||||
return this.destinationTypeRepo;
|
||||
|
||||
case 'VEHICLE_CATEGORY':
|
||||
case 'VEHICLE_CATEGORIES':
|
||||
case 'TBL_VEHICLE_CATEGORIES':
|
||||
return this.vehicleCategoryRepo;
|
||||
|
||||
case 'CABIN_PREFERENCE':
|
||||
case 'CABIN_PREFERENCES':
|
||||
case 'TBL_CABIN_PREFERENCES':
|
||||
return this.cabinPreferenceRepo;
|
||||
|
||||
case 'AIRLINE_PREFERENCE':
|
||||
case 'AIRLINE_PREFERENCES':
|
||||
case 'TBL_AIRLINE_PREFERENCES':
|
||||
return this.airlinePreferenceRepo;
|
||||
|
||||
case 'MEAL_CATEGORY':
|
||||
case 'MEAL_CATEGORIES':
|
||||
case 'TBL_MEAL_CATEGORIES':
|
||||
return this.mealCategoryRepo;
|
||||
|
||||
case 'SEAT_CATEGORY':
|
||||
case 'SEAT_CATEGORIES':
|
||||
case 'TBL_SEAT_CATEGORIES':
|
||||
return this.seatCategoryRepo;
|
||||
|
||||
case 'COMPENSATION_BASIS':
|
||||
case 'TBL_COMPENSATION_BASES':
|
||||
return this.compensationBasisRepo;
|
||||
|
||||
case 'TAX_TYPE':
|
||||
case 'TAX_TYPES':
|
||||
case 'TBL_TAX_TYPES':
|
||||
return this.taxTypeRepo;
|
||||
|
||||
case 'ROOM_TYPE':
|
||||
case 'ROOM_TYPES':
|
||||
case 'TBL_ROOM_TYPES':
|
||||
return this.roomTypeRepo;
|
||||
|
||||
default:
|
||||
throw new BadRequestException(`Invalid category: ${category}`);
|
||||
}
|
||||
@@ -344,6 +455,22 @@ export class MasterDataService implements OnModuleInit {
|
||||
{ code: 'currency', name: 'Currencies', tableName: 'tbl_currencies', description: 'Supported monetary currencies' },
|
||||
{ code: 'refund-method', name: 'Refund Methods', tableName: 'tbl_refund_methods', description: 'Payment payout channels for refunds' },
|
||||
{ code: 'amount-type', name: 'Amount Types', tableName: 'tbl_amount_types', description: 'Amount calculation types (percentage, fixed, formula)' },
|
||||
{ code: 'baggage-type', name: 'Baggage Types', tableName: 'tbl_baggage_types', description: 'Baggage type classifications' },
|
||||
{ code: 'compensation-rule', name: 'Compensation Rules', tableName: 'tbl_compensation_rules', description: 'Regulatory compensation rules' },
|
||||
{ code: 'approval-level', name: 'Approval Levels', tableName: 'tbl_approval_levels', description: 'Action approval hierarchy levels' },
|
||||
{ code: 'eligible-fare-type', name: 'Eligible Fare Types', tableName: 'tbl_eligible_fare_types', description: 'Fare types eligible for compensation' },
|
||||
{ code: 'applicable-product', name: 'Applicable Products', tableName: 'tbl_applicable_products', description: 'Products applicable for refunds/compensation' },
|
||||
{ code: 'meal-type', name: 'Meal Types', tableName: 'tbl_meal_types', description: 'Standard meal types' },
|
||||
{ code: 'airport-restriction', name: 'Airport Restrictions', tableName: 'tbl_airport_restrictions', description: 'Restrictions for specific airports' },
|
||||
{ code: 'destination-type', name: 'Destination Types', tableName: 'tbl_destination_types', description: 'Types of travel destinations' },
|
||||
{ code: 'vehicle-category', name: 'Vehicle Categories', tableName: 'tbl_vehicle_categories', description: 'Ground transportation vehicle categories' },
|
||||
{ code: 'cabin-preference', name: 'Cabin Preferences', tableName: 'tbl_cabin_preferences', description: 'Passenger cabin preferences' },
|
||||
{ code: 'airline-preference', name: 'Airline Preferences', tableName: 'tbl_airline_preferences', description: 'Passenger airline rebooking preferences' },
|
||||
{ code: 'meal-category', name: 'Meal Categories', tableName: 'tbl_meal_categories', description: 'Dietary meal categories' },
|
||||
{ code: 'seat-category', name: 'Seat Categories', tableName: 'tbl_seat_categories', description: 'Aircraft seat categories' },
|
||||
{ code: 'compensation-basis', name: 'Compensation Bases', tableName: 'tbl_compensation_bases', description: 'Basis for calculating compensation' },
|
||||
{ code: 'tax-type', name: 'Tax Types', tableName: 'tbl_tax_types', description: 'Aviation and booking tax types' },
|
||||
{ code: 'room-type', name: 'Room Types', tableName: 'tbl_room_types', description: 'Hotel accommodation room types' },
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { RuleCondition } from './entities/rule-condition.entity';
|
||||
import { PolicyAction } from './entities/policy-action.entity';
|
||||
import { PolicyActionValue } from './entities/policy-action-value.entity';
|
||||
import { MasterDataModule } from '../master-data/master-data.module';
|
||||
import { AuditLogModule } from '../audit-log/audit-log.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -21,6 +22,7 @@ import { MasterDataModule } from '../master-data/master-data.module';
|
||||
PolicyActionValue,
|
||||
]),
|
||||
MasterDataModule,
|
||||
AuditLogModule,
|
||||
],
|
||||
controllers: [PolicyEngineController],
|
||||
providers: [PolicyEngineService],
|
||||
@@ -28,3 +30,4 @@ import { MasterDataModule } from '../master-data/master-data.module';
|
||||
})
|
||||
export class PolicyEngineModule { }
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { CreatePolicyDto } from './dto/create-policy.dto';
|
||||
import { UpdatePolicyDto } from './dto/update-policy.dto';
|
||||
import { PolicyStatus, AudienceType } from './entities/policy.enums';
|
||||
import { Jurisdiction } from '../master-data/entities/jurisdiction.entity';
|
||||
import { AuditLogService } from '../audit-log/audit-log.service';
|
||||
|
||||
@Injectable()
|
||||
export class PolicyEngineService {
|
||||
@@ -20,6 +21,7 @@ export class PolicyEngineService {
|
||||
private readonly policyRepository: Repository<Policy>,
|
||||
@InjectRepository(Jurisdiction)
|
||||
private readonly jurisdictionRepository: Repository<Jurisdiction>,
|
||||
private readonly auditLogService: AuditLogService,
|
||||
) {}
|
||||
|
||||
private getRelations() {
|
||||
@@ -41,15 +43,13 @@ export class PolicyEngineService {
|
||||
|
||||
async create(createPolicyDto: CreatePolicyDto): Promise<Policy> {
|
||||
const tenantId = getTenantId();
|
||||
|
||||
// Create the policy entity and cascade nested associations using TypeORM
|
||||
const policy = this.policyRepository.create({
|
||||
...createPolicyDto,
|
||||
tenantId,
|
||||
});
|
||||
|
||||
const saved = await this.policyRepository.save(policy);
|
||||
return this.findOne(saved.id);
|
||||
// CREATE is logged automatically by AuditInterceptor (POST handler)
|
||||
}
|
||||
|
||||
async findAll(
|
||||
@@ -62,12 +62,8 @@ export class PolicyEngineService {
|
||||
const tenantId = getTenantId();
|
||||
|
||||
const whereClause: any = { tenantId };
|
||||
if (status) {
|
||||
whereClause.status = status;
|
||||
}
|
||||
if (audienceType) {
|
||||
whereClause.audienceType = audienceType;
|
||||
}
|
||||
if (status) whereClause.status = status;
|
||||
if (audienceType) whereClause.audienceType = audienceType;
|
||||
|
||||
const [data, total] = await this.policyRepository.findAndCount({
|
||||
where: whereClause,
|
||||
@@ -101,41 +97,32 @@ export class PolicyEngineService {
|
||||
}
|
||||
|
||||
async update(id: string, updatePolicyDto: UpdatePolicyDto): Promise<Policy> {
|
||||
// Ensure policy exists and belongs to the tenant
|
||||
await this.findOne(id);
|
||||
// Capture before snapshot
|
||||
const before = await this.findOne(id);
|
||||
const beforeSnapshot = { ...before };
|
||||
|
||||
const { targetAudiences, rules, ...policyData } = updatePolicyDto;
|
||||
|
||||
// Execute database operations in a transaction for clean replacement of nested objects
|
||||
return this.policyRepository.manager.transaction(async (transactionalEntityManager) => {
|
||||
// 1. Delete all existing target audiences associated with this policy
|
||||
const after = await this.policyRepository.manager.transaction(async (transactionalEntityManager) => {
|
||||
await transactionalEntityManager.delete(PolicyTargetAudience, { policyId: id });
|
||||
|
||||
// 2. Find and delete existing rules (cascades to conditions and actions/values)
|
||||
const existingRules = await transactionalEntityManager.find(PolicyRule, { where: { policyId: id } });
|
||||
if (existingRules.length > 0) {
|
||||
await transactionalEntityManager.remove(PolicyRule, existingRules);
|
||||
}
|
||||
|
||||
// 3. Fetch original policy again in transaction to ensure we have a fresh copy
|
||||
const policy = await transactionalEntityManager.findOneOrFail(Policy, {
|
||||
where: { id, tenantId: getTenantId() },
|
||||
});
|
||||
|
||||
// 4. Update policy basic columns
|
||||
Object.assign(policy, policyData);
|
||||
|
||||
// 5. Build new Target Audience entities if provided
|
||||
if (targetAudiences) {
|
||||
policy.targetAudiences = targetAudiences.map((target) =>
|
||||
transactionalEntityManager.create(PolicyTargetAudience, {
|
||||
...target,
|
||||
policyId: id,
|
||||
}),
|
||||
transactionalEntityManager.create(PolicyTargetAudience, { ...target, policyId: id }),
|
||||
);
|
||||
}
|
||||
|
||||
// 6. Build new Policy Rule entities with Conditions and Actions if provided
|
||||
if (rules) {
|
||||
policy.rules = rules.map((ruleDto) => {
|
||||
const conditions = ruleDto.conditions?.map((cond) =>
|
||||
@@ -146,7 +133,6 @@ export class PolicyEngineService {
|
||||
const values = act.values?.map((val) =>
|
||||
transactionalEntityManager.create(PolicyActionValue, val),
|
||||
) || [];
|
||||
|
||||
return transactionalEntityManager.create(PolicyAction, {
|
||||
actionTypeId: act.actionTypeId,
|
||||
sequence: act.sequence,
|
||||
@@ -164,25 +150,61 @@ export class PolicyEngineService {
|
||||
});
|
||||
}
|
||||
|
||||
// 7. Save updated policy and cascades
|
||||
const savedPolicy = await transactionalEntityManager.save(Policy, policy);
|
||||
|
||||
// Reload updated policy with all relations and return
|
||||
return transactionalEntityManager.findOneOrFail(Policy, {
|
||||
where: { id: savedPolicy.id },
|
||||
relations: this.getRelations(),
|
||||
});
|
||||
});
|
||||
|
||||
// Log UPDATE with before/after snapshots
|
||||
await this.auditLogService.log({
|
||||
module: 'policy-engine',
|
||||
action: 'UPDATE',
|
||||
entityId: id,
|
||||
entityLabel: (before as any).name ?? id,
|
||||
before: beforeSnapshot,
|
||||
after: { ...after },
|
||||
performedBy: getTenantId(),
|
||||
});
|
||||
|
||||
return after;
|
||||
}
|
||||
|
||||
async updateStatus(id: string, status: PolicyStatus): Promise<Policy> {
|
||||
const policy = await this.findOne(id);
|
||||
const beforeStatus = policy.status;
|
||||
|
||||
policy.status = status;
|
||||
return this.policyRepository.save(policy);
|
||||
const after = await this.policyRepository.save(policy);
|
||||
|
||||
await this.auditLogService.log({
|
||||
module: 'policy-engine',
|
||||
action: 'STATUS_CHANGE',
|
||||
entityId: id,
|
||||
entityLabel: (policy as any).name ?? id,
|
||||
before: { status: beforeStatus },
|
||||
after: { status },
|
||||
performedBy: getTenantId(),
|
||||
});
|
||||
|
||||
return after;
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const policy = await this.findOne(id);
|
||||
const beforeSnapshot = { ...policy };
|
||||
|
||||
await this.policyRepository.remove(policy);
|
||||
|
||||
await this.auditLogService.log({
|
||||
module: 'policy-engine',
|
||||
action: 'DELETE',
|
||||
entityId: id,
|
||||
entityLabel: (beforeSnapshot as any).name ?? id,
|
||||
before: beforeSnapshot,
|
||||
after: undefined,
|
||||
performedBy: getTenantId(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { RecoveryIncidentService } from './recovery-incident.service';
|
||||
import { RecoveryIncidentController } from './recovery-incident.controller';
|
||||
import { RecoveryIncident } from './entities/recovery-incident.entity';
|
||||
import { AuditLogModule } from '../audit-log/audit-log.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([RecoveryIncident])],
|
||||
imports: [TypeOrmModule.forFeature([RecoveryIncident]), AuditLogModule],
|
||||
controllers: [RecoveryIncidentController],
|
||||
providers: [RecoveryIncidentService],
|
||||
exports: [RecoveryIncidentService],
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Injectable, NotFoundException, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { RecoveryIncident } from './entities/recovery-incident.entity';
|
||||
import { CreateRecoveryIncidentDto } from './dto/create-recovery-incident.dto';
|
||||
import { UpdateRecoveryIncidentDto } from './dto/update-recovery-incident.dto';
|
||||
import { getTenantId } from '../../common/tenant/tenant.context';
|
||||
import { AuditLogService } from '../audit-log/audit-log.service';
|
||||
|
||||
export interface MetricCardData {
|
||||
id?: string;
|
||||
@@ -21,6 +22,7 @@ export class RecoveryIncidentService {
|
||||
constructor(
|
||||
@InjectRepository(RecoveryIncident)
|
||||
private readonly recoveryIncidentRepo: Repository<RecoveryIncident>,
|
||||
private readonly auditLogService: AuditLogService,
|
||||
) {}
|
||||
|
||||
async create(createDto: CreateRecoveryIncidentDto): Promise<RecoveryIncident> {
|
||||
@@ -31,6 +33,7 @@ export class RecoveryIncidentService {
|
||||
tenantId,
|
||||
});
|
||||
return this.recoveryIncidentRepo.save(incident);
|
||||
// CREATE is logged automatically by AuditInterceptor (POST handler)
|
||||
}
|
||||
|
||||
async findAll(): Promise<RecoveryIncident[]> {
|
||||
@@ -221,24 +224,38 @@ export class RecoveryIncidentService {
|
||||
const incident = await this.recoveryIncidentRepo.findOne({
|
||||
where: { id, tenantId },
|
||||
});
|
||||
|
||||
|
||||
if (!incident) {
|
||||
throw new NotFoundException(`RecoveryIncident with ID ${id} not found`);
|
||||
}
|
||||
|
||||
|
||||
return incident;
|
||||
}
|
||||
|
||||
async update(id: string, updateDto: UpdateRecoveryIncidentDto): Promise<RecoveryIncident> {
|
||||
const incident = await this.findOne(id);
|
||||
|
||||
const before = await this.findOne(id);
|
||||
const beforeSnapshot = { ...before };
|
||||
|
||||
const updateData: any = { ...updateDto };
|
||||
if (updateDto.date) {
|
||||
updateData.date = new Date(updateDto.date);
|
||||
}
|
||||
|
||||
Object.assign(incident, updateData);
|
||||
return this.recoveryIncidentRepo.save(incident);
|
||||
Object.assign(before, updateData);
|
||||
const after = await this.recoveryIncidentRepo.save(before);
|
||||
|
||||
// Log UPDATE with before/after snapshots
|
||||
await this.auditLogService.log({
|
||||
module: 'recovery-incident',
|
||||
action: 'UPDATE',
|
||||
entityId: id,
|
||||
entityLabel: before.recoveryCode ?? before.flightNumber,
|
||||
before: beforeSnapshot,
|
||||
after: { ...after },
|
||||
performedBy: getTenantId(),
|
||||
});
|
||||
|
||||
return after;
|
||||
}
|
||||
|
||||
async updateStatus(id: string, status: string): Promise<RecoveryIncident> {
|
||||
@@ -249,6 +266,54 @@ export class RecoveryIncidentService {
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const incident = await this.findOne(id);
|
||||
const beforeSnapshot = { ...incident };
|
||||
|
||||
await this.recoveryIncidentRepo.remove(incident);
|
||||
|
||||
// Log DELETE — before = deleted record, after = null
|
||||
await this.auditLogService.log({
|
||||
module: 'recovery-incident',
|
||||
action: 'DELETE',
|
||||
entityId: id,
|
||||
entityLabel: beforeSnapshot.recoveryCode ?? beforeSnapshot.flightNumber,
|
||||
before: beforeSnapshot,
|
||||
after: undefined,
|
||||
performedBy: getTenantId(),
|
||||
});
|
||||
}
|
||||
|
||||
// ─── AviationStack proxy ──────────────────────────────────────────────────
|
||||
async flightLookup(flightIata: string): Promise<any> {
|
||||
const apiKey = process.env.AVIATION_STACK_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new HttpException('AviationStack API key not configured', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
const url = `http://api.aviationstack.com/v1/flights?access_key=${apiKey}&flight_iata=${encodeURIComponent(flightIata.trim())}&limit=1`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new HttpException(
|
||||
`AviationStack responded with ${response.status}`,
|
||||
HttpStatus.BAD_GATEWAY,
|
||||
);
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
|
||||
if (json.error) {
|
||||
throw new HttpException(json.error.info || 'AviationStack error', HttpStatus.BAD_GATEWAY);
|
||||
}
|
||||
|
||||
const flights: any[] = json.data ?? [];
|
||||
return flights.length > 0 ? flights[0] : null;
|
||||
} catch (err: any) {
|
||||
if (err instanceof HttpException) throw err;
|
||||
throw new HttpException(
|
||||
`Failed to reach AviationStack: ${err.message}`,
|
||||
HttpStatus.BAD_GATEWAY,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user