fix error
This commit is contained in:
parent
3eae8d2805
commit
1ddc8e9ee4
@ -13,13 +13,10 @@ import databaseConfig from './config/database.config';
|
|||||||
|
|
||||||
// Import des modules
|
// Import des modules
|
||||||
import { PrismaService } from './shared/services/prisma.service';
|
import { PrismaService } from './shared/services/prisma.service';
|
||||||
import { AuthModule } from './modules/auth/auth.module';
|
import { OperatorsModule } from './modules/operators/operators.module';
|
||||||
import { PartnersModule } from './modules/partners/partners.module';
|
|
||||||
import { OperatorsModule } from './modules/operators/operators.module';
|
|
||||||
import { PaymentsModule } from './modules/payments/payments.module';
|
import { PaymentsModule } from './modules/payments/payments.module';
|
||||||
import { SubscriptionsModule } from './modules/subscriptions/subscriptions.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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -51,12 +48,9 @@ import { OtpChallengeModule } from './modules/challenge/otp.challenge.module';
|
|||||||
}),
|
}),
|
||||||
ScheduleModule.forRoot(),
|
ScheduleModule.forRoot(),
|
||||||
EventEmitterModule.forRoot(),
|
EventEmitterModule.forRoot(),
|
||||||
AuthModule,
|
|
||||||
PartnersModule,
|
|
||||||
OperatorsModule,
|
OperatorsModule,
|
||||||
PaymentsModule,
|
PaymentsModule,
|
||||||
SubscriptionsModule,
|
SubscriptionsModule,
|
||||||
NotificationsModule,
|
|
||||||
OtpChallengeModule
|
OtpChallengeModule
|
||||||
],
|
],
|
||||||
providers: [PrismaService],
|
providers: [PrismaService],
|
||||||
|
|||||||
@ -29,8 +29,7 @@ async function bootstrap() {
|
|||||||
.setTitle('Payment Hub API')
|
.setTitle('Payment Hub API')
|
||||||
.setDescription('Unified DCB Payment Aggregation Platform')
|
.setDescription('Unified DCB Payment Aggregation Platform')
|
||||||
.setVersion('1.0.0')
|
.setVersion('1.0.0')
|
||||||
.addBearerAuth()
|
.addBearerAuth()
|
||||||
.addTag('auth')
|
|
||||||
.addTag('payments')
|
.addTag('payments')
|
||||||
.addTag('subscriptions')
|
.addTag('subscriptions')
|
||||||
.build();
|
.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(
|
async verifyOtp(
|
||||||
@Param('challengeId') challengeId: string,
|
@Param('challengeId') challengeId: string,
|
||||||
@Body('otpCode') otpCode: string,
|
@Body('otpCode') otpCode: string,
|
||||||
@Headers('x-merchant-id') merchantId: string,
|
@Headers('X-Merchant-ID') merchantId: string,
|
||||||
@Headers('x-api-key') apiKey: string,
|
@Headers('x-API-KEY') apiKey: string,
|
||||||
): Promise<OtpChallengeResponseDto> {
|
): Promise<OtpChallengeResponseDto> {
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`[VERIFY] Merchant: ${merchantId}, Challenge: ${challengeId}`,
|
`[VERIFY] Merchant: ${merchantId}, Challenge: ${challengeId}`,
|
||||||
@ -186,11 +186,7 @@ export class OtpChallengeController {
|
|||||||
// Valider les headers
|
// Valider les headers
|
||||||
this.validateMerchantHeaders(merchantId, apiKey);
|
this.validateMerchantHeaders(merchantId, apiKey);
|
||||||
|
|
||||||
// Valider le code OTP
|
// Valider le code OTP
|
||||||
if (!otpCode || otpCode.trim().length === 0) {
|
|
||||||
throw new HttpException('OTP code is required', HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Appeler le service
|
// Appeler le service
|
||||||
const response = await this.otpChallengeService.verifyOtp(
|
const response = await this.otpChallengeService.verifyOtp(
|
||||||
challengeId,
|
challengeId,
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import { OtpChallengeService } from './otp.challenge.service';
|
|||||||
{
|
{
|
||||||
provide: 'ORANGE_CONFIG',
|
provide: 'ORANGE_CONFIG',
|
||||||
useFactory: (configService: ConfigService): OrangeConfig => ({
|
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'),
|
partnerId: configService.get<string>('ORANGE_PARTNER_ID', 'PDKSUB'),
|
||||||
clientId: configService.get<string>('ORANGE_CLIENT_ID', 'admin'),
|
clientId: configService.get<string>('ORANGE_CLIENT_ID', 'admin'),
|
||||||
clientSecret: configService.get<string>('ORANGE_CLIENT_SECRET', 'admin'),
|
clientSecret: configService.get<string>('ORANGE_CLIENT_SECRET', 'admin'),
|
||||||
|
|||||||
@ -28,6 +28,16 @@ export class OtpChallengeService implements IOtpChallengeService {
|
|||||||
// Appeler l'adaptateur Orange
|
// Appeler l'adaptateur Orange
|
||||||
const response = await this.orangeAdapter.initiateChallenge(request);
|
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;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return this.createErrorResponse(request, 'INITIATE_FAILED', error.message);
|
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',
|
message: 'Connection successful',
|
||||||
details: {
|
details: {
|
||||||
sessionId: result.sessionId,
|
sessionId: result.sessionId,
|
||||||
authMethod: result.authMethod,
|
// authMethod: result.authMethod,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -385,7 +385,7 @@ export class OperatorsService{
|
|||||||
return country;
|
return country;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private detectOperatorFromPrefix(msisdn: string, countryCode: string): string {
|
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);
|
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')
|
@Get('reference/:reference')
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ -148,26 +127,7 @@ export class PaymentsController {
|
|||||||
return this.paymentsService.retryPayment(paymentId, req.user.partnerId);
|
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')
|
@Post('validate')
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
|
|||||||
@ -28,23 +28,7 @@ export class SubscriptionsController {
|
|||||||
return this.subscriptionsService.create(req.user.partnerId, dto);
|
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')
|
@Get(':id')
|
||||||
@ApiOperation({ summary: 'Get subscription details' })
|
@ApiOperation({ summary: 'Get subscription details' })
|
||||||
|
|||||||
@ -8,8 +8,7 @@ import { PlanService } from './services/plan.service';
|
|||||||
import { BillingService } from './services/billing.service';
|
import { BillingService } from './services/billing.service';
|
||||||
import { PrismaService } from '../../shared/services/prisma.service';
|
import { PrismaService } from '../../shared/services/prisma.service';
|
||||||
import { PaymentsModule } from '../payments/payments.module';
|
import { PaymentsModule } from '../payments/payments.module';
|
||||||
import { NotificationsModule } from '../notifications/notifications.module';
|
import { HttpModule } from '@nestjs/axios';
|
||||||
import { HttpModule } from '@nestjs/axios';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -20,8 +19,7 @@ import { HttpModule } from '@nestjs/axios';
|
|||||||
BullModule.registerQueue({
|
BullModule.registerQueue({
|
||||||
name: 'billing',
|
name: 'billing',
|
||||||
}),
|
}),
|
||||||
PaymentsModule,
|
PaymentsModule,
|
||||||
NotificationsModule,
|
|
||||||
],
|
],
|
||||||
controllers: [SubscriptionsController],
|
controllers: [SubscriptionsController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user