Skip to content

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

POST /payments/verify
Content-Type: application/json

{
  "reference": "ref_xyz789"
}

List Payments

GET /payments?status=received&page=1&limit=20
Authorization: Bearer <access_token>

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