Skip to content

Commit

Permalink
This adds, funding, withdrawl features
Browse files Browse the repository at this point in the history
  • Loading branch information
ezehlivinus committed Nov 30, 2022
1 parent fcc09de commit d6d7371
Show file tree
Hide file tree
Showing 6 changed files with 549 additions and 28 deletions.
36 changes: 36 additions & 0 deletions migrations/20221126202514_payment-methods.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Knex } from 'knex';

export async function up(knex: Knex): Promise<void> {
return await knex.schema.createTable('paymentMethods', (table) => {
table.increments('id');
table.string('authorizationCode', 50).nullable();
table.json('card').nullable(); /*
This must store the card's
- Last4: '0456'
- exp_month: '12'
- exp_year: '2050'
- type: 'visa'
- brand: 'visa'
- account_name
- customer_code: 'CUS_i5yosncbl8h2kvc'
*/
table.string('recipientCode', 255).nullable();
table.json('bank').nullable(); /*
This must store the customer's bank's details
{
"authorization_code": null,
"account_number": "0001234567",
"account_name": null,
"bank_code": "058",
"bank_name": "Guaranty Trust Bank"
}
*/
table.integer('customer').references('id').inTable('users').unsigned();
table.timestamps(true, true, true);
});
}

export async function down(knex: Knex): Promise<void> {
return await knex.schema.dropTable('paymentMethods');
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,24 @@ export async function up(knex: Knex): Promise<void> {
table.increments('id');
table.enum('type', ['fund', 'withdraw']);
table.string('reference', 255).nullable();
table.string('transfer_code', 255).nullable();
table.string('transferCode', 255).nullable();
table.string('recipientCode', 50).nullable();
table.string('channel', 50).nullable();
table.double('amount').nullable().unsigned();
table.integer('transactionId').nullable().unsigned();
table.timestamps(true, true, true);

table
.integer('paymentMethod')
.references('id')
.inTable('paymentMethods')
.nullable()
.unsigned()
.comment('This payment method like fund or withdraw to/from wallets');

table.integer('customer').references('id').inTable('users').unsigned();
table.string('walletAddress').references('address').inTable('wallets');

table.timestamps(true, true, true);
});
}

Expand Down
28 changes: 18 additions & 10 deletions src/config/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { registerAs } from '@nestjs/config';

export default registerAs('app', () => ({
envName: process.env.NODE_ENV,
apiPrefix: 'api',
port: parseInt(process.env.APP_PORT || process.env.PORT, 10) || 3000,
payments: {
email: process.env.PAYMENTS_EMAIL,
paystackSecretKey: process.env.PAYSTACK_SECRET_KEY,
paystackPublicKey: process.env.PAYSTACK_PUBLIC_KEY
}
}));
export default registerAs('app', () => {
const PORT = parseInt(process.env.APP_PORT || process.env.PORT, 10) || 3000;

return {
envName: process.env.NODE_ENV,
apiPrefix: 'api',
port: PORT,
appURL:
process.env.NODE_ENV === 'production'
? process.env.APP_URL + '/api'
: `http://localhost:${PORT}/api`,
payments: {
email: process.env.PAYMENTS_EMAIL,
paystackSecretKey: process.env.PAYSTACK_SECRET_KEY,
paystackPublicKey: process.env.PAYSTACK_PUBLIC_KEY
}
};
});
145 changes: 144 additions & 1 deletion src/payments/payments.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Post,
Query,
Redirect,
Req,
Res
} from '@nestjs/common';

Expand All @@ -24,7 +25,11 @@ import { ErrorResponseDTO } from 'src/common/dtos/response.dto';
import { CreateTransactionDto } from 'src/transactions/transactions.dto';
import { Roles, UserFindDto } from 'src/users/users.dto';
import { WalletsService } from 'src/wallets/wallets.service';
import { InitializePaymentDto, PaymentDto } from './payments.dto';
import {
InitializePaymentDto,
MakeWithdrawalDto,
PaymentDto
} from './payments.dto';
import { PaymentsService } from './payments.service';

@ApiTags('payments')
Expand Down Expand Up @@ -188,4 +193,142 @@ export class PaymentsController {
@Body() initializePaymentDto: InitializePaymentDto,
@CurrentUser() auth: { id: number; email: string }
) {}

@Post('/webhook')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Endpoint for verifying customers transfer',
description: 'Only consumable by paystack'
})
@ApiOkResponse({
description: 'Transfer was successful'
// type: InitializePaymentDto
})
@ApiBadRequestResponse({
description: 'Credentials is invalid',
type: ErrorResponseDTO
})
async verifyTransfer(
@Req() req: express.Request,
@Res() res: express.Response
) {
const data = await this.paymentsService.validateWebhookEvent(req);

return res.send(200);
}

@Post('/withdraw')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Endpoint that let customers to withdraw',
description:
'This for customers who want to withdraw money from their wallet'
})
@ApiOkResponse({
description: 'With was successful'
// type: InitializePaymentDto
})
@ApiBadRequestResponse({
description: 'Credentials is invalid',
type: ErrorResponseDTO
})
@Auth([Roles.admin, Roles.user])
async withdraw(
@Body() body: MakeWithdrawalDto,
@CurrentUser() auth: UserFindDto
) {
const wallet = await this.walletsService.findOne({
owner: auth.id,
address: body.walletAddress
});

if (!wallet) {
return {
message: 'Wallet not found'
};
}
const newBalance = wallet.balance - body.amount;
if (newBalance < 0) {
throw new BadRequestException('insufficient balance');
}

// this is not efficient
const update = await this.walletsService.update(
{ address: wallet.address },
{
balance: newBalance
}
);

// verify customer account details
const bankDetails = await this.paymentsService.resolveAccountNumber(
body.accountNumber,
body.bankCode
);

// create transfer recipient using bank account
const newTransferRecipient =
await this.paymentsService.createTransferRecipient({
name: bankDetails.account_name,
accountNumber: bankDetails.account_number,
bankCode: body.bankCode,
amount: body.amount,
currency: 'NGN'
});
// I may save to database
let paymentMethod = await this.paymentsService.findOnePaymentMethod({
recipientCode: newTransferRecipient.recipient_code
});

// let newPaymentMethod;
if (!paymentMethod) {
await this.paymentsService.makeNewPaymentMethod({
recipientCode: newTransferRecipient.recipient_code,
bank: JSON.stringify(newTransferRecipient.bankDetails),
customer: auth.id
});
}

paymentMethod = await this.paymentsService.findOnePaymentMethod({
recipientCode: newTransferRecipient.recipient_code
});

// initiate transfer
const transfer = await this.paymentsService.initiateTransfer({
recipient: newTransferRecipient.recipient_code,
amount: body.amount,
userId: auth.id,
walletAddress: body.walletAddress,
paymentMethod: paymentMethod.id
});

return {
data: transfer
};

// verify transfer via webhook
}

@Post('/list-banks')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'List banks to find bank code',
description: 'List banks'
})
@ApiOkResponse({
description: 'With was successful'
// type: InitializePaymentDto
})
@ApiBadRequestResponse({
description: 'Credentials is invalid',
type: ErrorResponseDTO
})
@Auth([Roles.admin, Roles.user])
async listBanks() {
const banks = await this.paymentsService.listBanks();

return {
data: banks
};
}
}
97 changes: 92 additions & 5 deletions src/payments/payments.dto.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,97 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsNumber, IsString } from 'class-validator';
import {
IsEmail,
IsNotEmpty,
IsNumber,
IsOptional,
IsPositive,
IsString
} from 'class-validator';
import { TransactionTypes } from 'src/transactions/transactions.dto';

export enum PaymentTypeDto {
fund = 'fund',
withdraw = 'withdraw'
}

export class ResolveAccountNumberDto {
@ApiProperty({
type: String,
description: 'Account number',
example: '0001234567'
})
@IsString()
@IsNotEmpty()
accountNumber: string;

@ApiProperty()
@IsNotEmpty()
@IsNumber()
bankCode: number;
}

export class MakeWithdrawalDto {
@ApiProperty({
example: '044'
})
@IsString()
bankCode: string;

@ApiProperty({
type: String,
description: 'Account number',
example: '0001234567'
})
@IsString()
@IsNotEmpty()
accountNumber: string;

@ApiProperty()
@IsNumber()
@IsNotEmpty()
amount: number;

@ApiProperty({
description: 'wallet address to transfer money to'
})
@IsString()
@IsNotEmpty()
walletAddress: string;

}

export class CreateTransferRecipientDto {
@ApiProperty()
@IsString()
@IsNotEmpty()
name: string;

@ApiProperty()
@IsString()
@IsOptional()
currency? = 'NGN';

@ApiProperty({
example: '044'
})
@IsString()
bankCode: string;

@ApiProperty({
type: String,
description: 'Account number',
example: '0001234567'
})
@IsString()
@IsNotEmpty()
accountNumber: string;

@ApiProperty()
@IsNumber()
@IsNotEmpty()
amount?: number;
}

export class InitializePaymentDto {
@ApiProperty()
@IsString()
Expand All @@ -30,10 +115,10 @@ export class InitiateTransferDto {
@IsNumber()
amount: number;

@ApiProperty()
@IsString()
@IsNotEmpty()
walletAddress: string;
// @ApiProperty()
// @IsString()
// @IsNotEmpty()
// walletAddress: string;
}

export class PaymentDto extends InitializePaymentDto {
Expand Down Expand Up @@ -79,3 +164,5 @@ export class PaymentFindingDto {
@IsString()
walletAddress?: string;
}


Loading

0 comments on commit d6d7371

Please sign in to comment.