refactor: ( iam ) remove user relations

This commit is contained in:
Abubeker Yasin
2026-06-22 15:01:16 +03:00
parent a63b49d419
commit 4b3c47ca03
9 changed files with 92 additions and 39 deletions

View File

@@ -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";

View File

@@ -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")
}

View File

@@ -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) {};
}

View File

@@ -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);

View File

@@ -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,

View File

@@ -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() {

View File

@@ -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) {

View File

@@ -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)",

View File

@@ -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 };
}