feat: add DCB User Service API - Authentication system with KEYCLOAK - Modular architecture with services for each feature
This commit is contained in:
parent
a4834002df
commit
10a272fb85
@ -116,13 +116,12 @@ export interface ApiMerchant {
|
||||
adresse: string;
|
||||
phone: string;
|
||||
configs: ApiMerchantConfig[];
|
||||
merchantUsers: ApiMerchantUser[];
|
||||
users: ApiMerchantUser[];
|
||||
technicalContacts: ApiTechnicalContact[];
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
// === DTOs CRUD ===
|
||||
// === DTOs CRUD ===
|
||||
export interface CreateMerchantDto {
|
||||
name: string;
|
||||
@ -153,9 +152,6 @@ export interface UpdateUserRoleDto {
|
||||
role: UserRole;
|
||||
}
|
||||
|
||||
export interface UpdateUserRoleDto {
|
||||
role: UserRole; // Utilisation de vos rôles existants
|
||||
}
|
||||
|
||||
// === RÉPONSES API ===
|
||||
export interface ApiResponse<T> {
|
||||
|
||||
@ -43,10 +43,15 @@ export class RoleManagementService {
|
||||
}))
|
||||
})),
|
||||
tap(roles => this.availableRoles$.next(roles)),
|
||||
catchError(error => this.getDefaultHubRoles(error))
|
||||
catchError(error => {
|
||||
console.error('Error loading hub roles:', error);
|
||||
// On renvoie un observable valide pour éviter le crash
|
||||
return of({ roles: [] } as AvailableRolesWithPermissions);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Charge les rôles Marchands disponibles
|
||||
*/
|
||||
@ -59,56 +64,14 @@ export class RoleManagementService {
|
||||
}))
|
||||
})),
|
||||
tap(roles => this.availableRoles$.next(roles)),
|
||||
catchError(error => this.getDefaultMerchantRoles(error))
|
||||
catchError(error => {
|
||||
console.error('Error loading merchant roles:', error);
|
||||
// On renvoie un observable valide pour éviter le crash
|
||||
return of({ roles: [] } as AvailableRolesWithPermissions);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rôles Hub par défaut en cas d'erreur
|
||||
*/
|
||||
private getDefaultHubRoles(error?: any): Observable<AvailableRolesWithPermissions> {
|
||||
console.error('Error loading hub roles:', error);
|
||||
const defaultRoles: AvailableRolesWithPermissions = {
|
||||
roles: [
|
||||
UserRole.DCB_ADMIN,
|
||||
UserRole.DCB_SUPPORT,
|
||||
UserRole.DCB_PARTNER
|
||||
].map(role => ({
|
||||
value: role,
|
||||
label: this.getRoleLabel(role),
|
||||
description: this.getRoleDescription(role),
|
||||
allowedForCreation: true,
|
||||
userType: UserType.HUB,
|
||||
permissions: this.getPermissionsForRole(role)
|
||||
}))
|
||||
};
|
||||
this.availableRoles$.next(defaultRoles);
|
||||
return of(defaultRoles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rôles Marchands par défaut en cas d'erreur
|
||||
*/
|
||||
private getDefaultMerchantRoles(error?: any): Observable<AvailableRolesWithPermissions> {
|
||||
console.error('Error loading merchant roles:', error);
|
||||
const defaultRoles: AvailableRolesWithPermissions = {
|
||||
roles: [
|
||||
UserRole.DCB_PARTNER_ADMIN,
|
||||
UserRole.DCB_PARTNER_MANAGER,
|
||||
UserRole.DCB_PARTNER_SUPPORT
|
||||
].map(role => ({
|
||||
value: role,
|
||||
label: this.getRoleLabel(role),
|
||||
description: this.getRoleDescription(role),
|
||||
allowedForCreation: true,
|
||||
userType: UserType.MERCHANT_PARTNER,
|
||||
permissions: this.getPermissionsForRole(role)
|
||||
}))
|
||||
};
|
||||
this.availableRoles$.next(defaultRoles);
|
||||
return of(defaultRoles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit le rôle de l'utilisateur courant
|
||||
*/
|
||||
@ -192,10 +155,10 @@ export class RoleManagementService {
|
||||
canDeleteUsers: true,
|
||||
canManageRoles: true,
|
||||
canViewStats: true,
|
||||
canManageMerchants: true,
|
||||
canManageMerchants: false,
|
||||
canAccessAdmin: false,
|
||||
canAccessSupport: false,
|
||||
canAccessPartner: true,
|
||||
canAccessPartner: false,
|
||||
assignableRoles: [UserRole.DCB_PARTNER_MANAGER, UserRole.DCB_PARTNER_SUPPORT]
|
||||
};
|
||||
|
||||
|
||||
@ -51,8 +51,6 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
|
||||
@Output() openCreateMerchantModal = new EventEmitter<void>();
|
||||
@Output() editMerchantRequested = new EventEmitter<Merchant>();
|
||||
@Output() deleteMerchantRequested = new EventEmitter<Merchant>();
|
||||
@Output() activateMerchantRequested = new EventEmitter<Merchant>();
|
||||
@Output() deactivateMerchantRequested = new EventEmitter<Merchant>();
|
||||
|
||||
// Données
|
||||
allMerchants: Merchant[] = [];
|
||||
@ -259,14 +257,6 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
|
||||
this.deleteMerchantRequested.emit(merchant);
|
||||
}
|
||||
|
||||
activateMerchant(merchant: Merchant) {
|
||||
this.activateMerchantRequested.emit(merchant);
|
||||
}
|
||||
|
||||
deactivateMerchant(merchant: Merchant) {
|
||||
this.deactivateMerchantRequested.emit(merchant);
|
||||
}
|
||||
|
||||
// ==================== FILTRES ET RECHERCHE ====================
|
||||
|
||||
onSearch() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -31,61 +31,21 @@
|
||||
}
|
||||
</small>
|
||||
</div>
|
||||
@if (canCreateMerchants) {
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
(click)="openCreateMerchantModal()"
|
||||
>
|
||||
<ng-icon name="lucidePlus" class="me-1"></ng-icon>
|
||||
Nouveau Marchand
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Sélection du marchand pour les ADMIN -->
|
||||
@if (showMerchantPartnerField) {
|
||||
<!-- Message de succès -->
|
||||
@if (successMessage) {
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body py-2">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label mb-0">
|
||||
<strong>Filtrer par Marchand :</strong>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
[(ngModel)]="selectedMerchantPartnerId"
|
||||
(change)="onMerchantSelectionChange(selectedMerchantPartnerId)"
|
||||
>
|
||||
<option value="">Tous les marchands</option>
|
||||
@for (partner of merchantPartners; track partner.id) {
|
||||
<option [value]="partner.merchantPartnerId || partner.id">
|
||||
{{ partner.username }}
|
||||
@if (partner.firstName || partner.lastName) {
|
||||
- {{ partner.firstName }} {{ partner.lastName }}
|
||||
}
|
||||
@if (!partner.enabled) {
|
||||
<span class="badge bg-warning ms-1">Inactif</span>
|
||||
}
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
@if (loadingMerchantPartners) {
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Chargement...</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-success alert-dismissible fade show">
|
||||
<div class="d-flex align-items-center">
|
||||
<ng-icon name="lucideCheckCircle" class="me-2"></ng-icon>
|
||||
<div>{{ successMessage }}</div>
|
||||
<button type="button" class="btn-close ms-auto" (click)="successMessage = ''"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -108,7 +68,6 @@
|
||||
<span class="d-none d-md-inline-block align-middle">Marchands</span>
|
||||
</a>
|
||||
<ng-template ngbNavContent>
|
||||
<!-- COMPOSANT CORRIGÉ avec les bonnes propriétés -->
|
||||
<app-merchant-config-list
|
||||
#merchantConfigsList
|
||||
[canCreateMerchants]="canCreateMerchants"
|
||||
@ -130,6 +89,8 @@
|
||||
@if (selectedMerchantId) {
|
||||
<app-merchant-config-view
|
||||
[merchantId]="selectedMerchantId"
|
||||
(openCreateMerchantModal)="openCreateMerchantModal()"
|
||||
(editMerchantRequested)="onEditMerchantRequested($event)"
|
||||
(editConfigRequested)="onEditConfigRequested($event)"
|
||||
(back)="backToList()"
|
||||
/>
|
||||
@ -511,7 +472,16 @@
|
||||
|
||||
@if (selectedMerchantForEdit) {
|
||||
<form (ngSubmit)="updateMerchant()" #editForm="ngForm">
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- INFORMATIONS GÉNÉRALES -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12">
|
||||
<h6 class="border-bottom pb-2 text-primary">
|
||||
<ng-icon name="lucideBuilding" class="me-2"></ng-icon>
|
||||
Informations Générales
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Nom du marchand <span class="text-danger">*</span></label>
|
||||
<input
|
||||
@ -521,6 +491,7 @@
|
||||
name="name"
|
||||
required
|
||||
[disabled]="updatingMerchant"
|
||||
placeholder="Ex: Boutique ABC"
|
||||
>
|
||||
</div>
|
||||
|
||||
@ -532,6 +503,7 @@
|
||||
[(ngModel)]="selectedMerchantForEdit.logo"
|
||||
name="logo"
|
||||
[disabled]="updatingMerchant"
|
||||
placeholder="https://exemple.com/logo.png"
|
||||
>
|
||||
</div>
|
||||
|
||||
@ -543,6 +515,7 @@
|
||||
name="description"
|
||||
[disabled]="updatingMerchant"
|
||||
rows="2"
|
||||
placeholder="Description du marchand"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
@ -555,6 +528,7 @@
|
||||
name="adresse"
|
||||
required
|
||||
[disabled]="updatingMerchant"
|
||||
placeholder="Adresse complète"
|
||||
>
|
||||
</div>
|
||||
|
||||
@ -567,12 +541,226 @@
|
||||
name="phone"
|
||||
required
|
||||
[disabled]="updatingMerchant"
|
||||
placeholder="+XX X XX XX XX XX"
|
||||
>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer mt-4">
|
||||
<!-- CONFIGURATIONS TECHNIQUES -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center border-bottom pb-2">
|
||||
<h6 class="mb-0 text-primary">
|
||||
<ng-icon name="lucideSettings" class="me-2"></ng-icon>
|
||||
Configurations Techniques
|
||||
</h6>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
(click)="addConfigInEdit()"
|
||||
[disabled]="updatingMerchant"
|
||||
>
|
||||
<ng-icon name="lucidePlus" class="me-1"></ng-icon>
|
||||
Ajouter une configuration
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!selectedMerchantForEdit.configs || selectedMerchantForEdit.configs.length === 0) {
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning">
|
||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||
Au moins une configuration est requise
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Liste des configurations -->
|
||||
@for (config of selectedMerchantForEdit.configs; track trackByConfigId($index, config); let i = $index) {
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-light py-2 d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center">
|
||||
<ng-icon [name]="getConfigTypeIconSafe(config.name)" class="me-2 text-primary"></ng-icon>
|
||||
<span class="fw-semibold">Configuration {{ i + 1 }}</span>
|
||||
</div>
|
||||
@if (selectedMerchantForEdit.configs.length > 1) {
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
(click)="removeConfigInEdit(i)"
|
||||
[disabled]="updatingMerchant"
|
||||
>
|
||||
<ng-icon name="lucideTrash2" class="me-1"></ng-icon>
|
||||
Supprimer
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Type <span class="text-danger">*</span></label>
|
||||
<select
|
||||
class="form-select"
|
||||
[(ngModel)]="config.name"
|
||||
[name]="'editConfigType_' + i"
|
||||
required
|
||||
[disabled]="updatingMerchant"
|
||||
>
|
||||
<option value="" disabled>Sélectionnez un type</option>
|
||||
@for (type of configTypes; track type.name) {
|
||||
<option [value]="type.name">{{ type.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Opérateur <span class="text-danger">*</span></label>
|
||||
<select
|
||||
class="form-select"
|
||||
[(ngModel)]="config.operatorId"
|
||||
[name]="'editOperatorId_' + i"
|
||||
required
|
||||
[disabled]="updatingMerchant"
|
||||
>
|
||||
<option value="" disabled>Sélectionnez un opérateur</option>
|
||||
@for (operator of operators; track operator.id) {
|
||||
<option [value]="operator.id">{{ operator.name }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Valeur <span class="text-danger">*</span></label>
|
||||
<textarea
|
||||
class="form-control font-monospace"
|
||||
[(ngModel)]="config.value"
|
||||
[name]="'editValue_' + i"
|
||||
required
|
||||
[disabled]="updatingMerchant"
|
||||
rows="3"
|
||||
placeholder="Valeur de configuration"
|
||||
></textarea>
|
||||
@if (isSensitiveConfig(config)) {
|
||||
<div class="form-text text-warning">
|
||||
<ng-icon name="lucideShield" class="me-1"></ng-icon>
|
||||
Cette configuration contient des informations sensibles
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- CONTACTS TECHNIQUES -->
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center border-bottom pb-2">
|
||||
<h6 class="mb-0 text-primary">
|
||||
<ng-icon name="lucideUsers" class="me-2"></ng-icon>
|
||||
Contacts Techniques
|
||||
</h6>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
(click)="addTechnicalContactInEdit()"
|
||||
[disabled]="updatingMerchant"
|
||||
>
|
||||
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
|
||||
Ajouter un contact
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!selectedMerchantForEdit.technicalContacts || selectedMerchantForEdit.technicalContacts.length === 0) {
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning">
|
||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||
Au moins un contact technique est requis
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Liste des contacts techniques -->
|
||||
@for (contact of selectedMerchantForEdit.technicalContacts; track trackByContactId($index, contact); let i = $index) {
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-light py-2 d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center">
|
||||
<ng-icon name="lucideUser" class="me-2 text-primary"></ng-icon>
|
||||
<span class="fw-semibold">Contact {{ i + 1 }}</span>
|
||||
</div>
|
||||
@if (selectedMerchantForEdit.technicalContacts.length > 1) {
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
(click)="removeTechnicalContactInEdit(i)"
|
||||
[disabled]="updatingMerchant"
|
||||
>
|
||||
<ng-icon name="lucideTrash2" class="me-1"></ng-icon>
|
||||
Supprimer
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Prénom <span class="text-danger">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
[(ngModel)]="contact.firstName"
|
||||
[name]="'editFirstName_' + i"
|
||||
required
|
||||
[disabled]="updatingMerchant"
|
||||
placeholder="Prénom"
|
||||
>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Nom <span class="text-danger">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
[(ngModel)]="contact.lastName"
|
||||
[name]="'editLastName_' + i"
|
||||
required
|
||||
[disabled]="updatingMerchant"
|
||||
placeholder="Nom"
|
||||
>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Téléphone <span class="text-danger">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
[(ngModel)]="contact.phone"
|
||||
[name]="'editPhone_' + i"
|
||||
required
|
||||
[disabled]="updatingMerchant"
|
||||
placeholder="+XX X XX XX XX XX"
|
||||
>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Email <span class="text-danger">*</span></label>
|
||||
<input
|
||||
type="email"
|
||||
class="form-control"
|
||||
[(ngModel)]="contact.email"
|
||||
[name]="'editEmail_' + i"
|
||||
required
|
||||
[disabled]="updatingMerchant"
|
||||
placeholder="email@exemple.com"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="modal-footer mt-4 border-top pt-3">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-light"
|
||||
@ -585,7 +773,9 @@
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
[disabled]="!editForm.form.valid || updatingMerchant"
|
||||
[disabled]="!editForm.form.valid || updatingMerchant ||
|
||||
!selectedMerchantForEdit.technicalContacts.length ||
|
||||
!selectedMerchantForEdit.configs.length"
|
||||
>
|
||||
@if (updatingMerchant) {
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status">
|
||||
@ -594,7 +784,7 @@
|
||||
Mise à jour...
|
||||
} @else {
|
||||
<ng-icon name="lucideSave" class="me-1"></ng-icon>
|
||||
Enregistrer
|
||||
Enregistrer les modifications
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { HttpClient, HttpParams, HttpErrorResponse } from '@angular/common/http';
|
||||
import { environment } from '@environments/environment';
|
||||
import { Observable, map, catchError, throwError } from 'rxjs';
|
||||
import { Observable, map, catchError, throwError, retry, timeout } from 'rxjs';
|
||||
|
||||
// Import de vos modèles existants
|
||||
import { UserRole } from '@core/models/dcb-bo-hub-user.model';
|
||||
@ -25,24 +25,33 @@ import {
|
||||
ApiMerchantUser
|
||||
} from '@core/models/merchant-config.model';
|
||||
|
||||
// SERVICE DE CONVERSION
|
||||
import { MerchantDataAdapter } from './merchant-data-adapter.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class MerchantConfigService {
|
||||
private http = inject(HttpClient);
|
||||
private dataAdapter = inject(MerchantDataAdapter);
|
||||
private baseApiUrl = `${environment.configApiUrl}/merchants`;
|
||||
|
||||
// Merchant CRUD Operations
|
||||
private readonly REQUEST_TIMEOUT = 30000;
|
||||
private readonly MAX_RETRIES = 2;
|
||||
|
||||
// ==================== MERCHANT CRUD OPERATIONS ====================
|
||||
|
||||
createMerchant(createMerchantDto: CreateMerchantDto): Observable<Merchant> {
|
||||
return this.http.post<ApiMerchant>(this.baseApiUrl, createMerchantDto).pipe(
|
||||
const apiDto = this.dataAdapter.convertCreateMerchantToApi(createMerchantDto);
|
||||
|
||||
console.log('📤 Creating merchant:', apiDto);
|
||||
|
||||
return this.http.post<ApiMerchant>(this.baseApiUrl, apiDto).pipe(
|
||||
timeout(this.REQUEST_TIMEOUT),
|
||||
retry(this.MAX_RETRIES),
|
||||
map(apiMerchant => {
|
||||
console.log('Merchant created successfully:', apiMerchant);
|
||||
return this.mapApiMerchantToMerchant(apiMerchant);
|
||||
console.log('✅ Merchant created successfully:', apiMerchant);
|
||||
return this.dataAdapter.convertApiMerchantToFrontend(apiMerchant);
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error('Error creating merchant:', error);
|
||||
return throwError(() => new Error(
|
||||
error.error?.message || error.message || 'An unexpected error occurred while creating merchant'
|
||||
));
|
||||
})
|
||||
catchError(error => this.handleError('createMerchant', error))
|
||||
);
|
||||
}
|
||||
|
||||
@ -52,328 +61,313 @@ export class MerchantConfigService {
|
||||
.set('limit', limit.toString());
|
||||
|
||||
if (params?.query) {
|
||||
httpParams = httpParams.set('query', params.query);
|
||||
httpParams = httpParams.set('query', params.query.trim());
|
||||
}
|
||||
// L'API retourne directement un tableau de merchants
|
||||
return this.http.get<ApiMerchant[]>(this.baseApiUrl, { params: httpParams }).pipe(
|
||||
|
||||
console.log(`📥 Loading merchants page ${page}, limit ${limit}`, params);
|
||||
|
||||
return this.http.get<ApiMerchant[]>(this.baseApiUrl, {
|
||||
params: httpParams
|
||||
}).pipe(
|
||||
timeout(this.REQUEST_TIMEOUT),
|
||||
retry(this.MAX_RETRIES),
|
||||
map(apiMerchants => {
|
||||
const total = apiMerchants.length;
|
||||
const total = apiMerchants.length;
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
// Appliquer la pagination côté client
|
||||
const startIndex = (page - 1) * limit;
|
||||
const endIndex = startIndex + limit;
|
||||
const paginatedItems = apiMerchants.slice(startIndex, endIndex);
|
||||
|
||||
return {
|
||||
items: paginatedItems.map(apiMerchant => this.mapApiMerchantToMerchant(apiMerchant)),
|
||||
const response: PaginatedResponse<Merchant> = {
|
||||
items: paginatedItems.map(apiMerchant =>
|
||||
this.dataAdapter.convertApiMerchantToFrontend(apiMerchant)
|
||||
),
|
||||
total: total,
|
||||
page: page,
|
||||
limit: limit,
|
||||
totalPages: totalPages
|
||||
};
|
||||
|
||||
console.log(`✅ Loaded ${response.items.length} merchants`);
|
||||
return response;
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error('Error loading merchants:', error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
catchError(error => this.handleError('getMerchants', error))
|
||||
);
|
||||
}
|
||||
|
||||
getAllMerchants(params?: SearchMerchantsParams): Observable<Merchant[]> {
|
||||
let httpParams = new HttpParams();
|
||||
|
||||
if (params?.query) {
|
||||
httpParams = httpParams.set('query', params.query.trim());
|
||||
}
|
||||
|
||||
return this.http.get<ApiMerchant[]>(this.baseApiUrl, { params: httpParams }).pipe(
|
||||
timeout(this.REQUEST_TIMEOUT),
|
||||
map(apiMerchants =>
|
||||
apiMerchants.map(merchant =>
|
||||
this.dataAdapter.convertApiMerchantToFrontend(merchant)
|
||||
)
|
||||
),
|
||||
catchError(error => this.handleError('getAllMerchants', error))
|
||||
);
|
||||
}
|
||||
|
||||
getMerchantById(id: string): Observable<Merchant> {
|
||||
const numericId = parseInt(id);
|
||||
// L'API retourne directement l'objet merchant
|
||||
const numericId = this.convertIdToNumber(id);
|
||||
|
||||
console.log(`📥 Loading merchant ${id}`);
|
||||
|
||||
return this.http.get<ApiMerchant>(`${this.baseApiUrl}/${numericId}`).pipe(
|
||||
timeout(this.REQUEST_TIMEOUT),
|
||||
retry(this.MAX_RETRIES),
|
||||
map(apiMerchant => {
|
||||
return this.mapApiMerchantToMerchant(apiMerchant);
|
||||
console.log(`✅ Merchant ${id} loaded successfully`);
|
||||
return this.dataAdapter.convertApiMerchantToFrontend(apiMerchant);
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(`Error loading merchant ${id}:`, error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
catchError(error => this.handleError('getMerchantById', error, { merchantId: id }))
|
||||
);
|
||||
}
|
||||
|
||||
updateMerchant(id: string, updateMerchantDto: UpdateMerchantDto): Observable<Merchant> {
|
||||
const numericId = parseInt(id);
|
||||
// L'API retourne directement l'objet mis à jour
|
||||
return this.http.patch<ApiMerchant>(`${this.baseApiUrl}/${numericId}`, updateMerchantDto).pipe(
|
||||
const numericId = this.convertIdToNumber(id);
|
||||
|
||||
const apiDto = this.dataAdapter.convertUpdateMerchantToApi(updateMerchantDto);
|
||||
|
||||
console.log(`📤 Updating merchant ${id}:`, apiDto);
|
||||
|
||||
return this.http.patch<ApiMerchant>(`${this.baseApiUrl}/${numericId}`, apiDto).pipe(
|
||||
timeout(this.REQUEST_TIMEOUT),
|
||||
map(apiMerchant => {
|
||||
return this.mapApiMerchantToMerchant(apiMerchant);
|
||||
console.log(`✅ Merchant ${id} updated successfully`);
|
||||
return this.dataAdapter.convertApiMerchantToFrontend(apiMerchant);
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(`Error updating merchant ${id}:`, error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
catchError(error => this.handleError('updateMerchant', error, { merchantId: id }))
|
||||
);
|
||||
}
|
||||
|
||||
deleteMerchant(id: string): Observable<void> {
|
||||
const numericId = parseInt(id);
|
||||
// L'API ne retourne probablement rien ou un simple message
|
||||
const numericId = this.convertIdToNumber(id);
|
||||
|
||||
console.log(`🗑️ Deleting merchant ${id}`);
|
||||
|
||||
return this.http.delete<void>(`${this.baseApiUrl}/${numericId}`).pipe(
|
||||
timeout(this.REQUEST_TIMEOUT),
|
||||
map(() => {
|
||||
console.log(`✅ Merchant ${id} deleted successfully`);
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(`Error deleting merchant ${id}:`, error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
catchError(error => this.handleError('deleteMerchant', error, { merchantId: id }))
|
||||
);
|
||||
}
|
||||
|
||||
// User Management
|
||||
// ==================== USER MANAGEMENT ====================
|
||||
|
||||
addUserToMerchant(addUserDto: AddUserToMerchantDto): Observable<MerchantUser> {
|
||||
// Convertir merchantPartnerId en number pour l'API
|
||||
// CONVERSION AVEC L'ADAPTER
|
||||
const apiDto = {
|
||||
...addUserDto,
|
||||
merchantPartnerId: addUserDto.merchantPartnerId ? parseInt(addUserDto.merchantPartnerId) : undefined
|
||||
merchantPartnerId: addUserDto.merchantPartnerId ?
|
||||
this.convertIdToNumber(addUserDto.merchantPartnerId) : undefined
|
||||
};
|
||||
|
||||
// L'API retourne directement l'utilisateur ajouté
|
||||
console.log('📤 Adding user to merchant:', apiDto);
|
||||
|
||||
return this.http.post<ApiMerchantUser>(`${this.baseApiUrl}/users`, apiDto).pipe(
|
||||
timeout(this.REQUEST_TIMEOUT),
|
||||
map(apiUser => {
|
||||
return this.mapApiUserToUser(apiUser);
|
||||
console.log('✅ User added to merchant successfully');
|
||||
// ✅ UTILISATION DE L'ADAPTER
|
||||
return this.dataAdapter.convertApiUserToFrontend(apiUser);
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error('Error adding user to merchant:', error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
catchError(error => this.handleError('addUserToMerchant', error))
|
||||
);
|
||||
}
|
||||
|
||||
getMerchantUsers(merchantId: string): Observable<MerchantUser[]> {
|
||||
const numericMerchantId = parseInt(merchantId);
|
||||
// Option 1: Si vous avez un endpoint spécifique pour les users
|
||||
// return this.http.get<ApiMerchantUser[]>(`${this.baseApiUrl}/${numericMerchantId}/users`).pipe(
|
||||
const numericMerchantId = this.convertIdToNumber(merchantId);
|
||||
|
||||
// Option 2: Récupérer le merchant complet et extraire les users
|
||||
return this.http.get<ApiMerchant>(`${this.baseApiUrl}/${numericMerchantId}`).pipe(
|
||||
timeout(this.REQUEST_TIMEOUT),
|
||||
map(apiMerchant => {
|
||||
// Retourner les users mappés depuis merchantUsers
|
||||
return (apiMerchant.merchantUsers || []).map(user => this.mapApiUserToUser(user));
|
||||
return (apiMerchant.users || []).map(user =>
|
||||
// ✅ UTILISATION DE L'ADAPTER
|
||||
this.dataAdapter.convertApiUserToFrontend(user)
|
||||
);
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(`Error loading users for merchant ${merchantId}:`, error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
catchError(error => this.handleError('getMerchantUsers', error, { merchantId }))
|
||||
);
|
||||
}
|
||||
|
||||
updateUserRole(merchantId: string, userId: string, updateRoleDto: UpdateUserRoleDto): Observable<MerchantUser> {
|
||||
const numericMerchantId = parseInt(merchantId);
|
||||
// L'API retourne directement l'utilisateur mis à jour
|
||||
const numericMerchantId = this.convertIdToNumber(merchantId);
|
||||
|
||||
return this.http.patch<ApiMerchantUser>(
|
||||
`${this.baseApiUrl}/${numericMerchantId}/users/${userId}/role`,
|
||||
updateRoleDto
|
||||
).pipe(
|
||||
timeout(this.REQUEST_TIMEOUT),
|
||||
map(apiUser => {
|
||||
return this.mapApiUserToUser(apiUser);
|
||||
console.log(`✅ User ${userId} role updated successfully`);
|
||||
// ✅ UTILISATION DE L'ADAPTER
|
||||
return this.dataAdapter.convertApiUserToFrontend(apiUser);
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(`Error updating user role for merchant ${merchantId}, user ${userId}:`, error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
catchError(error => this.handleError('updateUserRole', error, { merchantId, userId }))
|
||||
);
|
||||
}
|
||||
|
||||
removeUserFromMerchant(merchantId: string, userId: string): Observable<void> {
|
||||
const numericMerchantId = parseInt(merchantId);
|
||||
// L'API ne retourne probablement rien
|
||||
const numericMerchantId = this.convertIdToNumber(merchantId);
|
||||
|
||||
return this.http.delete<void>(`${this.baseApiUrl}/${numericMerchantId}/users/${userId}`).pipe(
|
||||
timeout(this.REQUEST_TIMEOUT),
|
||||
map(() => {
|
||||
console.log(`✅ User ${userId} removed from merchant ${merchantId} successfully`);
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(`Error removing user ${userId} from merchant ${merchantId}:`, error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
catchError(error => this.handleError('removeUserFromMerchant', error, { merchantId, userId }))
|
||||
);
|
||||
}
|
||||
|
||||
getUserMerchants(userId: string): Observable<Merchant[]> {
|
||||
// L'API retourne directement un tableau de merchants
|
||||
return this.http.get<ApiMerchant[]>(`${this.baseApiUrl}/user/${userId}`).pipe(
|
||||
timeout(this.REQUEST_TIMEOUT),
|
||||
map(apiMerchants => {
|
||||
return apiMerchants.map(merchant => this.mapApiMerchantToMerchant(merchant));
|
||||
return apiMerchants.map(merchant =>
|
||||
this.dataAdapter.convertApiMerchantToFrontend(merchant)
|
||||
);
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(`Error loading merchants for user ${userId}:`, error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
catchError(error => this.handleError('getUserMerchants', error, { userId }))
|
||||
);
|
||||
}
|
||||
|
||||
// Config Management
|
||||
// ==================== CONFIG MANAGEMENT ====================
|
||||
|
||||
addConfigToMerchant(merchantId: string, config: Omit<MerchantConfig, 'id' | 'merchantPartnerId'>): Observable<MerchantConfig> {
|
||||
const numericMerchantId = parseInt(merchantId);
|
||||
const numericMerchantId = this.convertIdToNumber(merchantId);
|
||||
|
||||
// CONVERSION AVEC L'ADAPTER
|
||||
const apiConfig: Omit<ApiMerchantConfig, 'id'> = {
|
||||
...config,
|
||||
operatorId: config.operatorId,
|
||||
merchantPartnerId: numericMerchantId
|
||||
};
|
||||
|
||||
// L'API retourne directement la configuration créée
|
||||
console.log(`📤 Adding config to merchant ${merchantId}:`, apiConfig);
|
||||
|
||||
return this.http.post<ApiMerchantConfig>(`${this.baseApiUrl}/${numericMerchantId}/configs`, apiConfig).pipe(
|
||||
timeout(this.REQUEST_TIMEOUT),
|
||||
map(apiConfig => {
|
||||
return this.mapApiConfigToConfig(apiConfig);
|
||||
console.log('✅ Config added to merchant successfully');
|
||||
// ✅ UTILISATION DE L'ADAPTER
|
||||
return this.dataAdapter.convertApiConfigToFrontend(apiConfig);
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(`Error adding config to merchant ${merchantId}:`, error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
catchError(error => this.handleError('addConfigToMerchant', error, { merchantId }))
|
||||
);
|
||||
}
|
||||
|
||||
updateConfig(configId: string, config: Partial<MerchantConfig>): Observable<MerchantConfig> {
|
||||
const numericConfigId = parseInt(configId);
|
||||
const numericConfigId = this.convertIdToNumber(configId);
|
||||
|
||||
// Préparer l'objet de configuration pour l'API
|
||||
const apiConfig: Partial<ApiMerchantConfig> = {
|
||||
name: config.name,
|
||||
value: config.value,
|
||||
operatorId: config.operatorId,
|
||||
// Si merchantPartnerId est présent, le convertir en number
|
||||
...(config.merchantPartnerId && {
|
||||
merchantPartnerId: parseInt(config.merchantPartnerId)
|
||||
})
|
||||
};
|
||||
const apiConfig = this.dataAdapter.convertConfigUpdateToApi(config);
|
||||
|
||||
console.log(`📤 Updating config ${configId}:`, apiConfig);
|
||||
|
||||
// L'API retourne directement la configuration mise à jour
|
||||
return this.http.patch<ApiMerchantConfig>(`${this.baseApiUrl}/configs/${numericConfigId}`, apiConfig).pipe(
|
||||
timeout(this.REQUEST_TIMEOUT),
|
||||
map(apiConfig => {
|
||||
return this.mapApiConfigToConfig(apiConfig);
|
||||
console.log(`✅ Config ${configId} updated successfully`);
|
||||
// ✅ UTILISATION DE L'ADAPTER
|
||||
return this.dataAdapter.convertApiConfigToFrontend(apiConfig);
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(`Error updating config ${configId}:`, error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
catchError(error => this.handleError('updateConfig', error, { configId }))
|
||||
);
|
||||
}
|
||||
|
||||
deleteConfig(configId: string): Observable<void> {
|
||||
const numericConfigId = parseInt(configId);
|
||||
// L'API ne retourne probablement rien
|
||||
const numericConfigId = this.convertIdToNumber(configId);
|
||||
|
||||
console.log(`🗑️ Deleting config ${configId}`);
|
||||
|
||||
return this.http.delete<void>(`${this.baseApiUrl}/configs/${numericConfigId}`).pipe(
|
||||
timeout(this.REQUEST_TIMEOUT),
|
||||
map(() => {
|
||||
|
||||
console.log(`✅ Config ${configId} deleted successfully`);
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(`Error deleting config ${configId}:`, error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
catchError(error => this.handleError('deleteConfig', error, { configId }))
|
||||
);
|
||||
}
|
||||
|
||||
// Technical Contacts Management
|
||||
addTechnicalContactToMerchant(merchantId: string, contact: Omit<TechnicalContact, 'id' | 'merchantPartnerId'>): Observable<TechnicalContact> {
|
||||
const numericMerchantId = parseInt(merchantId);
|
||||
const apiContact: Omit<ApiTechnicalContact, 'id'> = {
|
||||
...contact,
|
||||
merchantPartnerId: numericMerchantId
|
||||
};
|
||||
|
||||
// L'API retourne directement le contact créé
|
||||
return this.http.post<ApiTechnicalContact>(`${this.baseApiUrl}/${numericMerchantId}/technical-contacts`, apiContact).pipe(
|
||||
map(apiContact => {
|
||||
return this.mapApiContactToContact(apiContact);
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(`Error adding technical contact to merchant ${merchantId}:`, error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
getMerchantConfigs(merchantId: string): Observable<MerchantConfig[]> {
|
||||
return this.getMerchantById(merchantId).pipe(
|
||||
map(merchant => merchant?.configs ?? [])
|
||||
);
|
||||
}
|
||||
|
||||
updateTechnicalContact(contactId: string, contact: Partial<TechnicalContact>): Observable<TechnicalContact> {
|
||||
const numericContactId = parseInt(contactId);
|
||||
// ==================== UTILITY METHODS ====================
|
||||
|
||||
const apiContact: Partial<ApiTechnicalContact> = {
|
||||
firstName: contact.firstName,
|
||||
lastName: contact.lastName,
|
||||
phone: contact.phone,
|
||||
email: contact.email,
|
||||
// Si merchantPartnerId est présent, le convertir en number
|
||||
...(contact.merchantPartnerId && {
|
||||
merchantPartnerId: parseInt(contact.merchantPartnerId)
|
||||
})
|
||||
private handleError(operation: string, error: any, context?: any): Observable<never> {
|
||||
console.error(`❌ Error in ${operation}:`, error, context);
|
||||
|
||||
let userMessage: string;
|
||||
|
||||
if (error instanceof HttpErrorResponse) {
|
||||
switch (error.status) {
|
||||
case 0:
|
||||
userMessage = 'Erreur de connexion. Vérifiez votre connexion internet.';
|
||||
break;
|
||||
case 400:
|
||||
userMessage = 'Données invalides. Vérifiez les informations saisies.';
|
||||
break;
|
||||
case 401:
|
||||
userMessage = 'Authentification requise. Veuillez vous reconnecter.';
|
||||
break;
|
||||
case 403:
|
||||
userMessage = 'Vous n\'avez pas les permissions pour effectuer cette action.';
|
||||
break;
|
||||
case 404:
|
||||
userMessage = 'Ressource non trouvée.';
|
||||
break;
|
||||
case 409:
|
||||
userMessage = 'Conflit de données. Cette ressource existe peut-être déjà.';
|
||||
break;
|
||||
case 500:
|
||||
userMessage = 'Erreur serveur. Veuillez réessayer plus tard.';
|
||||
break;
|
||||
case 503:
|
||||
userMessage = 'Service temporairement indisponible. Veuillez réessayer plus tard.';
|
||||
break;
|
||||
default:
|
||||
userMessage = 'Une erreur inattendue est survenue.';
|
||||
}
|
||||
} else if (error.name === 'TimeoutError') {
|
||||
userMessage = 'La requête a expiré. Veuillez réessayer.';
|
||||
} else {
|
||||
userMessage = 'Une erreur inattendue est survenue.';
|
||||
}
|
||||
|
||||
const errorDetails = {
|
||||
operation,
|
||||
context,
|
||||
status: error.status,
|
||||
message: error.message,
|
||||
url: error.url
|
||||
};
|
||||
|
||||
// L'API retourne directement le contact mis à jour
|
||||
return this.http.patch<ApiTechnicalContact>(`${this.baseApiUrl}/technical-contacts/${numericContactId}`, apiContact).pipe(
|
||||
map(apiContact => {
|
||||
return this.mapApiContactToContact(apiContact);
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(`Error updating technical contact ${contactId}:`, error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
console.error('Error details:', errorDetails);
|
||||
|
||||
return throwError(() => new Error(userMessage));
|
||||
}
|
||||
|
||||
deleteTechnicalContact(contactId: string): Observable<void> {
|
||||
const numericContactId = parseInt(contactId);
|
||||
// L'API ne retourne probablement rien
|
||||
return this.http.delete<void>(`${this.baseApiUrl}/technical-contacts/${numericContactId}`).pipe(
|
||||
map(() => {
|
||||
private convertIdToNumber(id: string): number {
|
||||
if (!id) {
|
||||
throw new Error('ID cannot be null or undefined');
|
||||
}
|
||||
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(`Error deleting technical contact ${contactId}:`, error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
const numericId = Number(id);
|
||||
if (isNaN(numericId)) {
|
||||
throw new Error(`Invalid ID format: ${id}`);
|
||||
}
|
||||
|
||||
// Stats
|
||||
getMerchantStats(): Observable<MerchantStatsResponse> {
|
||||
// L'API retourne directement les stats
|
||||
return this.http.get<MerchantStatsResponse>(`${this.baseApiUrl}/stats`).pipe(
|
||||
map(stats => {
|
||||
return stats;
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error('Error loading merchant stats:', error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== MAPPING METHODS ====================
|
||||
|
||||
// Méthode principale de mapping Merchant
|
||||
private mapApiMerchantToMerchant(apiMerchant: any): Merchant {
|
||||
|
||||
return {
|
||||
...apiMerchant,
|
||||
id: apiMerchant.id?.toString() || '', // number → string
|
||||
configs: (apiMerchant.configs || []).map((config: any) => this.mapApiConfigToConfig(config)),
|
||||
users: (apiMerchant.merchantUsers || []).map((user: any) => this.mapApiUserToUser(user)), // merchantUsers → users
|
||||
technicalContacts: (apiMerchant.technicalContacts || []).map((contact: any) => this.mapApiContactToContact(contact)),
|
||||
};
|
||||
}
|
||||
|
||||
// Méthode de mapping pour les configurations
|
||||
private mapApiConfigToConfig(apiConfig: any): MerchantConfig {
|
||||
return {
|
||||
...apiConfig,
|
||||
id: apiConfig.id?.toString(), // number → string
|
||||
merchantPartnerId: apiConfig.merchantPartnerId?.toString() // number → string
|
||||
};
|
||||
}
|
||||
|
||||
// Méthode de mapping pour les contacts techniques
|
||||
private mapApiContactToContact(apiContact: any): TechnicalContact {
|
||||
return {
|
||||
...apiContact,
|
||||
id: apiContact.id?.toString(), // number → string
|
||||
merchantPartnerId: apiContact.merchantPartnerId?.toString() // number → string
|
||||
};
|
||||
}
|
||||
|
||||
// Méthode de mapping pour les utilisateurs
|
||||
private mapApiUserToUser(apiUser: any): MerchantUser {
|
||||
return {
|
||||
...apiUser,
|
||||
merchantPartnerId: apiUser.merchantPartnerId?.toString() // number → string
|
||||
};
|
||||
return numericId;
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { FormsModule, ReactiveFormsModule, FormBuilder, Validators, FormArray, FormGroup } from '@angular/forms';
|
||||
import { NgIcon } from '@ng-icons/core';
|
||||
import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { catchError, map, of, Subject, takeUntil } from 'rxjs';
|
||||
import { catchError, finalize, map, of, Subject, takeUntil } from 'rxjs';
|
||||
|
||||
import { MerchantConfigService } from './merchant-config.service';
|
||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||
@ -19,10 +19,14 @@ import {
|
||||
Operator,
|
||||
UpdateMerchantDto,
|
||||
Merchant,
|
||||
MerchantConfig,
|
||||
TechnicalContact
|
||||
} from '@core/models/merchant-config.model';
|
||||
import { UserRole, UserType, PaginatedUserResponse, User } from '@core/models/dcb-bo-hub-user.model';
|
||||
import { HubUsersService } from '../hub-users-management/hub-users.service';
|
||||
|
||||
import { MerchantDataAdapter } from './merchant-data-adapter.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-merchant-config',
|
||||
standalone: true,
|
||||
@ -41,10 +45,9 @@ import { HubUsersService } from '../hub-users-management/hub-users.service';
|
||||
})
|
||||
export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
private modalService = inject(NgbModal);
|
||||
private fb = inject(FormBuilder);
|
||||
private authService = inject(AuthService);
|
||||
private merchantConfigService = inject(MerchantConfigService);
|
||||
private hubUsersService = inject(HubUsersService);
|
||||
private dataAdapter = inject(MerchantDataAdapter);
|
||||
protected roleService = inject(RoleManagementService);
|
||||
private cdRef = inject(ChangeDetectorRef);
|
||||
private destroy$ = new Subject<void>();
|
||||
@ -87,6 +90,10 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
selectedMerchantForEdit: Merchant | null = null;
|
||||
selectedMerchantForDelete: Merchant | null = null;
|
||||
|
||||
// États pour la gestion des modifications
|
||||
editingConfigs: { [configId: string]: boolean } = {};
|
||||
editingContacts: { [contactId: string]: boolean } = {};
|
||||
|
||||
// Références aux templates de modals
|
||||
@ViewChild('createMerchantModal') createMerchantModal!: TemplateRef<any>;
|
||||
@ViewChild('editMerchantModal') editMerchantModal!: TemplateRef<any>;
|
||||
@ -94,6 +101,7 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
|
||||
// Références aux composants enfants
|
||||
@ViewChild(MerchantConfigsList) merchantConfigsList!: MerchantConfigsList;
|
||||
@ViewChild(MerchantConfigView) merchantConfigView!: MerchantConfigView;
|
||||
|
||||
// Opérateurs disponibles
|
||||
operators = [
|
||||
@ -124,7 +132,6 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
ngOnInit() {
|
||||
this.activeTab = 'list';
|
||||
this.loadCurrentUserPermissions();
|
||||
this.loadMerchantPartnersIfNeeded();
|
||||
this.initializeMerchantPartnerContext();
|
||||
}
|
||||
|
||||
@ -163,6 +170,27 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
// Gestion des contacts dans l'édition
|
||||
addTechnicalContactInEdit(): void {
|
||||
if (!this.selectedMerchantForEdit?.technicalContacts) {
|
||||
this.selectedMerchantForEdit!.technicalContacts = [];
|
||||
}
|
||||
|
||||
this.selectedMerchantForEdit?.technicalContacts.push({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
phone: '',
|
||||
email: ''
|
||||
});
|
||||
}
|
||||
|
||||
removeTechnicalContactInEdit(index: number): void {
|
||||
if (this.selectedMerchantForEdit?.technicalContacts &&
|
||||
this.selectedMerchantForEdit.technicalContacts.length > 1) {
|
||||
this.selectedMerchantForEdit.technicalContacts.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthodes pour la gestion des configurations
|
||||
*/
|
||||
@ -184,60 +212,44 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== CONVERSION IDS ====================
|
||||
//Gestion des configs dans l'édition
|
||||
addConfigInEdit(): void {
|
||||
if (!this.selectedMerchantForEdit?.configs) {
|
||||
this.selectedMerchantForEdit!.configs = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un ID number en string pour Angular
|
||||
*/
|
||||
private convertIdToString(id: number): string {
|
||||
return id.toString();
|
||||
this.selectedMerchantForEdit?.configs.push({
|
||||
name: ConfigType.API_KEY,
|
||||
value: '',
|
||||
operatorId: Operator.ORANGE_OSN
|
||||
});
|
||||
}
|
||||
|
||||
removeConfigInEdit(index: number): void {
|
||||
if (this.selectedMerchantForEdit?.configs && this.selectedMerchantForEdit.configs.length > 1) {
|
||||
this.selectedMerchantForEdit.configs.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== CONVERSION IDS ====================
|
||||
|
||||
/**
|
||||
* Convertit un marchand avec des IDs number en string
|
||||
*/
|
||||
private convertMerchantToFrontend(merchant: any): Merchant {
|
||||
return {
|
||||
...merchant,
|
||||
id: merchant.id ? this.convertIdToString(merchant.id) : undefined,
|
||||
configs: merchant.configs?.map((config: any) => ({
|
||||
...config,
|
||||
id: config.id ? this.convertIdToString(config.id) : undefined,
|
||||
merchantPartnerId: config.merchantPartnerId ? this.convertIdToString(config.merchantPartnerId) : undefined
|
||||
})) || [],
|
||||
technicalContacts: merchant.technicalContacts?.map((contact: any) => ({
|
||||
...contact,
|
||||
id: contact.id ? this.convertIdToString(contact.id) : undefined,
|
||||
merchantPartnerId: contact.merchantPartnerId ? this.convertIdToString(contact.merchantPartnerId) : undefined
|
||||
})) || [],
|
||||
users: merchant.users?.map((user: any) => ({
|
||||
...user,
|
||||
merchantPartnerId: user.merchantPartnerId ? this.convertIdToString(user.merchantPartnerId) : undefined
|
||||
})) || []
|
||||
};
|
||||
return this.dataAdapter.convertApiMerchantToFrontend(merchant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un DTO avec des IDs string en number pour l'API
|
||||
*/
|
||||
private convertMerchantToBackend(dto: any): any {
|
||||
return this.dataAdapter.convertCreateMerchantToApi(dto);
|
||||
}
|
||||
|
||||
console.log("convertMerchantToBackend")
|
||||
return {
|
||||
...dto,
|
||||
// Pas de conversion pour l'ID du marchand en création (généré par le backend)
|
||||
configs: dto.configs?.map((config: any) => ({
|
||||
...config,
|
||||
// Pas de conversion pour l'ID de config en création
|
||||
operatorId: parseInt(config.operatorId),
|
||||
merchantPartnerId: config.merchantPartnerId
|
||||
})) || [],
|
||||
technicalContacts: dto.technicalContacts?.map((contact: any) => ({
|
||||
...contact,
|
||||
// Pas de conversion pour l'ID de contact en création
|
||||
merchantPartnerId: contact.merchantPartnerId
|
||||
})) || []
|
||||
};
|
||||
// Conversion pour la mise à jour
|
||||
private convertUpdateMerchantToBackend(dto: UpdateMerchantDto, existingMerchant?: Merchant): any {
|
||||
return this.dataAdapter.convertUpdateMerchantToApi(dto, existingMerchant);
|
||||
}
|
||||
|
||||
// ==================== GESTION DES PERMISSIONS ====================
|
||||
@ -300,40 +312,6 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
|
||||
// ==================== GESTION DES MARCHANDS ====================
|
||||
|
||||
private loadMerchantPartnersIfNeeded(): void {
|
||||
if (this.canManageMerchants) {
|
||||
this.loadMerchantPartners();
|
||||
}
|
||||
}
|
||||
|
||||
private loadMerchantPartners(): void {
|
||||
this.loadingMerchantPartners = true;
|
||||
this.merchantPartnersError = '';
|
||||
|
||||
this.hubUsersService.getAllDcbPartners()
|
||||
.pipe(
|
||||
map((response: PaginatedUserResponse) => response.users),
|
||||
takeUntil(this.destroy$),
|
||||
catchError(error => {
|
||||
console.error('Error loading merchant partners:', error);
|
||||
this.loadingMerchantPartners = false;
|
||||
this.merchantPartnersError = 'Impossible de charger la liste des marchands';
|
||||
return of([] as User[]);
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: (partners) => {
|
||||
this.merchantPartners = partners;
|
||||
this.loadingMerchantPartners = false;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('❌ Erreur lors du chargement des marchands:', error);
|
||||
this.loadingMerchantPartners = false;
|
||||
this.merchantPartnersError = 'Impossible de charger la liste des marchands';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private initializeMerchantPartnerContext(): void {
|
||||
if ((this.currentUserRole === UserRole.DCB_PARTNER ||
|
||||
this.currentUserRole === UserRole.DCB_PARTNER_ADMIN) &&
|
||||
@ -413,6 +391,7 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
onEditConfigRequested(configId: string): void {
|
||||
this.openEditMerchantModal(configId);
|
||||
}
|
||||
|
||||
// ==================== GESTION DES MODALS ====================
|
||||
|
||||
openModal(content: TemplateRef<any>, size: 'sm' | 'lg' | 'xl' = 'lg'): void {
|
||||
@ -535,30 +514,28 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
// Mise à jour COMPLÈTE du merchant
|
||||
updateMerchant(): void {
|
||||
if (!this.selectedMerchantForEdit) {
|
||||
this.updateMerchantError = 'Aucun marchand sélectionné pour modification';
|
||||
return;
|
||||
}
|
||||
|
||||
const validation = MerchantUtils.validateMerchantCreation(this.selectedMerchantForEdit as CreateMerchantDto);
|
||||
if (validation.length > 0) {
|
||||
this.updateMerchantError = validation.join(', ');
|
||||
// Validation des données complètes
|
||||
const validation = this.validateMerchantUpdate(this.selectedMerchantForEdit);
|
||||
if (!validation.isValid) {
|
||||
this.updateMerchantError = validation.errors.join(', ');
|
||||
return;
|
||||
}
|
||||
|
||||
this.updatingMerchant = true;
|
||||
this.updateMerchantError = '';
|
||||
|
||||
// Conversion pour l'API
|
||||
// Conversion pour l'API avec TOUTES les données
|
||||
const merchantId = this.selectedMerchantForEdit.id!;
|
||||
const updateDto: UpdateMerchantDto = {
|
||||
name: this.selectedMerchantForEdit.name,
|
||||
logo: this.selectedMerchantForEdit.logo,
|
||||
description: this.selectedMerchantForEdit.description,
|
||||
adresse: this.selectedMerchantForEdit.adresse,
|
||||
phone: this.selectedMerchantForEdit.phone
|
||||
};
|
||||
const updateDto = this.convertUpdateMerchantToBackend(this.selectedMerchantForEdit, this.selectedMerchantForEdit);
|
||||
|
||||
console.log('📤 Updating merchant with full data:', updateDto);
|
||||
|
||||
this.merchantConfigService.updateMerchant(merchantId, updateDto)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
@ -569,6 +546,7 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
|
||||
this.updatingMerchant = false;
|
||||
this.modalService.dismissAll();
|
||||
this.refreshMerchantsConfigsView();
|
||||
this.refreshMerchantsList();
|
||||
|
||||
// Mettre à jour le cache
|
||||
@ -576,6 +554,8 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
this.merchantProfiles[this.selectedMerchantId] = frontendMerchant;
|
||||
}
|
||||
|
||||
this.successMessage = 'Marchand modifié avec succès';
|
||||
|
||||
this.cdRef.detectChanges();
|
||||
},
|
||||
error: (error) => {
|
||||
@ -587,6 +567,74 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
// Validation complète pour la mise à jour
|
||||
validateMerchantUpdate(merchant: UpdateMerchantDto): { isValid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Validation des champs de base
|
||||
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');
|
||||
}
|
||||
|
||||
// Validation des configurations
|
||||
if (!merchant.configs || merchant.configs.length === 0) {
|
||||
errors.push('Au moins une configuration est requise');
|
||||
} else {
|
||||
merchant.configs.forEach((config, index) => {
|
||||
if (!config.name?.trim()) {
|
||||
errors.push(`Le type de configuration ${index + 1} est requis`);
|
||||
}
|
||||
if (!config.value?.trim()) {
|
||||
errors.push(`La valeur de configuration ${index + 1} est requise`);
|
||||
}
|
||||
if (!config.operatorId) {
|
||||
errors.push(`L'opérateur de configuration ${index + 1} est requis`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Validation des contacts techniques
|
||||
if (!merchant.technicalContacts || merchant.technicalContacts.length === 0) {
|
||||
errors.push('Au moins un contact technique est requis');
|
||||
} else {
|
||||
merchant.technicalContacts.forEach((contact, index) => {
|
||||
if (!contact.firstName?.trim()) {
|
||||
errors.push(`Le prénom du contact ${index + 1} est requis`);
|
||||
}
|
||||
if (!contact.lastName?.trim()) {
|
||||
errors.push(`Le nom du contact ${index + 1} est requis`);
|
||||
}
|
||||
if (!contact.phone?.trim()) {
|
||||
errors.push(`Le téléphone du contact ${index + 1} est requis`);
|
||||
}
|
||||
if (!contact.email?.trim()) {
|
||||
errors.push(`L'email du contact ${index + 1} est requis`);
|
||||
} else if (!this.isValidEmail(contact.email)) {
|
||||
errors.push(`L'email du contact ${index + 1} est invalide`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors: errors
|
||||
};
|
||||
}
|
||||
|
||||
// Validation d'email
|
||||
private isValidEmail(email: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
confirmDeleteMerchant(): void {
|
||||
if (!this.selectedMerchantForDelete) {
|
||||
this.deleteMerchantError = 'Aucun marchand sélectionné pour suppression';
|
||||
@ -653,7 +701,11 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private populateEditForm(merchant: Merchant): void {
|
||||
this.selectedMerchantForEdit = { ...merchant };
|
||||
this.selectedMerchantForEdit = {
|
||||
...merchant,
|
||||
configs: merchant.configs.map(config => ({ ...config })),
|
||||
technicalContacts: merchant.technicalContacts.map(contact => ({ ...contact }))
|
||||
};
|
||||
}
|
||||
|
||||
private refreshMerchantsList(): void {
|
||||
@ -666,6 +718,16 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private refreshMerchantsConfigsView(): void {
|
||||
if (this.merchantConfigView && typeof this.merchantConfigView.refresh === 'function') {
|
||||
console.log('🔄 Refreshing merchants config view...');
|
||||
this.merchantConfigView.refresh();
|
||||
} else {
|
||||
console.warn('❌ MerchantConfigsView component not available for refresh');
|
||||
this.showTab('list');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== GESTION DES ERREURS ====================
|
||||
|
||||
private getCreateErrorMessage(error: any): string {
|
||||
@ -702,6 +764,38 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
return MerchantUtils.getConfigTypeName(configType);
|
||||
}
|
||||
|
||||
// Méthodes utilitaires pour le template d'édition
|
||||
trackByConfigId(index: number, config: MerchantConfig): string {
|
||||
return config.id || `new-${index}`;
|
||||
}
|
||||
|
||||
trackByContactId(index: number, contact: TechnicalContact): string {
|
||||
return contact.id || `new-${index}`;
|
||||
}
|
||||
|
||||
// Méthode pour détecter les configs sensibles
|
||||
isSensitiveConfig(config: MerchantConfig): boolean {
|
||||
return config.name === ConfigType.API_KEY ||
|
||||
config.name === ConfigType.SECRET_KEY;
|
||||
}
|
||||
|
||||
// Méthode pour obtenir l'icône d'un type de config
|
||||
getConfigTypeIconSafe(configName: string): string {
|
||||
const icons: { [key: string]: string } = {
|
||||
[ConfigType.API_KEY]: 'lucideKey',
|
||||
[ConfigType.SECRET_KEY]: 'lucideShield',
|
||||
[ConfigType.WEBHOOK_URL]: 'lucideGlobe',
|
||||
[ConfigType.CALLBACK_URL]: 'lucideRefreshCw',
|
||||
[ConfigType.TIMEOUT]: 'lucideClock',
|
||||
[ConfigType.RETRY_COUNT]: 'lucideRepeat',
|
||||
[ConfigType.CUSTOM]: 'lucideSettings'
|
||||
};
|
||||
return icons[configName] || 'lucideSettings';
|
||||
}
|
||||
|
||||
// Propriété pour le message de succès
|
||||
successMessage: string = '';
|
||||
|
||||
// ==================== GETTERS TEMPLATE ====================
|
||||
|
||||
get currentMerchantProfile(): Merchant | null {
|
||||
|
||||
447
src/app/modules/merchant-config/merchant-data-adapter.service.ts
Normal file
447
src/app/modules/merchant-config/merchant-data-adapter.service.ts
Normal file
@ -0,0 +1,447 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
Merchant,
|
||||
CreateMerchantDto,
|
||||
UpdateMerchantDto,
|
||||
MerchantConfig,
|
||||
TechnicalContact,
|
||||
ApiMerchant,
|
||||
ApiMerchantConfig,
|
||||
ApiTechnicalContact,
|
||||
ConfigType,
|
||||
Operator
|
||||
} from '@core/models/merchant-config.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MerchantDataAdapter {
|
||||
|
||||
// ==================== CONVERSION API → FRONTEND ====================
|
||||
|
||||
/**
|
||||
* Convertit un merchant de l'API vers le format Frontend
|
||||
*/
|
||||
convertApiMerchantToFrontend(apiMerchant: ApiMerchant): Merchant {
|
||||
if (!apiMerchant) {
|
||||
throw new Error('Merchant data is required for conversion');
|
||||
}
|
||||
|
||||
return {
|
||||
...apiMerchant,
|
||||
id: this.convertIdToString(apiMerchant.id),
|
||||
configs: (apiMerchant.configs || []).map(config =>
|
||||
this.convertApiConfigToFrontend(config)
|
||||
),
|
||||
technicalContacts: (apiMerchant.technicalContacts || []).map(contact =>
|
||||
this.convertApiContactToFrontend(contact)
|
||||
),
|
||||
users: (apiMerchant.users || []).map(user =>
|
||||
this.convertApiUserToFrontend(user)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une configuration de l'API vers le format Frontend
|
||||
*/
|
||||
convertApiConfigToFrontend(apiConfig: ApiMerchantConfig): MerchantConfig {
|
||||
return {
|
||||
...apiConfig,
|
||||
id: this.convertIdToString(apiConfig.id),
|
||||
merchantPartnerId: this.convertIdToString(apiConfig.merchantPartnerId)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un contact technique de l'API vers le format Frontend
|
||||
*/
|
||||
convertApiContactToFrontend(apiContact: ApiTechnicalContact): TechnicalContact {
|
||||
return {
|
||||
...apiContact,
|
||||
id: this.convertIdToString(apiContact.id),
|
||||
merchantPartnerId: this.convertIdToString(apiContact.merchantPartnerId)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un utilisateur merchant de l'API vers le format Frontend
|
||||
*/
|
||||
convertApiUserToFrontend(apiUser: any): any {
|
||||
return {
|
||||
...apiUser,
|
||||
merchantPartnerId: this.convertIdToString(apiUser.merchantPartnerId)
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== CONVERSION FRONTEND → API ====================
|
||||
|
||||
/**
|
||||
* Convertit un DTO de création pour l'API
|
||||
*/
|
||||
convertCreateMerchantToApi(dto: CreateMerchantDto): any {
|
||||
this.validateCreateMerchantDto(dto);
|
||||
|
||||
return {
|
||||
name: dto.name?.trim(),
|
||||
logo: dto.logo?.trim() || undefined,
|
||||
description: dto.description?.trim() || undefined,
|
||||
adresse: dto.adresse?.trim(),
|
||||
phone: dto.phone?.trim(),
|
||||
configs: (dto.configs || []).map(config => ({
|
||||
name: config.name,
|
||||
value: config.value?.trim(),
|
||||
operatorId: this.validateOperatorId(config.operatorId)
|
||||
})),
|
||||
technicalContacts: (dto.technicalContacts || []).map(contact => ({
|
||||
firstName: contact.firstName?.trim(),
|
||||
lastName: contact.lastName?.trim(),
|
||||
phone: contact.phone?.trim(),
|
||||
email: contact.email?.trim()
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un DTO de mise à jour pour l'API
|
||||
*/
|
||||
convertUpdateMerchantToApi(dto: UpdateMerchantDto, existingMerchant?: Merchant): any {
|
||||
this.validateUpdateMerchantDto(dto);
|
||||
|
||||
const updateData: any = {};
|
||||
|
||||
// Champs de base
|
||||
if (dto.name !== undefined) updateData.name = dto.name?.trim();
|
||||
if (dto.logo !== undefined) updateData.logo = dto.logo?.trim() || null;
|
||||
if (dto.description !== undefined) updateData.description = dto.description?.trim() || null;
|
||||
if (dto.adresse !== undefined) updateData.adresse = dto.adresse?.trim();
|
||||
if (dto.phone !== undefined) updateData.phone = dto.phone?.trim();
|
||||
|
||||
// Configurations - seulement si présentes dans le DTO
|
||||
if (dto.configs !== undefined) {
|
||||
updateData.configs = (dto.configs || []).map(config => {
|
||||
const apiConfig: any = {
|
||||
name: config.name,
|
||||
value: config.value?.trim(),
|
||||
operatorId: this.validateOperatorId(config.operatorId)
|
||||
};
|
||||
|
||||
return apiConfig;
|
||||
});
|
||||
}
|
||||
|
||||
// Contacts techniques - seulement si présents dans le DTO
|
||||
if (dto.technicalContacts !== undefined) {
|
||||
updateData.technicalContacts = (dto.technicalContacts || []).map(contact => {
|
||||
const apiContact: any = {
|
||||
firstName: contact.firstName?.trim(),
|
||||
lastName: contact.lastName?.trim(),
|
||||
phone: contact.phone?.trim(),
|
||||
email: contact.email?.trim()
|
||||
};
|
||||
|
||||
return apiContact;
|
||||
});
|
||||
}
|
||||
|
||||
return updateData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une configuration pour la mise à jour via endpoint spécifique
|
||||
*/
|
||||
convertConfigUpdateToApi(config: Partial<MerchantConfig>): any {
|
||||
const updateData: any = {};
|
||||
|
||||
if (config.name !== undefined) updateData.name = config.name;
|
||||
if (config.value !== undefined) updateData.value = config.value?.trim();
|
||||
if (config.operatorId !== undefined) {
|
||||
updateData.operatorId = this.validateOperatorId(config.operatorId);
|
||||
}
|
||||
|
||||
if (config.merchantPartnerId !== undefined) {
|
||||
updateData.merchantPartnerId = this.convertIdToNumber(config.merchantPartnerId);
|
||||
}
|
||||
|
||||
return updateData;
|
||||
}
|
||||
|
||||
// ==================== VALIDATION ====================
|
||||
|
||||
/**
|
||||
* Valide le DTO de création
|
||||
*/
|
||||
private validateCreateMerchantDto(dto: CreateMerchantDto): void {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!dto.name?.trim()) {
|
||||
errors.push('Le nom du merchant est requis');
|
||||
}
|
||||
|
||||
if (!dto.adresse?.trim()) {
|
||||
errors.push('L\'adresse est requise');
|
||||
}
|
||||
|
||||
if (!dto.phone?.trim()) {
|
||||
errors.push('Le téléphone est requis');
|
||||
}
|
||||
|
||||
if (!dto.technicalContacts || dto.technicalContacts.length === 0) {
|
||||
errors.push('Au moins un contact technique est requis');
|
||||
} else {
|
||||
dto.technicalContacts.forEach((contact, index) => {
|
||||
if (!contact.firstName?.trim()) {
|
||||
errors.push(`Le prénom du contact ${index + 1} est requis`);
|
||||
}
|
||||
if (!contact.lastName?.trim()) {
|
||||
errors.push(`Le nom du contact ${index + 1} est requis`);
|
||||
}
|
||||
if (!contact.phone?.trim()) {
|
||||
errors.push(`Le téléphone du contact ${index + 1} est requis`);
|
||||
}
|
||||
if (!contact.email?.trim()) {
|
||||
errors.push(`L'email du contact ${index + 1} est requis`);
|
||||
} else if (!this.isValidEmail(contact.email)) {
|
||||
errors.push(`L'email du contact ${index + 1} est invalide`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!dto.configs || dto.configs.length === 0) {
|
||||
errors.push('Au moins une configuration est requise');
|
||||
} else {
|
||||
dto.configs.forEach((config, index) => {
|
||||
if (!config.name?.trim()) {
|
||||
errors.push(`Le type de configuration ${index + 1} est requis`);
|
||||
}
|
||||
if (!config.value?.trim()) {
|
||||
errors.push(`La valeur de configuration ${index + 1} est requise`);
|
||||
}
|
||||
if (!config.operatorId) {
|
||||
errors.push(`L'opérateur de configuration ${index + 1} est requis`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`Validation failed: ${errors.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide le DTO de mise à jour
|
||||
*/
|
||||
private validateUpdateMerchantDto(dto: UpdateMerchantDto): void {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Validation des champs de base si présents
|
||||
if (dto.name !== undefined && !dto.name.trim()) {
|
||||
errors.push('Le nom du merchant est requis');
|
||||
}
|
||||
|
||||
if (dto.adresse !== undefined && !dto.adresse.trim()) {
|
||||
errors.push('L\'adresse est requise');
|
||||
}
|
||||
|
||||
if (dto.phone !== undefined && !dto.phone.trim()) {
|
||||
errors.push('Le téléphone est requis');
|
||||
}
|
||||
|
||||
// Validation des configurations si présentes
|
||||
if (dto.configs !== undefined) {
|
||||
if (dto.configs.length === 0) {
|
||||
errors.push('Au moins une configuration est requise');
|
||||
} else {
|
||||
dto.configs.forEach((config, index) => {
|
||||
if (!config.name?.trim()) {
|
||||
errors.push(`Le type de configuration ${index + 1} est requis`);
|
||||
}
|
||||
if (!config.value?.trim()) {
|
||||
errors.push(`La valeur de configuration ${index + 1} est requise`);
|
||||
}
|
||||
if (!config.operatorId) {
|
||||
errors.push(`L'opérateur de configuration ${index + 1} est requis`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Validation des contacts si présents
|
||||
if (dto.technicalContacts !== undefined) {
|
||||
if (dto.technicalContacts.length === 0) {
|
||||
errors.push('Au moins un contact technique est requis');
|
||||
} else {
|
||||
dto.technicalContacts.forEach((contact, index) => {
|
||||
if (!contact.firstName?.trim()) {
|
||||
errors.push(`Le prénom du contact ${index + 1} est requis`);
|
||||
}
|
||||
if (!contact.lastName?.trim()) {
|
||||
errors.push(`Le nom du contact ${index + 1} est requis`);
|
||||
}
|
||||
if (!contact.phone?.trim()) {
|
||||
errors.push(`Le téléphone du contact ${index + 1} est requis`);
|
||||
}
|
||||
if (!contact.email?.trim()) {
|
||||
errors.push(`L'email du contact ${index + 1} est requis`);
|
||||
} else if (!this.isValidEmail(contact.email)) {
|
||||
errors.push(`L'email du contact ${index + 1} est invalide`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`Validation failed: ${errors.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Valide un operatorId
|
||||
*/
|
||||
private validateOperatorId(operatorId: Operator | number | string | null): number {
|
||||
if (operatorId === null || operatorId === undefined) {
|
||||
throw new Error('Operator ID is required');
|
||||
}
|
||||
|
||||
let numericId: number;
|
||||
|
||||
if (typeof operatorId === 'number') {
|
||||
numericId = operatorId;
|
||||
} else if (typeof operatorId === 'string') {
|
||||
numericId = parseInt(operatorId, 10);
|
||||
} else {
|
||||
// Cas "sécurité" : si TS voit encore "never"
|
||||
numericId = parseInt(String(operatorId), 10);
|
||||
}
|
||||
|
||||
if (isNaN(numericId)) {
|
||||
throw new Error(`Invalid operator ID: ${operatorId}`);
|
||||
}
|
||||
|
||||
// Vérifier que l'opérateur fait partie des valeurs enum valides
|
||||
const validOperators = Object.values(Operator).filter(val => typeof val === 'number');
|
||||
if (!validOperators.includes(numericId)) {
|
||||
throw new Error(`Unsupported operator ID: ${operatorId}`);
|
||||
}
|
||||
|
||||
return numericId;
|
||||
}
|
||||
|
||||
// ==================== UTILITAIRES DE CONVERSION ====================
|
||||
|
||||
/**
|
||||
* Convertit un ID number en string
|
||||
*/
|
||||
private convertIdToString(id: number | undefined): string | undefined {
|
||||
if (id === undefined || id === null) return undefined;
|
||||
return id.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un ID string en number
|
||||
*/
|
||||
private convertIdToNumber(id: string | undefined): number | undefined {
|
||||
if (!id) return undefined;
|
||||
|
||||
const numericId = parseInt(id, 10);
|
||||
if (isNaN(numericId)) {
|
||||
throw new Error(`Invalid ID for conversion to number: ${id}`);
|
||||
}
|
||||
|
||||
return numericId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide un email
|
||||
*/
|
||||
private isValidEmail(email: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
// ==================== MÉTHODES UTILITAIRES ====================
|
||||
|
||||
/**
|
||||
* Crée un nouveau merchant vide pour l'édition
|
||||
*/
|
||||
createEmptyMerchant(): Merchant {
|
||||
return {
|
||||
name: '',
|
||||
adresse: '',
|
||||
phone: '',
|
||||
configs: [this.createEmptyConfig()],
|
||||
technicalContacts: [this.createEmptyContact()],
|
||||
users: []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une configuration vide
|
||||
*/
|
||||
createEmptyConfig(): MerchantConfig {
|
||||
return {
|
||||
name: ConfigType.API_KEY,
|
||||
value: '',
|
||||
operatorId: Operator.ORANGE_OSN
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un contact technique vide
|
||||
*/
|
||||
createEmptyContact(): TechnicalContact {
|
||||
return {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
phone: '',
|
||||
email: ''
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone un merchant profondément (pour éviter les mutations)
|
||||
*/
|
||||
cloneMerchant(merchant: Merchant): Merchant {
|
||||
return {
|
||||
...merchant,
|
||||
configs: merchant.configs.map(config => ({ ...config })),
|
||||
technicalContacts: merchant.technicalContacts.map(contact => ({ ...contact })),
|
||||
users: merchant.users.map(user => ({ ...user }))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si un merchant a été modifié
|
||||
*/
|
||||
hasMerchantChanged(original: Merchant, updated: Merchant): boolean {
|
||||
return JSON.stringify(original) !== JSON.stringify(updated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrait seulement les champs modifiés pour l'update
|
||||
*/
|
||||
getChangedFields(original: Merchant, updated: Merchant): Partial<UpdateMerchantDto> {
|
||||
const changes: Partial<UpdateMerchantDto> = {};
|
||||
|
||||
if (original.name !== updated.name) changes.name = updated.name;
|
||||
if (original.logo !== updated.logo) changes.logo = updated.logo;
|
||||
if (original.description !== updated.description) changes.description = updated.description;
|
||||
if (original.adresse !== updated.adresse) changes.adresse = updated.adresse;
|
||||
if (original.phone !== updated.phone) changes.phone = updated.phone;
|
||||
|
||||
// Vérifier les changements dans les configurations
|
||||
const configsChanged = JSON.stringify(original.configs) !== JSON.stringify(updated.configs);
|
||||
if (configsChanged) {
|
||||
changes.configs = updated.configs;
|
||||
}
|
||||
|
||||
// Vérifier les changements dans les contacts
|
||||
const contactsChanged = JSON.stringify(original.technicalContacts) !== JSON.stringify(updated.technicalContacts);
|
||||
if (contactsChanged) {
|
||||
changes.technicalContacts = updated.technicalContacts;
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user