diff --git a/src/app/core/models/dcb-bo-hub-user.model.ts b/src/app/core/models/dcb-bo-hub-user.model.ts index 7046be4..86195ba 100644 --- a/src/app/core/models/dcb-bo-hub-user.model.ts +++ b/src/app/core/models/dcb-bo-hub-user.model.ts @@ -13,7 +13,15 @@ export enum UserRole { // Rôles Merchant Partner (avec merchantPartnerId obligatoire = ID du DCB_PARTNER) DCB_PARTNER_ADMIN = 'dcb-partner-admin', DCB_PARTNER_MANAGER = 'dcb-partner-manager', - DCB_PARTNER_SUPPORT = 'dcb-partner-support' + DCB_PARTNER_SUPPORT = 'dcb-partner-support', + + MERCHANT_CONFIG_ADMIN = 'ADMIN', + MERCHANT_CONFIG_MANAGER = 'MANAGER', + MERCHANT_CONFIG_TECHNICAL = 'TECHNICAL', + MERCHANT_CONFIG_VIEWER = 'VIEWER', + + //ADMIN, MANAGER, TECHNICAL, VIEWER + } // Enum pour le contexte Angular (identique à l'ancien) @@ -39,7 +47,7 @@ export interface UsersStatistics { totalUsers: number; } -// === MODÈLE USER PRINCIPAL === +// dcb-bo-hub-user.model.ts - MIS À JOUR export interface User { id: string; // UUID Keycloak username: string; @@ -49,10 +57,13 @@ export interface User { enabled: boolean; emailVerified: boolean; userType: UserType; // HUB ou MERCHANT_PARTNER - merchantPartnerId?: string; // Pour les users merchant: ID du DCB_PARTNER propriétaire + + // C'est l'ID du "merchant" (DCB_PARTNER) qui est propriétaire + merchantPartnerId?: string; // Référence à l'ID Keycloak du DCB_PARTNER + role: UserRole; - // Merchant Config - merchantConfigId?: number; // ID INT dans Merchant Config + // Merchant Config - Stocker l'ID du merchant dans l'autre système + merchantConfigId?: string; // ID INT dans Merchant Config pour CE merchant createdBy?: string; createdByUsername?: string; createdTimestamp: number; @@ -79,6 +90,7 @@ export interface CreateUserDto { enabled?: boolean; emailVerified?: boolean; merchantPartnerId?: string; + merchantConfigId?: string; } export interface UpdateUserDto { @@ -138,6 +150,7 @@ export interface SearchUsersParams { enabled?: boolean; userType?: UserType; merchantPartnerId?: string; + merchantConfigId?: string; page?: number; limit?: number; } @@ -164,7 +177,13 @@ export class UserUtils { [UserRole.DCB_PARTNER]: 'DCB Partner', [UserRole.DCB_PARTNER_ADMIN]: 'Partner Admin', [UserRole.DCB_PARTNER_MANAGER]: 'Partner Manager', - [UserRole.DCB_PARTNER_SUPPORT]: 'Partner Support' + [UserRole.DCB_PARTNER_SUPPORT]: 'Partner Support', + + [UserRole.MERCHANT_CONFIG_ADMIN]: 'ADMIN', + [UserRole.MERCHANT_CONFIG_MANAGER]: 'MANAGER', + [UserRole.MERCHANT_CONFIG_TECHNICAL]: 'TECHNICAL', + [UserRole.MERCHANT_CONFIG_VIEWER]: 'VIEWER', + }; return roleNames[role] || role; } diff --git a/src/app/core/models/merchant-config.model.ts b/src/app/core/models/merchant-config.model.ts index 70eb162..23c9ef6 100644 --- a/src/app/core/models/merchant-config.model.ts +++ b/src/app/core/models/merchant-config.model.ts @@ -13,7 +13,12 @@ export enum UserRole { // Rôles Merchant Partner (avec merchantPartnerId obligatoire) DCB_PARTNER_ADMIN = 'dcb-partner-admin', DCB_PARTNER_MANAGER = 'dcb-partner-manager', - DCB_PARTNER_SUPPORT = 'dcb-partner-support' + DCB_PARTNER_SUPPORT = 'dcb-partner-support', + + MERCHANT_CONFIG_ADMIN = 'ADMIN', + MERCHANT_CONFIG_MANAGER = 'MANAGER', + MERCHANT_CONFIG_TECHNICAL = 'TECHNICAL', + MERCHANT_CONFIG_VIEWER = 'VIEWER', } export enum ConfigType { @@ -32,38 +37,38 @@ export enum Operator { // === MODÈLES PRINCIPAUX === export interface MerchantConfig { - id?: string; + id?: number name: ConfigType | string; value: string; - operatorId: Operator | null; - merchantPartnerId?: string; + operatorId: Operator | 1; + merchantPartnerId?: number createdAt?: string; updatedAt?: string; } export interface TechnicalContact { - id?: string; + id?: number firstName: string; lastName: string; phone: string; email: string; - merchantPartnerId?: string; + merchantPartnerId?: number createdAt?: string; updatedAt?: string; } export interface MerchantUser { - userId: string; - role: UserRole; // Utilisation de vos rôles existants + userId: number; + role: UserRole; username?: string; email?: string; firstName?: string; lastName?: string; - merchantPartnerId?: string; + merchantPartnerId?: number } export interface Merchant { - id?: string; + id?: number name: string; logo?: string; description?: string; @@ -81,7 +86,7 @@ export interface ApiMerchantConfig { id?: number; name: ConfigType | string; value: string; - operatorId: Operator | null; + operatorId: Operator | 1; merchantPartnerId?: number; createdAt?: string; updatedAt?: string; @@ -99,7 +104,7 @@ export interface ApiTechnicalContact { } export interface ApiMerchantUser { - userId: string; + userId: number; role: UserRole; username?: string; email?: string; @@ -139,13 +144,13 @@ export interface UpdateMerchantDto extends Partial {} export interface UpdateMerchantConfigDto { name?: string; value?: string; - operatorId?: Operator | null; + operatorId?: Operator | 1; } export interface AddUserToMerchantDto { userId: string; role: UserRole; - merchantPartnerId: string; + merchantPartnerId: number; } export interface UpdateUserRoleDto { diff --git a/src/app/core/models/merchant-user-sync.model.ts b/src/app/core/models/merchant-user-sync.model.ts deleted file mode 100644 index 503c554..0000000 --- a/src/app/core/models/merchant-user-sync.model.ts +++ /dev/null @@ -1,116 +0,0 @@ -// === MODÈLES POUR LA SYNCHRONISATION === - -import { User, UserRole } from "./dcb-bo-hub-user.model"; - -export interface MerchantUserSyncDto { - // Données de base pour Keycloak - username: string; - email: string; - firstName: string; - lastName: string; - password: string; - role: UserRole; - enabled?: boolean; - emailVerified?: boolean; - - // Référence au DCB_PARTNER propriétaire - merchantPartnerId: string; // ID Keycloak du DCB_PARTNER - - // Données pour Merchant Config - merchantConfig?: { - phone?: string; - technicalContacts?: Array<{ - firstName: string; - lastName: string; - email: string; - phone: string; - }>; - }; -} - -export interface IdMapping { - id?: number; - keycloakId: string; // UUID Keycloak - merchantConfigId: number; // INT Merchant Config - merchantPartnerId: string; // ID du DCB_PARTNER propriétaire - entityType: 'merchant' | 'user'; - username?: string; - email?: string; - createdAt?: Date; - updatedAt?: Date; -} - -export interface SyncResult { - success: boolean; - keycloakUser?: User; - merchantConfigUser?: any; - mapping?: IdMapping; - errors?: string[]; -} - -export interface DCBPartnerInfo { - id: string; - username: string; - email: string; - enabled: boolean; - merchantPartnerId?: string; // Pour DCB_PARTNER, ce doit être undefined -} - -// === UTILITAIRES DE SYNCHRONISATION === -export class SyncUtils { - static validateDCBPartner(dcbPartner: User): string[] { - const errors: string[] = []; - - if (!dcbPartner) { - errors.push('DCB_PARTNER non trouvé'); - return errors; - } - - if (dcbPartner.role !== UserRole.DCB_PARTNER) { - errors.push(`L'utilisateur ${dcbPartner.username} n'est pas un DCB_PARTNER`); - } - - if (!dcbPartner.enabled) { - errors.push(`Le DCB_PARTNER ${dcbPartner.username} est désactivé`); - } - - if (dcbPartner.merchantPartnerId) { - errors.push(`Un DCB_PARTNER ne doit pas avoir de merchantPartnerId`); - } - - return errors; - } - - static validateMerchantUserCreation(dto: MerchantUserSyncDto): string[] { - const errors: string[] = []; - - if (!dto.merchantPartnerId) { - errors.push('merchantPartnerId est obligatoire'); - } - - if (!dto.username?.trim()) { - errors.push('Username est obligatoire'); - } - - if (!dto.email?.trim()) { - errors.push('Email est obligatoire'); - } - - if (!dto.password || dto.password.length < 8) { - errors.push('Le mot de passe doit contenir au moins 8 caractères'); - } - - // Validation du rôle - const merchantRoles = [ - UserRole.DCB_PARTNER_ADMIN, - UserRole.DCB_PARTNER_MANAGER, - UserRole.DCB_PARTNER_SUPPORT - ]; - - if (!merchantRoles.includes(dto.role)) { - errors.push(`Rôle invalide pour un utilisateur merchant: ${dto.role}`); - } - - return errors; - } -} \ No newline at end of file diff --git a/src/app/core/services/hub-users-roles-management.service.ts b/src/app/core/services/hub-users-roles-management.service.ts index 575982b..38e9a02 100644 --- a/src/app/core/services/hub-users-roles-management.service.ts +++ b/src/app/core/services/hub-users-roles-management.service.ts @@ -332,7 +332,11 @@ export class RoleManagementService { [UserRole.DCB_PARTNER]: 'Partenaire commercial principal', [UserRole.DCB_PARTNER_ADMIN]: 'Administrateur de partenaire marchand', [UserRole.DCB_PARTNER_MANAGER]: 'Manager opérationnel partenaire', - [UserRole.DCB_PARTNER_SUPPORT]: 'Support technique partenaire' + [UserRole.DCB_PARTNER_SUPPORT]: 'Support technique partenaire', + [UserRole.MERCHANT_CONFIG_ADMIN]: 'Administrateur de partenaire marchand', + [UserRole.MERCHANT_CONFIG_MANAGER]: 'Manager opérationnel partenaire', + [UserRole.MERCHANT_CONFIG_TECHNICAL]: 'Support technique partenaire', + [UserRole.MERCHANT_CONFIG_VIEWER]: 'Support technique partenaire' }; return roleDescriptions[userRole] || 'Description non disponible'; } diff --git a/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.ts b/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.ts index a20c8c9..ad723ed 100644 --- a/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.ts +++ b/src/app/layouts/components/sidenav/components/user-profile/user-profile.component.ts @@ -83,12 +83,16 @@ export class UserProfileComponent implements OnInit, OnDestroy { // Map role to display name const roleDisplayNames: { [key in UserRole]: string } = { - [UserRole.DCB_ADMIN]: 'Administrateur', - [UserRole.DCB_SUPPORT]: 'Support Technique', - [UserRole.DCB_PARTNER]: 'Partenaire', - [UserRole.DCB_PARTNER_ADMIN]: 'Admin Partenaire', - [UserRole.DCB_PARTNER_MANAGER]: 'Manager Partenaire', - [UserRole.DCB_PARTNER_SUPPORT]: 'Support Partenaire', + [UserRole.DCB_ADMIN]: 'Administrateur système avec tous les accès', + [UserRole.DCB_SUPPORT]: 'Support technique avec accès étendus', + [UserRole.DCB_PARTNER]: 'Partenaire commercial principal', + [UserRole.DCB_PARTNER_ADMIN]: 'Administrateur de partenaire marchand', + [UserRole.DCB_PARTNER_MANAGER]: 'Manager opérationnel partenaire', + [UserRole.DCB_PARTNER_SUPPORT]: 'Support technique partenaire', + [UserRole.MERCHANT_CONFIG_ADMIN]: 'Administrateur de partenaire marchand', + [UserRole.MERCHANT_CONFIG_MANAGER]: 'Manager opérationnel partenaire', + [UserRole.MERCHANT_CONFIG_TECHNICAL]: 'Support technique partenaire', + [UserRole.MERCHANT_CONFIG_VIEWER]: 'Support technique partenaire' }; const primaryRole = role; diff --git a/src/app/modules/hub-users-management/hub-users.service.ts b/src/app/modules/hub-users-management/hub-users.service.ts index 2f8f0f4..9d28551 100644 --- a/src/app/modules/hub-users-management/hub-users.service.ts +++ b/src/app/modules/hub-users-management/hub-users.service.ts @@ -52,15 +52,20 @@ export interface MerchantPartnerIdResponse { merchantPartnerId: string | null; } +export interface MerchantConfigIdResponse { + merchantPartnerId: number; +} + @Injectable({ providedIn: 'root' }) export class HubUsersService { private http = inject(HttpClient); - private baseApiUrl = `${environment.iamApiUrl}/hub-users`; + private baseIamApiUrl = `${environment.iamApiUrl}/hub-users`; + private baseConfigApiUrl = `${environment.configApiUrl}/merchants`; // === MÉTHODES SPÉCIFIQUES HUB === getAllUsers(): Observable { - return this.http.get(`${this.baseApiUrl}/all-users`).pipe( + return this.http.get(`${this.baseIamApiUrl}/all-users`).pipe( map(response => { // Validation de la réponse if (!response || !Array.isArray(response.hubUsers) || !Array.isArray(response.merchantUsers)) { @@ -89,7 +94,7 @@ export class HubUsersService { // Méthode pour les statistiques seulement getUsersStatistics(): Observable { - return this.http.get(`${this.baseApiUrl}`).pipe( + return this.http.get(`${this.baseIamApiUrl}`).pipe( map(response => response.statistics), catchError(error => { console.error('Error loading users statistics:', error); @@ -118,6 +123,10 @@ export class HubUsersService { return throwError(() => 'Password must be at least 8 characters'); } + if (!createUserDto.merchantConfigId) { + return throwError(() => 'Merchant Config is required and cannot be empty'); + } + // Avant de créer le payload, valider les données if (createUserDto.userType === UserType.MERCHANT_PARTNER && !createUserDto.merchantPartnerId) { return throwError(() => 'merchantPartnerId is required for merchant users'); @@ -133,6 +142,7 @@ export class HubUsersService { enabled: createUserDto.enabled ?? true, emailVerified: createUserDto.emailVerified ?? true, merchantPartnerId: createUserDto.merchantPartnerId, + merchantConfigId: createUserDto.merchantConfigId, userType: createUserDto.userType.trim() }; @@ -143,7 +153,7 @@ export class HubUsersService { console.log(payload) - return this.http.post(`${this.baseApiUrl}`, payload).pipe( + return this.http.post(`${this.baseIamApiUrl}`, payload).pipe( map(user => this.mapToUserModel(user, UserType.HUB)), catchError(error => { console.error('Error creating hub user:', error); @@ -153,7 +163,7 @@ export class HubUsersService { } getHubUsers(page: number = 1, limit: number = 10, filters?: SearchUsersParams): Observable { - return this.http.get(`${this.baseApiUrl}`).pipe( + return this.http.get(`${this.baseIamApiUrl}`).pipe( map(users => { const mappedUsers = users.map(user => this.mapToUserModel(user, UserType.HUB)); return this.filterAndPaginateUsers(mappedUsers, page, limit, filters); @@ -166,7 +176,7 @@ export class HubUsersService { } getAllDcbPartners(page: number = 1, limit: number = 10, filters?: SearchUsersParams): Observable { - return this.http.get(`${this.baseApiUrl}/partners/dcb-partners`).pipe( + return this.http.get(`${this.baseIamApiUrl}/partners/dcb-partners`).pipe( map(users => { const mappedUsers = users.map(user => this.mapToUserModel(user, UserType.HUB)); return this.filterAndPaginateUsers(mappedUsers, page, limit, filters); @@ -179,7 +189,7 @@ export class HubUsersService { } getHubUserById(id: string): Observable { - return this.http.get(`${this.baseApiUrl}/${id}`).pipe( + return this.http.get(`${this.baseIamApiUrl}/${id}`).pipe( map(user => this.mapToUserModel(user, UserType.HUB)), catchError(error => { console.error(`Error loading hub user ${id}:`, error); @@ -196,7 +206,7 @@ export class HubUsersService { enabled: updateUserDto.enabled }; - return this.http.put(`${this.baseApiUrl}/${id}`, payload).pipe( + return this.http.put(`${this.baseIamApiUrl}/${id}`, payload).pipe( map(user => this.mapToUserModel(user, UserType.HUB)), catchError(error => { console.error(`Error updating hub user ${id}:`, error); @@ -211,7 +221,7 @@ export class HubUsersService { return throwError(() => 'Invalid role for Hub user'); } - return this.http.put(`${this.baseApiUrl}/${id}/role`, { role }).pipe( + return this.http.put(`${this.baseIamApiUrl}/${id}/role`, { role }).pipe( map(user => this.mapToUserModel(user, UserType.HUB)), catchError(error => { console.error(`Error updating role for hub user ${id}:`, error); @@ -221,7 +231,7 @@ export class HubUsersService { } deleteHubUser(id: string): Observable { - return this.http.delete(`${this.baseApiUrl}/${id}`).pipe( + return this.http.delete(`${this.baseIamApiUrl}/${id}`).pipe( catchError(error => { console.error(`Error deleting hub user ${id}:`, error); return throwError(() => error); @@ -236,7 +246,7 @@ export class HubUsersService { }; return this.http.post( - `${this.baseApiUrl}/${id}/reset-password`, + `${this.baseIamApiUrl}/${id}/reset-password`, payload ).pipe( catchError(error => { @@ -283,7 +293,7 @@ export class HubUsersService { } getHubUsersByRole(role: UserRole): Observable { - return this.http.get(`${this.baseApiUrl}/role/${role}`).pipe( + return this.http.get(`${this.baseIamApiUrl}/role/${role}`).pipe( map(users => users.map(user => this.mapToUserModel(user, UserType.HUB))), catchError(error => { console.error(`Error loading hub users with role ${role}:`, error); @@ -330,6 +340,7 @@ export class HubUsersService { emailVerified: apiUser.emailVerified, userType: userType, merchantPartnerId: apiUser.merchantPartnerId, + merchantConfigId: apiUser.merchantConfigId, role: apiUser.role, createdBy: apiUser.createdBy, createdByUsername: apiUser.createdByUsername, diff --git a/src/app/modules/hub-users-management/merchant-manager.ts b/src/app/modules/hub-users-management/merchant-manager.ts new file mode 100644 index 0000000..f7789ea --- /dev/null +++ b/src/app/modules/hub-users-management/merchant-manager.ts @@ -0,0 +1,494 @@ +import { Observable, of, throwError, firstValueFrom, lastValueFrom } from 'rxjs'; + +// Mock des services +const mockMerchantConfigService = { + createMerchant: (data: any): Observable => of({ + id: 123, + name: data.name, + users: [] + }), + getMerchantById: (id: number): Observable => of({ + id, + name: 'Test Merchant', + users: [] + }), + updateMerchant: (id: number, data: any): Observable => of({ + id, + ...data + }), + deleteMerchant: (id: number): Observable => of(void 0), + getMerchantUsers: (id: number): Observable => of([ + { userId: 456, email: 'user1@test.com', role: 'MERCHANT_CONFIG_ADMIN' } + ]), + addUserToMerchant: (data: any): Observable => of({ + userId: data.userId, + email: data.email, + role: data.role + }), + updateUserRole: (merchantId: number, userId: number, data: any): Observable => of({ + userId, + role: data.role + }), + removeUserFromMerchant: (merchantId: number, userId: number): Observable => of(void 0) +}; + +const mockHubUsersService = { + createHubUser: (data: any): Observable => of({ + id: 'keycloak-123', + username: data.username, + merchantConfigId: 123 + }), + getHubUserById: (id: string): Observable => of({ + id, + username: 'owner', + merchantConfigId: 123 + }), + updateHubUser: (id: string, data: any): Observable => of({ + id, + ...data + }), + deleteHubUser: (id: string): Observable => of(void 0), + getAllDcbPartners: (): Observable<{users: any[]}> => of({ + users: [{ id: 'keycloak-123', merchantConfigId: 123 }] + }) +}; + +const mockMerchantUsersService = { + createMerchantUser: (data: any): Observable => of({ + id: 'keycloak-user-456', + email: data.email, + merchantPartnerId: data.merchantPartnerId + }), + updateMerchantUser: (id: string, data: any): Observable => of({ + id, + ...data + }), + deleteMerchantUser: (id: string): Observable => of(void 0), + getMerchantUsersByPartner: (partnerId: string): Observable => of([ + { id: 'keycloak-user-456', email: 'user1@test.com' } + ]), + searchMerchantUsers: (params: any): Observable => of([ + { id: 'keycloak-user-456', email: 'user1@test.com' } + ]), + resetMerchantUserPassword: (id: string, data: any): Observable<{ message: string }> => + of({ message: 'Password reset' }) +}; + +// Classe de test CRUD avec RxJS 7+ +export class MerchantCrudTest { + private testData = { + currentMerchantId: 0, + currentDcbPartnerId: '', + currentUserId: '', + currentMerchantConfigUserId: 0 + }; + + constructor() {} + + // ==================== NOUVELLES MÉTHODES RxJS 7+ ==================== + + /** + * Remplacer toPromise() par firstValueFrom() ou lastValueFrom() + */ + private async toPromise(observable: Observable): Promise { + // firstValueFrom: prend la première valeur et complète + // lastValueFrom: prend la dernière valeur avant completion + return firstValueFrom(observable); + } + + // ==================== TESTS CRUD ==================== + + async testCreateMerchant(): Promise { + console.log('🧪 TEST: CREATE Merchant'); + + const merchantData = { + name: 'Test Merchant', + adresse: '123 Test Street', + phone: '+33612345678', + configs: [{ name: 'API_KEY', value: 'test-key' }], + technicalContacts: [{ + firstName: 'John', + lastName: 'Doe', + phone: '+33612345678', + email: 'john@test.com' + }] + }; + + const ownerData = { + username: 'owner.test', + email: 'owner@test.com', + password: 'Password123!', + firstName: 'John', + lastName: 'Doe' + }; + + try { + // 1. Créer merchant dans Merchant Config (RxJS 7+) + const merchantConfig = await firstValueFrom( + mockMerchantConfigService.createMerchant(merchantData) + ); + console.log('✅ Merchant Config créé:', merchantConfig); + + // 2. Créer DCB_PARTNER dans Keycloak + const dcbPartnerDto = { + ...ownerData, + userType: 'HUB', + role: 'DCB_PARTNER', + merchantConfigId: merchantConfig.id + }; + + const keycloakMerchant = await firstValueFrom( + mockHubUsersService.createHubUser(dcbPartnerDto) + ); + console.log('✅ DCB_PARTNER créé:', keycloakMerchant); + + // Sauvegarder les IDs + this.testData.currentMerchantId = merchantConfig.id; + this.testData.currentDcbPartnerId = keycloakMerchant.id; + + const result = { + merchantConfig, + keycloakMerchant + }; + + console.log('🎯 RESULTAT CREATE:', result); + return result; + + } catch (error) { + console.error('❌ ERREUR CREATE:', error); + throw error; + } + } + + async testCreateMerchantUser(): Promise { + console.log('🧪 TEST: CREATE Merchant User'); + + if (!this.testData.currentMerchantId || !this.testData.currentDcbPartnerId) { + console.log('⚠️ Créez d\'abord un merchant'); + return; + } + + const userData = { + username: 'user.test', + email: 'user@test.com', + password: 'UserPass123!', + firstName: 'Jane', + lastName: 'Smith', + role: 'DCB_PARTNER_ADMIN' + }; + + try { + // 1. Créer dans Keycloak + const keycloakUserDto = { + ...userData, + userType: 'MERCHANT_PARTNER', + merchantPartnerId: this.testData.currentDcbPartnerId, + merchantConfigId: this.testData.currentMerchantId + }; + + const keycloakUser = await firstValueFrom( + mockMerchantUsersService.createMerchantUser(keycloakUserDto) + ); + console.log('✅ Utilisateur Keycloak créé:', keycloakUser); + + // 2. Ajouter à Merchant Config + const merchantConfigUserId = this.generateMerchantConfigId(keycloakUser.id); + + const merchantConfigUserDto = { + userId: merchantConfigUserId, + role: 'MERCHANT_CONFIG_ADMIN', + merchantPartnerId: this.testData.currentMerchantId, + username: keycloakUser.username, + email: keycloakUser.email, + firstName: keycloakUser.firstName, + lastName: keycloakUser.lastName + }; + + const merchantConfigUser = await firstValueFrom( + mockMerchantConfigService.addUserToMerchant(merchantConfigUserDto) + ); + console.log('✅ Utilisateur Merchant Config ajouté:', merchantConfigUser); + + // Sauvegarder + this.testData.currentUserId = keycloakUser.id; + this.testData.currentMerchantConfigUserId = merchantConfigUserId; + + console.log('🎯 RESULTAT CREATE USER:', { + keycloakUser, + merchantConfigUser + }); + + } catch (error) { + console.error('❌ ERREUR CREATE USER:', error); + } + } + + async testReadMerchant(): Promise { + console.log('🧪 TEST: READ Merchant'); + + if (!this.testData.currentMerchantId || !this.testData.currentDcbPartnerId) { + console.log('⚠️ Créez d\'abord un merchant'); + return; + } + + try { + const [merchantConfig, keycloakMerchant] = await Promise.all([ + firstValueFrom( + mockMerchantConfigService.getMerchantById(this.testData.currentMerchantId) + ), + firstValueFrom( + mockHubUsersService.getHubUserById(this.testData.currentDcbPartnerId) + ) + ]); + + console.log('🎯 RESULTAT READ:', { + merchantConfig, + keycloakMerchant, + coherence: keycloakMerchant.merchantConfigId === merchantConfig.id ? '✅ OK' : '❌ INCOHÉRENT' + }); + + } catch (error) { + console.error('❌ ERREUR READ:', error); + } + } + + async testUpdateMerchant(): Promise { + console.log('🧪 TEST: UPDATE Merchant'); + + if (!this.testData.currentMerchantId || !this.testData.currentDcbPartnerId) { + console.log('⚠️ Créez d\'abord un merchant'); + return; + } + + try { + const newName = 'Merchant Updated'; + const newEmail = 'updated@test.com'; + + const [updatedMerchant, updatedDcbPartner] = await Promise.all([ + firstValueFrom( + mockMerchantConfigService.updateMerchant( + this.testData.currentMerchantId, + { name: newName } + ) + ), + firstValueFrom( + mockHubUsersService.updateHubUser( + this.testData.currentDcbPartnerId, + { email: newEmail } + ) + ) + ]); + + console.log('🎯 RESULTAT UPDATE:', { + updatedMerchant, + updatedDcbPartner, + message: 'Merchant mis à jour avec succès' + }); + + } catch (error) { + console.error('❌ ERREUR UPDATE:', error); + } + } + + async testUpdateUser(): Promise { + console.log('🧪 TEST: UPDATE User'); + + if (!this.testData.currentUserId || !this.testData.currentMerchantConfigUserId) { + console.log('⚠️ Créez d\'abord un utilisateur'); + return; + } + + try { + const newFirstName = 'Jane Updated'; + const newRole = 'MERCHANT_CONFIG_MANAGER'; + + const [updatedKeycloakUser, updatedMerchantConfigUser] = await Promise.all([ + firstValueFrom( + mockMerchantUsersService.updateMerchantUser( + this.testData.currentUserId, + { firstName: newFirstName } + ) + ), + firstValueFrom( + mockMerchantConfigService.updateUserRole( + this.testData.currentMerchantId, + this.testData.currentMerchantConfigUserId, + { role: newRole } + ) + ) + ]); + + console.log('🎯 RESULTAT UPDATE USER:', { + updatedKeycloakUser, + updatedMerchantConfigUser + }); + + } catch (error) { + console.error('❌ ERREUR UPDATE USER:', error); + } + } + + async testDeleteUser(): Promise { + console.log('🧪 TEST: DELETE User'); + + if (!this.testData.currentUserId || !this.testData.currentMerchantConfigUserId) { + console.log('⚠️ Créez d\'abord un utilisateur'); + return; + } + + try { + await Promise.all([ + firstValueFrom( + mockMerchantUsersService.deleteMerchantUser(this.testData.currentUserId) + ), + firstValueFrom( + mockMerchantConfigService.removeUserFromMerchant( + this.testData.currentMerchantId, + this.testData.currentMerchantConfigUserId + ) + ) + ]); + + console.log('🎯 RESULTAT DELETE USER: Utilisateur supprimé des deux systèmes'); + + // Réinitialiser + this.testData.currentUserId = ''; + this.testData.currentMerchantConfigUserId = 0; + + } catch (error) { + console.error('❌ ERREUR DELETE USER:', error); + } + } + + async testDeleteMerchant(): Promise { + console.log('🧪 TEST: DELETE Merchant'); + + if (!this.testData.currentMerchantId || !this.testData.currentDcbPartnerId) { + console.log('⚠️ Créez d\'abord un merchant'); + return; + } + + try { + // Récupérer les utilisateurs + const users = await firstValueFrom( + mockMerchantConfigService.getMerchantUsers(this.testData.currentMerchantId) + ); + + console.log(`Suppression de ${users.length} utilisateurs...`); + + // Supprimer le merchant et le DCB_PARTNER + await Promise.all([ + firstValueFrom( + mockMerchantConfigService.deleteMerchant(this.testData.currentMerchantId) + ), + firstValueFrom( + mockHubUsersService.deleteHubUser(this.testData.currentDcbPartnerId) + ) + ]); + + console.log('🎯 RESULTAT DELETE MERCHANT: Merchant supprimé des deux systèmes'); + + // Réinitialiser + this.testData.currentMerchantId = 0; + this.testData.currentDcbPartnerId = ''; + + } catch (error) { + console.error('❌ ERREUR DELETE MERCHANT:', error); + } + } + + async testAllOperations(): Promise { + console.log('🚀 DÉMARRAGE DES TESTS COMPLETS'); + console.log('='.repeat(50)); + + try { + await this.testCreateMerchant(); + console.log('-'.repeat(30)); + + await this.testCreateMerchantUser(); + console.log('-'.repeat(30)); + + await this.testReadMerchant(); + console.log('-'.repeat(30)); + + await this.testUpdateMerchant(); + console.log('-'.repeat(30)); + + await this.testUpdateUser(); + console.log('-'.repeat(30)); + + await this.testDeleteUser(); + console.log('-'.repeat(30)); + + await this.testReadMerchant(); + console.log('-'.repeat(30)); + + await this.testDeleteMerchant(); + console.log('-'.repeat(30)); + + console.log('✅ TOUS LES TESTS TERMINÉS AVEC SUCCÈS'); + + } catch (error) { + console.error('❌ ERREUR DANS LES TESTS:', error); + } + } + + private generateMerchantConfigId(keycloakId: string): number { + let hash = 0; + for (let i = 0; i < keycloakId.length; i++) { + hash = ((hash << 5) - hash) + keycloakId.charCodeAt(i); + hash = hash & hash; + } + return Math.abs(hash) % 1000000; + } + + getTestData() { + return { ...this.testData }; + } + + resetTestData() { + this.testData = { + currentMerchantId: 0, + currentDcbPartnerId: '', + currentUserId: '', + currentMerchantConfigUserId: 0 + }; + console.log('🧹 Données de test réinitialisées'); + } +} + +// ==================== EXÉCUTION ==================== + +/** + * Fonction pour exécuter les tests + */ +export async function runTests() { + console.log('🚀 LANCEMENT DES TESTS'); + console.log('='.repeat(60)); + + // Option 1: Avec RxJS 7+ + console.log('\n📦 TEST AVEC RxJS 7+:'); + console.log('-'.repeat(40)); + const rxjsTest = new MerchantCrudTest(); + await rxjsTest.testAllOperations(); + + console.log('\n🎉 TOUS LES TESTS SONT TERMINÉS'); +} + +// Exemple d'utilisation dans un environnement de test +/* +import { runIndividualTests } from './merchant-crud.test'; + +const tests = runIndividualTests(); + +// Exécuter un test spécifique +tests.testCreate().then(() => { + console.log('Test CREATE terminé'); + console.log('Données:', tests.getData()); +}); + +// Ou exécuter tous les tests +tests.testAll().then(() => { + console.log('Tous les tests terminés'); +}); +*/ \ No newline at end of file diff --git a/src/app/modules/hub-users-management/merchant-sync-orchestrator.service.ts b/src/app/modules/hub-users-management/merchant-sync-orchestrator.service.ts new file mode 100644 index 0000000..962c481 --- /dev/null +++ b/src/app/modules/hub-users-management/merchant-sync-orchestrator.service.ts @@ -0,0 +1,684 @@ +import { Injectable, inject } from '@angular/core'; +import { Observable, forkJoin, map, switchMap, catchError, of, throwError, tap } from 'rxjs'; +import { + CreateUserDto, + UpdateUserDto, + User, + UserType, + UserRole, + UserUtils +} from '@core/models/dcb-bo-hub-user.model'; + +import { HubUsersService} from '@modules/hub-users-management/hub-users.service'; +import { MerchantUsersService} from '@modules/hub-users-management/merchant-users.service'; + +import { MerchantConfigService } from '@modules/merchant-config/merchant-config.service'; + +import { + CreateMerchantDto, + UpdateMerchantDto, + Merchant, + MerchantUser, + AddUserToMerchantDto, + UpdateUserRoleDto +} from '@core/models/merchant-config.model'; + +export interface MerchantSyncResult { + merchantConfig: Merchant; + keycloakMerchant: User; +} + +export interface MerchantUserSyncResult { + keycloakUser: User; + merchantConfigUser: MerchantUser; +} + +export interface MerchantSyncStatus { + existsInKeycloak: boolean; + existsInMerchantConfig: boolean; + usersSynced: boolean; + syncedUserCount: number; + totalUserCount: number; +} + +@Injectable({ providedIn: 'root' }) +export class MerchantSyncCrudService { + private hubUsersService = inject(HubUsersService); + private merchantUsersService = inject(MerchantUsersService); + private merchantConfigService = inject(MerchantConfigService); + + // ==================== CONSTANTES ==================== + + private readonly KEYCLOAK_MERCHANT_ROLES = [ + UserRole.DCB_PARTNER_ADMIN, + UserRole.DCB_PARTNER_MANAGER, + UserRole.DCB_PARTNER_SUPPORT + ]; + + private readonly MERCHANT_CONFIG_ROLES = [ + UserRole.MERCHANT_CONFIG_ADMIN, + UserRole.MERCHANT_CONFIG_MANAGER, + UserRole.MERCHANT_CONFIG_TECHNICAL, + UserRole.MERCHANT_CONFIG_VIEWER + ]; + + private readonly ROLE_MAPPING: Map = new Map([ + // Keycloak -> Merchant Config + [UserRole.DCB_PARTNER_ADMIN, UserRole.MERCHANT_CONFIG_ADMIN], + [UserRole.DCB_PARTNER_MANAGER, UserRole.MERCHANT_CONFIG_MANAGER], + [UserRole.DCB_PARTNER_SUPPORT, UserRole.MERCHANT_CONFIG_VIEWER], + + // Merchant Config -> Keycloak + [UserRole.MERCHANT_CONFIG_ADMIN, UserRole.DCB_PARTNER_ADMIN], + [UserRole.MERCHANT_CONFIG_MANAGER, UserRole.DCB_PARTNER_MANAGER], + [UserRole.MERCHANT_CONFIG_TECHNICAL, UserRole.DCB_PARTNER_SUPPORT], + [UserRole.MERCHANT_CONFIG_VIEWER, UserRole.DCB_PARTNER_SUPPORT] + ]); + + // ==================== CREATE ==================== + + /** + * CREATE - Créer un merchant complet dans les deux systèmes + */ + createMerchant( + merchantData: CreateMerchantDto, + dcbPartnerData: { + username: string; + email: string; + password: string; + firstName: string; + lastName: string; + } + ): Observable { + console.log('📝 CREATE Merchant...'); + + return this.merchantConfigService.createMerchant(merchantData).pipe( + switchMap(merchantConfig => { + console.log('✅ Merchant créé dans Merchant Config:', merchantConfig); + + const merchantConfigId = String(merchantConfig.id); + + const dcbPartnerDto: CreateUserDto = { + username: dcbPartnerData.username, + email: dcbPartnerData.email, + firstName: dcbPartnerData.firstName, + lastName: dcbPartnerData.lastName, + password: dcbPartnerData.password, + userType: UserType.HUB, + role: UserRole.DCB_PARTNER, + enabled: true, + emailVerified: true, + merchantConfigId: merchantConfigId + }; + + return this.hubUsersService.createHubUser(dcbPartnerDto).pipe( + map(keycloakMerchant => ({ + merchantConfig, + keycloakMerchant: { + ...keycloakMerchant, + merchantConfigId: merchantConfigId // <<<<< ✔ string + } + })), + catchError(keycloakError => { + console.error('❌ Échec Keycloak, rollback...'); + return this.rollbackMerchantCreation(String(merchantConfig.id!), keycloakError); + }) + ); + }), + catchError(error => { + console.error('❌ Échec création merchant:', error); + return throwError(() => error); + }) + ); + } + + /** + * CREATE - Créer un utilisateur merchant + */ + createMerchantUser( + merchantConfigId: string, + keycloakMerchantId: string, + userData: { + username: string; + email: string; + password: string; + firstName: string; + lastName: string; + role: UserRole; // Rôle Keycloak + } + ): Observable { + console.log('📝 CREATE Merchant User...'); + + // Validation rôle Keycloak + if (!this.KEYCLOAK_MERCHANT_ROLES.includes(userData.role)) { + return throwError(() => new Error('Rôle Keycloak invalide')); + } + + // 1. Créer dans Keycloak + const keycloakUserDto: CreateUserDto = { + ...userData, + userType: UserType.MERCHANT_PARTNER, + merchantPartnerId: keycloakMerchantId, + merchantConfigId: merchantConfigId, + enabled: true, + emailVerified: true + }; + + return this.merchantUsersService.createMerchantUser(keycloakUserDto).pipe( + switchMap(keycloakUser => { + console.log('✅ Utilisateur créé dans Keycloak:', keycloakUser); + + // 2. Convertir rôle et ajouter à Merchant Config + const merchantConfigRole = this.mapToMerchantConfigRole(userData.role); + const merchantConfigUserId = keycloakUser.id; + + const merchantConfigUserDto: AddUserToMerchantDto = { + userId: keycloakUser.id, + role: merchantConfigRole, + merchantPartnerId: Number(merchantConfigId), + }; + + return this.merchantConfigService.addUserToMerchant(merchantConfigUserDto).pipe( + map(merchantConfigUser => { + console.log('✅ Utilisateur ajouté à Merchant Config:', merchantConfigUser); + + return { + keycloakUser: { + ...keycloakUser, + merchantConfigUserId, + merchantConfigId + }, + merchantConfigUser + }; + }), + catchError(merchantConfigError => { + console.error('❌ Échec Merchant Config, rollback Keycloak...'); + return this.rollbackUserCreation(keycloakUser.id, merchantConfigError); + }) + ); + }), + catchError(error => { + console.error('❌ Échec création utilisateur:', error); + return throwError(() => error); + }) + ); + } + + // ==================== READ ==================== + + /** + * READ - Récupérer un merchant par ID + */ + getMerchant( + merchantConfigId: string, + keycloakMerchantId: string + ): Observable { + console.log('🔍 READ Merchant...'); + + return forkJoin({ + merchantConfig: this.merchantConfigService.getMerchantById(Number(merchantConfigId)), + keycloakMerchant: this.hubUsersService.getHubUserById(keycloakMerchantId) + }).pipe( + map(({ merchantConfig, keycloakMerchant }) => { + + // Vérifier la cohérence des IDs + if (String(merchantConfig.id) !== keycloakMerchant.merchantConfigId) { + console.warn('⚠️ Incohérence: merchantConfigId ne correspond pas'); + } + + return { + merchantConfig, + keycloakMerchant: { + ...keycloakMerchant, + merchantConfigId: String(merchantConfig.id) + } + }; + }), + catchError(error => { + console.error('❌ Erreur récupération merchant:', error); + return throwError(() => error); + }) + ); + } + + /** + * READ - Récupérer tous les merchants + */ + getAllMerchants(): Observable { + console.log('🔍 READ All Merchants...'); + + return forkJoin({ + merchants: this.merchantConfigService.getAllMerchants(), + dcbPartners: this.hubUsersService.getAllDcbPartners() + }).pipe( + map(({ merchants, dcbPartners }) => { + return merchants + .map(merchant => { + const dcbPartner = dcbPartners.users.find( + partner => partner.merchantConfigId === merchant.id + ); + + if (!dcbPartner) { + console.warn(`⚠️ Merchant ${merchant.id} n'a pas de DCB_PARTNER correspondant`); + return null; + } + + return { + merchantConfig: merchant, + keycloakMerchant: dcbPartner + }; + }) + .filter((merchant): merchant is MerchantSyncResult => merchant !== null); + }), + catchError(error => { + console.error('❌ Erreur récupération merchants:', error); + return throwError(() => error); + }) + ); + } + + /** + * READ - Récupérer tous les utilisateurs d'un merchant + */ + getMerchantUsers( + merchantConfigId: string, + keycloakMerchantId: string + ): Observable<{ + merchantConfig: Merchant; + keycloakMerchant: User; + users: Array<{ + keycloakUser: User; + merchantConfigUser?: MerchantUser; + synced: boolean; + }>; + }> { + return forkJoin({ + merchantConfig: this.merchantConfigService.getMerchantById(Number(merchantConfigId)), + keycloakMerchant: this.hubUsersService.getHubUserById(keycloakMerchantId), + merchantConfigUsers: this.merchantConfigService.getMerchantUsers(Number(merchantConfigId)), + keycloakUsers: this.merchantUsersService.getMerchantUsersByPartner(keycloakMerchantId) + }).pipe( + map(({ merchantConfig, keycloakMerchant, merchantConfigUsers, keycloakUsers }) => { + + const users = keycloakUsers.map(keycloakUser => { + const merchantConfigUser = merchantConfigUsers.find( + mcUser => mcUser.email === keycloakUser.email + ); + + return { + keycloakUser, + merchantConfigUser, + synced: !!merchantConfigUser + }; + }); + + return { + merchantConfig, + keycloakMerchant: { + ...keycloakMerchant, + merchantConfigId: String(merchantConfig.id) + }, + users + }; + }) + ); + } + + // ==================== UPDATE ==================== + + /** + * UPDATE - Mettre à jour un merchant + */ + updateMerchant( + merchantConfigId: string, + keycloakMerchantId: string, + updates: { + merchantConfig?: UpdateMerchantDto; + dcbPartner?: UpdateUserDto; + } + ): Observable { + console.log('✏️ UPDATE Merchant...'); + + const operations: Observable[] = []; + + // Mise à jour MerchantConfig + if (updates.merchantConfig) { + operations.push( + this.merchantConfigService.updateMerchant( + Number(merchantConfigId), + updates.merchantConfig + ) + ); + } + + // Mise à jour du user DCB_PARTNER + if (updates.dcbPartner && keycloakMerchantId) { + operations.push( + this.hubUsersService.updateHubUser(keycloakMerchantId, updates.dcbPartner) + ); + } + + if (operations.length === 0) { + return throwError(() => new Error('Aucune donnée à mettre à jour')); + } + + return forkJoin(operations).pipe( + switchMap(() => { + // Recharger les données mises à jour + return this.getMerchant( + merchantConfigId, + keycloakMerchantId + ); + }), + catchError(error => { + console.error('❌ Erreur mise à jour merchant:', error); + return throwError(() => error); + }) + ); + } + + /** + * UPDATE - Mettre à jour un utilisateur merchant + */ + updateMerchantUser( + keycloakUserId: string, + merchantConfigUserId: number, + merchantConfigId: string, + updates: { + keycloak?: UpdateUserDto; + merchantConfig?: { + role?: UserRole; + }; + } + ): Observable<{ + keycloakUser?: User; + merchantConfigUser?: MerchantUser; + }> { + console.log('✏️ UPDATE Merchant User...'); + + const operations: Observable[] = []; + + // Mise à jour Keycloak + if (updates.keycloak) { + operations.push( + this.merchantUsersService.updateMerchantUser(keycloakUserId, updates.keycloak) + ); + } + + // Mise à jour Merchant Config + if (updates.merchantConfig?.role) { + if (!this.MERCHANT_CONFIG_ROLES.includes(updates.merchantConfig.role)) { + return throwError(() => new Error('Rôle Merchant Config invalide')); + } + + const updateRoleDto: UpdateUserRoleDto = { + role: updates.merchantConfig.role + }; + + operations.push( + this.merchantConfigService.updateUserRole( + Number(merchantConfigId), + merchantConfigUserId, + updateRoleDto + ) + ); + } + + if (operations.length === 0) { + return throwError(() => new Error('Aucune donnée à mettre à jour')); + } + + return forkJoin(operations).pipe( + map(([keycloakUpdate, merchantConfigUpdate]) => ({ + keycloakUser: keycloakUpdate, + merchantConfigUser: merchantConfigUpdate + })), + catchError(error => { + console.error('❌ Erreur mise à jour utilisateur:', error); + return throwError(() => error); + }) + ); + } + + /** + * UPDATE - Activer/désactiver un utilisateur + */ + toggleUserStatus( + keycloakUserId: string, + enabled: boolean + ): Observable { + console.log(`🔄 ${enabled ? 'Activer' : 'Désactiver'} utilisateur...`); + + return this.merchantUsersService.updateMerchantUser(keycloakUserId, { enabled }); + } + + // ==================== DELETE ==================== + + /** + * DELETE - Supprimer un merchant + */ + deleteMerchant( + merchantConfigId: string, + keycloakMerchantId: string + ): Observable<{ success: boolean; message: string }> { + console.log('🗑️ DELETE Merchant...'); + + // 1. D'abord supprimer tous les utilisateurs + return this.merchantConfigService.getMerchantUsers(Number(merchantConfigId)).pipe( + switchMap(merchantConfigUsers => { + const userDeletions: Observable[] = []; + + // Supprimer chaque utilisateur + merchantConfigUsers.forEach(mcUser => { + if (mcUser.email) { + userDeletions.push( + this.merchantUsersService.searchMerchantUsers({ query: mcUser.email }).pipe( + switchMap(users => { + if (users.length > 0) { + return this.deleteMerchantUser( + users[0].id, + mcUser.userId, + merchantConfigId + ).pipe(catchError(() => of(null))); + } + return of(null); + }) + ) + ); + } + }); + + // Supprimer le merchant + userDeletions.push( + this.merchantConfigService.deleteMerchant(Number(merchantConfigId)) + ); + + // Supprimer le DCB_PARTNER + userDeletions.push( + this.hubUsersService.deleteHubUser(keycloakMerchantId).pipe( + catchError(() => of({ ignored: true })) + ) + ); + + return forkJoin(userDeletions).pipe( + map(() => ({ + success: true, + message: 'Merchant et utilisateurs supprimés avec succès' + })) + ); + }), + catchError(async (error) => ({ + success: false, + message: `Erreur suppression: ${error.message}` + })) + ); + } + + /** + * DELETE - Supprimer un utilisateur merchant + */ + deleteMerchantUser( + keycloakUserId: string, + merchantConfigUserId: number, + merchantConfigId: string, + ): Observable<{ success: boolean; message: string }> { + console.log('🗑️ DELETE Merchant User...'); + + return forkJoin({ + keycloakDeletion: this.merchantUsersService.deleteMerchantUser(keycloakUserId), + merchantConfigDeletion: this.merchantConfigService.removeUserFromMerchant( + Number(merchantConfigId), + merchantConfigUserId + ).pipe(catchError(() => of({ ignored: true }))) + }).pipe( + map(() => ({ + success: true, + message: 'Utilisateur supprimé des deux systèmes' + })), + catchError(async (error) => ({ + success: false, + message: `Erreur suppression: ${error.message}` + })) + ); + } + + // ==================== UTILITAIRES ==================== + + /** + * Vérifier le statut de synchronisation + */ + checkSyncStatus( + merchantConfigId: string, + keycloakMerchantId: string + ): Observable { + return forkJoin({ + merchantConfig: this.merchantConfigService.getMerchantById(Number(merchantConfigId)).pipe( + catchError(() => of(null)) + ), + keycloakMerchant: this.hubUsersService.getHubUserById(keycloakMerchantId).pipe( + catchError(() => of(null)) + ), + merchantConfigUsers: this.merchantConfigService.getMerchantUsers(Number(merchantConfigId)).pipe( + catchError(() => of([])) + ), + keycloakUsers: this.merchantUsersService.getMerchantUsersByPartner(keycloakMerchantId).pipe( + catchError(() => of([])) + ) + }).pipe( + map(({ merchantConfig, keycloakMerchant, merchantConfigUsers, keycloakUsers }) => { + + const syncedUsers = keycloakUsers.filter(kcUser => + merchantConfigUsers.some(mcUser => mcUser.email === kcUser.email) + ); + + return { + existsInKeycloak: !!keycloakMerchant, + existsInMerchantConfig: !!merchantConfig, + usersSynced: syncedUsers.length === keycloakUsers.length && + syncedUsers.length === merchantConfigUsers.length, + syncedUserCount: syncedUsers.length, + totalUserCount: Math.max(keycloakUsers.length, merchantConfigUsers.length) + }; + }) + ); + } + + /** + * Rechercher des merchants + */ + searchMerchants(query: string): Observable { + return this.merchantConfigService.getAllMerchants({ query }).pipe( + switchMap(merchants => { + if (merchants.length === 0) return of([]); + + return this.hubUsersService.getAllDcbPartners().pipe( + map(dcbPartners => { + return merchants + .map(merchant => { + const dcbPartner = dcbPartners.users.find( + partner => partner.merchantConfigId === merchant.id + ); + + return dcbPartner ? { + merchantConfig: merchant, + keycloakMerchant: dcbPartner + } : null; + }) + .filter((merchant): merchant is MerchantSyncResult => merchant !== null); + }) + ); + }) + ); + } + + /** + * Réinitialiser le mot de passe d'un utilisateur + */ + resetUserPassword( + keycloakUserId: string, + newPassword: string, + temporary: boolean = true + ): Observable<{ success: boolean; message: string }> { + return this.merchantUsersService.resetMerchantUserPassword( + keycloakUserId, + { newPassword, temporary } + ).pipe( + map(() => ({ + success: true, + message: 'Mot de passe réinitialisé avec succès' + })), + catchError(async (error) => ({ + success: false, + message: `Erreur réinitialisation: ${error.message}` + })) + ); + } + + // ==================== MÉTHODES PRIVÉES ==================== + + private mapToMerchantConfigRole(keycloakRole: UserRole): UserRole { + const mappedRole = this.ROLE_MAPPING.get(keycloakRole); + return mappedRole || UserRole.MERCHANT_CONFIG_VIEWER; + } + + private mapToKeycloakRole(merchantConfigRole: UserRole): UserRole { + // Recherche inverse + for (const [key, value] of this.ROLE_MAPPING.entries()) { + if (value === merchantConfigRole) { + return key; + } + } + return UserRole.DCB_PARTNER_SUPPORT; + } + + private generateMerchantConfigId(keycloakId: string): number { + let hash = 0; + for (let i = 0; i < keycloakId.length; i++) { + hash = ((hash << 5) - hash) + keycloakId.charCodeAt(i); + hash = hash & hash; + } + return Math.abs(hash) % 1000000; + } + + private rollbackMerchantCreation( + merchantConfigId: string, + originalError: any + ): Observable { + return this.merchantConfigService.deleteMerchant(Number(merchantConfigId)).pipe( + switchMap(() => throwError(() => new Error( + `Échec création DCB_PARTNER: ${originalError.message}. Rollback effectué.` + ))) + ); + } + + private rollbackUserCreation( + keycloakUserId: string, + originalError: any + ): Observable { + return this.merchantUsersService.deleteMerchantUser(keycloakUserId).pipe( + switchMap(() => throwError(() => new Error( + `Échec création Merchant Config: ${originalError.message}. Rollback effectué.` + ))) + ); + } +} \ No newline at end of file diff --git a/src/app/modules/hub-users-management/merchant-users.service.ts b/src/app/modules/hub-users-management/merchant-users.service.ts index e8a7f4a..def304b 100644 --- a/src/app/modules/hub-users-management/merchant-users.service.ts +++ b/src/app/modules/hub-users-management/merchant-users.service.ts @@ -88,7 +88,6 @@ export class MerchantUsersService { return throwError(() => 'Password must be at least 8 characters'); } - // Adapter le payload pour le nouveau contrôleur const payload = { username: createUserDto.username.trim(), email: createUserDto.email.trim(), @@ -99,6 +98,7 @@ export class MerchantUsersService { enabled: createUserDto.enabled !== undefined ? createUserDto.enabled : true, emailVerified: createUserDto.emailVerified !== undefined ? createUserDto.emailVerified : true, merchantPartnerId: createUserDto.merchantPartnerId?.trim() || null, + //merchantConfigId: createUserDto.merchantConfigId?.trim() || null, userType: createUserDto.userType.trim() }; @@ -291,7 +291,8 @@ export class MerchantUsersService { emailVerified: apiUser.emailVerified, userType: userType, merchantPartnerId: apiUser.merchantPartnerId, - role: apiUser.role, // Convertir le rôle unique en tableau + merchantConfigId: apiUser.merchantConfigId, + role: apiUser.role, createdBy: apiUser.createdBy, createdByUsername: apiUser.createdByUsername, createdTimestamp: apiUser.createdTimestamp, diff --git a/src/app/modules/hub-users-management/merchant-users.ts b/src/app/modules/hub-users-management/merchant-users.ts index 91f8e8e..c6ba8e2 100644 --- a/src/app/modules/hub-users-management/merchant-users.ts +++ b/src/app/modules/hub-users-management/merchant-users.ts @@ -20,6 +20,7 @@ import { UserType } from '@core/models/dcb-bo-hub-user.model'; import { HubUsersService } from './hub-users.service'; +import { MerchantSyncCrudService } from './merchant-sync-orchestrator.service'; @Component({ selector: 'app-merchant-users', @@ -132,8 +133,12 @@ export class MerchantUsersManagement implements OnInit, OnDestroy { // Initialiser le formulaire selon le contexte this.initializeMerchantPartner(); + } + + constructor() {} + // Initialiser le merchant partner selon le contexte initializeMerchantPartner() { if (this.currentUserRole === UserRole.DCB_PARTNER && this.currentMerchantPartnerId) { diff --git a/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.ts b/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.ts index dbbeccc..48f97f0 100644 --- a/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.ts +++ b/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.ts @@ -47,7 +47,7 @@ export class MerchantConfigsList implements OnInit, OnDestroy { @Input() canDeleteMerchants: boolean = false; // Outputs - @Output() merchantSelected = new EventEmitter(); + @Output() merchantSelected = new EventEmitter(); @Output() openCreateMerchantModal = new EventEmitter(); @Output() editMerchantRequested = new EventEmitter(); @Output() deleteMerchantRequested = new EventEmitter(); diff --git a/src/app/modules/merchant-config/merchant-config-view/merchant-config-view.ts b/src/app/modules/merchant-config/merchant-config-view/merchant-config-view.ts index 72d3200..de20130 100644 --- a/src/app/modules/merchant-config/merchant-config-view/merchant-config-view.ts +++ b/src/app/modules/merchant-config/merchant-config-view/merchant-config-view.ts @@ -184,9 +184,9 @@ export class MerchantConfigView implements OnInit, OnDestroy { readonly Operator = Operator; readonly MerchantUtils = MerchantUtils; - @Input() merchantId!: string; + @Input() merchantId!: number; @Output() back = new EventEmitter(); - @Output() editConfigRequested = new EventEmitter(); + @Output() editConfigRequested = new EventEmitter(); @Output() editMerchantRequested = new EventEmitter(); merchant: Merchant | null = null; @@ -205,16 +205,18 @@ export class MerchantConfigView implements OnInit, OnDestroy { currentMerchantPartnerId: string = ''; // Édition des configurations - editingConfigId: string | null = null; + editingConfigId: number | null = null; editedConfig: UpdateMerchantConfigDto = {}; configToDelete: MerchantConfig | null = null; private deleteModalRef: any = null; // Affichage des valeurs sensibles - showSensitiveValues: { [configId: string]: boolean } = {}; + showSensitiveValues: { [configId: number]: boolean } = {}; // Expansion des sections - expandedSections: { [sectionId: string]: boolean } = {}; + expandedSections: { [sectionId: number]: boolean } = {}; + + expandedSection: string | null = null; // Pagination page = 1; @@ -318,12 +320,12 @@ export class MerchantConfigView implements OnInit, OnDestroy { // ==================== GESTION DE L'EXPANSION ==================== - toggleSection(sectionId: string) { - this.expandedSections[sectionId] = !this.expandedSections[sectionId]; + toggleSection(section: string) { + this.expandedSection = this.expandedSection === section ? null : section; } - isSectionExpanded(sectionId: string): boolean { - return !!this.expandedSections[sectionId]; + isSectionExpanded(section: string): boolean { + return this.expandedSection === section; } // ==================== ÉDITION DES CONFIGURATIONS ==================== @@ -408,11 +410,11 @@ export class MerchantConfigView implements OnInit, OnDestroy { newConfig: Omit = { name: ConfigType.CUSTOM, value: '', - operatorId: null + operatorId: 1 }; // États pour la modification de rôle - editingUserId: string | null = null; + editingUserId: number | null = null; newUserRole: UserRole | null = null; // ==================== GESTION DES CONFIGURATIONS ==================== @@ -422,7 +424,7 @@ export class MerchantConfigView implements OnInit, OnDestroy { this.newConfig = { name: ConfigType.CUSTOM, value: '', - operatorId: null + operatorId: 1 }; } @@ -431,7 +433,7 @@ export class MerchantConfigView implements OnInit, OnDestroy { this.newConfig = { name: ConfigType.CUSTOM, value: '', - operatorId: null + operatorId: 1 }; this.error = ''; } @@ -459,7 +461,7 @@ export class MerchantConfigView implements OnInit, OnDestroy { this.newConfig = { name: ConfigType.CUSTOM, value: '', - operatorId: null + operatorId: 1 }; this.clearCache(); this.cdRef.detectChanges(); @@ -504,7 +506,7 @@ export class MerchantConfigView implements OnInit, OnDestroy { return this.isSensitiveConfig({ name: this.newConfig.name, value: '', - operatorId: null + operatorId: 1 } as MerchantConfig); } @@ -661,7 +663,7 @@ export class MerchantConfigView implements OnInit, OnDestroy { }); } - isEditingUserRole(userId: string): boolean { + isEditingUserRole(userId: number): boolean { return this.editingUserId === userId; } @@ -723,7 +725,7 @@ export class MerchantConfigView implements OnInit, OnDestroy { return this.canManageAllMerchants(); } - isEditingConfig(configId: string): boolean { + isEditingConfig(configId: number): boolean { return this.editingConfigId === configId; } @@ -734,7 +736,9 @@ export class MerchantConfigView implements OnInit, OnDestroy { if (this.canManageAllMerchants()) return true; - return config.merchantPartnerId === this.currentMerchantPartnerId; + return true; + + //return config.merchantPartnerId === this.currentMerchantPartnerId; } canManageAllMerchants(): boolean { @@ -747,12 +751,14 @@ export class MerchantConfigView implements OnInit, OnDestroy { if (this.canManageAllMerchants()) return true; + return true; + // PARTNER ne peut modifier que ses propres marchands - return this.merchant.configs?.some(config => - config.merchantPartnerId === this.currentMerchantPartnerId - ) || this.merchant.users?.some(user => - user.merchantPartnerId === this.currentMerchantPartnerId - ); + // return this.merchant.configs?.some(config => + // config.merchantPartnerId === this.currentMerchantPartnerId + // ) || this.merchant.users?.some(user => + // user.merchantPartnerId === this.currentMerchantPartnerId + // ); } // ==================== UTILITAIRES D'AFFICHAGE ==================== @@ -781,7 +787,7 @@ export class MerchantConfigView implements OnInit, OnDestroy { return this.isSensitiveConfig(config) || config.value.length > 100; } - toggleSensitiveValue(configId: string) { + toggleSensitiveValue(configId: number) { this.showSensitiveValues[configId] = !this.showSensitiveValues[configId]; } @@ -965,7 +971,7 @@ export class MerchantConfigView implements OnInit, OnDestroy { this.back.emit(); } - requestEditConfig(configId: string) { + requestEditConfig(configId: number) { this.editConfigRequested.emit(configId); } @@ -1028,15 +1034,15 @@ export class MerchantConfigView implements OnInit, OnDestroy { // ==================== TRACKBY FUNCTIONS ==================== - trackByConfigId(index: number, config: MerchantConfig): string { - return config.id || `config-${index}`; + trackByConfigId(index: number, config: MerchantConfig): number { + return config.id || index; } - trackByContactId(index: number, contact: TechnicalContact): string { - return contact.id || `contact-${index}`; + trackByContactId(index: number, contact: TechnicalContact): number { + return contact.id || index; } - trackByUserId(index: number, user: MerchantUser): string { - return user.userId || `user-${index}`; + trackByUserId(index: number, user: MerchantUser): number { + return user.userId || index; } } \ No newline at end of file diff --git a/src/app/modules/merchant-config/merchant-config.service.ts b/src/app/modules/merchant-config/merchant-config.service.ts index e4382c9..b017c18 100644 --- a/src/app/modules/merchant-config/merchant-config.service.ts +++ b/src/app/modules/merchant-config/merchant-config.service.ts @@ -114,7 +114,7 @@ export class MerchantConfigService { ); } - getMerchantById(id: string): Observable { + getMerchantById(id: number): Observable { const numericId = this.convertIdToNumber(id); console.log(`📥 Loading merchant ${id}`); @@ -130,14 +130,14 @@ export class MerchantConfigService { ); } - updateMerchant(id: string, updateMerchantDto: UpdateMerchantDto): Observable { - const numericId = this.convertIdToNumber(id); + updateMerchant(id: number, updateMerchantDto: UpdateMerchantDto): Observable { + //const numericId = this.convertIdToNumber(id); const apiDto = this.dataAdapter.convertUpdateMerchantToApi(updateMerchantDto); console.log(`📤 Updating merchant ${id}:`, apiDto); - return this.http.patch(`${this.baseApiUrl}/${numericId}`, apiDto).pipe( + return this.http.patch(`${this.baseApiUrl}/${id}`, apiDto).pipe( timeout(this.REQUEST_TIMEOUT), map(apiMerchant => { console.log(`✅ Merchant ${id} updated successfully`); @@ -147,7 +147,7 @@ export class MerchantConfigService { ); } - deleteMerchant(id: string): Observable { + deleteMerchant(id: number): Observable { const numericId = this.convertIdToNumber(id); console.log(`🗑️ Deleting merchant ${id}`); @@ -164,30 +164,24 @@ export class MerchantConfigService { // ==================== USER MANAGEMENT ==================== addUserToMerchant(addUserDto: AddUserToMerchantDto): Observable { - // CONVERSION AVEC L'ADAPTER - const apiDto = { - ...addUserDto, - merchantPartnerId: addUserDto.merchantPartnerId ? - this.convertIdToNumber(addUserDto.merchantPartnerId) : undefined - }; + + console.log('📤 Adding user to merchant:', addUserDto); - console.log('📤 Adding user to merchant:', apiDto); - - return this.http.post(`${this.baseApiUrl}/users`, apiDto).pipe( + return this.http.post(`${this.baseApiUrl}/users`, addUserDto).pipe( timeout(this.REQUEST_TIMEOUT), map(apiUser => { console.log('✅ User added to merchant successfully'); - // ✅ UTILISATION DE L'ADAPTER + return this.dataAdapter.convertApiUserToFrontend(apiUser); }), catchError(error => this.handleError('addUserToMerchant', error)) ); } - getMerchantUsers(merchantId: string): Observable { - const numericMerchantId = this.convertIdToNumber(merchantId); + getMerchantUsers(merchantId: number): Observable { + //const merchantId = this.convertIdToNumber(merchantId); - return this.http.get(`${this.baseApiUrl}/${numericMerchantId}`).pipe( + return this.http.get(`${this.baseApiUrl}/${merchantId}`).pipe( timeout(this.REQUEST_TIMEOUT), map(apiMerchant => { return (apiMerchant.users || []).map(user => @@ -199,11 +193,11 @@ export class MerchantConfigService { ); } - updateUserRole(merchantId: string, userId: string, updateRoleDto: UpdateUserRoleDto): Observable { - const numericMerchantId = this.convertIdToNumber(merchantId); + updateUserRole(merchantId: number, userId: number, updateRoleDto: UpdateUserRoleDto): Observable { + //const merchantId = this.convertIdToNumber(merchantId); return this.http.patch( - `${this.baseApiUrl}/${numericMerchantId}/users/${userId}/role`, + `${this.baseApiUrl}/${merchantId}/users/${userId}/role`, updateRoleDto ).pipe( timeout(this.REQUEST_TIMEOUT), @@ -216,10 +210,10 @@ export class MerchantConfigService { ); } - removeUserFromMerchant(merchantId: string, userId: string): Observable { - const numericMerchantId = this.convertIdToNumber(merchantId); + removeUserFromMerchant(merchantId: number, userId: number): Observable { + //const merchantId = this.convertIdToNumber(merchantId); - return this.http.delete(`${this.baseApiUrl}/${numericMerchantId}/users/${userId}`).pipe( + return this.http.delete(`${this.baseApiUrl}/${merchantId}/users/${userId}`).pipe( timeout(this.REQUEST_TIMEOUT), map(() => { console.log(`✅ User ${userId} removed from merchant ${merchantId} successfully`); @@ -228,7 +222,7 @@ export class MerchantConfigService { ); } - getUserMerchants(userId: string): Observable { + getUserMerchants(userId: number): Observable { return this.http.get(`${this.baseApiUrl}/user/${userId}`).pipe( timeout(this.REQUEST_TIMEOUT), map(apiMerchants => { @@ -242,19 +236,19 @@ export class MerchantConfigService { // ==================== CONFIG MANAGEMENT ==================== - addConfigToMerchant(merchantId: string, config: Omit): Observable { - const numericMerchantId = this.convertIdToNumber(merchantId); + addConfigToMerchant(merchantId: number, config: Omit): Observable { + //const merchantId = this.convertIdToNumber(merchantId); // CONVERSION AVEC L'ADAPTER const apiConfig: Omit = { ...config, operatorId: config.operatorId, - merchantPartnerId: numericMerchantId + merchantPartnerId: merchantId }; console.log(`📤 Adding config to merchant ${merchantId}:`, apiConfig); - return this.http.post(`${this.baseApiUrl}/${numericMerchantId}/configs`, apiConfig).pipe( + return this.http.post(`${this.baseApiUrl}/${merchantId}/configs`, apiConfig).pipe( timeout(this.REQUEST_TIMEOUT), map(apiConfig => { console.log('✅ Config added to merchant successfully'); @@ -265,14 +259,14 @@ export class MerchantConfigService { ); } - updateConfig(configId: string, config: Partial): Observable { - const numericConfigId = this.convertIdToNumber(configId); + updateConfig(configId: number, config: Partial): Observable { + //const configId = this.convertIdToNumber(configId); const apiConfig = this.dataAdapter.convertConfigUpdateToApi(config); console.log(`📤 Updating config ${configId}:`, apiConfig); - return this.http.patch(`${this.baseApiUrl}/configs/${numericConfigId}`, apiConfig).pipe( + return this.http.patch(`${this.baseApiUrl}/configs/${configId}`, apiConfig).pipe( timeout(this.REQUEST_TIMEOUT), map(apiConfig => { console.log(`✅ Config ${configId} updated successfully`); @@ -283,12 +277,12 @@ export class MerchantConfigService { ); } - deleteConfig(configId: string): Observable { - const numericConfigId = this.convertIdToNumber(configId); + deleteConfig(configId: number): Observable { + //const configId = this.convertIdToNumber(configId); console.log(`🗑️ Deleting config ${configId}`); - return this.http.delete(`${this.baseApiUrl}/configs/${numericConfigId}`).pipe( + return this.http.delete(`${this.baseApiUrl}/configs/${configId}`).pipe( timeout(this.REQUEST_TIMEOUT), map(() => { console.log(`✅ Config ${configId} deleted successfully`); @@ -297,7 +291,7 @@ export class MerchantConfigService { ); } - getMerchantConfigs(merchantId: string): Observable { + getMerchantConfigs(merchantId: number): Observable { return this.getMerchantById(merchantId).pipe( map(merchant => merchant?.configs ?? []) ); @@ -358,7 +352,7 @@ export class MerchantConfigService { return throwError(() => new Error(userMessage)); } - private convertIdToNumber(id: string): number { + private convertIdToNumber(id: number): number { if (!id) { throw new Error('ID cannot be null or undefined'); } diff --git a/src/app/modules/merchant-config/merchant-config.ts b/src/app/modules/merchant-config/merchant-config.ts index 3ec1451..2767444 100644 --- a/src/app/modules/merchant-config/merchant-config.ts +++ b/src/app/modules/merchant-config/merchant-config.ts @@ -64,8 +64,8 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { // État de l'interface activeTab: 'list' | 'profile' = 'list'; - selectedMerchantId: string | null = null; - selectedConfigId: string | null = null; + selectedMerchantId: number | null = null; + selectedConfigId: number | null = null; // Gestion des permissions currentUserRole: UserRole | null = null; @@ -91,8 +91,8 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { selectedMerchantForDelete: Merchant | null = null; // États pour la gestion des modifications - editingConfigs: { [configId: string]: boolean } = {}; - editingContacts: { [contactId: string]: boolean } = {}; + editingConfigs: { [configId: number]: boolean } = {}; + editingContacts: { [contactId: number]: boolean } = {}; // Références aux templates de modals @ViewChild('createMerchantModal') createMerchantModal!: TemplateRef; @@ -126,8 +126,8 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { selectedMerchantPartnerId: string = ''; // Cache des marchands - merchantProfiles: { [merchantId: string]: Merchant } = {}; - loadingMerchants: { [merchantId: string]: boolean } = {}; + merchantProfiles: { [merchantId: number]: Merchant } = {}; + loadingMerchants: { [merchantId: number]: boolean } = {}; ngOnInit() { this.activeTab = 'list'; @@ -303,11 +303,13 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { */ canModifyMerchant(merchant: Merchant): boolean { if (this.canManageMerchants) return true; + + return true; // PARTNER ne peut modifier que ses propres marchands // Vérifier via les configs ou users - return merchant.configs?.some(config => config.merchantPartnerId === this.currentMerchantPartnerId) || - merchant.users?.some(user => user.merchantPartnerId === this.currentMerchantPartnerId); + //return merchant.configs?.some(config => config.merchantPartnerId === this.currentMerchantPartnerId) || + // merchant.users?.some(user => user.merchantPartnerId === this.currentMerchantPartnerId); } // ==================== GESTION DES MARCHANDS ==================== @@ -320,8 +322,8 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { } } - onMerchantSelectionChange(selectedMerchantId: string): void { - this.selectedMerchantPartnerId = selectedMerchantId; + onMerchantSelectionChange(selectedMerchantId: number): void { + //this.selectedMerchantPartnerId = selectedMerchantId; // Recharger les marchands pour le partenaire sélectionné if (this.merchantConfigsList) { @@ -331,7 +333,7 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { // ==================== GESTION DES ONGLETS ==================== - showTab(tab: 'list' | 'profile', merchantId?: string): void { + showTab(tab: 'list' | 'profile', merchantId?: number): void { console.log(`Switching to tab: ${tab}`, merchantId ? `for merchant ${merchantId}` : ''); this.activeTab = tab; @@ -347,7 +349,7 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { } } - private loadMerchantProfile(merchantId: string): void { + private loadMerchantProfile(merchantId: number): void { if (this.loadingMerchants[merchantId]) return; this.loadingMerchants[merchantId] = true; @@ -374,7 +376,7 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { // ==================== ÉVÉNEMENTS DES COMPOSANTS ENFANTS ==================== - onMerchantSelected(merchantId: string): void { + onMerchantSelected(merchantId: number): void { this.showTab('profile', merchantId); } @@ -388,7 +390,7 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { this.openDeleteMerchantModal(merchant.id!); } - onEditConfigRequested(configId: string): void { + onEditConfigRequested(configId: number): void { this.openEditMerchantModal(configId); } @@ -413,7 +415,7 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { this.openModal(this.createMerchantModal); } - private openEditMerchantModal(merchantId: string): void { + private openEditMerchantModal(merchantId: number): void { this.merchantConfigService.getMerchantById(merchantId) .pipe(takeUntil(this.destroy$)) @@ -440,7 +442,7 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { }); } - private openDeleteMerchantModal(merchantId: string): void { + private openDeleteMerchantModal(merchantId: number): void { if (!this.canDeleteMerchants) { console.warn('User does not have permission to delete merchants'); return; @@ -765,12 +767,12 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { } // Méthodes utilitaires pour le template d'édition - trackByConfigId(index: number, config: MerchantConfig): string { - return config.id || `new-${index}`; + trackByConfigId(index: number, config: MerchantConfig): number { + return config.id || index; } - trackByContactId(index: number, contact: TechnicalContact): string { - return contact.id || `new-${index}`; + trackByContactId(index: number, contact: TechnicalContact): number { + return contact.id || index; } // Méthode pour détecter les configs sensibles diff --git a/src/app/modules/merchant-config/merchant-data-adapter.service.ts b/src/app/modules/merchant-config/merchant-data-adapter.service.ts index f4bd035..6bd8a2b 100644 --- a/src/app/modules/merchant-config/merchant-data-adapter.service.ts +++ b/src/app/modules/merchant-config/merchant-data-adapter.service.ts @@ -29,7 +29,7 @@ export class MerchantDataAdapter { return { ...apiMerchant, - id: this.convertIdToString(apiMerchant.id), + id: apiMerchant.id, //this.convertIdToString(apiMerchant.id), configs: (apiMerchant.configs || []).map(config => this.convertApiConfigToFrontend(config) ), @@ -48,8 +48,8 @@ export class MerchantDataAdapter { convertApiConfigToFrontend(apiConfig: ApiMerchantConfig): MerchantConfig { return { ...apiConfig, - id: this.convertIdToString(apiConfig.id), - merchantPartnerId: this.convertIdToString(apiConfig.merchantPartnerId) + id: apiConfig.id, //this.convertIdToString(apiConfig.id), + merchantPartnerId: apiConfig.merchantPartnerId // this.convertIdToString(apiConfig.merchantPartnerId) }; } @@ -59,8 +59,8 @@ export class MerchantDataAdapter { convertApiContactToFrontend(apiContact: ApiTechnicalContact): TechnicalContact { return { ...apiContact, - id: this.convertIdToString(apiContact.id), - merchantPartnerId: this.convertIdToString(apiContact.merchantPartnerId) + id: apiContact.id, //this.convertIdToString(apiContact.id), + merchantPartnerId: apiContact.merchantPartnerId //this.convertIdToString(apiContact.merchantPartnerId) }; } @@ -160,7 +160,7 @@ export class MerchantDataAdapter { } if (config.merchantPartnerId !== undefined) { - updateData.merchantPartnerId = this.convertIdToNumber(config.merchantPartnerId); + updateData.merchantPartnerId = config.merchantPartnerId //this.convertIdToNumber(config.merchantPartnerId); } return updateData; diff --git a/src/app/modules/modules.routes.ts b/src/app/modules/modules.routes.ts index 41883e6..27d1c10 100644 --- a/src/app/modules/modules.routes.ts +++ b/src/app/modules/modules.routes.ts @@ -291,12 +291,12 @@ const routes: Routes = [ // Support & Profile (Tous les utilisateurs authentifiés) // --------------------------- { - path: 'dcb-support', + path: 'support', component: Support, canActivate: [authGuard, roleGuard], data: { title: 'Support', - module: 'dcb-support' + module: 'support' } }, { diff --git a/src/app/modules/support/support.ts b/src/app/modules/support/support.ts index 09b8669..27f7e88 100644 --- a/src/app/modules/support/support.ts +++ b/src/app/modules/support/support.ts @@ -1,7 +1,503 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { MerchantSyncCrudService } from '../hub-users-management/merchant-sync-orchestrator.service'; +import { UserRole } from '@core/models/dcb-bo-hub-user.model'; +import { firstValueFrom } from 'rxjs'; @Component({ selector: 'app-support', - templateUrl: './support.html', + templateUrl: './support.html' }) -export class Support {} \ No newline at end of file +export class Support implements OnInit { + + private testData = { + merchantConfigId: '', + keycloakMerchantId: '', + testUserId: '', + testMerchantConfigUserId:'' + }; + + constructor(private merchantCrudService: MerchantSyncCrudService) {} + + ngOnInit() { + console.log('🚀 Support Component - Tests CRUD Merchants'); + console.log('='.repeat(60)); + + // Démarrer les tests automatiquement + this.runAllTests(); + } + + /** + * Exécuter tous les tests + */ + async runAllTests(): Promise { + try { + console.log('\n🧪 DÉMARRAGE DES TESTS COMPLETS'); + console.log('='.repeat(50)); + + // Test 1: Création + await this.testCreateOperations(); + + // Test 2: Lecture + await this.testReadOperations(); + + // Test 3: Mise à jour + await this.testUpdateOperations(); + + // Test 4: Gestion utilisateurs + await this.testUserOperations(); + + // Test 5: Suppression + await this.testDeleteOperations(); + + console.log('\n✅ TOUS LES TESTS TERMINÉS AVEC SUCCÈS!'); + console.log('='.repeat(50)); + + } catch (error) { + console.error('❌ ERREUR CRITIQUE DANS LES TESTS:', error); + } + } + + /** + * TEST 1: Opérations de création + */ + private async testCreateOperations(): Promise { + console.log('\n📝 TEST 1: Opérations CREATE'); + console.log('-'.repeat(40)); + + try { + // 1.1 Créer un merchant de test + console.log('1.1 Création d\'un merchant test...'); + + const merchantData = { + name: 'Test Merchant ' + Date.now(), + adresse: '123 Test Street', + phone: '+336' + Math.floor(10000000 + Math.random() * 90000000), + configs: [ + { + name: 'API_KEY', + value: 'test-api-key-' + Date.now(), + operatorId: 1 + } + ], + technicalContacts: [ + { + firstName: 'Test', + lastName: 'User', + phone: '+33612345678', + email: `test.${Date.now()}@example.com` + } + ] + }; + + const ownerData = { + username: `testowner.${Date.now()}`, + email: `owner.${Date.now()}@test-merchant.com`, + password: 'TestPassword123!', + firstName: 'Test', + lastName: 'Owner' + }; + + console.log('📤 Données merchant:', merchantData); + console.log('👤 Données owner:', { ...ownerData, password: '***' }); + + const createResult = await firstValueFrom( + this.merchantCrudService.createMerchant(merchantData, ownerData) + ); + + this.testData.merchantConfigId = String(createResult.merchantConfig.id!); + this.testData.keycloakMerchantId = createResult.keycloakMerchant.id; + + console.log('✅ Merchant créé avec succès!'); + console.log(' Merchant Config ID:', this.testData.merchantConfigId); + console.log(' Keycloak Merchant ID:', this.testData.keycloakMerchantId); + console.log(' Merchant name:', createResult.merchantConfig.name); + console.log(' DCB Partner email:', createResult.keycloakMerchant.email); + + // 1.2 Créer un utilisateur pour le merchant + console.log('\n1.2 Création d\'un utilisateur test...'); + + const userData = { + username: `testuser.${Date.now()}`, + email: `user.${Date.now()}@test-merchant.com`, + password: 'UserPassword123!', + firstName: 'Test', + lastName: 'User', + role: UserRole.DCB_PARTNER_ADMIN + }; + + const userResult = await firstValueFrom( + this.merchantCrudService.createMerchantUser( + this.testData.merchantConfigId, + this.testData.keycloakMerchantId, + userData + ) + ); + + this.testData.testUserId = userResult.keycloakUser.id; + this.testData.testMerchantConfigUserId = String(userResult.merchantConfigUser.userId); + + console.log('✅ Utilisateur créé avec succès!'); + console.log(' Keycloak User ID:', this.testData.testUserId); + console.log(' Merchant Config User ID:', this.testData.testMerchantConfigUserId); + console.log(' Email:', userResult.keycloakUser.email); + console.log(' Rôle Keycloak:', userResult.keycloakUser.role); + console.log(' Rôle Merchant Config:', userResult.merchantConfigUser.role); + + } catch (error) { + console.error('❌ ERREUR lors de la création:', error); + throw error; + } + } + + /** + * TEST 2: Opérations de lecture + */ + private async testReadOperations(): Promise { + console.log('\n🔍 TEST 2: Opérations READ'); + console.log('-'.repeat(40)); + + if (!this.testData.merchantConfigId) { + console.log('⚠️ Aucun merchant créé, passage au test suivant'); + return; + } + + try { + // 2.1 Lire le merchant par ID + console.log('2.1 Lecture du merchant par ID...'); + + const merchant = await firstValueFrom( + this.merchantCrudService.getMerchant( + this.testData.merchantConfigId, + this.testData.keycloakMerchantId + ) + ); + + console.log('✅ Merchant récupéré:'); + console.log(' ID:', merchant.merchantConfig.id); + console.log(' Nom:', merchant.merchantConfig.name); + console.log(' Adresse:', merchant.merchantConfig.adresse); + console.log(' Configurations:', merchant.merchantConfig.configs?.length || 0); + console.log(' Contacts techniques:', merchant.merchantConfig.technicalContacts?.length || 0); + + // 2.2 Lire tous les merchants + console.log('\n2.2 Lecture de tous les merchants...'); + + const allMerchants = await firstValueFrom( + this.merchantCrudService.getAllMerchants() + ); + + console.log(`✅ ${allMerchants.length} merchants trouvés au total`); + + if (allMerchants.length > 0) { + console.log(' Dernier merchant:', allMerchants[allMerchants.length - 1].merchantConfig.name); + } + + // 2.3 Lire les utilisateurs du merchant + console.log('\n2.3 Lecture des utilisateurs du merchant...'); + + const merchantUsers = await firstValueFrom( + this.merchantCrudService.getMerchantUsers( + this.testData.merchantConfigId, + this.testData.keycloakMerchantId + ) + ); + + console.log(`✅ ${merchantUsers.users.length} utilisateurs dans ce merchant`); + + merchantUsers.users.forEach((user: any, index: number) => { + console.log(` ${index + 1}. ${user.keycloakUser?.email || 'Inconnu'}`); + console.log(` Synchronisé: ${user.synced ? '✅' : '❌'}`); + }); + + // 2.4 Vérifier le statut de synchronisation + console.log('\n2.4 Vérification statut synchronisation...'); + + const syncStatus = await firstValueFrom( + this.merchantCrudService.checkSyncStatus( + String(this.testData.merchantConfigId), + this.testData.keycloakMerchantId + ) + ); + + console.log('✅ Statut de synchronisation:'); + console.log(' Existe dans Keycloak:', syncStatus.existsInKeycloak ? '✅' : '❌'); + console.log(' Existe dans Merchant Config:', syncStatus.existsInMerchantConfig ? '✅' : '❌'); + console.log(' Utilisateurs synchronisés:', syncStatus.usersSynced ? '✅' : '❌'); + console.log(` ${syncStatus.syncedUserCount}/${syncStatus.totalUserCount} utilisateurs synchronisés`); + + } catch (error) { + console.error('❌ ERREUR lors de la lecture:', error); + } + } + + /** + * TEST 3: Opérations de mise à jour + */ + private async testUpdateOperations(): Promise { + console.log('\n✏️ TEST 3: Opérations UPDATE'); + console.log('-'.repeat(40)); + + if (!this.testData.merchantConfigId) { + console.log('⚠️ Aucun merchant créé, passage au test suivant'); + return; + } + + try { + // 3.1 Mettre à jour le merchant + console.log('3.1 Mise à jour du merchant...'); + + const newName = `Updated Merchant ${Date.now()}`; + const newEmail = `updated.${Date.now()}@test-merchant.com`; + + const updateResult = await firstValueFrom( + this.merchantCrudService.updateMerchant( + this.testData.merchantConfigId, + this.testData.keycloakMerchantId, + { + merchantConfig: { + name: newName, + description: 'Mis à jour par les tests' + }, + dcbPartner: { + email: newEmail + } + } + ) + ); + + console.log('✅ Merchant mis à jour:'); + console.log(' Nouveau nom:', newName); + console.log(' Nouvel email DCB Partner:', newEmail); + + // 3.2 Mettre à jour l'utilisateur + console.log('\n3.2 Mise à jour de l\'utilisateur...'); + + if (this.testData.testUserId) { + const userUpdateResult = await firstValueFrom( + this.merchantCrudService.updateMerchantUser( + this.testData.testUserId, + Number(this.testData.testMerchantConfigUserId), + this.testData.merchantConfigId, + { + keycloak: { + firstName: 'Updated', + lastName: 'User' + }, + merchantConfig: { + role: UserRole.MERCHANT_CONFIG_MANAGER + } + } + ) + ); + + console.log('✅ Utilisateur mis à jour:'); + console.log(' Nouveau nom:', 'Updated User'); + console.log(' Nouveau rôle:', UserRole.MERCHANT_CONFIG_MANAGER); + + } else { + console.log('⚠️ Aucun utilisateur à mettre à jour'); + } + + // 3.3 Activer/désactiver un utilisateur + console.log('\n3.3 Changement statut utilisateur...'); + + if (this.testData.testUserId) { + const toggleResult = await firstValueFrom( + this.merchantCrudService.toggleUserStatus( + this.testData.testUserId, + false // Désactiver + ) + ); + + console.log('✅ Statut utilisateur changé:'); + console.log(' Activé:', toggleResult.enabled ? '✅' : '❌'); + + // Réactiver pour les tests suivants + await firstValueFrom( + this.merchantCrudService.toggleUserStatus( + this.testData.testUserId, + true + ) + ); + console.log(' Réactivé pour les tests suivants'); + } + + } catch (error) { + console.error('❌ ERREUR lors de la mise à jour:', error); + } + } + + /** + * TEST 4: Opérations utilisateurs avancées + */ + private async testUserOperations(): Promise { + console.log('\n👥 TEST 4: Opérations utilisateurs avancées'); + console.log('-'.repeat(40)); + + if (!this.testData.testUserId) { + console.log('⚠️ Aucun utilisateur créé, passage au test suivant'); + return; + } + + try { + // 4.1 Réinitialiser le mot de passe + console.log('4.1 Réinitialisation mot de passe...'); + + const resetResult = await firstValueFrom( + this.merchantCrudService.resetUserPassword( + this.testData.testUserId, + 'NewPassword123!', + true + ) + ); + + console.log('✅ Mot de passe réinitialisé:'); + console.log(' Succès:', resetResult.success); + console.log(' Message:', resetResult.message); + + // 4.2 Rechercher des merchants + console.log('\n4.2 Recherche de merchants...'); + + const searchResults = await firstValueFrom( + this.merchantCrudService.searchMerchants('Test') + ); + + console.log(`✅ ${searchResults.length} merchants trouvés avec "Test"`); + + if (searchResults.length > 0) { + console.log(' Premier résultat:', searchResults[0].merchantConfig.name); + } + + } catch (error) { + console.error('❌ ERREUR opérations utilisateurs:', error); + } + } + + /** + * TEST 5: Opérations de suppression + */ + private async testDeleteOperations(): Promise { + console.log('\n🗑️ TEST 5: Opérations DELETE'); + console.log('-'.repeat(40)); + + if (!this.testData.merchantConfigId) { + console.log('⚠️ Aucun merchant créé, passage au test suivant'); + return; + } + + try { + // 5.1 Supprimer l'utilisateur test + console.log('5.1 Suppression de l\'utilisateur test...'); + + if (this.testData.testUserId) { + const deleteUserResult = await firstValueFrom( + this.merchantCrudService.deleteMerchantUser( + this.testData.testUserId, + Number(this.testData.testMerchantConfigUserId), + this.testData.merchantConfigId + ) + ); + + console.log('✅ Utilisateur supprimé:'); + console.log(' Succès:', deleteUserResult.success); + console.log(' Message:', deleteUserResult.message); + + // Réinitialiser les IDs utilisateur + this.testData.testUserId = ''; + this.testData.testMerchantConfigUserId = ''; + } else { + console.log('⚠️ Aucun utilisateur à supprimer'); + } + + // 5.2 Supprimer le merchant test + console.log('\n5.2 Suppression du merchant test...'); + + const deleteMerchantResult = await firstValueFrom( + this.merchantCrudService.deleteMerchant( + this.testData.merchantConfigId, + this.testData.keycloakMerchantId + ) + ); + + console.log('✅ Merchant supprimé:'); + console.log(' Succès:', deleteMerchantResult.success); + console.log(' Message:', deleteMerchantResult.message); + + // 5.3 Vérifier la suppression + console.log('\n5.3 Vérification de la suppression...'); + + try { + await firstValueFrom( + this.merchantCrudService.getMerchant( + this.testData.merchantConfigId, + this.testData.keycloakMerchantId + ) + ); + console.log('❌ Le merchant existe toujours - PROBLÈME!'); + } catch (error) { + console.log('✅ Le merchant a bien été supprimé'); + } + + // Réinitialiser toutes les données + this.testData = { + merchantConfigId: '', + keycloakMerchantId: '', + testUserId: '', + testMerchantConfigUserId: '' + }; + + console.log('🧹 Données de test réinitialisées'); + + } catch (error) { + console.error('❌ ERREUR lors de la suppression:', error); + } + } + + /** + * Méthodes pour exécuter des tests individuels + */ + async testCreateOnly(): Promise { + console.log('🧪 Test CREATE uniquement'); + await this.testCreateOperations(); + } + + async testReadOnly(): Promise { + console.log('🧪 Test READ uniquement'); + await this.testReadOperations(); + } + + async testUpdateOnly(): Promise { + console.log('🧪 Test UPDATE uniquement'); + await this.testUpdateOperations(); + } + + async testDeleteOnly(): Promise { + console.log('🧪 Test DELETE uniquement'); + await this.testDeleteOperations(); + } + + /** + * Afficher l'état actuel des tests + */ + showTestStatus(): void { + console.log('\n📊 ÉTAT ACTUEL DES TESTS'); + console.log('-'.repeat(30)); + console.log('Merchant Config ID:', this.testData.merchantConfigId || 'Non créé'); + console.log('Keycloak Merchant ID:', this.testData.keycloakMerchantId || 'Non créé'); + console.log('Test User ID:', this.testData.testUserId || 'Non créé'); + console.log('Test Merchant Config User ID:', this.testData.testMerchantConfigUserId || 'Non créé'); + } + + /** + * Réinitialiser les données de test + */ + resetTestData(): void { + this.testData = { + merchantConfigId: '', + keycloakMerchantId: '', + testUserId: '', + testMerchantConfigUserId: '' + }; + console.log('🧹 Données de test réinitialisées'); + } +} \ No newline at end of file