first commit
This commit is contained in:
parent
bde4f90235
commit
b409b81b18
23
package-lock.json
generated
23
package-lock.json
generated
@ -26,6 +26,8 @@
|
|||||||
"cache-manager-redis-store": "^3.0.1",
|
"cache-manager-redis-store": "^3.0.1",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.2",
|
"class-validator": "^0.14.2",
|
||||||
|
"passport-headerapikey": "^1.2.2",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
@ -9201,11 +9203,30 @@
|
|||||||
"url": "https://github.com/sponsors/jaredhanson"
|
"url": "https://github.com/sponsors/jaredhanson"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/passport-headerapikey": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/passport-headerapikey/-/passport-headerapikey-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-4BvVJRrWsNJPrd3UoZfcnnl4zvUWYKEtfYkoDsaOKBsrWHYmzTApCjs7qUbncOLexE9ul0IRiYBFfBG0y9IVQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": "^4.17.15",
|
||||||
|
"passport-strategy": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/passport-jwt": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jsonwebtoken": "^9.0.0",
|
||||||
|
"passport-strategy": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/passport-strategy": {
|
"node_modules/passport-strategy": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
|
||||||
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
|
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4.0"
|
"node": ">= 0.4.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,6 +37,8 @@
|
|||||||
"cache-manager-redis-store": "^3.0.1",
|
"cache-manager-redis-store": "^3.0.1",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.2",
|
"class-validator": "^0.14.2",
|
||||||
|
"passport-headerapikey": "^1.2.2",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus } from '@nestjs/common';
|
import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus } from '@nestjs/common';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Response } from 'express';
|
||||||
import { Response } from 'express';
|
import { Prisma } from 'generated/prisma';
|
||||||
|
|
||||||
@Catch(Prisma.PrismaClientKnownRequestError)
|
@Catch(Prisma.PrismaClientKnownRequestError)
|
||||||
export class PrismaExceptionFilter implements ExceptionFilter {
|
export class PrismaExceptionFilter implements ExceptionFilter {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { registerAs } from '@nestjs/config';
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
export default registerAs('app', () => ({
|
export default registerAs('app', () => ({
|
||||||
port: parseInt(process.env.PORT, 10) || 3000,
|
port: parseInt(process.env.PORT ?? "3000", 10) || 3000,
|
||||||
env: process.env.NODE_ENV || 'development',
|
env: process.env.NODE_ENV || 'development',
|
||||||
apiPrefix: process.env.API_PREFIX || 'v2',
|
apiPrefix: process.env.API_PREFIX || 'v2',
|
||||||
jwtSecret: process.env.JWT_SECRET || 'your-secret-key',
|
jwtSecret: process.env.JWT_SECRET || 'your-secret-key',
|
||||||
@ -11,6 +11,6 @@ export default registerAs('app', () => ({
|
|||||||
},
|
},
|
||||||
redis: {
|
redis: {
|
||||||
host: process.env.REDIS_HOST || 'localhost',
|
host: process.env.REDIS_HOST || 'localhost',
|
||||||
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
|
port: parseInt(process.env.REDIS_PORT?? "6379", 10) || 6379,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
3
src/modules/auth/auth.controller.ts
Normal file
3
src/modules/auth/auth.controller.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export class AuthController{
|
||||||
|
|
||||||
|
}
|
||||||
@ -14,13 +14,23 @@ import { OperatorsModule } from '../operators/operators.module';
|
|||||||
PassportModule.register({ defaultStrategy: 'jwt' }),
|
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||||
JwtModule.registerAsync({
|
JwtModule.registerAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
|
inject: [ConfigService],
|
||||||
useFactory: async (configService: ConfigService) => ({
|
useFactory: async (configService: ConfigService) => ({
|
||||||
secret: configService.get<string>('app.jwtSecret'),
|
secret: configService.get<string>('JWT_SECRET'),
|
||||||
signOptions: {
|
signOptions: {
|
||||||
expiresIn: configService.get<string>('app.jwtExpiresIn'),
|
expiresIn: configService.get('JWT_EXPIRATION') || '1h',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
inject: [ConfigService],
|
/*todo
|
||||||
|
useFactory: async (configService: ConfigService) => {
|
||||||
|
return {
|
||||||
|
secret: configService.get<string>('JWT_SECRET'),
|
||||||
|
signOptions: {
|
||||||
|
expiresIn: configService.get<string | number>('JWT_EXPIRATION') || '1h'
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},*/
|
||||||
|
|
||||||
}),
|
}),
|
||||||
OperatorsModule,
|
OperatorsModule,
|
||||||
],
|
],
|
||||||
|
|||||||
@ -8,10 +8,7 @@ export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy, 'api-
|
|||||||
constructor(private readonly prisma: PrismaService) {
|
constructor(private readonly prisma: PrismaService) {
|
||||||
super(
|
super(
|
||||||
{ header: 'X-API-Key', prefix: '' },
|
{ header: 'X-API-Key', prefix: '' },
|
||||||
true,
|
true
|
||||||
async (apiKey, done) => {
|
|
||||||
return this.validate(apiKey, done);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Controller, Post, Get, Body, Param, Query, UseGuards, Request } from '@
|
|||||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
import { NotificationsService } from './services/notifications.service';
|
import { NotificationsService } from './services/notifications.service';
|
||||||
import { SendNotificationDto, BulkNotificationDto } from './dto/notification.dto';
|
import { SendNotificationDto, BulkNotificationDto } from './dto/notification.dto';
|
||||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from 'src/common/guards/jwt-auth.guard';
|
||||||
|
|
||||||
@ApiTags('notifications')
|
@ApiTags('notifications')
|
||||||
@Controller('notifications')
|
@Controller('notifications')
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
|
|||||||
import { BullModule } from '@nestjs/bull';
|
import { BullModule } from '@nestjs/bull';
|
||||||
import { HttpModule } from '@nestjs/axios';
|
import { HttpModule } from '@nestjs/axios';
|
||||||
import { NotificationsController } from './notifications.controller';
|
import { NotificationsController } from './notifications.controller';
|
||||||
import { NotificationsService } from './notifications.service';
|
import { NotificationsService } from './services/notifications.service';
|
||||||
import { SmsService } from './services/sms.service';
|
import { SmsService } from './services/sms.service';
|
||||||
import { EmailService } from './services/email.service';
|
import { EmailService } from './services/email.service';
|
||||||
import { WebhookService } from './services/webhook.service';
|
import { WebhookService } from './services/webhook.service';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Process, Processor } from '@nestjs/bull';
|
import { Process, Processor } from '@nestjs/bull';
|
||||||
import { Job } from 'bull';
|
import bull from 'bull';
|
||||||
import { NotificationsService } from '../notifications.service';
|
import { NotificationsService } from '../services/notifications.service';
|
||||||
import { WebhookService } from '../services/webhook.service';
|
import { WebhookService } from '../services/webhook.service';
|
||||||
|
|
||||||
@Processor('notifications')
|
@Processor('notifications')
|
||||||
@ -10,7 +10,7 @@ export class NotificationProcessor {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Process('send-notification')
|
@Process('send-notification')
|
||||||
async handleSendNotification(job: Job) {
|
async handleSendNotification(job: bull.Job) {
|
||||||
const { notificationId } = job.data;
|
const { notificationId } = job.data;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -23,9 +23,9 @@ export class NotificationProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Process('bulk-send')
|
@Process('bulk-send')
|
||||||
async handleBulkSend(job: Job) {
|
async handleBulkSend(job: bull.Job) {
|
||||||
const { notifications } = job.data;
|
const { notifications } = job.data;
|
||||||
const results = [];
|
const results:any = [];
|
||||||
|
|
||||||
for (const notificationId of notifications) {
|
for (const notificationId of notifications) {
|
||||||
try {
|
try {
|
||||||
@ -47,7 +47,7 @@ export class WebhookProcessor {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Process('send-webhook')
|
@Process('send-webhook')
|
||||||
async handleSendWebhook(job: Job) {
|
async handleSendWebhook(job: bull.Job) {
|
||||||
const { webhookId, attempt } = job.data;
|
const { webhookId, attempt } = job.data;
|
||||||
|
|
||||||
return await this.webhookService.processWebhook(webhookId, attempt);
|
return await this.webhookService.processWebhook(webhookId, attempt);
|
||||||
|
|||||||
98
src/modules/notifications/services/email.service.ts
Normal file
98
src/modules/notifications/services/email.service.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
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,12 +1,12 @@
|
|||||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||||
import { InjectQueue } from '@nestjs/bull';
|
import { InjectQueue } from '@nestjs/bull';
|
||||||
import { Queue } from 'bull';
|
import bull from 'bull';
|
||||||
import { PrismaService } from '../../shared/services/prisma.service';
|
import { SmsService } from './sms.service';
|
||||||
import { SmsService } from './services/sms.service';
|
import { EmailService } from './email.service';
|
||||||
import { EmailService } from './services/email.service';
|
import { WebhookService } from './webhook.service';
|
||||||
import { WebhookService } from './services/webhook.service';
|
import { NotificationTemplateService } from './template.service';
|
||||||
import { NotificationTemplateService } from './services/template.service';
|
import { SendNotificationDto, BulkNotificationDto } from '../dto/notification.dto';
|
||||||
import { SendNotificationDto, BulkNotificationDto } from './dto/notification.dto';
|
import { PrismaService } from 'src/shared/services/prisma.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotificationsService {
|
export class NotificationsService {
|
||||||
@ -16,12 +16,12 @@ export class NotificationsService {
|
|||||||
private readonly emailService: EmailService,
|
private readonly emailService: EmailService,
|
||||||
private readonly webhookService: WebhookService,
|
private readonly webhookService: WebhookService,
|
||||||
private readonly templateService: NotificationTemplateService,
|
private readonly templateService: NotificationTemplateService,
|
||||||
@InjectQueue('notifications') private notificationQueue: Queue,
|
@InjectQueue('notifications') private notificationQueue: bull.Queue,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async send(partnerId: string, dto: SendNotificationDto) {
|
async send(partnerId: string, dto: SendNotificationDto) {
|
||||||
// Valider l'utilisateur si fourni
|
// Valider l'utilisateur si fourni
|
||||||
let user = null;
|
let user:any = null;
|
||||||
if (dto.userToken) {
|
if (dto.userToken) {
|
||||||
user = await this.prisma.user.findFirst({
|
user = await this.prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
@ -70,13 +70,13 @@ export class NotificationsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sendBulk(partnerId: string, dto: BulkNotificationDto) {
|
async sendBulk(partnerId: string, dto: BulkNotificationDto) {
|
||||||
const notifications = [];
|
const notifications :any= [];
|
||||||
|
|
||||||
// Récupérer les destinataires selon les critères
|
// Récupérer les destinataires selon les critères
|
||||||
const recipients = await this.getRecipients(partnerId, dto);
|
const recipients:any = await this.getRecipients(partnerId, dto);
|
||||||
|
|
||||||
for (const recipient of recipients) {
|
for (const recipient of recipients) {
|
||||||
const notification = await this.prisma.notification.create({
|
const notification:any = await this.prisma.notification.create({
|
||||||
data: {
|
data: {
|
||||||
partnerId: partnerId,
|
partnerId: partnerId,
|
||||||
userId: recipient.userId,
|
userId: recipient.userId,
|
||||||
@ -148,6 +148,7 @@ export class NotificationsService {
|
|||||||
subject: notification.subject,
|
subject: notification.subject,
|
||||||
content: notification.content,
|
content: notification.content,
|
||||||
template: notification.templateId,
|
template: notification.templateId,
|
||||||
|
message: ''
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -230,7 +231,7 @@ export class NotificationsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getRecipients(partnerId: string, dto: BulkNotificationDto) {
|
private async getRecipients(partnerId: string, dto: BulkNotificationDto) {
|
||||||
const recipients = [];
|
const recipients:any = [];
|
||||||
|
|
||||||
if (dto.userIds && dto.userIds.length > 0) {
|
if (dto.userIds && dto.userIds.length > 0) {
|
||||||
const users = await this.prisma.user.findMany({
|
const users = await this.prisma.user.findMany({
|
||||||
@ -259,7 +260,7 @@ export class NotificationsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getSegmentUsers(partnerId: string, segments: string[]) {
|
private async getSegmentUsers(partnerId: string, segments: string[]) {
|
||||||
const users = [];
|
const users:any = [];
|
||||||
|
|
||||||
for (const segment of segments) {
|
for (const segment of segments) {
|
||||||
switch (segment) {
|
switch (segment) {
|
||||||
|
|||||||
6
src/modules/notifications/services/template.service.ts
Normal file
6
src/modules/notifications/services/template.service.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export class NotificationTemplateService{
|
||||||
|
render(templateId: string | undefined, arg1: any) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { HttpService } from '@nestjs/axios';
|
import { HttpService } from '@nestjs/axios';
|
||||||
import { InjectQueue } from '@nestjs/bull';
|
import { InjectQueue } from '@nestjs/bull';
|
||||||
import { Queue } from 'bull';
|
import bull from 'bull';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
import { PrismaService } from '../../../shared/services/prisma.service';
|
import { PrismaService } from '../../../shared/services/prisma.service';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
@ -11,7 +11,7 @@ export class WebhookService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly httpService: HttpService,
|
private readonly httpService: HttpService,
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
@InjectQueue('webhooks') private webhookQueue: Queue,
|
@InjectQueue('webhooks') private webhookQueue: bull.Queue,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async send(params: {
|
async send(params: {
|
||||||
|
|||||||
@ -1,3 +1,35 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export interface AuthValidateParams{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthValidateResponse{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefundParams{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SmsParams{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SmsResponse{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefundResponse{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubscriptionParams{
|
||||||
|
|
||||||
|
}
|
||||||
|
export interface SubscriptionResponse{
|
||||||
|
|
||||||
|
}
|
||||||
export interface IOperatorAdapter {
|
export interface IOperatorAdapter {
|
||||||
initializeAuth(params: AuthInitParams): Promise<AuthInitResponse>;
|
initializeAuth(params: AuthInitParams): Promise<AuthInitResponse>;
|
||||||
validateAuth(params: AuthValidateParams): Promise<AuthValidateResponse>;
|
validateAuth(params: AuthValidateParams): Promise<AuthValidateResponse>;
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
} from './operator.adapter.interface';
|
} from './operator.adapter.interface';
|
||||||
import { OrangeTransformer } from '../transformers/orange.transformer';
|
import { OrangeTransformer } from '../transformers/orange.transformer';
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrangeAdapter implements IOperatorAdapter {
|
export class OrangeAdapter implements IOperatorAdapter {
|
||||||
private baseUrl: string;
|
private baseUrl: string;
|
||||||
@ -21,8 +22,8 @@ export class OrangeAdapter implements IOperatorAdapter {
|
|||||||
private readonly httpService: HttpService,
|
private readonly httpService: HttpService,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
this.baseUrl = this.configService.get('ORANGE_API_URL');
|
this.baseUrl = this.configService.get('ORANGE_API_URL') as string;
|
||||||
this.accessToken = this.configService.get('ORANGE_ACCESS_TOKEN');
|
this.accessToken = this.configService.get('ORANGE_ACCESS_TOKEN') as string;
|
||||||
this.transformer = new OrangeTransformer();
|
this.transformer = new OrangeTransformer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class OrangeTransformer {
|
|
||||||
transformChargeResponse(bizaoResponse: any): any {
|
|
||||||
return {
|
|
||||||
paymentId: bizaoResponse.amountTransaction?.serverReferenceCode,
|
|
||||||
status: this.mapStatus(
|
|
||||||
bizaoResponse.amountTransaction?.transactionOperationStatus,
|
|
||||||
),
|
|
||||||
operatorReference: bizaoResponse.amountTransaction?.serverReferenceCode,
|
|
||||||
amount: parseFloat(
|
|
||||||
bizaoResponse.amountTransaction?.paymentAmount?.totalAmountCharged,
|
|
||||||
),
|
|
||||||
currency:
|
|
||||||
bizaoResponse.amountTransaction?.paymentAmount?.chargingInformation
|
|
||||||
?.currency,
|
|
||||||
createdAt: new Date(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private mapStatus(bizaoStatus: string): string {
|
|
||||||
const statusMap = {
|
|
||||||
Charged: 'SUCCESS',
|
|
||||||
Failed: 'FAILED',
|
|
||||||
Pending: 'PENDING',
|
|
||||||
};
|
|
||||||
return statusMap[bizaoStatus] || 'PENDING';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,3 +1,30 @@
|
|||||||
export class OrangeTransformer{
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OrangeTransformer {
|
||||||
|
transformChargeResponse(bizaoResponse: any): any {
|
||||||
|
return {
|
||||||
|
paymentId: bizaoResponse.amountTransaction?.serverReferenceCode,
|
||||||
|
status: this.mapStatus(
|
||||||
|
bizaoResponse.amountTransaction?.transactionOperationStatus,
|
||||||
|
),
|
||||||
|
operatorReference: bizaoResponse.amountTransaction?.serverReferenceCode,
|
||||||
|
amount: parseFloat(
|
||||||
|
bizaoResponse.amountTransaction?.paymentAmount?.totalAmountCharged,
|
||||||
|
),
|
||||||
|
currency:
|
||||||
|
bizaoResponse.amountTransaction?.paymentAmount?.chargingInformation
|
||||||
|
?.currency,
|
||||||
|
createdAt: new Date(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapStatus(bizaoStatus: string): string {
|
||||||
|
const statusMap = {
|
||||||
|
Charged: 'SUCCESS',
|
||||||
|
Failed: 'FAILED',
|
||||||
|
Pending: 'PENDING',
|
||||||
|
};
|
||||||
|
return statusMap[bizaoStatus] || 'PENDING';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
4
src/modules/partners/partners.controller.ts
Normal file
4
src/modules/partners/partners.controller.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
//todoe
|
||||||
|
export class PartnersController{
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { Process, Processor } from '@nestjs/bull';
|
import { Process, Processor } from '@nestjs/bull';
|
||||||
import { Job } from 'bull';
|
import bull from 'bull';
|
||||||
import { SubscriptionsService } from '../subscriptions.service';
|
import { SubscriptionsService } from '../subscriptions.service';
|
||||||
import { HttpService } from '@nestjs/axios';
|
import { HttpService } from '@nestjs/axios';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
@ -12,7 +12,7 @@ export class SubscriptionProcessor {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Process('webhook-notification')
|
@Process('webhook-notification')
|
||||||
async handleWebhookNotification(job: Job) {
|
async handleWebhookNotification(job: bull.Job) {
|
||||||
const { url, event, subscription, payment } = job.data;
|
const { url, event, subscription, payment } = job.data;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -50,13 +50,13 @@ export class BillingProcessor {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Process('process-renewal')
|
@Process('process-renewal')
|
||||||
async handleRenewal(job: Job) {
|
async handleRenewal(job: bull.Job) {
|
||||||
const { subscriptionId } = job.data;
|
const { subscriptionId } = job.data;
|
||||||
await this.subscriptionsService.processRenewal(subscriptionId);
|
await this.subscriptionsService.processRenewal(subscriptionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Process('trial-end')
|
@Process('trial-end')
|
||||||
async handleTrialEnd(job: Job) {
|
async handleTrialEnd(job: bull.Job) {
|
||||||
const { subscriptionId } = job.data;
|
const { subscriptionId } = job.data;
|
||||||
|
|
||||||
// Convertir de TRIAL à ACTIVE et traiter le premier paiement
|
// Convertir de TRIAL à ACTIVE et traiter le premier paiement
|
||||||
@ -64,7 +64,7 @@ export class BillingProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Process('retry-renewal')
|
@Process('retry-renewal')
|
||||||
async handleRetryRenewal(job: Job) {
|
async handleRetryRenewal(job: bull.Job) {
|
||||||
const { subscriptionId, attempt } = job.data;
|
const { subscriptionId, attempt } = job.data;
|
||||||
|
|
||||||
console.log(`Retrying renewal for subscription ${subscriptionId}, attempt ${attempt}`);
|
console.log(`Retrying renewal for subscription ${subscriptionId}, attempt ${attempt}`);
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
import { InjectQueue } from '@nestjs/bull';
|
import { InjectQueue } from '@nestjs/bull';
|
||||||
import { Queue } from 'bull';
|
import bull from 'bull';
|
||||||
import { PrismaService } from '../../../shared/services/prisma.service';
|
import { PrismaService } from '../../../shared/services/prisma.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SubscriptionScheduler {
|
export class SubscriptionScheduler {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
@InjectQueue('billing') private billingQueue: Queue,
|
@InjectQueue('billing') private billingQueue: bull.Queue,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Cron(CronExpression.EVERY_HOUR)
|
@Cron(CronExpression.EVERY_HOUR)
|
||||||
|
|||||||
@ -45,6 +45,7 @@ export class BillingService {
|
|||||||
end: subscription.currentPeriodEnd,
|
end: subscription.currentPeriodEnd,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
partnerId: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mettre à jour la facture
|
// Mettre à jour la facture
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
|
import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
|
||||||
import { PrismaService } from '../../../shared/services/prisma.service';
|
import { PrismaService } from '../../../shared/services/prisma.service';
|
||||||
import { CreatePlanDto, UpdatePlanDto } from '../dto/plan.dto';
|
import { CreatePlanDto, UpdatePlanDto } from '../dto/plan.dto';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from 'generated/prisma';
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PlanService {
|
export class PlanService {
|
||||||
|
|||||||
@ -9,6 +9,15 @@ import { CreateSubscriptionDto, UpdateSubscriptionDto } from './dto/subscription
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SubscriptionsService {
|
export class SubscriptionsService {
|
||||||
|
get(id: string, partnerId: any) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
list(arg0: { partnerId: any; status: string | undefined; userId: string | undefined; page: number; limit: number; }) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
getInvoices(id: string, partnerId: any) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly paymentsService: PaymentsService,
|
private readonly paymentsService: PaymentsService,
|
||||||
@ -141,7 +150,7 @@ export class SubscriptionsService {
|
|||||||
if (dto.status === 'PAUSED' && subscription.status === 'ACTIVE') {
|
if (dto.status === 'PAUSED' && subscription.status === 'ACTIVE') {
|
||||||
updateData.status = 'PAUSED';
|
updateData.status = 'PAUSED';
|
||||||
updateData.pausedAt = new Date();
|
updateData.pausedAt = new Date();
|
||||||
} else if (dto.status === 'ACTIVE' && subscription.status === 'PAUSED') {
|
} else if (dto.status === 'ACTIVE' && subscription.status === 'SUSPENDED') {
|
||||||
updateData.status = 'ACTIVE';
|
updateData.status = 'ACTIVE';
|
||||||
updateData.pausedAt = null;
|
updateData.pausedAt = null;
|
||||||
// Recalculer la prochaine date de facturation
|
// Recalculer la prochaine date de facturation
|
||||||
@ -166,7 +175,7 @@ export class SubscriptionsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dto.metadata) {
|
if (dto.metadata) {
|
||||||
updateData.metadata = { ...subscription.metadata, ...dto.metadata };
|
updateData.metadata = { metadata:subscription.metadata, ...dto.metadata };
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedSubscription = await this.prisma.subscription.update({
|
const updatedSubscription = await this.prisma.subscription.update({
|
||||||
@ -202,9 +211,7 @@ export class SubscriptionsService {
|
|||||||
data: {
|
data: {
|
||||||
status: 'CANCELLED',
|
status: 'CANCELLED',
|
||||||
cancelledAt: new Date(),
|
cancelledAt: new Date(),
|
||||||
cancellationReason: reason,
|
|
||||||
metadata: {
|
metadata: {
|
||||||
...subscription.metadata,
|
|
||||||
cancellationDetails: {
|
cancellationDetails: {
|
||||||
reason,
|
reason,
|
||||||
cancelledBy: 'partner',
|
cancelledBy: 'partner',
|
||||||
@ -227,7 +234,7 @@ export class SubscriptionsService {
|
|||||||
where: { id: partnerId },
|
where: { id: partnerId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (partner?.callbacks?.subscription?.onCancel) {
|
if (partner?.callbacks?subscription?.onCancel) {
|
||||||
await this.subscriptionQueue.add('webhook-notification', {
|
await this.subscriptionQueue.add('webhook-notification', {
|
||||||
url: partner.callbacks.subscription.onCancel,
|
url: partner.callbacks.subscription.onCancel,
|
||||||
event: 'SUBSCRIPTION_CANCELLED',
|
event: 'SUBSCRIPTION_CANCELLED',
|
||||||
@ -259,10 +266,11 @@ export class SubscriptionsService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Créer le paiement de renouvellement
|
// Créer le paiement de renouvellement
|
||||||
|
//todo
|
||||||
const payment = await this.paymentsService.createCharge({
|
const payment = await this.paymentsService.createCharge({
|
||||||
userToken: subscription.user.userToken,
|
userToken: subscription.user.userToken,
|
||||||
amount: subscription.amount,
|
amount: subscription.plan.amount,
|
||||||
currency: subscription.currency,
|
currency: subscription.plan.currency,
|
||||||
description: `Renewal: ${subscription.plan.name}`,
|
description: `Renewal: ${subscription.plan.name}`,
|
||||||
reference: `REN-${subscription.id}-${Date.now()}`,
|
reference: `REN-${subscription.id}-${Date.now()}`,
|
||||||
metadata: {
|
metadata: {
|
||||||
@ -273,6 +281,7 @@ export class SubscriptionsService {
|
|||||||
end: this.calculatePeriodEnd(subscription.plan, subscription.currentPeriodEnd),
|
end: this.calculatePeriodEnd(subscription.plan, subscription.currentPeriodEnd),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
partnerId: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
if (payment.status === 'SUCCESS') {
|
if (payment.status === 'SUCCESS') {
|
||||||
@ -315,7 +324,7 @@ export class SubscriptionsService {
|
|||||||
await this.handleRenewalFailure(subscription);
|
await this.handleRenewalFailure(subscription);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//todo
|
||||||
private async processInitialPayment(subscription: any, callbackUrl?: string) {
|
private async processInitialPayment(subscription: any, callbackUrl?: string) {
|
||||||
try {
|
try {
|
||||||
const payment = await this.paymentsService.createCharge({
|
const payment = await this.paymentsService.createCharge({
|
||||||
@ -329,6 +338,7 @@ export class SubscriptionsService {
|
|||||||
subscriptionId: subscription.id,
|
subscriptionId: subscription.id,
|
||||||
type: 'initial',
|
type: 'initial',
|
||||||
},
|
},
|
||||||
|
partnerId:""
|
||||||
});
|
});
|
||||||
|
|
||||||
if (payment.status === 'SUCCESS') {
|
if (payment.status === 'SUCCESS') {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from 'generated/prisma';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user