fix error

This commit is contained in:
Mamadou Khoussa [028918 DSI/DAC/DIF/DS] 2025-10-25 00:28:27 +00:00
parent 3eae8d2805
commit 1ddc8e9ee4
28 changed files with 25 additions and 2081 deletions

View File

@ -13,13 +13,10 @@ import databaseConfig from './config/database.config';
// Import des modules
import { PrismaService } from './shared/services/prisma.service';
import { AuthModule } from './modules/auth/auth.module';
import { PartnersModule } from './modules/partners/partners.module';
import { OperatorsModule } from './modules/operators/operators.module';
import { OperatorsModule } from './modules/operators/operators.module';
import { PaymentsModule } from './modules/payments/payments.module';
import { SubscriptionsModule } from './modules/subscriptions/subscriptions.module';
import { NotificationsModule } from './modules/notifications/notifications.module';
import { OtpChallengeModule } from './modules/challenge/otp.challenge.module';
import { OtpChallengeModule } from './modules/challenge/otp.challenge.module';
@Module({
imports: [
@ -51,12 +48,9 @@ import { OtpChallengeModule } from './modules/challenge/otp.challenge.module';
}),
ScheduleModule.forRoot(),
EventEmitterModule.forRoot(),
AuthModule,
PartnersModule,
OperatorsModule,
PaymentsModule,
SubscriptionsModule,
NotificationsModule,
OtpChallengeModule
],
providers: [PrismaService],

View File

@ -30,7 +30,6 @@ async function bootstrap() {
.setDescription('Unified DCB Payment Aggregation Platform')
.setVersion('1.0.0')
.addBearerAuth()
.addTag('auth')
.addTag('payments')
.addTag('subscriptions')
.build();

View File

@ -1,10 +0,0 @@
import { Controller, Get } from "@nestjs/common";
//todo
@Controller()
export class AuthController{
@Get()
getHello(): string {
return 'Hello World!';
}
}

View File

@ -1,41 +0,0 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './strategies/jwt.strategy';
import { ApiKeyStrategy } from './strategies/api-key.strategy';
import { PrismaService } from '../../shared/services/prisma.service';
import { OperatorsModule } from '../operators/operators.module';
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
expiresIn: configService.get('JWT_EXPIRATION') || '1h',
},
}),
/*todo
useFactory: async (configService: ConfigService) => {
return {
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
expiresIn: configService.get<string | number>('JWT_EXPIRATION') || '1h'
},
};
},*/
}),
OperatorsModule,
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy, ApiKeyStrategy, PrismaService],
exports: [AuthService, JwtModule],
})
export class AuthModule {}

View File

@ -1,245 +0,0 @@
import {
Injectable,
UnauthorizedException,
BadRequestException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '../../shared/services/prisma.service';
import { OperatorsService } from '../operators/operators.service';
import * as bcrypt from 'bcrypt';
import { AuthInitDto, AuthValidateDto, LoginDto } from './dto/auth.dto';
@Injectable()
export class AuthService {
constructor(
private readonly prisma: PrismaService,
private readonly jwtService: JwtService,
private readonly operatorsService: OperatorsService,
) {}
async initializeUserAuth(partnerId: string, dto: AuthInitDto) {
// Vérifier le partenaire
const partner = await this.prisma.partner.findUnique({
where: { id: partnerId },
});
if (!partner || partner.status !== 'ACTIVE') {
throw new UnauthorizedException('Invalid partner');
}
// Déterminer l'opérateur basé sur le numéro
const operator = this.detectOperator(dto.msisdn, dto.country);
// Obtenir l'adaptateur approprié
const adapter = this.operatorsService.getAdapter(operator, dto.country);
// Initialiser l'authentification avec l'opérateur
const authResponse = await adapter.initializeAuth({
msisdn: dto.msisdn,
country: dto.country,
metadata: dto.metadata,
});
// Créer une session temporaire
const session = await this.prisma.authSession.create({
data: {
sessionId: authResponse.sessionId,
partnerId: partnerId,
msisdn: dto.msisdn,
operator: operator,
country: dto.country,
authMethod: dto.authMethod,
challengeId: authResponse.challengeId,
status: 'PENDING',
expiresAt: authResponse.expiresAt,
},
});
return {
sessionId: session.sessionId,
authMethod: dto.authMethod,
status: 'PENDING',
redirectUrl: authResponse.redirectUrl,
challengeId: authResponse.challengeId,
expiresAt: authResponse.expiresAt,
};
}
async validateUserAuth(dto: AuthValidateDto) {
// Récupérer la session
const session = await this.prisma.authSession.findUnique({
where: { sessionId: dto.sessionId },
});
if (!session) {
throw new BadRequestException('Invalid session');
}
if (session.status !== 'PENDING') {
throw new BadRequestException('Session already processed');
}
if (new Date() > session.expiresAt) {
throw new BadRequestException('Session expired');
}
// Obtenir l'adaptateur
const adapter = this.operatorsService.getAdapter(
session.operator,
session.country,
);
// Valider avec l'opérateur
const validationResponse = await adapter.validateAuth({
challengeId: session.challengeId,
otpCode: dto.otpCode,
msisdn: session.msisdn,
country: session.country,
});
if (!validationResponse.success) {
await this.prisma.authSession.update({
where: { id: session.id },
data: { status: 'FAILED' },
});
throw new UnauthorizedException('Authentication failed');
}
// Créer ou mettre à jour l'utilisateur
const user = await this.prisma.user.upsert({
where: { msisdn: session.msisdn },
update: {
userToken: validationResponse.userToken,
userAlias: validationResponse.userAlias,
updatedAt: new Date(),
},
create: {
msisdn: session.msisdn,
userToken: validationResponse.userToken,
userAlias: validationResponse.userAlias,
operatorId: await this.getOperatorId(session.operator, session.country),
partnerId: session.partnerId,
country: session.country,
},
});
// Mettre à jour la session
await this.prisma.authSession.update({
where: { id: session.id },
data: {
status: 'SUCCESS',
userId: user.id,
},
});
// Créer un JWT pour le partenaire
const payload = {
userId: user.id,
partnerId: session.partnerId,
msisdn: user.msisdn,
operator: session.operator,
};
return {
success: true,
accessToken: this.jwtService.sign(payload),
userToken: validationResponse.userToken,
userAlias: validationResponse.userAlias,
msisdn: session.msisdn,
operator: session.operator,
country: session.country,
expiresAt: validationResponse.expiresAt,
};
}
async loginPartner(dto: LoginDto) {
const partner = await this.prisma.partner.findUnique({
where: { email: dto.email },
});
if (!partner) {
throw new UnauthorizedException('Invalid credentials');
}
const isPasswordValid = await bcrypt.compare(
dto.password,
partner.passwordHash,
);
if (!isPasswordValid) {
throw new UnauthorizedException('Invalid credentials');
}
const payload = {
partnerId: partner.id,
email: partner.email,
type: 'partner',
};
return {
accessToken: this.jwtService.sign(payload),
partner: {
id: partner.id,
name: partner.name,
email: partner.email,
status: partner.status,
},
};
}
private detectOperator(msisdn: string, country: string): string {
// Logique pour détecter l'opérateur basé sur le préfixe
const prefixMap = {
CI: {
'07': 'ORANGE',
'08': 'ORANGE',
'09': 'ORANGE',
'04': 'MTN',
'05': 'MTN',
'06': 'MTN',
'01': 'MOOV',
},
SN: {
'77': 'ORANGE',
'78': 'ORANGE',
'76': 'FREE',
'70': 'EXPRESSO',
},
// Ajouter d'autres pays
};
const countryPrefixes = prefixMap[country];
if (!countryPrefixes) {
throw new BadRequestException(`Country ${country} not supported`);
}
const prefix = msisdn.substring(0, 2);
const operator = countryPrefixes[prefix];
if (!operator) {
throw new BadRequestException(`Cannot detect operator for ${msisdn}`);
}
return operator;
}
private async getOperatorId(
operatorCode: string,
country: string,
): Promise<string> {
const operator = await this.prisma.operator.findFirst({
where: {
code: operatorCode as any,
country: country,
},
});
if (!operator) {
throw new BadRequestException(
`Operator ${operatorCode} not found in ${country}`,
);
}
return operator.id;
}
}

View File

@ -1,50 +0,0 @@
import { IsString, IsEnum, IsOptional, IsMobilePhone } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class AuthInitDto {
@ApiProperty()
@IsMobilePhone()
msisdn: string;
@ApiProperty()
@IsString()
country: string;
@ApiProperty({ enum: ['OTP_SMS', 'REDIRECT_3G', 'SMS_MO', 'USSD'] })
@IsEnum(['OTP_SMS', 'REDIRECT_3G', 'SMS_MO', 'USSD'])
authMethod: string;
@ApiProperty({ required: false })
@IsOptional()
redirectUrl?: string;
@ApiProperty({ required: false })
@IsOptional()
metadata?: Record<string, any>;
}
export class AuthValidateDto {
@ApiProperty()
@IsString()
sessionId: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
otpCode?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
challengeResponse?: string;
}
export class LoginDto {
@ApiProperty()
@IsString()
email: string;
@ApiProperty()
@IsString()
password: string;
}

View File

@ -1,26 +0,0 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { HeaderAPIKeyStrategy } from 'passport-headerapikey';
import { PrismaService } from '../../../shared/services/prisma.service';
@Injectable()
export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy, 'api-key') {
constructor(private readonly prisma: PrismaService) {
super(
{ header: 'X-API-Key', prefix: '' },
true
);
}
async validate(apiKey: string, done: any) {
const partner = await this.prisma.partner.findUnique({
where: { apiKey },
});
if (!partner || partner.status !== 'ACTIVE') {
return done(new UnauthorizedException(), false);
}
return done(null, partner);
}
}

