blob: 7da3a5118f16865a5ed35e5b4f867e397e682cd8 [file] [view]
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Apache Fineract Business Intelligence
The analytics pipeline for [Apache Fineract](https://fineract.apache.org/): the open-source core banking platform for financial inclusion.
This project reads data from a Fineract PostgreSQL database, transforms it through a layered dbt pipeline, and serves interactive dashboards in Apache Superset. It is designed to be **downstream and separate** from Fineract: the only connection is a read-only credential to the Fineract database. Everything else — the analytics warehouse, transformations, and dashboards — runs independently.
---
## Dashboards
| Dashboard | What it shows |
|---|---|
| **Portfolio Health** | Gross Loan Portfolio, active borrowers vs loans, PAR ratio, NPA ratio, disbursement and collection trends, portfolio composition by branch and product |
| **Delinquency & PAR** | Portfolio At Risk by DPD bucket (PAR 30/60/90/NPA), delinquency trend over time, bucket migration |
---
## Key assumption: Fineract database
This project connects to a **real Apache Fineract PostgreSQL database**. It does not manage or start the Fineract application — you run Fineract separately and point this project at its database.
For local development, clone and run Fineract locally (see [Setup](#setup) below). In production, set the `SOURCE_*` environment variables to point at your existing Fineract PostgreSQL instance.
The extractor connects via a **read-only** credential (`SOURCE_REPLICA_USER`) created by `bootstrap_source.sh`. It never writes to Fineract.
---
## Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Apache Fineract PostgreSQL (your Fineract database) │
│ fineract_default → bi_connector_source (read-only views)│
└───────────────────────────┬─────────────────────────────┘
│ read-only (fineract_reader)
┌─────────────────────────────────────────────────────────┐
│ Extractor (Python) │
│ Incremental watermark-based CDC │
│ → raw.raw_m_* tables in Analytics Warehouse │
└───────────────────────────┬─────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Analytics Warehouse (PostgreSQL) │
│ raw → staging (views) → facts (incremental) → marts │
│ Transformation engine: dbt │
└───────────────────────────┬─────────────────────────────┘
│ read-only (analytics_reader)
┌─────────────────────────────────────────────────────────┐
│ Apache Superset :8088 │
│ Row-level security — branch managers see their office │
│ Admin sees all offices │
└─────────────────────────────────────────────────────────┘
```
**Pipeline loop** (runs automatically inside the extractor container):
```
backfill once on startup
└─► loop every PIPELINE_INTERVAL_SECONDS (default: 1 hour):
1. extractor incremental — pull changed rows from Fineract DB
2. dbt build — rebuild marts (only if step 1 succeeded)
3. superset refresh — sync chart metadata (only if step 2 succeeded)
```
---
## Prerequisites
| Tool | Version | Notes |
|---|---|---|
| Docker Desktop | 24+ | Must be running |
| Docker Compose | v2 (plugin) | `docker compose version` |
| Git | any | |
| Git Bash | any | For running `.sh` scripts on Windows |
No local Python, Java, or database tools required.
---
## Setup
### Step 1 — Start the Fineract database
Clone the Fineract repository and start its PostgreSQL container:
```powershell
git clone -b develop https://github.com/apache/fineract.git
cd fineract
$env:PWD = (Get-Location).Path.Replace('\', '/')
docker compose -f docker-compose-postgresql.yml up -d db
```
Wait ~10 seconds for the container to become healthy.
### Step 2 — Pull and tag the Fineract image
Fineract publishes a pre-built image to Docker Hub — no local build needed:
```powershell
docker pull apache/fineract:latest
docker tag apache/fineract:latest fineract:latest
```
### Step 3 — Start Fineract (runs Flyway migrations)
```powershell
cd "C:\Users\<you>\Desktop\fineract"
$env:PWD = (Get-Location).Path.Replace('\', '/')
docker compose -f docker-compose-postgresql.yml up -d fineract
```
Watch the logs until Flyway finishes creating all `m_*` tables (2–5 minutes):
```powershell
cd "C:\Users\<you>\Desktop\fineract"
$env:PWD = (Get-Location).Path.Replace('\', '/')
docker compose -f docker-compose-postgresql.yml logs -f fineract
```
Wait for this line then press `Ctrl+C`:
```
Started FineractApplication in X.XXX seconds
```
### Step 4 — Verify tables exist
```powershell
cd "C:\Users\<you>\Desktop\fineract"
$env:PWD = (Get-Location).Path.Replace('\', '/')
docker compose -f docker-compose-postgresql.yml exec db psql -U root -d fineract_default -c '\dt m_*'
```
You should see 100+ tables like `m_loan`, `m_client`, `m_office`, etc.
### Step 5 — Seed demo data
Load demo data into the Fineract DB. This gives you:
- **25 clients**, **71 loans** across 3 offices and 4 products
- Staggered vintages from 1 month to 36 months ago — enough history for trend charts
- PAR-30, PAR-60, PAR-90, and NPA loans — every delinquency bucket populated
Run from PowerShell (from the `fineract-business-intelligence` directory):
```powershell
Get-Content "C:\Users\<you>\Desktop\fineract-business-intelligence\warehouse\seed\seed_fineract_source.sql" | docker exec -i fineract-db-1 psql -U root -d fineract_default
```
> Skip this step if pointing at a real Fineract instance that already has data.
### Step 6 — Clone and configure this project
```powershell
cd "C:\Users\<you>\Desktop"
git clone https://github.com/apache/fineract-business-intelligence.git
cd fineract-business-intelligence
Copy-Item .env.example .env
```
The defaults in `.env` work for local development without any edits.
### Step 7 — Bootstrap the source database
One-time step. Run from Git Bash inside the `fineract-business-intelligence` directory:
```bash
bash scripts/bootstrap_source.sh
```
This creates the `bi_connector_source` schema with compatibility views on the Fineract DB and grants read-only access to `fineract_reader`. Safe to re-run — all operations are idempotent.
Expected output:
```
[bootstrap-source] Connection OK
[bootstrap-source] Compatibility views created in schema 'bi_connector_source'
[bootstrap-source] Creating replica user if not exists...
[bootstrap-source] Read access granted to 'fineract_reader'
[bootstrap-source] === Source bootstrap complete. You can now run the pipeline. ===
```
### Step 8 — Start the BI stack
```powershell
cd "C:\Users\<you>\Desktop\fineract-business-intelligence"
docker compose up -d warehouse superset dbt extractor
```
This starts 4 services:
| Service | Role | Port |
|---|---|---|
| `warehouse` | Analytics PostgreSQL warehouse | 5434 |
| `extractor` | ETL pipeline — runs automatically on schedule | — |
| `dbt` | Transformation container | — |
| `superset` | Dashboard UI | **8088** |
The extractor waits 30 seconds for Superset to initialise, runs a full backfill, then loops every hour. Watch it:
```powershell
docker compose logs -f extractor
```
Wait for:
```
Done. PASS=81 WARN=0 ERROR=0
[pipeline] Pipeline run complete in Xs
```
### Step 9 — Open Superset
```
http://localhost:8088
```
| Role | Username | Password (default) | Sees |
|---|---|---|---|
| Admin | `admin` | `admin_dev_only` | All offices |
| North Branch Manager | `north_manager` | `north_manager_dev_only` | North Branch only |
| South Branch Manager | `south_manager` | `south_manager_dev_only` | South Branch only |
Navigate to **Dashboards** → **Portfolio Health** and **Delinquency & PAR**.
---
## Keeping Dashboards Fresh
### Automatic (default)
The extractor runs the full pipeline every hour automatically. No action needed.
### After changing data in Fineract
Force an immediate update instead of waiting for the next hour:
```powershell
docker compose restart extractor
```
### Force full pipeline run manually
```powershell
docker compose logs --tail=30 extractor # check current status first
```
Then from Git Bash:
```bash
docker exec fineract-bi-extractor bash -c "bash /app/scripts/run_pipeline.sh backfill"
```
### Check all container logs
```powershell
docker compose logs --tail=30 warehouse superset dbt extractor
```
What to look for:
| Container | Healthy sign |
|---|---|
| `warehouse` | `database system is ready to accept connections` |
| `superset` | `Portfolio Health dashboard created` |
| `dbt` | (idle — no output expected) |
| `extractor` | `PASS=81 WARN=0 ERROR=0` + `Pipeline run complete` |
---
## After a Machine Reboot
Fineract DB and the BI stack are separate — restart both:
```powershell
# 1. Restart Fineract DB
cd "C:\Users\<you>\Desktop\fineract"
$env:PWD = (Get-Location).Path.Replace('\', '/')
docker compose -f docker-compose-postgresql.yml up -d db fineract
# 2. Restart BI stack
cd "C:\Users\<you>\Desktop\fineract-business-intelligence"
docker compose up -d warehouse superset dbt extractor
```
No need to re-run bootstrap or re-seed — data is persisted in Docker volumes.
---
## Production Deployment
### Connecting to a remote Fineract database
Set these in `.env`:
```bash
SOURCE_DB_HOST=<your-fineract-db-host>
SOURCE_DB_HOST_PORT=5432
SOURCE_DB_NAME=fineract_default
SOURCE_BOOTSTRAP_USER=<admin-user>
SOURCE_BOOTSTRAP_PASSWORD=<secret>
SOURCE_REPLICA_USER=fineract_reader
SOURCE_REPLICA_PASSWORD=<secret>
SOURCE_DB_SCHEMA=bi_connector_source
```
Run bootstrap once against the remote database (from Git Bash):
```bash
bash scripts/bootstrap_source.sh
```
Then start the BI stack — it connects to the remote Fineract DB directly via `SOURCE_DB_HOST`.
### Recommended production settings
```bash
# Generate with: python -c "import secrets; print(secrets.token_hex(32))"
SUPERSET_SECRET_KEY=<64-char-hex>
# Run pipeline daily after COB completes
PIPELINE_INTERVAL_SECONDS=86400
# All passwords via your secrets manager
WAREHOUSE_ADMIN_PASSWORD=<secret>
WAREHOUSE_LOADER_PASSWORD=<secret>
WAREHOUSE_READER_PASSWORD=<secret>
SUPERSET_ADMIN_PASSWORD=<secret>
SUPERSET_NORTH_MANAGER_PASSWORD=<secret>
SUPERSET_SOUTH_MANAGER_PASSWORD=<secret>
```
---
## Data Pipeline Details
### Layer architecture
```
fineract_default.public.* Fineract source tables
│ bi_connector_source.* Compatibility views (bootstrap_source.sh)
│ Normalises schema differences across Fineract versions
raw.raw_m_* Raw layer — exact copy of source rows
+ tenant_id + source_loaded_at
staging.stg_m_* Staging views — rename columns, cast types,
drop PII (date_of_birth → age_band),
add pseudonymous client_hash
analytics.fact_loan_snapshot Daily grain: one row per (loan, date)
analytics.fact_delinquency_event One row per delinquency tag lifecycle event
analytics.mart_portfolio_health Grain: office × product × currency × date
analytics.mart_delinquency_par Grain: office × product × bucket × date
```
### PII handling
- `date_of_birth` is dropped in `stg_m_client` and replaced with `age_band` (6 cohorts)
- `client_id` is replaced downstream by `client_hash` = MD5(tenant_id || '::' || id)
- All presentation marts are aggregated at office × product level — no individual client rows reach Superset
- Row-level security in Superset restricts branch managers to their own office data
### Watermark-based incremental extraction
The extractor tracks a per-table `last_modified_on_utc` cursor in `meta.watermarks`. Each incremental run fetches only rows changed since the last successful extraction. A 10-minute lookback window (`EXTRACT_LOOKBACK_SECONDS=600`) handles clock skew and late-arriving updates.
---
## Project Structure
```
fineract-business-intelligence/
├── compose.yaml Docker Compose — 4-service BI stack
├── .env.example Environment template (copy to .env)
├── scripts/
│ ├── common.sh Shared helpers (docker checks, env loading)
│ ├── bootstrap_source.sh One-time: create views + grants on Fineract DB
│ └── run_pipeline.sh Full pipeline: extractor → dbt → superset
├── extractor/ Python ETL service
│ ├── cli.py Entry point: backfill | incremental
│ ├── extractor.py Extraction logic (11 tables, watermark-based)
│ ├── config.py Config from environment variables
│ └── watermark_manager.py Per-table cursor tracking
├── dbt/ dbt transformation project (fineract_bi)
│ ├── models/
│ │ ├── staging/ stg_* views (clean + rename)
│ │ └── marts/
│ │ ├── dimensions/ dim_office, dim_client, dim_product, …
│ │ ├── facts/ fact_loan_snapshot, fact_delinquency_event
│ │ └── presentations/ mart_portfolio_health, mart_delinquency_par
│ └── macros/
│ └── safe_divide.sql NULL-safe division macro
├── warehouse/
│ ├── schema/ DDL for raw, staging, mart, meta schemas
│ └── seed/
│ └── seed_fineract_source.sql Demo data (25 clients, 71 loans)
└── docker/
├── postgres-warehouse/ Warehouse init scripts (roles, permissions)
├── extractor/ Extractor Dockerfile (includes Docker CLI)
├── dbt/ dbt Dockerfile (dbt-postgres pinned <2.0)
└── superset/
├── Dockerfile
├── init_superset.sh First-run: DB migrate, admin user, dashboards
├── refresh_superset_assets.sh
├── bootstrap_superset_assets.py
└── superset_config.py
```
---
## Contributing
### Running dbt manually
```bash
# Enter the dbt container
docker compose exec dbt bash
# Run all models
dbt build
# Run a specific model
dbt run --select mart_portfolio_health
# Run tests only
dbt test
# Full rebuild (drops and recreates incremental tables)
dbt build --full-refresh
```
### Adding a new dbt model
1. Add SQL in the appropriate `dbt/models/` subdirectory
2. Add schema tests in the corresponding `_*.yml` file
3. Add Apache license header to the SQL file
4. Run `dbt build --select <your_model>` to verify
### Code standards
- All source files must carry the Apache License 2.0 header
- SQL: snake_case identifiers, explicit column lists (no `SELECT *` in models)
- Python: type annotations, dataclasses for config, no hardcoded credentials
- Shell: `set -euo pipefail`, source `common.sh` for shared helpers
---
## Troubleshooting
### Extractor fails: `role "fineract_reader" does not exist`
Bootstrap has not been run, or the Fineract DB was recreated. Re-run from Git Bash:
```bash
bash scripts/bootstrap_source.sh
```
Then restart the extractor:
```powershell
docker compose restart extractor
```
### Extractor cannot reach the Fineract DB
The extractor connects to the Fineract DB container by name (`fineract-db-1`) on the shared Docker network. If your Fineract DB container has a different name, update `SOURCE_DB_HOST` in `.env`:
```bash
SOURCE_DB_HOST=<your-fineract-db-container-name-or-host>
```
### Dashboards show "No data"
The pipeline has not completed yet. Check:
```powershell
docker compose logs --tail=30 extractor
```
Look for `Pipeline run complete`. If it shows `ERROR`, check the specific error and consult the relevant section below.
Force an immediate run:
```bash
docker exec fineract-bi-extractor bash -c "bash /app/scripts/run_pipeline.sh backfill"
```
### dbt fails: `adapter is not yet supported by dbt Fusion`
dbt 2.0 dropped PostgreSQL support. The dbt image pin (`dbt-postgres<2.0.0a1`) should prevent this, but if it happens rebuild the image:
```powershell
docker compose build dbt
docker compose up -d --force-recreate dbt
```
### dbt test fails: `unique_fact_delinquency_event_delinquency_event_key`
Duplicate rows in the incremental table from a previous run. Fix with a full-refresh of that model:
```bash
docker exec fineract-bi-dbt bash -c "cd /app/dbt && dbt build --full-refresh --select fact_delinquency_event"
```
### Shell scripts fail with `\r': command not found`
Windows Git added CRLF line endings. Fix with:
```bash
sed -i 's/\r//' scripts/bootstrap_source.sh scripts/common.sh scripts/run_pipeline.sh
```
### Warehouse container exits with code 126
An init script has CRLF line endings. Fix and restart:
```bash
sed -i 's/\r//' docker/postgres-warehouse/initdb/002_create_warehouse_roles.sh
```
```powershell
docker compose down -v
docker compose up -d warehouse superset dbt extractor
```
### Full clean slate
```powershell
# Stop everything and remove volumes
docker compose down -v
# Restart from Step 8
docker compose up -d warehouse superset dbt extractor
```
> `docker compose down -v` deletes all warehouse data and Superset metadata. The Fineract DB (managed separately) is unaffected. Re-run bootstrap (Step 7) before starting the BI stack again.
---
## License
Apache License 2.0 — see [LICENSE](LICENSE).
This project is part of the [Apache Fineract](https://fineract.apache.org/) ecosystem, started as part of Google Summer of Code 2026.