Skip to content

Repository files navigation

Orcta Pay

Standalone payments service for Orcta products (OrctaGo, POS). Implements ADR-034 and ADR-035: one webhook per gateway, optd-{product}-{gateway}-{ulid} reference, reservation before payout, three-timestamp double-entry ledger, transactional outbox, and Valkey-ranked gateway routing.

Design documentation lives in orcta-go-docs/. Start with ARCHITECTURE.md and CONVENTIONS.md. Decisions are recorded as ADRs in orcta-go-docs/docs/adr/.

Getting started

task tools        # install sqlc, golangci-lint, migrate, goimports
cp .env.example .env
task dev:up       # start Postgres and Valkey
task migrate:up   # apply migrations
task run          # serve on :8080

If task dev:up fails to bind a port, point DATABASE_URL / VALKEY_ADDR at the host services or set POSTGRES_PORT / VALKEY_PORT in .env.

Verify:

curl localhost:8080/healthz   # liveness — process is up
curl localhost:8080/readyz    # readiness — dependencies reachable
curl localhost:8080/metrics   # Prometheus exposition

Common tasks

task lists everything. The ones used most:

Command Purpose
task check Format, vet, lint, and race tests — run before pushing
task test:race What CI runs
task generate Regenerate sqlc query code after changing SQL
task migrate:new -- add_payout_table Create a migration pair
task dev:reset Destroy local data and re-migrate from scratch
task run:worker Run background jobs (payout batch, outbox drain, reconciliation)

Layout

cmd/api/            HTTP server entrypoint — wiring only
cmd/worker/         Background jobs — wiring only

internal/
  charges/          ChargeRequest, ChargeResult (sealed), Service with Initiate + Status
  payouts/          PayoutBatch, Service (reservation → disbursement)
  gateway/          AggregatorClient (Initiate/Verify/Refund/Payout), Hubtel + Paystack adapters, ChargerRouter
  ledger/           vendor_ledger_entries, platform_commission_entries, payout_reservations
  config/           Env parsing, validated once at startup
  platform/         Composition root — builds the dependency graph
  api/              chi router, middleware, handlers (charges, payouts, webhooks)
  observability/    slog + OTel + Prometheus behind vendor-agnostic accessors
  storage/
    postgres/       sqlc-generated code + adapters, tx.go with WithTx
    valkey/         gateway health, circuit breaker
  money/            Money type — integer minor units, never float

api/                OpenAPI spec and spec.go embedding
migrations/         golang-migrate SQL files
orcta-go-docs/      Architecture, conventions, ADRs
deploy/
  data/             long-lived Postgres + Valkey stack
  app/              API + worker + migrate (Kamal blue/green)

API

api/openapi.yaml is the wire contract. A test walks the router and fails if the spec and the served routes disagree.

Method Path Description
POST /v1/charges Initiate a charge (product, amount, wallet) → optd-... reference
GET /v1/charges/{ref}/status Gateway truth via GetTransactionStatus
POST /v1/payouts Batch payout (uses reservation)
POST /webhooks/hubtel Hubtel webhook (HMAC, dedup)
POST /webhooks/paystack Paystack webhook (HMAC, dedup)

task api:docs renders the reference. task api:lint validates the spec.

Deployment

Trunk-continuous via Orcta Runtime. See docs/DEPLOYMENT.md (canonical).

master is the only deployable branch. fix/* → PR (5 required checks) → merge to masterghcr.io/...:<sha> → GHCR registry_package webhook → Orcta pulls deploy/app/docker-compose.yml (DEPLOY_SHA=<sha>, web + Kamal blue/green, health 8085/healthz), auto-rollback on fail. Caddy serves api.pay.orctatech.com.

Datastores are a separate long-lived stack (deploy/data//srv/apps/orcta-pay-data/), never in deploy/app/. See DEPLOYMENT.md §2 and deploy/data/BACKUP.md for backups (daily pg_dump, WAL archiving).

Backups and point-in-time recovery are documented in deploy/data/BACKUP.md: nightly pg_dump --format=custom sidecar, WAL archiving (wal_level=replica, archive_timeout=60) for PITR, BACKUP_S3_BUCKET off-host sync, and the down -v guardrail (deploy/data/scripts/guard.sh, task data:down). Run task data:down (never down -v) on prod — it requires ALLOW_DATA_LOSS=1.

Observability

Domain packages never import OpenTelemetry or Prometheus directly — they use internal/observability:

ctx, span := observability.StartSpan(ctx, "charges.Initiate")
defer span.End()
observability.SetEventField(ctx, "charge_ref", ref)
observability.LoggerFromContext(ctx).InfoContext(ctx, "charge_initiated")

Each request emits one wide event with every layer's fields on a single line, alongside level-based logs. Every ERROR should have an alert attached.

deploy

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages