ECG Payment & Recharge
Orchestrator
A microservices-based electricity prepaid and postpaid payment platform built for the Electricity Company of Ghana. Vendor requests flow through a public REST gateway into durable Wolverine saga workflows coordinating six backend services.
Platform Overview
The platform exposes a vendor-facing REST API and orchestrates multi-step payment workflows via Wolverine sagas backed by Marten/PostgreSQL and RabbitMQ. Sagas provide durable, compensatable workflows — if any step fails, the saga can roll back or retry safely without duplicating side-effects.
Services
The platform is decomposed into six microservices. Api.Gateway is the only publicly accessible service; all others communicate internally via durable RabbitMQ messaging.
| Service | Port | Role |
|---|---|---|
| Api.Gateway | 32780 / 32781 |
Public REST API — sole external entry point. Runs the pre-transaction guard chain and dispatches saga commands via RabbitMQ. |
| Services.Orchestrator | 32775 / 32776 |
Long-running Wolverine saga coordinator. Manages all multi-step payment workflows with durable inbox/outbox guarantees against Marten/PostgreSQL. |
| Services.Tokens | — | Meter recharge and electricity token generation. Routes to the correct vendor API per MeterCategoryCode; tracks per-category failure counts. |
| Services.Billing | — | Invoice lifecycle management, top-up and settlement invoice creation, and scheduled daily settlement via an hourly TickerQ cron job. |
| Services.Browser | — | Headless browser rendering of HTML templates into PDF documents. Uploads output to Azure Blob Storage and returns the resulting URL. |
| Services.Notifications | — | Consumes seven RabbitMQ queues to deliver SMS (Arkesel), email (Brevo), and HTTP webhook callbacks to customers and vendors. |
System Architecture
The gateway is the only publicly reachable service. All mutation operations are dispatched to the orchestrator via RabbitMQ commands, which then fans out to the appropriate leaf services. Read-heavy endpoints are served directly from the gateway using a two-tier Redis cache.
Features
1 Prepaid Meter Vending
Accepts a vend request from a vendor, validates it against the pre-transaction guard chain, then dispatches a VendPrepaidSaga. The saga transfers funds from the vendor's Fineract float account to the meter wallet, calls the correct meter vendor API to generate an electricity token, and delivers an SMS to the customer plus a webhook callback to the vendor.
PaymentData block accepts channel (cash or mobile-money), provider, accountNumber, accountName, and referenceId. All fields are persisted in the payment record's JSONB Extra column.
2 Postpaid Meter Vending
Extends the vend flow for postpaid meters. Before any funds are moved, the VendPostpaidSaga first queries the SUBS Postpaid API to validate the meter and retrieve the outstanding bill. After payment is complete, a postpaid bill credit is recorded and postpaid-specific notifications are sent, followed by an additional GL entry for the notification cost.
Same endpoint as prepaid — routing is determined by the meter's category code.
3 Vend Status Lookup
Allows a vendor to query the outcome of a previously submitted vend using either the platform's internal reference or the vendor's own external reference. Returns the generated electricity token if vending succeeded. The endpoint response is output-cached per reference in Redis.
4 Multi-Vendor Meter Recharge Routing
Routes token generation requests to the correct meter vendor API based on the meter's MeterCategoryCode. Each vendor has dedicated credentials, endpoint logic, and a failure count that is tracked and surfaced to detect service degradation.
| Category Code | Vendor |
|---|---|
MMS | ECG MMS Meter Layer API |
NURI | NURI Meters |
HOLLEY | HOLLEY Meters |
MBH | MBH Meters |
CLOU | CLOU Meters |
ECASH4 | ECASH4 Meters |
IMES | IMES Meters |
ALPHA | ALPHA Meters |
On repeated failures, the MeterCategoryFailureTrackingHandler increments the category's failure count, which is exposed through the meter category status endpoint for vendor visibility.
5 Meter Category Status Query
Returns the meter categories assigned to the authenticated vendor, along with the active or inactive status of each. This allows vendors to know in real time which meter types are available for vending. Responses are output-cached per vendor with a 2-minute TTL backed by Redis.
6 Transaction Reversal Ticketing
Vendors can raise a formal reversal ticket against a completed vend transaction. The ticket carries a typed reason, a description, and a status lifecycle managed by the platform. When an admin approves the ticket, a MeterVendReversalSaga executes a Fineract fund refund and GL correction. Vendors can also query existing tickets by transaction reference.
| ID | Description |
|---|---|
| 1 | Token not received |
| 2 | Meter not recharged |
| 3 | Payment processed but no delivery |
| 4 | Duplicate charge |
| 5 | Incorrect amount charged |
| 6 | Other |
x-idempotency-key header with a 48-hour replay window, preventing duplicate reversal tickets from being created for the same transaction.
7 Vendor Balance Enquiry
Returns the authenticated vendor's current available float balance, total Fineract account balance, account currency, and account status. Because balance data is fetched live from Fineract during the VendorChecker preprocessor and placed into the per-request state bag, no additional database round-trip is needed at the endpoint itself.
8 Vendor Top-Up
Processes a vendor float account top-up initiated by an external payment (mobile money or bank transfer). The TopupSaga tracks both the base Amount and the commission-inclusive TotalAmount separately — the total is deposited into Fineract while the commission split is recorded in the GL entry. A top-up invoice PDF is generated and delivered by email and webhook.
9 Vendor Settlement
Moves funds from a vendor's Fineract float account to an external bank or mobile money account on a vendor-configured daily schedule. The BillingCronJobs runs an hourly TickerQ cron job (0 0 * * * *) that finds all vendors whose configured SettlementTime falls within the current ±5-minute window and triggers a settlement saga for each.
10 Invoice Lifecycle Management
Manages the complete lifecycle of vendor invoices from creation through to verified payment or rejection. Every state change produces an audit entry. Two mark-as-paid modes are supported: a basic confirmation and an enhanced version (V2) that accepts proof-of-payment file attachments. On verification, a receipt PDF is generated and delivered; on rejection, a rejection notification is sent.
| Method | Path | Action |
|---|---|---|
| POST | /invoices | Create a new invoice |
| POST | /invoices/{id}/pdf | Attach a generated PDF URL |
| POST | /invoices/{id}/receipt | Attach a receipt PDF URL |
| POST | /invoices/{id}/mark-paid | Mark invoice as paid (basic) |
| POST | /invoices/{id}/status | Update invoice verification status |
| POST | /invoices/{id}/verify | Verify or reject a payment |
Invoice types: settlement and topup. Email templates: invoice.html, payment-received.html, payment-rejected.html.
11 Document Generation (PDF & Screenshots)
Renders HTML templates or arbitrary HTML strings into PDF documents or screenshots using a headless browser. Output files are uploaded to Azure Blob Storage and the resulting URL is returned to the caller for storage or delivery.
| Method | Path | Action |
|---|---|---|
| POST | /generate/pdf | Render raw HTML to PDF |
| POST | /generate/pdf/template | Render a named template to PDF |
| POST | /generate/screenshot/template | Render a named template to screenshot |
| Template | Used for |
|---|---|
topup_invoice.html | Vendor top-up invoice |
settlement_invoice.html | Vendor settlement invoice |
invoice-template.html | General vendor invoice |
receipt-template.html | Payment receipt |
invoice_receipt.html | Combined invoice + receipt |
12 Multi-Channel Notifications
Delivers notifications via SMS, email (direct and template-based), and HTTP webhook callbacks. Supports both individual and bulk delivery modes. All channels are driven by dedicated RabbitMQ queues with Wolverine durable inbox/outbox guarantees, ensuring at-least-once delivery even across service restarts.
| Queue | Mode | Provider |
|---|---|---|
notifications-sms-queue | Single SMS | Arkesel (sms.arkesel.com) |
notifications-bulk-sms-queue | Bulk SMS | Arkesel |
notifications-email-queue | Single email | Brevo SMTP (smtp-relay.brevo.com:587) |
notifications-email-template-queue | Template-based email | Brevo SMTP |
notifications-bulk-email-queue | Bulk email | Brevo SMTP |
notifications-webhook-queue | Single webhook | Vendor callback URLs |
notifications-bulk-webhook-queue | Bulk webhook | Vendor callback URLs |
13 Vendor Authentication & Authorization
Dual-mode authentication at the gateway. API key authentication validates the key against an internal Auth API and issues a JWT bearing a VendorId claim. OAuth2/OIDC via Keycloak is also supported for JWT introspection.
Every protected endpoint runs the VendorChecker preprocessor, which hydrates the vendor's Fineract balance, status, business rules, and meter categories into a per-request state bag using a two-tier hybrid cache: a 10-second in-memory layer backed by a 20-second Redis layer. This eliminates per-request database round-trips while keeping data fresh.
14 Pre-Transaction Guard Chain
A sequenced pipeline of preprocessors executes before any vend or mutation endpoint. If any guard fails, a standardised error response is returned immediately — the handler is never reached. Guards run in order and each receives the full hydrated vendor state bag from the VendorChecker.
15 Idempotency & Output Caching
Prevents duplicate mutations and avoids redundant computation on reads. Mutation endpoints that accept an x-idempotency-key header will replay the original response from Redis for any duplicate request within the configured window — the handler is never re-executed. Read endpoints use FastEndpoints output caching with per-vendor or per-reference cache keys backed by Redis.
| Endpoint | Window | Key |
|---|---|---|
POST /api/v1/meters/vend |
Configured per deployment | x-idempotency-key header |
POST /api/v1/meters/reversals |
48 hours | x-idempotency-key header |
16 Meter Lookup (Prepaid & Postpaid)
Resolves meter details before or during a vend, using different paths depending on meter type:
- Prepaid — Performed at the gateway via the SUBS Enterprise API using HMAC-SHA256 signed requests, before dispatching the vend command to the orchestrator.
- Postpaid — Performed inside the orchestrator saga as Step 1 of
VendPostpaidSaga, via the SUBS Postpaid API, to validate the meter and retrieve the outstanding bill before any funds are moved.
Service Responsibility Matrix
Which services are involved in each feature.
| Feature | Api.Gateway | Orchestrator | Tokens | Billing | Browser | Notifications |
|---|---|---|---|---|---|---|
| Prepaid vend | ✓ | ✓ | ✓ | ✓ | ||
| Postpaid vend | ✓ | ✓ | ✓ | |||
| Vend status lookup | ✓ | |||||
| Multi-vendor routing | ✓ | |||||
| Meter category status | ✓ | |||||
| Reversal ticketing | ✓ | ✓ | ||||
| Vendor balance | ✓ | |||||
| Vendor top-up | ✓ | ✓ | ✓ | ✓ | ||
| Vendor settlement | ✓ | ✓ | ✓ | ✓ | ||
| Invoice lifecycle | ✓ | ✓ | ✓ | ✓ | ||
| Document generation | ✓ | |||||
| Multi-channel notifications | ✓ | |||||
| Vendor auth & authorization | ✓ | |||||
| Pre-transaction guards | ✓ | |||||
| Idempotency & caching | ✓ | |||||
| Meter lookup | ✓ | ✓ |