import { Injectable } from '@angular/core'; import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; import { Observable, throwError, forkJoin, of } from 'rxjs'; import { map, timeout, catchError, retry } from 'rxjs/operators'; import { ReportParams, TransactionReport, SubscriptionReport, SyncResponse, HealthCheckStatus, ChartDataNormalized } from '../models/dcb-reporting.models'; import { environment } from '@environments/environment'; @Injectable({ providedIn: 'root' }) export class ReportService { private baseUrl = `${environment.reportingApiUrl}/reporting`; private readonly DEFAULT_TIMEOUT = 5000; // Timeout réduit pour les health checks private readonly DEFAULT_RETRY = 1; private readonly REPORTING_TIMEOUT = 10000; // Timeout normal pour les autres requêtes // Configuration des APIs à scanner private apiEndpoints = { IAM: environment.iamApiUrl, CONFIG: environment.configApiUrl, CORE: environment.apiCoreUrl, REPORTING: environment.reportingApiUrl }; constructor(private http: HttpClient) {} // --------------------- // Helpers // --------------------- private buildParams(params?: ReportParams): HttpParams { let httpParams = new HttpParams(); if (!params) return httpParams; Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) httpParams = httpParams.set(key, value.toString()); }); return httpParams; } private handleError(err: any) { return throwError(() => ({ message: err?.message || 'Unknown error', status: err?.status || 0, error: err?.error || null })); } formatDate(date: Date): string { const y = date.getFullYear(); const m = ('0' + (date.getMonth() + 1)).slice(-2); const d = ('0' + date.getDate()).slice(-2); return `${y}-${m}-${d}`; } // --------------------- // Health checks // --------------------- private checkApiAvailability( service: string, url: string ): Observable { const startTime = Date.now(); return this.http.head(url, { observe: 'response' }).pipe( timeout(this.DEFAULT_TIMEOUT), map((resp: HttpResponse) => { const responseTime = Date.now() - startTime; return { service, url, status: 'UP' as const, statusCode: resp.status, checkedAt: new Date().toISOString(), responseTime: `${responseTime}ms` }; }), catchError((error) => { const responseTime = Date.now() - startTime; // Si HEAD échoue, essayez GET sur la racine return this.http.get(url, { observe: 'response' }).pipe( timeout(this.DEFAULT_TIMEOUT), map((resp: HttpResponse) => { const finalResponseTime = Date.now() - startTime; return { service, url, status: 'UP' as const, statusCode: resp.status, checkedAt: new Date().toISOString(), responseTime: `${finalResponseTime}ms`, note: 'Used GET fallback' }; }), catchError((finalError) => { const finalResponseTime = Date.now() - startTime; return of({ service, url, status: 'DOWN' as const, statusCode: finalError.status || 0, checkedAt: new Date().toISOString(), responseTime: `${finalResponseTime}ms`, error: this.getErrorMessage(finalError) }); }) ); }) ); } private getErrorMessage(error: any): string { if (error.status === 0) { return 'Network error - Service unreachable'; } else if (error.status === 404) { return 'Endpoint not found but service responds'; } else if (error.status === 401 || error.status === 403) { return 'Service requires authentication'; } else { return error.message || `HTTP ${error.status}`; } } private checkApiHealth( service: string, url: string, method: 'GET' | 'POST' | 'HEAD' ): Observable { const startTime = Date.now(); return this.http.request(method, url, { observe: 'response', responseType: 'text' }).pipe( timeout(this.DEFAULT_TIMEOUT), retry(this.DEFAULT_RETRY), map((resp: HttpResponse) => { const responseTime = Date.now() - startTime; return { service, url, status: resp.status >= 200 && resp.status < 500 ? 'UP' as const : 'DOWN' as const, statusCode: resp.status, checkedAt: new Date().toISOString(), responseTime: `${responseTime}ms` }; }), catchError((error) => { const responseTime = Date.now() - startTime; return of({ service, url, status: 'DOWN' as const, statusCode: error.status || 0, checkedAt: new Date().toISOString(), responseTime: `${responseTime}ms`, error: error.message || 'Connection failed' }); }) ); } /** * Health check global de toutes les APIs * Scanne chaque URL d'API directement */ globalHealthCheck(): Observable { const healthChecks: Observable[] = []; // Vérifiez chaque service avec sa racine Object.entries(this.apiEndpoints).forEach(([service, url]) => { healthChecks.push(this.checkApiAvailability(service, url)); }); return forkJoin(healthChecks); } /** * Health check spécifique pour l'API de synchronisation */ syncHealthCheck(): Observable { return this.checkApiHealth('SYNC', `${this.baseUrl}/sync/full`, 'POST'); } /** * Health check rapide (vérifie seulement si les APIs répondent) */ quickHealthCheck(): Observable<{ [key: string]: boolean }> { const services = Object.entries(this.apiEndpoints); const checks = services.map(([service, url]) => this.checkApiHealth(service, url, 'GET').pipe( map(result => ({ [service]: result.status === 'UP' })), catchError(() => of({ [service]: false })) ) ); return forkJoin(checks).pipe( map(results => { return results.reduce((acc, curr) => ({ ...acc, ...curr }), {}); }) ); } /** * Health check détaillé avec métriques */ detailedHealthCheck(): Observable<{ summary: { total: number; up: number; down: number; timestamp: string }; details: HealthCheckStatus[]; }> { return this.globalHealthCheck().pipe( map(results => { const adjustedResults = results.map(result => ({ ...result, status: result.statusCode > 0 ? 'UP' as const : 'DOWN' as const })); const upCount = adjustedResults.filter(r => r.status === 'UP').length; const downCount = adjustedResults.filter(r => r.status === 'DOWN').length; return { summary: { total: adjustedResults.length, up: upCount, down: downCount, timestamp: new Date().toISOString() }, details: adjustedResults }; }), catchError(err => this.handleError(err)) ); } triggerManualSync(): Observable { return this.http.post(`${this.baseUrl}/sync/full`, {}).pipe( timeout(this.REPORTING_TIMEOUT), retry(this.DEFAULT_RETRY), catchError(err => this.handleError(err)) ); } // --------------------- // Transactions & Subscriptions Reports // --------------------- getTransactionReport(params?: ReportParams, period: 'daily' | 'weekly' | 'monthly' | 'yearly' = 'daily'): Observable { return this.http.get(`${this.baseUrl}/transactions/${period}`, { params: this.buildParams(params) }).pipe( timeout(this.REPORTING_TIMEOUT), retry(this.DEFAULT_RETRY), catchError(err => this.handleError(err)) ); } getSubscriptionReport(params?: ReportParams, period: 'daily' | 'weekly' | 'monthly' | 'yearly' = 'daily'): Observable { return this.http.get(`${this.baseUrl}/subscriptions/${period}`, { params: this.buildParams(params) }).pipe( timeout(this.REPORTING_TIMEOUT), retry(this.DEFAULT_RETRY), catchError(err => this.handleError(err)) ); } getTransactionsWithDates(startDate: string, endDate: string, merchantPartnerId?: number) { return this.getTransactionReport({ startDate, endDate, merchantPartnerId }, 'daily'); } // --------------------- // Multi-partner normalization for charts // --------------------- normalizeForChart(data: TransactionReport | SubscriptionReport, key: string): ChartDataNormalized { const labels = data.items.map(i => i.period); const dataset = data.items.map(i => (i as any)[key] ?? 0); return { labels, dataset }; } // --------------------- // Multi-period & Multi-partner reporting // --------------------- getMultiPartnerReports(partners: number[], period: 'daily' | 'weekly' | 'monthly' | 'yearly' = 'daily', type: 'transaction' | 'subscription' = 'transaction') { const requests = partners.map(id => { return type === 'transaction' ? this.getTransactionReport({ merchantPartnerId: id }, period) : this.getSubscriptionReport({ merchantPartnerId: id }, period); }); return forkJoin(requests); } }