View File

@ -1,23 +0,0 @@
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('app.jwtSecret'),
});
}
async validate(payload: any) {
return {
userId: payload.userId,
partnerId: payload.partnerId,
email: payload.email,
};
}
}

View File

@ -175,8 +175,8 @@ export class OtpChallengeController {
async verifyOtp(
@Param('challengeId') challengeId: string,
@Body('otpCode') otpCode: string,
@Headers('x-merchant-id') merchantId: string,
@Headers('x-api-key') apiKey: string,
@Headers('X-Merchant-ID') merchantId: string,
@Headers('x-API-KEY') apiKey: string,
): Promise<OtpChallengeResponseDto> {
this.logger.log(
`[VERIFY] Merchant: ${merchantId}, Challenge: ${challengeId}`,
@ -187,10 +187,6 @@ export class OtpChallengeController {
this.validateMerchantHeaders(merchantId, apiKey);
// Valider le code OTP
if (!otpCode || otpCode.trim().length === 0) {
throw new HttpException('OTP code is required', HttpStatus.BAD_REQUEST);
}
// Appeler le service
const response = await this.otpChallengeService.verifyOtp(
challengeId,

View File

@ -15,7 +15,7 @@ import { OtpChallengeService } from './otp.challenge.service';
{
provide: 'ORANGE_CONFIG',
useFactory: (configService: ConfigService): OrangeConfig => ({
baseUrl: configService.get<string>('ORANGE_BASE_URL', 'https://api-gateway.app.cameleonapp.com/api-orange-dcb/1.0.0'),
baseUrl: configService.get<string>('ORANGE_BASE_URL', 'https://webhook.site/69ce9344-f87b-421a-a494-c59eca7c54ce'),
partnerId: configService.get<string>('ORANGE_PARTNER_ID', 'PDKSUB'),
clientId: configService.get<string>('ORANGE_CLIENT_ID', 'admin'),
clientSecret: configService.get<string>('ORANGE_CLIENT_SECRET', 'admin'),

View File

@ -28,6 +28,16 @@ export class OtpChallengeService implements IOtpChallengeService {
// Appeler l'adaptateur Orange
const response = await this.orangeAdapter.initiateChallenge(request);
if (response.challengeId || true) {
this.challengeCache.set(response.challengeId, { request, response });
// Nettoyer le cache après expiration (par défaut 5 minutes)
const expirationTime = (response.expiresIn || 300) * 1000;
setTimeout(() => {
this.challengeCache.delete(response.challengeId);
}, expirationTime);
}
return response;
} catch (error) {
return this.createErrorResponse(request, 'INITIATE_FAILED', error.message);

View File

@ -1,91 +0,0 @@
import { IsString, IsEnum, IsOptional, IsArray, IsDateString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class SendNotificationDto {
@ApiProperty({ enum: ['PAYMENT', 'SUBSCRIPTION', 'ALERT', 'MARKETING'] })
@IsEnum(['PAYMENT', 'SUBSCRIPTION', 'ALERT', 'MARKETING'])
type: string;
@ApiProperty({ enum: ['SMS', 'EMAIL', 'WEBHOOK'] })
@IsEnum(['SMS', 'EMAIL', 'WEBHOOK'])
channel: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
userToken?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
recipient?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
subject?: string;
@ApiProperty()
@IsString()
content: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
templateId?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsDateString()
scheduledFor?: string;
@ApiProperty({ required: false })
@IsOptional()
metadata?: Record<string, any>;
}
export class BulkNotificationDto {
@ApiProperty({ enum: ['PAYMENT', 'SUBSCRIPTION', 'ALERT', 'MARKETING'] })
@IsEnum(['PAYMENT', 'SUBSCRIPTION', 'ALERT', 'MARKETING'])
type: string;
@ApiProperty({ enum: ['SMS', 'EMAIL'] })
@IsEnum(['SMS', 'EMAIL'])
channel: string;
@ApiProperty({ required: false })
@IsOptional()
@IsArray()
userIds?: string[];
@ApiProperty({ required: false })
@IsOptional()
@IsArray()
segments?: string[];
@ApiProperty({ required: false })
@IsOptional()
@IsString()
subject?: string;
@ApiProperty()
@IsString()
content: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
templateId?: string;
@ApiProperty({ required: false })
@IsOptional()
variables?: Record<string, any>;
@ApiProperty()
@IsString()
batchId: string;
@ApiProperty({ required: false })
@IsOptional()
metadata?: Record<string, any>;
}

View File

@ -1,37 +0,0 @@
import { Controller, Post, Get, Body, Param, Query, UseGuards, Request } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { NotificationsService } from './services/notifications.service';
import { SendNotificationDto, BulkNotificationDto } from './dto/notification.dto';
import { JwtAuthGuard } from 'src/common/guards/jwt-auth.guard';
@ApiTags('notifications')
@Controller('notifications')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
export class NotificationsController {
constructor(private readonly notificationsService: NotificationsService) {}
@Post('send')
@ApiOperation({ summary: 'Send a notification' })
async send(@Request() req, @Body() dto: SendNotificationDto) {
return this.notificationsService.send(req.user.partnerId, dto);
}
@Post('bulk')
@ApiOperation({ summary: 'Send bulk notifications' })
async sendBulk(@Request() req, @Body() dto: BulkNotificationDto) {
return this.notificationsService.sendBulk(req.user.partnerId, dto);
}
@Get(':id/status')
@ApiOperation({ summary: 'Get notification status' })
async getStatus(@Request() req, @Param('id') id: string) {
return this.notificationsService.getStatus(id, req.user.partnerId);
}
@Get('batch/:batchId/status')
@ApiOperation({ summary: 'Get batch notification status' })
async getBatchStatus(@Request() req, @Param('batchId') batchId: string) {
return this.notificationsService.getBatchStatus(batchId, req.user.partnerId);
}
}

View File

@ -1,40 +0,0 @@
import { Module } from '@nestjs/common';
import { BullModule } from '@nestjs/bull';
import { HttpModule } from '@nestjs/axios';
import { NotificationsController } from './notifications.controller';
import { NotificationsService } from './services/notifications.service';
import { SmsService } from './services/sms.service';
import { EmailService } from './services/email.service';
import { WebhookService } from './services/webhook.service';
import { NotificationProcessor } from './processors/notification.processor';
import { NotificationTemplateService } from './services/template.service';
import { PrismaService } from '../../shared/services/prisma.service';
import { OperatorsModule } from '../operators/operators.module';
@Module({
imports: [
BullModule.registerQueue({
name: 'notifications',
}),
BullModule.registerQueue({
name: 'webhooks',
}),
HttpModule.register({
timeout: 10000,
maxRedirects: 2,
}),
OperatorsModule,
],
controllers: [NotificationsController],
providers: [
NotificationsService,
SmsService,
EmailService,
WebhookService,
NotificationProcessor,
NotificationTemplateService,
PrismaService,
],
exports: [NotificationsService, SmsService, WebhookService],
})
export class NotificationsModule {}

View File

@ -1,55 +0,0 @@
import { Process, Processor } from '@nestjs/bull';
import bull from 'bull';
import { NotificationsService } from '../services/notifications.service';
import { WebhookService } from '../services/webhook.service';
@Processor('notifications')
export class NotificationProcessor {
constructor(
private readonly notificationsService: NotificationsService,
) {}
@Process('send-notification')
async handleSendNotification(job: bull.Job) {
const { notificationId } = job.data;
try {
await this.notificationsService.processNotification(notificationId);
return { success: true, notificationId };
} catch (error) {
console.error(`Failed to send notification ${notificationId}:`, error);
throw error;
}
}
@Process('bulk-send')
async handleBulkSend(job: bull.Job) {
const { notifications } = job.data;
const results:any = [];
for (const notificationId of notifications) {
try {
await this.notificationsService.processNotification(notificationId);
results.push({ notificationId, success: true });
} catch (error) {
results.push({ notificationId, success: false, error: error.message });
}
}
return results;
}
}
@Processor('webhooks')
export class WebhookProcessor {
constructor(
private readonly webhookService: WebhookService,
) {}
@Process('send-webhook')
async handleSendWebhook(job: bull.Job) {
const { webhookId, attempt } = job.data;
return await this.webhookService.processWebhook(webhookId, attempt);
}
}

View File

@ -1,98 +0,0 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { OperatorsService } from '../../operators/operators.service';
import { PrismaService } from '../../../shared/services/prisma.service';
//todo rewrite
@Injectable()
export class EmailService {
constructor(
private readonly operatorsService: OperatorsService,
private readonly prisma: PrismaService,
private readonly configService: ConfigService,
) {}
async send(params: {
to: string;
message: string;
subject?:string;
content?:string;
template?:string
userToken?: string;
userAlias?: string;
from?: string;
}) {
// Si on a un userToken, utiliser l'opérateur de l'utilisateur
if (params.userToken) {
const user = await this.prisma.user.findUnique({
where: { userToken: params.userToken },
include: { operator: true },
});
if (user) {
const adapter = this.operatorsService.getAdapter(
user.operator.code,
user.country,
);
return await adapter.sendSms({
to: params.to,
message: params.message,
userToken: params.userToken,
userAlias: params.userAlias,
from: params.from,
country: user.country,
});
}
}
// Sinon, détecter l'opérateur par le numéro
const operator = this.detectOperatorByNumber(params.to);
const adapter = this.operatorsService.getAdapter(operator.code, operator.country);
return await adapter.sendSms({
to: params.to,
message: params.message,
from: params.from,
country: operator.country,
});
}
async sendOtp(msisdn: string, code: string, template?: string) {
const message = template
? template.replace('{code}', code)
: `Your verification code is: ${code}`;
return this.send({
to: msisdn,
message: message,
});
}
async sendTransactional(params: {
to: string;
template: string;
variables: Record<string, any>;
userToken?: string;
}) {
// Remplacer les variables dans le template
let message = params.template;
for (const [key, value] of Object.entries(params.variables)) {
message = message.replace(new RegExp(`{${key}}`, 'g'), value);
}
return this.send({
to: params.to,
message: message,
userToken: params.userToken,
});
}
private detectOperatorByNumber(msisdn: string) {
// Logique de détection basée sur le préfixe
// Pour simplifier, on retourne Orange CI par défaut
return {
code: 'ORANGE',
country: 'CI',
};
}
}

View File

@ -1,292 +0,0 @@
import { Injectable, BadRequestException } from '@nestjs/common';
import { InjectQueue } from '@nestjs/bull';
import bull from 'bull';
import { SmsService } from './sms.service';
import { EmailService } from './email.service';
import { WebhookService } from './webhook.service';
import { NotificationTemplateService } from './template.service';
import { SendNotificationDto, BulkNotificationDto } from '../dto/notification.dto';
import { PrismaService } from 'src/shared/services/prisma.service';
@Injectable()
export class NotificationsService {
constructor(
private readonly prisma: PrismaService,
private readonly smsService: SmsService,
private readonly emailService: EmailService,
private readonly webhookService: WebhookService,
private readonly templateService: NotificationTemplateService,
@InjectQueue('notifications') private notificationQueue: bull.Queue,
) {}
async send(partnerId: string, dto: SendNotificationDto) {
// Valider l'utilisateur si fourni
let user:any = null;
if (dto.userToken) {
user = await this.prisma.user.findFirst({
where: {
userToken: dto.userToken,
partnerId: partnerId,
},
include: { operator: true },
});
if (!user) {
throw new BadRequestException('Invalid user token');
}
}
// Créer l'enregistrement de notification
const notification = await this.prisma.notification.create({
data: {
partnerId: partnerId,
userId: user?.id,
type: dto.type,
channel: dto.channel,
recipient: dto.recipient || user?.msisdn,
subject: dto.subject,
content: dto.content,
templateId: dto.templateId,
status: 'PENDING',
metadata: dto.metadata,
scheduledFor: dto.scheduledFor,
},
});
// Si programmée pour plus tard, ajouter à la queue avec délai
if (dto.scheduledFor && new Date(dto.scheduledFor) > new Date()) {
const delay = new Date(dto.scheduledFor).getTime() - Date.now();
await this.notificationQueue.add(
'send-notification',
{ notificationId: notification.id },
{ delay },
);
} else {
// Envoyer immédiatement
await this.processNotification(notification.id);
}
return notification;
}
async sendBulk(partnerId: string, dto: BulkNotificationDto) {
const notifications :any= [];
// Récupérer les destinataires selon les critères
const recipients:any = await this.getRecipients(partnerId, dto);
for (const recipient of recipients) {
const notification:any = await this.prisma.notification.create({
data: {
partnerId: partnerId,
userId: recipient.userId,
type: dto.type,
channel: dto.channel,
recipient: recipient.contact,
subject: dto.subject,
content:'{}',
//content: await this.templateService.render(dto.templateId, {
// ...recipient.data,
// ...dto.variables,
//}),
templateId: dto.templateId,
status: 'PENDING',
batchId: dto.batchId,
metadata: dto.metadata,
},
});
notifications.push(notification);
// Ajouter à la queue avec rate limiting
await this.notificationQueue.add(
'send-notification',
{ notificationId: notification.id },
{
delay: notifications.length * 100, // 100ms entre chaque envoi
attempts: 3,
},
);
}
return {
batchId: dto.batchId,
totalRecipients: notifications.length,
status: 'PROCESSING',
};
}
async processNotification(notificationId: string) {
const notification = await this.prisma.notification.findUnique({
where: { id: notificationId },
include: {
user: true,
partner: true,
},
});
if (!notification) {
throw new BadRequestException('Notification not found');
}
try {
let result;
switch (notification.channel) {
case 'SMS':
result = await this.smsService.send({
to: notification.recipient,
message: notification.content,
userToken: notification.user?.userToken,
userAlias: notification.user?.userAlias,
//from: notification.metadata?.from,
});
break;
case 'EMAIL':
result = await this.emailService.send({
to: notification.recipient,
// subject: notification.subject,
content: notification.content,
//template: notification.templateId,
message: ''
});
break;
case 'WEBHOOK':
result = await this.webhookService.send({
url: notification.recipient,
event: notification.type,
payload: {
subject: notification.subject,
content: notification.content,
metadata: notification.metadata,
},
});
break;
default:
throw new BadRequestException(`Unsupported channel: ${notification.channel}`);
}
// Mettre à jour le statut
await this.prisma.notification.update({
where: { id: notificationId },
data: {
status: 'SENT',
sentAt: new Date(),
response: result,
},
});
return result;
} catch (error) {
await this.prisma.notification.update({
where: { id: notificationId },
data: {
status: 'FAILED',
failureReason: error.message,
failedAt: new Date(),
},
});
throw error;
}
}
async getStatus(notificationId: string, partnerId: string) {
const notification = await this.prisma.notification.findFirst({
where: {
id: notificationId,
partnerId: partnerId,
},
});
if (!notification) {
throw new BadRequestException('Notification not found');
}
return notification;
}
async getBatchStatus(batchId: string, partnerId: string) {
const notifications = await this.prisma.notification.findMany({
where: {
batchId: batchId,
partnerId: partnerId,
},
select: {
status: true,
},
});
const statusCount = notifications.reduce((acc, n) => {
acc[n.status] = (acc[n.status] || 0) + 1;
return acc;
}, {});
return {
batchId,
total: notifications.length,
statusBreakdown: statusCount,
};
}
private async getRecipients(partnerId: string, dto: BulkNotificationDto) {
const recipients:any = [];
if (dto.userIds && dto.userIds.length > 0) {
const users = await this.prisma.user.findMany({
where: {
id: { in: dto.userIds },
partnerId: partnerId,
},
});
for (const user of users) {
recipients.push({
userId: user.id,
contact: dto.channel === 'SMS' ? user.msisdn : user.msisdn,
data: { name: user.msisdn, msisdn: user.msisdn },
});
}
}
if (dto.segments) {
// Logique pour récupérer les utilisateurs par segments
const segmentUsers = await this.getSegmentUsers(partnerId, dto.segments);
recipients.push(...segmentUsers);
}
return recipients;
}
private async getSegmentUsers(partnerId: string, segments: string[]) {
const users:any = [];
for (const segment of segments) {
switch (segment) {
case 'ACTIVE_SUBSCRIBERS':
const activeUsers = await this.prisma.user.findMany({
where: {
partnerId: partnerId,
subscriptions: {
some: {
status: 'ACTIVE',
},
},
},
});
users.push(...activeUsers.map(u => ({
userId: u.id,
contact: u.msisdn,
data: { name: u.msisdn, msisdn: u.msisdn },
})));
break;
// Ajouter d'autres segments
}
}
return users;
}
}

View File

@ -1,95 +0,0 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { OperatorsService } from '../../operators/operators.service';
import { PrismaService } from '../../../shared/services/prisma.service';
@Injectable()
export class SmsService {
constructor(
private readonly operatorsService: OperatorsService,
private readonly prisma: PrismaService,
private readonly configService: ConfigService,
) {}
async send(params: {
to: string;
message: string;
userToken?: string;
userAlias?: string;
from?: string;
}) {
// Si on a un userToken, utiliser l'opérateur de l'utilisateur
if (params.userToken) {
const user = await this.prisma.user.findUnique({
where: { userToken: params.userToken },
include: { operator: true },
});
if (user) {
const adapter = this.operatorsService.getAdapter(
user.operator.code,
user.country,
);
return await adapter.sendSms({
to: params.to,
message: params.message,
userToken: params.userToken,
userAlias: params.userAlias,
from: params.from,
country: user.country,
});
}
}
// Sinon, détecter l'opérateur par le numéro
const operator = this.detectOperatorByNumber(params.to);
const adapter = this.operatorsService.getAdapter(operator.code, operator.country);
return await adapter.sendSms({
to: params.to,
message: params.message,
from: params.from,
country: operator.country,
});
}
async sendOtp(msisdn: string, code: string, template?: string) {
const message = template
? template.replace('{code}', code)
: `Your verification code is: ${code}`;
return this.send({
to: msisdn,
message: message,
});
}
async sendTransactional(params: {
to: string;
template: string;
variables: Record<string, any>;
userToken?: string;
}) {
// Remplacer les variables dans le template
let message = params.template;
for (const [key, value] of Object.entries(params.variables)) {
message = message.replace(new RegExp(`{${key}}`, 'g'), value);
}
return this.send({
to: params.to,
message: message,
userToken: params.userToken,
});
}
private detectOperatorByNumber(msisdn: string) {
// Logique de détection basée sur le préfixe
// Pour simplifier, on retourne Orange CI par défaut
return {
code: 'ORANGE',
country: 'CI',
};
}
}

View File

@ -1,6 +0,0 @@
export class NotificationTemplateService{
render(templateId: string | undefined, arg1: any) {
throw new Error('Method not implemented.');
}
}

View File

@ -1,126 +0,0 @@
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { InjectQueue } from '@nestjs/bull';
import bull from 'bull';
import { firstValueFrom } from 'rxjs';
import { PrismaService } from '../../../shared/services/prisma.service';
import * as crypto from 'crypto';
@Injectable()
export class WebhookService {
constructor(
private readonly httpService: HttpService,
private readonly prisma: PrismaService,
@InjectQueue('webhooks') private webhookQueue: bull.Queue,
) {}
async send(params: {
url: string;
event: string;
payload: any;
partnerId?: string;
retries?: number;
}) {
const webhook = await this.prisma.webhook.create({
data: {
url: params.url,
event: params.event,
payload: params.payload,
status: 'PENDING',
partnerId: params.partnerId,
},
});
// Ajouter à la queue
await this.webhookQueue.add(
'send-webhook',
{
webhookId: webhook.id,
attempt: 1,
},
{
attempts: params.retries || 3,
backoff: {
type: 'exponential',
delay: 2000,
},
},
);
return webhook;
}
async processWebhook(webhookId: string, attempt: number) {
const webhook = await this.prisma.webhook.findUnique({
where: { id: webhookId },
include: { partner: true },
});
if (!webhook) {
throw new Error('Webhook not found');
}
try {
const signature = this.generateSignature(
webhook.payload,
webhook.partner?.secretKey as string,
);
const response = await firstValueFrom(
this.httpService.post(webhook.url, webhook.payload, {
headers: {
'Content-Type': 'application/json',
'X-Webhook-Event': webhook.event,
'X-Webhook-Signature': signature,
'X-Webhook-Timestamp': new Date().toISOString(),
'X-Webhook-Attempt': attempt.toString(),
},
timeout: 10000,
}),
);
await this.prisma.webhook.update({
where: { id: webhookId },
data: {
status: 'SUCCESS',
response: response.data,
//responseCode: response.status,
//deliveredAt: new Date(),
attempts: attempt,
},
});
return response.data;
} catch (error) {
await this.prisma.webhook.update({
where: { id: webhookId },
data: {
status: attempt >= 3 ? 'FAILED' : 'RETRYING',
//lastError: error.message,
attempts: attempt,
lastAttempt: new Date(),
},
});
if (attempt >= 3) {
// Notifier l'échec définitif
await this.notifyWebhookFailure(webhook);
}
throw error;
}
}
private generateSignature(payload: any, secret: string): string {
if (!secret) return '';
const hmac = crypto.createHmac('sha256', secret);
hmac.update(JSON.stringify(payload));
return hmac.digest('hex');
}
private async notifyWebhookFailure(webhook: any) {
// Envoyer un email ou une notification au partenaire
console.error(`Webhook failed after max retries: ${webhook.id}`);
}
}

View File

@ -149,7 +149,7 @@ export class OperatorsService{
message: 'Connection successful',
details: {
sessionId: result.sessionId,
authMethod: result.authMethod,
// authMethod: result.authMethod,
},
};
} catch (error) {
@ -385,7 +385,7 @@ export class OperatorsService{
return country;
}
}
return null;
return "";
}
private detectOperatorFromPrefix(msisdn: string, countryCode: string): string {

View File

@ -1,178 +0,0 @@
import {
IsString,
IsEmail,
IsOptional,
IsObject,
MinLength,
IsUrl,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreatePartnerDto {
@ApiProperty()
@IsString()
name: string;
@ApiProperty()
@IsEmail()
email: string;
@ApiProperty()
@IsString()
@MinLength(8)
password: string;
@ApiProperty()
@IsString()
country: string;
@ApiProperty({ required: false })
@IsOptional()
@IsObject()
companyInfo?: {
legalName: string;
taxId: string;
address: string;
phone?: string;
website?: string;
};
@ApiProperty({ required: false })
@IsOptional()
metadata?: Record<string, any>;
}
export class UpdateCallbacksDto {
@ApiProperty({ required: false })
@IsOptional()
headerEnrichment?: {
url: string;
method: string;
headers?: Record<string, string>;
};
@ApiProperty({ required: false })
@IsOptional()
subscription?: {
onCreate?: string;
onRenew?: string;
onCancel?: string;
onExpire?: string;
};
@ApiProperty({ required: false })
@IsOptional()
payment?: {
onSuccess?: string;
onFailure?: string;
onRefund?: string;
};
@ApiProperty({ required: false })
@IsOptional()
authentication?: {
onSuccess?: string;
onFailure?: string;
};
@IsOptional()
@IsUrl()
success?: string;
@IsOptional()
@IsUrl()
cancel?: string;
@IsOptional()
@IsUrl()
webhook?: string;
}
export class LoginPartnerDto {
@ApiProperty()
@IsEmail()
email: string;
@ApiProperty()
@IsString()
password: string;
}
export class ChangePasswordDto {
@ApiProperty()
@IsString()
currentPassword: string;
@ApiProperty()
@IsString()
@MinLength(8)
newPassword: string;
}
export class UpdatePartnerDto {
@ApiProperty({ required: false })
@IsOptional()
@IsString()
name?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsObject()
companyInfo?: {
legalName?: string;
taxId?: string;
address?: string;
phone?: string;
website?: string;
};
@ApiProperty({ required: false })
@IsOptional()
metadata?: Record<string, any>;
}
export class PartnerResponseDto {
@ApiProperty()
id: string;
@ApiProperty()
name: string;
@ApiProperty()
email: string;
@ApiProperty()
status: string;
@ApiProperty()
apiKey: string;
@ApiProperty()
country: string;
@ApiProperty({ required: false })
companyInfo?: any;
@ApiProperty()
createdAt: Date;
}
export class PartnerStatsResponseDto {
@ApiProperty()
totalUsers: number;
@ApiProperty()
activeSubscriptions: number;
@ApiProperty()
todayPayments: number;
@ApiProperty()
monthRevenue: number;
@ApiProperty()
successRate: number;
@ApiProperty()
avgPaymentAmount: number;
}

View File

@ -1,342 +0,0 @@
import {
Controller,
Post,
Get,
Put,
Delete,
Body,
Param,
Query,
UseGuards,
Request,
HttpCode,
HttpStatus,
Patch,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiBearerAuth,
ApiResponse,
ApiQuery,
} from '@nestjs/swagger';
import { PartnersService } from './partners.service';
import {
CreatePartnerDto,
UpdatePartnerDto,
UpdateCallbacksDto,
LoginPartnerDto,
ChangePasswordDto,
PartnerResponseDto,
PartnerStatsResponseDto,
} from './dto/partner.dto';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { Public } from '../../common/decorators/public.decorator';
import { AuthService } from '../auth/auth.service';
@ApiTags('partners')
@Controller('partners')
export class PartnersController {
constructor(
private readonly partnersService: PartnersService,
private readonly authService: AuthService,
) {}
@Post('register')
@Public()
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Register a new partner' })
@ApiResponse({
status: 201,
description: 'Partner registered successfully',
type: PartnerResponseDto,
})
@ApiResponse({ status: 409, description: 'Email already exists' })
async register(@Body() createPartnerDto: CreatePartnerDto) {
return this.partnersService.register(createPartnerDto);
}
@Post('login')
@Public()
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Partner login' })
@ApiResponse({
status: 200,
description: 'Login successful',
})
@ApiResponse({ status: 401, description: 'Invalid credentials' })
async login(@Body() loginDto: LoginPartnerDto) {
return this.authService.loginPartner(loginDto);
}
@Get('profile')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get partner profile' })
@ApiResponse({
status: 200,
description: 'Partner profile retrieved',
type: PartnerResponseDto,
})
async getProfile(@Request() req) {
return this.partnersService.getPartner(req.user.partnerId);
}
@Put('profile')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Update partner profile' })
@ApiResponse({
status: 200,
description: 'Profile updated successfully',
type: PartnerResponseDto,
})
async updateProfile(
@Request() req,
@Body() updatePartnerDto: UpdatePartnerDto,
) {
return this.partnersService.updatePartner(
req.user.partnerId,
updatePartnerDto,
);
}
@Put('callbacks')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Update callback URLs' })
@ApiResponse({
status: 200,
description: 'Callbacks updated successfully',
})
async updateCallbacks(
@Request() req,
@Body() updateCallbacksDto: UpdateCallbacksDto,
) {
return this.partnersService.updateCallbacks(
req.user.partnerId,
updateCallbacksDto,
);
}
@Get('callbacks')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get callback configuration' })
@ApiResponse({
status: 200,
description: 'Callback configuration retrieved',
})
async getCallbacks(@Request() req) {
return this.partnersService.getCallbacks(req.user.partnerId);
}
@Post('keys/regenerate')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Regenerate API keys' })
@ApiResponse({
status: 200,
description: 'Keys regenerated successfully',
})
async regenerateKeys(@Request() req) {
return this.partnersService.regenerateKeys(req.user.partnerId);
}
@Get('statistics')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get partner statistics' })
@ApiResponse({
status: 200,
description: 'Statistics retrieved',
type: PartnerStatsResponseDto,
})
async getStatistics(@Request() req) {
return this.partnersService.getPartnerStats(req.user.partnerId);
}
@Get('statistics/dashboard')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get dashboard statistics' })
@ApiQuery({ name: 'period', required: false, enum: ['daily', 'weekly', 'monthly', 'yearly'] })
async getDashboardStats(
@Request() req,
@Query('period') period: string = 'monthly',
) {
return this.partnersService.getDashboardStats(req.user.partnerId, period);
}
@Post('password/change')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Change password' })
@ApiResponse({
status: 200,
description: 'Password changed successfully',
})
async changePassword(
@Request() req,
@Body() changePasswordDto: ChangePasswordDto,
) {
return this.partnersService.changePassword(
req.user.partnerId,
changePasswordDto,
);
}
@Get('users')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'List partner users' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number })
@ApiQuery({ name: 'status', required: false })
async listUsers(
@Request() req,
@Query('page') page: number = 1,
@Query('limit') limit: number = 20,
@Query('status') status?: string,
) {
return this.partnersService.listPartnerUsers(req.user.partnerId, {
page,
limit,
status,
});
}
@Get('users/:userId')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get user details' })
async getUserDetails(@Request() req, @Param('userId') userId: string) {
return this.partnersService.getUserDetails(req.user.partnerId, userId);
}
@Get('transactions')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get transaction history' })
@ApiQuery({ name: 'startDate', required: false, type: Date })
@ApiQuery({ name: 'endDate', required: false, type: Date })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number })
async getTransactions(
@Request() req,
@Query('startDate') startDate?: string,
@Query('endDate') endDate?: string,
@Query('page') page: number = 1,
@Query('limit') limit: number = 20,
) {
return this.partnersService.getTransactionHistory(req.user.partnerId, {
startDate: startDate ? new Date(startDate) : undefined,
endDate: endDate ? new Date(endDate) : undefined,
page,
limit,
});
}
@Get('revenue')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get revenue analytics' })
@ApiQuery({ name: 'period', required: false, enum: ['daily', 'weekly', 'monthly', 'yearly'] })
@ApiQuery({ name: 'groupBy', required: false, enum: ['day', 'week', 'month', 'operator', 'country'] })
async getRevenue(
@Request() req,
@Query('period') period: string = 'monthly',
@Query('groupBy') groupBy: string = 'day',
) {
return this.partnersService.getRevenueAnalytics(req.user.partnerId, {
period,
groupBy,
});
}
@Post('webhook/test')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Test webhook configuration' })
async testWebhook(
@Request() req,
@Body() body: { url: string; event: string },
) {
return this.partnersService.testWebhook(
req.user.partnerId,
body.url,
body.event,
);
}
@Get('settings')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get partner settings' })
async getSettings(@Request() req) {
return this.partnersService.getSettings(req.user.partnerId);
}
@Put('settings')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Update partner settings' })
async updateSettings(@Request() req, @Body() settings: any) {
return this.partnersService.updateSettings(req.user.partnerId, settings);
}
@Get('api-usage')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get API usage statistics' })
@ApiQuery({ name: 'period', required: false, enum: ['hourly', 'daily', 'weekly', 'monthly'] })
async getApiUsage(
@Request() req,
@Query('period') period: string = 'daily',
) {
return this.partnersService.getApiUsageStats(req.user.partnerId, period);
}
@Get('logs')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get activity logs' })
@ApiQuery({ name: 'type', required: false })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number })
async getActivityLogs(
@Request() req,
@Query('type') type?: string,
@Query('page') page: number = 1,
@Query('limit') limit: number = 50,
) {
return this.partnersService.getActivityLogs(req.user.partnerId, {
type,
page,
limit,
});
}
@Delete('account')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Delete partner account' })
@ApiResponse({
status: 200,
description: 'Account deletion initiated',
})
async deleteAccount(
@Request() req,
@Body() body: { password: string; reason?: string },
) {
return this.partnersService.deleteAccount(
req.user.partnerId,
body.password,
body.reason,
);
}
}

View File

@ -1,13 +0,0 @@
import { Module } from '@nestjs/common';
import { PartnersController } from './partners.controller';
import { PartnersService } from './partners.service';
import { PrismaService } from '../../shared/services/prisma.service';
import { AuthModule } from '../auth/auth.module';
@Module({
imports:[AuthModule],
controllers: [PartnersController],
providers: [PartnersService, PrismaService],
exports: [PartnersService],
})
export class PartnersModule {}

View File

@ -1,229 +0,0 @@
import {
Injectable,
ConflictException,
NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../../shared/services/prisma.service';
import * as bcrypt from 'bcrypt';
import * as crypto from 'crypto';
import { ChangePasswordDto, CreatePartnerDto, UpdateCallbacksDto, UpdatePartnerDto } from './dto/partner.dto';
import { Prisma } from 'generated/prisma';
@Injectable()
export class PartnersService {
getActivityLogs(partnerId: any, arg1: { type: string | undefined; page: number; limit: number; }) {
throw new Error('Method not implemented.');
}
getApiUsageStats(partnerId: any, period: string) {
throw new Error('Method not implemented.');
}
getSettings(partnerId: any) {
throw new Error('Method not implemented.');
}
updateSettings(partnerId: any, settings: any) {
throw new Error('Method not implemented.');
}
getTransactionHistory(partnerId: any, arg1: { startDate: Date | undefined; endDate: Date | undefined; page: number; limit: number; }) {
throw new Error('Method not implemented.');
}
listPartnerUsers(partnerId: any, arg1: { page: number; limit: number; status: string | undefined; }) {
throw new Error('Method not implemented.');
}
testWebhook(partnerId: any, url: string, event: string) {
throw new Error('Method not implemented.');
}
getRevenueAnalytics(partnerId: any, arg1: { period: string; groupBy: string; }) {
throw new Error('Method not implemented.');
}
getCallbacks(partnerId: any) {
throw new Error('Method not implemented.');
}
getUserDetails(partnerId: any, userId: string) {
throw new Error('Method not implemented.');
}
changePassword(partnerId: any, changePasswordDto: ChangePasswordDto) {
throw new Error('Method not implemented.');
}
getDashboardStats(partnerId: any, period: string) {
throw new Error('Method not implemented.');
}
updatePartner(partnerId: any, updatePartnerDto: UpdatePartnerDto) {
throw new Error('Method not implemented.');
}
deleteAccount(partnerId: any, password: string, reason: string | undefined) {
throw new Error('Method not implemented.');
}
constructor(private readonly prisma: PrismaService) {}
async register(dto: CreatePartnerDto) {
// Vérifier si l'email existe déjà
const existingPartner = await this.prisma.partner.findUnique({
where: { email: dto.email },
});
if (existingPartner) {
throw new ConflictException('Email already registered');
}
// Générer les clés API
const apiKey = this.generateApiKey();
const secretKey = this.generateSecretKey();
// Hasher le mot de passe
const passwordHash = await bcrypt.hash(dto.password, 10);
// Créer le partenaire
const partner = await this.prisma.partner.create({
data: {
name: dto.name,
email: dto.email,
passwordHash: passwordHash,
apiKey: apiKey,
secretKey: secretKey,
status: 'PENDING',
companyInfo: dto.companyInfo,
country: dto.country,
metadata: dto.metadata,
},
});
return {
partnerId: partner.id,
apiKey: partner.apiKey,
secretKey: partner.secretKey,
status: partner.status,
message: 'Partner registered successfully. Awaiting approval.',
};
}
async updateCallbacks(partnerId: string, dto: UpdateCallbacksDto) {
const partner = await this.prisma.partner.findUnique({
where: { id: partnerId },
});
if (!partner) {
throw new NotFoundException('Partner not found');
}
const updatedPartner = await this.prisma.partner.update({
where: { id: partnerId },
data: {
//callbacks: dto as unknown as Prisma.JsonValue,
// ou
callbacks: JSON.parse(JSON.stringify(dto)),
},
});
return {
partnerId: updatedPartner.id,
callbacks: updatedPartner.callbacks,
};
}
async getPartner(partnerId: string) {
const partner = await this.prisma.partner.findUnique({
where: { id: partnerId },
select: {
id: true,
name: true,
email: true,
status: true,
callbacks: true,
companyInfo: true,
createdAt: true,
_count: {
select: {
users: true,
subscriptions: true,
payments: true,
},
},
},
});
if (!partner) {
throw new NotFoundException('Partner not found');
}
return partner;
}
async getPartnerStats(partnerId: string) {
const today = new Date();
today.setHours(0, 0, 0, 0);
const [totalUsers, activeSubscriptions, todayPayments, monthRevenue] =
await Promise.all([
this.prisma.user.count({
where: { partnerId },
}),
this.prisma.subscription.count({
where: {
partnerId,
status: 'ACTIVE',
},
}),
this.prisma.payment.count({
where: {
partnerId,
createdAt: { gte: today },
},
}),
this.prisma.payment.aggregate({
where: {
partnerId,
status: 'SUCCESS',
createdAt: {
gte: new Date(today.getFullYear(), today.getMonth(), 1),
},
},
_sum: {
amount: true,
},
}),
]);
return {
totalUsers,
activeSubscriptions,
todayPayments,
monthRevenue: monthRevenue._sum.amount || 0,
};
}
async regenerateKeys(partnerId: string) {
const partner = await this.prisma.partner.findUnique({
where: { id: partnerId },
});
if (!partner) {
throw new NotFoundException('Partner not found');
}
const newApiKey = this.generateApiKey();
const newSecretKey = this.generateSecretKey();
const updatedPartner = await this.prisma.partner.update({
where: { id: partnerId },
data: {
apiKey: newApiKey,
secretKey: newSecretKey,
keysRotatedAt: new Date(),
},
});
return {
apiKey: updatedPartner.apiKey,
secretKey: updatedPartner.secretKey,
message: 'Keys regenerated successfully',
};
}
private generateApiKey(): string {
return `pk_${crypto.randomBytes(32).toString('hex')}`;
}
private generateSecretKey(): string {
return `sk_${crypto.randomBytes(32).toString('hex')}`;
}
}

View File

@ -89,28 +89,7 @@ export class PaymentsController {
return this.paymentsService.getPayment(paymentId, req.user.partnerId);
}
@Get()
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'List payments' })
@ApiQuery({ name: 'status', required: false, enum: ['PENDING', 'SUCCESS', 'FAILED', 'REFUNDED'] })
@ApiQuery({ name: 'userId', required: false })
@ApiQuery({ name: 'subscriptionId', required: false })
@ApiQuery({ name: 'startDate', required: false, type: Date })
@ApiQuery({ name: 'endDate', required: false, type: Date })
@ApiQuery({ name: 'page', required: false, type: Number, default: 1 })
@ApiQuery({ name: 'limit', required: false, type: Number, default: 20 })
@ApiResponse({
status: 200,
description: 'List of payments',
type: PaymentListResponseDto,
})
async listPayments(@Request() req, @Query() query: PaymentQueryDto) {
return this.paymentsService.listPayments({
partnerId: req.user.partnerId,
...query,
});
}
@Get('reference/:reference')
@UseGuards(JwtAuthGuard)
@ -148,26 +127,7 @@ export class PaymentsController {
return this.paymentsService.retryPayment(paymentId, req.user.partnerId);
}
@Get('statistics/summary')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get payment statistics' })
@ApiQuery({ name: 'period', required: false, enum: ['daily', 'weekly', 'monthly', 'yearly'] })
@ApiQuery({ name: 'startDate', required: false, type: Date })
@ApiQuery({ name: 'endDate', required: false, type: Date })
async getStatistics(
@Request() req,
@Query('period') period?: string,
@Query('startDate') startDate?: string,
@Query('endDate') endDate?: string,
) {
return this.paymentsService.getStatistics({
partnerId: req.user.partnerId,
period: period || 'monthly',
startDate: startDate ? new Date(startDate) : undefined,
endDate: endDate ? new Date(endDate) : undefined,
});
}
@Post('validate')
@UseGuards(JwtAuthGuard)

View File

@ -28,23 +28,7 @@ export class SubscriptionsController {
return this.subscriptionsService.create(req.user.partnerId, dto);
}
@Get()
@ApiOperation({ summary: 'List subscriptions' })
async list(
@Request() req,
@Query('status') status?: string,
@Query('userId') userId?: string,
@Query('page') page = 1,
@Query('limit') limit = 20,
) {
return this.subscriptionsService.list({
partnerId: req.user.partnerId,
status,
userId,
page,
limit,
});
}
@Get(':id')
@ApiOperation({ summary: 'Get subscription details' })

View File

@ -8,8 +8,7 @@ import { PlanService } from './services/plan.service';
import { BillingService } from './services/billing.service';
import { PrismaService } from '../../shared/services/prisma.service';
import { PaymentsModule } from '../payments/payments.module';
import { NotificationsModule } from '../notifications/notifications.module';
import { HttpModule } from '@nestjs/axios';
import { HttpModule } from '@nestjs/axios';
@Module({
imports: [
@ -21,7 +20,6 @@ import { HttpModule } from '@nestjs/axios';
name: 'billing',
}),
PaymentsModule,
NotificationsModule,
],
controllers: [SubscriptionsController],
providers: [