From 389488bf283f4138b85bcfc6f539109f6acb09d9 Mon Sep 17 00:00:00 2001 From: diallolatoile Date: Tue, 4 Nov 2025 21:06:44 +0000 Subject: [PATCH] feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature --- src/auth/services/startup.service.ts | 2 +- .../merchant-partners.controller.ts | 298 --------------- src/hub-users/dto/merchant-partners.dto.ts | 96 ----- src/hub-users/services/hub-users.service.ts | 3 +- .../services/merchant-partners.service.ts | 353 ------------------ 5 files changed, 3 insertions(+), 749 deletions(-) delete mode 100644 src/hub-users/controllers/merchant-partners.controller.ts delete mode 100644 src/hub-users/dto/merchant-partners.dto.ts delete mode 100644 src/hub-users/services/merchant-partners.service.ts diff --git a/src/auth/services/startup.service.ts b/src/auth/services/startup.service.ts index ee6b910..28e2f7c 100644 --- a/src/auth/services/startup.service.ts +++ b/src/auth/services/startup.service.ts @@ -50,7 +50,7 @@ export class StartupService implements OnModuleInit { async onModuleInit() { if (process.env.RUN_STARTUP_TESTS === 'true') { - this.logger.log('🚀 Starting comprehensive tests (Hub + Merchants with isolation)...'); + this.logger.log('Starting comprehensive tests (Hub + Merchants with isolation)...'); await this.runAllTests(); } } diff --git a/src/hub-users/controllers/merchant-partners.controller.ts b/src/hub-users/controllers/merchant-partners.controller.ts deleted file mode 100644 index 86b64fc..0000000 --- a/src/hub-users/controllers/merchant-partners.controller.ts +++ /dev/null @@ -1,298 +0,0 @@ -import { - Controller, - Get, - Post, - Put, - Delete, - Body, - Param, - Query, - Request, - HttpStatus, - HttpCode, - Logger, - DefaultValuePipe, - ParseIntPipe, - BadRequestException, -} from '@nestjs/common'; - -import { AuthenticatedUser, Resource, Scopes } from "nest-keycloak-connect"; - -import { - MerchantPartnersService, - MerchantPartner, - CreateMerchantPartnerData, - MerchantStats, -} from '../services/merchant-partners.service'; -import { RESOURCES } from '../../constants/resources'; -import { SCOPES } from '../../constants/scopes'; - -// DTOs -import { - CreateMerchantPartnerDto, - UpdateMerchantPartnerDto, - SuspendMerchantPartnerDto, -} from '../dto/merchant-partners.dto'; - -export interface ApiResponse { - success: boolean; - message: string; - data?: T; - timestamp: string; -} - -export interface PaginatedResponse { - items: T[]; - total: number; - page: number; - limit: number; - totalPages: number; -} - -@Controller('partners') -@Resource(RESOURCES.MERCHANT_USER) -export class MerchantPartnersController { - private readonly logger = new Logger(MerchantPartnersController.name); - - constructor(private readonly merchantPartnersService: MerchantPartnersService) {} - - private createApiResponse( - success: boolean, - message: string, - data?: T, - ): ApiResponse { - return { - success, - message, - data, - timestamp: new Date().toISOString(), - }; - } - - // ===== CRÉATION DE MERCHANT PARTNERS ===== - @Post() - @Scopes(SCOPES.WRITE) - async createMerchantPartner( - @Body() createMerchantDto: CreateMerchantPartnerDto, - @Request() req: any, - ): Promise> { - this.logger.log(`Creating merchant partner: ${createMerchantDto.name}`); - - const creatorId = req.user.sub; - - const merchantData: CreateMerchantPartnerData = { - ...createMerchantDto, - dcbPartnerOwner: { - username: createMerchantDto.dcbPartnerOwnerUsername, - email: createMerchantDto.dcbPartnerOwnerEmail, - firstName: createMerchantDto.dcbPartnerOwnerFirstName, - lastName: createMerchantDto.dcbPartnerOwnerLastName, - password: createMerchantDto.dcbPartnerOwnerPassword, - }, - }; - - const merchant = await this.merchantPartnersService.createMerchantPartner(creatorId, merchantData); - - this.logger.log(`Merchant partner created successfully: ${merchant.name} (ID: ${merchant.id})`); - - return this.createApiResponse( - true, - 'Merchant partner created successfully', - merchant, - ); - } - - // ===== RÉCUPÉRATION DE MERCHANT PARTNERS ===== - @Get() - @Scopes(SCOPES.READ) - async getAllMerchantPartners( - @Request() req: any, - @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, - @Query('limit', new DefaultValuePipe(50), ParseIntPipe) limit: number, - @Query('status') status?: string, - ): Promise>> { - const requesterId = req.user.sub; - - const safeLimit = Math.min(limit, 100); - const safePage = Math.max(page, 1); - - let merchants = await this.merchantPartnersService.getAllMerchantPartners(requesterId); - - // Filtrage par statut - if (status && ['ACTIVE', 'SUSPENDED', 'PENDING'].includes(status)) { - merchants = merchants.filter(merchant => merchant.status === status); - } - - // Pagination - const startIndex = (safePage - 1) * safeLimit; - const endIndex = startIndex + safeLimit; - const paginatedMerchants = merchants.slice(startIndex, endIndex); - - const response: PaginatedResponse = { - items: paginatedMerchants, - total: merchants.length, - page: safePage, - limit: safeLimit, - totalPages: Math.ceil(merchants.length / safeLimit), - }; - - return this.createApiResponse( - true, - 'Merchant partners retrieved successfully', - response, - ); - } - - @Get('stats') - @Scopes(SCOPES.READ) - async getMerchantStats( - @Request() req: any, - ): Promise> { - const requesterId = req.user.sub; - const stats = await this.merchantPartnersService.getMerchantStats(requesterId); - - return this.createApiResponse( - true, - 'Merchant statistics retrieved successfully', - stats, - ); - } - - @Get(':id') - @Scopes(SCOPES.READ) - async getMerchantPartnerById( - @Param('id') merchantId: string, - @Request() req: any, - ): Promise> { - const requesterId = req.user.sub; - const merchant = await this.merchantPartnersService.getMerchantPartnerById(merchantId, requesterId); - - return this.createApiResponse( - true, - 'Merchant partner retrieved successfully', - merchant, - ); - } - - // ===== MISE À JOUR DE MERCHANT PARTNERS ===== - @Put(':id') - @Scopes(SCOPES.WRITE) - async updateMerchantPartner( - @Param('id') merchantId: string, - @Body() updateMerchantDto: UpdateMerchantPartnerDto, - @Request() req: any, - ): Promise> { - this.logger.log(`Updating merchant partner: ${merchantId}`); - - const requesterId = req.user.sub; - const merchant = await this.merchantPartnersService.updateMerchantPartner( - merchantId, - updateMerchantDto, - requesterId, - ); - - this.logger.log(`Merchant partner updated successfully: ${merchant.name}`); - - return this.createApiResponse( - true, - 'Merchant partner updated successfully', - merchant, - ); - } - - @Put(':id/suspend') - @Scopes(SCOPES.WRITE) - @HttpCode(HttpStatus.OK) - async suspendMerchantPartner( - @Param('id') merchantId: string, - @Body() suspendDto: SuspendMerchantPartnerDto, - @Request() req: any, - ): Promise> { - this.logger.log(`Suspending merchant partner: ${merchantId}`); - - const requesterId = req.user.sub; - await this.merchantPartnersService.suspendMerchantPartner( - merchantId, - suspendDto.reason, - requesterId, - ); - - this.logger.log(`Merchant partner suspended successfully: ${merchantId}`); - - return this.createApiResponse( - true, - 'Merchant partner suspended successfully', - ); - } - - @Put(':id/activate') - @Scopes(SCOPES.WRITE) - @HttpCode(HttpStatus.OK) - async activateMerchantPartner( - @Param('id') merchantId: string, - @Request() req: any, - ): Promise> { - this.logger.log(`Activating merchant partner: ${merchantId}`); - - const requesterId = req.user.sub; - const merchant = await this.merchantPartnersService.updateMerchantPartner( - merchantId, - { status: 'ACTIVE' }, - requesterId, - ); - - this.logger.log(`Merchant partner activated successfully: ${merchant.name}`); - - return this.createApiResponse( - true, - 'Merchant partner activated successfully', - merchant, - ); - } - - // ===== SUPPRESSION DE MERCHANT PARTNERS ===== - @Delete(':id') - @Scopes(SCOPES.DELETE) - @HttpCode(HttpStatus.NO_CONTENT) - async deleteMerchantPartner( - @Param('id') merchantId: string, - @Request() req: any, - ): Promise { - this.logger.log(`Deleting merchant partner: ${merchantId}`); - - const requesterId = req.user.sub; - // Implémentez la logique de suppression si nécessaire - // await this.merchantPartnersService.deleteMerchantPartner(merchantId, requesterId); - - this.logger.log(`Merchant partner deletion scheduled: ${merchantId}`); - } - - // ===== RECHERCHE ET FILTRES ===== - @Get('search/by-name') - @Scopes(SCOPES.READ) - async searchMerchantsByName( - @Query('name') name: string, - @Request() req: any, - ): Promise> { - if (!name || name.length < 2) { - throw new BadRequestException('Name must be at least 2 characters long'); - } - - this.logger.log(`Searching merchants by name: ${name}`); - - const requesterId = req.user.sub; - const allMerchants = await this.merchantPartnersService.getAllMerchantPartners(requesterId); - - const filteredMerchants = allMerchants.filter(merchant => - merchant.name.toLowerCase().includes(name.toLowerCase()), - ); - - this.logger.log(`Found ${filteredMerchants.length} merchants matching name: ${name}`); - - return this.createApiResponse( - true, - 'Merchants search completed successfully', - filteredMerchants, - ); - } -} \ No newline at end of file diff --git a/src/hub-users/dto/merchant-partners.dto.ts b/src/hub-users/dto/merchant-partners.dto.ts deleted file mode 100644 index e0ad6ff..0000000 --- a/src/hub-users/dto/merchant-partners.dto.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { - IsEmail, - IsEnum, - IsNotEmpty, - IsOptional, - IsString, - MinLength, - Matches, -} from 'class-validator'; - -export class CreateMerchantPartnerDto { - @IsNotEmpty() - @IsString() - @MinLength(2) - name: string; - - @IsNotEmpty() - @IsString() - @MinLength(2) - legalName: string; - - @IsNotEmpty() - @IsEmail() - email: string; - - @IsOptional() - @IsString() - phone?: string; - - @IsOptional() - @IsString() - address?: string; - - // Propriétaire DCB_PARTNER - @IsNotEmpty() - @IsString() - @MinLength(3) - dcbPartnerOwnerUsername: string; - - @IsNotEmpty() - @IsEmail() - dcbPartnerOwnerEmail: string; - - @IsNotEmpty() - @IsString() - @MinLength(2) - dcbPartnerOwnerFirstName: string; - - @IsNotEmpty() - @IsString() - @MinLength(2) - dcbPartnerOwnerLastName: string; - - @IsNotEmpty() - @IsString() - @MinLength(8) - @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/, { - message: 'Password must contain at least one uppercase letter, one lowercase letter, one number and one special character', - }) - dcbPartnerOwnerPassword: string; -} - -export class UpdateMerchantPartnerDto { - @IsOptional() - @IsString() - @MinLength(2) - name?: string; - - @IsOptional() - @IsString() - @MinLength(2) - legalName?: string; - - @IsOptional() - @IsEmail() - email?: string; - - @IsOptional() - @IsString() - phone?: string; - - @IsOptional() - @IsString() - address?: string; - - @IsOptional() - @IsEnum(['ACTIVE', 'SUSPENDED', 'PENDING']) - status?: 'ACTIVE' | 'SUSPENDED' | 'PENDING'; -} - -export class SuspendMerchantPartnerDto { - @IsNotEmpty() - @IsString() - @MinLength(5) - reason: string; -} \ No newline at end of file diff --git a/src/hub-users/services/hub-users.service.ts b/src/hub-users/services/hub-users.service.ts index d996a01..8ed1edb 100644 --- a/src/hub-users/services/hub-users.service.ts +++ b/src/hub-users/services/hub-users.service.ts @@ -151,7 +151,8 @@ export class HubUsersService { try { const userRoles = await this.keycloakApi.getUserClientRoles(user.id); - const isMerchant = userRoles.some(role => role.name === UserRole.DCB_PARTNER); + const isMerchant = userRoles.some( + role => role.name === UserRole.DCB_PARTNER || UserRole.DCB_PARTNER_ADMIN || UserRole.DCB_PARTNER_MANAGER || UserRole.DCB_PARTNER_SUPPORT); if (isMerchant) { merchants.push(this.mapToHubUser(user, userRoles)); diff --git a/src/hub-users/services/merchant-partners.service.ts b/src/hub-users/services/merchant-partners.service.ts deleted file mode 100644 index 685ad97..0000000 --- a/src/hub-users/services/merchant-partners.service.ts +++ /dev/null @@ -1,353 +0,0 @@ -import { Injectable, Logger, BadRequestException, ForbiddenException, NotFoundException } from '@nestjs/common'; -import { KeycloakApiService } from '../../auth/services/keycloak-api.service'; -import { CreateUserData, UserRole, KeycloakUser } from '../../auth/services/keycloak-user.model'; - -export interface MerchantPartner { - id: string; - name: string; - legalName: string; - email: string; - phone?: string; - address?: string; - status: 'ACTIVE' | 'SUSPENDED' | 'PENDING'; - createdAt: Date; - updatedAt: Date; - dcbPartnerUserId: string; // ID du user DCB_PARTNER propriétaire -} - -export interface CreateMerchantPartnerData { - name: string; - legalName: string; - email: string; - phone?: string; - address?: string; - dcbPartnerOwner: { - username: string; - email: string; - firstName: string; - lastName: string; - password: string; - }; -} - -export interface MerchantStats { - totalMerchants: number; - activeMerchants: number; - suspendedMerchants: number; - pendingMerchants: number; - totalUsers: number; -} - -@Injectable() -export class MerchantPartnersService { - private readonly logger = new Logger(MerchantPartnersService.name); - - constructor(private readonly keycloakApi: KeycloakApiService) {} - - // ===== CRÉATION D'UN MERCHANT PARTNER ===== - async createMerchantPartner( - creatorId: string, - merchantData: CreateMerchantPartnerData - ): Promise { - this.logger.log(`Creating merchant partner: ${merchantData.name}`); - - // Validation des permissions du créateur - await this.validateMerchantCreationPermissions(creatorId); - - // Vérifier les doublons - await this.checkDuplicateMerchant(merchantData); - - // 1. Créer le user DCB_PARTNER (propriétaire) - const dcbPartnerUserData: CreateUserData = { - username: merchantData.dcbPartnerOwner.username, - email: merchantData.dcbPartnerOwner.email, - firstName: merchantData.dcbPartnerOwner.firstName, - lastName: merchantData.dcbPartnerOwner.lastName, - password: merchantData.dcbPartnerOwner.password, - enabled: true, - emailVerified: true, - clientRoles: [UserRole.DCB_PARTNER], - createdBy: creatorId, - }; - - const dcbPartnerUserId = await this.keycloakApi.createUser(creatorId, dcbPartnerUserData); - - // 2. Créer l'entité Merchant Partner (dans la base de données ou Keycloak attributes) - const merchantPartner: MerchantPartner = { - id: merchantData.dcbPartnerOwner.username, - name: merchantData.name, - legalName: merchantData.legalName, - email: merchantData.email, - phone: merchantData.phone, - address: merchantData.address, - status: 'ACTIVE', - createdAt: new Date(), - updatedAt: new Date(), - dcbPartnerUserId, - }; - - // 3. Stocker les infos du merchant dans les attributs du user DCB_PARTNER - await this.keycloakApi.setUserAttributes(dcbPartnerUserId, { - merchantPartnerName: [merchantData.name], - merchantLegalName: [merchantData.legalName], - merchantEmail: [merchantData.email], - merchantPhone: [merchantData.phone || ''], - merchantAddress: [merchantData.address || ''], - merchantStatus: ['ACTIVE'], - merchantCreatedAt: [new Date().toISOString()], - }); - - this.logger.log(`Merchant partner created successfully: ${merchantData.name}`); - return merchantPartner; - } - - // ===== GESTION DES MERCHANTS ===== - async getAllMerchantPartners(requesterId: string): Promise { - await this.validateHubAccess(requesterId); - - // Récupérer tous les users DCB_PARTNER - const allUsers = await this.keycloakApi.getAllUsers(); - const merchants: MerchantPartner[] = []; - - for (const user of allUsers) { - if (!user.id) continue; - - try { - const userRoles = await this.keycloakApi.getUserClientRoles(user.id); - const isDcbPartner = userRoles.some(role => role.name === UserRole.DCB_PARTNER); - - if (isDcbPartner && user.attributes?.merchantPartnerName?.[0]) { - merchants.push(this.mapToMerchantPartner(user)); - } - } catch (error) { - this.logger.warn(`Could not process merchant user ${user.id}: ${error.message}`); - } - } - - return merchants; - } - - async getMerchantPartnerById(merchantId: string, requesterId: string): Promise { - await this.validateMerchantAccess(requesterId, merchantId); - - // Trouver le user DCB_PARTNER correspondant au merchant - const dcbPartnerUser = await this.findDcbPartnerByMerchantId(merchantId); - return this.mapToMerchantPartner(dcbPartnerUser); - } - - async updateMerchantPartner( - merchantId: string, - updates: Partial<{ - name: string; - legalName: string; - email: string; - phone: string; - address: string; - status: 'ACTIVE' | 'SUSPENDED' | 'PENDING'; - }>, - requesterId: string - ): Promise { - await this.validateMerchantManagementPermissions(requesterId, merchantId); - - const dcbPartnerUser = await this.findDcbPartnerByMerchantId(merchantId); - - // Mettre à jour les attributs - const attributes: any = {}; - if (updates.name) attributes.merchantPartnerName = [updates.name]; - if (updates.legalName) attributes.merchantLegalName = [updates.legalName]; - if (updates.email) attributes.merchantEmail = [updates.email]; - if (updates.phone) attributes.merchantPhone = [updates.phone]; - if (updates.address) attributes.merchantAddress = [updates.address]; - if (updates.status) attributes.merchantStatus = [updates.status]; - - attributes.merchantUpdatedAt = [new Date().toISOString()]; - - await this.keycloakApi.setUserAttributes(dcbPartnerUser.id!, attributes); - - return this.getMerchantPartnerById(merchantId, requesterId); - } - - async suspendMerchantPartner(merchantId: string, reason: string, requesterId: string): Promise { - await this.validateHubAccess(requesterId); - - const dcbPartnerUser = await this.findDcbPartnerByMerchantId(merchantId); - - // Suspendre le merchant et tous ses utilisateurs - await this.keycloakApi.setUserAttributes(dcbPartnerUser.id!, { - merchantStatus: ['SUSPENDED'], - suspensionReason: [reason], - suspendedAt: [new Date().toISOString()], - }); - - // Suspendre tous les utilisateurs du merchant - await this.suspendAllMerchantUsers(merchantId, requesterId); - - this.logger.log(`Merchant partner suspended: ${merchantId}, reason: ${reason}`); - } - - // ===== STATISTIQUES ===== - async getMerchantStats(requesterId: string): Promise { - await this.validateHubAccess(requesterId); - - const allMerchants = await this.getAllMerchantPartners(requesterId); - const allUsers = await this.keycloakApi.getAllUsers(); - - let totalUsers = 0; - for (const merchant of allMerchants) { - const merchantUsers = allUsers.filter(user => - user.attributes?.merchantPartnerId?.[0] === merchant.id - ); - totalUsers += merchantUsers.length; - } - - return { - totalMerchants: allMerchants.length, - activeMerchants: allMerchants.filter(m => m.status === 'ACTIVE').length, - suspendedMerchants: allMerchants.filter(m => m.status === 'SUSPENDED').length, - pendingMerchants: allMerchants.filter(m => m.status === 'PENDING').length, - totalUsers, - }; - } - - // ===== MÉTHODES PRIVÉES ===== - private async validateMerchantCreationPermissions(creatorId: string): Promise { - const creatorRoles = await this.keycloakApi.getUserClientRoles(creatorId); - const canCreateMerchant = creatorRoles.some(role => - [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT].includes(role.name as UserRole) - ); - - if (!canCreateMerchant) { - throw new ForbiddenException('Only hub administrators can create merchant partners'); - } - } - - private async validateHubAccess(requesterId: string): Promise { - const requesterRoles = await this.keycloakApi.getUserClientRoles(requesterId); - const isHubAdmin = requesterRoles.some(role => - [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT].includes(role.name as UserRole) - ); - - if (!isHubAdmin) { - throw new ForbiddenException('Only hub administrators can access this resource'); - } - } - - private async validateMerchantAccess(requesterId: string, merchantId?: string): Promise { - const requesterRoles = await this.keycloakApi.getUserClientRoles(requesterId); - - this.logger.debug(`Validating merchant access: requester=${requesterId}, merchant=${merchantId}`); - this.logger.debug(`Requester roles: ${requesterRoles.map(r => r.name).join(', ')}`); - - // Les admins Hub ont accès complet - const isHubAdmin = requesterRoles.some(role => - [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT].includes(role.name as UserRole) - ); - - if (isHubAdmin) { - this.logger.debug('Hub admin access granted'); - return; - } - - // CORRECTION: Service account est considéré comme admin hub - if (requesterId.includes('service-account')) { - this.logger.debug('Service account access granted'); - return; - } - - // Les DCB_PARTNER n'ont accès qu'à leur propre merchant - if (requesterRoles.some(role => role.name === UserRole.DCB_PARTNER)) { - if (merchantId && requesterId === merchantId) { - this.logger.debug('DCB_PARTNER access to own merchant granted'); - return; // DCB_PARTNER accède à son propre merchant - } - throw new ForbiddenException('DCB_PARTNER can only access their own merchant partner'); - } - - // Les autres rôles merchants n'ont accès qu'à leur merchant - const requesterMerchantId = await this.keycloakApi.getUserMerchantPartnerId(requesterId); - if (requesterMerchantId && merchantId && requesterMerchantId === merchantId) { - this.logger.debug('Merchant user access to own merchant granted'); - return; - } - - throw new ForbiddenException('Insufficient permissions to access this merchant'); - } - - private async validateMerchantManagementPermissions(requesterId: string, merchantId: string): Promise { - const requesterRoles = await this.keycloakApi.getUserClientRoles(requesterId); - - // Seuls les admins Hub peuvent modifier les merchants - const isHubAdmin = requesterRoles.some(role => - [UserRole.DCB_ADMIN, UserRole.DCB_SUPPORT].includes(role.name as UserRole) - ); - - if (!isHubAdmin) { - throw new ForbiddenException('Only hub administrators can manage merchant partners'); - } - } - - private async checkDuplicateMerchant(merchantData: CreateMerchantPartnerData): Promise { - const existingUsers = await this.keycloakApi.findUserByUsername(merchantData.dcbPartnerOwner.username); - if (existingUsers.length > 0) { - throw new BadRequestException(`Merchant partner with username ${merchantData.dcbPartnerOwner.username} already exists`); - } - - const existingEmails = await this.keycloakApi.findUserByEmail(merchantData.dcbPartnerOwner.email); - if (existingEmails.length > 0) { - throw new BadRequestException(`Merchant partner with email ${merchantData.dcbPartnerOwner.email} already exists`); - } - } - - private async findDcbPartnerByMerchantId(merchantId: string): Promise { - const users = await this.keycloakApi.findUserByUsername(merchantId); - if (users.length === 0) { - throw new NotFoundException(`Merchant partner not found: ${merchantId}`); - } - - const user = users[0]; - const userRoles = await this.keycloakApi.getUserClientRoles(user.id!); - const isDcbPartner = userRoles.some(role => role.name === UserRole.DCB_PARTNER); - - if (!isDcbPartner) { - throw new NotFoundException(`User ${merchantId} is not a DCB_PARTNER`); - } - - return user; - } - - private mapToMerchantPartner(user: KeycloakUser): MerchantPartner { - return { - id: user.username, - name: user.attributes?.merchantPartnerName?.[0] || user.username, - legalName: user.attributes?.merchantLegalName?.[0] || '', - email: user.attributes?.merchantEmail?.[0] || user.email, - phone: user.attributes?.merchantPhone?.[0], - address: user.attributes?.merchantAddress?.[0], - status: (user.attributes?.merchantStatus?.[0] as 'ACTIVE' | 'SUSPENDED' | 'PENDING') || 'ACTIVE', - createdAt: user.attributes?.merchantCreatedAt?.[0] - ? new Date(user.attributes.merchantCreatedAt[0]) - : new Date(user.createdTimestamp || Date.now()), - updatedAt: user.attributes?.merchantUpdatedAt?.[0] - ? new Date(user.attributes.merchantUpdatedAt[0]) - : new Date(user.createdTimestamp || Date.now()), - dcbPartnerUserId: user.id!, - }; - } - - private async suspendAllMerchantUsers(merchantId: string, requesterId: string): Promise { - const allUsers = await this.keycloakApi.getAllUsers(); - const merchantUsers = allUsers.filter(user => - user.attributes?.merchantPartnerId?.[0] === merchantId - ); - - for (const user of merchantUsers) { - if (user.id) { - try { - await this.keycloakApi.updateUser(user.id, { enabled: false }, requesterId); - } catch (error) { - this.logger.warn(`Could not suspend user ${user.id}: ${error.message}`); - } - } - } - } -} \ No newline at end of file