Skip to content

About

Insurance back office REST API: FastAPI, JWT, role-based access, Redis caching and idempotent claim filing. 35 tests.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Policy and Claims API

CI

A small insurance back office as a REST service. Agents write policies and file claims, assessors decide them, admins see everything. Built with FastAPI, PostgreSQL and Redis, with JWT auth, role based access control and idempotent claim filing.

The interesting part isn't the CRUD. It's the rules that stop money going out twice.

What it actually enforces

Rule Why it's here
A claim can't exceed the policy's sum insured The obvious one
Approved claims can't add up past the sum insured The one people miss. Three claims of 40k each on a 100k policy are each individually valid
A rejected claim frees its cover again A rejection must not quietly eat the limit
A claim can't be decided twice Second decision returns 409, not a silent overwrite
You can't approve more than was claimed Partial approval is allowed, inflation isn't
No claims on a cancelled or out-of-period policy Checked against today, not against the record alone
An agent sees only its own policies and claims Returns 404, not 403, so the API never confirms a record exists to someone who can't read it
Idempotency-Key makes filing safe to retry A dropped response must not create a second claim
Money is stored in paise as an integer Never a float. 0.07 comes back as 0.07

Roles

agent assessor admin
Create a policy yes no yes
Read a policy own only all all
Cancel a policy no no yes
File a claim own policies no yes
Decide a claim no yes yes

The role is carried in the JWT for speed but re-checked against the database row on every request, so a demotion takes effect immediately instead of waiting for the token to expire.

Running it

cp .env.example .env          # set JWT_SECRET
docker compose up --build     # api on :8000, postgres on :5432, redis on :6379
open http://localhost:8000/docs

Without Docker:

python -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.txt
export DATABASE_URL="sqlite:///./local.db" JWT_SECRET="dev-secret"
uvicorn app.main:app --reload

A full flow

BASE=http://localhost:8000

curl -s -X POST $BASE/auth/register -H 'content-type: application/json' \
  -d '{"email":"agent@example.com","password":"sup3rsecret","role":"agent"}'
curl -s -X POST $BASE/auth/register -H 'content-type: application/json' \
  -d '{"email":"assessor@example.com","password":"sup3rsecret","role":"assessor"}'

AGENT=$(curl -s -X POST $BASE/auth/login \
  -d 'username=agent@example.com&password=sup3rsecret' | python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
ASSESSOR=$(curl -s -X POST $BASE/auth/login \
  -d 'username=assessor@example.com&password=sup3rsecret' | python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')

POLICY=$(curl -s -X POST $BASE/policies -H "authorization: Bearer $AGENT" \
  -H 'content-type: application/json' -d '{
    "holder_name":"Asha Kumari","product":"motor-own-damage",
    "premium":"12500.00","sum_insured":"500000.00",
    "start_date":"2026-01-01","end_date":"2026-12-31"}' \
  | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')

# Retry this as many times as you like. The key makes it file exactly one claim.
curl -s -X POST $BASE/claims -H "authorization: Bearer $AGENT" \
  -H 'content-type: application/json' -H 'Idempotency-Key: claim-2026-0042' \
  -d "{\"policy_id\":$POLICY,\"amount\":\"20000.00\",\"reason\":\"bumper and headlight\"}"

curl -s -X POST $BASE/claims/1/decision -H "authorization: Bearer $ASSESSOR" \
  -H 'content-type: application/json' \
  -d '{"approve":true,"amount":"18000.00","note":"depreciation applied"}'

Caching

Policy reads go through Redis with a short TTL, and a cancel drops the key. Two things the cache deliberately does not do:

  • It caches the row, not the permission. Authorisation runs again on every request, so a warm cache can't leak a policy to the wrong agent. There's a test for exactly that.
  • It never takes the API down with it. Every Redis call is wrapped, and a failure is treated as a miss. test_a_broken_cache_does_not_break_the_api points an exploding client at it and the endpoint still answers 200.

Tests

pytest --cov=app --cov-report=term-missing

35 tests, 93% line coverage, running against SQLite so CI needs no services. They cover the auth surface, every role boundary, the money rules above, idempotency including two retries racing each other, and the cache behaviour with a fake Redis.

CI runs ruff, the suite with coverage, and a separate job that builds the image, boots the container and curls /health.

Layout

app/
  config.py      settings from the environment
  db.py          engine, session, declarative base
  models.py      users, policies, claims; money in paise
  schemas.py     request and response bodies, rupee to paise conversion
  security.py    bcrypt hashing and JWT issue/verify
  deps.py        current_user and the require(*roles) guard
  cache.py       Redis with a no-op fallback
  routers/       auth, policies, claims
tests/           35 tests

Things I'd add next

  • Alembic migrations. create_all is fine at this size and wrong at the next one.
  • A refresh token flow. Right now a token just expires.
  • An audit trail table, because in this domain "who approved it and when" is usually a regulatory question rather than a nice to have.

About

Insurance back office REST API: FastAPI, JWT, role-based access, Redis caching and idempotent claim filing. 35 tests.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages