NestJS integration
Keep NestJS infrastructure and settlement logic outside the SDK.
NestJS owns the HTTP and application boundaries.
@payments-sdk/payments remains a plain configured object that
performs provider operations. Exporting that object from a small
module keeps the provider configuration outside Nest's
dependency-injection lifecycle while services and controllers can
import it.
Configure once
// payments.ts
import { esewa } from '@payments-sdk/esewa';
import { fonepay } from '@payments-sdk/fonepay';
import { createPayments } from '@payments-sdk/payments';
export const payments = createPayments({
providers: [
fonepay({ merchantCode, secretKey, username, password }),
esewa({
productCode,
secretKey: esewaSecretKey,
environment: 'sandbox',
}),
],
});The values above should come from Nest's configuration system or a secret manager in a real application. Keep this module server-only.
Application service
The service owns persistence, idempotency, and settlement. The SDK
does not provide createPayment() or settlePayment(); those are
application business operations around the SDK:
// payment.service.ts
import { Injectable } from '@nestjs/common';
import { payments } from './payments.js';
@Injectable()
export class PaymentService {
async createPayment(orderId: string) {
const order = await this.orders.findForCheckout(orderId);
const result = await payments.checkout({
provider: order.provider,
amount: order.amountMinor,
currency: order.currency,
reference: order.paymentReference,
providerOptions: order.providerOptions,
});
await this.payments.saveAction(order.paymentReference, result);
return result;
}
async settlePayment(reference: string) {
const payment = await this.payments.findByReference(reference);
if (!payment || payment.settledAt) return payment;
const verified = await payments.verify({
provider: payment.provider,
reference,
providerOptions: payment.verifyOptions,
});
if (verified.status !== 'succeeded') return verified;
// Use a transaction or an equivalent idempotency boundary.
return this.payments.markSettledAndFulfill(payment.id, verified);
}
}markSettledAndFulfill() must be safe when called more than once. A
provider return, a retry, and a reconciliation job may all attempt
settlement for the same payment.
Controller routes
Controllers translate HTTP requests into calls to the service. The SDK does not register these routes:
// payment.controller.ts
import { Controller, Get, Param, Post, Req } from '@nestjs/common';
import type { Request } from 'express';
@Controller('payments')
export class PaymentController {
constructor(private readonly service: PaymentService) {}
@Post(':orderId/checkout')
createCheckout(@Param('orderId') orderId: string) {
return this.service.createPayment(orderId);
}
@Get('returns/esewa')
async esewaReturn(@Req() request: Request) {
const returned = await payments.returns.esewa(request);
return this.service.settlePayment(returned.reference!);
}
}When using Express, pass the request shape that preserves the raw URL
and body needed by the provider. A Web Request or the normalized raw
request shape is also supported by the SDK.
Webhook route
Webhook support is capability-driven. Only add a typed
payments.webhooks.<provider> call for a configured provider that
implements a webhook parser:
@Post('webhooks/provider-name')
async providerWebhook(@Req() request: Request) {
const event = await payments.webhooks.providerName(request);
return this.service.settlePayment(event.reference!);
}The initial eSewa, FonePay, and Khalti packages expose returns where
applicable, but do not expose webhooks. A future webhook-capable
provider adds that namespace through its own package without changing
@payments-sdk/payments.
Do not treat a browser return and a webhook as the same event. Returns are browser-facing inbound requests; webhooks are server-to-server events. Both should be verified and settled idempotently according to the provider contract.