Skip to content

ObeeJ/gopayrollengine

Repository files navigation


CI  Go  Postgres  Redis  License


Async bulk salary disbursement engine built to global FinTech production standards.
PCI DSS · NDPR · SOC 2 · CBN — compliance by design, not by retrofit.




Overview

go-payroll-engine is a backend system for processing bulk payroll disbursements through Monnify. It handles the full lifecycle — from payroll batch creation through background processing, real-time webhook reconciliation, and compliance evidence generation.

The codebase is designed as a reference implementation of what production FinTech infrastructure looks like: every security control, compliance requirement, and observability concern addressed at the architecture level rather than patched in later.




Architecture


  Client Request
       │
       ▼
  ┌─────────────────────────────────────────────────────┐
  │  API Layer                                          │
  │                                                     │
  │  Global:    SecurityHeaders → BodyLimit → Logger    │
  │             → Metrics → RateLimit → Recovery        │
  │  Per group: JWTAuth → TenantScope → DataResidency   │
  └────────────────────────┬────────────────────────────┘
                           │
                           ▼
  ┌─────────────────────────────────────────────────────┐
  │  Service Layer                                      │
  │                                                     │
  │  CreatePayroll → DB Transaction → Redis Enqueue     │
  └────────────────────────┬────────────────────────────┘
                           │
                           ▼
  ┌─────────────────────────────────────────────────────┐
  │  Worker Layer                          (Asynq)      │
  │                                                     │
  │  FSM Transition → Employee Hash Map → Monnify API   │
  └────────────────────────┬────────────────────────────┘
                           │
                           ▼
  ┌─────────────────────────────────────────────────────┐
  │  Webhook Handler                                    │
  │                                                     │
  │  HMAC Verify → Bloom Filter → FSM → Atomic Counter  │
  │  → Audit Log → Reconcile Parent Payroll             │
  └─────────────────────────────────────────────────────┘



Security


Authentication Encryption Transport Abuse Prevention

JWT with org-scoped claims

RBAC role gates

bcrypt cost-12

Timing-safe key comparison

AES-256-GCM on all PII fields

Transparent GORM serializer

Envelope encryption (KEK/DEK)

KMS-ready interface

OWASP security headers

1 MB body size limit

Non-root Docker image

Production startup guards

Token bucket rate limiting

Idempotency keys (Redis, 24h)

Bloom filter deduplication

HMAC-SHA512 webhook verification




Data Integrity


Pattern File Guarantee
Finite State Machine models/models.go Illegal status transitions rejected before any DB write
Atomic Counter models/models.go Webhook reconciliation fires exactly once per batch
Hash Map workers/payroll_worker.go O(1) employee lookup — eliminates N+1 query pattern
Bloom Filter middleware/bloom.go ~99% of duplicate webhooks caught before DB read
Append-Only Log audit_events table Immutable record of every state change — no UPDATE, no DELETE
Idempotency Map middleware/idempotency.go Network retry cannot create duplicate payroll batch
Weighted Sliding Window services/analytics_service.go Cash flow forecast biased toward recent data
Envelope Encryption models/encryption.go PII encrypted at rest, decrypted transparently on read; HMAC blind index for searchable email
Postgres Row Level Security db/migrations/000008–000011 Every tenant-scoped table enforces organization_id = app.org_id at the DB layer — a forgotten WHERE clause returns zero rows, not another tenant's data
Money as Kobo (BIGINT) pkg/money/money.go All monetary values stored as int64 minor units — no float64 rounding, no silent precision loss
FSM CAS Counter Init workers/payroll_worker.go Worker sets status=processing and pending_count=N in one atomic UPDATE — closes the race where webhooks arriving during the Monnify call would decrement from 0



Compliance


CBN NDPR SOC 2 Data Residency

BVN verification via Dojah at employee creation.

KYC recorded before any payroll data is stored.

Response hash stored — not the BVN itself.

Append-only consent records per employee per type.

Withdrawal creates a new row — history is never mutated.

Consent expiry enforced at query time.

Daily evidence snapshots to JSON.

One-click compliance report endpoint.

Migration version, audit counts, security checks all captured.

data_region field on every organization.

Geo-fencing middleware rejects cross-region requests.

Default region ng — zero breaking changes for existing orgs.




Observability


  /metrics  ──→  Prometheus (scrape interval: 15s)
                      │
                      ▼
               Grafana Dashboard
                      │
       ┌──────────────┼──────────────┐
       │              │              │
  HTTP Layer     Payment Layer   Security Layer
       │              │              │
  request rate    Monnify p95    auth failures
  error rate %    success rate   rate limit hits
  latency p95     batch duration  webhook dupes

