diff --git a/src/app/core/models/dcb-bo-hub-user.model.ts b/src/app/core/models/dcb-bo-hub-user.model.ts index 2d52bc7..7046be4 100644 --- a/src/app/core/models/dcb-bo-hub-user.model.ts +++ b/src/app/core/models/dcb-bo-hub-user.model.ts @@ -8,9 +8,9 @@ export enum UserRole { // Rôles Hub (sans merchantPartnerId) DCB_ADMIN = 'dcb-admin', DCB_SUPPORT = 'dcb-support', - DCB_PARTNER = 'dcb-partner', + DCB_PARTNER = 'dcb-partner', // Propriétaire de merchants - // Rôles Merchant Partner (avec merchantPartnerId obligatoire) + // Rôles Merchant Partner (avec merchantPartnerId obligatoire = ID du DCB_PARTNER) DCB_PARTNER_ADMIN = 'dcb-partner-admin', DCB_PARTNER_MANAGER = 'dcb-partner-manager', DCB_PARTNER_SUPPORT = 'dcb-partner-support' @@ -41,16 +41,18 @@ export interface UsersStatistics { // === MODÈLE USER PRINCIPAL === export interface User { - id: string; + id: string; // UUID Keycloak username: string; email: string; firstName: string; lastName: string; enabled: boolean; emailVerified: boolean; - userType: UserType; // HUB ou MERCHANT - merchantPartnerId?: string; + userType: UserType; // HUB ou MERCHANT_PARTNER + merchantPartnerId?: string; // Pour les users merchant: ID du DCB_PARTNER propriétaire role: UserRole; + // Merchant Config + merchantConfigId?: number; // ID INT dans Merchant Config createdBy?: string; createdByUsername?: string; createdTimestamp: number; @@ -58,6 +60,13 @@ export interface User { profileImage?: string | null } +export interface SyncResult { + success: boolean; + user?: User; + errors?: string[]; + warnings?: string[]; +} + // === DTOs CRUD === export interface CreateUserDto { username: string; diff --git a/src/app/core/models/merchant-user-sync.model.ts b/src/app/core/models/merchant-user-sync.model.ts new file mode 100644 index 0000000..503c554 --- /dev/null +++ b/src/app/core/models/merchant-user-sync.model.ts @@ -0,0 +1,116 @@ +// === MODÈLES POUR LA SYNCHRONISATION === + +import { User, UserRole } from "./dcb-bo-hub-user.model"; + +export interface MerchantUserSyncDto { + // Données de base pour Keycloak + username: string; + email: string; + firstName: string; + lastName: string; + password: string; + role: UserRole; + enabled?: boolean; + emailVerified?: boolean; + + // Référence au DCB_PARTNER propriétaire + merchantPartnerId: string; // ID Keycloak du DCB_PARTNER + + // Données pour Merchant Config + merchantConfig?: { + phone?: string; + technicalContacts?: Array<{ + firstName: string; + lastName: string; + email: string; + phone: string; + }>; + }; +} + +export interface IdMapping { + id?: number; + keycloakId: string; // UUID Keycloak + merchantConfigId: number; // INT Merchant Config + merchantPartnerId: string; // ID du DCB_PARTNER propriétaire + entityType: 'merchant' | 'user'; + username?: string; + email?: string; + createdAt?: Date; + updatedAt?: Date; +} + +export interface SyncResult { + success: boolean; + keycloakUser?: User; + merchantConfigUser?: any; + mapping?: IdMapping; + errors?: string[]; +} + +export interface DCBPartnerInfo { + id: string; + username: string; + email: string; + enabled: boolean; + merchantPartnerId?: string; // Pour DCB_PARTNER, ce doit être undefined +} + +// === UTILITAIRES DE SYNCHRONISATION === +export class SyncUtils { + static validateDCBPartner(dcbPartner: User): string[] { + const errors: string[] = []; + + if (!dcbPartner) { + errors.push('DCB_PARTNER non trouvé'); + return errors; + } + + if (dcbPartner.role !== UserRole.DCB_PARTNER) { + errors.push(`L'utilisateur ${dcbPartner.username} n'est pas un DCB_PARTNER`); + } + + if (!dcbPartner.enabled) { + errors.push(`Le DCB_PARTNER ${dcbPartner.username} est désactivé`); + } + + if (dcbPartner.merchantPartnerId) { + errors.push(`Un DCB_PARTNER ne doit pas avoir de merchantPartnerId`); + } + + return errors; + } + + static validateMerchantUserCreation(dto: MerchantUserSyncDto): string[] { + const errors: string[] = []; + + if (!dto.merchantPartnerId) { + errors.push('merchantPartnerId est obligatoire'); + } + + if (!dto.username?.trim()) { + errors.push('Username est obligatoire'); + } + + if (!dto.email?.trim()) { + errors.push('Email est obligatoire'); + } + + if (!dto.password || dto.password.length < 8) { + errors.push('Le mot de passe doit contenir au moins 8 caractères'); + } + + // Validation du rôle + const merchantRoles = [ + UserRole.DCB_PARTNER_ADMIN, + UserRole.DCB_PARTNER_MANAGER, + UserRole.DCB_PARTNER_SUPPORT + ]; + + if (!merchantRoles.includes(dto.role)) { + errors.push(`Rôle invalide pour un utilisateur merchant: ${dto.role}`); + } + + return errors; + } +} \ No newline at end of file diff --git a/src/app/modules/dcb-dashboard/components/active-subscriptions.ts b/src/app/modules/dcb-dashboard/components/active-subscriptions.ts index 34311c0..0586516 100644 --- a/src/app/modules/dcb-dashboard/components/active-subscriptions.ts +++ b/src/app/modules/dcb-dashboard/components/active-subscriptions.ts @@ -1,44 +1,192 @@ -import { Component } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { CommonModule } from '@angular/common'; import { NgIconComponent } from '@ng-icons/core'; import { NgbProgressbarModule } from '@ng-bootstrap/ng-bootstrap'; import { CountUpModule } from 'ngx-countup'; +import { DcbReportingService, SubscriptionItem } from '../services/dcb-reporting.service'; +import { catchError, finalize } from 'rxjs/operators'; +import { of, Subscription } from 'rxjs'; @Component({ selector: 'app-active-subscriptions', - imports: [NgIconComponent, NgbProgressbarModule, CountUpModule], + imports: [CommonModule, NgIconComponent, NgbProgressbarModule, CountUpModule], template: `
+
+
+
Abonnements Actifs
+ +
+
-
-
-
Abonnements Actifs
-

- 12,543 -

-

Total abonnements

-
-
- + +
+
+ Chargement...
- - -
-
- Nouveaux -
156
+ +
+
+
+
Total actifs
+

+ + {{ formatNumber(totalSubscriptions) }} + +

+
+
+ +
-
- Renouvellements -
89%
+ + +
+
+ Taux d'activité + {{ activePercentage | number:'1.1-1' }}% +
+ +
+ +
+
+ Nouveaux +
{{ newSubscriptionsToday }}
+
+
+ Annulés +
{{ cancelledSubscriptions }}
+
+
+
+ + +
+
+ + {{ error }}
- `, + styles: [` + .spin { + animation: spin 1s linear infinite; + } + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + .alert-sm { + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + } + `] }) -export class ActiveSubscriptions {} \ No newline at end of file +export class ActiveSubscriptions implements OnInit, OnDestroy { + loading = false; + error: string | null = null; + lastUpdated = new Date(); + + // Données d'abonnements + totalSubscriptions = 0; + newSubscriptionsToday = 0; + cancelledSubscriptions = 0; + activePercentage = 0; + + private apiSubscription?: Subscription; + + constructor(private reportingService: DcbReportingService) {} + + ngOnInit() { + this.loadSubscriptionData(); + } + + ngOnDestroy() { + if (this.apiSubscription) { + this.apiSubscription.unsubscribe(); + } + } + + loadSubscriptionData() { + this.loading = true; + this.error = null; + + const today = new Date(); + const startDate = this.reportingService.formatDate(today); + + console.log('ActiveSubscriptions - Loading data for date:', startDate); + + this.apiSubscription = this.reportingService.getDailySubscriptions(startDate, startDate) + .pipe( + catchError(err => { + console.error('ActiveSubscriptions - API error:', err); + this.error = 'Impossible de charger les données'; + return of([]); + }), + finalize(() => { + this.loading = false; + this.lastUpdated = new Date(); + }) + ) + .subscribe({ + next: (subscriptions: SubscriptionItem[]) => { + console.log('ActiveSubscriptions - Received data:', subscriptions); + this.processSubscriptionData(subscriptions); + } + }); + } + + processSubscriptionData(subscriptions: SubscriptionItem[]) { + if (!subscriptions || subscriptions.length === 0) { + console.warn('ActiveSubscriptions - No data available'); + return; + } + + // Prendre les données du jour (ou la période la plus récente) + const latestData = subscriptions[subscriptions.length - 1]; + + console.log('ActiveSubscriptions - Latest data:', latestData); + + // Utiliser les données brutes de l'API + // Note: Votre API retourne activeCount et cancelledCount + this.totalSubscriptions = latestData.activeCount || 0; + this.cancelledSubscriptions = latestData.cancelledCount || 0; + + // Pour les nouveaux abonnements aujourd'hui + // Si l'API ne fournit pas cette donnée, on peut estimer ou utiliser 0 + this.newSubscriptionsToday = 0; // À adapter selon votre logique + + // Calculer le pourcentage d'activité + const totalCount = latestData.count || 0; + this.activePercentage = totalCount > 0 ? + ((latestData.activeCount || 0) / totalCount) * 100 : 0; + } + + formatNumber(value: number): string { + return new Intl.NumberFormat('fr-FR').format(value); + } + + refresh() { + console.log('ActiveSubscriptions - Refreshing data'); + + // Essayer de vider le cache si disponible + if (this.reportingService.clearCache) { + this.reportingService.clearCache(); + } + + this.loadSubscriptionData(); + } +} \ No newline at end of file diff --git a/src/app/modules/dcb-dashboard/components/alert-widget.ts b/src/app/modules/dcb-dashboard/components/alert-widget.ts deleted file mode 100644 index 01fb6cd..0000000 --- a/src/app/modules/dcb-dashboard/components/alert-widget.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Component } from '@angular/core'; -import { NgIconComponent } from '@ng-icons/core'; - -@Component({ - selector: 'app-alert-widget', - imports: [NgIconComponent], - template: ` -
-
-
Alertes Récentes
- 3 -
- -
-
-
-
- -
-
Taux d'échec élevé Airtel
-

Le taux d'échec a augmenté de 15%

- Il y a 30 min -
-
-
- -
-
- -
-
Maintenance planifiée
-

Maintenance ce soir de 22h à 00h

- Il y a 2h -
-
-
- -
-
- -
-
Nouveau partenaire
-

MTN Sénégal configuré avec succès

- Il y a 4h -
-
-
-
-
- - -
- `, -}) -export class AlertWidget {} \ No newline at end of file diff --git a/src/app/modules/dcb-dashboard/components/dcb-dashboard-report.ts b/src/app/modules/dcb-dashboard/components/dcb-dashboard-report.ts new file mode 100644 index 0000000..d970f96 --- /dev/null +++ b/src/app/modules/dcb-dashboard/components/dcb-dashboard-report.ts @@ -0,0 +1,612 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Chartjs } from '@app/components/chartjs'; +import { ChartConfiguration } from 'chart.js'; +import { getColor } from '@/app/utils/color-utils'; +import { NgbProgressbarModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgIconComponent } from '@ng-icons/core'; +import { CountUpModule } from 'ngx-countup'; +import { DcbReportingService, TransactionItem, SubscriptionItem } from '../services/dcb-reporting.service'; +import { catchError } from 'rxjs/operators'; +import { of, Subscription } from 'rxjs'; + +@Component({ + selector: 'app-dashboard-report', + imports: [ + CommonModule, + Chartjs, + NgbProgressbarModule, + NgbDropdownModule, // Ajoutez ce module + NgIconComponent, + CountUpModule + ], + template: ` +
+ +
+
+
+

Dashboard Reporting

+ +
+
+
+ + +
+ +
+
+
+ +
Transactions Journalières
+

+ + {{ dailyTransactions.total }} + +

+

Transactions

+
+ {{ formatCurrency(dailyTransactions.revenue) }} + + + {{ dailyTransactions.successRate | number:'1.1-1' }}% + +
+
+
+
+ + +
+
+
+ +
Transactions Hebdomadaires
+

+ + {{ weeklyTransactions.total }} + +

+

Transactions

+
+ {{ formatCurrency(weeklyTransactions.revenue) }} + + + {{ weeklyTransactions.successRate | number:'1.1-1' }}% + +
+
+
+
+ + +
+
+
+ +
Transactions Mensuelles
+

+ + {{ monthlyTransactions.total }} + +

+

Transactions

+
+ {{ formatCurrency(monthlyTransactions.revenue) }} + + + {{ monthlyTransactions.successRate | number:'1.1-1' }}% + +
+
+
+
+ + +
+
+
+ +
Taux global
+

+ + {{ overallSuccessRate | number:'1.1-1' }} + % +

+

Succès (30 jours)

+
+ Période: 30j + + {{ getPerformanceLabel(overallSuccessRate) }} + +
+
+
+
+
+ + +
+ +
+
+
+
Revenue Mensuel
+
+
+ +
+
+
+ + +
+
+
+
+
Abonnements
+
+ +
+ + + +
+
+
+
+
+ +
+
Journaliers
+
+ +
+

{{ dailySubscriptions.active }}

+ Actifs +
+
+ Total: {{ dailySubscriptions.total }}
+ Annulés: {{ dailySubscriptions.cancelled }} +
+
+
+ +
+
Hebdomadaires
+
+ +
+

{{ weeklySubscriptions.active }}

+ Actifs +
+
+ Total: {{ weeklySubscriptions.total }}
+ Annulés: {{ weeklySubscriptions.cancelled }} +
+
+
+ +
+
Mensuels
+
+ +
+

{{ monthlySubscriptions.active }}

+ Actifs +
+
+ Total: {{ monthlySubscriptions.total }}
+ Annulés: {{ monthlySubscriptions.cancelled }} +
+
+
+ + +
+
+ Taux d'activité + {{ getCurrentActivityRate() | number:'1.1-1' }}% +
+ + + {{ getCurrentActiveSubscriptions() }} actifs sur {{ getCurrentTotalSubscriptions() }} total + +
+
+
+
+
+ + +
+
+
+
+
+
Transactions Récentes
+
+ +
+ + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
PériodeTransactionsMontant TotalRéussiesÉchouéesEn attenteTaux de succès
{{ item.period }}{{ item.count }}{{ formatCurrency(item.totalAmount) }}{{ item.successCount }}{{ item.failedCount }}{{ item.pendingCount }} + + {{ getSuccessRate(item) | number:'1.1-1' }}% + +
+ Aucune donnée de transaction disponible +
+
+
+
+
+
+
+ `, + styles: [` + .spin { + animation: spin 1s linear infinite; + } + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + `] +}) +export class DashboardReport implements OnInit, OnDestroy { + + // Périodes d'affichage + subscriptionPeriod: 'daily' | 'weekly' | 'monthly' = 'daily'; + transactionPeriod: 'daily' | 'weekly' | 'monthly' = 'daily'; + + // Données des transactions + dailyTransactions = { total: 0, revenue: 0, successRate: 0 }; + weeklyTransactions = { total: 0, revenue: 0, successRate: 0 }; + monthlyTransactions = { total: 0, revenue: 0, successRate: 0 }; + overallSuccessRate = 0; + + // Données brutes pour les tableaux + dailyTransactionItems: TransactionItem[] = []; + weeklyTransactionItems: TransactionItem[] = []; + monthlyTransactionItems: TransactionItem[] = []; + + // Données des abonnements + dailySubscriptions = { total: 0, active: 0, cancelled: 0 }; + weeklySubscriptions = { total: 0, active: 0, cancelled: 0 }; + monthlySubscriptions = { total: 0, active: 0, cancelled: 0 }; + + // Données brutes pour les abonnements + dailySubscriptionItems: SubscriptionItem[] = []; + weeklySubscriptionItems: SubscriptionItem[] = []; + monthlySubscriptionItems: SubscriptionItem[] = []; + + // Données pour le graphique + chartData: { date: string; revenue: number }[] = []; + + // Abonnements aux API + private subscriptions: Subscription[] = []; + + constructor(private reportingService: DcbReportingService) {} + + ngOnInit() { + this.loadAllData(); + } + + ngOnDestroy() { + this.cleanupSubscriptions(); + } + + private cleanupSubscriptions() { + this.subscriptions.forEach(sub => sub.unsubscribe()); + this.subscriptions = []; + } + + loadAllData() { + this.cleanupSubscriptions(); + + // Charger toutes les données en parallèle + this.subscriptions.push( + this.reportingService.getDailyTransactions() + .pipe(catchError(() => of([]))) + .subscribe(data => { + this.dailyTransactionItems = data; + this.dailyTransactions = this.calculateTransactionStats(data); + }) + ); + + this.subscriptions.push( + this.reportingService.getWeeklyTransactions() + .pipe(catchError(() => of([]))) + .subscribe(data => { + this.weeklyTransactionItems = data; + this.weeklyTransactions = this.calculateTransactionStats(data); + }) + ); + + this.subscriptions.push( + this.reportingService.getMonthlyTransactions() + .pipe(catchError(() => of([]))) + .subscribe(data => { + this.monthlyTransactionItems = data; + this.monthlyTransactions = this.calculateTransactionStats(data); + this.overallSuccessRate = this.monthlyTransactions.successRate; + this.prepareChartData(data); + }) + ); + + this.subscriptions.push( + this.reportingService.getDailySubscriptions() + .pipe(catchError(() => of([]))) + .subscribe(data => { + this.dailySubscriptionItems = data; + this.dailySubscriptions = this.calculateSubscriptionStats(data); + }) + ); + + this.subscriptions.push( + this.reportingService.getWeeklySubscriptions() + .pipe(catchError(() => of([]))) + .subscribe(data => { + this.weeklySubscriptionItems = data; + this.weeklySubscriptions = this.calculateSubscriptionStats(data); + }) + ); + + this.subscriptions.push( + this.reportingService.getMonthlySubscriptions() + .pipe(catchError(() => of([]))) + .subscribe(data => { + this.monthlySubscriptionItems = data; + this.monthlySubscriptions = this.calculateSubscriptionStats(data); + }) + ); + } + + // Méthodes pour changer la période d'affichage + showDailySubscriptions() { + this.subscriptionPeriod = 'daily'; + } + + showWeeklySubscriptions() { + this.subscriptionPeriod = 'weekly'; + } + + showMonthlySubscriptions() { + this.subscriptionPeriod = 'monthly'; + } + + showDailyTransactions() { + this.transactionPeriod = 'daily'; + } + + showWeeklyTransactions() { + this.transactionPeriod = 'weekly'; + } + + showMonthlyTransactions() { + this.transactionPeriod = 'monthly'; + } + + // Méthodes utilitaires + calculateTransactionStats(data: TransactionItem[]): { total: number; revenue: number; successRate: number } { + if (!data || data.length === 0) { + return { total: 0, revenue: 0, successRate: 0 }; + } + + const total = data.reduce((sum, item) => sum + (item.count || 0), 0); + const revenue = data.reduce((sum, item) => sum + (item.totalAmount || 0), 0); + const successful = data.reduce((sum, item) => sum + (item.successCount || 0), 0); + + const successRate = total > 0 ? (successful / total) * 100 : 0; + + return { total, revenue, successRate }; + } + + calculateSubscriptionStats(data: SubscriptionItem[]): { total: number; active: number; cancelled: number } { + if (!data || data.length === 0) { + return { total: 0, active: 0, cancelled: 0 }; + } + + // Prendre la dernière période + const latest = data[data.length - 1]; + + return { + total: latest.count || 0, + active: latest.activeCount || 0, + cancelled: latest.cancelledCount || 0 + }; + } + + getSubscriptionPeriodLabel(): string { + switch (this.subscriptionPeriod) { + case 'daily': return 'Journalier'; + case 'weekly': return 'Hebdomadaire'; + case 'monthly': return 'Mensuel'; + default: return 'Période'; + } + } + + getTransactionPeriodLabel(): string { + switch (this.transactionPeriod) { + case 'daily': return 'Journalières'; + case 'weekly': return 'Hebdomadaires'; + case 'monthly': return 'Mensuelles'; + default: return 'Période'; + } + } + + prepareChartData(data: TransactionItem[]) { + if (!data || data.length === 0) { + this.chartData = []; + return; + } + + // Prendre les 6 derniers mois pour le graphique + this.chartData = data + .slice(-6) + .map(item => ({ + date: item.period, // Ex: "2025-11" + revenue: item.totalAmount || 0 + })); + } + + getCurrentTransactions(): TransactionItem[] { + switch (this.transactionPeriod) { + case 'daily': return this.dailyTransactionItems; + case 'weekly': return this.weeklyTransactionItems; + case 'monthly': return this.monthlyTransactionItems; + default: return this.dailyTransactionItems; + } + } + + getCurrentTotalSubscriptions(): number { + switch (this.subscriptionPeriod) { + case 'daily': return this.dailySubscriptions.total; + case 'weekly': return this.weeklySubscriptions.total; + case 'monthly': return this.monthlySubscriptions.total; + default: return this.dailySubscriptions.total; + } + } + + getCurrentActiveSubscriptions(): number { + switch (this.subscriptionPeriod) { + case 'daily': return this.dailySubscriptions.active; + case 'weekly': return this.weeklySubscriptions.active; + case 'monthly': return this.monthlySubscriptions.active; + default: return this.dailySubscriptions.active; + } + } + + getCurrentActivityRate(): number { + const total = this.getCurrentTotalSubscriptions(); + const active = this.getCurrentActiveSubscriptions(); + return total > 0 ? (active / total) * 100 : 0; + } + + getSuccessRate(item: TransactionItem): number { + const count = item.count || 0; + const success = item.successCount || 0; + return count > 0 ? (success / count) * 100 : 0; + } + + getPerformanceLabel(successRate: number): string { + if (successRate >= 95) return 'Excellent'; + if (successRate >= 90) return 'Bon'; + if (successRate >= 80) return 'Moyen'; + if (successRate >= 70) return 'Passable'; + return 'À améliorer'; + } + + formatCurrency(amount: number): string { + if (amount >= 1000000) { + return `${(amount / 1000000).toFixed(1)}M XOF`; + } else if (amount >= 1000) { + return `${(amount / 1000).toFixed(0)}K XOF`; + } + return `${Math.round(amount)} XOF`; + } + + revenueChart = (): ChartConfiguration => ({ + type: 'bar', + data: { + labels: this.chartData.map(item => { + // Gérer différents formats de période + if (item.date.includes('-')) { + const [year, month] = item.date.split('-'); + const months = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun', 'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc']; + const monthIndex = parseInt(month) - 1; + if (monthIndex >= 0 && monthIndex < months.length) { + return `${months[monthIndex]} ${year}`; + } + } + return item.date; + }), + datasets: [ + { + label: 'Revenue (XOF)', + data: this.chartData.map(item => Math.round(item.revenue / 1000)), // En milliers + backgroundColor: this.chartData.map((_, index) => + getColor(index === this.chartData.length - 1 ? 'chart-primary' : 'chart-secondary') + ), + borderRadius: 6, + borderSkipped: false, + } + ] + }, + options: { + responsive: true, + plugins: { + legend: { display: false }, + tooltip: { + callbacks: { + label: (context) => { + const value = context.raw as number; + return `Revenue: ${this.formatCurrency(value * 1000)}`; + } + } + } + }, + scales: { + x: { + grid: { display: false } + }, + y: { + beginAtZero: true, + ticks: { + callback: (value) => `${value}K XOF` + }, + title: { + display: true, + text: 'Revenue (en milliers XOF)' + } + } + } + } + }); + + refreshAll() { + this.loadAllData(); + } +} \ No newline at end of file diff --git a/src/app/modules/dcb-dashboard/components/operator-performance.ts b/src/app/modules/dcb-dashboard/components/operator-performance.ts deleted file mode 100644 index c6cc170..0000000 --- a/src/app/modules/dcb-dashboard/components/operator-performance.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Component } from '@angular/core'; -import { NgIconComponent } from '@ng-icons/core'; - -@Component({ - selector: 'app-operator-performance', - imports: [NgIconComponent], - template: ` -
-
-
-
-
Performance Opérateurs
-
-
- -
-
- -
-
- Orange -
- 98.5% -
-
-
-
-
- -
- MTN -
- 96.2% -
-
-
-
-
- -
- Airtel -
- 87.4% -
-
-
-
-
- -
- Moov -
- 92.1% -
-
-
-
-
-
-
- -
- `, -}) -export class OperatorPerformance {} \ No newline at end of file diff --git a/src/app/modules/dcb-dashboard/components/payment-stats.ts b/src/app/modules/dcb-dashboard/components/payment-stats.ts index 604ba39..950415f 100644 --- a/src/app/modules/dcb-dashboard/components/payment-stats.ts +++ b/src/app/modules/dcb-dashboard/components/payment-stats.ts @@ -1,17 +1,37 @@ -import { Component } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { CommonModule } from '@angular/common'; import { NgIconComponent } from '@ng-icons/core'; import { CountUpModule } from 'ngx-countup'; +import { DcbReportingService, TransactionItem } from '../services/dcb-reporting.service'; +import { catchError, finalize } from 'rxjs/operators'; +import { of, Subscription } from 'rxjs'; @Component({ selector: 'app-payment-stats', - imports: [NgIconComponent, CountUpModule], + imports: [CommonModule, NgIconComponent, CountUpModule], template: `
-
Statistiques des Paiements
+
+
Statistiques des Paiements
+ +
+
-
+ +
+
+ Chargement... +
+

Chargement des statistiques...

+
+ + +
@@ -19,14 +39,16 @@ import { CountUpModule } from 'ngx-countup';
Journalier

- 342 + + {{ dailyStats.transactions }} +

Transactions

- 245K XOF - - - 98.2% + {{ formatCurrency(dailyStats.revenue) }} + + + {{ dailyStats.successRate | number:'1.1-1' }}%
@@ -40,14 +62,16 @@ import { CountUpModule } from 'ngx-countup';
Hebdomadaire

- 2,150 + + {{ weeklyStats.transactions }} +

Transactions

- 1.58M XOF - - - 97.8% + {{ formatCurrency(weeklyStats.revenue) }} + + + {{ weeklyStats.successRate | number:'1.1-1' }}%
@@ -61,35 +85,38 @@ import { CountUpModule } from 'ngx-countup';
Mensuel

- 8,450 + + {{ monthlyStats.transactions }} +

Transactions

- 6.25M XOF - - - 96.5% + {{ formatCurrency(monthlyStats.revenue) }} + + + {{ monthlyStats.successRate | number:'1.1-1' }}%
- +
- -
Annuel
+ +
Taux global

- 12,500 + + {{ overallSuccessRate | number:'1.1-1' }} + %

-

Transactions

+

Succès (30 jours)

- 9.85M XOF - - - 95.2% + Période: 30j + + {{ getPerformanceLabel(overallSuccessRate) }}
@@ -98,16 +125,22 @@ import { CountUpModule } from 'ngx-countup';
-
+
Performance globale: - 97.4% de taux de succès + + {{ overallSuccessRate | number:'1.1-1' }}% de taux de succès + + + ({{ dailyStats.transactions }} transactions aujourd'hui) +
- Dernière mise à jour: {{ getCurrentTime() }} + + Mise à jour: {{ lastUpdated | date:'HH:mm' }}
@@ -116,12 +149,264 @@ import { CountUpModule } from 'ngx-countup';
`, + styles: [` + .spin { + animation: spin 1s linear infinite; + } + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + `] }) -export class PaymentStats { - getCurrentTime(): string { - return new Date().toLocaleTimeString('fr-FR', { - hour: '2-digit', - minute: '2-digit' +export class PaymentStats implements OnInit, OnDestroy { + loading = false; + error: string | null = null; + + // Statistiques calculées à partir des données brutes + dailyStats = { transactions: 0, revenue: 0, successRate: 0 }; + weeklyStats = { transactions: 0, revenue: 0, successRate: 0 }; + monthlyStats = { transactions: 0, revenue: 0, successRate: 0 }; + overallSuccessRate = 0; + lastUpdated = new Date(); + + // Données brutes de l'API + private dailyData: TransactionItem[] = []; + private weeklyData: TransactionItem[] = []; + private monthlyData: TransactionItem[] = []; + + private dailySubscription?: Subscription; + private weeklySubscription?: Subscription; + private monthlySubscription?: Subscription; + + constructor(private reportingService: DcbReportingService) {} + + ngOnInit() { + this.loadStats(); + } + + ngOnDestroy() { + this.cleanupSubscriptions(); + } + + private cleanupSubscriptions() { + if (this.dailySubscription) { + this.dailySubscription.unsubscribe(); + } + if (this.weeklySubscription) { + this.weeklySubscription.unsubscribe(); + } + if (this.monthlySubscription) { + this.monthlySubscription.unsubscribe(); + } + } + + loadStats() { + console.log('PaymentStats - Starting loadStats()'); + + this.cleanupSubscriptions(); // Nettoyer les anciennes souscriptions + + this.loading = true; + this.error = null; + + const today = new Date(); + const threeDaysAgo = new Date(today); + threeDaysAgo.setDate(threeDaysAgo.getDate() - 3); + + const twoWeeksAgo = new Date(today); + twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14); + + const threeMonthsAgo = new Date(today); + threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); + + console.log('PaymentStats - Date ranges:', { + threeDaysAgo: this.reportingService.formatDate(threeDaysAgo), + today: this.reportingService.formatDate(today), + twoWeeksAgo: this.reportingService.formatDate(twoWeeksAgo), + threeMonthsAgo: this.reportingService.formatDate(threeMonthsAgo) + }); + + // Charger les données séquentiellement + this.loadSequentialData(threeDaysAgo, twoWeeksAgo, threeMonthsAgo, today); + } + + loadSequentialData(threeDaysAgo: Date, twoWeeksAgo: Date, threeMonthsAgo: Date, today: Date) { + // 1. Charger les données quotidiennes + this.dailySubscription = this.reportingService.getDailyTransactions( + this.reportingService.formatDate(threeDaysAgo), + this.reportingService.formatDate(today) + ) + .pipe( + catchError(err => { + console.error('PaymentStats - Error loading daily data:', err); + return of([]); + }) + ) + .subscribe({ + next: (dailyData) => { + console.log('PaymentStats - Daily data loaded:', dailyData.length); + this.dailyData = dailyData; + + // 2. Après les données quotidiennes, charger les données hebdomadaires + this.weeklySubscription = this.reportingService.getWeeklyTransactions( + this.reportingService.formatDate(twoWeeksAgo), + this.reportingService.formatDate(today) + ) + .pipe( + catchError(err => { + console.error('PaymentStats - Error loading weekly data:', err); + return of([]); + }) + ) + .subscribe({ + next: (weeklyData) => { + console.log('PaymentStats - Weekly data loaded:', weeklyData.length); + this.weeklyData = weeklyData; + + // 3. Après les données hebdomadaires, charger les données mensuelles + this.monthlySubscription = this.reportingService.getMonthlyTransactions( + this.reportingService.formatDate(threeMonthsAgo), + this.reportingService.formatDate(today) + ) + .pipe( + catchError(err => { + console.error('PaymentStats - Error loading monthly data:', err); + return of([]); + }), + finalize(() => { + console.log('PaymentStats - All data loaded, processing...'); + this.loading = false; + this.lastUpdated = new Date(); + this.processAllData(); + }) + ) + .subscribe({ + next: (monthlyData) => { + console.log('PaymentStats - Monthly data loaded:', monthlyData.length); + this.monthlyData = monthlyData; + }, + error: (error) => { + console.error('PaymentStats - Monthly subscription error:', error); + } + }); + }, + error: (error) => { + console.error('PaymentStats - Weekly subscription error:', error); + } + }); + }, + error: (error) => { + console.error('PaymentStats - Daily subscription error:', error); + } }); } + + processAllData() { + console.log('PaymentStats - Processing all data:', { + daily: this.dailyData.length, + weekly: this.weeklyData.length, + monthly: this.monthlyData.length + }); + + // Vérifier si nous avons des données + const hasAnyData = this.dailyData.length > 0 || + this.weeklyData.length > 0 || + this.monthlyData.length > 0; + + if (!hasAnyData) { + console.warn('PaymentStats - No data available'); + return; + } + + // Calculer les statistiques avec les données disponibles + this.calculateStatsFromRawData(); + } + + calculateStatsFromRawData() { + // Calculer les statistiques pour chaque période à partir des données brutes + this.dailyStats = this.calculatePeriodStats(this.dailyData); + this.weeklyStats = this.calculatePeriodStats(this.weeklyData); + this.monthlyStats = this.calculatePeriodStats(this.monthlyData); + + // Utiliser le taux de succès mensuel comme taux global + this.overallSuccessRate = this.monthlyStats.successRate; + + // Si aucune donnée mensuelle, utiliser la meilleure période disponible + if (this.monthlyStats.transactions === 0) { + if (this.weeklyStats.transactions > 0) { + this.overallSuccessRate = this.weeklyStats.successRate; + } else if (this.dailyStats.transactions > 0) { + this.overallSuccessRate = this.dailyStats.successRate; + } + } + + console.log('PaymentStats - Calculated stats from raw data:', { + daily: this.dailyStats, + weekly: this.weeklyStats, + monthly: this.monthlyStats, + overallSuccess: this.overallSuccessRate + }); + + // Si toutes les données sont vides, afficher un message + if (this.dailyStats.transactions === 0 && + this.weeklyStats.transactions === 0 && + this.monthlyStats.transactions === 0) { + this.error = 'Les données sont actuellement vides.'; + } + } + + calculatePeriodStats(data: TransactionItem[]): { transactions: number; revenue: number; successRate: number } { + if (!data || data.length === 0) { + return { transactions: 0, revenue: 0, successRate: 0 }; + } + + // Calculer les totaux sur tous les éléments + const totalTransactions = data.reduce((sum, item) => sum + (item.count || 0), 0); + const totalRevenue = data.reduce((sum, item) => sum + (item.totalAmount || 0), 0); + const totalSuccessful = data.reduce((sum, item) => sum + (item.successCount || 0), 0); + + const successRate = totalTransactions > 0 ? (totalSuccessful / totalTransactions) * 100 : 0; + + console.log('PaymentStats - Period stats calculation:', { + dataLength: data.length, + totalTransactions, + totalRevenue, + totalSuccessful, + successRate + }); + + return { + transactions: totalTransactions, + revenue: totalRevenue, + successRate + }; + } + + getPerformanceLabel(successRate: number): string { + if (successRate >= 95) return 'Excellent'; + if (successRate >= 90) return 'Bon'; + if (successRate >= 80) return 'Moyen'; + if (successRate >= 70) return 'Passable'; + return 'À améliorer'; + } + + formatCurrency(amount: number): string { + if (amount >= 1000000) { + return `${(amount / 1000000).toFixed(1)}M XOF`; + } else if (amount >= 1000) { + return `${(amount / 1000).toFixed(0)}K XOF`; + } + return `${Math.round(amount)} XOF`; + } + + refresh() { + console.log('PaymentStats - Refreshing data'); + + // Essayer de vider le cache si disponible + if (this.reportingService.clearCache) { + this.reportingService.clearCache(); + } + + this.loadStats(); + } } \ No newline at end of file diff --git a/src/app/modules/dcb-dashboard/components/recent-transactions.ts b/src/app/modules/dcb-dashboard/components/recent-transactions.ts index 89874bd..2ed4746 100644 --- a/src/app/modules/dcb-dashboard/components/recent-transactions.ts +++ b/src/app/modules/dcb-dashboard/components/recent-transactions.ts @@ -1,190 +1,247 @@ -import { Component } from '@angular/core'; -import { NgIconComponent } from '@ng-icons/core'; -import { DecimalPipe } from '@angular/common'; -import { NgbDropdownModule, NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'; - -interface Transaction { - id: string; - user: string; - operator: string; - amount: number; - status: 'success' | 'pending' | 'failed'; - date: string; -} +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SubscriptionsService } from '../../subscriptions/subscriptions.service'; +import { Subscription} from '@core/models/dcb-bo-hub-subscription.model'; +import { catchError, finalize } from 'rxjs/operators'; +import { of } from 'rxjs'; @Component({ selector: 'app-recent-transactions', - imports: [ - NgIconComponent, - DecimalPipe, - NgbPaginationModule, - NgbDropdownModule, - ], + imports: [CommonModule], template: `
-
-

Transactions Récentes

- - - Exporter - +
+
+

Abonnements Récents

+
+ + Voir tout +
+
- +
-
- + +
+
+ Chargement... +
+

Chargement des abonnements...

+
+ + +
+ +

{{ error }}

+ +
+ + +
+
- - - - + + - + + + - @for (transaction of transactions; track transaction.id) { - - - - - - - - - } + + + + + + + + + + + +
TransactionUtilisateurOpérateurMontantIDDate de création StatutMontantPériodicitéClient
- #{{ transaction.id }} - - {{ transaction.user }} - - {{ transaction.operator }} - - {{ transaction.amount | number }} XOF - - - {{ getStatusText(transaction.status) }} - - - -
+
#{{ subscription.id }}
+ {{ subscription.externalReference || 'N/A' }} +
+
{{ subscription.createdAt | date:'dd/MM/yyyy' }}
+ {{ subscription.createdAt | date:'HH:mm' }} +
+ + + {{ getStatusLabel(subscription.status) }} + + +
{{ subscription.amount | currency:subscription.currency:'symbol':'1.0-0' }}
+ {{ subscription.currency }} +
+
{{ getPeriodicityLabel(subscription.periodicity) }}
+ + Prochain: {{ subscription.nextPaymentDate | date:'dd/MM' }} + +
+
Client #{{ subscription.customerId }}
+ Partenaire: {{ subscription.merchantPartnerId }} +
+
+ +

Aucun abonnement trouvé

+
+
-
- \ No newline at end of file diff --git a/src/app/modules/dcb-dashboard/dcb-dashboard.ts b/src/app/modules/dcb-dashboard/dcb-dashboard.ts index 957385b..ad7e254 100644 --- a/src/app/modules/dcb-dashboard/dcb-dashboard.ts +++ b/src/app/modules/dcb-dashboard/dcb-dashboard.ts @@ -1,24 +1,14 @@ import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; import { PageTitle } from '@app/components/page-title/page-title'; -import { PaymentStats } from './components/payment-stats'; -import { ActiveSubscriptions } from './components/active-subscriptions'; -import { RevenueChart } from './components/revenue-chart'; -import { OperatorPerformance } from './components/operator-performance'; -import { RecentTransactions } from './components/recent-transactions'; -import { SystemHealth } from './components/system-health'; -import { AlertWidget } from './components/alert-widget'; +import { DashboardReport } from './components/dcb-dashboard-report'; @Component({ selector: 'app-dcb-dashboard', imports: [ + CommonModule, PageTitle, - PaymentStats, - ActiveSubscriptions, - RevenueChart, - OperatorPerformance, - RecentTransactions, - SystemHealth, - AlertWidget, + DashboardReport, ], templateUrl: './dcb-dashboard.html', }) diff --git a/src/app/modules/dcb-dashboard/models/dcb-dashboard.models.ts b/src/app/modules/dcb-dashboard/models/dcb-dashboard.models.ts index 82fe287..7261249 100644 --- a/src/app/modules/dcb-dashboard/models/dcb-dashboard.models.ts +++ b/src/app/modules/dcb-dashboard/models/dcb-dashboard.models.ts @@ -47,4 +47,59 @@ export interface DcbDashboardData { subscriptionStats: SubscriptionStatsModel; alerts: Alert[]; lastUpdated: Date; +} + +// Interface pour la réponse brute de l'API +export interface ApiTransactionItem { + period: string; + totalAmount: number; + totalTax: number; + count: number; + successCount: number; + failedCount: number; + pendingCount: number; + merchantPartnerId?: number; +} + +export interface ApiTransactionResponse { + type: string; + period: string; + startDate: string; + endDate: string; + merchantPartnerId?: number; + totalAmount: number; + totalCount: number; + items: ApiTransactionItem[]; + summary: { + avgAmount: number; + minAmount: number; + maxAmount: number; + }; + generatedAt: string; +} + +export interface ApiSubscriptionItem { + period: string; + active: number; + trial: number; + cancelled: number; + expired: number; + new: number; + total: number; + merchantPartnerId?: number; +} + +export interface ApiSubscriptionResponse { + type: string; + period: string; + startDate: string; + endDate: string; + merchantPartnerId?: number; + totalCount: number; + items: ApiSubscriptionItem[]; + summary: { + avgActive: number; + avgNew: number; + }; + generatedAt: string; } \ No newline at end of file diff --git a/src/app/modules/dcb-dashboard/services/dcb-dashboard.service.ts b/src/app/modules/dcb-dashboard/services/dcb-dashboard.service.ts index 023ab40..d81ce9a 100644 --- a/src/app/modules/dcb-dashboard/services/dcb-dashboard.service.ts +++ b/src/app/modules/dcb-dashboard/services/dcb-dashboard.service.ts @@ -1,22 +1,49 @@ import { Injectable, inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable, of } from 'rxjs'; -import { map, catchError } from 'rxjs/operators'; -import { DcbDashboardData, KpiCardModel, PaymentChartData, SubscriptionStatsModel, Alert } from '../models/dcb-dashboard.models'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable, of, forkJoin } from 'rxjs'; +import { map, catchError, shareReplay } from 'rxjs/operators'; +import { + DcbDashboardData, + KpiCardModel, + PaymentChartData, + SubscriptionStatsModel, + Alert +} from '../models/dcb-dashboard.models'; import { environment } from '@environments/environment'; +export interface TransactionReport { + date: string; + successful: number; + failed: number; + pending: number; + revenue: number; + total: number; +} + +export interface SubscriptionReport { + date: string; + active: number; + trial: number; + cancelled: number; + expired: number; + new: number; + total: number; +} + @Injectable({ providedIn: 'root' }) export class DcbDashboardService { private http = inject(HttpClient); - private apiUrl = `${environment.iamApiUrl}/dcb-dashboard`; + private reportingApiUrl = `${environment.reportingApiUrl}/reporting`; + + private cache = new Map>(); - // Données mockées pour le développement + // Données mockées pour fallback private mockData: DcbDashboardData = { kpis: [ { - title: 'Revenue Total', - value: 125430, - change: 12.5, + title: 'Revenue Mensuel', + value: 0, + change: 0, changeType: 'positive', icon: 'lucideTrendingUp', color: 'success', @@ -24,110 +51,409 @@ export class DcbDashboardService { currency: 'XOF' }, { - title: 'Paiements Today', - value: 342, - change: -2.3, - changeType: 'negative', + title: 'Transactions Journalières', + value: 0, + change: 0, + changeType: 'positive', icon: 'lucideCreditCard', color: 'primary', format: 'number' }, { title: 'Taux de Succès', - value: 98.2, - change: 1.2, + value: 0, + change: 0, changeType: 'positive', icon: 'lucideCheckCircle', color: 'info', format: 'percentage' }, { - title: 'Abonnements Actifs', - value: 12543, - change: 8.7, + title: 'Nouveaux Abonnements', + value: 0, + change: 0, changeType: 'positive', icon: 'lucideUsers', color: 'warning', format: 'number' } ], - paymentChart: [ - { period: 'Jan', successful: 12000, failed: 300, pending: 500, revenue: 4500000 }, - { period: 'Fév', successful: 15000, failed: 250, pending: 400, revenue: 5200000 }, - { period: 'Mar', successful: 18000, failed: 200, pending: 300, revenue: 6100000 }, - { period: 'Avr', successful: 22000, failed: 150, pending: 250, revenue: 7500000 }, - { period: 'Mai', successful: 25000, failed: 100, pending: 200, revenue: 8900000 }, - { period: 'Jun', successful: 30000, failed: 80, pending: 150, revenue: 10500000 } - ], + paymentChart: [], subscriptionStats: { - total: 15600, - active: 12543, - trial: 1850, - cancelled: 857, - expired: 350, - growthRate: 8.7 + total: 0, + active: 0, + trial: 0, + cancelled: 0, + expired: 0, + growthRate: 0 }, - alerts: [ - { - id: '1', - type: 'warning', - title: 'Taux d\'échec élevé Orange CI', - message: 'Le taux d\'échec des paiements Orange Côte d\'Ivoire a augmenté de 15%', - timestamp: new Date(Date.now() - 30 * 60 * 1000), // 30 minutes ago - acknowledged: false, - priority: 'medium', - action: { - label: 'Voir les détails', - route: '/payments' - } - }, - { - id: '2', - type: 'info', - title: 'Maintenance planifiée', - message: 'Maintenance système prévue ce soir de 22h à 00h', - timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago - acknowledged: false, - priority: 'low' - }, - { - id: '3', - type: 'success', - title: 'Nouveau partenaire activé', - message: 'Le partenaire "MTN Senegal" a été configuré avec succès', - timestamp: new Date(Date.now() - 4 * 60 * 60 * 1000), // 4 hours ago - acknowledged: true, - priority: 'low' - } - ], + alerts: [], lastUpdated: new Date() }; + /** + * Récupère toutes les données du dashboard + */ getDcbDashboardData(): Observable { - // En production, utiliser l'API réelle - if (environment.production) { - return this.http.get(this.apiUrl).pipe( - catchError(() => of(this.mockData)) // Fallback sur les données mockées en cas d'erreur + const cacheKey = 'dashboard-full-data'; + + if (!this.cache.has(cacheKey)) { + const dashboardData$ = this.fetchAllDashboardData().pipe( + shareReplay({ bufferSize: 1, refCount: true }), + catchError(error => { + console.error('Erreur dashboard:', error); + return of(this.mockData); + }) ); + + this.cache.set(cacheKey, dashboardData$); } - // En développement, retourner les données mockées - return of(this.mockData); + return this.cache.get(cacheKey)!; } - acknowledgeAlert(alertId: string): Observable<{ success: boolean }> { - // Simuler l'acknowledgement - const alert = this.mockData.alerts.find(a => a.id === alertId); - if (alert) { - alert.acknowledged = true; - } + /** + * Récupère toutes les données nécessaires + */ + private fetchAllDashboardData(): Observable { + /*if (!environment.production) { + return of(this.mockData); + }*/ + + // Calcul des dates (30 derniers jours) + const endDate = new Date(); + const startDate = new Date(); + startDate.setDate(startDate.getDate() - 30); + + const startDateStr = this.formatDate(startDate); + const endDateStr = this.formatDate(endDate); + + // Récupération des transactions mensuelles pour les KPIs + return this.getMonthlyTransactions(startDateStr, endDateStr).pipe( + map(monthlyData => this.transformDashboardData(monthlyData)) + ); + } + + /** + * Récupère les transactions journalières (pour le graphique) + */ + getDailyTransactions( + startDate?: string, + endDate?: string, + merchantPartnerId?: number + ): Observable { + let params = new HttpParams(); - return of({ success: true }); + if (startDate) params = params.set('startDate', startDate); + if (endDate) params = params.set('endDate', endDate); + if (merchantPartnerId) params = params.set('merchantPartnerId', merchantPartnerId.toString()); + + const url = `${this.reportingApiUrl}/transactions/daily`; + return this.http.get(url, { params }).pipe( + map(response => response.data || []), + catchError(() => of([])) + ); } + /** + * Récupère les transactions mensuelles (pour les KPIs) + */ + getMonthlyTransactions( + startDate?: string, + endDate?: string, + merchantPartnerId?: number + ): Observable { + let params = new HttpParams(); + + if (startDate) params = params.set('startDate', startDate); + if (endDate) params = params.set('endDate', endDate); + if (merchantPartnerId) params = params.set('merchantPartnerId', merchantPartnerId.toString()); + + const url = `${this.reportingApiUrl}/transactions/monthly`; + return this.http.get(url, { params }).pipe( + map(response => response.data || []), + catchError(() => of([])) + ); + } + + /** + * Récupère les abonnements journaliers + */ + getDailySubscriptions( + startDate?: string, + endDate?: string, + merchantPartnerId?: number + ): Observable { + let params = new HttpParams(); + + if (startDate) params = params.set('startDate', startDate); + if (endDate) params = params.set('endDate', endDate); + if (merchantPartnerId) params = params.set('merchantPartnerId', merchantPartnerId.toString()); + + const url = `${this.reportingApiUrl}/subscriptions/daily`; + return this.http.get(url, { params }).pipe( + map(response => response.data || []), + catchError(() => of([])) + ); + } + + /** + * Récupère les abonnements mensuels + */ + getMonthlySubscriptions( + startDate?: string, + endDate?: string, + merchantPartnerId?: number + ): Observable { + let params = new HttpParams(); + + if (startDate) params = params.set('startDate', startDate); + if (endDate) params = params.set('endDate', endDate); + if (merchantPartnerId) params = params.set('merchantPartnerId', merchantPartnerId.toString()); + + const url = `${this.reportingApiUrl}/subscriptions/monthly`; + return this.http.get(url, { params }).pipe( + map(response => response.data || []), + catchError(() => of([])) + ); + } + + /** + * Lance une synchronisation manuelle + */ + triggerManualSync(): Observable<{ success: boolean; message?: string }> { + const url = `${this.reportingApiUrl}/sync/full`; + return this.http.post<{ success: boolean; message?: string }>(url, {}).pipe( + catchError(error => of({ + success: false, + message: error.message || 'Erreur lors de la synchronisation' + })) + ); + } + + /** + * Transforme les données d'API en format dashboard + */ + private transformDashboardData(monthlyTransactions: TransactionReport[]): DcbDashboardData { + // Calcul des KPIs à partir des transactions mensuelles + const currentMonth = this.getCurrentMonthData(monthlyTransactions); + const previousMonth = this.getPreviousMonthData(monthlyTransactions); + + const totalRevenue = this.calculateTotalRevenue(monthlyTransactions); + const revenueChange = this.calculateRevenueChange(currentMonth, previousMonth); + + const totalTransactions = this.calculateTotalTransactions(monthlyTransactions); + const transactionChange = this.calculateTransactionChange(currentMonth, previousMonth); + + const successRate = this.calculateSuccessRate(monthlyTransactions); + const successRateChange = this.calculateSuccessRateChange(currentMonth, previousMonth); + + // Récupération des abonnements pour le 4ème KPI + const today = this.formatDate(new Date()); + const lastMonthDate = this.formatDate(new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)); + + return { + kpis: [ + { + title: 'Revenue Mensuel', + value: totalRevenue, + change: revenueChange, + changeType: revenueChange >= 0 ? 'positive' : 'negative', + icon: 'lucideTrendingUp', + color: 'success', + format: 'currency', + currency: 'XOF' + }, + { + title: 'Transactions Total', + value: totalTransactions, + change: transactionChange, + changeType: transactionChange >= 0 ? 'positive' : 'negative', + icon: 'lucideCreditCard', + color: 'primary', + format: 'number' + }, + { + title: 'Taux de Succès', + value: successRate, + change: successRateChange, + changeType: successRateChange >= 0 ? 'positive' : 'negative', + icon: 'lucideCheckCircle', + color: 'info', + format: 'percentage' + }, + { + title: 'Nouveaux Abonnements', + value: 0, // À calculer via getDailySubscriptions + change: 0, + changeType: 'positive', + icon: 'lucideUsers', + color: 'warning', + format: 'number' + } + ], + paymentChart: this.preparePaymentChartData(monthlyTransactions), + subscriptionStats: { + total: 0, // À calculer via getMonthlySubscriptions + active: 0, + trial: 0, + cancelled: 0, + expired: 0, + growthRate: 0 + }, + alerts: [], + lastUpdated: new Date() + }; + } + + /** + * Calcule le revenue total des dernières transactions + */ + private calculateTotalRevenue(transactions: TransactionReport[]): number { + if (!transactions || transactions.length === 0) return 0; + + return transactions.reduce((sum, transaction) => sum + (transaction.revenue || 0), 0); + } + + /** + * Calcule le changement de revenue + */ + private calculateRevenueChange(currentMonth: TransactionReport[], previousMonth: TransactionReport[]): number { + const currentRevenue = this.calculateTotalRevenue(currentMonth); + const previousRevenue = this.calculateTotalRevenue(previousMonth); + + if (previousRevenue === 0) return currentRevenue > 0 ? 100 : 0; + + return ((currentRevenue - previousRevenue) / previousRevenue) * 100; + } + + /** + * Calcule le total des transactions + */ + private calculateTotalTransactions(transactions: TransactionReport[]): number { + if (!transactions || transactions.length === 0) return 0; + + return transactions.reduce((sum, transaction) => sum + (transaction.total || 0), 0); + } + + /** + * Calcule le changement de transactions + */ + private calculateTransactionChange(currentMonth: TransactionReport[], previousMonth: TransactionReport[]): number { + const currentTransactions = this.calculateTotalTransactions(currentMonth); + const previousTransactions = this.calculateTotalTransactions(previousMonth); + + if (previousTransactions === 0) return currentTransactions > 0 ? 100 : 0; + + return ((currentTransactions - previousTransactions) / previousTransactions) * 100; + } + + /** + * Calcule le taux de succès moyen + */ + private calculateSuccessRate(transactions: TransactionReport[]): number { + if (!transactions || transactions.length === 0) return 0; + + let totalSuccess = 0; + let totalTransactions = 0; + + transactions.forEach(transaction => { + totalSuccess += transaction.successful || 0; + totalTransactions += transaction.total || 0; + }); + + if (totalTransactions === 0) return 0; + + return (totalSuccess / totalTransactions) * 100; + } + + /** + * Calcule le changement du taux de succès + */ + private calculateSuccessRateChange(currentMonth: TransactionReport[], previousMonth: TransactionReport[]): number { + const currentRate = this.calculateSuccessRate(currentMonth); + const previousRate = this.calculateSuccessRate(previousMonth); + + if (previousRate === 0) return currentRate > 0 ? 100 : 0; + + return currentRate - previousRate; + } + + /** + * Prépare les données pour le graphique + */ + private preparePaymentChartData(transactions: TransactionReport[]): PaymentChartData[] { + if (!transactions || transactions.length === 0) return []; + + // Limiter aux 6 derniers mois pour le graphique + const recentTransactions = transactions.slice(-6); + + return recentTransactions.map(transaction => ({ + period: this.formatMonthName(transaction.date), + successful: transaction.successful || 0, + failed: transaction.failed || 0, + pending: transaction.pending || 0, + revenue: transaction.revenue || 0 + })); + } + + /** + * Récupère les données du mois courant + */ + private getCurrentMonthData(transactions: TransactionReport[]): TransactionReport[] { + const currentMonth = new Date().getMonth(); + const currentYear = new Date().getFullYear(); + + return transactions.filter(transaction => { + const transactionDate = new Date(transaction.date); + return transactionDate.getMonth() === currentMonth && + transactionDate.getFullYear() === currentYear; + }); + } + + /** + * Récupère les données du mois précédent + */ + private getPreviousMonthData(transactions: TransactionReport[]): TransactionReport[] { + const now = new Date(); + const previousMonth = now.getMonth() === 0 ? 11 : now.getMonth() - 1; + const year = now.getMonth() === 0 ? now.getFullYear() - 1 : now.getFullYear(); + + return transactions.filter(transaction => { + const transactionDate = new Date(transaction.date); + return transactionDate.getMonth() === previousMonth && + transactionDate.getFullYear() === year; + }); + } + + /** + * Format une date au format YYYY-MM-DD + */ + private formatDate(date: Date): string { + return date.toISOString().split('T')[0]; + } + + /** + * Format le nom du mois à partir d'une date + */ + private formatMonthName(dateString: string): string { + const date = new Date(dateString); + const months = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun', 'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc']; + return months[date.getMonth()]; + } + + /** + * Vide le cache + */ + clearCache(): void { + this.cache.clear(); + } + + /** + * Rafraîchit les données + */ refreshData(): Observable { - // Simuler un rafraîchissement des données - this.mockData.lastUpdated = new Date(); - return of(this.mockData); + this.clearCache(); + return this.getDcbDashboardData(); } } \ No newline at end of file diff --git a/src/app/modules/dcb-dashboard/services/dcb-reporting.service.ts b/src/app/modules/dcb-dashboard/services/dcb-reporting.service.ts new file mode 100644 index 0000000..306c697 --- /dev/null +++ b/src/app/modules/dcb-dashboard/services/dcb-reporting.service.ts @@ -0,0 +1,256 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable, of } from 'rxjs'; +import { map, catchError } from 'rxjs/operators'; +import { environment } from '@environments/environment'; + +// Interfaces pour les réponses de l'API +export interface TransactionItem { + period: string; + totalAmount: number; + totalTax: number; + count: number; + successCount: number; + failedCount: number; + pendingCount: number; + merchantPartnerId?: number; +} + +export interface TransactionResponse { + type: string; + period: string; + startDate: string; + endDate: string; + merchantPartnerId?: number; + totalAmount: number; + totalCount: number; + items: TransactionItem[]; + summary: { + avgAmount: number; + minAmount: number; + maxAmount: number; + }; + generatedAt: string; +} + +export interface SubscriptionItem { + period: string; + totalAmount: number; + count: number; + activeCount: number; + cancelledCount: number; +} + +export interface SubscriptionResponse { + type: string; + period: string; + startDate: string; + endDate: string; + merchantPartnerId?: number; + totalAmount: number; + totalCount: number; + items: SubscriptionItem[]; + summary: { + avgAmount: number; + minAmount: number; + maxAmount: number; + }; + generatedAt: string; +} + +@Injectable({ providedIn: 'root' }) +export class DcbReportingService { + private http = inject(HttpClient); + private reportingApiUrl = `${environment.reportingApiUrl}/reporting`; + + /** + * Récupère les transactions journalières + */ + getDailyTransactions( + startDate?: string, + endDate?: string, + merchantPartnerId?: number + ): Observable { + let params = new HttpParams(); + + if (startDate) params = params.set('startDate', startDate); + if (endDate) params = params.set('endDate', endDate); + if (merchantPartnerId) params = params.set('merchantPartnerId', merchantPartnerId.toString()); + + const url = `${this.reportingApiUrl}/transactions/daily`; + console.log('Fetching daily transactions from:', url, 'with params:', params.toString()); + + return this.http.get(url, { params }).pipe( + map(response => { + console.log('Daily transactions raw response:', response); + return response?.items || []; + }), + catchError((error) => { + console.error('Error fetching daily transactions:', error); + return of([]); + }) + ); + } + + /** + * Récupère les transactions hebdomadaires + */ + getWeeklyTransactions( + startDate?: string, + endDate?: string, + merchantPartnerId?: number + ): Observable { + let params = new HttpParams(); + + if (startDate) params = params.set('startDate', startDate); + if (endDate) params = params.set('endDate', endDate); + if (merchantPartnerId) params = params.set('merchantPartnerId', merchantPartnerId.toString()); + + const url = `${this.reportingApiUrl}/transactions/weekly`; + console.log('Fetching weekly transactions from:', url); + + return this.http.get(url, { params }).pipe( + map(response => { + console.log('Weekly transactions raw response:', response); + return response?.items || []; + }), + catchError((error) => { + console.error('Error fetching weekly transactions:', error); + return of([]); + }) + ); + } + + /** + * Récupère les transactions mensuelles + */ + getMonthlyTransactions( + startDate?: string, + endDate?: string, + merchantPartnerId?: number + ): Observable { + let params = new HttpParams(); + + if (startDate) params = params.set('startDate', startDate); + if (endDate) params = params.set('endDate', endDate); + if (merchantPartnerId) params = params.set('merchantPartnerId', merchantPartnerId.toString()); + + const url = `${this.reportingApiUrl}/transactions/monthly`; + console.log('Fetching monthly transactions from:', url); + + return this.http.get(url, { params }).pipe( + map(response => { + console.log('Monthly transactions raw response:', response); + return response?.items || []; + }), + catchError((error) => { + console.error('Error fetching monthly transactions:', error); + return of([]); + }) + ); + } + + /** + * Récupère les abonnements journaliers + */ + getDailySubscriptions( + startDate?: string, + endDate?: string, + merchantPartnerId?: number + ): Observable { + let params = new HttpParams(); + + if (startDate) params = params.set('startDate', startDate); + if (endDate) params = params.set('endDate', endDate); + if (merchantPartnerId) params = params.set('merchantPartnerId', merchantPartnerId.toString()); + + const url = `${this.reportingApiUrl}/subscriptions/daily`; + console.log('Fetching daily subscriptions from:', url); + + return this.http.get(url, { params }).pipe( + map(response => { + console.log('Daily subscriptions raw response:', response); + return response?.items || []; + }), + catchError((error) => { + console.error('Error fetching daily subscriptions:', error); + return of([]); + }) + ); + } + + /** + * Récupère les abonnements hebdomadaires + */ + getWeeklySubscriptions( + startDate?: string, + endDate?: string, + merchantPartnerId?: number + ): Observable { + let params = new HttpParams(); + + if (startDate) params = params.set('startDate', startDate); + if (endDate) params = params.set('endDate', endDate); + if (merchantPartnerId) params = params.set('merchantPartnerId', merchantPartnerId.toString()); + + const url = `${this.reportingApiUrl}/subscriptions/weekly`; + console.log('Fetching weekly subscriptions from:', url); + + return this.http.get(url, { params }).pipe( + map(response => { + console.log('Weekly subscriptions raw response:', response); + return response?.items || []; + }), + catchError((error) => { + console.error('Error fetching weekly subscriptions:', error); + return of([]); + }) + ); + } + + /** + * Récupère les abonnements mensuels + */ + getMonthlySubscriptions( + startDate?: string, + endDate?: string, + merchantPartnerId?: number + ): Observable { + let params = new HttpParams(); + + if (startDate) params = params.set('startDate', startDate); + if (endDate) params = params.set('endDate', endDate); + if (merchantPartnerId) params = params.set('merchantPartnerId', merchantPartnerId.toString()); + + const url = `${this.reportingApiUrl}/subscriptions/monthly`; + console.log('Fetching monthly subscriptions from:', url); + + return this.http.get(url, { params }).pipe( + map(response => { + console.log('Monthly subscriptions raw response:', response); + return response?.items || []; + }), + catchError((error) => { + console.error('Error fetching monthly subscriptions:', error); + return of([]); + }) + ); + } + + /** + * Format une date au format YYYY-MM-DD + */ + formatDate(date: Date): string { + const year = date.getFullYear(); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const day = date.getDate().toString().padStart(2, '0'); + return `${year}-${month}-${day}`; + } + + /** + * Vide le cache + */ + clearCache(): void { + // Pas de cache dans cette version simplifiée + } +} \ No newline at end of file diff --git a/src/app/modules/subscriptions/subscriptions.service.ts b/src/app/modules/subscriptions/subscriptions.service.ts index 0a45fed..904737a 100644 --- a/src/app/modules/subscriptions/subscriptions.service.ts +++ b/src/app/modules/subscriptions/subscriptions.service.ts @@ -19,6 +19,19 @@ export interface MessageResponse { message: string; } +// Interface pour la réponse paginée de l'API +export interface ApiSubscriptionsResponse { + data: any[]; // Les données brutes de l'API + meta: { + total: number; + page: number; + limit: number; + totalPages: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + }; +} + // ===== SERVICE SUBSCRIPTIONS ===== @Injectable({ providedIn: 'root' }) @@ -44,11 +57,16 @@ export class SubscriptionsService { }); } - return this.http.get(this.subcriptionBaseApiUrl, { params: httpParams }).pipe( - map(subscriptions => ({ - subscriptions: subscriptions.map(sub => this.mapToSubscriptionModel(sub)), - statistics: this.calculateSubscriptionStats(subscriptions) - })), + return this.http.get(this.subcriptionBaseApiUrl, { params: httpParams }).pipe( + map(apiResponse => { + // Vérifier si apiResponse.data existe et est un tableau + const subscriptionsData = apiResponse?.data || []; + + return { + subscriptions: subscriptionsData.map((sub: any) => this.mapToSubscriptionModel(sub)), + statistics: this.calculateSubscriptionStats(subscriptionsData) + }; + }), catchError(error => { console.error('Error loading subscriptions:', error); return throwError(() => error); @@ -73,13 +91,17 @@ export class SubscriptionsService { } } - return this.http.get(`${this.subcriptionBaseApiUrl}/merchant/${merchantId}`, { + return this.http.get(`${this.subcriptionBaseApiUrl}/merchant/${merchantId}`, { params: httpParams }).pipe( - map(subscriptions => ({ - subscriptions: subscriptions.map(sub => this.mapToSubscriptionModel(sub)), - statistics: this.calculateSubscriptionStats(subscriptions) - })), + map(apiResponse => { + const subscriptionsData = apiResponse?.data || []; + + return { + subscriptions: subscriptionsData.map((sub: any) => this.mapToSubscriptionModel(sub)), + statistics: this.calculateSubscriptionStats(subscriptionsData) + }; + }), catchError(error => { console.error(`Error loading subscriptions for merchant ${merchantId}:`, error); return throwError(() => error); @@ -91,8 +113,12 @@ export class SubscriptionsService { * Récupère un abonnement par son ID */ getSubscriptionById(subscriptionId: string): Observable { - return this.http.get(`${this.subcriptionBaseApiUrl}/${subscriptionId}`).pipe( - map(subscription => this.mapToSubscriptionModel(subscription)), + return this.http.get(`${this.subcriptionBaseApiUrl}/${subscriptionId}`).pipe( + map(apiResponse => { + // L'API retourne probablement un objet avec data[0] + const subscriptionData = apiResponse?.data?.[0] || apiResponse; + return this.mapToSubscriptionModel(subscriptionData); + }), catchError(error => { console.error(`Error loading subscription ${subscriptionId}:`, error); return throwError(() => error); @@ -108,14 +134,18 @@ export class SubscriptionsService { const merchantIdNum = typeof merchantId === 'string' ? parseInt(merchantId, 10) : merchantId; const subscriptionIdNum = typeof subscriptionId === 'string' ? parseInt(subscriptionId, 10) : subscriptionId; - return this.http.get( + return this.http.get( `${this.paymentBaseApiUrl}/merchant/${merchantIdNum}/subscription/${subscriptionIdNum}` ).pipe( - map(payments => ({ - merchantId: merchantIdNum.toString(), - subscriptionId: subscriptionIdNum.toString(), - payments: payments.map(payment => this.mapToPaymentModel(payment)) - })), + map(apiResponse => { + const paymentsData = apiResponse?.data || []; + + return { + merchantId: merchantIdNum.toString(), + subscriptionId: subscriptionIdNum.toString(), + payments: paymentsData.map((payment: any) => this.mapToPaymentModel(payment)) + }; + }), catchError(error => { console.error(`Error loading payments for subscription ${subscriptionId} for merchant ${merchantId}:`, error); return throwError(() => error); @@ -160,6 +190,16 @@ export class SubscriptionsService { return this.getSubscriptionsByStatus(SubscriptionStatus.SUSPENDED); } + /** + * Récupère les abonnements récents (pour le dashboard) + */ + getRecentSubscriptions(limit: number = 10): Observable { + return this.getSubscriptions({ + page: 1, + limit, + }); + } + // === MÉTHODES UTILITAIRES === /** @@ -174,10 +214,26 @@ export class SubscriptionsService { averageAmount: number; } { const total = subscriptions.length; - const active = subscriptions.filter(sub => sub.status === SubscriptionStatus.ACTIVE).length; - const suspended = subscriptions.filter(sub => sub.status === SubscriptionStatus.SUSPENDED).length; - const cancelled = subscriptions.filter(sub => sub.status === SubscriptionStatus.CANCELLED).length; - const totalRevenue = subscriptions.reduce((sum, sub) => sum + sub.amount, 0); + + // Compter par statut + const active = subscriptions.filter(sub => + sub.status === SubscriptionStatus.ACTIVE || sub.status === 'ACTIVE' + ).length; + + const suspended = subscriptions.filter(sub => + sub.status === SubscriptionStatus.SUSPENDED || sub.status === 'SUSPENDED' + ).length; + + const cancelled = subscriptions.filter(sub => + sub.status === SubscriptionStatus.CANCELLED || sub.status === 'CANCELLED' + ).length; + + // Calculer le revenue total + const totalRevenue = subscriptions.reduce((sum, sub) => { + const amount = parseFloat(sub.amount) || 0; + return sum + amount; + }, 0); + const averageAmount = total > 0 ? totalRevenue / total : 0; return { @@ -197,20 +253,20 @@ export class SubscriptionsService { return { id: apiSubscription.id, externalReference: apiSubscription.externalReference, - periodicity: apiSubscription.periodicity as SubscriptionPeriodicity, + periodicity: (apiSubscription.periodicity as SubscriptionPeriodicity) || SubscriptionPeriodicity.DAILY, startDate: apiSubscription.startDate, endDate: apiSubscription.endDate, - amount: apiSubscription.amount, - currency: apiSubscription.currency as Currency, + amount: parseFloat(apiSubscription.amount) || 0, + currency: (apiSubscription.currency as Currency) || Currency.XOF, token: apiSubscription.token, - status: apiSubscription.status as SubscriptionStatus, + status: (apiSubscription.status as SubscriptionStatus) || SubscriptionStatus.ACTIVE, nextPaymentDate: apiSubscription.nextPaymentDate, suspendedAt: apiSubscription.suspendedAt, merchantPartnerId: apiSubscription.merchantPartnerId, customerId: apiSubscription.customerId, planId: apiSubscription.planId, serviceId: apiSubscription.serviceId, - failureCount: apiSubscription.failureCount, + failureCount: apiSubscription.failureCount || 0, createdAt: apiSubscription.createdAt, updatedAt: apiSubscription.updatedAt, metadata: apiSubscription.metadata || {} @@ -224,9 +280,9 @@ export class SubscriptionsService { return { id: apiPayment.id, subscriptionId: apiPayment.subscriptionId, - amount: apiPayment.amount, - currency: apiPayment.currency as Currency, - status: apiPayment.status as 'PENDING' | 'SUCCESS' | 'FAILED', + amount: parseFloat(apiPayment.amount) || 0, + currency: (apiPayment.currency as Currency) || Currency.XOF, + status: apiPayment.status as 'PENDING' | 'SUCCESS' | 'FAILED' || 'PENDING', reference: apiPayment.reference, description: apiPayment.description, metadata: apiPayment.metadata || { internatRef: '' }, diff --git a/src/environments/environment.preprod.ts b/src/environments/environment.preprod.ts index 900df35..33da13e 100644 --- a/src/environments/environment.preprod.ts +++ b/src/environments/environment.preprod.ts @@ -4,4 +4,5 @@ export const environment = { iamApiUrl: "https://api-user-service.dcb.pixpay.sn/api/v1", configApiUrl: 'https://api-merchant-config-service.dcb.pixpay.sn/api/v1', apiCoreUrl: 'https://api-core-service.dcb.pixpay.sn/api/v1', + reportingApiUrl: 'https://api-reporting-service.dcb.pixpay.sn/api/v1/', }; diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 900df35..33da13e 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -4,4 +4,5 @@ export const environment = { iamApiUrl: "https://api-user-service.dcb.pixpay.sn/api/v1", configApiUrl: 'https://api-merchant-config-service.dcb.pixpay.sn/api/v1', apiCoreUrl: 'https://api-core-service.dcb.pixpay.sn/api/v1', + reportingApiUrl: 'https://api-reporting-service.dcb.pixpay.sn/api/v1/', }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index eb55f4c..80c1eef 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -4,4 +4,5 @@ export const environment = { iamApiUrl: "http://localhost:3000/api/v1", configApiUrl: 'https://api-merchant-config-service.dcb.pixpay.sn/api/v1', apiCoreUrl: 'https://api-core-service.dcb.pixpay.sn/api/v1', + reportingApiUrl: 'https://api-reporting-service.dcb.pixpay.sn/api/v1', } \ No newline at end of file