dcb-backoffice/src/app/modules/dcb-dashboard/services/dcb-reporting.service.ts

296 lines
9.4 KiB
TypeScript

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<HealthCheckStatus> {
const startTime = Date.now();
return this.http.head(url, {
observe: 'response'
}).pipe(
timeout(this.DEFAULT_TIMEOUT),
map((resp: HttpResponse<any>) => {
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<any>) => {
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<HealthCheckStatus> {
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<any>) => {
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<HealthCheckStatus[]> {
const healthChecks: Observable<HealthCheckStatus>[] = [];
// 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<HealthCheckStatus> {
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<SyncResponse> {
return this.http.post<SyncResponse>(`${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<TransactionReport> {
return this.http.get<TransactionReport>(`${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<SubscriptionReport> {
return this.http.get<SubscriptionReport>(`${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);
}
}