From 7091f1665d6c3af7349af95011d3f9f73a83d846 Mon Sep 17 00:00:00 2001 From: diallolatoile Date: Mon, 17 Nov 2025 17:28:16 +0000 Subject: [PATCH] feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature --- src/app/core/models/merchant-config.model.ts | 239 +++++++++++++ .../core/services/HUB-DCB-APIs.code-workspace | 11 + src/app/core/services/menu.service.ts | 2 +- src/app/core/services/permissions.service.ts | 2 +- src/app/layouts/components/data.ts | 2 +- .../merchant-config/merchant-config.html | 335 ++++++++++++++++++ .../merchant-config.service.ts | 315 ++++++++++++++++ .../merchant-config/merchant-config.ts | 294 +++++++++++++++ src/app/modules/modules.routes.ts | 17 +- src/environments/environment.preprod.ts | 1 + src/environments/environment.prod.ts | 1 + src/environments/environment.ts | 1 + 12 files changed, 1216 insertions(+), 4 deletions(-) create mode 100644 src/app/core/models/merchant-config.model.ts create mode 100644 src/app/core/services/HUB-DCB-APIs.code-workspace create mode 100644 src/app/modules/merchant-config/merchant-config.html create mode 100644 src/app/modules/merchant-config/merchant-config.service.ts create mode 100644 src/app/modules/merchant-config/merchant-config.ts diff --git a/src/app/core/models/merchant-config.model.ts b/src/app/core/models/merchant-config.model.ts new file mode 100644 index 0000000..edcc3eb --- /dev/null +++ b/src/app/core/models/merchant-config.model.ts @@ -0,0 +1,239 @@ +// === ENUMS COHÉRENTS === +export enum UserType { + HUB = 'HUB', + MERCHANT_PARTNER = 'MERCHANT' +} + +export enum UserRole { + // Rôles Hub (sans merchantPartnerId) + DCB_ADMIN = 'dcb-admin', + DCB_SUPPORT = 'dcb-support', + DCB_PARTNER = 'dcb-partner', + + // 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' +} + +export enum MerchantStatus { + ACTIVE = 'ACTIVE', + INACTIVE = 'INACTIVE', + PENDING = 'PENDING', + SUSPENDED = 'SUSPENDED' +} + +export enum ConfigType { + API_KEY = 'API_KEY', + SECRET_KEY = 'SECRET_KEY', + WEBHOOK_URL = 'WEBHOOK_URL', + CALLBACK_URL = 'CALLBACK_URL', + TIMEOUT = 'TIMEOUT', + RETRY_COUNT = 'RETRY_COUNT', + CUSTOM = 'CUSTOM' +} + +export enum Operator { + ORANGE_CI = 1, + MTN_CI = 2, + MOOV_CI = 3, + WAVE = 4 +} + +// === MODÈLES PRINCIPAUX === +export interface MerchantConfig { + id?: number; + name: ConfigType | string; + value: string; + operatorId: Operator; + merchantId?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface TechnicalContact { + id?: number; + firstName: string; + lastName: string; + phone: string; + email: string; + merchantId?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface MerchantUser { + userId: string; + role: UserRole; // Utilisation de vos rôles existants + username?: string; + email?: string; + firstName?: string; + lastName?: string; + merchantPartnerId?: number; +} + +export interface Merchant { + id?: number; + name: string; + logo?: string; + description?: string; + adresse: string; + phone: string; + status?: MerchantStatus; + configs: MerchantConfig[]; + users: MerchantUser[]; + technicalContacts: TechnicalContact[]; + createdAt?: string; + updatedAt?: string; + createdBy?: string; + createdByUsername?: string; +} + +// === DTOs CRUD === +export interface CreateMerchantDto { + name: string; + logo?: string; + description?: string; + adresse: string; + phone: string; + configs: Omit[]; + technicalContacts: Omit[]; +} + +export interface UpdateMerchantDto extends Partial { + status?: MerchantStatus; +} + +export interface AddUserToMerchantDto { + userId: string; + role: UserRole; // Utilisation de vos rôles existants + merchantPartnerId: number; +} + +export interface UpdateUserRoleDto { + role: UserRole; // Utilisation de vos rôles existants +} + +// === RÉPONSES API === +export interface ApiResponse { + success: boolean; + data?: T; + error?: string; + message?: string; +} + +export interface PaginatedResponse { + items: T[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + +export interface MerchantStatsResponse { + totalMerchants: number; + activeMerchants: number; + inactiveMerchants: number; + pendingMerchants: number; + totalConfigs: number; + totalTechnicalContacts: number; +} + +// === SEARCH === +export interface SearchMerchantsParams { + query?: string; + status?: MerchantStatus; + page?: number; + limit?: number; +} + +// === UTILITAIRES === +export class MerchantUtils { + static getStatusDisplayName(status: MerchantStatus): string { + const statusNames = { + [MerchantStatus.ACTIVE]: 'Actif', + [MerchantStatus.INACTIVE]: 'Inactif', + [MerchantStatus.PENDING]: 'En attente', + [MerchantStatus.SUSPENDED]: 'Suspendu' + }; + return statusNames[status] || status; + } + + static getStatusBadgeClass(status: MerchantStatus): string { + const statusClasses = { + [MerchantStatus.ACTIVE]: 'badge bg-success', + [MerchantStatus.INACTIVE]: 'badge bg-secondary', + [MerchantStatus.PENDING]: 'badge bg-warning', + [MerchantStatus.SUSPENDED]: 'badge bg-danger' + }; + return statusClasses[status] || 'badge bg-secondary'; + } + + static getOperatorName(operatorId: Operator): string { + const operatorNames = { + [Operator.ORANGE_CI]: 'Orange CI', + [Operator.MTN_CI]: 'MTN CI', + [Operator.MOOV_CI]: 'Moov CI', + [Operator.WAVE]: 'Wave' + }; + return operatorNames[operatorId] || 'Inconnu'; + } + + static getConfigTypeName(configName: ConfigType | string): string { + const configTypeNames = { + [ConfigType.API_KEY]: 'Clé API', + [ConfigType.SECRET_KEY]: 'Clé Secrète', + [ConfigType.WEBHOOK_URL]: 'URL Webhook', + [ConfigType.CALLBACK_URL]: 'URL Callback', + [ConfigType.TIMEOUT]: 'Timeout (ms)', + [ConfigType.RETRY_COUNT]: 'Nombre de tentatives', + [ConfigType.CUSTOM]: 'Personnalisé' + }; + return configTypeNames[configName as ConfigType] || configName; + } + + static validateMerchantCreation(merchant: CreateMerchantDto): string[] { + const errors: string[] = []; + + if (!merchant.name?.trim()) { + errors.push('Le nom du merchant est requis'); + } + + if (!merchant.adresse?.trim()) { + errors.push('L\'adresse est requise'); + } + + if (!merchant.phone?.trim()) { + errors.push('Le téléphone est requis'); + } + + if (!merchant.technicalContacts || merchant.technicalContacts.length === 0) { + errors.push('Au moins un contact technique est requis'); + } + + if (!merchant.configs || merchant.configs.length === 0) { + errors.push('Au moins une configuration est requise'); + } + + return errors; + } + + // Méthode pour obtenir les rôles disponibles pour les merchants + static getAvailableMerchantRoles(): UserRole[] { + return [ + UserRole.DCB_PARTNER_ADMIN, + UserRole.DCB_PARTNER_MANAGER, + UserRole.DCB_PARTNER_SUPPORT + ]; + } + + // Vérifier si un rôle est valide pour un merchant + static isValidMerchantRole(role: UserRole): boolean { + const merchantRoles = [ + UserRole.DCB_PARTNER_ADMIN, + UserRole.DCB_PARTNER_MANAGER, + UserRole.DCB_PARTNER_SUPPORT + ]; + return merchantRoles.includes(role); + } +} \ No newline at end of file diff --git a/src/app/core/services/HUB-DCB-APIs.code-workspace b/src/app/core/services/HUB-DCB-APIs.code-workspace new file mode 100644 index 0000000..1926586 --- /dev/null +++ b/src/app/core/services/HUB-DCB-APIs.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "../../../../../dcb-user-service" + }, + { + "path": "../../../.." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/src/app/core/services/menu.service.ts b/src/app/core/services/menu.service.ts index 02eda37..17154ee 100644 --- a/src/app/core/services/menu.service.ts +++ b/src/app/core/services/menu.service.ts @@ -117,7 +117,7 @@ export class MenuService { }, { label: 'Configurations', isTitle: true }, - { label: 'Merchant Configs', icon: 'lucideStore', url: '/merchant-configs' }, + { label: 'Merchant Config', icon: 'lucideStore', url: '/merchant-config' }, { label: 'Paramètres Système', icon: 'lucideSettings', url: '/settings' }, { label: 'Intégrations Externes', icon: 'lucidePlug', url: '/integrations' }, diff --git a/src/app/core/services/permissions.service.ts b/src/app/core/services/permissions.service.ts index 330df80..aca31c8 100644 --- a/src/app/core/services/permissions.service.ts +++ b/src/app/core/services/permissions.service.ts @@ -76,7 +76,7 @@ export class PermissionsService { // Settings - Tout le monde { - module: 'merchant-configs', + module: 'merchant-config', roles: this.allRoles }, diff --git a/src/app/layouts/components/data.ts b/src/app/layouts/components/data.ts index 719f848..4d586d8 100644 --- a/src/app/layouts/components/data.ts +++ b/src/app/layouts/components/data.ts @@ -126,7 +126,7 @@ export const menuItems: MenuItemType[] = [ // Paramètres & Intégrations // --------------------------- { label: 'Configurations', isTitle: true }, - { label: 'Merchant Configs', icon: 'lucideStore', url: '/merchant-configs' }, + { label: 'Merchant Config', icon: 'lucideStore', url: '/merchant-config' }, { label: 'Paramètres Système', icon: 'lucideSettings', url: '/settings' }, { label: 'Intégrations Externes', icon: 'lucidePlug', url: '/integrations' }, diff --git a/src/app/modules/merchant-config/merchant-config.html b/src/app/modules/merchant-config/merchant-config.html new file mode 100644 index 0000000..e8896d6 --- /dev/null +++ b/src/app/modules/merchant-config/merchant-config.html @@ -0,0 +1,335 @@ + + + Gestion des Merchants + + +
+ + + + + + + + @if (configError) { +
+ + {{ configError }} +
+ } + + @if (configSuccess) { +
+ + {{ configSuccess }} +
+ } + + +
+ @for (step of wizardSteps; track $index; let i = $index) { +
+
+ + @if (i === 0) { +
+
+ +
+ + @if (basicInfo.get('name')?.invalid && basicInfo.get('name')?.touched) { +
Le nom du merchant est requis
+ } +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ + @if (basicInfo.get('adresse')?.invalid && basicInfo.get('adresse')?.touched) { +
L'adresse est requise
+ } +
+
+
+ +
+ + @if (basicInfo.get('phone')?.invalid && basicInfo.get('phone')?.touched) { +
Le téléphone est requis et doit être valide
+ } +
+
+
+ } + + + @if (i === 1) { +
+
+
+
Contacts Techniques
+ +
+
+ + @for (contact of technicalContacts.controls; track $index; let idx = $index) { +
+
+
Contact #{{ idx + 1 }}
+ @if (technicalContacts.length > 1) { + + } +
+ +
+
+ + + @if (getContactControl(contact, 'firstName').invalid && getContactControl(contact, 'firstName').touched) { +
Le prénom est requis
+ } +
+
+ + + @if (getContactControl(contact, 'lastName').invalid && getContactControl(contact, 'lastName').touched) { +
Le nom est requis
+ } +
+
+ + + @if (getContactControl(contact, 'phone').invalid && getContactControl(contact, 'phone').touched) { +
Le téléphone est requis
+ } +
+
+ + + @if (getContactControl(contact, 'email').invalid && getContactControl(contact, 'email').touched) { +
L'email est requis et doit être valide
+ } +
+
+
+ } +
+ } + + + @if (i === 2) { +
+
+
+
Configurations Techniques
+ +
+
+ + @for (config of configs.controls; track $index; let idx = $index) { +
+
+
Configuration #{{ idx + 1 }}
+ @if (configs.length > 1) { + + } +
+ +
+
+ + + @if (getConfigControl(config, 'name').invalid && getConfigControl(config, 'name').touched) { +
Le nom est requis
+ } +
+
+ + + @if (getConfigControl(config, 'value').invalid && getConfigControl(config, 'value').touched) { +
La valeur est requise
+ } +
+
+ + + @if (getConfigControl(config, 'operatorId').invalid && getConfigControl(config, 'operatorId').touched) { +
L'opérateur est requis
+ } +
+
+
+ } +
+ } + + + @if (i === 4) { +
+
+
+ + Vérifiez les informations avant de créer le merchant +
+ +
+
+
Récapitulatif
+ +
+
+ Informations de Base:
+ Nom: {{ basicInfo.value.name || 'Non renseigné' }}
+ Description: {{ basicInfo.value.description || 'Non renseigné' }}
+ Adresse: {{ basicInfo.value.adresse || 'Non renseigné' }}
+ Téléphone: {{ basicInfo.value.phone || 'Non renseigné' }} +
+
+ +
+
+ Contacts Techniques: + @for (contact of technicalContacts.value; track $index; let idx = $index) { +
+ Contact #{{ idx + 1 }}: + {{ contact.firstName }} {{ contact.lastName }} - + {{ contact.phone }} - {{ contact.email }} +
+ } +
+
+ +
+
+ Configurations: + @for (config of configs.value; track $index; let idx = $index) { +
+ Configuration #{{ idx + 1 }}: + {{ getConfigTypeName(config.name) }} = {{ config.value }} + ({{ getOperatorName(config.operatorId) }}) +
+ } +
+
+
+
+
+
+ } +
+ + +
+ @if (i > 0) { + + } @else { +
+ } + + @if (i < wizardSteps.length - 1) { + + } @else { + + } +
+
+ } +
+
+
\ 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 new file mode 100644 index 0000000..f7e0e79 --- /dev/null +++ b/src/app/modules/merchant-config/merchant-config.service.ts @@ -0,0 +1,315 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { environment } from '@environments/environment'; +import { Observable, map, catchError, throwError } from 'rxjs'; + +// Import de vos modèles existants +import { UserRole } from '@core/models/dcb-bo-hub-user.model'; + +import { + Merchant, + CreateMerchantDto, + UpdateMerchantDto, + MerchantUser, + AddUserToMerchantDto, + UpdateUserRoleDto, + MerchantConfig, + TechnicalContact, + ApiResponse, + PaginatedResponse, + MerchantStatsResponse, + SearchMerchantsParams, + MerchantStatus +} from '@core/models/merchant-config.model'; + +@Injectable({ providedIn: 'root' }) +export class MerchantConfigService { + private http = inject(HttpClient); + private baseApiUrl = `${environment.configApiUrl}/merchants`; + + // Merchant CRUD Operations + createMerchant(createMerchantDto: CreateMerchantDto): Observable { + return this.http.post>(this.baseApiUrl, createMerchantDto).pipe( + map(response => { + if (!response.success) { + throw new Error(response.error || 'Failed to create merchant'); + } + return response.data!; + }), + catchError(error => { + console.error('Error creating merchant:', error); + return throwError(() => error); + }) + ); + } + + getMerchants(page: number = 1, limit: number = 10, params?: SearchMerchantsParams): Observable> { + let httpParams = new HttpParams() + .set('page', page.toString()) + .set('limit', limit.toString()); + + if (params?.query) { + httpParams = httpParams.set('query', params.query); + } + if (params?.status) { + httpParams = httpParams.set('status', params.status); + } + + return this.http.get>>(this.baseApiUrl, { params: httpParams }).pipe( + map(response => { + if (!response.success) { + throw new Error(response.error || 'Failed to load merchants'); + } + return response.data!; + }), + catchError(error => { + console.error('Error loading merchants:', error); + return throwError(() => error); + }) + ); + } + + getMerchantById(id: number): Observable { + return this.http.get>(`${this.baseApiUrl}/${id}`).pipe( + map(response => { + if (!response.success) { + throw new Error(response.error || 'Failed to load merchant'); + } + return response.data!; + }), + catchError(error => { + console.error(`Error loading merchant ${id}:`, error); + return throwError(() => error); + }) + ); + } + + updateMerchant(id: number, updateMerchantDto: UpdateMerchantDto): Observable { + return this.http.patch>(`${this.baseApiUrl}/${id}`, updateMerchantDto).pipe( + map(response => { + if (!response.success) { + throw new Error(response.error || 'Failed to update merchant'); + } + return response.data!; + }), + catchError(error => { + console.error(`Error updating merchant ${id}:`, error); + return throwError(() => error); + }) + ); + } + + deleteMerchant(id: number): Observable { + return this.http.delete>(`${this.baseApiUrl}/${id}`).pipe( + map(response => { + if (!response.success) { + throw new Error(response.error || 'Failed to delete merchant'); + } + }), + catchError(error => { + console.error(`Error deleting merchant ${id}:`, error); + return throwError(() => error); + }) + ); + } + + // User Management + addUserToMerchant(addUserDto: AddUserToMerchantDto): Observable { + return this.http.post>(`${this.baseApiUrl}/users`, addUserDto).pipe( + map(response => { + if (!response.success) { + throw new Error(response.error || 'Failed to add user to merchant'); + } + return response.data!; + }), + catchError(error => { + console.error('Error adding user to merchant:', error); + return throwError(() => error); + }) + ); + } + + getMerchantUsers(merchantId: number): Observable { + return this.http.get>(`${this.baseApiUrl}/${merchantId}/users`).pipe( + map(response => { + if (!response.success) { + throw new Error(response.error || 'Failed to load merchant users'); + } + return response.data!; + }), + catchError(error => { + console.error(`Error loading users for merchant ${merchantId}:`, error); + return throwError(() => error); + }) + ); + } + + updateUserRole(merchantId: number, userId: string, updateRoleDto: UpdateUserRoleDto): Observable { + return this.http.patch>( + `${this.baseApiUrl}/${merchantId}/users/${userId}/role`, + updateRoleDto + ).pipe( + map(response => { + if (!response.success) { + throw new Error(response.error || 'Failed to update user role'); + } + return response.data!; + }), + catchError(error => { + console.error(`Error updating user role for merchant ${merchantId}, user ${userId}:`, error); + return throwError(() => error); + }) + ); + } + + removeUserFromMerchant(merchantId: number, userId: string): Observable { + return this.http.delete>(`${this.baseApiUrl}/${merchantId}/users/${userId}`).pipe( + map(response => { + if (!response.success) { + throw new Error(response.error || 'Failed to remove user from merchant'); + } + }), + catchError(error => { + console.error(`Error removing user ${userId} from merchant ${merchantId}:`, error); + return throwError(() => error); + }) + ); + } + + getUserMerchants(userId: string): Observable { + return this.http.get>(`${this.baseApiUrl}/user/${userId}`).pipe( + map(response => { + if (!response.success) { + throw new Error(response.error || 'Failed to load user merchants'); + } + return response.data!; + }), + catchError(error => { + console.error(`Error loading merchants for user ${userId}:`, error); + return throwError(() => error); + }) + ); + } + + // Config Management + addConfigToMerchant(merchantId: number, config: Omit): Observable { + return this.http.post>(`${this.baseApiUrl}/${merchantId}/configs`, config).pipe( + map(response => { + if (!response.success) { + throw new Error(response.error || 'Failed to add config to merchant'); + } + return response.data!; + }), + catchError(error => { + console.error(`Error adding config to merchant ${merchantId}:`, error); + return throwError(() => error); + }) + ); + } + + updateConfig(configId: number, config: Partial): Observable { + return this.http.patch>(`${this.baseApiUrl}/configs/${configId}`, config).pipe( + map(response => { + if (!response.success) { + throw new Error(response.error || 'Failed to update config'); + } + return response.data!; + }), + catchError(error => { + console.error(`Error updating config ${configId}:`, error); + return throwError(() => error); + }) + ); + } + + deleteConfig(configId: number): Observable { + return this.http.delete>(`${this.baseApiUrl}/configs/${configId}`).pipe( + map(response => { + if (!response.success) { + throw new Error(response.error || 'Failed to delete config'); + } + }), + catchError(error => { + console.error(`Error deleting config ${configId}:`, error); + return throwError(() => error); + }) + ); + } + + // Technical Contacts Management + addTechnicalContact(merchantId: number, contact: Omit): Observable { + return this.http.post>(`${this.baseApiUrl}/${merchantId}/contacts`, contact).pipe( + map(response => { + if (!response.success) { + throw new Error(response.error || 'Failed to add technical contact'); + } + return response.data!; + }), + catchError(error => { + console.error(`Error adding technical contact to merchant ${merchantId}:`, error); + return throwError(() => error); + }) + ); + } + + updateTechnicalContact(contactId: number, contact: Partial): Observable { + return this.http.patch>(`${this.baseApiUrl}/contacts/${contactId}`, contact).pipe( + map(response => { + if (!response.success) { + throw new Error(response.error || 'Failed to update technical contact'); + } + return response.data!; + }), + catchError(error => { + console.error(`Error updating technical contact ${contactId}:`, error); + return throwError(() => error); + }) + ); + } + + deleteTechnicalContact(contactId: number): Observable { + return this.http.delete>(`${this.baseApiUrl}/contacts/${contactId}`).pipe( + map(response => { + if (!response.success) { + throw new Error(response.error || 'Failed to delete technical contact'); + } + }), + catchError(error => { + console.error(`Error deleting technical contact ${contactId}:`, error); + return throwError(() => error); + }) + ); + } + + // Statistics + getMerchantStats(): Observable { + return this.http.get>(`${this.baseApiUrl}/stats`).pipe( + map(response => { + if (!response.success) { + throw new Error(response.error || 'Failed to load merchant statistics'); + } + return response.data!; + }), + catchError(error => { + console.error('Error loading merchant statistics:', error); + return throwError(() => error); + }) + ); + } + + // Status Management + updateMerchantStatus(merchantId: number, status: MerchantStatus): Observable { + return this.http.patch>(`${this.baseApiUrl}/${merchantId}/status`, { status }).pipe( + map(response => { + if (!response.success) { + throw new Error(response.error || 'Failed to update merchant status'); + } + return response.data!; + }), + catchError(error => { + console.error(`Error updating status for merchant ${merchantId}:`, error); + return throwError(() => error); + }) + ); + } +} \ No newline at end of file diff --git a/src/app/modules/merchant-config/merchant-config.ts b/src/app/modules/merchant-config/merchant-config.ts new file mode 100644 index 0000000..ccf6904 --- /dev/null +++ b/src/app/modules/merchant-config/merchant-config.ts @@ -0,0 +1,294 @@ +import { ChangeDetectorRef, Component, inject, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule, FormBuilder, Validators, FormArray, FormGroup, FormControl } from '@angular/forms'; +import { NgIcon } from '@ng-icons/core'; +import { NgbProgressbarModule } from '@ng-bootstrap/ng-bootstrap'; +import { UiCard } from '@app/components/ui-card'; +import { MerchantConfigService } from './merchant-config.service'; +import { CreateMerchantDto, MerchantConfig as MerchantConfigModel, TechnicalContact, MerchantUtils, ConfigType, Operator } from '@core/models/merchant-config.model'; +import { UserRole } from '@core/models/dcb-bo-hub-user.model'; +import { firstValueFrom, Subject } from 'rxjs'; +import { RoleManagementService } from '@core/services/hub-users-roles-management.service'; + +@Component({ + selector: 'app-merchant-config', + standalone: true, + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + NgIcon, + NgbProgressbarModule, + UiCard + ], + templateUrl: './merchant-config.html' +}) +export class MerchantConfig implements OnInit { + private fb = inject(FormBuilder); + private merchantConfigService = inject(MerchantConfigService); + protected roleService = inject(RoleManagementService); + private cdRef = inject(ChangeDetectorRef); + private destroy$ = new Subject(); + + // Configuration wizard + currentStep = 0; + wizardSteps = [ + { id: 'basic-info', icon: 'lucideBuilding', title: 'Informations de Base', subtitle: 'Détails du merchant' }, + { id: 'technical-contacts', icon: 'lucideUsers', title: 'Contacts Techniques', subtitle: 'Personnes de contact' }, + { id: 'configurations', icon: 'lucideSettings', title: 'Configurations', subtitle: 'Paramètres techniques' }, + { id: 'review', icon: 'lucideCheckCircle', title: 'Validation', subtitle: 'Vérification finale' } + ]; + + configLoading = false; + configError = ''; + configSuccess = ''; + + // Formulaires + merchantForm = this.fb.group({ + basicInfo: this.fb.group({ + name: ['', [Validators.required, Validators.minLength(2)]], + logo: [''], + description: [''], + adresse: ['', [Validators.required]], + phone: ['', [Validators.required, Validators.pattern(/^\+?[0-9\s\-\(\)]+$/)]] + }), + technicalContacts: this.fb.array([]), + configs: this.fb.array([]) + }); + + // Opérateurs disponibles + operators = [ + { id: Operator.ORANGE_CI, name: 'Orange CI' }, + { id: Operator.MTN_CI, name: 'MTN CI' }, + { id: Operator.MOOV_CI, name: 'Moov CI' }, + { id: Operator.WAVE, name: 'Wave' } + ]; + + // Types de configuration + configTypes = [ + { name: ConfigType.API_KEY, label: 'Clé API' }, + { name: ConfigType.SECRET_KEY, label: 'Clé Secrète' }, + { name: ConfigType.WEBHOOK_URL, label: 'URL Webhook' }, + { name: ConfigType.CALLBACK_URL, label: 'URL Callback' }, + { name: ConfigType.TIMEOUT, label: 'Timeout (ms)' }, + { name: ConfigType.RETRY_COUNT, label: 'Nombre de tentatives' }, + { name: ConfigType.CUSTOM, label: 'Personnalisé' } + ]; + + // Rôles disponibles pour les merchants (utilisation de vos rôles existants) + availableMerchantRoles = MerchantUtils.getAvailableMerchantRoles(); + + ngOnInit() { + // Ajouter un contact technique par défaut + this.addTechnicalContact(); + // Ajouter une configuration par défaut + this.addConfig(); + } + + // Navigation du wizard + get progressValue(): number { + return ((this.currentStep + 1) / this.wizardSteps.length) * 100; + } + + nextStep() { + if (this.currentStep < this.wizardSteps.length - 1 && this.isStepValid(this.currentStep)) { + this.currentStep++; + } + } + + previousStep() { + if (this.currentStep > 0) { + this.currentStep--; + } + } + + goToStep(index: number) { + if (this.isStepAccessible(index)) { + this.currentStep = index; + } + } + + isStepAccessible(index: number): boolean { + if (index === 0) return true; + + for (let i = 0; i < index; i++) { + if (!this.isStepValid(i)) { + return false; + } + } + return true; + } + + isStepValid(stepIndex: number): boolean { + switch (stepIndex) { + case 0: // Basic Info + return this.basicInfo.valid; + case 1: // Technical Contacts + return this.technicalContacts.valid && this.technicalContacts.length > 0; + case 2: // Configurations + return this.configs.valid; + case 3: // Review + return this.merchantForm.valid; + default: + return false; + } + } + + // Gestion des contacts techniques + get technicalContacts(): FormArray { + return this.merchantForm.get('technicalContacts') as FormArray; + } + + addTechnicalContact() { + const contactGroup = this.fb.group({ + firstName: ['', [Validators.required]], + lastName: ['', [Validators.required]], + phone: ['', [Validators.required, Validators.pattern(/^\+?[0-9\s\-\(\)]+$/)]], + email: ['', [Validators.required, Validators.email]] + }); + this.technicalContacts.push(contactGroup); + } + + removeTechnicalContact(index: number) { + if (this.technicalContacts.length > 1) { + this.technicalContacts.removeAt(index); + } + } + + getContactControl(contact: any, field: string): FormControl { + return contact.get(field) as FormControl; + } + + // Gestion des configurations + get configs(): FormArray { + return this.merchantForm.get('configs') as FormArray; + } + + addConfig() { + const configGroup = this.fb.group({ + name: ['', [Validators.required]], + value: ['', [Validators.required]], + operatorId: [Operator.ORANGE_CI, [Validators.required]] + }); + this.configs.push(configGroup); + } + + removeConfig(index: number) { + if (this.configs.length > 1) { + this.configs.removeAt(index); + } + } + + getConfigControl(config: any, field: string): FormControl { + return config.get(field) as FormControl; + } + + // Soumission du formulaire + async submitForm() { + if (this.merchantForm.valid) { + this.configLoading = true; + this.configError = ''; + + try { + const formData = this.merchantForm.value; + + const createMerchantDto: CreateMerchantDto = { + name: this.safeString(formData.basicInfo?.name), + logo: this.safeString(formData.basicInfo?.logo), + description: this.safeString(formData.basicInfo?.description), + adresse: this.safeString(formData.basicInfo?.adresse), + phone: this.safeString(formData.basicInfo?.phone), + technicalContacts: (formData.technicalContacts || []).map((contact: any) => ({ + firstName: this.safeString(contact.firstName), + lastName: this.safeString(contact.lastName), + phone: this.safeString(contact.phone), + email: this.safeString(contact.email) + })), + configs: (formData.configs || []).map((config: any) => ({ + name: this.safeString(config.name), + value: this.safeString(config.value), + operatorId: this.safeNumber(config.operatorId) + })) + }; + + // Validation avant envoi + const validationErrors = MerchantUtils.validateMerchantCreation(createMerchantDto); + if (validationErrors.length > 0) { + this.configError = validationErrors.join(', '); + return; + } + + const response = await firstValueFrom( + this.merchantConfigService.createMerchant(createMerchantDto) + ); + + this.configSuccess = `Merchant créé avec succès! ID: ${response.id}`; + this.merchantForm.reset(); + this.currentStep = 0; + + // Réinitialiser les tableaux + this.technicalContacts.clear(); + this.configs.clear(); + this.addTechnicalContact(); + this.addConfig(); + + } catch (error: any) { + this.configError = error.message || 'Erreur lors de la création du merchant'; + console.error('Error creating merchant:', error); + } finally { + this.configLoading = false; + } + } else { + this.configError = 'Veuillez corriger les erreurs dans le formulaire'; + this.markAllFieldsAsTouched(); + } + } + + // Méthodes utilitaires + private safeString(value: string | null | undefined): string { + return value || ''; + } + + private safeNumber(value: number | null | undefined): number { + return value || 0; + } + + private markAllFieldsAsTouched() { + Object.keys(this.merchantForm.controls).forEach(key => { + const control = this.merchantForm.get(key); + if (control instanceof FormGroup) { + Object.keys(control.controls).forEach(subKey => { + control.get(subKey)?.markAsTouched(); + }); + } else if (control instanceof FormArray) { + control.controls.forEach((arrayControl: any) => { + if (arrayControl instanceof FormGroup) { + Object.keys(arrayControl.controls).forEach(subKey => { + arrayControl.get(subKey)?.markAsTouched(); + }); + } + }); + } else { + control?.markAsTouched(); + } + }); + } + + // Getters pour les formulaires + get basicInfo() { + return this.merchantForm.get('basicInfo') as FormGroup; + } + + // Méthodes pour le template + getOperatorName(operatorId: Operator): string { + return MerchantUtils.getOperatorName(operatorId); + } + + getConfigTypeName(configName: string): string { + return MerchantUtils.getConfigTypeName(configName); + } + + getRoleLabel(role: UserRole): string { + return this.roleService.getRoleLabel(role); + } +} \ No newline at end of file diff --git a/src/app/modules/modules.routes.ts b/src/app/modules/modules.routes.ts index 193dca5..3c9f76e 100644 --- a/src/app/modules/modules.routes.ts +++ b/src/app/modules/modules.routes.ts @@ -23,6 +23,7 @@ import { Help } from '@modules/help/help'; import { About } from '@modules/about/about'; import { SubscriptionsManagement } from './subscriptions/subscriptions'; import { SubscriptionPayments } from './subscriptions/subscription-payments/subscription-payments'; +import { MerchantConfig } from './merchant-config/merchant-config'; const routes: Routes = [ // --------------------------- @@ -164,7 +165,7 @@ const routes: Routes = [ }, // --------------------------- - // Partners (existant - gardé pour référence) + // Partners // --------------------------- { path: 'merchant-users-management', @@ -184,6 +185,20 @@ const routes: Routes = [ ] } }, + + // --------------------------- + // Merchant Config + // --------------------------- + { + path: 'merchant-config', + component: MerchantConfig, + canActivate: [authGuard, roleGuard], + data: { + title: 'Merchant Config', + module: 'merchant-config' + } + }, + // --------------------------- // Operators (Admin seulement) // --------------------------- diff --git a/src/environments/environment.preprod.ts b/src/environments/environment.preprod.ts index b156dfd..900df35 100644 --- a/src/environments/environment.preprod.ts +++ b/src/environments/environment.preprod.ts @@ -2,5 +2,6 @@ export const environment = { production: true, localServiceTestApiUrl: "https://backoffice.dcb.pixpay.sn/api/v1", iamApiUrl: "https://api-user-service.dcb.pixpay.sn/api/v1", + configApiUrl: 'https://api-merchant-config-service.dcb.pixpay.sn/api/v1', apiCoreUrl: 'https://api-core-service.dcb.pixpay.sn/api/v1', }; diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index b156dfd..900df35 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -2,5 +2,6 @@ export const environment = { production: true, localServiceTestApiUrl: "https://backoffice.dcb.pixpay.sn/api/v1", iamApiUrl: "https://api-user-service.dcb.pixpay.sn/api/v1", + configApiUrl: 'https://api-merchant-config-service.dcb.pixpay.sn/api/v1', apiCoreUrl: 'https://api-core-service.dcb.pixpay.sn/api/v1', }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index fe3670e..eb55f4c 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -2,5 +2,6 @@ export const environment = { production: false, localServiceTestApiUrl: "http://localhost:4200/api/v1", iamApiUrl: "http://localhost:3000/api/v1", + configApiUrl: 'https://api-merchant-config-service.dcb.pixpay.sn/api/v1', apiCoreUrl: 'https://api-core-service.dcb.pixpay.sn/api/v1', } \ No newline at end of file