payment object
This commit is contained in:
parent
767201ec06
commit
d8ad43a56a
@ -1,5 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { HttpService } from '@nestjs/axios';
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
import axios, { AxiosInstance, AxiosError } from 'axios';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
@ -10,27 +11,49 @@ import {
|
|||||||
ChargeResponse,
|
ChargeResponse,
|
||||||
} from './operator.adapter.interface';
|
} from './operator.adapter.interface';
|
||||||
import { OrangeTransformer } from '../transformers/orange.transformer';
|
import { OrangeTransformer } from '../transformers/orange.transformer';
|
||||||
|
import {
|
||||||
|
DEFAULT_ORANGE_CONFIG,
|
||||||
|
COUNTRY_CODE_MAPPING,
|
||||||
|
} from './orange.config';
|
||||||
|
|
||||||
|
import type { OrangeConfig } from './orange.config';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrangeAdapter implements IOperatorAdapter {
|
export class OrangeAdapter implements IOperatorAdapter {
|
||||||
|
private readonly logger = new Logger(OrangeAdapter.name);
|
||||||
|
private config: OrangeConfig;
|
||||||
private baseUrl: string;
|
private baseUrl: string;
|
||||||
private accessToken: string;
|
private accessToken: string;
|
||||||
private transformer: OrangeTransformer;
|
private transformer: OrangeTransformer;
|
||||||
|
private tokenExpiresAt: number = 0;
|
||||||
|
private axiosInstance: AxiosInstance;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly httpService: HttpService,
|
private readonly httpService: HttpService,
|
||||||
private readonly configService: ConfigService,
|
@Inject('ORANGE_CONFIG')config: OrangeConfig,
|
||||||
) {
|
) {
|
||||||
this.baseUrl = this.configService.get('ORANGE_API_URL') as string;
|
this.config = { ...DEFAULT_ORANGE_CONFIG, ...config } as OrangeConfig;
|
||||||
this.accessToken = this.configService.get('ORANGE_ACCESS_TOKEN') as string;
|
this.axiosInstance = axios.create({
|
||||||
|
baseURL: this.config.baseUrl,
|
||||||
|
timeout: this.config.timeout,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: '*/*',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.axiosInstance.interceptors.response.use(
|
||||||
|
response => response,
|
||||||
|
error => this.handleError(error)
|
||||||
|
);
|
||||||
|
|
||||||
this.transformer = new OrangeTransformer();
|
this.transformer = new OrangeTransformer();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async initializeAuth(params: AuthInitParams): Promise<AuthInitResponse> {
|
async initializeAuth(params: AuthInitParams): Promise<AuthInitResponse> {
|
||||||
const countryCode = this.getCountryCode(params.country);
|
const countryCode = this.getCountryCode(params.country);
|
||||||
|
|
||||||
const bizaoRequest = {
|
const hubRequest = {
|
||||||
challenge: {
|
challenge: {
|
||||||
method: 'OTP-SMS-AUTH',
|
method: 'OTP-SMS-AUTH',
|
||||||
country: countryCode,
|
country: countryCode,
|
||||||
@ -64,7 +87,7 @@ export class OrangeAdapter implements IOperatorAdapter {
|
|||||||
const response = await firstValueFrom(
|
const response = await firstValueFrom(
|
||||||
this.httpService.post(
|
this.httpService.post(
|
||||||
`${this.baseUrl}/challenge/v1/challenges`,
|
`${this.baseUrl}/challenge/v1/challenges`,
|
||||||
bizaoRequest,
|
hubRequest,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${this.accessToken}`,
|
Authorization: `Bearer ${this.accessToken}`,
|
||||||
@ -87,7 +110,7 @@ export class OrangeAdapter implements IOperatorAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async validateAuth(params: any): Promise<any> {
|
async validateAuth(params: any): Promise<any> {
|
||||||
const bizaoRequest = {
|
const hubRequest = {
|
||||||
challenge: {
|
challenge: {
|
||||||
method: 'OTP-SMS-AUTH',
|
method: 'OTP-SMS-AUTH',
|
||||||
country: params.country,
|
country: params.country,
|
||||||
@ -113,7 +136,7 @@ export class OrangeAdapter implements IOperatorAdapter {
|
|||||||
const response = await firstValueFrom(
|
const response = await firstValueFrom(
|
||||||
this.httpService.post(
|
this.httpService.post(
|
||||||
`${this.baseUrl}/challenge/v1/challenges/${params.challengeId}`,
|
`${this.baseUrl}/challenge/v1/challenges/${params.challengeId}`,
|
||||||
bizaoRequest,
|
hubRequest,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${this.accessToken}`,
|
Authorization: `Bearer ${this.accessToken}`,
|
||||||
@ -139,7 +162,11 @@ export class OrangeAdapter implements IOperatorAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async charge(params: ChargeParams): Promise<ChargeResponse> {
|
async charge(params: ChargeParams): Promise<ChargeResponse> {
|
||||||
const bizaoRequest = {
|
this.logger.debug(
|
||||||
|
`[orange adapter charge ]: ${JSON.stringify(params, null, 2)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const hubRequest = {
|
||||||
amountTransaction: {
|
amountTransaction: {
|
||||||
endUserId: 'acr:OrangeAPIToken',
|
endUserId: 'acr:OrangeAPIToken',
|
||||||
paymentAmount: {
|
paymentAmount: {
|
||||||
@ -149,31 +176,38 @@ export class OrangeAdapter implements IOperatorAdapter {
|
|||||||
description: params.description,
|
description: params.description,
|
||||||
},
|
},
|
||||||
chargingMetaData: {
|
chargingMetaData: {
|
||||||
onBehalfOf: 'PaymentHub',
|
onBehalfOf: 'PaymentHub', //from config todo
|
||||||
|
purchaseCategoryCode: 'Service', //todo from config
|
||||||
serviceId: 'BIZAO',
|
serviceId: 'BIZAO',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
transactionOperationStatus: 'Charged',
|
transactionOperationStatus: 'Charged',
|
||||||
referenceCode: params.reference,
|
referenceCode: params.reference,
|
||||||
clientCorrelator: `${params.reference}-${Date.now()}`,
|
clientCorrelator: `${params.reference}-${Date.now()}`, //uniquely identifies this create charge request.
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const token = await this.getAccessToken();
|
||||||
|
this.logger.debug(
|
||||||
|
`[requesting to ]: ${this.config.baseUrl}/payment/v1/acr%3AOrangeAPIToken/transactions/amount`,
|
||||||
|
);
|
||||||
|
|
||||||
const response = await firstValueFrom(
|
const response = await firstValueFrom(
|
||||||
this.httpService.post(
|
this.httpService.post(
|
||||||
`${this.baseUrl}/payment/v1/acr%3AOrangeAPIToken/transactions/amount`,
|
`${this.config.baseUrl}}/payment/v1/acr%3AOrangeAPIToken/transactions/amount`,
|
||||||
bizaoRequest,
|
hubRequest,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${this.accessToken}`,
|
Authorization: `Bearer ${token}`,
|
||||||
'bizao-token': params.userToken,
|
'X-Orange-ISE2': params.userToken,
|
||||||
'bizao-alias': params.userAlias,
|
'X-Orange-MCO': 'orange', //from country todo
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.logger.debug(`[response fromm orange ]: ${JSON.stringify(response.data, null, 2)}`,)
|
||||||
|
|
||||||
return this.transformer.transformChargeResponse(response.data);
|
return this.transformer.transformChargeResponse(response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,4 +291,53 @@ export class OrangeAdapter implements IOperatorAdapter {
|
|||||||
};
|
};
|
||||||
return senderMap[country];
|
return senderMap[country];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getAccessToken(): Promise<string> {
|
||||||
|
// Vérifier si le token est encore valide (avec une marge de 60 secondes)
|
||||||
|
if (this.accessToken && Date.now() < this.tokenExpiresAt - 60000) {
|
||||||
|
return this.accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const auth = Buffer.from(
|
||||||
|
`${this.config.clientId}:${this.config.clientSecret}`,
|
||||||
|
).toString('base64');
|
||||||
|
|
||||||
|
//this.logger.debug( `request to get acces token , ${this.config.baseUrl}${this.config.tokenEndpoint}`)
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
`${this.config.baseUrl}${this.config.tokenEndpoint}`,
|
||||||
|
'grant_type=client_credentials',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${auth}`,
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
Accept: '*/*',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.accessToken = response.data.access_token;
|
||||||
|
const expiresIn = response.data.expires_in || 3600;
|
||||||
|
this.tokenExpiresAt = Date.now() + expiresIn * 1000;
|
||||||
|
|
||||||
|
return this.accessToken as string;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to obtain Orange access token: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private handleError(error: AxiosError): never {
|
||||||
|
if (error.response) {
|
||||||
|
const data = error.response.data as any;
|
||||||
|
throw new Error(
|
||||||
|
`Orange API Error: ${data?.error?.message || error.message} (Code: ${data?.error?.code || error.response.status})`
|
||||||
|
);
|
||||||
|
} else if (error.request) {
|
||||||
|
throw new Error(`No response from Orange API: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Request error: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
src/modules/operators/adapters/orange.config.ts
Normal file
46
src/modules/operators/adapters/orange.config.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
export interface OrangeConfig {
|
||||||
|
baseUrl: string;
|
||||||
|
partnerId: string;
|
||||||
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
defaultService: string;
|
||||||
|
defaultOtpLength: number;
|
||||||
|
defaultSenderName: string;
|
||||||
|
defaultOtpMessage: string;
|
||||||
|
tokenEndpoint: string;
|
||||||
|
challengeEndpoint: string;
|
||||||
|
timeout: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_ORANGE_CONFIG: Partial<OrangeConfig> = {
|
||||||
|
defaultOtpLength: 4,
|
||||||
|
defaultOtpMessage: 'To confirm your purchase please enter the code %OTP%',
|
||||||
|
tokenEndpoint: '/oauth/v3/token',
|
||||||
|
challengeEndpoint: '/challenge/v1/challenges',
|
||||||
|
timeout: 30000, // 30 secondes
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping des codes pays ISO vers les codes Orange
|
||||||
|
*/
|
||||||
|
export const COUNTRY_CODE_MAPPING: Record<string, string> = {
|
||||||
|
'SN': 'SEN', // Sénégal
|
||||||
|
'CI': 'CIV', // Côte d'Ivoire
|
||||||
|
'CM': 'CMR', // Cameroun
|
||||||
|
'CD': 'COD', // RD Congo
|
||||||
|
'BF': 'BFA', // Burkina Faso
|
||||||
|
'TN': 'TUN', // Tunisie
|
||||||
|
'ML': 'MLI', // Mali
|
||||||
|
'GN': 'GIN', // Guinée
|
||||||
|
'NE': 'NER', // Niger
|
||||||
|
'MG': 'MDG', // Madagascar
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping des méthodes OTP génériques vers Orange
|
||||||
|
*/
|
||||||
|
export const OTP_METHOD_MAPPING: Record<string, string> = {
|
||||||
|
'SMS': 'OTP-SMS-AUTH',
|
||||||
|
'USSD': 'OTP-USSD-AUTH',
|
||||||
|
'IVR': 'OTP-IVR-AUTH',
|
||||||
|
};
|
||||||
@ -8,6 +8,8 @@ import { MTNAdapter } from './adapters/mtn.adapter';
|
|||||||
import { OrangeTransformer } from './transformers/orange.transformer';
|
import { OrangeTransformer } from './transformers/orange.transformer';
|
||||||
import { MTNTransformer } from './transformers/mtn.transformer';
|
import { MTNTransformer } from './transformers/mtn.transformer';
|
||||||
import { PrismaService } from '../../shared/services/prisma.service';
|
import { PrismaService } from '../../shared/services/prisma.service';
|
||||||
|
import { OrangeConfig } from './adapters/orange.config';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -15,9 +17,32 @@ import { PrismaService } from '../../shared/services/prisma.service';
|
|||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
maxRedirects: 3,
|
maxRedirects: 3,
|
||||||
}),
|
}),
|
||||||
|
ConfigModule
|
||||||
|
|
||||||
],
|
],
|
||||||
controllers: [OperatorsController],
|
controllers: [OperatorsController],
|
||||||
providers: [
|
providers: [
|
||||||
|
{
|
||||||
|
provide: 'ORANGE_CONFIG',
|
||||||
|
useFactory: (configService: ConfigService): OrangeConfig => ({
|
||||||
|
baseUrl: configService.get<string>('ORANGE_BASE_URL', 'https://webhook.site/69ce9344-f87b-421a-a494-c59eca7c54ce'),
|
||||||
|
//tokenUrl: configService.get<string>('ORANGE_BASE_URL', 'https://webhook.site/69ce9344-f87b-421a-a494-c59eca7c54ce'),
|
||||||
|
partnerId: configService.get<string>('ORANGE_PARTNER_ID', 'PDKSUB'),
|
||||||
|
clientId: configService.get<string>('ORANGE_CLIENT_ID', 'admin'),
|
||||||
|
clientSecret: configService.get<string>('ORANGE_CLIENT_SECRET', 'admin'),
|
||||||
|
defaultService: configService.get<string>('ORANGE_DEFAULT_SERVICE', 'DCB_SERVICE'),
|
||||||
|
defaultOtpLength: configService.get<number>('ORANGE_DEFAULT_OTP_LENGTH', 4),
|
||||||
|
defaultSenderName: configService.get<string>('ORANGE_DEFAULT_SENDER_NAME', 'OTP'),
|
||||||
|
defaultOtpMessage: configService.get<string>(
|
||||||
|
'ORANGE_DEFAULT_OTP_MESSAGE',
|
||||||
|
'To confirm your purchase please enter the code %OTP%'
|
||||||
|
),
|
||||||
|
tokenEndpoint: '/oauth/v3/token',
|
||||||
|
challengeEndpoint: '/challenge/v1/challenges',
|
||||||
|
timeout: configService.get<number>('ORANGE_TIMEOUT', 30000),
|
||||||
|
}),
|
||||||
|
inject: [ConfigService],
|
||||||
|
},
|
||||||
OperatorsService,
|
OperatorsService,
|
||||||
OperatorAdapterFactory,
|
OperatorAdapterFactory,
|
||||||
OrangeAdapter,
|
OrangeAdapter,
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { BadRequestException, NotFoundException } from "@nestjs/common";
|
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { PrismaService } from "src/shared/services/prisma.service";
|
import { PrismaService } from "src/shared/services/prisma.service";
|
||||||
import { OperatorAdapterFactory } from "./adapters/operator-adapter.factory";
|
import { OperatorAdapterFactory } from "./adapters/operator-adapter.factory";
|
||||||
import { HttpService } from "@nestjs/axios";
|
import { HttpService } from "@nestjs/axios";
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
//todo tomaj
|
//todo tomaj
|
||||||
|
@Injectable()
|
||||||
export class OperatorsService{
|
export class OperatorsService{
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -127,12 +128,6 @@ export class OperatorsService{
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private getCountryName(code: string): string {
|
private getCountryName(code: string): string {
|
||||||
const countries = {
|
const countries = {
|
||||||
CI: 'Côte d\'Ivoire',
|
CI: 'Côte d\'Ivoire',
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export class ChargeDto {
|
|||||||
@ApiProperty({ required: false, description: 'Subscription ID if recurring' })
|
@ApiProperty({ required: false, description: 'Subscription ID if recurring' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
subscriptionId?: string;
|
subscriptionId?: number;
|
||||||
|
|
||||||
@ApiProperty({ required: false, description: 'Callback URL for notifications' })
|
@ApiProperty({ required: false, description: 'Callback URL for notifications' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -49,7 +49,15 @@ export class ChargeDto {
|
|||||||
|
|
||||||
@ApiProperty({ required: false, description: 'partnerId ' })
|
@ApiProperty({ required: false, description: 'partnerId ' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
partnerId: string;
|
partnerId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, description: 'country ' })
|
||||||
|
@IsOptional()
|
||||||
|
country: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, description: 'operator ' })
|
||||||
|
@IsOptional()
|
||||||
|
operator: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RefundDto {
|
export class RefundDto {
|
||||||
|
|||||||
@ -10,6 +10,9 @@ import {
|
|||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
|
Headers,
|
||||||
|
Logger,
|
||||||
|
ParseIntPipe,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
ApiTags,
|
ApiTags,
|
||||||
@ -32,10 +35,12 @@ import { ApiKeyGuard } from '../../common/guards/api-key.guard';
|
|||||||
@ApiTags('payments')
|
@ApiTags('payments')
|
||||||
@Controller('payments')
|
@Controller('payments')
|
||||||
export class PaymentsController {
|
export class PaymentsController {
|
||||||
|
private readonly logger = new Logger(PaymentsController.name);
|
||||||
|
|
||||||
constructor(private readonly paymentsService: PaymentsService) {}
|
constructor(private readonly paymentsService: PaymentsService) {}
|
||||||
|
|
||||||
@Post('charge')
|
@Post('charge')
|
||||||
@UseGuards(JwtAuthGuard)
|
//@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@HttpCode(HttpStatus.CREATED)
|
@HttpCode(HttpStatus.CREATED)
|
||||||
@ApiOperation({ summary: 'Create a new charge' })
|
@ApiOperation({ summary: 'Create a new charge' })
|
||||||
@ -46,10 +51,18 @@ export class PaymentsController {
|
|||||||
})
|
})
|
||||||
@ApiResponse({ status: 400, description: 'Bad request' })
|
@ApiResponse({ status: 400, description: 'Bad request' })
|
||||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||||
async createCharge(@Request() req, @Body() chargeDto: ChargeDto) {
|
async createCharge(
|
||||||
|
@Headers('X-Merchant-ID') merchantId: string,
|
||||||
|
@Headers('X-COUNTRY') coutnry: string,
|
||||||
|
@Headers('X-OPERATOR') operator: string,
|
||||||
|
@Request() req, @Body() chargeDto: ChargeDto) {
|
||||||
|
this.logger.debug(
|
||||||
|
`[request charge to hub ]: ${JSON.stringify(chargeDto, null, 2)}`,
|
||||||
|
)
|
||||||
return this.paymentsService.createCharge({
|
return this.paymentsService.createCharge({
|
||||||
...chargeDto,
|
...chargeDto,
|
||||||
partnerId: req.user.partnerId,
|
country: coutnry,
|
||||||
|
operator: operator,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +105,7 @@ export class PaymentsController {
|
|||||||
|
|
||||||
|
|
||||||
@Get('reference/:reference')
|
@Get('reference/:reference')
|
||||||
@UseGuards(JwtAuthGuard)
|
//@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOperation({ summary: 'Get payment by reference' })
|
@ApiOperation({ summary: 'Get payment by reference' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
@ -111,7 +124,7 @@ export class PaymentsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post(':paymentId/retry')
|
@Post(':paymentId/retry')
|
||||||
@UseGuards(JwtAuthGuard)
|
// @UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: 'Retry a failed payment' })
|
@ApiOperation({ summary: 'Retry a failed payment' })
|
||||||
@ -130,7 +143,7 @@ export class PaymentsController {
|
|||||||
|
|
||||||
|
|
||||||
@Post('validate')
|
@Post('validate')
|
||||||
@UseGuards(JwtAuthGuard)
|
//@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: 'Validate payment before processing' })
|
@ApiOperation({ summary: 'Validate payment before processing' })
|
||||||
@ -143,7 +156,7 @@ export class PaymentsController {
|
|||||||
|
|
||||||
// Webhook endpoints
|
// Webhook endpoints
|
||||||
@Post('webhook/callback')
|
@Post('webhook/callback')
|
||||||
@UseGuards(ApiKeyGuard)
|
//@UseGuards(ApiKeyGuard)
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: 'Webhook callback for payment updates' })
|
@ApiOperation({ summary: 'Webhook callback for payment updates' })
|
||||||
async handleWebhook(@Request() req, @Body() payload: any) {
|
async handleWebhook(@Request() req, @Body() payload: any) {
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import { PaymentProcessor } from './processors/payment.processor';
|
|||||||
import { WebhookService } from './services/webhook.service';
|
import { WebhookService } from './services/webhook.service';
|
||||||
import { PrismaService } from '../../shared/services/prisma.service';
|
import { PrismaService } from '../../shared/services/prisma.service';
|
||||||
import { OperatorsModule } from '../operators/operators.module';
|
import { OperatorsModule } from '../operators/operators.module';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { SubscriptionsModule } from '../subscriptions/subscriptions.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -16,6 +18,8 @@ import { OperatorsModule } from '../operators/operators.module';
|
|||||||
name: 'webhooks',
|
name: 'webhooks',
|
||||||
}),
|
}),
|
||||||
OperatorsModule,
|
OperatorsModule,
|
||||||
|
SubscriptionsModule,
|
||||||
|
|
||||||
],
|
],
|
||||||
controllers: [PaymentsController],
|
controllers: [PaymentsController],
|
||||||
providers: [PaymentsService, PaymentProcessor, WebhookService, PrismaService],
|
providers: [PaymentsService, PaymentProcessor, WebhookService, PrismaService],
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
import { Injectable, BadRequestException, Logger } from '@nestjs/common';
|
||||||
import { OperatorsService } from '../operators/operators.service';
|
import { OperatorsService } from '../operators/operators.service';
|
||||||
import { PrismaService } from '../../shared/services/prisma.service';
|
import { PrismaService } from '../../shared/services/prisma.service';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
@ -8,6 +8,7 @@ import { PaymentType, TransactionStatus } from 'generated/prisma';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PaymentsService {
|
export class PaymentsService {
|
||||||
|
private readonly logger = new Logger(PaymentsService.name);
|
||||||
handleWebhook(arg0: { partnerId: any; event: any; payload: any; signature: any; }) {
|
handleWebhook(arg0: { partnerId: any; event: any; payload: any; signature: any; }) {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
@ -33,7 +34,7 @@ export class PaymentsService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createCharge(chargeDto: ChargeDto) {
|
async createCharge(chargeDto: ChargeDto) {
|
||||||
// Récupérer les informations de l'utilisateur
|
/* Récupérer les informations de l'utilisateur
|
||||||
const user = await this.prisma.user.findUnique({
|
const user = await this.prisma.user.findUnique({
|
||||||
where: { userToken: chargeDto.userToken },
|
where: { userToken: chargeDto.userToken },
|
||||||
// include: { operator: true },
|
// include: { operator: true },
|
||||||
@ -42,12 +43,13 @@ export class PaymentsService {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
throw new BadRequestException('Invalid user token');
|
throw new BadRequestException('Invalid user token');
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// Créer la transaction dans la base
|
// Créer la transaction dans la base
|
||||||
const payment = await this.prisma.payment.create({
|
const payment = await this.prisma.payment.create({
|
||||||
|
|
||||||
data: {
|
data: {
|
||||||
merchantPartnerId:1, // À remplacer par le bon partnerId
|
merchantPartnerId:chargeDto.partnerId , // À remplacer par le bon partnerId
|
||||||
customerId: 1, // todo À remplacer par user.id
|
customerId: 1, // todo À remplacer par user.id
|
||||||
amount: chargeDto.amount,
|
amount: chargeDto.amount,
|
||||||
currency: chargeDto.currency,
|
currency: chargeDto.currency,
|
||||||
@ -61,21 +63,29 @@ export class PaymentsService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Router vers le bon opérateur
|
// Router vers le bon opérateur
|
||||||
|
this.logger.debug(
|
||||||
|
`[getting adaptator for ]: ${chargeDto.operator}_${chargeDto.country} `)
|
||||||
const adapter = this.operatorsService.getAdapter(
|
const adapter = this.operatorsService.getAdapter(
|
||||||
'user.operator.code',
|
chargeDto.operator,
|
||||||
user.country,
|
chargeDto.country,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.logger.debug(`Processing payment ${payment.id} through operator adapter ${adapter.constructor.name}`);
|
||||||
|
|
||||||
const chargeParams = {
|
const chargeParams = {
|
||||||
userToken: user.userToken,
|
userToken: chargeDto.userToken,
|
||||||
userAlias: user.userAlias,
|
userAlias: chargeDto.userToken,//todo make alias in contrat
|
||||||
amount: chargeDto.amount,
|
amount: chargeDto.amount,
|
||||||
currency: chargeDto.currency,
|
currency: chargeDto.currency,
|
||||||
description: chargeDto.description,
|
description: chargeDto.description,
|
||||||
reference: 'payment.reference,',//todo À remplacer par payment.reference
|
subscriptionId: chargeDto.subscriptionId,
|
||||||
|
reference: chargeDto.reference +'',//todo make reference in contrat
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await adapter.charge(chargeParams);
|
const result = await adapter.charge(chargeParams);
|
||||||
|
this.logger.debug(`result frm adaptaor ${result} for payment ${payment.id}`);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Mettre à jour le paiement
|
// Mettre à jour le paiement
|
||||||
const updatedPayment = await this.prisma.payment.update({
|
const updatedPayment = await this.prisma.payment.update({
|
||||||
@ -104,7 +114,7 @@ export class PaymentsService {
|
|||||||
return updatedPayment;
|
return updatedPayment;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// En cas d'erreur, marquer comme échoué
|
// En cas d'erreur, marquer comme échoué
|
||||||
await this.prisma.payment.update({
|
const resultFinal= await this.prisma.payment.update({
|
||||||
where: { id: payment.id },
|
where: { id: payment.id },
|
||||||
data: {
|
data: {
|
||||||
status: TransactionStatus.FAILED,
|
status: TransactionStatus.FAILED,
|
||||||
@ -112,7 +122,7 @@ export class PaymentsService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
throw error;
|
return { ...resultFinal };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
UseGuards,
|
UseGuards,
|
||||||
Request,
|
Request,
|
||||||
Logger,
|
Logger,
|
||||||
|
ParseIntPipe,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
import { SubscriptionsService } from './subscriptions.service';
|
import { SubscriptionsService } from './subscriptions.service';
|
||||||
@ -37,12 +38,24 @@ export class SubscriptionsController {
|
|||||||
return this.subscriptionsService.create(merchantId, dto);
|
return this.subscriptionsService.create(merchantId, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('/')
|
||||||
|
@ApiOperation({ summary: 'Get subscription list' })
|
||||||
|
async getAll(@Request() req) {
|
||||||
|
return this.subscriptionsService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('merchant/:merchantId')
|
||||||
|
@ApiOperation({ summary: 'Get subscription list by merchant' })
|
||||||
|
async getAllByMErchant(@Request() req, @Param('merchantId', ParseIntPipe) merchantId: number) {
|
||||||
|
return this.subscriptionsService.findAllByMerchant(merchantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@ApiOperation({ summary: 'Get subscription details' })
|
@ApiOperation({ summary: 'Get subscription details' })
|
||||||
async get(@Request() req, @Param('id') id: string) {
|
async get(@Request() req, @Param('id') id: number) {
|
||||||
return this.subscriptionsService.get(id, req.user.partnerId);
|
return this.subscriptionsService.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -56,9 +69,5 @@ export class SubscriptionsController {
|
|||||||
return this.subscriptionsService.cancel(id, req.user.partnerId, reason);
|
return this.subscriptionsService.cancel(id, req.user.partnerId, reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/invoices')
|
|
||||||
@ApiOperation({ summary: 'Get subscription invoices' })
|
|
||||||
async getInvoices(@Request() req, @Param('id') id: string) {
|
|
||||||
return this.subscriptionsService.getInvoices(id, req.user.partnerId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -18,7 +18,7 @@ import { PaymentsModule } from '../payments/payments.module';
|
|||||||
BullModule.registerQueue({
|
BullModule.registerQueue({
|
||||||
name: 'billing',
|
name: 'billing',
|
||||||
}),
|
}),
|
||||||
PaymentsModule,
|
// PaymentsModule,
|
||||||
],
|
],
|
||||||
controllers: [SubscriptionsController],
|
controllers: [SubscriptionsController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@ -4,23 +4,54 @@ import bull from 'bull';
|
|||||||
import { PrismaService } from '../../shared/services/prisma.service';
|
import { PrismaService } from '../../shared/services/prisma.service';
|
||||||
import { PaymentsService } from '../payments/payments.service';
|
import { PaymentsService } from '../payments/payments.service';
|
||||||
import { CreateSubscriptionDto, UpdateSubscriptionDto } from './dto/subscription.dto';
|
import { CreateSubscriptionDto, UpdateSubscriptionDto } from './dto/subscription.dto';
|
||||||
|
import { Subscription } from 'generated/prisma';
|
||||||
//import { SubscriptionStatus } from '@prisma/client';
|
//import { SubscriptionStatus } from '@prisma/client';
|
||||||
//import { SubscriptionStatus, Prisma } from '@prisma/client';
|
//import { SubscriptionStatus, Prisma } from '@prisma/client';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SubscriptionsService {
|
export class SubscriptionsService {
|
||||||
get(id: string, partnerId: any) {
|
async get(id: number):Promise<any> {
|
||||||
throw new Error('Method not implemented.');
|
const service = await this.prisma.subscription.findUnique({
|
||||||
|
where: { id },
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!service) {
|
||||||
|
throw new NotFoundException(`Service with ID ${id} not found`);
|
||||||
}
|
}
|
||||||
list(arg0: { partnerId: any; status: string | undefined; userId: string | undefined; page: number; limit: number; }) {
|
|
||||||
throw new Error('Method not implemented.');
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findAllByMerchant(merchantId: number): Promise<Subscription[]> {
|
||||||
|
// Check if merchant exists
|
||||||
|
|
||||||
|
return this.prisma.subscription.findMany({
|
||||||
|
where: { merchantPartnerId: merchantId },
|
||||||
|
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(): Promise<Subscription[]> {
|
||||||
|
// Check if merchant exists
|
||||||
|
|
||||||
|
return this.prisma.subscription.findMany({
|
||||||
|
// where: { merchantPartnerId: merchantId },
|
||||||
|
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getInvoices(id: string, partnerId: any) {
|
getInvoices(id: string, partnerId: any) {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly paymentsService: PaymentsService,
|
|
||||||
@InjectQueue('subscriptions') private subscriptionQueue: bull.Queue,
|
@InjectQueue('subscriptions') private subscriptionQueue: bull.Queue,
|
||||||
@InjectQueue('billing') private billingQueue: bull.Queue,
|
@InjectQueue('billing') private billingQueue: bull.Queue,
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user