From 4b3c47ca034991dabd2fa28a246dc21e1a795b5c Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Mon, 22 Jun 2026 15:01:16 +0300 Subject: [PATCH] refactor: ( iam ) remove user relations --- .../migration.sql | 39 +++++++++++++++++++ apps/edr-passenger-api/prisma/schema.prisma | 7 +--- .../src/common/iam-adapter.ts | 6 --- .../src/common/roles.decorator.ts | 3 +- .../src/modules/agents/agents.service.ts | 10 +++-- .../currencies/currencies.controller.ts | 20 +++++----- .../notifications/notifications.controller.ts | 16 ++++---- .../modules/payments/payments.controller.ts | 7 ++-- .../src/modules/reports/reports.service.ts | 23 +++++++++-- 9 files changed, 92 insertions(+), 39 deletions(-) create mode 100644 apps/edr-passenger-api/prisma/migrations/20260622000002_agent_iam_user_id_drop_user_fks/migration.sql diff --git a/apps/edr-passenger-api/prisma/migrations/20260622000002_agent_iam_user_id_drop_user_fks/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260622000002_agent_iam_user_id_drop_user_fks/migration.sql new file mode 100644 index 000000000..dcd55c066 --- /dev/null +++ b/apps/edr-passenger-api/prisma/migrations/20260622000002_agent_iam_user_id_drop_user_fks/migration.sql @@ -0,0 +1,39 @@ +-- ──────────────────────────────────────────────────────────── +-- 1. Add iamUserId to Agent +-- ──────────────────────────────────────────────────────────── +ALTER TABLE passenger."Agent" ADD COLUMN IF NOT EXISTS "iamUserId" TEXT; + +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'Agent_iamUserId_key' + AND conrelid = 'passenger."Agent"'::regclass + ) THEN + ALTER TABLE passenger."Agent" ADD CONSTRAINT "Agent_iamUserId_key" UNIQUE ("iamUserId"); + END IF; +END $$; + +CREATE INDEX IF NOT EXISTS "Agent_iamUserId_idx" ON passenger."Agent"("iamUserId"); + +-- ──────────────────────────────────────────────────────────── +-- 2. Populate iamUserId for existing agent records +-- Match via User.email → iam.users.email +-- ──────────────────────────────────────────────────────────── +UPDATE passenger."Agent" a +SET "iamUserId" = iu.id +FROM passenger."User" u +JOIN iam.users iu ON iu.email = u.email +WHERE a."userId" = u.id + AND a."iamUserId" IS NULL; + +-- ──────────────────────────────────────────────────────────── +-- 3. Drop Agent.userId FK and column — iamUserId replaces it entirely +-- ──────────────────────────────────────────────────────────── +ALTER TABLE passenger."Agent" DROP CONSTRAINT IF EXISTS "Agent_userId_fkey"; +DROP INDEX IF EXISTS passenger."Agent_userId_key"; +ALTER TABLE passenger."Agent" DROP COLUMN IF EXISTS "userId"; + +-- ──────────────────────────────────────────────────────────── +-- 4. Drop Passenger.userId FK (column stays as plain nullable string) +-- ──────────────────────────────────────────────────────────── +ALTER TABLE passenger."Passenger" DROP CONSTRAINT IF EXISTS "Passenger_userId_fkey"; diff --git a/apps/edr-passenger-api/prisma/schema.prisma b/apps/edr-passenger-api/prisma/schema.prisma index bba73740b..1321b749a 100644 --- a/apps/edr-passenger-api/prisma/schema.prisma +++ b/apps/edr-passenger-api/prisma/schema.prisma @@ -260,8 +260,6 @@ model User { faydaVerifiedAt DateTime? faydaSub String? @unique - passenger Passenger? - agent Agent? sessions Session[] @@schema("passenger") @@ -288,7 +286,6 @@ model Passenger { preferredLanguage String? blockedUntil DateTime? createdAt DateTime @default(now()) - user User? @relation(fields: [userId], references: [id]) bookings Booking[] loyalty LoyaltyAccount? wallet WalletAccount? @@ -1057,16 +1054,16 @@ model SegmentFareRule { model Agent { id String @id @default(uuid()) - userId String @unique + iamUserId String? @unique agentCode String @unique stationId String? commissionRate Int @default(5) active Boolean @default(true) createdAt DateTime @default(now()) - user User @relation(fields: [userId], references: [id]) bookings AgentBooking[] shifts AgentShift[] commissions AgentCommission[] + @@index([iamUserId]) @@schema("passenger") } diff --git a/apps/edr-passenger-api/src/common/iam-adapter.ts b/apps/edr-passenger-api/src/common/iam-adapter.ts index 9976caf52..96168dba8 100644 --- a/apps/edr-passenger-api/src/common/iam-adapter.ts +++ b/apps/edr-passenger-api/src/common/iam-adapter.ts @@ -1,7 +1 @@ -// Thin adapter: re-exports IAM guard and a stub IamRoles decorator so that -// controllers written against the forthcoming IamGuard compile today. export { JwtGuard as IamGuard } from '@tria-plc/api-common/modules/auth/services/jwt.guard'; - -export function IamRoles(..._roles: string[]) { - return function (_target: any, _key?: any, _descriptor?: any) {}; -} diff --git a/apps/edr-passenger-api/src/common/roles.decorator.ts b/apps/edr-passenger-api/src/common/roles.decorator.ts index ec0c377c6..e038e1682 100644 --- a/apps/edr-passenger-api/src/common/roles.decorator.ts +++ b/apps/edr-passenger-api/src/common/roles.decorator.ts @@ -1,5 +1,4 @@ import { SetMetadata } from '@nestjs/common'; -import { UserRole } from '@prisma/client'; export const ROLES_KEY = 'roles'; -export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles); +export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); diff --git a/apps/edr-passenger-api/src/modules/agents/agents.service.ts b/apps/edr-passenger-api/src/modules/agents/agents.service.ts index 12982f570..1cee0b4e4 100644 --- a/apps/edr-passenger-api/src/modules/agents/agents.service.ts +++ b/apps/edr-passenger-api/src/modules/agents/agents.service.ts @@ -13,9 +13,13 @@ export class AgentsService { constructor(private prisma: PrismaService) {} async createAgentBooking(dto: CreateAgentBookingDto) { - const agent = await this.prisma.agent.findUnique({ where: { id: dto.agentId }, include: { user: { include: { passenger: true } } } }); + const agent = await this.prisma.agent.findUnique({ where: { id: dto.agentId } }); if (!agent || !agent.active) throw new NotFoundException('Agent not found or inactive'); - if (!agent.user.passenger) throw new BadRequestException('Agent must have passenger account'); + + const passenger = agent.iamUserId + ? await this.prisma.passenger.findUnique({ where: { iamUserId: agent.iamUserId } }) + : null; + if (!passenger) throw new BadRequestException('Agent must have a linked passenger account'); const schedule = await this.prisma.trainSchedule.findUnique({ where: { id: dto.scheduleId } }); if (!schedule) throw new NotFoundException('Schedule not found'); @@ -30,7 +34,7 @@ export class AgentsService { const booking = await this.prisma.booking.create({ data: { bookingRef: generateRef(), - passengerId: agent.user.passenger.id, + passengerId: passenger.id, scheduleId: dto.scheduleId, status: dto.paymentMethod === 'CASH' ? 'CONFIRMED' : 'PENDING_PAYMENT', totalMinor, diff --git a/apps/edr-passenger-api/src/modules/currencies/currencies.controller.ts b/apps/edr-passenger-api/src/modules/currencies/currencies.controller.ts index 3081c7a75..29209af50 100644 --- a/apps/edr-passenger-api/src/modules/currencies/currencies.controller.ts +++ b/apps/edr-passenger-api/src/modules/currencies/currencies.controller.ts @@ -2,7 +2,9 @@ import { Controller, Get, Post, Patch, Delete, Body, Param, HttpCode, UseGuards import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { CurrenciesService } from './currencies.service'; import { CreateCurrencyDto, UpdateCurrencyDto } from './currencies.dto'; -import { IamGuard, IamRoles } from '../../common/iam-adapter'; +import { IamGuard } from '../../common/iam-adapter'; +import { Roles } from '../../common/roles.decorator'; +import { RolesGuard } from '../../common/roles.guard'; @ApiTags('Currencies') @Controller('currencies') @@ -15,8 +17,8 @@ export class CurrenciesController { } @Post() - @UseGuards(IamGuard) - @IamRoles('ADMIN') + @UseGuards(IamGuard, RolesGuard) + @Roles('ADMIN') @ApiBearerAuth('IAM-auth') @HttpCode(201) createCurrency(@Body() dto: CreateCurrencyDto) { @@ -24,24 +26,24 @@ export class CurrenciesController { } @Patch(':id') - @UseGuards(IamGuard) - @IamRoles('ADMIN') + @UseGuards(IamGuard, RolesGuard) + @Roles('ADMIN') @ApiBearerAuth('IAM-auth') updateCurrency(@Param('id') id: string, @Body() dto: UpdateCurrencyDto) { return this.currenciesService.updateCurrency(id, dto); } @Delete(':id') - @UseGuards(IamGuard) - @IamRoles('ADMIN') + @UseGuards(IamGuard, RolesGuard) + @Roles('ADMIN') @ApiBearerAuth('IAM-auth') deleteCurrency(@Param('id') id: string) { return this.currenciesService.deleteCurrency(id); } @Post('sync-rates') - @UseGuards(IamGuard) - @IamRoles('ADMIN') + @UseGuards(IamGuard, RolesGuard) + @Roles('ADMIN') @ApiBearerAuth('IAM-auth') @HttpCode(200) syncRates() { diff --git a/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts b/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts index a7921bad8..95fc0c640 100644 --- a/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts +++ b/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts @@ -2,7 +2,9 @@ import { Controller, Get, Param, Patch, Post, Body, UseGuards } from '@nestjs/co import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody } from '@nestjs/swagger'; import { NotificationsService } from './notifications.service'; import { JwtGuard } from '../../common/jwt.guard'; -import { IamGuard, IamRoles } from '../../common/iam-adapter'; +import { IamGuard } from '../../common/iam-adapter'; +import { Roles } from '../../common/roles.decorator'; +import { RolesGuard } from '../../common/roles.guard'; import { TestNotificationDto } from './notifications.dto'; import { EmailClientService } from './email-client.service'; import { SmsClientService } from './sms-client.service'; @@ -39,8 +41,8 @@ export class NotificationsController { } @Post('send/email') - @UseGuards(IamGuard) - @IamRoles('ADMIN', 'STAFF') + @UseGuards(IamGuard, RolesGuard) + @Roles('ADMIN', 'STAFF') @ApiOperation({ summary: 'Send a direct email via the email microservice' }) @ApiBody({ type: SendEmail }) sendEmail(@Body() dto: SendEmail) { @@ -48,8 +50,8 @@ export class NotificationsController { } @Post('send/sms') - @UseGuards(IamGuard) - @IamRoles('ADMIN', 'STAFF') + @UseGuards(IamGuard, RolesGuard) + @Roles('ADMIN', 'STAFF') @ApiOperation({ summary: 'Send a direct SMS via the SMS microservice' }) @ApiBody({ type: SingleMessageDto }) sendSms(@Body() dto: SingleMessageDto) { @@ -57,8 +59,8 @@ export class NotificationsController { } @Post('send/sms/bulk') - @UseGuards(IamGuard) - @IamRoles('ADMIN', 'STAFF') + @UseGuards(IamGuard, RolesGuard) + @Roles('ADMIN', 'STAFF') @ApiOperation({ summary: 'Send bulk SMS messages via the SMS microservice' }) @ApiBody({ type: BulkMessagesDto }) sendBulkSms(@Body() dto: BulkMessagesDto) { diff --git a/apps/edr-passenger-api/src/modules/payments/payments.controller.ts b/apps/edr-passenger-api/src/modules/payments/payments.controller.ts index a64c4ae9c..a87a00118 100644 --- a/apps/edr-passenger-api/src/modules/payments/payments.controller.ts +++ b/apps/edr-passenger-api/src/modules/payments/payments.controller.ts @@ -31,7 +31,6 @@ import { import { JwtGuard } from "../../common/jwt.guard"; import { RolesGuard } from "../../common/roles.guard"; import { Roles } from "../../common/roles.decorator"; -import { UserRole } from "@prisma/client"; @ApiTags("Payment") @Controller("payments") @@ -40,7 +39,7 @@ export class PaymentsController { @Get("all") @UseGuards(JwtGuard, RolesGuard) - @Roles(UserRole.ADMIN, UserRole.SUPERVISOR, UserRole.STAFF) + @Roles('ADMIN', 'SUPERVISOR', 'STAFF') @ApiBearerAuth("JWT-auth") @ApiOperation({ summary: "Get all payments with filters (staff/admin only)" }) @ApiQuery({ name: "search", required: false }) @@ -103,7 +102,7 @@ export class PaymentsController { @Post("refund") @UseGuards(JwtGuard, RolesGuard) - @Roles(UserRole.ADMIN, UserRole.STAFF, UserRole.AGENT) + @Roles('ADMIN', 'STAFF', 'AGENT') @ApiBearerAuth("JWT-auth") @ApiOperation({ summary: "Refund a confirmed booking (staff/agent only)" }) refund(@Body() dto: RefundDto) { @@ -112,7 +111,7 @@ export class PaymentsController { @Post("methods") @UseGuards(JwtGuard, RolesGuard) - @Roles(UserRole.ADMIN, UserRole.STAFF) + @Roles('ADMIN', 'STAFF') @ApiBearerAuth("JWT-auth") @ApiOperation({ summary: "Add a payment system to the platform catalog (admin only)", diff --git a/apps/edr-passenger-api/src/modules/reports/reports.service.ts b/apps/edr-passenger-api/src/modules/reports/reports.service.ts index d1f25dde7..5c1b5e582 100644 --- a/apps/edr-passenger-api/src/modules/reports/reports.service.ts +++ b/apps/edr-passenger-api/src/modules/reports/reports.service.ts @@ -1,10 +1,15 @@ import { Injectable } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; import { PrismaService } from '../../common/prisma.service'; import { GenerateReportDto, ReportType } from './reports.dto'; @Injectable() export class ReportsService { - constructor(private prisma: PrismaService) {} + constructor( + private prisma: PrismaService, + @InjectDataSource() private dataSource: DataSource, + ) {} async generateReport(dto: GenerateReportDto) { const dateFrom = new Date(dto.dateFrom); @@ -113,13 +118,25 @@ export class ReportsService { ...(agentId ? { agentId } : {}) }, include: { - agent: { include: { user: true } }, + agent: { select: { id: true, iamUserId: true, agentCode: true } }, booking: true } }); + const iamUserIds = [...new Set( + agentBookings.map(ab => ab.agent.iamUserId).filter(Boolean) as string[] + )]; + const iamRows = iamUserIds.length > 0 + ? await this.dataSource.query<{ id: string; name: { en?: string; am?: string } | null }[]>( + `SELECT id, name FROM iam.users WHERE id = ANY($1)`, + [iamUserIds], + ) + : []; + const iamMap = new Map(iamRows.map(r => [r.id, r])); + const byAgent = agentBookings.reduce((acc, ab) => { - const agentName = ab.agent.user.fullName; + const iam = ab.agent.iamUserId ? iamMap.get(ab.agent.iamUserId) : undefined; + const agentName = iam?.name?.en ?? iam?.name?.am ?? ab.agent.agentCode; if (!acc[agentName]) { acc[agentName] = { bookings: 0, revenueMinor: 0, cashCollected: 0 }; }