fix error
This commit is contained in:
parent
3eae8d2805
commit
1ddc8e9ee4
@ -13,12 +13,9 @@ 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 { 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';
|
||||
|
||||
@Module({
|
||||
@ -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],
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
import { Controller, Get } from "@nestjs/common";
|
||||
|
||||
//todo
|
||||
@Controller()
|
||||
export class AuthController{
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
export class NotificationTemplateService{
|
||||
render(templateId: string | undefined, arg1: any) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
}
|
||||
@ -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}`);
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
@ -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')}`;
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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' })
|
||||
|
||||
@ -8,7 +8,6 @@ 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';
|
||||
|
||||
@Module({
|
||||
@ -21,7 +20,6 @@ import { HttpModule } from '@nestjs/axios';
|
||||
name: 'billing',
|
||||
}),
|
||||
PaymentsModule,
|
||||
NotificationsModule,
|
||||
],
|
||||
controllers: [SubscriptionsController],
|
||||
providers: [
|
||||
|
||||
Loading…
Reference in New Issue
Block a user