Vendease Payments Backend¶
Overview¶
The Vendease Payments Backend is a NestJS service that owns the payments
domain for Vendease. The README.md describes it briefly as the Vendease
Payments Service, and the package.json confirms the package name
vendease-payment-service at version 1.0.1. In practice the codebase
covers the full surface area of a payments platform: collecting funds via
multiple gateways and payment methods, holding wallet and virtual-account
balances, processing payouts and settlements, running KYC, capturing audit
trails, and exposing an admin console for the operations team.
The service hosts both a REST API (under /api/v1) and a NATS JetStream
microservice consumer in the same Nest process. Two databases back it: a
primary SQL store (Postgres via Sequelize) for the transactional records,
and a secondary MongoDB instance for documents that benefit from a flexible
schema (configuration, audit fragments, KYC payloads, etc.). Background work
runs through Bull queues backed by Redis.
The service is the integration point for the payment gateways listed in
src/payment-gateway/integration/: Paystack, dLocal, Monnify,
Paga, Providus, Optimus, Sterling, Wema (which also
houses the AlatPay flow), and the in-house Global Accelerex
integration. Each one has both an outbound client and an inbound webhook
middleware wired into AppModule.
Architecture & Technology¶
The high-level shape is the Nest standard, with a few deliberate additions:
- HTTP entry —
src/main.tsbuilds the Nest app, applies a global/api/v1prefix, mounts a Helmet middleware (getHelmetConfig), a CORS policy (getCORSConfig), an IP-proxy middleware, a raw-body middleware (used for webhook signature verification), a request logger, and the globalAppExceptionFilter. Swagger is mounted at/api/v1/documentationfromswagger_options. The configured port falls back to3000whenPORTis not set. - Microservice transport — the same process attaches a NATS JetStream
microservice via
@nestjs-plugins/nestjs-nats-jetstream-transportand starts it withapp.startAllMicroservices(). The shared JetStream configuration lives insrc/shared/jetstream/config. - Module composition —
AppModule(src/app.module.ts) imports Sequelize (configured fromsrc/db/main/models), Mongoose (configured fromsrc/db/secondary/config/mongodb-config), Nest'sScheduleModule, Bull (bull_configfromsrc/resources/constants/Queues), the Throttler module (with TTL and limits read fromConfigService), plus the domain modules. - Domain modules — the major capability areas each have their own Nest
module:
AuthModule,ClientModule,PaymentModule,WebhookModule,LocationModule,WalletModule,VendpayModule,VirtualAccountModule,PaymentMethodModule,OtpModule,EntityModule,AdminModule,CacheStoreModule,ProcessingFeeModule,NatsJetstreamModule,HealthModule,FileUploadModule,QrCodeModule,TransactionHistoryModule,InvoiceModule,AuditTrailModule,AccessControlModule,PaymentGatewayModule,PayoutFeeModule,KycModule,ConfigHubModule,SettlementModule,WalletTransferRequestModuleandBankTransferRequestModule. - Authentication middleware —
AppModule.configurewires several middlewares per route group: - gateway-specific webhook verifiers
(
PaystackWebhookMiddleware,DlocalWebhookMiddleware,MonnifyWebhookMiddleware,PagaWebhookMiddleware,ProvidusWebhookMiddleware,OptimusWebhookMiddleware,GlobalAccelerexWebhookMiddleware,WemaWebhookMiddleware) attached to/webhooks/<provider>routes; - the standard customer-side
Authenticatemiddleware for the payment, wallet, virtual-account, invoice, subscription and settlement-payment controllers; AdminAuthfor the admin / back-office controllers (clients, payment methods, processing fees, gateways, transaction history, virtual accounts, access control, audit trail, admins, wallet data, payout fees, KYC, ConfigHub, settlements, businesses, transfer requests, etc.);ExternalClientAuthfor client login;SandboxAuthfor the sandbox controller;VendPayAuthenticationfor the AlatPay path (with one explicit exclusion for the AlatPay access-code GET).- Persistence — Sequelize models live under
src/db/main/, withsequelize-climigrations and seeders driven by the.sequelizercfile and thedb:migrate*/db:seed*scripts inpackage.json. Mongo schemas live undersrc/db/secondary/, with migrations driven bymigrate-mongo-config.js(urlfromMONGODB_URI,migrationsDir: src/db/secondary/migrations, change log collectionchangelog, TypeScript migration files,useFileHash: false). - Background workers and scheduling — Bull (and BullMQ) provide queues;
Nest's
ScheduleModuleprovides time-based jobs via theCronModuleinsrc/shared/cron. - Caching —
cache-managerwithcache-manager-redis-storeis used through theCacheStoreModule. - Notifications —
@sendgrid/mailfor transactional email, plus shared notification plumbing undersrc/shared/notification. - Document and asset handling —
qrcodefor QR generation,aws-sdkfor S3 uploads (FileUploadModule),csv-parsefor CSV ingestion,date-holidaysfor holiday-aware scheduling. - Validation and serialisation —
class-validator,class-transformer,class-validator-date,joi(dev) and the standard Nest pipes. - Logging —
winstonwithwinston-daily-rotate-file, plusmorganfor HTTP access logs. - Inter-service messaging —
amqplibfor AMQP and the NATS JetStream transport noted above.
The interior of src/ is organised by capability area, each of which
generally pairs a Nest module with one or more controllers, a service
layer, repositories, DTOs, schemas / models, and (where applicable) a
webhook controller and a queue processor.
How It Fits With Other Systems¶
Inputs:
- Customer-facing clients — internal Vendease applications hit the
/api/v1REST surface to initiate payments, manage wallets and virtual accounts, raise transfer requests, run KYC checks, and so on. TheAuthenticatemiddleware enforces the standard client authentication. - Payment-gateway webhooks — every supported gateway has a dedicated
/webhooks/<gateway>route with its own signature-verification middleware, so external providers can post settlement and transaction notifications back into the system. - Admin / back-office — operators authenticate via
AdminAuthto reach the configuration, processing-fee, payout-fee, gateway, transaction-history, KYC and settlement controllers insrc/admin/. - Sandbox clients — the sandbox controller, gated by
SandboxAuth, exposes a subset of the API for integration testing. - NATS JetStream — the embedded microservice consumer (configured via
getNATSConfig) listens for cross-service events from the rest of the Vendease platform. - Vendpay / AlatPay — the AlatPay flow has its own authentication
(
VendPayAuthentication) and runs through a dedicated controller inpayment-gateway/integration/wema/alatpay.
Outputs:
- Payment gateways — outbound API calls to Paystack, dLocal, Monnify,
Paga, Providus, Optimus, Sterling, Wema / AlatPay and Global
Accelerex for transaction creation, virtual-account provisioning,
status polling, refunds and payouts. Integration code per provider
lives in
src/payment-gateway/integration/<provider>/. - Postgres (primary store) — Sequelize models for all transactional data: clients, payments, wallets, virtual accounts, invoices, transaction history, settlement, KYC outcomes, fee schedules, audit trail summaries, etc.
- MongoDB (secondary store) — Mongoose schemas for ConfigHub,
audit-trail detail, KYC payloads, and other document-shaped data, with
schema migrations under
src/db/secondary/migrationsdriven bymigrate-mongo. - Redis — Bull queues, the cache store, throttler counters.
- Email (SendGrid) — transactional notifications.
- Object storage (S3) — file uploads handled by
FileUploadModule. - NATS JetStream — outbound events for other Vendease services to consume.
The service is the system of record for everything money-related at Vendease and the only component allowed to talk to the external payment processors directly. Other services interact with it either through the REST API or by exchanging events on NATS.
Repository Layout¶
Top-level files:
package.json,tsconfig.json,tsconfig.build.json,nest-cli.json,.eslintrc.js,.prettierrc— Nest build, TypeScript and lint config.Dockerfile—node:18-slimbase, copies the source, installs dependencies, runsnpm run buildand starts vianpm run start:prod, exposing port3000.docker-compose.yml— local dev compose that brings up a single NATS server (nats:2.9-alpine3.17) on thecore-networksDocker network, with JetStream enabled. Application services and databases are not in this compose file and are expected to be provided externally.migrate-mongo-config.js—migrate-mongoconfiguration; readsMONGODB_URIfrom.envand points atsrc/db/secondary/migrations..sequelizerc— Sequelize CLI configuration for the primary Postgres store.env.sample— template environment file. The keys present areNODE_ENV,PORT,TEST_HOST/TEST_DB/TEST_USER/TEST_PASSWORD/TEST_PORT,DEV_*,PROD_*,APP_TOKEN_SECRET,PAYSTACK_NG_SECRET, andPAYSTACK_URL. The runtime needs additional secrets for the other gateways (Monnify, dLocal, Paga, Providus, Optimus, Sterling, Wema / AlatPay, Global Accelerex), MongoDB, Redis, NATS, AWS / S3 and SendGrid; these are populated through deployment configuration rather thanenv.sample.sonar-project.properties— SonarQube project metadata.README.md— short quickstart with install, run, migration / seed and test commands.config-data-files/— JSON reference data shipped alongside the source. Three files are present:country-region.json,kyc.json, andtrigger.json. These are loaded as static data inside the application rather than stored in either database.coverage/,dist/,node_modules/,test-report.xml— generated / runtime artefacts.
The application source under src/ is organised by capability area.
Each top-level folder roughly corresponds to a Nest module:
access-control/,admin/,app.module.ts,app.controller.ts,app.service.ts,app.controller.spec.ts— root and admin surfaces.audit-trail/— audit logging endpoints and persistence.auth/— authentication middlewares (per-gateway webhook verifiers, customer auth, admin auth, sandbox auth, external client auth, vendpay auth) and supporting strategies.client/— client (merchant) records, login, and sandbox controller.config-hub/— runtime configuration entries surfaced via the ConfigHub API.db/— database wiring split intodb/main/(Sequelize models, migrations, seeders, repositories) anddb/secondary/(Mongoose schemas, migrations, mongodb config).entity/— generic entity service.health/— Terminus-based health checks.invoice/— invoice CRUD and webhook flows.kyc/— KYC submission and review.location/— country / region lookups (likely backed bycountry-region.json).otp/— one-time password issuance and verification.payment/— top-level payment operations and subscription support.payment-gateway/— abstractions, factory, setup, and per-providerintegration/folders forpaystack,dlocal,monnify,paga,providus,optimus,sterlingandwema.payment-method/— supported payment methods and their configuration.payment-request/— bank- and wallet-transfer request flows (bank-transfer-request/,wallet-transfer-request/).payout-fee/,processing-fee/— fee configuration and computation.resources/— Nest filters, middleware (raw body, IP proxy, logger), configs and the globalappProvider.scripts/— operational scripts includingscripts/migrations/index.tsreferenced by themigrate:eprocurementnpm script.settlement/— settlement requests and the settlement payment controller.shared/— cross-cutting modules: cache store, ConfigHub support, CORS, cron, event, file upload, helmet, JetStream consumer / config, migration helpers, notification, queue, request, swagger config and the QR-code module.test/— testing scaffolding.transaction-history/— transaction listing endpoints.vendpay/,virtual-account/,wallet/— the three balance / wallet capability areas, each with controllers and modules.
Running, Building & Deploying¶
The package.json scripts cover the standard Nest workflow with a few
additions specific to the dual-database setup and the migration scripts:
npm install, thennpm run start:devfor the watch-mode dev server.npm run start:prodruns the builtdist/src/mainwith--max-old-space-size=512to cap heap usage.npm run buildcompiles vianest buildtodist/.npm run lint,npm run format— eslint with--fixand prettier write.npm test,npm run test:watch,npm run test:cov,npm run test:e2e,npm run test:debug— Jest variants.jest-sonar-reporteris the configuredtestResultsProcessor, so reports flow into SonarQube via thesonar-project.propertiesfile. Thejestblock inpackage.jsoncarries an extensivecoveragePathIgnorePatternslist that excludes generated DB code, gateway integrations, shared infra, modules, repositories, schemas, and DTOs from coverage.npm run db:migrate,npm run db:migrate:dev,npm run db:migrate:prod(uses$POSTGRES_URL),npm run db:migrate:undo,npm run db:migrate:undo:all— Sequelize migrations.npm run db:seed,npm run db:seed:undo:all— Sequelize seeders.npm run db:migrate-mongo,npm run db:migrate-mongo:dev—migrate-mongoexecution against the secondary Mongo store, using a built configuration file.npm run setup:db— convenience target that builds, seeds, then migrates Postgres in one go.npm run migrate:eprocurement— runssrc/scripts/migrations/index.tsviats-node, intended for one-off data migrations from E-Procurement.
The Dockerfile produces a single image that both serves HTTP and runs
the NATS JetStream microservice consumer. The docker-compose.yml only
ships a NATS broker for local development — Postgres, MongoDB and Redis
are expected to come from elsewhere (a separate compose file or external
hosted instances).
Configuration is environment-driven: values come in through process.env
and are validated through Nest's ConfigModule. The env.sample file
lists the minimum keys the codebase expects in committed form
(NODE_ENV, PORT, the per-environment Postgres credentials, the app
token secret, and the Paystack secret / URL). The full set used by the
running service includes credentials for every supported gateway plus
MongoDB, Redis, NATS, AWS and SendGrid.
Operational Notes & Risks¶
- Single process, large surface — one Nest process owns the REST API,
the NATS JetStream consumer, Bull queue producers, scheduled cron jobs,
Sequelize and Mongoose connections. Memory and event-loop pressure from
any one of these can affect the others; the production start script
caps heap at 512 MB (
--max-old-space-size=512), which makes that shared budget tight. - Webhook signature verification is per-gateway — each external
webhook route is guarded by a provider-specific middleware. Adding a
new provider requires both an integration folder under
src/payment-gateway/integration/<provider>/and a webhook middleware wired inAppModule.configure. Missing the second step leaves the webhook unauthenticated. - Two databases, two migration tools — Sequelize handles Postgres
via
sequelize-cli;migrate-mongohandles MongoDB. Schema changes to the dual-store model have to be coordinated across two distinct command pipelines. migrate-mongorequires a build first —npm run db:migrate-mongoreads the migration runner fromdist/src/db/secondary/config/migrate-mongo-build-config.js, so a staledist/will run stale migrations. The:devvariant builds first; the bare variant assumes a fresh build.- Throttler limits read from config —
ThrottlerModuleis configured withthrottle_ttlandthrottle_limitfromConfigService, so missing or zero values disable rate limiting. - Heavy use of middleware ordering —
AppModule.configureregisters more than a dozen middlewares against specific routes and controllers. The order in which controllers are added toforRoutes(...)matters for which authentication strategy applies; new controllers need to be appended to the rightforRoutesblock, not just imported. - Coverage exclusions are broad — the
jestblock excludes a lot of the DB layer, all gateway integrations, severalshared/modules and the entirepayment-gateway/integration/tree from coverage. Test metrics therefore cover only the business-logic core; the third-party surface is not measured. - Shipped reference data —
config-data-files/country-region.json,kyc.jsonandtrigger.jsonare committed alongside source. Updates flow through repository commits rather than database changes, so a rollback of a deployment also rolls back any data changes in those files. - NATS-only
docker-compose.yml— the local compose only starts NATS, so a complete local environment requires Postgres, MongoDB, Redis (and any required gateway sandboxes) to be brought up separately. - Sandbox controller exists —
SandboxAuthandSandboxControllerexist as a parallel surface for integration testing. Its scope and data isolation should be reviewed before exposing it to external partners. - Gateway secrets — the codebase expects credentials for at least
Paystack, dLocal, Monnify, Paga, Providus, Optimus, Sterling, Wema /
AlatPay and Global Accelerex. The
env.sampleonly lists the Paystack ones, so the rest are tracked outside source control. Losing any of them would silently break the corresponding gateway path until the credentials are restored. UNLICENSEDpackage —package.jsondeclares"license": "UNLICENSED". The repository is intended for internal use and should not be redistributed.