Start the full observability stack:

docker-compose up
Service URL
API http://localhost:28080
Grafana http://localhost:3000
Prometheus http://localhost:9090
Metrics http://localhost:28080/metrics



Getting Started


Prerequisites: Go 1.25+, Docker, PostgreSQL 15, Redis 7

# Clone
git clone https://github.com/ObeeJ/gopayrollengine.git
cd go-payroll-engine

# Configure
cp .env.example .env
# Edit .env — set JWT_SECRET, ENCRYPTION_KEK (base64 32 bytes),
# ENCRYPTION_HMAC_KEY (base64 32 bytes), MONNIFY credentials.
# Generate keys: openssl rand -base64 32

# Start infrastructure (Postgres on :5432, Redis on :6379)
docker-compose up db redis -d

# Apply migrations — runs all up.sql files in order
make migrate-up   # or: APP_MODE=api go run cmd/api/main.go (migrates on startup)

# Seed database (creates ORG-DEMO-0001 with 3 employees + 3 months history)
APP_MODE=seed go run cmd/api/main.go

# Start API server
APP_MODE=api go run cmd/api/main.go

# Start background worker (separate terminal)
APP_MODE=worker go run cmd/api/main.go



API


Authentication

Method Path Description
POST /api/v1/auth/login Issue JWT
POST /api/v1/auth/refresh Refresh token

Employees

Method Path Description
POST /api/v1/employees/ Create employee — BVN verified, consent recorded, PII encrypted
GET /api/v1/employees/ List employees (paginated)

Payroll

Method Path Description
POST /api/v1/payrolls/ Initiate batch — idempotent, queued async
GET /api/v1/payrolls/:id Batch status and all disbursement items

Analytics & Compliance

Method Path Description
GET /api/v1/analytics/predictive Cash flow forecast with risk level
POST /api/v1/consent/ Record NDPR consent
GET /api/v1/consent/:employee_id Full consent history
GET /api/v1/compliance/report 30-day SOC 2 / CBN evidence bundle — requires role=compliance

Infrastructure

Method Path Description
POST /api/v1/webhooks/monnify Disbursement reconciliation — HMAC verified
GET /healthz Liveness probe
GET /readyz Readiness — checks DB, Redis, encryption key
GET /metrics Prometheus scrape endpoint



Stack


Go PostgreSQL Redis Docker Prometheus Grafana GitHub Actions


Layer Technology
Language Go 1.25+
Web framework Gin
ORM GORM
Background jobs Asynq
Migrations golang-migrate
Auth golang-jwt/jwt/v5
Metrics Prometheus + Grafana
Payment gateway Monnify
KYC Dojah



Project Structure


go-payroll-engine/
├── cmd/api/                    # Entrypoint — api | worker | seed | collect-evidence
├── config/                     # Prometheus config, Grafana dashboard
├── internal/
│   ├── api/
│   │   ├── handlers/           # auth, employees, payrolls, webhooks, compliance
│   │   ├── middleware/         # jwt, ratelimit, idempotency, bloom, residency, logger
│   │   └── routes.go
│   ├── db/
│   │   └── migrations/         # 000001 → 000012 versioned SQL (RLS, Kobo, encryption, indexes)
│   ├── integrations/
│   │   └── monnify/            # bulk transfer + wallet balance (mock-aware)
│   ├── models/                 # GORM models, FSM, encryption, audit log
│   ├── observability/          # Prometheus metric definitions
│   ├── services/               # payroll, analytics, BVN, SOC 2 collector
│   └── workers/                # Asynq handlers, Redis client, Asynq client
└── pkg/
    └── money/                  # Kobo type — integer arithmetic, banker's rounding



Testing


The Makefile wraps every step. Integration tests need a Postgres instance with all migrations applied; the targets below handle both.

# Boot the test database (port 5433) and apply migrations
make bootstrap

# Unit tests only
make test

# Unit + integration (against the bootstrapped database)
make test-integration

# With race detector
make test-race

# Aggregate coverage
make cover

Integration tests are gated behind the integration build tag and live alongside the code they exercise (*_integration_test.go). They prove the RLS policy blocks cross-tenant reads/writes, the webhook handler reconciles exactly once under concurrent fire, and the worker initializes its counter before the Monnify call.




MIT License


About

Production-grade payroll disbursement engine in Go. JWT auth, AES-256-GCM PII encryption, FSM lifecycle, Bloom filter deduplication, Prometheus metrics, NDPR/SOC2 compliance, multi-tenancy, versioned migrations.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors