Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/modules/gateway/api/output.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TransactionStatusEnum } from '../../../shared/validations/transaction/status';

export interface TransactionGatewayOutput {
transactionId: number;
transactionStatus: TransactionStatusEnum;
}

export interface GatewayOutput {
status: number;
statusText: string;
data: TransactionGatewayOutput;
}

export interface GatewayResult {
result: GatewayOutput;
}
Empty file.
73 changes: 73 additions & 0 deletions src/modules/gateway/domain/gateway.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { HttpService } from '@nestjs/axios';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AxiosResponse } from 'axios';
import { firstValueFrom } from 'rxjs';
import { jsonStringifyReplacer } from 'src/shared/utils/json.utils';
import { WebhookPayload } from '../../../modules/transaction/app/input';
import { EnvVariables } from '../../../shared/config/envEnums';
import { AppLoggerService } from '../../../shared/logger/app-logger.service';
import { GatewayOutput, GatewayResult } from '../api/output';

@Injectable()
export class GatewayService {
private readonly gatewayUrl: string;
private readonly webhook: string;
private get logPrefix(): string {
return `[${this.constructor.name}]`;
}
constructor(
private readonly httpService: HttpService,
private readonly configService: ConfigService,
private readonly logger: AppLoggerService,
) {
this.logger.debug(this.logPrefix, 'GatewayService constructor');
this.gatewayUrl = this.setGatewayUrl();
this.webhook = this.setWebhook();
}

public async getStatus(payload: WebhookPayload): Promise<GatewayOutput> {
const response: AxiosResponse<GatewayResult> = await firstValueFrom(
this.httpService.post(this.gatewayUrl, payload, {
headers: {
webhook: this.webhook,
},
}),
);
this.logger.debug(
this.logPrefix,
`Received response - Status: ${response.status}, Data: ${JSON.stringify(response.data, jsonStringifyReplacer)}`,
);
return {
status: response.status,
statusText: response.statusText,
data: {
transactionId: response.data.result.data.transactionId,
transactionStatus: response.data.result.data.transactionStatus,
},
};
}

private setGatewayUrl(): string {
const gatewayUrl = this.configService.get<string>(EnvVariables.GATEWAY_URL);
if (!gatewayUrl) {
throw new Error(
`${EnvVariables.GATEWAY_URL} not visible in .env file. Please update and restart the app`,
);
}
return gatewayUrl;
}

private setWebhook(): string {
const webhookUrl = this.configService.get<string>(
EnvVariables.INTERNAL_WEBHOOk_URL,
);
if (!webhookUrl) {
throw new Error(
`${EnvVariables.INTERNAL_WEBHOOk_URL} not visible in .env file. Please update and restart the app`,
);
}
this.logger.debug(this.logPrefix, `Using webhook URL: ${webhookUrl}`);
return webhookUrl;
}
}
Empty file.
11 changes: 11 additions & 0 deletions src/modules/gateway/gateway.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { LoggerModule } from '../../shared/logger/logger.module';
import { GatewayService } from './domain/gateway.service';

@Module({
imports: [HttpModule, LoggerModule],
providers: [GatewayService],
exports: [GatewayService],
})
export class GatewayModule {}
2 changes: 2 additions & 0 deletions src/modules/health/health.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Module } from '@nestjs/common';
import { AppLoggerService } from 'src/shared/logger/app-logger.service';
import { LoggerModule } from 'src/shared/logger/logger.module';
import { HealthController } from './interfaces/controllers/health.controller';

