Skip to content

Commit

Permalink
Feat: This allows users to fund their accounts
Browse files Browse the repository at this point in the history
  • Loading branch information
ezehlivinus committed Nov 29, 2022
1 parent 5585f03 commit 04194e8
Show file tree
Hide file tree
Showing 22 changed files with 866 additions and 42 deletions.
12 changes: 12 additions & 0 deletions migrations/20221119164830_transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,21 @@ export async function up(knex: Knex): Promise<void> {
table.enum('type', ['credit', 'debit']);
table.string('purpose', 255).nullable();
table.double('amount').notNullable().unsigned();
table
.double('walletBalance')
.nullable()
.unsigned()
.comment('This is this users wallet balance for this transaction');
table.timestamps(true, true, true);
table.string('from').references('address').inTable('wallets');
table.string('to').references('address').inTable('wallets');
table.string('reference', 50).notNullable();
table
.integer('customer')
.index()
.references('id')
.inTable('users')
.unsigned();
});
}

Expand Down
19 changes: 19 additions & 0 deletions migrations/20221126202514_payments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Knex } from 'knex';

export async function up(knex: Knex): Promise<void> {
return await knex.schema.createTable('payments', (table) => {
table.increments('id');
table.enum('type', ['fund', 'withdraw']);
table.string('reference', 255).nullable();
table.string('channel', 50).nullable();
table.double('amount').nullable().unsigned();
table.integer('transactionId').nullable().unsigned();
table.timestamps(true, true, true);
table.integer('customer').references('id').inTable('users').unsigned();
table.string('walletAddress').references('address').inTable('wallets');
});
}

export async function down(knex: Knex): Promise<void> {
return await knex.schema.dropTable('payments');
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
"@nestjs/passport": "^9.0.0",
"@nestjs/platform-express": "^9.0.0",
"@nestjs/swagger": "^6.1.3",
"@nestjsplus/redirect": "^1.0.0",
"axios": "^1.2.0",
"bcrypt": "^5.1.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
Expand All @@ -38,6 +40,7 @@
"mysql": "^2.18.1",
"mysql2": "^2.3.3",
"nest-knexjs": "^0.0.12",
"node-fetch": "^3.3.0",
"passport": "^0.6.0",
"passport-jwt": "^4.0.0",
"reflect-metadata": "^0.1.13",
Expand Down
7 changes: 5 additions & 2 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ import appConfig from './config/app.config';
import { AuthModule } from './auth/auth.module';
import { WalletsModule } from './wallets/wallets.module';
import { TransactionsModule } from './transactions/transactions.module';
import { PaymentsModule } from './payments/payments.module';
import jwtConfig from './config/jwt.config';
import databaseConfig from './config/database.config';
import paymentConfig from './config/payment.config';

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [appConfig, databaseConfig, jwtConfig]
load: [appConfig, databaseConfig, jwtConfig, paymentConfig]
}),

KnexModule.forRootAsync({
Expand All @@ -29,7 +31,8 @@ import databaseConfig from './config/database.config';
UsersModule,
AuthModule,
WalletsModule,
TransactionsModule
TransactionsModule,
PaymentsModule
],
controllers: [AppController],
providers: []
Expand Down
7 changes: 6 additions & 1 deletion src/auth/auth.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,15 @@ export class LoginDTO {
// data: LoginActionDTO;
// }

class _CreateUserResponseDTO extends CreateUserDto {
class _CreateUserResponseDTO {
@ApiProperty()
id: string;

@ApiProperty()
@IsEmail()
@IsNotEmpty()
email: string;

@ApiProperty()
wallet: string;

Expand Down
7 changes: 6 additions & 1 deletion src/config/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@ 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
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
}
}));
7 changes: 7 additions & 0 deletions src/config/payment.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { registerAs } from '@nestjs/config';

export default registerAs('payments', () => ({
email: process.env.PAYMENTS_EMAIL,
paystackSecretKey: process.env.PAYSTACK_SECRET_KEY,
paystackPublicKey: process.env.PAYSTACK_PUBLIC_KEY
}));
18 changes: 18 additions & 0 deletions src/payments/payments.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PaymentsController } from './payments.controller';

describe('PaymentsController', () => {
let controller: PaymentsController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [PaymentsController],
}).compile();

controller = module.get<PaymentsController>(PaymentsController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
172 changes: 172 additions & 0 deletions src/payments/payments.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import {
BadRequestException,
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Post,
Query,
Redirect,
Res
} from '@nestjs/common';

import {
ApiBadRequestResponse,
ApiOkResponse,
ApiOperation,
ApiTags
} from '@nestjs/swagger';
import * as express from 'express';
import { Auth } from 'src/common/decorators/http.decorator';
import { CurrentUser } from 'src/common/decorators/user.decorator';
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 { PaymentsService } from './payments.service';

@ApiTags('payments')
@Controller('payments')
export class PaymentsController {
constructor(
private paymentsService: PaymentsService,
private walletsService: WalletsService
) {}

@Post('/')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Endpoint for for funding wallet and withdraw from wallet'
})
@ApiOkResponse({
description: 'Registration is successful',
type: InitializePaymentDto
})
@ApiBadRequestResponse({
description: 'Credentials is invalid',
type: ErrorResponseDTO
})
@Auth([Roles.user])
async initialize(
@Body() initializePaymentDto: InitializePaymentDto,
@CurrentUser() auth: { id: number; email: string }
) {
const data = {
...initializePaymentDto,
user: {
id: auth.id,
email: auth.email
}
};

const wallet = await this.walletsService.findOne({
owner: auth.id,
address: data.walletAddress
});

if (!wallet) {
throw new BadRequestException('Wallet not found for this user');
}

const result = await this.paymentsService.initialize(data);

if (!result.status) {
throw new BadRequestException(result.message);
}

// redirection to the payment url did not work

return {
data: {
message:
'copy and paste the authorization_url in your browser to make your payment',
...result.data
}
};
}

@Get('/callback-url')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary:
'Endpoint for payments response as it relates with paystack callback url'
})
@ApiOkResponse({
description: ' success',
type: class Force {
data: any;
}
})
async callbackUrl(@Query() query) {
const result = await this.paymentsService.verify(query.reference);

const data = {
reference: result.reference,
transactionId: result.id,
channel: result.channel,
amount: result.amount
};

const payment = await this.paymentsService.findOne({
reference: data.reference
});

if (payment?.reference && payment.amount) {
return {
data: {
message:
'This payment was successfully and has been processed earlier',
payment
}
};
}

const updatePayment = await this.paymentsService.update(
{ reference: result.reference },
data
);

const updateWallet = await this.walletsService.fund(
{
owner: updatePayment.customer,
address: updatePayment.walletAddress
},
{
balance: result.amount / 100 // This is to convert back from to naira amount
}
);

return {
data: {
message: 'payment was success',
payment: updatePayment,
wallet: updateWallet
}
};
}

@Get('/')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Endpoint for listing user payments (funding and withrawal)'
})
@ApiOkResponse({
description: ' success',
type: [PaymentDto]
})
@Auth([Roles.user])
async myPayments(@CurrentUser() auth: Partial<UserFindDto>) {
const payments = await this.paymentsService.find({
customer: auth.id
});

if (!payments?.length) {
return {
message: 'No payments found'
};
}
return { data: payments };
}
}
69 changes: 69 additions & 0 deletions src/payments/payments.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsNumber, IsString } from 'class-validator';
import { TransactionTypes } from 'src/transactions/transactions.dto';

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

export class InitializePaymentDto {
@ApiProperty()
@IsString()
@IsNotEmpty()
walletAddress: string;

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

@ApiProperty({ enum: PaymentTypeDto, default: PaymentTypeDto.fund })
@IsString()
@IsNotEmpty()
type: PaymentTypeDto;
}

export class PaymentDto extends InitializePaymentDto {
@ApiProperty()
@IsString()
reference: string;

@ApiProperty()
@IsString()
walletAddress: string;

@ApiProperty()
@IsNumber()
customer: number;

@ApiProperty()
@IsNumber()
id?: number;

@ApiProperty()
@IsString()
channel: string;

@ApiProperty()
@IsString()
transactionId: string;
}

export class PaymentFindingDto {
@ApiProperty()
@IsNumber()
id?: number;

@ApiProperty()
@IsString()
reference?: string;

@ApiProperty()
@IsNumber()
customer?: number;

@ApiProperty()
@IsString()
walletAddress?: string;
}
12 changes: 12 additions & 0 deletions src/payments/payments.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { PaymentsService } from './payments.service';
import { PaymentsController } from './payments.controller';
import { WalletsModule } from 'src/wallets/wallets.module';

@Module({
imports: [WalletsModule],
providers: [PaymentsService],
controllers: [PaymentsController],
exports: [PaymentsService]
})
export class PaymentsModule {}
Loading

0 comments on commit 04194e8

Please sign in to comment.