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
26 changes: 26 additions & 0 deletions backend/src/payment/dto/create-payment.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { PaymentProvider } from '../enums/payment-provider.enum';
import { IsString, IsNumber, IsEnum, IsOptional } from 'class-validator';

export class CreatePaymentDto {
@IsString()
orderId: string;

@IsString()
userId: string;

@IsNumber()
amount: number;

@IsString()
currency: string;

@IsEnum(PaymentProvider)
provider: PaymentProvider;

@IsOptional()
@IsString()
providerReference?: string;

@IsOptional()
metadata?: Record<string, any>;
}
6 changes: 6 additions & 0 deletions backend/src/payment/dto/refund-payment.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { IsString } from 'class-validator';

export class RefundPaymentDto {
@IsString()
reason: string;
}
48 changes: 48 additions & 0 deletions backend/src/payment/entities/payment.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
} from 'typeorm';
import { PaymentStatus } from '../enums/payment-status.enum';
import { PaymentProvider } from '../enums/payment-provider.enum';

@Entity()
export class Payment {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
orderId: string;

@Column()
userId: string;

@Column('decimal')
amount: number;

@Column()
currency: string;

@Column({
type: 'enum',
enum: PaymentStatus,
default: PaymentStatus.PENDING,
})
status: PaymentStatus;

@Column({
type: 'enum',
enum: PaymentProvider,
})
provider: PaymentProvider;

@Column({ nullable: true })
providerReference: string;

@Column({ type: 'jsonb', nullable: true })
metadata: Record<string, any>;

@CreateDateColumn()
createdAt: Date;
}
5 changes: 5 additions & 0 deletions backend/src/payment/enums/payment-provider.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum PaymentProvider {
STRIPE = 'stripe',
PAYSTACK = 'paystack',
MANUAL = 'manual',
}
6 changes: 6 additions & 0 deletions backend/src/payment/enums/payment-status.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum PaymentStatus {
PENDING = 'pending',
COMPLETED = 'completed',
FAILED = 'failed',
REFUNDED = 'refunded',
}
54 changes: 54 additions & 0 deletions backend/src/payment/payments.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
Controller,
Post,
Get,
Param,
Body,
Query,
Req,
} from '@nestjs/common';

import { PaymentsService } from './payments.service';
import { CreatePaymentDto } from './dto/create-payment.dto';
import { RefundPaymentDto } from './dto/refund-payment.dto';

@Controller('payments')
export class PaymentsController {
constructor(private readonly paymentsService: PaymentsService) {}

@Post()
create(@Body() dto: CreatePaymentDto) {
return this.paymentsService.createPayment(dto);
}

@Get()
getPayments(
@Query('page') page = 1,
@Query('limit') limit = 10,
) {
return this.paymentsService.getPayments(Number(page), Number(limit));
}

@Get(':id')
getPayment(@Param('id') id: string) {
return this.paymentsService.getPaymentById(id);
}

@Get('order/:orderId')
getPaymentsByOrder(@Param('orderId') orderId: string) {
return this.paymentsService.getPaymentsByOrder(orderId);
}

@Post(':id/refund')
refundPayment(
@Param('id') id: string,
@Body() dto: RefundPaymentDto,
) {
return this.paymentsService.refundPayment(id, dto);
}

@Post('webhook')
handleWebhook(@Req() req: any) {
return this.paymentsService.handleWebhook(req.body);
}
}
14 changes: 14 additions & 0 deletions backend/src/payment/payments.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

import { PaymentsController } from './payments.controller';
import { PaymentsService } from './payments.service';
import { Payment } from './entities/payment.entity';

@Module({
imports: [TypeOrmModule.forFeature([Payment])],
controllers: [PaymentsController],
providers: [PaymentsService],
exports: [PaymentsService],
})
export class PaymentsModule {}
77 changes: 77 additions & 0 deletions backend/src/payment/payments.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

import { Payment } from './entities/payment.entity';
import { CreatePaymentDto } from './dto/create-payment.dto';
import { RefundPaymentDto } from './dto/refund-payment.dto';
import { PaymentStatus } from './enums/payment-status.enum';

@Injectable()
export class PaymentsService {
constructor(
@InjectRepository(Payment)
private paymentRepo: Repository<Payment>,
) {}

async createPayment(dto: CreatePaymentDto): Promise<Payment> {
const payment = this.paymentRepo.create(dto);
return this.paymentRepo.save(payment);
}

async getPayments(page = 1, limit = 10) {
const [data, total] = await this.paymentRepo.findAndCount({
skip: (page - 1) * limit,
take: limit,
order: { createdAt: 'DESC' },
});

return {
data,
total,
page,
limit,
};
}

async getPaymentById(id: string): Promise<Payment> {
const payment = await this.paymentRepo.findOne({ where: { id } });

if (!payment) {
throw new NotFoundException('Payment not found');
}

return payment;
}

async getPaymentsByOrder(orderId: string): Promise<Payment[]> {
return this.paymentRepo.find({
where: { orderId },
order: { createdAt: 'DESC' },
});
}

async refundPayment(id: string, dto: RefundPaymentDto): Promise<Payment> {
const payment = await this.getPaymentById(id);

payment.status = PaymentStatus.REFUNDED;

payment.metadata = {
...(payment.metadata || {}),
refundReason: dto.reason,
refundedAt: new Date(),
};

return this.paymentRepo.save(payment);
}

async handleWebhook(payload: any) {
// provider-agnostic webhook handling
// store event metadata for tracking

return {
received: true,
payload,
};
}
}
Loading