@Module({
imports: [LoggerModule],
providers: [AppLoggerService],
controllers: [HealthController],
})
Expand Down
55 changes: 46 additions & 9 deletions src/modules/transaction/api/transaction.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
Post,
ValidationPipe,
} from '@nestjs/common';
import { GatewayOutput } from 'src/modules/gateway/api/output';
import { TransactionWebhookDto } from 'src/shared/dto/transaction-webhook-payload.dto';
import { FundsRepresentation } from '../../../modules/wallet/api/representation';
import { CreateTransactionDto } from '../../../shared/dto/create-transaction.dto';
import { AppLoggerService } from '../../../shared/logger/app-logger.service';
Expand All @@ -21,12 +23,10 @@ import { CancelTransactionUseCase } from '../app/cancel-transaction-use-case/can
import { CompleteTransactionUseCase } from '../app/complete-transaction-use-case/complete-transaction.use-case';
import { CreateTransactionUseCase } from '../app/create-transaction-use-case/create-transaction.use-case';
import { CreateTransactionInput } from '../app/input';
import { UpdateTransactionFromWebhookUseCase } from '../app/update-transaction-from-webhook-use-case/update-transaction-from-webhook.use-case';
import { TransactionService } from '../domain/services/transaction.service';
import { Transaction } from '../domain/transaction.entity';
import {
OutputRepresentation,
TransactionRepresentation,
} from './representation';
import { TransactionRepresentation } from './representation';
import { TransactionRepresentationMapper } from './representationMapper';

@Controller(ApiRoutes.TRANSACTION)
Expand All @@ -41,6 +41,7 @@ export class TransactionController {
private readonly completeTransactionUseCase: CompleteTransactionUseCase,
private readonly transactionRepresentationMapper: TransactionRepresentationMapper,
private readonly cancelTransactionUseCase: CancelTransactionUseCase,
private readonly updateTransactionFromWebhookUseCase: UpdateTransactionFromWebhookUseCase,
) {}

@Post()
Expand Down Expand Up @@ -101,19 +102,17 @@ export class TransactionController {
};
}

@Patch(TransactionRoutes.COMPLETE)
@Post(TransactionRoutes.COMPLETE)
@HttpCode(HttpStatus.OK)
async completeTransactions(
@Param('walletId', ParseIntPipe) walletId: number,
): Promise<{ elements: number; data: OutputRepresentation[] }> {
): Promise<{ elements: number; data: GatewayOutput[] }> {
this.logger.log(
this.logPrefix,
`Completing all transactions for wallet: ${walletId}`,
);
const results = await this.completeTransactionUseCase.run(walletId);
const representation =
this.transactionRepresentationMapper.getOutput(results);
return { elements: representation.length, data: representation };
return { elements: results.length, data: results };
}

@Patch(TransactionRoutes.CANCEL)
Expand All @@ -124,4 +123,42 @@ export class TransactionController {
const result = await this.cancelTransactionUseCase.run(transactionId);
return result;
}

@Post(TransactionRoutes.WEBHOOK)
@HttpCode(HttpStatus.OK)
async webhook(
@Body(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
)
input: TransactionWebhookDto,
): Promise<void> {
try {
this.logger.log(
this.logPrefix,
`Webhook called with input: ${JSON.stringify(input, jsonStringifyReplacer)}`,
);
const transaction =
await this.updateTransactionFromWebhookUseCase.run(input);
this.logger.log(
this.logPrefix,
`Processes transaction: ${JSON.stringify(transaction, jsonStringifyReplacer)}`,
);
} catch (error) {
this.logger.error(
this.logPrefix,
`Error processing webhook: ${error instanceof Error ? error.message : String(error)}`,
);
if (error instanceof Error && error.stack) {
this.logger.error(this.logPrefix, error.stack);
}
throw error;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Injectable } from '@nestjs/common';
import { WalletService } from 'src/modules/wallet/app/services/app-wallet.service';
import { TransactionStatusEnum } from 'src/shared/validations/transaction/status';
import { BadGatewayException, Injectable } from '@nestjs/common';
import { AppLoggerService } from '../../../../shared/logger/app-logger.service';
import { TransactionStatusEnum } from '../../../../shared/validations/transaction/status';
import { GatewayOutput } from '../../../gateway/api/output';
import { GatewayService } from '../../../gateway/domain/gateway.service';
import { WalletService } from '../../../wallet/app/services/app-wallet.service';
import { TransactionService } from '../../domain/services/transaction.service';
import { Transaction } from '../../domain/transaction.entity';
import { TransactionOutput } from './output';
import { WebhookPayload } from '../input';

@Injectable()
export class CompleteTransactionUseCase {
Expand All @@ -15,17 +17,18 @@ export class CompleteTransactionUseCase {
private readonly logger: AppLoggerService,
private readonly transactionService: TransactionService,
private readonly walletService: WalletService,
private readonly gatewayService: GatewayService,
) {}

public async run(
walletId: number,
transactionStatus?: TransactionStatusEnum,
): Promise<TransactionOutput[]> {
): Promise<GatewayOutput[]> {
this.logger.log(
this.logPrefix,
`Updating ${TransactionStatusEnum.PENDING} transactions for wallet: ${walletId}`,
);
const results: TransactionOutput[] = [];
const results: GatewayOutput[] = [];
const transactions: Transaction[] =
await this.transactionService.getAllTransactionsByWallet(
walletId,
Expand All @@ -34,36 +37,83 @@ export class CompleteTransactionUseCase {

for (const transaction of transactions) {
if (transaction.status === TransactionStatusEnum.PENDING) {
const transactionResult = await this.updateTransaction(transaction);
const transactionResult = await this.callGateway(transaction);
results.push(transactionResult);
}
}
return results;
}

private async updateTransaction(
transaction: Transaction,
): Promise<TransactionOutput> {
/**
* The function `callGateway` processes a transaction by checking the wallet balance, determining the
* transaction status, and interacting with a gateway service.
* @param {Transaction} transaction - The `transaction` parameter in the `callGateway` function
* represents an object containing information about a financial transaction. It likely includes
* properties such as `id`, `walletId`, `amount`, and other relevant details needed to process the
* transaction. This function is responsible for interacting with a gateway service to process
* @returns The function `callGateway` returns a `Promise` that resolves to a `GatewayOutput` object.
*/
private async callGateway(transaction: Transaction): Promise<GatewayOutput> {
const wallet = await this.walletService.getByWalletId(transaction.walletId);
const newBalance = wallet.balance + transaction.amount;
const status: TransactionStatusEnum =
newBalance < 0
? TransactionStatusEnum.FAILED
: TransactionStatusEnum.COMPLETED;
: TransactionStatusEnum.PENDING;

if (status === TransactionStatusEnum.FAILED) {
return this.updateFailedTransaction(transaction, newBalance);
}

const gatewayPayload = this.buildWebhookPayload(transaction, status);
const gatewayResponse = await this.gatewayService.getStatus(gatewayPayload);
if (gatewayResponse.status !== 200) {
const err = `Gateway response status: ${gatewayResponse.status} for transaction: ${transaction.id}`;
this.logger.error(this.logPrefix, err);
throw new BadGatewayException(err);
}
return gatewayResponse;
}

private buildWebhookPayload(
transaction: Transaction,
status: TransactionStatusEnum,
): WebhookPayload {
const payload: WebhookPayload = {
id: transaction.id,
status,
amount: Number(transaction.amount),
currency: transaction.currentCurrency,
originCreatedAt: transaction.clientTransactionDate
? transaction.clientTransactionDate
: new Date(),
};
return payload;
}

private async updateFailedTransaction(
transaction: Transaction,
newBalance: bigint,
): Promise<GatewayOutput> {
this.logger.warn(
this.logPrefix,
`Transaction: ${transaction.id} failed to complete. New balance: ${Number(newBalance)} is less than 0.`,
);
const updatedTransaction = await this.transactionService.update({
transactionId: transaction.id,
status,
status: TransactionStatusEnum.FAILED,
});

if (updatedTransaction.status === TransactionStatusEnum.COMPLETED) {
const funds = await this.walletService.updateBalance(
wallet.tokenId,
transaction.amount,
transaction.currentCurrency,
);
return { transaction: updatedTransaction, funds };
}
return { transaction: updatedTransaction, funds: null };
this.logger.log(
this.logPrefix,
`Transaction: ${updatedTransaction.id} updated to status: ${updatedTransaction.status} successfully.`,
);
return {
status: 200,
statusText: 'OK',
data: {
transactionId: updatedTransaction.id,
transactionStatus: updatedTransaction.status,
},
};
}
}
8 changes: 8 additions & 0 deletions src/modules/transaction/app/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,11 @@ export interface TransactionInput {
clientTransactionDate?: Date;
idempotencyKey?: string;
}

export interface WebhookPayload {
id: number;
status: TransactionStatusEnum;
amount: number;
currency: CurrencyEnum;
originCreatedAt: Date;
}
Loading