Skip to content

Commit 0255b5c

Browse files
Merge pull request #645 from lishmanTech/payment
feat(payments): implement PaymentsModule with payment ledger and webhook support
2 parents e63400e + 56b25aa commit 0255b5c

8 files changed

Lines changed: 236 additions & 0 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { PaymentProvider } from '../enums/payment-provider.enum';
2+
import { IsString, IsNumber, IsEnum, IsOptional } from 'class-validator';
3+
4+
export class CreatePaymentDto {
5+
@IsString()
6+
orderId: string;
7+
8+
@IsString()
9+
userId: string;
10+
11+
@IsNumber()
12+
amount: number;
13+
14+
@IsString()
15+
currency: string;
16+
17+
@IsEnum(PaymentProvider)
18+
provider: PaymentProvider;
19+
20+
@IsOptional()
21+
@IsString()
22+
providerReference?: string;
23+
24+
@IsOptional()
25+
metadata?: Record<string, any>;
26+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { IsString } from 'class-validator';
2+
3+
export class RefundPaymentDto {
4+
@IsString()
5+
reason: string;
6+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import {
2+
Entity,
3+
PrimaryGeneratedColumn,
4+
Column,
5+
CreateDateColumn,
6+
} from 'typeorm';
7+
import { PaymentStatus } from '../enums/payment-status.enum';
8+
import { PaymentProvider } from '../enums/payment-provider.enum';
9+
10+
@Entity()
11+
export class Payment {
12+
@PrimaryGeneratedColumn('uuid')
13+
id: string;
14+
15+
@Column()
16+
orderId: string;
17+
18+
@Column()
19+
userId: string;
20+
21+
@Column('decimal')
22+
amount: number;
23+
24+
@Column()
25+
currency: string;
26+
27+
@Column({
28+
type: 'enum',
29+
enum: PaymentStatus,
30+
default: PaymentStatus.PENDING,
31+
})
32+
status: PaymentStatus;
33+
34+
@Column({
35+
type: 'enum',
36+
enum: PaymentProvider,
37+
})
38+
provider: PaymentProvider;
39+
40+
@Column({ nullable: true })
41+
providerReference: string;
42+
43+
@Column({ type: 'jsonb', nullable: true })
44+
metadata: Record<string, any>;
45+
46+
@CreateDateColumn()
47+
createdAt: Date;
48+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export enum PaymentProvider {
2+
STRIPE = 'stripe',
3+
PAYSTACK = 'paystack',
4+
MANUAL = 'manual',
5+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export enum PaymentStatus {
2+
PENDING = 'pending',
3+
COMPLETED = 'completed',
4+
FAILED = 'failed',
5+
REFUNDED = 'refunded',
6+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {
2+
Controller,
3+
Post,
4+
Get,
5+
Param,
6+
Body,
7+
Query,
8+
Req,
9+
} from '@nestjs/common';
10+
11+
import { PaymentsService } from './payments.service';
12+
import { CreatePaymentDto } from './dto/create-payment.dto';
13+
import { RefundPaymentDto } from './dto/refund-payment.dto';
14+
15+
@Controller('payments')
16+
export class PaymentsController {
17+
constructor(private readonly paymentsService: PaymentsService) {}
18+
19+
@Post()
20+
create(@Body() dto: CreatePaymentDto) {
21+
return this.paymentsService.createPayment(dto);
22+
}
23+
24+
@Get()
25+
getPayments(
26+
@Query('page') page = 1,
27+
@Query('limit') limit = 10,
28+
) {
29+
return this.paymentsService.getPayments(Number(page), Number(limit));
30+
}
31+
32+
@Get(':id')
33+
getPayment(@Param('id') id: string) {
34+
return this.paymentsService.getPaymentById(id);
35+
}
36+
37+
@Get('order/:orderId')
38+
getPaymentsByOrder(@Param('orderId') orderId: string) {
39+
return this.paymentsService.getPaymentsByOrder(orderId);
40+
}
41+
42+
@Post(':id/refund')
43+
refundPayment(
44+
@Param('id') id: string,
45+
@Body() dto: RefundPaymentDto,
46+
) {
47+
return this.paymentsService.refundPayment(id, dto);
48+
}
49+
50+
@Post('webhook')
51+
handleWebhook(@Req() req: any) {
52+
return this.paymentsService.handleWebhook(req.body);
53+
}
54+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Module } from '@nestjs/common';
2+
import { TypeOrmModule } from '@nestjs/typeorm';
3+
4+
import { PaymentsController } from './payments.controller';
5+
import { PaymentsService } from './payments.service';
6+
import { Payment } from './entities/payment.entity';
7+
8+
@Module({
9+
imports: [TypeOrmModule.forFeature([Payment])],
10+
controllers: [PaymentsController],
11+
providers: [PaymentsService],
12+
exports: [PaymentsService],
13+
})
14+
export class PaymentsModule {}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { Injectable, NotFoundException } from '@nestjs/common';
2+
import { InjectRepository } from '@nestjs/typeorm';
3+
import { Repository } from 'typeorm';
4+
5+
import { Payment } from './entities/payment.entity';
6+
import { CreatePaymentDto } from './dto/create-payment.dto';
7+
import { RefundPaymentDto } from './dto/refund-payment.dto';
8+
import { PaymentStatus } from './enums/payment-status.enum';
9+
10+
@Injectable()
11+
export class PaymentsService {
12+
constructor(
13+
@InjectRepository(Payment)
14+
private paymentRepo: Repository<Payment>,
15+
) {}
16+
17+
async createPayment(dto: CreatePaymentDto): Promise<Payment> {
18+
const payment = this.paymentRepo.create(dto);
19+
return this.paymentRepo.save(payment);
20+
}
21+
22+
async getPayments(page = 1, limit = 10) {
23+
const [data, total] = await this.paymentRepo.findAndCount({
24+
skip: (page - 1) * limit,
25+
take: limit,
26+
order: { createdAt: 'DESC' },
27+
});
28+
29+
return {
30+
data,
31+
total,
32+
page,
33+
limit,
34+
};
35+
}
36+
37+
async getPaymentById(id: string): Promise<Payment> {
38+
const payment = await this.paymentRepo.findOne({ where: { id } });
39+
40+
if (!payment) {
41+
throw new NotFoundException('Payment not found');
42+
}
43+
44+
return payment;
45+
}
46+
47+
async getPaymentsByOrder(orderId: string): Promise<Payment[]> {
48+
return this.paymentRepo.find({
49+
where: { orderId },
50+
order: { createdAt: 'DESC' },
51+
});
52+
}
53+
54+
async refundPayment(id: string, dto: RefundPaymentDto): Promise<Payment> {
55+
const payment = await this.getPaymentById(id);
56+
57+
payment.status = PaymentStatus.REFUNDED;
58+
59+
payment.metadata = {
60+
...(payment.metadata || {}),
61+
refundReason: dto.reason,
62+
refundedAt: new Date(),
63+
};
64+
65+
return this.paymentRepo.save(payment);
66+
}
67+
68+
async handleWebhook(payload: any) {
69+
// provider-agnostic webhook handling
70+
// store event metadata for tracking
71+
72+
return {
73+
received: true,
74+
payload,
75+
};
76+
}
77+
}

0 commit comments

Comments
 (0)