Skip to content

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 entrysrc/main.ts builds the Nest app, applies a global /api/v1 prefix, 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 global AppExceptionFilter. Swagger is mounted at /api/v1/documentation from swagger_options. The configured port falls back to 3000 when PORT is not set.
  • Microservice transport — the same process attaches a NATS JetStream microservice via @nestjs-plugins/nestjs-nats-jetstream-transport and starts it with app.startAllMicroservices(). The shared JetStream configuration lives in src/shared/jetstream/config.
  • Module compositionAppModule (src/app.module.ts) imports Sequelize (configured from src/db/main/models), Mongoose (configured from src/db/secondary/config/mongodb-config), Nest's ScheduleModule, Bull (bull_config from src/resources/constants/Queues), the Throttler module (with TTL and limits read from ConfigService), 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, WalletTransferRequestModule and BankTransferRequestModule.
  • Authentication middlewareAppModule.configure wires 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 Authenticate middleware for the payment, wallet, virtual-account, invoice, subscription and settlement-payment controllers;
  • AdminAuth for 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.);
  • ExternalClientAuth for client login;
  • SandboxAuth for the sandbox controller;
  • VendPayAuthentication for the AlatPay path (with one explicit exclusion for the AlatPay access-code GET).
  • Persistence — Sequelize models live under src/db/main/, with sequelize-cli migrations and seeders driven by the .sequelizerc file and the db:migrate* / db:seed* scripts in package.json. Mongo schemas live under src/db/secondary/, with migrations driven by migrate-mongo-config.js (url from MONGODB_URI, migrationsDir: src/db/secondary/migrations, change log collection changelog, TypeScript migration files, useFileHash: false).
  • Background workers and scheduling — Bull (and BullMQ) provide queues; Nest's ScheduleModule provides time-based jobs via the CronModule in src/shared/cron.
  • Cachingcache-manager with cache-manager-redis-store is used through the CacheStoreModule.
  • Notifications@sendgrid/mail for transactional email, plus shared notification plumbing under src/shared/notification.
  • Document and asset handlingqrcode for QR generation, aws-sdk for S3 uploads (FileUploadModule), csv-parse for CSV ingestion, date-holidays for holiday-aware scheduling.
  • Validation and serialisationclass-validator, class-transformer, class-validator-date, joi (dev) and the standard Nest pipes.
  • Loggingwinston with winston-daily-rotate-file, plus morgan for HTTP access logs.
  • Inter-service messagingamqplib for 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/v1 REST surface to initiate payments, manage wallets and virtual accounts, raise transfer requests, run KYC checks, and so on. The Authenticate middleware 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 AdminAuth to reach the configuration, processing-fee, payout-fee, gateway, transaction-history, KYC and settlement controllers in src/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 in payment-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/migrations driven by migrate-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.
  • Dockerfilenode:18-slim base, copies the source, installs dependencies, runs npm run build and starts via npm run start:prod, exposing port 3000.
  • docker-compose.yml — local dev compose that brings up a single NATS server (nats:2.9-alpine3.17) on the core-networks Docker network, with JetStream enabled. Application services and databases are not in this compose file and are expected to be provided externally.
  • migrate-mongo-config.jsmigrate-mongo configuration; reads MONGODB_URI from .env and points at src/db/secondary/migrations.
  • .sequelizerc — Sequelize CLI configuration for the primary Postgres store.
  • env.sample — template environment file. The keys present are NODE_ENV, PORT, TEST_HOST / TEST_DB / TEST_USER / TEST_PASSWORD / TEST_PORT, DEV_*, PROD_*, APP_TOKEN_SECRET, PAYSTACK_NG_SECRET, and PAYSTACK_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 than env.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, and trigger.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 into db/main/ (Sequelize models, migrations, seeders, repositories) and db/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 by country-region.json).
  • otp/ — one-time password issuance and verification.
  • payment/ — top-level payment operations and subscription support.
  • payment-gateway/ — abstractions, factory, setup, and per-provider integration/ folders for paystack, dlocal, monnify, paga, providus, optimus, sterling and wema.
  • 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 global appProvider.
  • scripts/ — operational scripts including scripts/migrations/index.ts referenced by the migrate:eprocurement npm 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, then npm run start:dev for the watch-mode dev server.
  • npm run start:prod runs the built dist/src/main with --max-old-space-size=512 to cap heap usage.
  • npm run build compiles via nest build to dist/.
  • npm run lint, npm run format — eslint with --fix and prettier write.
  • npm test, npm run test:watch, npm run test:cov, npm run test:e2e, npm run test:debug — Jest variants. jest-sonar-reporter is the configured testResultsProcessor, so reports flow into SonarQube via the sonar-project.properties file. The jest block in package.json carries an extensive coveragePathIgnorePatterns list 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:devmigrate-mongo execution 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 — runs src/scripts/migrations/index.ts via ts-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 in AppModule.configure. Missing the second step leaves the webhook unauthenticated.
  • Two databases, two migration tools — Sequelize handles Postgres via sequelize-cli; migrate-mongo handles MongoDB. Schema changes to the dual-store model have to be coordinated across two distinct command pipelines.
  • migrate-mongo requires a build firstnpm run db:migrate-mongo reads the migration runner from dist/src/db/secondary/config/migrate-mongo-build-config.js, so a stale dist/ will run stale migrations. The :dev variant builds first; the bare variant assumes a fresh build.
  • Throttler limits read from configThrottlerModule is configured with throttle_ttl and throttle_limit from ConfigService, so missing or zero values disable rate limiting.
  • Heavy use of middleware orderingAppModule.configure registers more than a dozen middlewares against specific routes and controllers. The order in which controllers are added to forRoutes(...) matters for which authentication strategy applies; new controllers need to be appended to the right forRoutes block, not just imported.
  • Coverage exclusions are broad — the jest block excludes a lot of the DB layer, all gateway integrations, several shared/ modules and the entire payment-gateway/integration/ tree from coverage. Test metrics therefore cover only the business-logic core; the third-party surface is not measured.
  • Shipped reference dataconfig-data-files/country-region.json, kyc.json and trigger.json are 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 existsSandboxAuth and SandboxController exist 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.sample only 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.
  • UNLICENSED packagepackage.json declares "license": "UNLICENSED". The repository is intended for internal use and should not be redistributed.