Payment Processing
Payment processing integrates with Paystack payment gateway for collecting payments, verifying transactions, and handling payouts and refunds.
Overview
Key features: - Paystack integration for Nigeria/Africa - Payment initialization with authorization URLs - Payment verification after customer pays - Webhook handling for async notifications - Seller payouts after order completion - Refund processing for cancellations - Fee calculation (platform + gateway fees)
Payment Flow
1. Initiate Payment
Customer → Initiates payment for service
↓
App → Calls PaymentGateway.generatePaymentLink()
↓
Paystack → Returns authorization URL
↓
App → Redirects customer to Paystack
Implementation:
async function initiatePayment(
data: { serviceId: string; selectedOptions: string[] },
paymentGateway: PaymentGateway,
user: User
) {
// Calculate order amount
const orderAmount = service.basicPrice + sum(options.map(o => o.price));
// Add gateway fees
const totalAmount = orderAmount + (orderAmount * gatewayFeesRate);
// Generate payment link
const { authorizationCode, paymentLink, reference } =
await paymentGateway.generatePaymentLink(
totalAmount,
user.email,
'XOF' // or NGN
);
// Save payment event
await paymentEventRepository.add(
new PaymentInitiated(reference, {
serviceId,
optionIds,
orderAmount,
totalAmount,
userId: user.id
})
);
return { paymentLink, reference };
}
2. Customer Pays
Customer → Enters payment details on Paystack
↓
Paystack → Processes payment (card/bank transfer)
↓
Paystack → Redirects back to app with reference
3. Verify Payment
App → Receives callback with reference
↓
App → Calls PaymentGateway.verifyTransaction(reference)
↓
Paystack → Returns payment status
↓
App → Creates Payment entity (if success)
↓
App → Creates Order entity
Implementation:
async function verifyPayment(
reference: string,
paymentGateway: PaymentGateway
) {
// Verify with gateway
const result = await paymentGateway.verifyTransaction(reference);
if (result.status !== 'success') {
throw new PaymentNotSuccessful();
}
// Get payment initiation details
const initiatedEvent = await paymentEventRepository.findByReference(reference);
// Create Payment entity
const payment = Payment.create(
paymentRepository.nextId(),
new Decimal(result.amount),
initiatedEvent.metadata.orderAmount,
result.currency,
initiatedEvent.metadata.userId,
'received'
);
await paymentRepository.add(payment);
// Create Order
await createOrder({
paymentId: payment.id,
customerId: initiatedEvent.metadata.userId,
serviceId: initiatedEvent.metadata.serviceId,
selectedOptions: initiatedEvent.metadata.optionIds
});
return payment;
}
4. Webhook Handling
Paystack sends webhooks for payment events:
@Post('webhook/paystack')
async handleWebhook(
@Body() payload: any,
@Headers('x-paystack-signature') signature: string
) {
// 1. Verify signature
const isValid = this.verifySignature(payload, signature);
if (!isValid) throw new UnauthorizedException();
// 2. Handle event
switch (payload.event) {
case 'charge.success':
await this.handlePaymentSuccess(payload.data.reference);
break;
case 'transfer.success':
await this.handleTransferSuccess(payload.data.reference);
break;
case 'transfer.failed':
await this.handleTransferFailed(payload.data.reference);
break;
}
return { success: true };
}
Payouts (Seller Withdrawals)
Order completed → Payment released → Seller account credited
↓
Seller requests withdrawal
↓
Admin approves
↓
Transfer initiated via Paystack
↓
Transfer completed
Implementation:
async function processPayout(
orderId: string,
paymentGateway: PaymentGateway
) {
const order = await orderRepository.get(orderId);
const seller = await userRepository.get(order.sellerId);
// Calculate payout (minus commission)
const payoutAmount = order.totalAmount.minus(order.commissionAmount);
// Get seller bank account
const recipientCode = seller.bankAccount.recipientCode;
// Initiate transfer
const { transferCode, reference } = await paymentGateway.initiateTransfer(
recipientCode,
payoutAmount,
'XOF',
`Payout for order ${order.id}`
);
// Update payment status
await paymentRepository.updateStatus(order.paymentId, 'released');
return { transferCode, reference };
}
Refunds
Order rejected/cancelled → Refund initiated
↓
Paystack processes refund
↓
Payment status = 'refunded'
↓
Customer account credited
Implementation:
async function processRefund(
orderId: string,
paymentGateway: PaymentGateway
) {
const order = await orderRepository.get(orderId);
const payment = await paymentRepository.get(order.paymentId);
// Create refund entity
const refund = Refund.create(
refundRepository.nextId(),
order.customerId,
order.id,
payment.id,
payment.amount,
payment.currency,
`Order ${order.state}`
);
await refundRepository.add(refund);
// Process refund via gateway
await paymentGateway.refund(payment.gatewayReference, payment.amount);
// Update statuses
await refundRepository.updateStatus(refund.id, 'completed');
await paymentRepository.updateStatus(payment.id, 'refunded');
return refund;
}
API Endpoints
Initiate Payment
POST /payments/initiate
Authorization: Bearer <access_token>
Content-Type: application/json
{
"serviceId": "service-123",
"selectedOptions": ["opt-1", "opt-2"]
}
Response:
{
"orderAmount": 15000,
"totalAmount": 15300,
"authorizationCode": "abc123",
"paymentLink": "https://checkout.paystack.com/abc123",
"reference": "ref_xyz789"
}
Verify Payment
List Payments
Environment Variables
# Paystack
PAYSTACK_SECRET_KEY=sk_test_xxx
PAYSTACK_PUBLIC_KEY=pk_test_xxx
PAYSTACK_WEBHOOK_SECRET=whsec_xxx
# Fees
GATEWAY_FEE_RATE=0.02 # 2%
PLATFORM_COMMISSION=0.10 # 10%
Security Best Practices
✅ Do
- Verify webhook signatures
- Use HTTPS for all payment communications
- Store payment secrets in environment variables
- Log all payment transactions
- Verify payment amounts match expectations
- Implement idempotency (don't process same reference twice)
- Use test keys in development
❌ Don't
- Don't trust client-side payment confirmations
- Don't expose secret keys in frontend
- Don't skip webhook signature verification
- Don't process payments without verification
- Don't log sensitive payment data (card numbers)
- Don't hardcode payment amounts
Error Handling
try {
const payment = await verifyPayment(reference);
} catch (error) {
if (error instanceof PaymentNotSuccessful) {
// Payment failed or abandoned
return { status: 'failed', message: 'Payment not successful' };
} else if (error instanceof PaymentAlreadyProcessed) {
// Duplicate processing attempt
return { status: 'duplicate', message: 'Payment already processed' };
} else {
// Gateway error
logger.error(`Payment verification error: ${error}`);
throw new PaymentException('Payment verification failed');
}
}
Testing
Mock Payment Gateway
export class MockPaymentGateway extends PaymentGateway {
async generatePaymentLink(amount: Decimal, email: string, currency: string) {
return {
authorizationCode: 'mock_auth',
paymentLink: 'https://mock.pay/test',
reference: `mock_ref_${Date.now()}`
};
}
async verifyTransaction(reference: string) {
return {
status: 'success',
amount: 15000,
currency: 'XOF',
reference
};
}
}
Summary
Payment processing provides: - Paystack integration for payment collection - Payment verification to confirm transactions - Webhook handling for async updates - Seller payouts after order completion - Refund processing for cancellations - Security best practices built-in