296 lines
9.4 KiB
TypeScript
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);
|
|
}
|
|
} |