From 5551e7d3f215971cf8c1b09925b91250673b93d8 Mon Sep 17 00:00:00 2001 From: Shashank Chandel <21042676+shashankch@users.noreply.github.com> Date: Sat, 3 Oct 2026 13:06:55 +0530 Subject: [PATCH] docs(mkdocs): migrate portal to MkDocs with D2 diagrams and CI automation - Migrate documentation portal from Docsify to Material for MkDocs - Redraw 12 architectural diagrams in D2 with automated drift checks - Add bidirectional JaCoCo coverage links and dark mode diagram styling --- .github/workflows/ci.yml | 39 +- .github/workflows/generate-diagrams.yml | 52 + .gitignore | 12 + CHANGELOG.md | 18 +- README.md | 133 ++- SECURITY.md | 4 +- docs-site/_navbar.md | 6 - docs-site/_sidebar.md | 24 - docs-site/index.html | 260 ----- docs/API_SPECIFICATION.md | 561 ++++++----- docs/ARCHITECTURE.md | 929 ++++++++++++------ docs/CHANGELOG.md | 1 + docs/CONTRIBUTING.md | 1 + docs/CONVENTIONS.md | 9 + docs/LICENSE.md | 1 + docs/ROADMAP.md | 1 + docs/SECURITY.md | 1 + ...030-ci-cd-quality-gates-jacoco-spotbugs.md | 2 +- docs/assets/diagrams/cache-aside-workflow.svg | 57 ++ docs/assets/diagrams/deadlock-elimination.svg | 53 + .../diagrams/distributed-lock-sequence.svg | 58 ++ .../assets/diagrams/idempotency-lifecycle.svg | 57 ++ .../kubernetes-deployment-topology.svg | 68 ++ .../assets/diagrams/security-filter-chain.svg | 54 + docs/assets/diagrams/system-overview.svg | 69 ++ docs/assets/diagrams/system-topology.svg | 87 ++ .../diagrams/transactional-outbox-flow.svg | 56 ++ .../diagrams/upi-validation-fallback.svg | 59 ++ docs/assets/diagrams/user-rate-limiting.svg | 53 + docs/assets/diagrams/zero-trust-lifecycle.svg | 59 ++ docs/assets/icons/ai.svg | 1 + docs/assets/icons/client.svg | 1 + docs/assets/icons/docker.svg | 1 + docs/assets/icons/gateway.svg | 1 + docs/assets/icons/github.svg | 1 + docs/assets/icons/grafana.svg | 1 + docs/assets/icons/java.svg | 1 + docs/assets/icons/kafka.svg | 1 + docs/assets/icons/kubernetes.svg | 1 + docs/assets/icons/lock.svg | 1 + docs/assets/icons/postgresql.svg | 1 + docs/assets/icons/prometheus.svg | 1 + docs/assets/icons/queue.svg | 1 + docs/assets/icons/redis.svg | 1 + docs/assets/icons/server.svg | 1 + docs/assets/icons/shield.svg | 1 + docs/assets/icons/spring.svg | 1 + docs/coverage.md | 83 ++ docs/diagrams/cache-aside-workflow.d2 | 32 + docs/diagrams/deadlock-elimination.d2 | 23 + docs/diagrams/distributed-lock-sequence.d2 | 36 + docs/diagrams/idempotency-lifecycle.d2 | 32 + .../kubernetes-deployment-topology.d2 | 92 ++ docs/diagrams/security-filter-chain.d2 | 34 + docs/diagrams/system-overview.d2 | 77 ++ docs/diagrams/system-topology.d2 | 99 ++ docs/diagrams/transactional-outbox-flow.d2 | 35 + docs/diagrams/upi-validation-fallback.d2 | 40 + docs/diagrams/user-rate-limiting.d2 | 31 + docs/diagrams/zero-trust-lifecycle.d2 | 74 ++ docs/index.md | 185 ++++ docs/javascripts/extra.js | 255 +++++ docs/stylesheets/extra.css | 351 +++++++ mkdocs.yml | 139 +++ requirements.txt | 3 + scripts/generate-diagrams.sh | 62 ++ 66 files changed, 3588 insertions(+), 896 deletions(-) create mode 100644 .github/workflows/generate-diagrams.yml delete mode 100644 docs-site/_navbar.md delete mode 100644 docs-site/_sidebar.md delete mode 100644 docs-site/index.html create mode 120000 docs/CHANGELOG.md create mode 120000 docs/CONTRIBUTING.md create mode 120000 docs/LICENSE.md create mode 120000 docs/SECURITY.md create mode 100644 docs/assets/diagrams/cache-aside-workflow.svg create mode 100644 docs/assets/diagrams/deadlock-elimination.svg create mode 100644 docs/assets/diagrams/distributed-lock-sequence.svg create mode 100644 docs/assets/diagrams/idempotency-lifecycle.svg create mode 100644 docs/assets/diagrams/kubernetes-deployment-topology.svg create mode 100644 docs/assets/diagrams/security-filter-chain.svg create mode 100644 docs/assets/diagrams/system-overview.svg create mode 100644 docs/assets/diagrams/system-topology.svg create mode 100644 docs/assets/diagrams/transactional-outbox-flow.svg create mode 100644 docs/assets/diagrams/upi-validation-fallback.svg create mode 100644 docs/assets/diagrams/user-rate-limiting.svg create mode 100644 docs/assets/diagrams/zero-trust-lifecycle.svg create mode 100644 docs/assets/icons/ai.svg create mode 100644 docs/assets/icons/client.svg create mode 100644 docs/assets/icons/docker.svg create mode 100644 docs/assets/icons/gateway.svg create mode 100644 docs/assets/icons/github.svg create mode 100644 docs/assets/icons/grafana.svg create mode 100644 docs/assets/icons/java.svg create mode 100644 docs/assets/icons/kafka.svg create mode 100644 docs/assets/icons/kubernetes.svg create mode 100644 docs/assets/icons/lock.svg create mode 100644 docs/assets/icons/postgresql.svg create mode 100644 docs/assets/icons/prometheus.svg create mode 100644 docs/assets/icons/queue.svg create mode 100644 docs/assets/icons/redis.svg create mode 100644 docs/assets/icons/server.svg create mode 100644 docs/assets/icons/shield.svg create mode 100644 docs/assets/icons/spring.svg create mode 100644 docs/coverage.md create mode 100644 docs/diagrams/cache-aside-workflow.d2 create mode 100644 docs/diagrams/deadlock-elimination.d2 create mode 100644 docs/diagrams/distributed-lock-sequence.d2 create mode 100644 docs/diagrams/idempotency-lifecycle.d2 create mode 100644 docs/diagrams/kubernetes-deployment-topology.d2 create mode 100644 docs/diagrams/security-filter-chain.d2 create mode 100644 docs/diagrams/system-overview.d2 create mode 100644 docs/diagrams/system-topology.d2 create mode 100644 docs/diagrams/transactional-outbox-flow.d2 create mode 100644 docs/diagrams/upi-validation-fallback.d2 create mode 100644 docs/diagrams/user-rate-limiting.d2 create mode 100644 docs/diagrams/zero-trust-lifecycle.d2 create mode 100644 docs/index.md create mode 100644 docs/javascripts/extra.js create mode 100644 docs/stylesheets/extra.css create mode 100644 mkdocs.yml create mode 100644 requirements.txt create mode 100755 scripts/generate-diagrams.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8484896..f67ef6a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,21 +52,40 @@ jobs: retention-days: 14 if-no-files-found: ignore - - name: Prepare Documentation and Coverage Site + - name: Install D2 Diagram Compiler + run: | + curl -fsSL https://d2lang.com/install.sh | sh -s -- + + - name: Verify D2 Diagrams & Detect Drift + run: | + chmod +x scripts/generate-diagrams.sh + ./scripts/generate-diagrams.sh --check + + - name: Set up Python for Documentation Build + uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'pip' + + - name: Install MkDocs Dependencies + run: | + pip install -r requirements.txt + + - name: Validate MkDocs Documentation Build (Strict Mode) + run: | + mkdocs build --strict + + - name: Package Documentation Portal & JaCoCo Coverage for GitHub Pages if: github.ref == 'refs/heads/main' && github.event_name == 'push' run: | - mkdir -p public/coverage - cp README.md public/ - cp CHANGELOG.md public/ - cp SECURITY.md public/ - cp -r docs public/ - cp -r docs-site/* public/ - cp -r target/site/jacoco/* public/coverage/ + mkdocs build -d public --strict + mkdir -p public/coverage-report + cp -r target/site/jacoco/* public/coverage-report/ touch public/.nojekyll python3 -c " import glob - nav_html = '''
← Back to Documentation PortalPayflow API — Automated JaCoCo Code Coverage
''' - for path in glob.glob('public/coverage/**/*.html', recursive=True): + nav_html = '''
← Back to Documentation Portal|📊 Coverage Quality Gates|Coverage Dashboard
Payflow API — Automated JaCoCo Code Coverage (90% Line / 73% Branch)
''' + for path in glob.glob('public/coverage-report/**/*.html', recursive=True): with open(path, 'r', encoding='utf-8') as f: content = f.read() if '> "$GITHUB_OUTPUT" + echo "Detected SVG changes." + else + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "No diagram changes detected." + fi + + - name: Commit and Push Updated Diagrams + if: steps.git-diff.outputs.changed == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add docs/assets/diagrams/ + git commit -m "docs(diagrams): regenerate D2 architecture diagrams [skip ci]" + git push diff --git a/.gitignore b/.gitignore index 38de85b..c795e7b 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,18 @@ build/ ### VS Code ### .vscode/ +### Python & Virtual Environments ### +.venv/ +venv/ +ENV/ +env/ +__pycache__/ +*.py[cod] + +### MkDocs & Documentation Build Artifacts ### +site/ +.cache/ + ### Internal docs (not committed) ### internal/ internal/* diff --git a/CHANGELOG.md b/CHANGELOG.md index 07b78cf..b64b3fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added - Documentation Modernization & D2 Engine Migration +- Replaced legacy Docsify single-page app with **Material for MkDocs** static site generator (`mkdocs-material` 9.5+) featuring instantaneous client search, dark/light theme switching, and responsive design. +- Redrew and modernized all 12 architectural topology, sequence, and data flow diagrams across `docs/ARCHITECTURE.md`, `README.md`, and `docs/index.md` using the declarative **D2 diagramming engine** (`docs/diagrams/*.d2`) with offline vector icons and Eclipse Layout Kernel (ELK) collision-free routing. +- Implemented **Option 1 Hybrid Pre-rendered SVG + Collapsible D2 Source** pattern: pre-rendered self-contained SVGs (`docs/assets/diagrams/*.svg`) with embedded `
📐 View Declarative D2 Diagram Source...` blocks, guaranteeing crisp, zero-flicker rendering across GitHub web, IDE previewers, and the documentation portal. +- Authored automated compilation script `scripts/generate-diagrams.sh` with dual light/dark theme compilation (`--theme=0 --dark-theme=200`) and `--check` drift detection. +- Configured enterprise CI/CD quality gates in `.github/workflows/ci.yml`: + - Enforced zero diagram drift on PR and push via `./scripts/generate-diagrams.sh --check`. + - Enforced strict documentation compilation on PR and push via `mkdocs build --strict`. + - Automated deployment of the documentation portal alongside JaCoCo test coverage reports to GitHub Pages on `main` merge. +- Created `.github/workflows/generate-diagrams.yml` enabling 1-click manual workflow dispatch to recompile D2 diagrams and automatically commit changes without requiring local D2 installation. +- Created `requirements.txt` specifying deterministic Python dependencies (`mkdocs>=1.6.1`, `mkdocs-material>=9.5.0`, `mkdocs-d2-plugin>=1.6.0`). +- Created offline SVG icon library under `docs/assets/icons/` (16 scalable vector icons) for zero-dependency compilation. +- Eliminated redundant documentation files and deduplicated governance markdowns (`CHANGELOG.md`, `CONTRIBUTING.md`, `SECURITY.md`, `LICENSE`) via single-source relative symlinks in `docs/`. +- Hardened `.gitignore` to prevent committing build artifacts (`site/`, `.cache/`, `.venv/`, Python bytecode). +- Corrected API endpoint routes and verified complete parity with Java 25 & Spring Boot 4.1.0 codebase (191 tests, 90% line / 73% branch coverage). + ### Added - Phase 11A (Multi-Stage Containerization & Full-Stack Docker Compose) - Created hardened multi-stage `Dockerfile`: - Stage 1: Build stage utilizing OpenJDK 25 (`eclipse-temurin:25-jdk`) and Maven layer caching. @@ -40,7 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Integrated `jacoco-maven-plugin:0.8.15` enforcing minimum 80% line coverage and 70% branch coverage across core business and security packages; achieved **90% line coverage** and **73% branch coverage** across 191 unit tests with 0 missed classes. - Created comprehensive unit test suites: `JwtAccessDeniedHandlerTest`, `JwtAuthenticationEntryPointTest`, `JwtAuthenticationFilterTest`, `SecurityUtilsTest`, and `IdempotencyCleanupServiceTest`. - Hardened `.github/workflows/ci.yml` executing `mvn clean verify -B` with automated artifact upload for JaCoCo coverage reports and SpotBugs analysis (14-day retention). -- Added automated GitHub Pages deployment for the Docsify public documentation portal (`https://shashankch.github.io/payflow-api/`) and live interactive JaCoCo coverage reports (`https://shashankch.github.io/payflow-api/coverage/`). +- Added automated GitHub Pages deployment for the Material for MkDocs public documentation portal (`https://shashankch.github.io/payflow-api/`), Code Coverage Quality Gate summary (`https://shashankch.github.io/payflow-api/coverage/`), and live interactive JaCoCo coverage reports (`https://shashankch.github.io/payflow-api/coverage-report/`). - Authored ADR-030 (*Automated CI/CD Quality Gates, JaCoCo Coverage Enforcement, and SpotBugs Static Analysis*). - Added `spring-modulith-events-kafka`, `spring-kafka`, `spring-kafka-test`, and Testcontainers `kafka` dependencies to `pom.xml`. - Marked domain event `TransferCompletedEvent` with `@Externalized` and configured programmatic dynamic routing via `EventExternalizationConfiguration` in `KafkaConfig.java`, routing to `${payflow.kafka.transfers-topic}` partitioned by `senderUpi` for strict chronological delivery per account. diff --git a/README.md b/README.md index 5f5ea28..14b5cd4 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ [![CI Build](https://img.shields.io/badge/CI-Passing-brightgreen?logo=githubactions&logoColor=white&style=flat-square)](https://github.com/shashankch/payflow-api/actions/workflows/ci.yml) [![Coverage](https://img.shields.io/badge/Coverage-90%25%20Line%20%7C%2073%25%20Branch-brightgreen?style=flat-square)](https://shashankch.github.io/payflow-api/coverage/) +[![Docs](https://img.shields.io/badge/Docs-MkDocs%20Material-526cfe?logo=materialformkdocs&logoColor=white&style=flat-square)](https://shashankch.github.io/payflow-api/) [![Java 25](https://img.shields.io/badge/Java-25-ED8B00?logo=openjdk&logoColor=white&style=flat-square)](https://dev.java/) [![Spring Boot](https://img.shields.io/badge/Spring%20Boot-4.1.0-6DB33F?logo=springboot&logoColor=white&style=flat-square)](https://spring.io/projects/spring-boot) [![Docker](https://img.shields.io/badge/Docker-Multi--stage%20Temurin%2025-2496ED?logo=docker&logoColor=white&style=flat-square)](Dockerfile) @@ -52,50 +53,91 @@ Payflow API evolves through a structured, 12-phase capability roadmap advancing ## 🏗️ System Architecture Overview -```mermaid -graph TD - subgraph ClientLayer["📱 Client & Interface Layer"] - Client["HTTP Client / Ingress"] -->|"POST /api/v1/transactions"| Filter["RequestIdFilter (MDC X-Request-Id)"] - Filter --> Controller["TransactionController (@Valid DTO)"] - end - - subgraph DomainLayer["🔒 Core Transaction Domain & Lock Manager"] - Controller -->|"sendMoney()"| TxService["TransactionService (@Transactional)"] - TxService --> LockOrder["Alphabetical Lock Ordering (Deadlock Avoidance)"] - LockOrder --> UserDomain["User Entity (Domain Invariants: debit / credit)"] - end - - subgraph PersistenceLayer["🗄️ Persistence & Double-Entry Ledger"] - UserDomain -->|"Pessimistic Lock (SELECT FOR UPDATE)"| UserRepo["UserRepository"] - TxService -->|"Append Immutable DEBIT & CREDIT Audit Entries"| LedgerRepo["BalanceLedgerRepository"] - UserRepo --> DB[("PostgreSQL 17 Database")] - LedgerRepo --> DB - end - - subgraph OutboxStreaming["⚡ Event Outbox & Kafka Streaming"] - TxService -->|"Atomic Outbox Publication"| Outbox[("Event Publication Registry")] - Outbox --> Kafka["Apache Kafka 3.9 (KRaft)"] - end - - subgraph ObservabilityStack["📊 Telemetry & Monitoring"] - Controller -.-> Prom["Prometheus (/actuator/prometheus)"] - Prom -.-> Grafana["Grafana Dashboards (Port 3000)"] - end - - classDef clientStyle fill:#1e293b,stroke:#475569,stroke-width:2px,color:#f8fafc; - classDef webStyle fill:#0f172a,stroke:#3b82f6,stroke-width:2px,color:#f8fafc; - classDef domainStyle fill:#1e1b4b,stroke:#6366f1,stroke-width:2px,color:#f8fafc; - classDef dbStyle fill:#064e3b,stroke:#10b981,stroke-width:2px,color:#f8fafc; - classDef streamStyle fill:#701a75,stroke:#d946ef,stroke-width:2px,color:#f8fafc; - classDef obsStyle fill:#14532d,stroke:#22c55e,stroke-width:2px,color:#f8fafc; - - class Client clientStyle; - class Filter,Controller webStyle; - class TxService,LockOrder,UserDomain domainStyle; - class UserRepo,LedgerRepo,DB dbStyle; - class Outbox,Kafka streamStyle; - class Prom,Grafana obsStyle; +![System Architecture Overview](docs/assets/diagrams/system-overview.svg) + +
+📐 View Declarative D2 Diagram Source + +```d2 +direction: down + +client: Client / Mobile App { + shape: rectangle + icon: "docs/assets/icons/client.svg" +} + +ingress: Ingress / API Gateway { + shape: rectangle + icon: "docs/assets/icons/gateway.svg" +} + +security: Spring Security (JWT Filter) { + shape: rectangle + icon: "docs/assets/icons/shield.svg" +} + +idemp: Idempotency Filter (SHA-256) { + shape: rectangle + icon: "docs/assets/icons/lock.svg" +} + +controller: TransactionController { + shape: rectangle + icon: "docs/assets/icons/spring.svg" +} + +service: TransactionService { + shape: rectangle + icon: "docs/assets/icons/java.svg" +} + +client -> ingress: "HTTP POST (Idempotency-Key)" +ingress -> security: "TLS 1.3" +security -> idemp: "Bearer JWT" +idemp -> controller: "Validated Request" +controller -> service: "sendMoney()" + +core_storage: Core Concurrency & Storage { + postgres: PostgreSQL 17 { + shape: cylinder + icon: "docs/assets/icons/postgresql.svg" + } + redis: Redis 7 (Redlock & Cache) { + shape: cylinder + icon: "docs/assets/icons/redis.svg" + } +} + +async_events: Asynchronous Messaging & Events { + outbox: Transactional Outbox { + shape: cylinder + icon: "docs/assets/icons/queue.svg" + } + kafka: Apache Kafka (KRaft) { + shape: queue + icon: "docs/assets/icons/kafka.svg" + } + outbox -> kafka: "spring-modulith-events-kafka" +} + +ai_resilience: AI & Resilience { + ai: Ollama Gen-AI { + shape: rectangle + icon: "docs/assets/icons/ai.svg" + } + r4j: Resilience4j { + shape: rectangle + icon: "docs/assets/icons/shield.svg" + } +} + +service -> core_storage.postgres: "Row Lock & Ledger Entries" +service -> core_storage.redis: "Redlock & Cache-Aside" +service -> async_events.outbox: "Atomically Persist Event" +service -> ai_resilience.ai: "Spend Insights" +service -> ai_resilience.r4j: "Rate Limiting Guard" ``` +
--- @@ -103,8 +145,9 @@ graph TD | Document | Description | | :--- | :--- | -| 🚀 **[Live Documentation Hub](https://shashankch.github.io/payflow-api/)** | Interactive Docsify portal with full-text search, dark mode, and dynamic markdown rendering | -| 📊 **[JaCoCo Code Coverage Report](https://shashankch.github.io/payflow-api/coverage/)** | Live interactive code coverage report (90% Line, 73% Branch) generated per-build | +| 🚀 **[Live Documentation Hub](https://shashankch.github.io/payflow-api/)** | Material for MkDocs documentation portal with full-text search, D2 diagram rendering, and dark mode | +| 📊 **[Code Coverage & Quality Gates](https://shashankch.github.io/payflow-api/coverage/)** | Automated CI/CD quality gates, bundle-level line/branch thresholds, and SpotBugs audit | +| 🚀 **[Interactive JaCoCo Report](https://shashankch.github.io/payflow-api/coverage-report/)** | Live interactive code coverage drilldown (90% Line, 73% Branch) generated per-build | | 📘 **[System Architecture](docs/ARCHITECTURE.md)** | Deep-dive concurrency models, pessimistic locking mechanics, test pyramid | | 🛡️ **[Security Architecture & Threat Model](docs/ARCHITECTURE.md#18-security-architecture-and-threat-model)** | Zero-Trust filter chain, STRIDE threat model, IAM policy matrix, financial concurrency controls | | 🔒 **[Security Policy](SECURITY.md)** | Open-source vulnerability reporting guidelines and project security posture | diff --git a/SECURITY.md b/SECURITY.md index 9e81dfd..cc51824 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,6 @@ # Security Policy — Payflow API -> **Project Status**: Payflow API is an open-source educational and reference architecture project licensed under the [MIT License](LICENSE). The project is currently under active development and is not deployed to production environments. +> **Project Status**: Payflow API is an open-source educational and reference architecture project licensed under the [MIT License](https://github.com/shashankch/payflow-api/blob/main/LICENSE). The project is currently under active development and is not deployed to production environments. --- @@ -8,7 +8,7 @@ Payflow API implements defense-in-depth security principles for financial ledger integrity, including Zero-Trust request validation, STRIDE threat mitigations, deterministic row locking, immutable double-entry balance ledgers, and SHA-256 payload verification. -For full architectural specifications, see [Security Architecture & Threat Model](docs/ARCHITECTURE.md#18-security-architecture-and-threat-model). +For full architectural specifications, see [Security Architecture & Threat Model](ARCHITECTURE.md#18-security-architecture-and-threat-model). --- diff --git a/docs-site/_navbar.md b/docs-site/_navbar.md deleted file mode 100644 index ed06d33..0000000 --- a/docs-site/_navbar.md +++ /dev/null @@ -1,6 +0,0 @@ -- [📘 Architecture](docs/ARCHITECTURE.md) -- [🌐 API Specs](docs/API_SPECIFICATION.md) -- [🗓️ Roadmap](docs/ROADMAP.md) -- [📜 ADRs](docs/adr/README.md) -- [📊 JaCoCo Coverage](https://shashankch.github.io/payflow-api/coverage/ ':target=_blank') -- [GitHub](https://github.com/shashankch/payflow-api) diff --git a/docs-site/_sidebar.md b/docs-site/_sidebar.md deleted file mode 100644 index 4c5901c..0000000 --- a/docs-site/_sidebar.md +++ /dev/null @@ -1,24 +0,0 @@ -- **Overview** - - [Home](README.md) - - [Changelog](CHANGELOG.md) - - [Security Policy](SECURITY.md) - -- **Architecture & Design** - - [System Architecture](docs/ARCHITECTURE.md) - - [Security Architecture](docs/ARCHITECTURE.md#18-security-architecture-and-threat-model) - - [Testing Strategy](docs/ARCHITECTURE.md#15-testing-strategy-rigor-concurrency--unit-verification) - - [Engineering Roadmap](docs/ROADMAP.md) - -- **API & Standards** - - [REST API Specification](docs/API_SPECIFICATION.md) - - [Engineering Conventions](docs/CONVENTIONS.md) - -- **Architecture Decisions (ADRs)** - - [ADR Index](docs/adr/README.md) - - [ADR-028: Multi-Stage Docker](docs/adr/0028-multi-stage-containerization-docker-compose.md) - - [ADR-029: Kubernetes Topology](docs/adr/0029-kubernetes-deployment-topology-hpa.md) - - [ADR-030: CI/CD Quality Gates](docs/adr/0030-ci-cd-quality-gates-jacoco-spotbugs.md) - -- **Code Quality & Telemetry** - - [📊 JaCoCo Coverage Report](https://shashankch.github.io/payflow-api/coverage/ ':target=_blank') - - [GitHub Actions CI](https://github.com/shashankch/payflow-api/actions/workflows/ci.yml) diff --git a/docs-site/index.html b/docs-site/index.html deleted file mode 100644 index 8fce5ed..0000000 --- a/docs-site/index.html +++ /dev/null @@ -1,260 +0,0 @@ - - - - - Payflow API — Documentation Hub - - - - - - - - - - -
Loading Payflow API Documentation...
- - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/API_SPECIFICATION.md b/docs/API_SPECIFICATION.md index 95b294a..c10d984 100644 --- a/docs/API_SPECIFICATION.md +++ b/docs/API_SPECIFICATION.md @@ -64,23 +64,36 @@ Authenticates a registered user by UPI ID and issues a cryptographically signed - **Request Body DTO (`LoginRequest`)**: - `upiId`: String, required (`@NotBlank`), valid UPI format (`@Pattern`). -#### Request Example -```json -{ - "upiId": "alice@payflow" -} -``` +=== "cURL Example" + ```bash + curl -X POST https://api.payflow.com/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{ + "upiId": "alice@payflow" + }' + ``` + +=== "Request Payload" + ```json + { + "upiId": "alice@payflow" + } + ``` -#### Response Example (`200 OK`) -```json -{ - "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "tokenType": "Bearer", - "expiresIn": 3600, - "upiId": "alice@payflow", - "referenceId": "550e8400-e29b-41d4-a716-446655440000" -} -``` +=== "Response (200 OK)" + ```json + { + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "tokenType": "Bearer", + "expiresIn": 3600, + "upiId": "alice@payflow", + "referenceId": "550e8400-e29b-41d4-a716-446655440000" + } + ``` + +=== "Error Responses" + - `401 Unauthorized`: Invalid UPI ID or user account does not exist. + - `422 Unprocessable Entity`: UPI ID format validation failure. --- @@ -96,33 +109,45 @@ Registers a new client profile with an initial balance. - `phoneNumber`: String, required (`@NotBlank`), exactly 10 digits (`@Pattern(regexp = "^\\d{10}$")`). - `balance`: BigDecimal, required (`@NotNull`), non-negative (`@DecimalMin("0.0")`), denominated in Indian Rupees (INR, ₹). -#### Request Example -```json -{ - "name": "Aarav Sharma", - "upiId": "aarav@upi", - "phoneNumber": "9876543210", - "balance": 1000.00 -} -``` +=== "cURL Example" + ```bash + curl -X POST https://api.payflow.com/api/v1/users \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Aarav Sharma", + "upiId": "aarav@upi", + "phoneNumber": "9876543210", + "balance": 1000.00 + }' + ``` + +=== "Request Payload" + ```json + { + "name": "Aarav Sharma", + "upiId": "aarav@upi", + "phoneNumber": "9876543210", + "balance": 1000.00 + } + ``` -#### Response Example (`201 Created`) -Headers: `Location: /api/v1/users/a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d` -```json -{ - "referenceId": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d", - "name": "Aarav Sharma", - "upiId": "aarav@upi", - "phoneNumber": "9876543210", - "balance": 1000.0000, - "createdAt": "2026-08-01T16:00:00Z", - "updatedAt": "2026-08-01T16:00:00Z" -} -``` +=== "Response (201 Created)" + *Headers: `Location: /api/v1/users/a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d`* + ```json + { + "referenceId": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d", + "name": "Aarav Sharma", + "upiId": "aarav@upi", + "phoneNumber": "9876543210", + "balance": 1000.0000, + "createdAt": "2026-08-01T16:00:00Z", + "updatedAt": "2026-08-01T16:00:00Z" + } + ``` -#### Error Responses -- `409 Conflict`: User with the requested UPI ID already exists (`DuplicateUpiIdException`). -- `422 Unprocessable Entity`: Input validation failure, or external UPI verification rejected the UPI ID (`InvalidUpiException`, type: `https://api.payflow.com/errors/invalid-upi-id`). +=== "Error Responses" + - `409 Conflict`: User with the requested UPI ID already exists (`DuplicateUpiIdException`). + - `422 Unprocessable Entity`: Input validation failure, or external UPI verification rejected the UPI ID (`InvalidUpiException`, type: `https://api.payflow.com/errors/invalid-upi-id`). --- @@ -137,33 +162,39 @@ Retrieves a paginated list of registered users. Requires administrative privileg - `size`: Integer, optional. Page size (`@Min(1) @Max(100)`). Default: `10`. - `sortBy`: String, optional. Column name to sort. Default: `userId`. -#### Response Example (`200 OK`) -```json -{ - "content": [ +=== "cURL Example" + ```bash + curl -X GET "https://api.payflow.com/api/v1/users?page=0&size=10&sortBy=userId" \ + -H "Authorization: Bearer " + ``` + +=== "Response (200 OK)" + ```json { - "referenceId": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d", - "name": "Aarav Sharma", - "upiId": "aarav@upi", - "phoneNumber": "9876543210", - "balance": 1000.0000, - "createdAt": "2026-08-01T16:00:00Z", - "updatedAt": "2026-08-01T16:00:00Z" + "content": [ + { + "referenceId": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d", + "name": "Aarav Sharma", + "upiId": "aarav@upi", + "phoneNumber": "9876543210", + "balance": 1000.0000, + "createdAt": "2026-08-01T16:00:00Z", + "updatedAt": "2026-08-01T16:00:00Z" + } + ], + "page": 0, + "size": 10, + "totalElements": 1, + "totalPages": 1, + "first": true, + "last": true } - ], - "page": 0, - "size": 10, - "totalElements": 1, - "totalPages": 1, - "first": true, - "last": true -} -``` + ``` -#### Error Responses -- `401 Unauthorized`: Missing, expired, or invalid JWT token. -- `403 Forbidden`: Caller lacks administrative role privileges (`ForbiddenOperationException`, type: `https://api.payflow.com/errors/forbidden-operation`). -- `422 Unprocessable Entity`: Query parameter validation failure (e.g. `size < 1` or `size > 100`). +=== "Error Responses" + - `401 Unauthorized`: Missing, expired, or invalid JWT token. + - `403 Forbidden`: Caller lacks administrative role privileges (`ForbiddenOperationException`, type: `https://api.payflow.com/errors/forbidden-operation`). + - `422 Unprocessable Entity`: Query parameter validation failure (e.g. `size < 1` or `size > 100`). --- @@ -174,19 +205,29 @@ Fetches a single user record by their unique UUID reference ID. - **Path**: `/api/v1/users/{id}` - **Authentication**: `Authorization: Bearer ` (User can only inspect their own profile) -#### Response Example (`200 OK`) -```json -{ - "referenceId": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d", - "name": "Aarav Sharma", - "upiId": "aarav@upi", - "phoneNumber": "9876543210", - "balance": 1000.0000, - "createdAt": "2026-08-01T16:00:00Z", - "updatedAt": "2026-08-01T16:00:00Z" -} -``` -*If not found, returns `404 Not Found`.* +=== "cURL Example" + ```bash + curl -X GET https://api.payflow.com/api/v1/users/a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d \ + -H "Authorization: Bearer " + ``` + +=== "Response (200 OK)" + ```json + { + "referenceId": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d", + "name": "Aarav Sharma", + "upiId": "aarav@upi", + "phoneNumber": "9876543210", + "balance": 1000.0000, + "createdAt": "2026-08-01T16:00:00Z", + "updatedAt": "2026-08-01T16:00:00Z" + } + ``` + +=== "Error Responses" + - `401 Unauthorized`: Missing or invalid JWT bearer token. + - `403 Forbidden`: Authenticated user cannot view another user's profile. + - `404 Not Found`: User reference ID not found. --- @@ -197,20 +238,29 @@ Fetches a single user record by their unique UPI ID. - **Path**: `/api/v1/users/upi/{upiId}` - **Authentication**: `Authorization: Bearer ` (User can only inspect their own profile) -#### Response Example (`200 OK`) -```json -{ - "userId": 2, - "name": "Priya Patel", - "upiId": "priya@upi", - "phoneNumber": "9876543211", - "balance": 50.0000, - "version": 0, - "createdAt": "2026-08-01T16:00:00Z", - "updatedAt": "2026-08-01T16:00:00Z" -} -``` -*If not found, returns `404 Not Found`.* +=== "cURL Example" + ```bash + curl -X GET https://api.payflow.com/api/v1/users/upi/priya@upi \ + -H "Authorization: Bearer " + ``` + +=== "Response (200 OK)" + ```json + { + "userId": 2, + "name": "Priya Patel", + "upiId": "priya@upi", + "phoneNumber": "9876543211", + "balance": 50.0000, + "version": 0, + "createdAt": "2026-08-01T16:00:00Z", + "updatedAt": "2026-08-01T16:00:00Z" + } + ``` + +=== "Error Responses" + - `401 Unauthorized`: Missing or invalid JWT bearer token. + - `404 Not Found`: User UPI ID not found. --- @@ -224,30 +274,40 @@ Retrieves paginated double-entry balance ledger audit entries for a user by UUID - `size`: Integer, optional (default `10`), min `1`, max `100`. - **Authentication**: `Authorization: Bearer ` (User can only view their own ledger) -#### Response Example (`200 OK`) -```json -{ - "content": [ +=== "cURL Example" + ```bash + curl -X GET "https://api.payflow.com/api/v1/users/a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d/ledger?page=0&size=10" \ + -H "Authorization: Bearer " + ``` + +=== "Response (200 OK)" + ```json { - "ledgerId": 101, - "userReferenceId": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d", - "transactionReferenceId": "f9e8d7c6-b5a4-3f2e-1d0c-9b8a7f6e5d4c", - "entryType": "DEBIT", - "amount": 100.0000, - "balanceBefore": 500.0000, - "balanceAfter": 400.0000, - "createdAt": "2026-08-09T14:00:00Z" + "content": [ + { + "ledgerId": 101, + "userReferenceId": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d", + "transactionReferenceId": "f9e8d7c6-b5a4-3f2e-1d0c-9b8a7f6e5d4c", + "entryType": "DEBIT", + "amount": 100.0000, + "balanceBefore": 500.0000, + "balanceAfter": 400.0000, + "createdAt": "2026-08-09T14:00:00Z" + } + ], + "page": 0, + "size": 10, + "totalElements": 1, + "totalPages": 1, + "first": true, + "last": true } - ], - "page": 0, - "size": 10, - "totalElements": 1, - "totalPages": 1, - "first": true, - "last": true -} -``` -*If user not found, returns `404 Not Found`.* + ``` + +=== "Error Responses" + - `401 Unauthorized`: Missing or invalid JWT bearer token. + - `403 Forbidden`: Authenticated user not authorized to inspect this ledger. + - `404 Not Found`: User not found. --- @@ -260,25 +320,31 @@ Retrieves a list of users whose balance exceeds the specified minimum threshold. - **Path Variables**: - `amount`: BigDecimal, required. Minimum balance threshold denominated in INR (₹). -#### Response Example (`200 OK`) -```json -[ - { - "referenceId": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d", - "name": "Aarav Sharma", - "upiId": "aarav@payflow", - "phoneNumber": "9876543210", - "balance": 1000.0000, - "createdAt": "2026-08-01T16:00:00Z", - "updatedAt": "2026-08-01T16:00:00Z" - } -] -``` - -#### Error Responses -- `400 Bad Request`: Parameter type mismatch (e.g. non-numeric amount string). -- `401 Unauthorized`: Missing, expired, or invalid JWT token. -- `403 Forbidden`: Caller lacks administrative role privileges (`ForbiddenOperationException`, type: `https://api.payflow.com/errors/forbidden-operation`). +=== "cURL Example" + ```bash + curl -X GET https://api.payflow.com/api/v1/users/balance/500.00 \ + -H "Authorization: Bearer " + ``` + +=== "Response (200 OK)" + ```json + [ + { + "referenceId": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d", + "name": "Aarav Sharma", + "upiId": "aarav@payflow", + "phoneNumber": "9876543210", + "balance": 1000.0000, + "createdAt": "2026-08-01T16:00:00Z", + "updatedAt": "2026-08-01T16:00:00Z" + } + ] + ``` + +=== "Error Responses" + - `400 Bad Request`: Parameter type mismatch (non-numeric amount). + - `401 Unauthorized`: Missing or invalid JWT token. + - `403 Forbidden`: Caller lacks administrative role privileges. --- @@ -296,45 +362,60 @@ Executes a peer-to-peer fund transfer request with guaranteed exactly-once idemp - `amount`: BigDecimal, required (`@NotNull`), minimum ₹0.01 (`@DecimalMin("0.01")`), maximum ₹10,00,000 (`@DecimalMax("1000000.00")`), denominated in Indian Rupees (INR, ₹). - `note`: String, optional, max 255 characters (`@Size(max = 255)`). -#### Request Example -```http -POST /api/v1/transactions HTTP/1.1 -Host: api.payflow.com -Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d -Content-Type: application/json +=== "cURL Example" + ```bash + curl -X POST https://api.payflow.com/api/v1/transactions \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -H "Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d" \ + -d '{ + "senderUpiId": "aarav@upi", + "receiverUpiId": "priya@upi", + "amount": 150.00, + "note": "Dinner bill split" + }' + ``` + +=== "HTTP Request" + ```http + POST /api/v1/transactions HTTP/1.1 + Host: api.payflow.com + Authorization: Bearer + Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d + Content-Type: application/json -{ - "senderUpiId": "aarav@upi", - "receiverUpiId": "priya@upi", - "amount": 150.00, - "note": "Dinner bill split" -} -``` + { + "senderUpiId": "aarav@upi", + "receiverUpiId": "priya@upi", + "amount": 150.00, + "note": "Dinner bill split" + } + ``` -#### Response Example (`201 Created`) -Headers: `Location: /api/v1/transactions/550e8400-e29b-41d4-a716-446655440000` -```json -{ - "transactionId": 1, - "referenceId": "550e8400-e29b-41d4-a716-446655440000", - "senderUpiId": "aarav@upi", - "receiverUpiId": "priya@upi", - "amount": 150.0000, - "status": "COMPLETED", - "type": "TRANSFER", - "note": "Dinner bill split", - "createdAt": "2026-08-01T16:05:00Z" -} -``` +=== "Response (201 Created)" + *Headers: `Location: /api/v1/transactions/550e8400-e29b-41d4-a716-446655440000`* + ```json + { + "transactionId": 1, + "referenceId": "550e8400-e29b-41d4-a716-446655440000", + "senderUpiId": "aarav@upi", + "receiverUpiId": "priya@upi", + "amount": 150.0000, + "status": "COMPLETED", + "type": "TRANSFER", + "note": "Dinner bill split", + "createdAt": "2026-08-01T16:05:00Z" + } + ``` -#### Error Responses -- `400 Bad Request`: Missing `Idempotency-Key` header, or key reused with a mismatched payload. -- `401 Unauthorized`: Missing or invalid JWT bearer token. -- `403 Forbidden`: Authenticated user is not authorized to transfer from requested sender UPI. -- `409 Conflict`: A request with the same `Idempotency-Key` is currently in-flight. -- `422 Unprocessable Entity`: Validation constraint failure or insufficient sender balance. -- `429 Too Many Requests`: Per-user rate limit exceeded (maximum 10 requests per second; response includes `Retry-After: 1` header). -- `503 Service Unavailable`: Downstream dependency circuit breaker is open (`upiValidation`) or external provider failure. +=== "Error Responses" + - `400 Bad Request`: Missing `Idempotency-Key` header, or key reused with a mismatched payload. + - `401 Unauthorized`: Missing or invalid JWT bearer token. + - `403 Forbidden`: Authenticated user is not authorized to transfer from requested sender UPI. + - `409 Conflict`: A request with the same `Idempotency-Key` is currently in-flight. + - `422 Unprocessable Entity`: Validation constraint failure or insufficient sender balance. + - `429 Too Many Requests`: Per-user rate limit exceeded (maximum 10 requests per second; response includes `Retry-After: 1` header). + - `503 Service Unavailable`: Downstream dependency circuit breaker is open (`upiValidation`) or external provider failure. --- @@ -345,20 +426,31 @@ Fetches details of a single transaction by its unique UUID reference ID. - **Path**: `/api/v1/transactions/{id}` - **Authentication**: `Authorization: Bearer ` (Only sender or receiver participants can view) -#### Response Example (`200 OK`) -```json -{ - "transactionId": 1, - "referenceId": "550e8400-e29b-41d4-a716-446655440000", - "senderUpiId": "aarav@upi", - "receiverUpiId": "priya@upi", - "amount": 150.0000, - "status": "COMPLETED", - "type": "TRANSFER", - "note": "Dinner bill split", - "createdAt": "2026-08-01T16:05:00Z" -} -``` +=== "cURL Example" + ```bash + curl -X GET https://api.payflow.com/api/v1/transactions/550e8400-e29b-41d4-a716-446655440000 \ + -H "Authorization: Bearer " + ``` + +=== "Response (200 OK)" + ```json + { + "transactionId": 1, + "referenceId": "550e8400-e29b-41d4-a716-446655440000", + "senderUpiId": "aarav@upi", + "receiverUpiId": "priya@upi", + "amount": 150.0000, + "status": "COMPLETED", + "type": "TRANSFER", + "note": "Dinner bill split", + "createdAt": "2026-08-01T16:05:00Z" + } + ``` + +=== "Error Responses" + - `401 Unauthorized`: Missing or invalid JWT bearer token. + - `403 Forbidden`: Authenticated principal was neither the sender nor the receiver. + - `404 Not Found`: Transaction reference ID does not exist. --- @@ -366,38 +458,51 @@ Fetches details of a single transaction by its unique UUID reference ID. Retrieves a paginated list of all transactions where the specified UPI ID is either the sender or receiver. - **HTTP Method**: `GET` -- **Path**: `/api/v1/transactions` +- **Path**: `/api/v1/transactions/user/{upiId}` - **Authentication**: `Authorization: Bearer ` +- **Path Variables**: + - `upiId`: String, required. Registered UPI handle (e.g. `aarav@upi`). Must match authenticated principal or caller must be a participant. - **Query Parameters**: - - `upiId`: String, required. UPI ID to filter transactions. - `page`: Integer, optional. Page index (0-based, `@Min(0)`). Default: `0`. - `size`: Integer, optional. Page size (`@Min(1) @Max(100)`). Default: `10`. - `sortBy`: String, optional. Column name to sort. Default: `createdAt`. -#### Response Example (`200 OK`) -```json -{ - "content": [ +=== "cURL Example" + ```bash + curl -X GET "https://api.payflow.com/api/v1/transactions/user/aarav@upi?page=0&size=10" \ + -H "Authorization: Bearer " + ``` + +=== "Response (200 OK)" + ```json { - "transactionId": 1, - "referenceId": "550e8400-e29b-41d4-a716-446655440000", - "senderUpiId": "aarav@upi", - "receiverUpiId": "priya@upi", - "amount": 150.0000, - "status": "COMPLETED", - "type": "TRANSFER", - "note": "Dinner bill split", - "createdAt": "2026-08-01T16:05:00Z" + "content": [ + { + "transactionId": 1, + "referenceId": "550e8400-e29b-41d4-a716-446655440000", + "senderUpiId": "aarav@upi", + "receiverUpiId": "priya@upi", + "amount": 150.0000, + "status": "COMPLETED", + "type": "TRANSFER", + "note": "Dinner bill split", + "createdAt": "2026-08-01T16:05:00Z" + } + ], + "page": 0, + "size": 10, + "totalElements": 1, + "totalPages": 1, + "first": true, + "last": true } - ], - "page": 0, - "size": 10, - "totalElements": 1, - "totalPages": 1, - "first": true, - "last": true -} -``` + ``` + +=== "Error Responses" + - `401 Unauthorized`: Missing or invalid JWT bearer token. + - `403 Forbidden`: Authenticated principal is not authorized to view transactions for this UPI ID. + +--- ### 10. Generate Spend Insights for Transaction Analyzes transaction metadata and recipient UPI handle using Spring AI structured prompt engineering to classify the expenditure into personal finance categories and generate actionable budget insights. @@ -417,26 +522,32 @@ Analyzes transaction metadata and recipient UPI handle using Spring AI structure - `confidenceScore`: Double (0.0 to 1.0) - `source`: String (`AI_MODEL` or `RULE_BASED_FALLBACK`) -#### Response Example (`200 OK`) -```json -{ - "transactionReferenceId": "550e8400-e29b-41d4-a716-446655440000", - "category": "FOOD_AND_DINING", - "amount": 450.00, - "summary": "Dining or grocery expenditure with swiggy@upi.", - "budgetingTip": "Set a dedicated dining out ceiling to optimize discretionary spending.", - "confidenceScore": 0.94, - "source": "AI_MODEL" -} -``` +=== "cURL Example" + ```bash + curl -X POST https://api.payflow.com/api/v1/transactions/550e8400-e29b-41d4-a716-446655440000/insights \ + -H "Authorization: Bearer " + ``` + +=== "Response (200 OK)" + ```json + { + "transactionReferenceId": "550e8400-e29b-41d4-a716-446655440000", + "category": "FOOD_AND_DINING", + "amount": 450.00, + "summary": "Dining or grocery expenditure with swiggy@upi.", + "budgetingTip": "Set a dedicated dining out ceiling to optimize discretionary spending.", + "confidenceScore": 0.94, + "source": "AI_MODEL" + } + ``` -#### Fault-Tolerant Behavior -- Downstream LLM timeouts, rate limits, or outages trip Resilience4j `aiCircuitBreaker`, automatically executing `ruleBasedFallback` with deterministic keyword heuristic categorization (`source: "RULE_BASED_FALLBACK"`), guaranteeing zero API downtime for end users. +=== "Fault Tolerance" + - Downstream LLM timeouts, rate limits, or outages trip Resilience4j `aiCircuitBreaker`, automatically executing `ruleBasedFallback` with deterministic keyword heuristic categorization (`source: "RULE_BASED_FALLBACK"`), guaranteeing zero API downtime for end users. -#### Error Responses -- `403 Forbidden`: Authenticated user is neither the sender nor receiver of this transaction. -- `404 Not Found`: Transaction reference ID does not exist. -- `503 Service Unavailable`: Gen-AI Spend Insights feature is disabled (`payflow.ai.enabled: false`). +=== "Error Responses" + - `403 Forbidden`: Authenticated user is neither the sender nor receiver of this transaction. + - `404 Not Found`: Transaction reference ID does not exist. + - `503 Service Unavailable`: Gen-AI Spend Insights feature is disabled (`payflow.ai.enabled: false`). --- diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f95e2b4..8a21d50 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -7,7 +7,7 @@ > - **Created Date**: 2026-08-01 > - **Last Updated**: 2026-09-30 > - **Authoritative Location**: [ARCHITECTURE.md](ARCHITECTURE.md) -> - **Related Documents**: [API Specification](API_SPECIFICATION.md) | [Security Policy](../SECURITY.md) | [Architecture Decisions (ADRs)](adr/README.md) | [Phased Roadmap](ROADMAP.md) | [Engineering Conventions](CONVENTIONS.md) +> - **Related Documents**: [API Specification](API_SPECIFICATION.md) | [Security Policy](SECURITY.md) | [Architecture Decisions (ADRs)](adr/README.md) | [Phased Roadmap](ROADMAP.md) | [Engineering Conventions](CONVENTIONS.md) --- @@ -69,39 +69,113 @@ Payflow solves these challenges through: The diagram below maps the target environment topology, showcasing both the synchronous request-response flow and the asynchronous event-driven pipelines. -```mermaid -graph TD - %% Clients and Gateway - Client["Client App"] -- "HTTP POST (with Idempotency-Key)" --> Gateway["API Gateway (Spring Security / JWT / v1 Versioning)"] - - subgraph "Kubernetes Pod / Spring Boot Container" - Gateway --> Controller["UserController / TransactionController"] - Controller --> Service["TransactionService / UserService / AIService"] - Service --> Locking["Pessimistic Lock / Redis Distributed Lock"] - Service --> Idempotency["Idempotency Filter & Registry"] - Service --> EventPub["ApplicationEventPublisher"] - EventPub --> ModulithLog["Spring Modulith Event Publication Log"] - ModulithLog --> EventListener["@ApplicationModuleListener"] - - %% Observability - Tracing["Micrometer Tracing + OTel Bridge"] -.-> TraceOut["W3C traceparent propagation"] - Logging["MDC Logger (Structured JSON)"] -.-> LogOut["traceId / spanId / requestId"] - end - - %% Databases & Cache - Locking -- "SELECT ... FOR UPDATE" --> PostgresDB[("PostgreSQL DB")] - Idempotency -- "Check / Save State" --> PostgresDB - ModulithLog -- "Atomic write in same TX" --> PostgresDB - - Service -- "Cache-Aside / Distributed Lock" --> Redis[("Redis Cache & Distributed Lock (Redisson)")] - - %% Kafka bridge (Phase 9A) - ModulithLog -- "spring-modulith-events-kafka" --> Kafka["Apache Kafka Broker (KRaft Mode)"] - - %% Observers - Prometheus["Prometheus Server"] -.->|"/actuator/prometheus"| Controller - Grafana["Grafana Dashboards"] -.-> Prometheus +![System Topology & Core Flow](assets/diagrams/system-topology.svg) + +
+📐 View Declarative D2 Diagram Source + +```d2 +direction: down + +client: Client App { + shape: rectangle + icon: "docs/assets/icons/client.svg" +} + +gateway: API Gateway / Ingress { + shape: rectangle + icon: "docs/assets/icons/gateway.svg" + tooltip: "Spring Security / JWT / v1 Versioning" +} + +client -> gateway: "HTTP POST (Idempotency-Key)" + +k8s: Kubernetes Pod / Spring Boot Container { + controllers: REST Controllers { + shape: rectangle + icon: "docs/assets/icons/spring.svg" + label: "UserController / TransactionController" + } + + services: Domain Services { + shape: rectangle + icon: "docs/assets/icons/java.svg" + label: "TransactionService / UserService / SpendInsightsService" + } + + concurrency: Concurrency & Idempotency { + shape: rectangle + icon: "docs/assets/icons/lock.svg" + label: "Pessimistic Lock / Redisson Lock / Idempotency" + } + + outbox: Spring Modulith Outbox { + shape: rectangle + icon: "docs/assets/icons/queue.svg" + label: "ApplicationEventPublisher -> event_publication" + } + + telemetry: Observability Pipeline { + shape: rectangle + icon: "docs/assets/icons/shield.svg" + label: "OTel Tracing + Structured MDC Logging" + } + + controllers -> services: "DTO Delegation" + services -> concurrency: "Lock & Idempotency" + services -> outbox: "Outbox Publisher" +} + +gateway -> k8s.controllers: "Bearer JWT Authentication" + +stores: Persistence & Streaming Tier { + postgres: PostgreSQL 17 Database { + shape: cylinder + icon: "docs/assets/icons/postgresql.svg" + label: "PostgreSQL 17\n(Ledger, Transactions, Locks)" + } + + redis: Redis 7 Cluster { + shape: cylinder + icon: "docs/assets/icons/redis.svg" + label: "Redis 7 (Redlock & Cache-Aside)" + } + + kafka: Apache Kafka (KRaft) { + shape: queue + icon: "docs/assets/icons/kafka.svg" + label: "Apache Kafka Broker\n(topic: payflow.transfers)" + } + + ai_engine: Ollama Gen-AI { + shape: rectangle + icon: "docs/assets/icons/ai.svg" + label: "Ollama Gen-AI (Spend Insights)" + } +} + +monitoring: Metrics & Telemetry { + prometheus: Prometheus Server { + shape: rectangle + icon: "docs/assets/icons/prometheus.svg" + } + grafana: Grafana Dashboards { + shape: rectangle + icon: "docs/assets/icons/grafana.svg" + } + grafana -> prometheus: "PromQL Queries" +} + +k8s.concurrency -> stores.postgres: "SELECT ... FOR UPDATE" +k8s.concurrency -> stores.redis: "Redlock RLock (2s wait, 10s lease)" +k8s.services -> stores.redis: "Cache-Aside (users, user_ledgers)" +k8s.services -> stores.ai_engine: "Structured LLM Prompt" +k8s.outbox -> stores.postgres: "Atomic Outbox Write" +k8s.outbox -> stores.kafka: "spring-modulith-events-kafka" + +monitoring.prometheus -> k8s.controllers: "Scrape /actuator/prometheus" ``` +
--- @@ -136,39 +210,50 @@ Optional findByUpiIdWithLock(@Param("upiId") String upiId); ### B. Distributed Locking (Cross-Service & Multi-Instance Coordination) When payment requests enter a distributed, multi-pod Kubernetes cluster, duplicate submissions with identical `Idempotency-Key` headers can reach different application instances concurrently. To intercept race conditions at the API gateway layer before database connection pool allocation or transactional row locking, Payflow API implements a **Redis Distributed Lock (Redlock)** using **Redisson** (`RLock`): -```mermaid -sequenceDiagram - autonumber - actor Client as API Client / Ingress - participant Pod1 as Pod 1 (IdempotencyFilter) - participant Pod2 as Pod 2 (IdempotencyFilter) - participant Redis as Redis Distributed Lock (Redisson) - participant DB as PostgreSQL Database - - Client->>Pod1: POST /api/v1/transactions (Key: tx-123) - Client->>Pod2: POST /api/v1/transactions (Key: tx-123 [Duplicate]) - - par Pod 1 tries lock - Pod1->>Redis: tryLock("payflow:lock:idemp:tx-123", wait=2s, lease=10s) - Redis-->>Pod1: true (Lock Acquired) - and Pod 2 tries lock - Pod2->>Redis: tryLock("payflow:lock:idemp:tx-123", wait=2s, lease=10s) - Note over Pod2,Redis: Pod 2 blocks waiting (up to 2s) - end - - Pod1->>DB: Check & record PROCESSING status - Pod1->>DB: Execute transaction & commit - Pod1->>DB: Record SUCCESS with response body - Pod1->>Redis: unlock("payflow:lock:idemp:tx-123") - Redis-->>Pod1: Lock Released - Pod1-->>Client: 201 Created (Transaction Response) - - Redis-->>Pod2: Lock Acquired (after Pod 1 release) - Pod2->>DB: Find existing idempotency record - Note over Pod2,DB: Status is SUCCESS - Pod2-->>Client: 201 Created (Replay Cached Response) - Pod2->>Redis: unlock("payflow:lock:idemp:tx-123") +![Redisson Distributed Locking Sequence](assets/diagrams/distributed-lock-sequence.svg) + +
+📐 View Declarative D2 Diagram Source + +```d2 +shape: sequence_diagram + +client: API Client / Ingress { + icon: "docs/assets/icons/client.svg" +} +pod1: Pod 1 (IdempotencyFilter) { + icon: "docs/assets/icons/server.svg" +} +pod2: Pod 2 (IdempotencyFilter) { + icon: "docs/assets/icons/server.svg" +} +redis: Redis Lock (Redisson) { + icon: "docs/assets/icons/redis.svg" +} +db: PostgreSQL Database { + icon: "docs/assets/icons/postgresql.svg" +} + +client -> pod1: "1. POST /api/v1/transactions (Key: tx-123)" +client -> pod2: "2. POST /api/v1/transactions (Key: tx-123 [Duplicate])" + +pod1 -> redis: "3. tryLock(\"payflow:lock:idemp:tx-123\", wait=2s, lease=10s)" +redis -> pod1: "4. Lock Acquired (true)" +pod2 -> redis: "5. tryLock(\"payflow:lock:idemp:tx-123\", wait=2s, lease=10s) [Blocks up to 2s]" + +pod1 -> db: "6. Check & record PROCESSING status" +pod1 -> db: "7. Execute transaction & atomic commit" +pod1 -> db: "8. Record SUCCESS with response body" +pod1 -> redis: "9. unlock(\"payflow:lock:idemp:tx-123\")" +redis -> pod1: "10. Lock Released" +pod1 -> client: "11. 201 Created (Transaction Response)" + +redis -> pod2: "12. Lock Acquired (after Pod 1 release)" +pod2 -> db: "13. SELECT idempotency record (Status: SUCCESS)" +pod2 -> client: "14. 201 Created (Replay Cached Response)" +pod2 -> redis: "15. unlock(\"payflow:lock:idemp:tx-123\")" ``` +
#### Key Design Characteristics: 1. **Profile-Conditional Architecture**: @@ -303,44 +388,46 @@ CREATE INDEX idx_idemp_created ON idempotency_registry(created_at); ### Idempotency Filter Lifecycle Flow -```mermaid -sequenceDiagram - autonumber - actor Client as 📱 HTTP Client - participant Filter as 🛡️ IdempotencyFilter - participant DB as 🗄️ PostgreSQL (idempotency_registry) - participant Engine as 🔒 TransactionService - - Client->>Filter: POST /api/v1/transactions (Header: Idempotency-Key) - alt Missing Idempotency-Key - Filter-->>Client: 400 Bad Request (RFC 7807: Missing Required Header) - else Key Present - Filter->>Filter: Compute SHA-256(requestBody) - Filter->>DB: SELECT * FROM idempotency_registry WHERE idempotency_key = ? - alt Key Exists & Different Hash - Filter-->>Client: 400 Bad Request (RFC 7807: Key Reuse with Different Payload) - else Key Exists & Status is PROCESSING / INITIATED - alt Lease Expired (> 2 minutes) - Filter->>DB: UPDATE idempotency_registry SET updated_at = NOW() - Filter->>Engine: Allow Retry Execution -> sendMoney() - else Lease Active (<= 2 minutes) - Filter-->>Client: 409 Conflict (Request Currently In-Flight) - end - else Key Exists & Status is SUCCESS - Filter-->>Client: Replay Cached HTTP Response (Code & JSON Body) - else Key Not Found - alt Concurrent Insert Collision (DataIntegrityViolationException) - Filter-->>Client: 409 Conflict (Concurrent Request Being Processed) - else Successful Insert - Filter->>DB: INSERT INTO idempotency_registry (key, hash, status=PROCESSING) - Filter->>Engine: doFilterInternal() -> sendMoney() - Engine-->>Filter: HTTP 201 Created (TransactionResponse) - Filter->>DB: UPDATE idempotency_registry SET status=SUCCESS, response_code=201, response_body=... - Filter-->>Client: HTTP 201 Created (Original Response) - end - end - end +![Idempotency Filter Lifecycle Flow](assets/diagrams/idempotency-lifecycle.svg) + +
+📐 View Declarative D2 Diagram Source + +```d2 +shape: sequence_diagram + +client: HTTP Client { + icon: "docs/assets/icons/client.svg" +} +filter: IdempotencyFilter { + icon: "docs/assets/icons/lock.svg" +} +db: PostgreSQL (idempotency_registry) { + icon: "docs/assets/icons/postgresql.svg" +} +service: TransactionService { + icon: "docs/assets/icons/spring.svg" +} + +client -> filter: "1. POST /api/v1/transactions (Header: Idempotency-Key)" +filter -> filter: "2. Validate key regex & compute SHA-256(body)" +filter -> db: "3. SELECT * FROM idempotency_registry WHERE key = ?" + +filter -> db: "4. (Case: Key Not Found) INSERT key, hash, status=PROCESSING" +filter -> service: "5. doFilterInternal() -> sendMoney(request)" +service -> filter: "6. HTTP 201 Created (TransactionResponse)" +filter -> db: "7. UPDATE status=SUCCESS, code=201, body=..." +filter -> client: "8. HTTP 201 Created (Original Response)" + +client -> filter: "9. [Duplicate Request] Same Key & Body" +filter -> db: "10. SELECT * FROM idempotency_registry WHERE key = ?" +filter -> client: "11. Replay Cached HTTP 201 Response" + +client -> filter: "12. [Tampered Request] Same Key & Different Body" +filter -> filter: "13. Compare SHA-256 checksums -> Mismatch detected" +filter -> client: "14. 400 Bad Request (Key Reuse with Different Payload)" ``` +
### In-Flight Lease Recovery & Distributed Race Handling - **Crashed Worker Node Recovery**: If an application node crashes mid-flight while a transaction is in status `PROCESSING`, the in-flight lease automatically expires after 2 minutes (`IN_FLIGHT_TIMEOUT`), allowing client retries to re-acquire the lock without waiting for the 24-hour TTL purge. @@ -373,31 +460,49 @@ CREATE INDEX IF NOT EXISTS idx_event_pub_date ON event_publication(publication_d ### Transactional Outbox Flow -```mermaid -sequenceDiagram - autonumber - actor Client as 📱 HTTP Client - participant Service as 🔒 TransactionService - participant Publisher as 📢 ApplicationEventPublisher - participant DB as 🗄️ PostgreSQL (users / transactions / event_publication) - participant Listener as ⚡ TransferEventListener (@ApplicationModuleListener) - - Client->>Service: sendMoney(TransferMoneyRequest) - Note over Service,DB: Transaction Boundary: @Transactional(READ_COMMITTED) - Service->>DB: UPDATE users SET balance = balance - amount WHERE ... (Sender) - Service->>DB: UPDATE users SET balance = balance + amount WHERE ... (Receiver) - Service->>DB: INSERT INTO transactions ... - Service->>DB: INSERT INTO balance_ledger (DEBIT & CREDIT entries) ... - Service->>Publisher: publishEvent(TransferCompletedEvent) - Publisher->>DB: INSERT INTO event_publication (id, listener_id, event_type, serialized_event, publication_date) - Service-->>Client: 201 Created (TransactionResponse) - Note over Service,DB: Transaction Commits Atomically in Single DB Unit of Work - - Note over DB,Listener: Post-Commit Asynchronous Invocation (Spring Modulith) - Publisher->>Listener: onTransferCompleted(TransferCompletedEvent) - Listener->>Listener: Asynchronous audit / notification logging - Listener->>DB: UPDATE event_publication SET completion_date = NOW() WHERE id = ? +![Transactional Outbox Flow](assets/diagrams/transactional-outbox-flow.svg) + +
+📐 View Declarative D2 Diagram Source + +```d2 +shape: sequence_diagram + +client: HTTP Client { + icon: "docs/assets/icons/client.svg" +} +service: TransactionService { + icon: "docs/assets/icons/java.svg" +} +publisher: ApplicationEventPublisher { + icon: "docs/assets/icons/spring.svg" +} +db: PostgreSQL (Atomicity Boundary) { + icon: "docs/assets/icons/postgresql.svg" +} +listener: TransferEventListener { + icon: "docs/assets/icons/queue.svg" +} +kafka: Apache Kafka (payflow.transfers) { + icon: "docs/assets/icons/kafka.svg" +} + +client -> service: "1. sendMoney(TransferMoneyRequest)" +service -> db: "2. [Single ACID TX] SELECT ... FOR UPDATE (Row Lock Sender & Receiver)" +service -> db: "3. UPDATE users SET balance = balance - amount (Sender)" +service -> db: "4. UPDATE users SET balance = balance + amount (Receiver)" +service -> db: "5. INSERT INTO balance_ledger (DEBIT & CREDIT rows)" +service -> db: "6. INSERT INTO transactions (status=COMPLETED)" +service -> publisher: "7. publishEvent(TransferCompletedEvent)" +publisher -> db: "8. INSERT INTO event_publication (id, listener_id, event_type, payload)" +service -> client: "9. 201 Created (TransactionResponse) [TX Commits Atomically]" + +publisher -> listener: "10. [Post-Commit Async] onTransferCompleted(event)" +listener -> kafka: "11. (prod profile) ProducerRecord(topic=payflow.transfers, key=senderUpi)" +kafka -> listener: "12. acks=all RecordMetadata (offset, partition)" +listener -> db: "13. UPDATE event_publication SET completion_date = NOW() WHERE id = ?" ``` +
### Module Boundary Verification Spring Modulith continuously validates domain encapsulation and architectural coupling rules across packages via `ModulithStructureTest`: @@ -443,34 +548,48 @@ Payflow secures all financial and private user endpoints using **Spring Security ### Security Filter Chain Sequence -```mermaid -sequenceDiagram - autonumber - actor Client as 📱 HTTP Client - participant Filter as 🛡️ JwtAuthenticationFilter - participant Provider as 🔑 JwtTokenProvider - participant Context as 🧠 SecurityContextHolder - participant Controller as 🎯 Protected Controller - participant EntryPoint as 🚫 JwtAuthenticationEntryPoint - - Client->>Filter: Request with Authorization: Bearer - alt Missing Authorization Header on Protected Route - Filter->>Controller: Chain continues without auth - Controller-->>EntryPoint: Access Denied / Unauthenticated - EntryPoint-->>Client: 401 Unauthorized (RFC 7807 ProblemDetail) - else Invalid or Expired Token - Filter->>Provider: validateToken(token) -> false - Filter->>Controller: Chain continues without auth - Controller-->>EntryPoint: Access Denied - EntryPoint-->>Client: 401 Unauthorized (RFC 7807 ProblemDetail) - else Valid JWT Bearer Token - Filter->>Provider: validateToken(token) -> true - Filter->>Provider: getUpiIdFromToken(token) -> upiId - Filter->>Context: setAuthentication(UsernamePasswordAuthenticationToken) - Filter->>Controller: FilterChain.doFilter(req, res) - Controller-->>Client: 200 / 201 Response Payload - end +![Security Filter Chain Sequence](assets/diagrams/security-filter-chain.svg) + +
+📐 View Declarative D2 Diagram Source + +```d2 +shape: sequence_diagram + +client: HTTP Client { + icon: "docs/assets/icons/client.svg" +} +filter: JwtAuthenticationFilter { + icon: "docs/assets/icons/shield.svg" +} +provider: JwtTokenProvider { + icon: "docs/assets/icons/lock.svg" +} +context: SecurityContextHolder { + icon: "docs/assets/icons/spring.svg" +} +controller: Protected Controller { + icon: "docs/assets/icons/java.svg" +} +entrypoint: JwtAuthenticationEntryPoint { + icon: "docs/assets/icons/shield.svg" +} + +client -> filter: "1. Request with Authorization: Bearer " +filter -> provider: "2. validateToken(token)" + +provider -> filter: "3a. (Case: Invalid / Expired Signature) false" +filter -> entrypoint: "4a. commence() Exception translation" +entrypoint -> client: "5a. 401 Unauthorized (RFC 7807 ProblemDetail)" + +provider -> filter: "3b. (Case: Valid Token) true" +filter -> provider: "4b. getUpiIdFromToken(token)" +provider -> filter: "5b. upiId & referenceId claims" +filter -> context: "6b. setAuthentication(UsernamePasswordAuthenticationToken)" +filter -> controller: "7b. FilterChain.doFilter(req, res)" +controller -> client: "8b. 200 OK / 201 Created Response" ``` +
### Principal-Bound Resource Authorization & Sender Verification (Phase 7B) @@ -507,50 +626,54 @@ During user onboarding (`POST /api/v1/users`), Payflow integrates with upstream - If the external gateway is persistently unreachable or times out, the `@Recover` handler (`UpiValidationService.recoverFromValidationFailure`) logs a warning and proceeds with registration. External third-party outages never compromise user registration availability. - If the external gateway explicitly reports `valid = false`, registration is blocked with `InvalidUpiException` (`422 Unprocessable Entity`). -```mermaid -sequenceDiagram - autonumber - actor Client as 📱 Mobile Client - participant Controller as 🎯 UserController - participant Service as 💼 UserService - participant Validator as 🛡️ UpiValidationService - participant HTTPClient as 🌐 UpiValidationClient (RestClient) - participant Gateway as 🏦 Upstream Banking Gateway - - Client->>Controller: POST /api/v1/users { upiId: "alice@payflow", ... } - Controller->>Service: registerUser(request) - Service->>Validator: validateUpi("alice@payflow") - - alt Validation Disabled (Feature Flag) - Validator-->>Service: Skip check (Instant OK) - else Validation Enabled - Validator->>HTTPClient: verify("alice@payflow") - HTTPClient->>Gateway: GET /api/v1/upi/verify/alice%40payflow - - alt Success (valid: true) - Gateway-->>HTTPClient: 200 OK { "valid": true, "bankName": "HDFC" } - HTTPClient-->>Validator: UpiVerificationResponse(valid=true) - Validator-->>Service: Proceed with Registration - else Explicit Rejection (valid: false) - Gateway-->>HTTPClient: 200 OK { "valid": false } - HTTPClient-->>Validator: UpiVerificationResponse(valid=false) - Validator-->>Controller: throw InvalidUpiException (422 Unprocessable Entity) - Controller-->>Client: 422 ProblemDetail (invalid-upi-id) - else Gateway Down / Timeout (Network Failure) - Gateway--xHTTPClient: 503 / Timeout (Attempt 1) - Note over Validator: Retry 1: Backoff 500ms + Jitter - Gateway--xHTTPClient: 503 / Timeout (Attempt 2) - Note over Validator: Retry 2: Backoff 1000ms + Jitter - Gateway--xHTTPClient: 503 / Timeout (Attempt 3) - Validator->>Validator: @Recover handler fallback (Proceed with warning) - Validator-->>Service: Fallback OK (Non-blocking onboarding) - end - end - - Service->>Service: Persist User Entity to DB - Service-->>Controller: User Entity - Controller-->>Client: 201 Created (UserResponse) +![UPI Validation & Fallback Flow](assets/diagrams/upi-validation-fallback.svg) + +
+📐 View Declarative D2 Diagram Source + +```d2 +shape: sequence_diagram + +client: Mobile Client { + icon: "docs/assets/icons/client.svg" +} +controller: UserController { + icon: "docs/assets/icons/spring.svg" +} +service: UserService { + icon: "docs/assets/icons/java.svg" +} +validator: UpiValidationService { + icon: "docs/assets/icons/shield.svg" +} +http_client: UpiValidationClient { + icon: "docs/assets/icons/gateway.svg" +} +gateway: Banking Network Gateway { + icon: "docs/assets/icons/server.svg" +} + +client -> controller: "1. POST /api/v1/users (CreateUserRequest)" +controller -> service: "2. registerUser(request)" +service -> validator: "3. validateUpi(upiId)" + +validator -> http_client: "4. verify(upiId)" +http_client -> gateway: "5. GET /api/v1/upi/verify/{upiId}" + +gateway -> http_client: "6a. (Case: Valid UPI) 200 OK { valid: true }" +http_client -> validator: "7a. UpiVerificationResponse(valid=true)" +validator -> service: "8a. Validation Passed" +service -> controller: "9a. Persist User Entity to DB" +controller -> client: "10a. 201 Created (UserResponse)" + +gateway -> http_client: "6b. (Case: Downstream 503 / Timeout)" +http_client -> validator: "7b. Retries with backoff (500ms, 1000ms)" +validator -> validator: "8b. @Recover fallback triggered" +validator -> service: "9b. Proceed with Registration (Warning Logged)" +service -> controller: "10b. Persist User Entity (Zero Onboarding Outage)" +controller -> client: "11b. 201 Created (Non-blocking Fallback)" ``` +
--- @@ -598,46 +721,106 @@ Payflow API adopts an immutable, production-hardened cloud-native deployment mod ### B. Kubernetes Deployment Topology & Elasticity (Phase 11B) -```mermaid -graph TD - subgraph K8sCluster["☸️ Kubernetes Production Cluster (Namespace: default)"] - Ingress["Ingress Controller / API Gateway"] -->|"Round Robin / HTTP"| Service["payflow-service (ClusterIP:8080)"] - - subgraph WorkloadTopology["Pod Replica Set (Managed by HPA: 2 to 10 Pods)"] - Pod1["Pod: payflow-api-1 (UID: 10001)"] - Pod2["Pod: payflow-api-2 (UID: 10001)"] - PodN["Pod: payflow-api-N (Auto-Scaled)"] - end - - Service --> Pod1 - Service --> Pod2 - Service -.-> PodN - - ConfigMap["ConfigMap: payflow-config"] -.->|"envFrom"| WorkloadTopology - Secret["Secret: payflow-secrets"] -.->|"envFrom"| WorkloadTopology - HPA["HorizontalPodAutoscaler (HPA)
Target: CPU 75%, Mem 80%"] -->|"Scale Replicas"| WorkloadTopology - PDB["PodDisruptionBudget (PDB)
minAvailable: 1"] -->|"Quorum Protection"| WorkloadTopology - end - - subgraph BackingServices["🗄️ Managed External Banking Backing Services"] - Pod1 --> DB[("PostgreSQL 17 Database")] - Pod1 --> RedisCache[("Redis 7 Cluster")] - Pod1 --> KafkaCluster[("Apache Kafka KRaft Broker")] - Pod2 --> DB - Pod2 --> RedisCache - Pod2 --> KafkaCluster - end - - classDef k8sStyle fill:#0f172a,stroke:#38bdf8,stroke-width:2px,color:#f8fafc; - classDef podStyle fill:#1e1b4b,stroke:#818cf8,stroke-width:2px,color:#f8fafc; - classDef dbStyle fill:#064e3b,stroke:#10b981,stroke-width:2px,color:#f8fafc; - classDef controlStyle fill:#451a03,stroke:#f59e0b,stroke-width:2px,color:#f8fafc; - - class K8sCluster k8sStyle; - class Pod1,Pod2,PodN podStyle; - class DB,RedisCache,KafkaCluster dbStyle; - class HPA,PDB,ConfigMap,Secret controlStyle; +![Kubernetes Deployment Topology & Elasticity](assets/diagrams/kubernetes-deployment-topology.svg) + +
+📐 View Declarative D2 Diagram Source + +```d2 +direction: down + +ingress: Ingress Controller / API Gateway { + shape: rectangle + icon: "docs/assets/icons/gateway.svg" +} + +cluster: Kubernetes Production Cluster (Namespace: default) { + service: payflow-service { + shape: rectangle + icon: "docs/assets/icons/kubernetes.svg" + label: "payflow-service (ClusterIP:8080)" + } + + workloads: Pod Replica Set (HPA 2 to 10 Pods) { + pod1: Pod payflow-api-1 { + shape: rectangle + icon: "docs/assets/icons/docker.svg" + label: "Pod 1 (payflow:10001)\nVirtual Threads Enabled" + } + + pod2: Pod payflow-api-2 { + shape: rectangle + icon: "docs/assets/icons/docker.svg" + label: "Pod 2 (payflow:10001)\nVirtual Threads Enabled" + } + + podN: Pod payflow-api-N { + shape: rectangle + icon: "docs/assets/icons/docker.svg" + label: "Pod N (Auto-Scaled)" + } + } + + controls: Cluster Governance & Scaling { + hpa: HorizontalPodAutoscaler { + shape: rectangle + icon: "docs/assets/icons/kubernetes.svg" + label: "HPA (Min: 2, Max: 10)\nTarget: CPU 75%, Mem 80%" + } + + pdb: PodDisruptionBudget { + shape: rectangle + icon: "docs/assets/icons/shield.svg" + label: "PDB (minAvailable: 1)" + } + + config: ConfigMap & Secrets { + shape: rectangle + icon: "docs/assets/icons/lock.svg" + label: "payflow-config & payflow-secrets" + } + } + + service -> workloads.pod1: "Round Robin" + service -> workloads.pod2: "Round Robin" + service -> workloads.podN: "Round Robin" + + controls.hpa -> workloads: "Scale Replicas" + controls.pdb -> workloads: "Quorum Protection" + controls.config -> workloads: "envFrom Injection" +} + +ingress -> cluster.service: "TLS 1.3 / HTTP" + +backing: Managed External Backing Services { + postgres: PostgreSQL 17 { + shape: cylinder + icon: "docs/assets/icons/postgresql.svg" + label: "PostgreSQL 17 Database" + } + + redis: Redis 7 Cluster { + shape: cylinder + icon: "docs/assets/icons/redis.svg" + label: "Redis 7 Cluster (Redlock & Cache)" + } + + kafka: Apache Kafka (KRaft) { + shape: queue + icon: "docs/assets/icons/kafka.svg" + label: "Apache Kafka Broker (KRaft Mode)" + } +} + +cluster.workloads.pod1 -> backing.postgres: "HikariCP (Max 20 Pool)" +cluster.workloads.pod1 -> backing.redis: "Lettuce & Redisson" +cluster.workloads.pod1 -> backing.kafka: "payflow.transfers" + +cluster.workloads.pod2 -> backing.postgres +cluster.workloads.pod2 -> backing.redis +cluster.workloads.pod2 -> backing.kafka ``` +
- **Manifest Suite (`k8s/`)**: - `deployment.yaml`: Configured with zero-downtime rolling updates (`maxSurge: 1`, `maxUnavailable: 0`), non-root security context (`runAsNonRoot: true`, `runAsUser: 10001`), resource requests (`250m` CPU, `512Mi` RAM) and limits (`1000m` CPU, `1024Mi` RAM). @@ -660,7 +843,7 @@ In distributed Kubernetes environments, terminating pods requires coordinated dr - **Line Coverage**: Minimum 80% (Achieved: **90%** across 191 tests). - **Branch Coverage**: Minimum 70% (Achieved: **73%** across 191 tests). - Pure DTOs, configuration classes, entities, and generated MapStruct classes are excluded from coverage calculations. -- **GitHub Actions Workflow Hardening**: `.github/workflows/ci.yml` executes `mvn clean verify -B`, failing fast on formatting, checkstyle, test failures, SpotBugs warnings, or coverage violations, while uploading JaCoCo and SpotBugs report artifacts for 14-day retention and deploying the Docsify documentation portal (`https://shashankch.github.io/payflow-api/`) with live interactive JaCoCo coverage reports (`https://shashankch.github.io/payflow-api/coverage/`) to GitHub Pages. +- **GitHub Actions Workflow Hardening**: `.github/workflows/ci.yml` executes `mvn clean verify -B`, failing fast on formatting, checkstyle, test failures, SpotBugs warnings, or coverage violations, while uploading JaCoCo and SpotBugs report artifacts for 14-day retention and deploying the Material for MkDocs documentation portal (`https://shashankch.github.io/payflow-api/`) with D2 diagrams, coverage quality gates (`https://shashankch.github.io/payflow-api/coverage/`), and live interactive JaCoCo coverage reports (`https://shashankch.github.io/payflow-api/coverage-report/`) to GitHub Pages. --- @@ -711,28 +894,45 @@ Payflow API incorporates **Resilience4j** to safeguard system stability under pe ### B. Dynamic Per-User Rate Limiting Pattern Unlike traditional global rate limiting, which allows a single abusive script to exhaust server throughput for all customers, Payflow enforces **per-authenticated-user partition isolation**: -```mermaid -sequenceDiagram - autonumber - actor Client as Client / User A - participant Filter as JwtAuthenticationFilter - participant Resolver as SecurityContextRateLimiterKeyResolver - participant Service as UserRateLimiterService - participant Reg as RateLimiterRegistry - participant Core as TransactionService - - Client->>Filter: POST /api/v1/transactions (Bearer JWT) - Filter->>Resolver: Resolve User Principal (alice@payflow) - Resolver->>Service: Key = "transferLimiter:alice@payflow" - Service->>Reg: rateLimiter("transferLimiter:alice@payflow", "transferLimiter") - Note over Service,Reg: Inherits base 10 req/s template & tracks access timestamp - alt Permitted (<= 10 req/s) - Service->>Core: Proceed with transfer - Core-->>Client: 201 Created (TransactionResponse) - else Exceeded (> 10 req/s) - Service-->>Client: 429 Too Many Requests (Retry-After: 1) - end +![Dynamic Per-User Rate Limiting Flow](assets/diagrams/user-rate-limiting.svg) + +
+📐 View Declarative D2 Diagram Source + +```d2 +shape: sequence_diagram + +client: Client / User A { + icon: "docs/assets/icons/client.svg" +} +filter: JwtAuthenticationFilter { + icon: "docs/assets/icons/shield.svg" +} +resolver: SecurityContextRateLimiterKeyResolver { + icon: "docs/assets/icons/shield.svg" +} +service: UserRateLimiterService { + icon: "docs/assets/icons/spring.svg" +} +reg: RateLimiterRegistry { + icon: "docs/assets/icons/lock.svg" +} +core: TransactionService { + icon: "docs/assets/icons/java.svg" +} + +client -> filter: "POST /api/v1/transactions (Bearer JWT)" +filter -> resolver: "Resolve Principal (alice@payflow)" +resolver -> service: "Key = \"transferLimiter:alice@payflow\"" +service -> reg: "rateLimiter(key, \"transferLimiter\")" +service -> reg: "Inherits base 10 req/s template & tracks access timestamp" { + style.stroke-dash: 3 +} +service -> core: "[Permitted <= 10 req/s] Proceed with transfer" +core -> client: "201 Created (TransactionResponse)" +service -> client: "[Exceeded > 10 req/s] 429 Too Many Requests (Retry-After: 1)" ``` +
- **Memory Eviction Safeguard**: Dynamically generated rate limiter instances are tracked by access timestamp. `UserRateLimiterService.evictInactiveLimiters()` executes periodically, purging partitions inactive for >15 minutes from the registry to prevent unbounded memory growth. @@ -759,35 +959,46 @@ To achieve sub-10ms response latencies on repetitive user profile lookups and ba ### A. Cache-Aside Workflow & Sequence Diagram In the Cache-Aside pattern, the application service inspects the cache before accessing the underlying database. On write operations, the cache entries are evicted rather than updated inline, eliminating race conditions between concurrent database writes and cache updates. -```mermaid -sequenceDiagram - autonumber - actor Client as API Client / Controller - participant Service as UserService / TxService - participant Cache as CacheManager (Redis / Caffeine) - participant DB as PostgreSQL Database - - alt Read Query (e.g. getUserByReferenceId) - Client->>Service: Read User Profile / Ledger - Service->>Cache: GET key (e.g. "users::refId") - alt Cache Hit - Cache-->>Service: Return cached JSON / entity - Service-->>Client: Return UserResponse / LedgerResponse - else Cache Miss - Cache-->>Service: null - Service->>DB: SELECT query - DB-->>Service: Entity record - Service->>Cache: PUT key with configured TTL - Service-->>Client: Return UserResponse / LedgerResponse - end - else Write Mutation (e.g. sendMoney or registerUser) - Client->>Service: Transfer Funds / Register User - Service->>DB: Execute ACID transaction & commit - DB-->>Service: Transaction committed - Service->>Cache: EVICT affected caches (users, user_ledgers) - Service-->>Client: Return Success Response - end +![Cache-Aside Workflow & Sequence Diagram](assets/diagrams/cache-aside-workflow.svg) + +
+📐 View Declarative D2 Diagram Source + +```d2 +shape: sequence_diagram + +client: API Client / Controller { + icon: "docs/assets/icons/client.svg" +} +service: UserService / TxService { + icon: "docs/assets/icons/spring.svg" +} +cache: CacheManager (Redis / Caffeine) { + icon: "docs/assets/icons/redis.svg" +} +db: PostgreSQL Database { + icon: "docs/assets/icons/postgresql.svg" +} + +client -> service: "Read User Profile / Ledger" +service -> cache: "GET key (e.g. users::refId)" + +cache -> service: "[Cache Hit] Return cached JSON" +service -> client: "Return UserResponse / LedgerResponse" + +cache -> service: "[Cache Miss] Return null" +service -> db: "SELECT query" +db -> service: "Entity record" +service -> cache: "PUT key with configured TTL" +service -> client: "Return UserResponse / LedgerResponse" + +client -> service: "Transfer Funds / Register User" +service -> db: "Execute ACID transaction & commit" +db -> service: "Transaction committed" +service -> cache: "EVICT affected caches (users, user_ledgers)" +service -> client: "Return Success Response" ``` +
### B. Profile-Conditional Cache Strategy The cache infrastructure adapts transparently across environments via `CacheConfig.java`: @@ -933,29 +1144,88 @@ Payflow API is engineered from the ground up with a **Zero-Trust** and **Defense ### A. Zero-Trust Request Lifecycle & Filter Chain -```mermaid -graph TD - Client["Client / API Consumer"] -->|HTTPS / TLS 1.3| WAF["API Gateway / WAF"] - WAF -->|X-Request-Id & Bearer JWT| FilterChain["Spring Security Filter Chain"] - - subgraph Security_Filters["Security Filter Tier"] - FilterChain --> RLFilter["RequestLoggingFilter (MDC & Audit)"] - RLFilter --> IdFilter["RequestIdFilter (Correlation)"] - IdFilter --> JwtFilter["JwtAuthenticationFilter (HMAC-SHA256)"] - JwtFilter --> IdempFilter["IdempotencyFilter (SHA-256 Hashing)"] - end - - subgraph Domain_Tier["Domain & Authorization Tier"] - IdempFilter --> DomainAuth["Principal-Bound Verification (SecurityUtils)"] - DomainAuth --> TxService["TransactionService (ACID Invariants)"] - end - - subgraph Concurrency_Tier["Concurrency & Data Tier"] - TxService --> LockOrder["Deterministic Alphabetical Row Lock"] - LockOrder --> PessimisticLock["SELECT ... FOR UPDATE (PostgreSQL)"] - PessimisticLock --> DoubleEntry["Immutable Balance Ledger"] - end +![Zero-Trust Request Lifecycle & Filter Chain](assets/diagrams/zero-trust-lifecycle.svg) + +
+📐 View Declarative D2 Diagram Source + +```d2 +direction: down + +ingress: Ingress & Perimeter Gateway { + grid-columns: 2 + + client: Client / API Consumer { + shape: rectangle + icon: "docs/assets/icons/client.svg" + } + waf: API Gateway / WAF { + shape: rectangle + icon: "docs/assets/icons/gateway.svg" + } + client -> waf: "HTTPS / TLS 1.3" +} + +filters: Security Filter Tier { + grid-columns: 4 + + req_id: RequestIdFilter (MDC Correlation) { + shape: rectangle + icon: "docs/assets/icons/shield.svg" + } + rl: RequestLoggingFilter (Audit & Timing) { + shape: rectangle + icon: "docs/assets/icons/shield.svg" + } + jwt: JwtAuthenticationFilter (HMAC-SHA256) { + shape: rectangle + icon: "docs/assets/icons/lock.svg" + } + idemp: IdempotencyFilter (SHA-256 Hashing) { + shape: rectangle + icon: "docs/assets/icons/lock.svg" + } + + req_id -> rl -> jwt -> idemp +} + +app_persistence: Application & Persistence Layer { + grid-columns: 2 + + domain: Domain & Invariants { + auth: Principal Verification (SecurityUtils) { + shape: rectangle + icon: "docs/assets/icons/shield.svg" + } + tx: TransactionService (ACID Invariants) { + shape: rectangle + icon: "docs/assets/icons/spring.svg" + } + auth -> tx + } + + data: Concurrency & Ledger Tier { + ordering: Deterministic Alphabetical Lock Ordering { + shape: rectangle + } + pessimistic: SELECT ... FOR UPDATE (PostgreSQL) { + shape: cylinder + icon: "docs/assets/icons/postgresql.svg" + } + ledger: Immutable Double-Entry Ledger { + shape: cylinder + icon: "docs/assets/icons/postgresql.svg" + } + ordering -> pessimistic -> ledger + } + + domain.tx -> data.ordering +} + +ingress -> filters: "TLS 1.3 & Bearer JWT" +filters -> app_persistence: "MDC Correlated & Idempotency Key Checked" ``` +
### B. Threat Model (STRIDE Analysis) @@ -1016,22 +1286,37 @@ When two users concurrently transfer funds to each other (e.g. Aarav sends to Pr - Payflow resolves this by sorting both UPI IDs lexicographically (`upiA.compareTo(upiB)`) before issuing `SELECT ... FOR UPDATE` queries. - Both transactions request locks in the exact same sequence (`aarav@payflow` first, then `priya@payflow`), converting cyclical deadlocks into sequential lock queues. -```mermaid -sequenceDiagram - autonumber - participant Tx1 as Transaction 1 (Aarav -> Priya) - participant DB as PostgreSQL Engine - participant Tx2 as Transaction 2 (Priya -> Aarav) - - Note over Tx1,Tx2: Deterministic Ordering: 'aarav@payflow' < 'priya@payflow' - Tx1->>DB: Lock 'aarav@payflow' (ACQUIRED) - Tx2->>DB: Lock 'aarav@payflow' (BLOCKED / WAITING) - Tx1->>DB: Lock 'priya@payflow' (ACQUIRED) - Tx1->>DB: Debit Aarav, Credit Priya, Commit & Release Locks - DB-->>Tx2: Lock 'aarav@payflow' (GRANTED) - Tx2->>DB: Lock 'priya@payflow' (ACQUIRED) - Tx2->>DB: Debit Priya, Credit Aarav, Commit & Release Locks +![Deterministic Alphabetical Row Locking Deadlock Elimination](assets/diagrams/deadlock-elimination.svg) + +
+📐 View Declarative D2 Diagram Source + +```d2 +shape: sequence_diagram + +tx1: "Transaction 1 (Aarav -> Priya)" { + icon: "docs/assets/icons/java.svg" +} +db: "PostgreSQL Engine" { + icon: "docs/assets/icons/postgresql.svg" +} +tx2: "Transaction 2 (Priya -> Aarav)" { + icon: "docs/assets/icons/java.svg" +} + +tx1 -> db: "Deterministic Ordering: 'aarav@payflow' < 'priya@payflow'" { + style.stroke-dash: 3 +} + +tx1 -> db: "Lock 'aarav@payflow' (ACQUIRED)" +tx2 -> db: "Lock 'aarav@payflow' (BLOCKED / WAITING)" +tx1 -> db: "Lock 'priya@payflow' (ACQUIRED)" +tx1 -> db: "Debit Aarav, Credit Priya, Commit & Release" +db -> tx2: "Lock 'aarav@payflow' (GRANTED)" +tx2 -> db: "Lock 'priya@payflow' (ACQUIRED)" +tx2 -> db: "Debit Priya, Credit Aarav, Commit & Release" ``` +
#### 3. Financial Base-10 Arithmetic Precision - All monetary columns (`balance`, `amount`, `balance_before`, `balance_after`) strictly use Java `BigDecimal` denominated in Indian Rupees (INR, ₹) mapped to database column `@Column(precision = 19, scale = 4, nullable = false)`. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md new file mode 120000 index 0000000..04c99a5 --- /dev/null +++ b/docs/CHANGELOG.md @@ -0,0 +1 @@ +../CHANGELOG.md \ No newline at end of file diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 120000 index 0000000..44fcc63 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1 @@ +../CONTRIBUTING.md \ No newline at end of file diff --git a/docs/CONVENTIONS.md b/docs/CONVENTIONS.md index d25a9d4..683f226 100644 --- a/docs/CONVENTIONS.md +++ b/docs/CONVENTIONS.md @@ -71,6 +71,15 @@ This document outlines the coding standards, repository conventions, Git workflo - **Parity**: Every feature implemented in code must be documented in `README.md`, `docs/ARCHITECTURE.md`, and `docs/API_SPECIFICATION.md`. - **ADRs**: Any major architectural decision (e.g., locking strategy, caching library choice) must be recorded as an individual numbered ADR under `docs/adr/`. - **Changelog**: Every merged PR must include an entry in `CHANGELOG.md` under `[Unreleased]`. +- **Architectural Diagrams (D2 & SVGs)**: + - All architecture, sequence, and data flow diagrams are authored using the **D2 diagramming language**. + - Canonical D2 sources are stored in `docs/diagrams/*.d2`. + - Self-contained SVGs are pre-compiled into `docs/assets/diagrams/*.svg` with automatic dual light/dark theme support (`d2 --theme=0 --dark-theme=200`). + - Markdowns embed the pre-rendered SVG (`![Alt Text](assets/diagrams/name.svg)`) alongside a collapsible `
📐 View Declarative D2 Diagram Source...` block, ensuring native, universal image rendering across GitHub, IDE markdown previewers (VS Code, IntelliJ, PyCharm), and the Material for MkDocs documentation portal. + - **CI/CD Quality Gates & Automation**: + - **Drift Enforcement**: PR and push builds in `.github/workflows/ci.yml` run `./scripts/generate-diagrams.sh --check` to fail fast if any SVG has drifted from its `.d2` source. + - **Docs Validation**: PR builds execute `mkdocs build --strict` to validate navigation, links, and markdown syntax. + - **1-Click Regeneration**: Maintainers can trigger `.github/workflows/generate-diagrams.yml` via GitHub Actions `workflow_dispatch` to automatically recompile all diagrams and commit them directly back to the branch without needing a local D2 installation. - **Design Document Structure**: All architectural design documents in `docs/` must incorporate standard design document sections: - **Metadata Header**: Short title, author, creation date, status (Draft/Approved), and authoritative relative link. - **Executive Summary & Background**: High-level problem statement, business motivation, and non-obvious domain context. diff --git a/docs/LICENSE.md b/docs/LICENSE.md new file mode 120000 index 0000000..ea5b606 --- /dev/null +++ b/docs/LICENSE.md @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1ef0693..82e3f61 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -96,6 +96,7 @@ Cloud-native packaging, orchestration, and automated quality gates: - **11A — Multi-Stage Containerization & Local Orchestration**: Minimal multi-stage Docker build (Java 25 JRE, non-root user, healthcheck) and full-stack Docker Compose (PostgreSQL, Redis, Kafka, Ollama, Prometheus, Grafana). - **11B — Cloud-Native Kubernetes Deployment**: Declarative Kubernetes manifests (Deployments, Services, ConfigMaps, Secrets, HPA, PDB) with graceful shutdown coordination. - **11C — CI/CD Pipeline Hardening & Quality Gates**: Automated static analysis (SpotBugs) and strict code coverage enforcement (JaCoCo) integrated into GitHub Actions. +- **11D — Documentation Modernization & Declarative D2 Diagrams**: Modernized public documentation portal with Material for MkDocs, declarative D2 architecture diagrams with zero drift enforcement, and bidirectional JaCoCo code coverage navigation. --- diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 120000 index 0000000..9d57138 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1 @@ +../SECURITY.md \ No newline at end of file diff --git a/docs/adr/0030-ci-cd-quality-gates-jacoco-spotbugs.md b/docs/adr/0030-ci-cd-quality-gates-jacoco-spotbugs.md index a98faf3..0762d59 100644 --- a/docs/adr/0030-ci-cd-quality-gates-jacoco-spotbugs.md +++ b/docs/adr/0030-ci-cd-quality-gates-jacoco-spotbugs.md @@ -52,7 +52,7 @@ We adopted **Option C**. The quality enforcement system consists of: 3. **Hardened GitHub Actions CI Pipeline**: - Updated `.github/workflows/ci.yml` to run `mvn clean verify -B`. - - Added automated GitHub Pages deployment for Docsify public documentation portal (`https://shashankch.github.io/payflow-api/`) and live interactive JaCoCo coverage reports at `https://shashankch.github.io/payflow-api/coverage/`. + - Added automated GitHub Pages deployment for Material for MkDocs public documentation portal (`https://shashankch.github.io/payflow-api/`) with D2 diagrams, coverage quality gates at `https://shashankch.github.io/payflow-api/coverage/`, and live interactive JaCoCo coverage reports at `https://shashankch.github.io/payflow-api/coverage-report/`. - Added code coverage status badge to `README.md` linking directly to the live GitHub Pages report. ## Consequences diff --git a/docs/assets/diagrams/cache-aside-workflow.svg b/docs/assets/diagrams/cache-aside-workflow.svg new file mode 100644 index 0000000..67ffbb9 --- /dev/null +++ b/docs/assets/diagrams/cache-aside-workflow.svg @@ -0,0 +1,57 @@ +API Client / ControllerUserService / TxServiceCacheManager (Redis / Caffeine)PostgreSQL Database Read User Profile / LedgerGET key (e.g. users::refId)[Cache Hit] Return cached JSONReturn UserResponse / LedgerResponse[Cache Miss] Return nullSELECT queryEntity recordPUT key with configured TTLReturn UserResponse / LedgerResponseTransfer Funds / Register UserExecute ACID transaction & commitTransaction committedEVICT affected caches (users, user_ledgers)Return Success Response + + + + + + + + + + + + + + + + diff --git a/docs/assets/diagrams/deadlock-elimination.svg b/docs/assets/diagrams/deadlock-elimination.svg new file mode 100644 index 0000000..e20b6c4 --- /dev/null +++ b/docs/assets/diagrams/deadlock-elimination.svg @@ -0,0 +1,53 @@ +Transaction 1 (Aarav -> Priya)PostgreSQL EngineTransaction 2 (Priya -> Aarav) Deterministic Ordering: 'aarav@payflow' < 'priya@payflow' Lock 'aarav@payflow' (ACQUIRED)Lock 'aarav@payflow' (BLOCKED / WAITING)Lock 'priya@payflow' (ACQUIRED)Debit Aarav, Credit Priya, Commit & ReleaseLock 'aarav@payflow' (GRANTED)Lock 'priya@payflow' (ACQUIRED)Debit Priya, Credit Aarav, Commit & Release + + + + + + + + + + diff --git a/docs/assets/diagrams/distributed-lock-sequence.svg b/docs/assets/diagrams/distributed-lock-sequence.svg new file mode 100644 index 0000000..5506c73 --- /dev/null +++ b/docs/assets/diagrams/distributed-lock-sequence.svg @@ -0,0 +1,58 @@ +API Client / IngressPod 1 (IdempotencyFilter)Pod 2 (IdempotencyFilter)Redis Lock (Redisson)PostgreSQL Database 1. POST /api/v1/transactions (Key: tx-123)2. POST /api/v1/transactions (Key: tx-123 [Duplicate])3. tryLock("payflow:lock:idemp:tx-123", wait=2s, lease=10s)4. Lock Acquired (true)5. tryLock("payflow:lock:idemp:tx-123", wait=2s, lease=10s) [Blocks up to 2s]6. Check & record PROCESSING status7. Execute transaction & atomic commit8. Record SUCCESS with response body9. unlock("payflow:lock:idemp:tx-123")10. Lock Released11. 201 Created (Transaction Response)12. Lock Acquired (after Pod 1 release)13. SELECT idempotency record (Status: SUCCESS)14. 201 Created (Replay Cached Response)15. unlock("payflow:lock:idemp:tx-123") + + + + + + + + + + + + + + + + + diff --git a/docs/assets/diagrams/idempotency-lifecycle.svg b/docs/assets/diagrams/idempotency-lifecycle.svg new file mode 100644 index 0000000..512bd08 --- /dev/null +++ b/docs/assets/diagrams/idempotency-lifecycle.svg @@ -0,0 +1,57 @@ +HTTP ClientIdempotencyFilterPostgreSQL (idempotency_registry)TransactionService 1. POST /api/v1/transactions (Header: Idempotency-Key)2. Validate key regex & compute SHA-256(body)3. SELECT * FROM idempotency_registry WHERE key = ?4. (Case: Key Not Found) INSERT key, hash, status=PROCESSING5. doFilterInternal() -> sendMoney(request)6. HTTP 201 Created (TransactionResponse)7. UPDATE status=SUCCESS, code=201, body=...8. HTTP 201 Created (Original Response)9. [Duplicate Request] Same Key & Body10. SELECT * FROM idempotency_registry WHERE key = ?11. Replay Cached HTTP 201 Response12. [Tampered Request] Same Key & Different Body13. Compare SHA-256 checksums -> Mismatch detected14. 400 Bad Request (Key Reuse with Different Payload) + + + + + + + + + + + + + + + + diff --git a/docs/assets/diagrams/kubernetes-deployment-topology.svg b/docs/assets/diagrams/kubernetes-deployment-topology.svg new file mode 100644 index 0000000..165f6c9 --- /dev/null +++ b/docs/assets/diagrams/kubernetes-deployment-topology.svg @@ -0,0 +1,68 @@ +Ingress Controller / API GatewayKubernetes Production Cluster (Namespace: default)Managed External Backing Servicespayflow-service (ClusterIP:8080)Pod Replica Set (HPA 2 to 10 Pods)Cluster Governance & ScalingPostgreSQL 17 DatabaseRedis 7 Cluster (Redlock & Cache)Apache Kafka Broker (KRaft Mode)Pod 1 (payflow:10001)Virtual Threads EnabledPod 2 (payflow:10001)Virtual Threads EnabledPod N (Auto-Scaled)HPA (Min: 2, Max: 10)Target: CPU 75%, Mem 80%PDB (minAvailable: 1)payflow-config & payflow-secrets Round RobinRound RobinRound RobinScale ReplicasQuorum ProtectionenvFrom InjectionTLS 1.3 / HTTPHikariCP (Max 20 Pool)Lettuce & Redissonpayflow.transfers + + + + + + + + + + + + diff --git a/docs/assets/diagrams/security-filter-chain.svg b/docs/assets/diagrams/security-filter-chain.svg new file mode 100644 index 0000000..161f3be --- /dev/null +++ b/docs/assets/diagrams/security-filter-chain.svg @@ -0,0 +1,54 @@ +HTTP ClientJwtAuthenticationFilterJwtTokenProviderSecurityContextHolderProtected ControllerJwtAuthenticationEntryPoint 1. Request with Authorization: Bearer <token>2. validateToken(token)3a. (Case: Invalid / Expired Signature) false4a. commence() Exception translation5a. 401 Unauthorized (RFC 7807 ProblemDetail)3b. (Case: Valid Token) true4b. getUpiIdFromToken(token)5b. upiId & referenceId claims6b. setAuthentication(UsernamePasswordAuthenticationToken)7b. FilterChain.doFilter(req, res)8b. 200 OK / 201 Created Response + + + + + + + + + + + + + diff --git a/docs/assets/diagrams/system-overview.svg b/docs/assets/diagrams/system-overview.svg new file mode 100644 index 0000000..0438820 --- /dev/null +++ b/docs/assets/diagrams/system-overview.svg @@ -0,0 +1,69 @@ +Client / Mobile AppIngress / API GatewaySpring Security (JWT Filter)Idempotency Filter (SHA-256)TransactionControllerTransactionServiceCore Concurrency & StorageAsynchronous Messaging & EventsAI & ResiliencePostgreSQL 17Redis 7 (Redlock & Cache)Transactional OutboxApache Kafka (KRaft)Ollama Gen-AIResilience4j HTTP POST (Idempotency-Key)TLS 1.3Bearer JWTValidated RequestsendMoney()spring-modulith-events-kafkaRow Lock & Ledger EntriesRedlock & Cache-AsideAtomically Persist EventSpend InsightsRate Limiting Guard + + + + + + + + + + + + + diff --git a/docs/assets/diagrams/system-topology.svg b/docs/assets/diagrams/system-topology.svg new file mode 100644 index 0000000..3a593f0 --- /dev/null +++ b/docs/assets/diagrams/system-topology.svg @@ -0,0 +1,87 @@ +Client AppAPI Gateway / IngressSpring Security / JWT / v1 VersioningKubernetes Pod / Spring Boot ContainerPersistence & Streaming TierMetrics & TelemetryUserController / TransactionControllerTransactionService / UserService / SpendInsightsServicePessimistic Lock / Redisson Lock / IdempotencyApplicationEventPublisher -> event_publicationOTel Tracing + Structured MDC LoggingPostgreSQL 17(Ledger, Transactions, Locks)Redis 7 (Redlock & Cache-Aside)Apache Kafka Broker(topic: payflow.transfers)Ollama Gen-AI (Spend Insights)Prometheus ServerGrafana Dashboards HTTP POST (Idempotency-Key)DTO DelegationLock & IdempotencyOutbox PublisherBearer JWT AuthenticationPromQL QueriesSELECT ... FOR UPDATERedlock RLock (2s wait, 10s lease)Cache-Aside (users, user_ledgers)Structured LLM PromptAtomic Outbox Writespring-modulith-events-kafkaScrape /actuator/prometheusSpring Security / JWT / v1 Versioning + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/diagrams/transactional-outbox-flow.svg b/docs/assets/diagrams/transactional-outbox-flow.svg new file mode 100644 index 0000000..b55af4a --- /dev/null +++ b/docs/assets/diagrams/transactional-outbox-flow.svg @@ -0,0 +1,56 @@ +HTTP ClientTransactionServiceApplicationEventPublisherPostgreSQL (Atomicity Boundary)TransferEventListenerApache Kafka (payflow.transfers) 1. sendMoney(TransferMoneyRequest)2. [Single ACID TX] SELECT ... FOR UPDATE (Row Lock Sender & Receiver)3. UPDATE users SET balance = balance - amount (Sender)4. UPDATE users SET balance = balance + amount (Receiver)5. INSERT INTO balance_ledger (DEBIT & CREDIT rows)6. INSERT INTO transactions (status=COMPLETED)7. publishEvent(TransferCompletedEvent)8. INSERT INTO event_publication (id, listener_id, event_type, payload)9. 201 Created (TransactionResponse) [TX Commits Atomically]10. [Post-Commit Async] onTransferCompleted(event)11. (prod profile) ProducerRecord(topic=payflow.transfers, key=senderUpi)12. acks=all RecordMetadata (offset, partition)13. UPDATE event_publication SET completion_date = NOW() WHERE id = ? + + + + + + + + + + + + + + + diff --git a/docs/assets/diagrams/upi-validation-fallback.svg b/docs/assets/diagrams/upi-validation-fallback.svg new file mode 100644 index 0000000..f2e43d3 --- /dev/null +++ b/docs/assets/diagrams/upi-validation-fallback.svg @@ -0,0 +1,59 @@ +Mobile ClientUserControllerUserServiceUpiValidationServiceUpiValidationClientBanking Network Gateway 1. POST /api/v1/users (CreateUserRequest)2. registerUser(request)3. validateUpi(upiId)4. verify(upiId)5. GET /api/v1/upi/verify/{upiId}6a. (Case: Valid UPI) 200 OK { valid: true }7a. UpiVerificationResponse(valid=true)8a. Validation Passed9a. Persist User Entity to DB10a. 201 Created (UserResponse)6b. (Case: Downstream 503 / Timeout)7b. Retries with backoff (500ms, 1000ms)8b. @Recover fallback triggered9b. Proceed with Registration (Warning Logged)10b. Persist User Entity (Zero Onboarding Outage)11b. 201 Created (Non-blocking Fallback) + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/diagrams/user-rate-limiting.svg b/docs/assets/diagrams/user-rate-limiting.svg new file mode 100644 index 0000000..0514763 --- /dev/null +++ b/docs/assets/diagrams/user-rate-limiting.svg @@ -0,0 +1,53 @@ +Client / User AJwtAuthenticationFilterSecurityContextRateLimiterKeyResolverUserRateLimiterServiceRateLimiterRegistryTransactionService POST /api/v1/transactions (Bearer JWT)Resolve Principal (alice@payflow)Key = "transferLimiter:alice@payflow"rateLimiter(key, "transferLimiter") Inherits base 10 req/s template & tracks access timestamp[Permitted <= 10 req/s] Proceed with transfer201 Created (TransactionResponse)[Exceeded > 10 req/s] 429 Too Many Requests (Retry-After: 1) + + + + + + + + + + diff --git a/docs/assets/diagrams/zero-trust-lifecycle.svg b/docs/assets/diagrams/zero-trust-lifecycle.svg new file mode 100644 index 0000000..8a81866 --- /dev/null +++ b/docs/assets/diagrams/zero-trust-lifecycle.svg @@ -0,0 +1,59 @@ +Ingress & Perimeter GatewaySecurity Filter TierApplication & Persistence LayerClient / API ConsumerAPI Gateway / WAFRequestIdFilter (MDC Correlation)RequestLoggingFilter (Audit & Timing)JwtAuthenticationFilter (HMAC-SHA256)IdempotencyFilter (SHA-256 Hashing)Domain & InvariantsConcurrency & Ledger TierPrincipal Verification (SecurityUtils)TransactionService (ACID Invariants)Deterministic Alphabetical Lock OrderingSELECT ... FOR UPDATE (PostgreSQL)Immutable Double-Entry Ledger HTTPS / TLS 1.3TLS 1.3 & Bearer JWTMDC Correlated & Idempotency Key Checked + + + + + diff --git a/docs/assets/icons/ai.svg b/docs/assets/icons/ai.svg new file mode 100644 index 0000000..231eada --- /dev/null +++ b/docs/assets/icons/ai.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/icons/client.svg b/docs/assets/icons/client.svg new file mode 100644 index 0000000..608bdb3 --- /dev/null +++ b/docs/assets/icons/client.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/icons/docker.svg b/docs/assets/icons/docker.svg new file mode 100644 index 0000000..6a9d98c --- /dev/null +++ b/docs/assets/icons/docker.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/icons/gateway.svg b/docs/assets/icons/gateway.svg new file mode 100644 index 0000000..b02833e --- /dev/null +++ b/docs/assets/icons/gateway.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/icons/github.svg b/docs/assets/icons/github.svg new file mode 100644 index 0000000..32cc724 --- /dev/null +++ b/docs/assets/icons/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/icons/grafana.svg b/docs/assets/icons/grafana.svg new file mode 100644 index 0000000..0b0bf22 --- /dev/null +++ b/docs/assets/icons/grafana.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/icons/java.svg b/docs/assets/icons/java.svg new file mode 100644 index 0000000..122330b --- /dev/null +++ b/docs/assets/icons/java.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/icons/kafka.svg b/docs/assets/icons/kafka.svg new file mode 100644 index 0000000..7344e5d --- /dev/null +++ b/docs/assets/icons/kafka.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/icons/kubernetes.svg b/docs/assets/icons/kubernetes.svg new file mode 100644 index 0000000..4abfef0 --- /dev/null +++ b/docs/assets/icons/kubernetes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/icons/lock.svg b/docs/assets/icons/lock.svg new file mode 100644 index 0000000..1294b89 --- /dev/null +++ b/docs/assets/icons/lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/icons/postgresql.svg b/docs/assets/icons/postgresql.svg new file mode 100644 index 0000000..48c9641 --- /dev/null +++ b/docs/assets/icons/postgresql.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/icons/prometheus.svg b/docs/assets/icons/prometheus.svg new file mode 100644 index 0000000..0df90fb --- /dev/null +++ b/docs/assets/icons/prometheus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/icons/queue.svg b/docs/assets/icons/queue.svg new file mode 100644 index 0000000..b3d9a17 --- /dev/null +++ b/docs/assets/icons/queue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/icons/redis.svg b/docs/assets/icons/redis.svg new file mode 100644 index 0000000..dabd8ae --- /dev/null +++ b/docs/assets/icons/redis.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/icons/server.svg b/docs/assets/icons/server.svg new file mode 100644 index 0000000..0d079d6 --- /dev/null +++ b/docs/assets/icons/server.svg @@ -0,0 +1 @@ + diff --git a/docs/assets/icons/shield.svg b/docs/assets/icons/shield.svg new file mode 100644 index 0000000..21402bf --- /dev/null +++ b/docs/assets/icons/shield.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/icons/spring.svg b/docs/assets/icons/spring.svg new file mode 100644 index 0000000..6e76563 --- /dev/null +++ b/docs/assets/icons/spring.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/coverage.md b/docs/coverage.md new file mode 100644 index 0000000..21395d8 --- /dev/null +++ b/docs/coverage.md @@ -0,0 +1,83 @@ +# 📊 Automated Code Coverage & Quality Metrics + +[![Coverage](https://img.shields.io/badge/Coverage-90%25%20Line%20%7C%2073%25%20Branch-brightgreen?style=flat-square)](https://shashankch.github.io/payflow-api/coverage-report/) +[![Quality Gate](https://img.shields.io/badge/SpotBugs-Passed%20(0%20Bugs)-brightgreen?style=flat-square)](https://github.com/shashankch/payflow-api/actions/workflows/ci.yml) +[![Tests](https://img.shields.io/badge/Tests-191%20Passed%20%7C%200%20Failed-brightgreen?style=flat-square)](https://github.com/shashankch/payflow-api/actions/workflows/ci.yml) + +The Payflow API continuous integration pipeline strictly enforces automated bytecode static analysis via **SpotBugs** and bundle-level code coverage thresholds via **JaCoCo** (`jacoco-maven-plugin:0.8.15`). + +--- + +## 🎯 Coverage Quality Thresholds & Status + +| Quality Dimension | Enforced Gate Threshold | Current Achieved Status | Status | +| :--- | :--- | :--- | :--- | +| **Line Coverage** | `80%` minimum | **90%** (across core business packages) | :white_check_mark: **PASSED** | +| **Branch Coverage** | `70%` minimum | **73%** (across complex conditional paths) | :white_check_mark: **PASSED** | +| **Unit & Integration Tests** | `100%` passing | **191 / 191 tests** (0 failures, 0 errors) | :white_check_mark: **PASSED** | +| **Missed Classes** | `0` missed | **0 missed classes** | :white_check_mark: **PASSED** | +| **SpotBugs Bytecode Audit** | Max Priority: Low (0 bugs) | **0 bugs, 0 errors** detected | :white_check_mark: **PASSED** | + +--- + +## 🚀 Live Interactive JaCoCo Report + +The complete, class-by-class interactive JaCoCo coverage report is automatically generated on every build and published alongside the documentation portal. + +

+ + 📊 Launch Interactive JaCoCo Code Coverage Report → + +

+ +> [!NOTE] +> Every page within the interactive JaCoCo report features an integrated, persistent top navigation header allowing seamless single-click bidirectional navigation: +> +> - `← Back to Documentation Portal`: Returns directly to the root documentation homepage. +> - `📊 Coverage Quality Gates`: Returns directly to this Quality Gate & Thresholds summary page. + +--- + +## 📦 Package Coverage Breakdown + +Coverage enforcement is focused on operational correctness, concurrency invariants, and data integrity: + +| Package | Key Architectural Responsibilities | Coverage Profile | +| :--- | :--- | :--- | +| `com.payflow.service` | `TransactionService`, `UserService`, `SpendInsightsService`, `RedissonDistributedLockService` | High Line & Branch coverage; all edge cases tested | +| `com.payflow.filter` | `IdempotencyFilter`, `RequestLoggingFilter`, request payload SHA-256 caching | High branch coverage for in-flight contention & key reuse | +| `com.payflow.security` | `JwtAuthenticationFilter`, `JwtTokenProvider`, `SecurityUtils`, RBAC | 100% path coverage for authentication & principal authorization | +| `com.payflow.resilience` | `PerUserRateLimiterAspect`, `UserRateLimiterService`, Circuit Breaker | Full verification of token bucket permissions & 429 translation | +| `com.payflow.controller` | `TransactionController`, `UserController`, `AuthController` | WebMvcTest verification of HTTP status codes & RFC 7807 payloads | +| `com.payflow.event` | `TransferCompletedEvent`, `TransferEventListener`, Kafka externalization | Verification of transactional outbox event lifecycle | + +--- + +## 🛡️ Excluded Packages & Justification + +Following industry best practices ([ADR-030](adr/0030-ci-cd-quality-gates-jacoco-spotbugs.md)), purely declarative components without executable logic are excluded from bundle-level coverage metrics to prevent artificial metric skew: + +1. **Entities & Value Objects** (`com/payflow/entity/*`): Pure JPA entities with getters/setters/builders. +2. **Data Transfer Objects** (`com/payflow/dto/*`): Validation records and request/response payloads. +3. **Generated Mappers** (`com/payflow/mapper/*Impl*`): Compile-time generated MapStruct mapping bytecode. +4. **Spring Configuration Classes** (`com/payflow/config/*`): Declarative `@Bean` wiring definitions. + + diff --git a/docs/diagrams/cache-aside-workflow.d2 b/docs/diagrams/cache-aside-workflow.d2 new file mode 100644 index 0000000..edc18a1 --- /dev/null +++ b/docs/diagrams/cache-aside-workflow.d2 @@ -0,0 +1,32 @@ +shape: sequence_diagram + +client: API Client / Controller { + icon: "../assets/icons/client.svg" +} +service: UserService / TxService { + icon: "../assets/icons/spring.svg" +} +cache: CacheManager (Redis / Caffeine) { + icon: "../assets/icons/redis.svg" +} +db: PostgreSQL Database { + icon: "../assets/icons/postgresql.svg" +} + +client -> service: "Read User Profile / Ledger" +service -> cache: "GET key (e.g. users::refId)" + +cache -> service: "[Cache Hit] Return cached JSON" +service -> client: "Return UserResponse / LedgerResponse" + +cache -> service: "[Cache Miss] Return null" +service -> db: "SELECT query" +db -> service: "Entity record" +service -> cache: "PUT key with configured TTL" +service -> client: "Return UserResponse / LedgerResponse" + +client -> service: "Transfer Funds / Register User" +service -> db: "Execute ACID transaction & commit" +db -> service: "Transaction committed" +service -> cache: "EVICT affected caches (users, user_ledgers)" +service -> client: "Return Success Response" diff --git a/docs/diagrams/deadlock-elimination.d2 b/docs/diagrams/deadlock-elimination.d2 new file mode 100644 index 0000000..bd02c13 --- /dev/null +++ b/docs/diagrams/deadlock-elimination.d2 @@ -0,0 +1,23 @@ +shape: sequence_diagram + +tx1: "Transaction 1 (Aarav -> Priya)" { + icon: "../assets/icons/java.svg" +} +db: "PostgreSQL Engine" { + icon: "../assets/icons/postgresql.svg" +} +tx2: "Transaction 2 (Priya -> Aarav)" { + icon: "../assets/icons/java.svg" +} + +tx1 -> db: "Deterministic Ordering: 'aarav@payflow' < 'priya@payflow'" { + style.stroke-dash: 3 +} + +tx1 -> db: "Lock 'aarav@payflow' (ACQUIRED)" +tx2 -> db: "Lock 'aarav@payflow' (BLOCKED / WAITING)" +tx1 -> db: "Lock 'priya@payflow' (ACQUIRED)" +tx1 -> db: "Debit Aarav, Credit Priya, Commit & Release" +db -> tx2: "Lock 'aarav@payflow' (GRANTED)" +tx2 -> db: "Lock 'priya@payflow' (ACQUIRED)" +tx2 -> db: "Debit Priya, Credit Aarav, Commit & Release" diff --git a/docs/diagrams/distributed-lock-sequence.d2 b/docs/diagrams/distributed-lock-sequence.d2 new file mode 100644 index 0000000..81643a6 --- /dev/null +++ b/docs/diagrams/distributed-lock-sequence.d2 @@ -0,0 +1,36 @@ +shape: sequence_diagram + +client: API Client / Ingress { + icon: "../assets/icons/client.svg" +} +pod1: Pod 1 (IdempotencyFilter) { + icon: "../assets/icons/server.svg" +} +pod2: Pod 2 (IdempotencyFilter) { + icon: "../assets/icons/server.svg" +} +redis: Redis Lock (Redisson) { + icon: "../assets/icons/redis.svg" +} +db: PostgreSQL Database { + icon: "../assets/icons/postgresql.svg" +} + +client -> pod1: "1. POST /api/v1/transactions (Key: tx-123)" +client -> pod2: "2. POST /api/v1/transactions (Key: tx-123 [Duplicate])" + +pod1 -> redis: "3. tryLock(\"payflow:lock:idemp:tx-123\", wait=2s, lease=10s)" +redis -> pod1: "4. Lock Acquired (true)" +pod2 -> redis: "5. tryLock(\"payflow:lock:idemp:tx-123\", wait=2s, lease=10s) [Blocks up to 2s]" + +pod1 -> db: "6. Check & record PROCESSING status" +pod1 -> db: "7. Execute transaction & atomic commit" +pod1 -> db: "8. Record SUCCESS with response body" +pod1 -> redis: "9. unlock(\"payflow:lock:idemp:tx-123\")" +redis -> pod1: "10. Lock Released" +pod1 -> client: "11. 201 Created (Transaction Response)" + +redis -> pod2: "12. Lock Acquired (after Pod 1 release)" +pod2 -> db: "13. SELECT idempotency record (Status: SUCCESS)" +pod2 -> client: "14. 201 Created (Replay Cached Response)" +pod2 -> redis: "15. unlock(\"payflow:lock:idemp:tx-123\")" diff --git a/docs/diagrams/idempotency-lifecycle.d2 b/docs/diagrams/idempotency-lifecycle.d2 new file mode 100644 index 0000000..6bfb314 --- /dev/null +++ b/docs/diagrams/idempotency-lifecycle.d2 @@ -0,0 +1,32 @@ +shape: sequence_diagram + +client: HTTP Client { + icon: "../assets/icons/client.svg" +} +filter: IdempotencyFilter { + icon: "../assets/icons/lock.svg" +} +db: PostgreSQL (idempotency_registry) { + icon: "../assets/icons/postgresql.svg" +} +service: TransactionService { + icon: "../assets/icons/spring.svg" +} + +client -> filter: "1. POST /api/v1/transactions (Header: Idempotency-Key)" +filter -> filter: "2. Validate key regex & compute SHA-256(body)" +filter -> db: "3. SELECT * FROM idempotency_registry WHERE key = ?" + +filter -> db: "4. (Case: Key Not Found) INSERT key, hash, status=PROCESSING" +filter -> service: "5. doFilterInternal() -> sendMoney(request)" +service -> filter: "6. HTTP 201 Created (TransactionResponse)" +filter -> db: "7. UPDATE status=SUCCESS, code=201, body=..." +filter -> client: "8. HTTP 201 Created (Original Response)" + +client -> filter: "9. [Duplicate Request] Same Key & Body" +filter -> db: "10. SELECT * FROM idempotency_registry WHERE key = ?" +filter -> client: "11. Replay Cached HTTP 201 Response" + +client -> filter: "12. [Tampered Request] Same Key & Different Body" +filter -> filter: "13. Compare SHA-256 checksums -> Mismatch detected" +filter -> client: "14. 400 Bad Request (Key Reuse with Different Payload)" diff --git a/docs/diagrams/kubernetes-deployment-topology.d2 b/docs/diagrams/kubernetes-deployment-topology.d2 new file mode 100644 index 0000000..d6de1c3 --- /dev/null +++ b/docs/diagrams/kubernetes-deployment-topology.d2 @@ -0,0 +1,92 @@ +direction: down + +ingress: Ingress Controller / API Gateway { + shape: rectangle + icon: "../assets/icons/gateway.svg" +} + +cluster: Kubernetes Production Cluster (Namespace: default) { + service: payflow-service { + shape: rectangle + icon: "../assets/icons/kubernetes.svg" + label: "payflow-service (ClusterIP:8080)" + } + + workloads: Pod Replica Set (HPA 2 to 10 Pods) { + pod1: Pod payflow-api-1 { + shape: rectangle + icon: "../assets/icons/docker.svg" + label: "Pod 1 (payflow:10001)\nVirtual Threads Enabled" + } + + pod2: Pod payflow-api-2 { + shape: rectangle + icon: "../assets/icons/docker.svg" + label: "Pod 2 (payflow:10001)\nVirtual Threads Enabled" + } + + podN: Pod payflow-api-N { + shape: rectangle + icon: "../assets/icons/docker.svg" + label: "Pod N (Auto-Scaled)" + } + } + + controls: Cluster Governance & Scaling { + hpa: HorizontalPodAutoscaler { + shape: rectangle + icon: "../assets/icons/kubernetes.svg" + label: "HPA (Min: 2, Max: 10)\nTarget: CPU 75%, Mem 80%" + } + + pdb: PodDisruptionBudget { + shape: rectangle + icon: "../assets/icons/shield.svg" + label: "PDB (minAvailable: 1)" + } + + config: ConfigMap & Secrets { + shape: rectangle + icon: "../assets/icons/lock.svg" + label: "payflow-config & payflow-secrets" + } + } + + service -> workloads.pod1: "Round Robin" + service -> workloads.pod2: "Round Robin" + service -> workloads.podN: "Round Robin" + + controls.hpa -> workloads: "Scale Replicas" + controls.pdb -> workloads: "Quorum Protection" + controls.config -> workloads: "envFrom Injection" +} + +ingress -> cluster.service: "TLS 1.3 / HTTP" + +backing: Managed External Backing Services { + postgres: PostgreSQL 17 { + shape: cylinder + icon: "../assets/icons/postgresql.svg" + label: "PostgreSQL 17 Database" + } + + redis: Redis 7 Cluster { + shape: cylinder + icon: "../assets/icons/redis.svg" + label: "Redis 7 Cluster (Redlock & Cache)" + } + + kafka: Apache Kafka (KRaft) { + shape: queue + icon: "../assets/icons/kafka.svg" + label: "Apache Kafka Broker (KRaft Mode)" + } +} + +cluster.workloads.pod1 -> backing.postgres: "HikariCP (Max 20 Pool)" +cluster.workloads.pod1 -> backing.redis: "Lettuce & Redisson" +cluster.workloads.pod1 -> backing.kafka: "payflow.transfers" + +cluster.workloads.pod2 -> backing.postgres +cluster.workloads.pod2 -> backing.redis +cluster.workloads.pod2 -> backing.kafka diff --git a/docs/diagrams/security-filter-chain.d2 b/docs/diagrams/security-filter-chain.d2 new file mode 100644 index 0000000..f506ee8 --- /dev/null +++ b/docs/diagrams/security-filter-chain.d2 @@ -0,0 +1,34 @@ +shape: sequence_diagram + +client: HTTP Client { + icon: "../assets/icons/client.svg" +} +filter: JwtAuthenticationFilter { + icon: "../assets/icons/shield.svg" +} +provider: JwtTokenProvider { + icon: "../assets/icons/lock.svg" +} +context: SecurityContextHolder { + icon: "../assets/icons/spring.svg" +} +controller: Protected Controller { + icon: "../assets/icons/java.svg" +} +entrypoint: JwtAuthenticationEntryPoint { + icon: "../assets/icons/shield.svg" +} + +client -> filter: "1. Request with Authorization: Bearer " +filter -> provider: "2. validateToken(token)" + +provider -> filter: "3a. (Case: Invalid / Expired Signature) false" +filter -> entrypoint: "4a. commence() Exception translation" +entrypoint -> client: "5a. 401 Unauthorized (RFC 7807 ProblemDetail)" + +provider -> filter: "3b. (Case: Valid Token) true" +filter -> provider: "4b. getUpiIdFromToken(token)" +provider -> filter: "5b. upiId & referenceId claims" +filter -> context: "6b. setAuthentication(UsernamePasswordAuthenticationToken)" +filter -> controller: "7b. FilterChain.doFilter(req, res)" +controller -> client: "8b. 200 OK / 201 Created Response" diff --git a/docs/diagrams/system-overview.d2 b/docs/diagrams/system-overview.d2 new file mode 100644 index 0000000..baf7fed --- /dev/null +++ b/docs/diagrams/system-overview.d2 @@ -0,0 +1,77 @@ +direction: down + +client: Client / Mobile App { + shape: rectangle + icon: "../assets/icons/client.svg" +} + +ingress: Ingress / API Gateway { + shape: rectangle + icon: "../assets/icons/gateway.svg" +} + +security: Spring Security (JWT Filter) { + shape: rectangle + icon: "../assets/icons/shield.svg" +} + +idemp: Idempotency Filter (SHA-256) { + shape: rectangle + icon: "../assets/icons/lock.svg" +} + +controller: TransactionController { + shape: rectangle + icon: "../assets/icons/spring.svg" +} + +service: TransactionService { + shape: rectangle + icon: "../assets/icons/java.svg" +} + +client -> ingress: "HTTP POST (Idempotency-Key)" +ingress -> security: "TLS 1.3" +security -> idemp: "Bearer JWT" +idemp -> controller: "Validated Request" +controller -> service: "sendMoney()" + +core_storage: Core Concurrency & Storage { + postgres: PostgreSQL 17 { + shape: cylinder + icon: "../assets/icons/postgresql.svg" + } + redis: Redis 7 (Redlock & Cache) { + shape: cylinder + icon: "../assets/icons/redis.svg" + } +} + +async_events: Asynchronous Messaging & Events { + outbox: Transactional Outbox { + shape: cylinder + icon: "../assets/icons/queue.svg" + } + kafka: Apache Kafka (KRaft) { + shape: queue + icon: "../assets/icons/kafka.svg" + } + outbox -> kafka: "spring-modulith-events-kafka" +} + +ai_resilience: AI & Resilience { + ai: Ollama Gen-AI { + shape: rectangle + icon: "../assets/icons/ai.svg" + } + r4j: Resilience4j { + shape: rectangle + icon: "../assets/icons/shield.svg" + } +} + +service -> core_storage.postgres: "Row Lock & Ledger Entries" +service -> core_storage.redis: "Redlock & Cache-Aside" +service -> async_events.outbox: "Atomically Persist Event" +service -> ai_resilience.ai: "Spend Insights" +service -> ai_resilience.r4j: "Rate Limiting Guard" diff --git a/docs/diagrams/system-topology.d2 b/docs/diagrams/system-topology.d2 new file mode 100644 index 0000000..f91637f --- /dev/null +++ b/docs/diagrams/system-topology.d2 @@ -0,0 +1,99 @@ +direction: down + +client: Client App { + shape: rectangle + icon: "../assets/icons/client.svg" +} + +gateway: API Gateway / Ingress { + shape: rectangle + icon: "../assets/icons/gateway.svg" + tooltip: "Spring Security / JWT / v1 Versioning" +} + +client -> gateway: "HTTP POST (Idempotency-Key)" + +k8s: Kubernetes Pod / Spring Boot Container { + controllers: REST Controllers { + shape: rectangle + icon: "../assets/icons/spring.svg" + label: "UserController / TransactionController" + } + + services: Domain Services { + shape: rectangle + icon: "../assets/icons/java.svg" + label: "TransactionService / UserService / SpendInsightsService" + } + + concurrency: Concurrency & Idempotency { + shape: rectangle + icon: "../assets/icons/lock.svg" + label: "Pessimistic Lock / Redisson Lock / Idempotency" + } + + outbox: Spring Modulith Outbox { + shape: rectangle + icon: "../assets/icons/queue.svg" + label: "ApplicationEventPublisher -> event_publication" + } + + telemetry: Observability Pipeline { + shape: rectangle + icon: "../assets/icons/shield.svg" + label: "OTel Tracing + Structured MDC Logging" + } + + controllers -> services: "DTO Delegation" + services -> concurrency: "Lock & Idempotency" + services -> outbox: "Outbox Publisher" +} + +gateway -> k8s.controllers: "Bearer JWT Authentication" + +stores: Persistence & Streaming Tier { + postgres: PostgreSQL 17 Database { + shape: cylinder + icon: "../assets/icons/postgresql.svg" + label: "PostgreSQL 17\n(Ledger, Transactions, Locks)" + } + + redis: Redis 7 Cluster { + shape: cylinder + icon: "../assets/icons/redis.svg" + label: "Redis 7 (Redlock & Cache-Aside)" + } + + kafka: Apache Kafka (KRaft) { + shape: queue + icon: "../assets/icons/kafka.svg" + label: "Apache Kafka Broker\n(topic: payflow.transfers)" + } + + ai_engine: Ollama Gen-AI { + shape: rectangle + icon: "../assets/icons/ai.svg" + label: "Ollama Gen-AI (Spend Insights)" + } +} + +monitoring: Metrics & Telemetry { + prometheus: Prometheus Server { + shape: rectangle + icon: "../assets/icons/prometheus.svg" + } + grafana: Grafana Dashboards { + shape: rectangle + icon: "../assets/icons/grafana.svg" + } + grafana -> prometheus: "PromQL Queries" +} + +k8s.concurrency -> stores.postgres: "SELECT ... FOR UPDATE" +k8s.concurrency -> stores.redis: "Redlock RLock (2s wait, 10s lease)" +k8s.services -> stores.redis: "Cache-Aside (users, user_ledgers)" +k8s.services -> stores.ai_engine: "Structured LLM Prompt" +k8s.outbox -> stores.postgres: "Atomic Outbox Write" +k8s.outbox -> stores.kafka: "spring-modulith-events-kafka" + +monitoring.prometheus -> k8s.controllers: "Scrape /actuator/prometheus" diff --git a/docs/diagrams/transactional-outbox-flow.d2 b/docs/diagrams/transactional-outbox-flow.d2 new file mode 100644 index 0000000..61a78c5 --- /dev/null +++ b/docs/diagrams/transactional-outbox-flow.d2 @@ -0,0 +1,35 @@ +shape: sequence_diagram + +client: HTTP Client { + icon: "../assets/icons/client.svg" +} +service: TransactionService { + icon: "../assets/icons/java.svg" +} +publisher: ApplicationEventPublisher { + icon: "../assets/icons/spring.svg" +} +db: PostgreSQL (Atomicity Boundary) { + icon: "../assets/icons/postgresql.svg" +} +listener: TransferEventListener { + icon: "../assets/icons/queue.svg" +} +kafka: Apache Kafka (payflow.transfers) { + icon: "../assets/icons/kafka.svg" +} + +client -> service: "1. sendMoney(TransferMoneyRequest)" +service -> db: "2. [Single ACID TX] SELECT ... FOR UPDATE (Row Lock Sender & Receiver)" +service -> db: "3. UPDATE users SET balance = balance - amount (Sender)" +service -> db: "4. UPDATE users SET balance = balance + amount (Receiver)" +service -> db: "5. INSERT INTO balance_ledger (DEBIT & CREDIT rows)" +service -> db: "6. INSERT INTO transactions (status=COMPLETED)" +service -> publisher: "7. publishEvent(TransferCompletedEvent)" +publisher -> db: "8. INSERT INTO event_publication (id, listener_id, event_type, payload)" +service -> client: "9. 201 Created (TransactionResponse) [TX Commits Atomically]" + +publisher -> listener: "10. [Post-Commit Async] onTransferCompleted(event)" +listener -> kafka: "11. (prod profile) ProducerRecord(topic=payflow.transfers, key=senderUpi)" +kafka -> listener: "12. acks=all RecordMetadata (offset, partition)" +listener -> db: "13. UPDATE event_publication SET completion_date = NOW() WHERE id = ?" diff --git a/docs/diagrams/upi-validation-fallback.d2 b/docs/diagrams/upi-validation-fallback.d2 new file mode 100644 index 0000000..7610a75 --- /dev/null +++ b/docs/diagrams/upi-validation-fallback.d2 @@ -0,0 +1,40 @@ +shape: sequence_diagram + +client: Mobile Client { + icon: "../assets/icons/client.svg" +} +controller: UserController { + icon: "../assets/icons/spring.svg" +} +service: UserService { + icon: "../assets/icons/java.svg" +} +validator: UpiValidationService { + icon: "../assets/icons/shield.svg" +} +http_client: UpiValidationClient { + icon: "../assets/icons/gateway.svg" +} +gateway: Banking Network Gateway { + icon: "../assets/icons/server.svg" +} + +client -> controller: "1. POST /api/v1/users (CreateUserRequest)" +controller -> service: "2. registerUser(request)" +service -> validator: "3. validateUpi(upiId)" + +validator -> http_client: "4. verify(upiId)" +http_client -> gateway: "5. GET /api/v1/upi/verify/{upiId}" + +gateway -> http_client: "6a. (Case: Valid UPI) 200 OK { valid: true }" +http_client -> validator: "7a. UpiVerificationResponse(valid=true)" +validator -> service: "8a. Validation Passed" +service -> controller: "9a. Persist User Entity to DB" +controller -> client: "10a. 201 Created (UserResponse)" + +gateway -> http_client: "6b. (Case: Downstream 503 / Timeout)" +http_client -> validator: "7b. Retries with backoff (500ms, 1000ms)" +validator -> validator: "8b. @Recover fallback triggered" +validator -> service: "9b. Proceed with Registration (Warning Logged)" +service -> controller: "10b. Persist User Entity (Zero Onboarding Outage)" +controller -> client: "11b. 201 Created (Non-blocking Fallback)" diff --git a/docs/diagrams/user-rate-limiting.d2 b/docs/diagrams/user-rate-limiting.d2 new file mode 100644 index 0000000..8951331 --- /dev/null +++ b/docs/diagrams/user-rate-limiting.d2 @@ -0,0 +1,31 @@ +shape: sequence_diagram + +client: Client / User A { + icon: "../assets/icons/client.svg" +} +filter: JwtAuthenticationFilter { + icon: "../assets/icons/shield.svg" +} +resolver: SecurityContextRateLimiterKeyResolver { + icon: "../assets/icons/shield.svg" +} +service: UserRateLimiterService { + icon: "../assets/icons/spring.svg" +} +reg: RateLimiterRegistry { + icon: "../assets/icons/lock.svg" +} +core: TransactionService { + icon: "../assets/icons/java.svg" +} + +client -> filter: "POST /api/v1/transactions (Bearer JWT)" +filter -> resolver: "Resolve Principal (alice@payflow)" +resolver -> service: "Key = \"transferLimiter:alice@payflow\"" +service -> reg: "rateLimiter(key, \"transferLimiter\")" +service -> reg: "Inherits base 10 req/s template & tracks access timestamp" { + style.stroke-dash: 3 +} +service -> core: "[Permitted <= 10 req/s] Proceed with transfer" +core -> client: "201 Created (TransactionResponse)" +service -> client: "[Exceeded > 10 req/s] 429 Too Many Requests (Retry-After: 1)" diff --git a/docs/diagrams/zero-trust-lifecycle.d2 b/docs/diagrams/zero-trust-lifecycle.d2 new file mode 100644 index 0000000..1909213 --- /dev/null +++ b/docs/diagrams/zero-trust-lifecycle.d2 @@ -0,0 +1,74 @@ +direction: down + +ingress: Ingress & Perimeter Gateway { + grid-columns: 2 + + client: Client / API Consumer { + shape: rectangle + icon: "../assets/icons/client.svg" + } + waf: API Gateway / WAF { + shape: rectangle + icon: "../assets/icons/gateway.svg" + } + client -> waf: "HTTPS / TLS 1.3" +} + +filters: Security Filter Tier { + grid-columns: 4 + + req_id: RequestIdFilter (MDC Correlation) { + shape: rectangle + icon: "../assets/icons/shield.svg" + } + rl: RequestLoggingFilter (Audit & Timing) { + shape: rectangle + icon: "../assets/icons/shield.svg" + } + jwt: JwtAuthenticationFilter (HMAC-SHA256) { + shape: rectangle + icon: "../assets/icons/lock.svg" + } + idemp: IdempotencyFilter (SHA-256 Hashing) { + shape: rectangle + icon: "../assets/icons/lock.svg" + } + + req_id -> rl -> jwt -> idemp +} + +app_persistence: Application & Persistence Layer { + grid-columns: 2 + + domain: Domain & Invariants { + auth: Principal Verification (SecurityUtils) { + shape: rectangle + icon: "../assets/icons/shield.svg" + } + tx: TransactionService (ACID Invariants) { + shape: rectangle + icon: "../assets/icons/spring.svg" + } + auth -> tx + } + + data: Concurrency & Ledger Tier { + ordering: Deterministic Alphabetical Lock Ordering { + shape: rectangle + } + pessimistic: SELECT ... FOR UPDATE (PostgreSQL) { + shape: cylinder + icon: "../assets/icons/postgresql.svg" + } + ledger: Immutable Double-Entry Ledger { + shape: cylinder + icon: "../assets/icons/postgresql.svg" + } + ordering -> pessimistic -> ledger + } + + domain.tx -> data.ordering +} + +ingress -> filters: "TLS 1.3 & Bearer JWT" +filters -> app_persistence: "MDC Correlated & Idempotency Key Checked" diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..180e8a1 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,185 @@ +# ₹ Payflow API + +### Enterprise Transaction & Double-Entry Payment Ledger Engine + +

+ CI Build + JaCoCo Coverage + Java 25 + Spring Boot 4.1.0 + Docker + Kubernetes + JUnit 5 Tests + Modular Monolith + License: MIT +

+ +A high-concurrency peer-to-peer payment backend built with **Java 25** and **Spring Boot 4.1.0**. +**Guaranteed zero double-spending • Deterministic row locking • Distributed Redisson locks • Immutable balance ledger • Transactional Outbox & Kafka streaming** + +--- + +## 🚀 Key Architectural Pillars + +| Architectural Pillar | Core Guarantee & Engineering Mechanics | +| :--- | :--- | +| **🔒 Concurrency Safety** | Row-level pessimistic write locking (`SELECT ... FOR UPDATE`) paired with deterministic alphabetical lock ordering by UPI ID, eliminating race conditions and deadlocks during concurrent account debits and credits. | +| **📜 Double-Entry Ledger** | Atomic paired `DEBIT` and `CREDIT` records with strict base-10 `BigDecimal` arithmetic precision denominated in Indian Rupees (`INR`, symbol: `₹`, scale = 4) and Banker's Rounding (`HALF_EVEN`), preserving an immutable financial audit trail. | +| **🔁 Durable Idempotency** | Mandatory `Idempotency-Key` headers (validated 255-char regex boundary) backed by raw SHA-256 payload hashing to prevent tampering, coupled with Redisson distributed locking (`payflow:lock:idemp:{key}`) to coordinate mutations across multi-instance clusters. | +| **🛡️ Resilience & Fault Tolerance** | Dynamic per-user rate limiting (10 req/s, RFC 6585 `Retry-After: 1`), Resilience4j circuit breaking on external banking rails, bounded timeouts, and automatic memory eviction of inactive limiter buckets. | +| **⚡ Transactional Outbox** | Spring Modulith Event Publication Registry atomically persisting domain events (`TransferCompletedEvent`) within the database transaction, bridging to Apache Kafka without dual-write inconsistency, with automated background retention cleanup (`OutboxCleanupService`). | +| **🔐 Zero-Trust Security** | Stateless HMAC-SHA256 JWT tokens with fail-fast production secret validation, strict principal-bound sender verification, role-based access control (`ROLE_ADMIN` on user enumeration), clickjacking defense (`sameOrigin`), and RFC 9457 ProblemDetail error responses. | +| **📊 Enterprise Observability** | Native Elastic Common Schema (ECS) JSON structured logging, MDC trace correlation (`requestId`, `traceId`, `spanId`), Prometheus metrics, and profile-conditional Redis distributed caching with targeted cache eviction and Caffeine local fallback. | +| **🤖 Gen-AI Spend Insights** | Spring AI 2.0.1 integration providing automated expenditure classification and contextual budgeting tips with structured JSON output, guarded by Resilience4j circuit breakers and deterministic keyword heuristic fallback. | +| **🚀 Virtual Threads & Concurrency** | Java 25 Project Loom Virtual Threads enabled globally (`spring.threads.virtual.enabled: true`), with bounded HikariCP connection pool configurations and a low-memory `prod-light` profile designed for <= 1 GiB single-node production environments. | +| **🐳 Cloud-Native Orchestration & Quality Gates** | Hardened multi-stage Docker build (`eclipse-temurin:25-jre`, unprivileged `payflow:10001` user), full-stack Docker Compose (PostgreSQL 17, Redis 7, Kafka KRaft, Ollama, Prometheus, Grafana), Kubernetes HPA/PDB topology with zero-downtime graceful shutdown, SpotBugs static analysis, and automated JaCoCo coverage enforcement (90% Line / 73% Branch). | + +--- + +## 🏗️ System Architecture Overview + +![System Architecture Overview](assets/diagrams/system-overview.svg) + +
+📐 View Declarative D2 Diagram Source + +```d2 +direction: down + +client: Client / Mobile App { + shape: rectangle + icon: "docs/assets/icons/client.svg" +} + +ingress: Ingress / API Gateway { + shape: rectangle + icon: "docs/assets/icons/gateway.svg" +} + +security: Spring Security (JWT Filter) { + shape: rectangle + icon: "docs/assets/icons/shield.svg" +} + +idemp: Idempotency Filter (SHA-256) { + shape: rectangle + icon: "docs/assets/icons/lock.svg" +} + +controller: TransactionController { + shape: rectangle + icon: "docs/assets/icons/spring.svg" +} + +service: TransactionService { + shape: rectangle + icon: "docs/assets/icons/java.svg" +} + +client -> ingress: "HTTP POST (Idempotency-Key)" +ingress -> security: "TLS 1.3" +security -> idemp: "Bearer JWT" +idemp -> controller: "Validated Request" +controller -> service: "sendMoney()" + +core_storage: Core Concurrency & Storage { + postgres: PostgreSQL 17 { + shape: cylinder + icon: "docs/assets/icons/postgresql.svg" + } + redis: Redis 7 (Redlock & Cache) { + shape: cylinder + icon: "docs/assets/icons/redis.svg" + } +} + +async_events: Asynchronous Messaging & Events { + outbox: Transactional Outbox { + shape: cylinder + icon: "docs/assets/icons/queue.svg" + } + kafka: Apache Kafka (KRaft) { + shape: queue + icon: "docs/assets/icons/kafka.svg" + } + outbox -> kafka: "spring-modulith-events-kafka" +} + +ai_resilience: AI & Resilience { + ai: Ollama Gen-AI { + shape: rectangle + icon: "docs/assets/icons/ai.svg" + } + r4j: Resilience4j { + shape: rectangle + icon: "docs/assets/icons/shield.svg" + } +} + +service -> core_storage.postgres: "Row Lock & Ledger Entries" +service -> core_storage.redis: "Redlock & Cache-Aside" +service -> async_events.outbox: "Atomically Persist Event" +service -> ai_resilience.ai: "Spend Insights" +service -> ai_resilience.r4j: "Rate Limiting Guard" +``` +
+ +--- + +## 📚 Project Documentation Hub + +| Document | Description | +| :--- | :--- | +| 📊 **[Code Coverage & Quality Gates](coverage.md)** | Automated CI/CD quality gates, bundle-level line/branch thresholds, and SpotBugs audit | +| 🚀 **[Interactive JaCoCo Report](https://shashankch.github.io/payflow-api/coverage-report/)** | Live interactive code coverage drilldown (90% Line, 73% Branch) generated per-build | +| 📘 **[System Architecture](ARCHITECTURE.md)** | Deep-dive concurrency models, pessimistic locking mechanics, test pyramid | +| 🛡️ **[Security Architecture & Threat Model](ARCHITECTURE.md#18-security-architecture-and-threat-model)** | Zero-Trust filter chain, STRIDE threat model, IAM policy matrix, financial concurrency controls | +| 🌐 **[REST API Specification](API_SPECIFICATION.md)** | Complete REST endpoint contracts, schemas, RFC 9457 ProblemDetail payloads | +| 🗓️ **[Phased Roadmap](ROADMAP.md)** | Full 12-phase technical expansion blueprint and milestone statuses | +| 📋 **[Engineering Conventions](CONVENTIONS.md)** | Java 25 standards, Spotless/Checkstyle rules, testing guidelines | +| 📜 **[Architecture Decisions (ADRs)](adr/README.md)** | Master index of modular architectural decision records (ADR-001 through ADR-030) | +| 📝 **[Changelog](CHANGELOG.md)** | Version-by-version implementation notes | +| 🔒 **[Security Policy](SECURITY.md)** | Open-source vulnerability reporting guidelines and project security posture | +| 🤝 **[Contributing Guide](CONTRIBUTING.md)** | Development environment setup and pull request requirements | + +--- + +## ⚡ Quick Start + +=== "Local Development" + ```bash + # Start server with active 'local' profile (H2 in-memory, port 8080) + mvn spring-boot:run + ``` + - **Swagger UI Interactive Docs**: [http://localhost:8080/swagger-ui.html](http://localhost:8080/swagger-ui.html) + - **OpenAPI 3.0 JSON Specification**: [http://localhost:8080/v3/api-docs](http://localhost:8080/v3/api-docs) + - **H2 Console**: [http://localhost:8080/h2-console](http://localhost:8080/h2-console) (`jdbc:h2:mem:payupidb`, Credentials: `user` / `user`) + +=== "Docker Compose" + ```bash + # Spin up complete distributed stack (Postgres, Redis, Kafka, Ollama, Prometheus, Grafana) + docker compose up -d --build + ``` + - **Payflow API**: [http://localhost:8080](http://localhost:8080) + - **Prometheus**: [http://localhost:9090](http://localhost:9090) + - **Grafana**: [http://localhost:3000](http://localhost:3000) (Credentials: `admin` / `admin`) + - **Ollama AI**: [http://localhost:11434](http://localhost:11434) + +=== "Kubernetes (HPA & Topology)" + ```bash + # Deploy declarative manifests to Kubernetes cluster + kubectl apply -f k8s/ + ``` + - ConfigMap, Secrets, Deployment with rolling updates + - Horizontal Pod Autoscaler (HPA) min 2, max 10 + - Pod Disruption Budget (PDB) minAvailable: 1 + +=== "Quality Verification Pipeline" + ```bash + # Execute full verification pipeline: Spotless, Checkstyle, SpotBugs, Tests & JaCoCo gates + mvn clean verify -DskipITs + ``` + - **Spotless**: Code formatting verification + - **Checkstyle**: Enterprise static code linting + - **SpotBugs**: Bytecode bug pattern analysis + - **JaCoCo**: Quality gates requiring >= 80% line and >= 70% branch coverage (90% / 73% achieved across 191 tests) diff --git a/docs/javascripts/extra.js b/docs/javascripts/extra.js new file mode 100644 index 0000000..216c3d1 --- /dev/null +++ b/docs/javascripts/extra.js @@ -0,0 +1,255 @@ +/* ========================================================================== + Payflow API — MkDocs Material Interactive Enhancements + Mermaid diagram rendering, dynamic theme synchronization, interactive zoom/pan, + and fullscreen modal viewer. + ========================================================================== */ + +(function () { + "use strict"; + + // Determine current theme scheme + function getCurrentScheme() { + const palette = typeof __md_get !== "undefined" ? __md_get("__palette") : null; + if (palette && palette.color && palette.color.scheme === "slate") { + return "slate"; + } + const htmlScheme = document.documentElement.getAttribute("data-md-color-scheme"); + if (htmlScheme === "slate") return "slate"; + if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) { + return "slate"; + } + return "default"; + } + + // Ensure modal element exists + function ensureModal() { + let modal = document.getElementById("mermaid-fullscreen-modal"); + if (!modal) { + modal = document.createElement("div"); + modal.id = "mermaid-fullscreen-modal"; + modal.className = "mermaid-modal"; + modal.innerHTML = ` +
+
+ + + +
+ +
+
+
+
+ `; + document.body.appendChild(modal); + + let modalScale = 1.0; + const content = modal.querySelector(".mermaid-modal-content"); + const viewport = modal.querySelector(".mermaid-modal-viewport"); + + function updateModalScale(scale) { + modalScale = Math.min(Math.max(0.4, scale), 4.0); + content.style.transform = `scale(${modalScale})`; + } + + modal.querySelector('[data-modal-action="zoom-in"]').addEventListener("click", (e) => { + e.preventDefault(); + updateModalScale(modalScale + 0.25); + }); + + modal.querySelector('[data-modal-action="zoom-out"]').addEventListener("click", (e) => { + e.preventDefault(); + updateModalScale(modalScale - 0.25); + }); + + modal.querySelector('[data-modal-action="reset"]').addEventListener("click", (e) => { + e.preventDefault(); + updateModalScale(1.0); + viewport.scrollLeft = (content.scrollWidth - viewport.clientWidth) / 2; + viewport.scrollTop = (content.scrollHeight - viewport.clientHeight) / 2; + }); + + modal.querySelector(".mermaid-modal-close").addEventListener("click", () => { + modal.classList.remove("active"); + content.innerHTML = ""; + }); + + window.addEventListener("keydown", (e) => { + if (e.key === "Escape" && modal.classList.contains("active")) { + modal.classList.remove("active"); + content.innerHTML = ""; + } + }); + } + return modal; + } + + // Render Mermaid diagrams + async function renderMermaid() { + if (typeof mermaid === "undefined") { + console.warn("Mermaid library not loaded yet."); + return; + } + + const scheme = getCurrentScheme(); + const mermaidTheme = scheme === "slate" ? "dark" : "default"; + + mermaid.initialize({ + startOnLoad: false, + theme: mermaidTheme, + securityLevel: "loose", + fontFamily: "Inter, -apple-system, BlinkMacSystemFont, sans-serif" + }); + + const mermaidBlocks = document.querySelectorAll(".mermaid"); + if (!mermaidBlocks.length) return; + + for (const block of mermaidBlocks) { + // Save raw definition if not already saved + if (!block.getAttribute("data-processed-source")) { + block.setAttribute("data-processed-source", block.textContent.trim()); + } else if (block.getAttribute("data-rendered-theme") === mermaidTheme) { + continue; + } + + const rawSource = block.getAttribute("data-processed-source"); + const id = "mermaid-svg-" + Math.random().toString(36).substring(2, 9); + + try { + const { svg } = await mermaid.render(id, rawSource); + block.innerHTML = svg; + block.setAttribute("data-rendered-theme", mermaidTheme); + attachDiagramControls(block); + } catch (err) { + console.error("Mermaid rendering failed for block:", err); + } + } + } + + function attachDiagramControls(mermaidEl) { + if (mermaidEl.closest(".mermaid-wrapper")) return; + + const modal = ensureModal(); + + const wrapper = document.createElement("div"); + wrapper.className = "mermaid-wrapper"; + + const controls = document.createElement("div"); + controls.className = "diagram-controls"; + controls.innerHTML = ` + + + + + `; + + const viewport = document.createElement("div"); + viewport.className = "mermaid-viewport"; + + mermaidEl.parentNode.insertBefore(wrapper, mermaidEl); + viewport.appendChild(mermaidEl); + wrapper.appendChild(controls); + wrapper.appendChild(viewport); + + let currentScale = 1; + let isDragging = false; + let startX, startY, scrollLeft, scrollTop; + + function updateScale(newScale) { + currentScale = Math.min(Math.max(0.5, newScale), 3.0); + mermaidEl.style.transform = `scale(${currentScale})`; + } + + controls.querySelector('[data-action="zoom-in"]').addEventListener("click", (e) => { + e.preventDefault(); + updateScale(currentScale + 0.2); + }); + + controls.querySelector('[data-action="zoom-out"]').addEventListener("click", (e) => { + e.preventDefault(); + updateScale(currentScale - 0.2); + }); + + controls.querySelector('[data-action="reset"]').addEventListener("click", (e) => { + e.preventDefault(); + updateScale(1.0); + viewport.scrollLeft = (mermaidEl.scrollWidth - viewport.clientWidth) / 2; + viewport.scrollTop = 0; + }); + + controls.querySelector('[data-action="fullscreen"]').addEventListener("click", (e) => { + e.preventDefault(); + const svg = mermaidEl.querySelector("svg"); + const content = modal.querySelector(".mermaid-modal-content"); + content.innerHTML = ""; + if (svg) { + const clone = svg.cloneNode(true); + clone.removeAttribute("width"); + clone.removeAttribute("height"); + clone.style.width = "100%"; + clone.style.height = "100%"; + content.appendChild(clone); + } else { + content.innerHTML = mermaidEl.innerHTML; + } + content.style.transform = "scale(1)"; + modal.classList.add("active"); + }); + + // Mouse drag panning + viewport.addEventListener("mousedown", (e) => { + if (e.target.closest(".diagram-controls")) return; + isDragging = true; + startX = e.pageX - viewport.offsetLeft; + startY = e.pageY - viewport.offsetTop; + scrollLeft = viewport.scrollLeft; + scrollTop = viewport.scrollTop; + }); + + viewport.addEventListener("mouseleave", () => { isDragging = false; }); + viewport.addEventListener("mouseup", () => { isDragging = false; }); + + viewport.addEventListener("mousemove", (e) => { + if (!isDragging) return; + e.preventDefault(); + const x = e.pageX - viewport.offsetLeft; + const y = e.pageY - viewport.offsetTop; + const walkX = (x - startX) * 1.5; + const walkY = (y - startY) * 1.5; + viewport.scrollLeft = scrollLeft - walkX; + viewport.scrollTop = scrollTop - walkY; + }); + + // Wheel zoom + viewport.addEventListener("wheel", (e) => { + if (e.ctrlKey || e.metaKey || e.altKey) { + e.preventDefault(); + const delta = e.deltaY > 0 ? -0.1 : 0.1; + updateScale(currentScale + delta); + } + }, { passive: false }); + } + + // Setup on page load and instant navigation + function setup() { + renderMermaid(); + + // Listen for theme palette toggle + const toggles = document.querySelectorAll("[data-md-color-scheme]"); + const observer = new MutationObserver(() => { + renderMermaid(); + }); + observer.observe(document.body, { attributes: true, attributeFilter: ["data-md-color-scheme"] }); + observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-md-color-scheme"] }); + } + + if (typeof document$ !== "undefined") { + document$.subscribe(() => { + setTimeout(setup, 100); + }); + } else { + document.addEventListener("DOMContentLoaded", () => { + setTimeout(setup, 100); + }); + } +})(); diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 0000000..f6aef3e --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,351 @@ +/* ========================================================================== + Payflow API — MkDocs Material Theme Enhancements & Polish + High-contrast typography, dark mode visibility, diagram zoom & tab styles + ========================================================================== */ + +/* -------------------------------------------------------------------------- + Light Mode Theme Variables & Typography + -------------------------------------------------------------------------- */ +[data-md-color-scheme="default"] { + --md-primary-fg-color: #2563eb; + --md-primary-fg-color--light: #3b82f6; + --md-primary-fg-color--dark: #1d4ed8; + --md-accent-fg-color: #2563eb; + + --md-typeset-color: #0f172a; + --md-default-fg-color: #0f172a; + --md-default-fg-color--light: #334155; + --md-default-fg-color--lighter: #64748b; + --md-default-bg-color: #ffffff; + --md-default-bg-color--light: #f8fafc; + + --md-code-bg-color: #f1f5f9; + --md-code-fg-color: #0f172a; + + --diagram-bg: #f8fafc; + --diagram-border: #e2e8f0; + --table-border: #e2e8f0; + --table-head-bg: #f1f5f9; + --table-even-bg: #f8fafc; +} + +/* -------------------------------------------------------------------------- + Dark Mode Theme Variables & Contrast Hardening (Slate) + -------------------------------------------------------------------------- */ +[data-md-color-scheme="slate"] { + --md-primary-fg-color: #38bdf8; + --md-primary-fg-color--light: #7dd3fc; + --md-primary-fg-color--dark: #0284c7; + --md-accent-fg-color: #38bdf8; + + --md-typeset-color: #e2e8f0; + --md-default-fg-color: #e2e8f0; + --md-default-fg-color--light: #cbd5e1; + --md-default-fg-color--lighter: #94a3b8; + --md-default-bg-color: #0b0f19; + --md-default-bg-color--light: #111827; + + --md-code-bg-color: #0f172a; + --md-code-fg-color: #38bdf8; + + --diagram-bg: #0f172a; + --diagram-border: #1e293b; + --table-border: #1e293b; + --table-head-bg: #1e293b; + --table-even-bg: #111827; +} + +/* -------------------------------------------------------------------------- + Headings & Text Visibility Hardening (Guaranteed Sharp Contrast) + -------------------------------------------------------------------------- */ +.md-typeset h1, +.md-typeset h2, +.md-typeset h3, +.md-typeset h4, +.md-typeset h5, +.md-typeset h6 { + font-weight: 700; + letter-spacing: -0.02em; + color: var(--md-default-fg-color); +} + +[data-md-color-scheme="slate"] .md-typeset h1, +[data-md-color-scheme="slate"] .md-typeset h2, +[data-md-color-scheme="slate"] .md-typeset h3, +[data-md-color-scheme="slate"] .md-typeset h4 { + color: #f8fafc !important; +} + +[data-md-color-scheme="slate"] .md-typeset p, +[data-md-color-scheme="slate"] .md-typeset li, +[data-md-color-scheme="slate"] .md-typeset blockquote { + color: #cbd5e1; +} + +[data-md-color-scheme="slate"] .md-typeset strong { + color: #f8fafc; +} + +/* -------------------------------------------------------------------------- + Tables Styling & Responsive Layout + -------------------------------------------------------------------------- */ +.md-typeset table:not([class]) { + width: 100%; + display: table; + border-collapse: collapse; + border: 1px solid var(--table-border); + border-radius: 6px; + overflow: hidden; + margin: 1.5rem 0; + font-size: 0.88rem; +} + +.md-typeset table:not([class]) th { + background: var(--table-head-bg); + color: var(--md-default-fg-color); + font-weight: 600; + padding: 0.85rem 1rem; + border: 1px solid var(--table-border); + text-align: left; +} + +.md-typeset table:not([class]) td { + padding: 0.85rem 1rem; + border: 1px solid var(--table-border); + line-height: 1.6; +} + +.md-typeset table:not([class]) tr:nth-child(even) { + background: var(--table-even-bg); +} + +/* -------------------------------------------------------------------------- + Shields.io Badges & Header Image Elements + -------------------------------------------------------------------------- */ +.md-typeset p a img, +.md-typeset div a img { + display: inline-block; + margin: 2px 3px; + vertical-align: middle; + border-radius: 3px; + transition: transform 0.15s ease; +} + +.md-typeset p a img:hover, +.md-typeset div a img:hover { + transform: translateY(-1px); +} + +/* Center all standalone content images & architecture diagrams */ +.md-typeset p > img:only-child { + display: block; + margin: 1.5rem auto; + max-width: 100%; + height: auto; +} + +/* -------------------------------------------------------------------------- + Mermaid Diagrams & Interactive Zoom Controls + -------------------------------------------------------------------------- */ +.mermaid-wrapper { + position: relative; + background: var(--diagram-bg); + border: 1px solid var(--diagram-border); + border-radius: 8px; + margin: 2rem 0; + padding: 1.5rem 1rem; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05); + overflow: hidden; +} + +.mermaid-viewport { + overflow: auto; + display: flex; + justify-content: center; + align-items: center; + min-height: 380px; + max-height: 650px; + cursor: grab; + user-select: none; + padding: 1rem 0; +} + +.mermaid-viewport:active { + cursor: grabbing; +} + +.mermaid-viewport .mermaid { + transform-origin: center top; + transition: transform 0.15s ease-out; + display: flex; + justify-content: center; + width: 100%; +} + +.mermaid-viewport .mermaid svg { + max-width: 100% !important; + height: auto; +} + +/* Diagram Interactive Toolbar */ +.diagram-controls { + position: absolute; + top: 12px; + right: 12px; + display: flex; + gap: 6px; + background: rgba(15, 23, 42, 0.85); + backdrop-filter: blur(8px); + padding: 4px 8px; + border-radius: 6px; + border: 1px solid rgba(255, 255, 255, 0.1); + z-index: 10; + box-shadow: 0 2px 4px rgba(0,0,0,0.15); +} + +.zoom-btn { + background: transparent; + color: #f8fafc; + border: none; + cursor: pointer; + padding: 4px 8px; + font-size: 13px; + font-weight: 600; + border-radius: 4px; + transition: background 0.15s, color 0.15s; + display: flex; + align-items: center; + justify-content: center; + line-height: 1; +} + +.zoom-btn:hover { + background: rgba(255, 255, 255, 0.2); + color: #38bdf8; +} + +/* Fullscreen Modal for Diagrams */ +.mermaid-modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(11, 15, 25, 0.95); + backdrop-filter: blur(12px); + z-index: 99999; + flex-direction: column; + box-sizing: border-box; +} + +.mermaid-modal.active { + display: flex; +} + +.mermaid-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 2rem; + background: rgba(15, 23, 42, 0.85); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.mermaid-modal-controls { + display: flex; + gap: 8px; +} + +.modal-zoom-btn { + background: #1e293b; + color: #f8fafc; + border: 1px solid #475569; + padding: 6px 12px; + font-size: 13px; + font-weight: 600; + border-radius: 4px; + cursor: pointer; + transition: background 0.15s, color 0.15s; +} + +.modal-zoom-btn:hover { + background: #334155; + color: #38bdf8; +} + +.mermaid-modal-close { + background: #1e293b; + color: #f8fafc; + border: 1px solid #475569; + font-size: 14px; + font-weight: 600; + padding: 6px 14px; + border-radius: 6px; + cursor: pointer; + transition: background 0.2s; +} + +.mermaid-modal-close:hover { + background: #ef4444; + color: #ffffff; +} + +.mermaid-modal-viewport { + flex: 1; + overflow: auto; + display: flex; + justify-content: center; + align-items: center; + padding: 2rem; +} + +.mermaid-modal-content { + transform-origin: center center; + transition: transform 0.15s ease-out; + display: flex; + justify-content: center; + align-items: center; + width: 90vw; + height: 80vh; +} + +.mermaid-modal-content svg { + max-width: 95% !important; + max-height: 75vh !important; + height: auto; +} + +/* -------------------------------------------------------------------------- + Content Tabs Polish + -------------------------------------------------------------------------- */ +.md-typeset .tabbed-set { + border-radius: 8px; + overflow: hidden; + border: 1px solid var(--diagram-border); + margin: 1.5rem 0; +} + +.md-typeset .tabbed-labels { + background: var(--table-head-bg); + border-bottom: 1px solid var(--diagram-border); + padding: 0 0.5rem; +} + +.md-typeset .tabbed-content { + padding: 1rem; +} + +/* -------------------------------------------------------------------------- + Code Block Borders & Inline Code + -------------------------------------------------------------------------- */ +.md-typeset code { + border-radius: 4px; + padding: 0.15em 0.4em; + font-size: 0.85em; +} + +.highlight { + border-radius: 6px; + overflow: hidden; +} diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..4443016 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,139 @@ +site_name: Payflow API +site_description: Enterprise Transaction & Double-Entry Payment Ledger Engine +site_url: https://shashankch.github.io/payflow-api/ +repo_url: https://github.com/shashankch/payflow-api +repo_name: shashankch/payflow-api + +theme: + name: material + language: en + icon: + logo: material/currency-inr + repo: fontawesome/brands/github + palette: + # Auto preference + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + # Light theme + - scheme: default + primary: indigo + accent: indigo + media: "(prefers-color-scheme: light)" + toggle: + icon: material/brightness-7 + name: Switch to dark mode + # Dark theme + - scheme: slate + primary: indigo + accent: sky + media: "(prefers-color-scheme: dark)" + toggle: + icon: material/brightness-4 + name: Switch to system preference + font: + text: Inter + code: JetBrains Mono + features: + - navigation.instant + - navigation.tracking + - navigation.tabs + - navigation.sections + - navigation.expand + - navigation.top + - search.suggest + - search.highlight + - search.share + - content.code.copy + - content.tabs.link + +extra_css: + - stylesheets/extra.css + +extra_javascript: + - javascripts/extra.js + +plugins: + - search + - d2: + layout: dagre + theme: 0 + dark_theme: 200 + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - tables + - attr_list + - md_in_html + - toc: + permalink: true + +nav: + - Overview: + - Home: index.md + - Engineering Roadmap: ROADMAP.md + - Architecture & Design: + - System Architecture: ARCHITECTURE.md + - API & Standards: + - REST API Specification: API_SPECIFICATION.md + - Engineering Conventions: CONVENTIONS.md + - Architecture Decisions (ADRs): + - ADR Index: adr/README.md + - 0001 — BigDecimal Monetary Values: adr/0001-use-bigdecimal-for-monetary-values.md + - 0002 — Constructor Injection: adr/0002-constructor-injection-over-field-injection.md + - 0003 — Rich Domain Model: adr/0003-rich-domain-model-over-anemic-domain-model.md + - 0004 — URI Versioning & DTO Isolation: adr/0004-uri-based-api-versioning-and-dto-isolation-layer.md + - 0005 — MapStruct DTO Mapping: adr/0005-mapstruct-for-compile-time-type-safe-dto-mapping.md + - 0006 — RFC 7807 ProblemDetail: adr/0006-rfc-7807-problemdetail-and-centralized-exception-handling.md + - 0007 — UUID Reference IDs: adr/0007-uuid-reference-ids-over-auto-increment-primary-keys.md + - 0008 — Spring Modulith Monolith: adr/0008-spring-modulith-modular-monolith-over-distributed-microservices.md + - 0009 — OpenTelemetry Tracing: adr/0009-opentelemetry-distributed-tracing-via-micrometer-bridge.md + - 0010 — Pessimistic Write Locking: adr/0010-pessimistic-locking-for-high-concurrency-balance-operations.md + - 0011 — Deterministic Lock Ordering: adr/0011-deterministic-lock-ordering-for-deadlock-prevention.md + - 0012 — Double-Entry Ledger: adr/0012-double-entry-balance-ledger-as-immutable-audit-trail.md + - 0013 — Flyway Database Migrations: adr/0013-flyway-database-migrations-over-ddl-auto-generation.md + - 0014 — Spring Profiles & Testcontainers: adr/0014-spring-environment-profiles-and-testcontainers-integration-testing-strategy.md + - 0015 — SHA-256 Idempotency Engine: adr/0015-sha-256-request-payload-hashing-and-durable-idempotency-engine.md + - 0016 — Modulith Outbox Pattern: adr/0016-spring-modulith-event-publication-registry-and-transactional-outbox.md + - 0017 — Stateless JWT Authentication: adr/0017-stateless-jwt-authentication-and-spring-security-architecture.md + - 0018 — Principal-Bound Authorization: adr/0018-principal-bound-resource-access-control-and-sender-verification.md + - 0019 — Declarative RestClient: adr/0019-declarative-http-interface-client-and-resilient-external-service-integration.md + - 0020 — Structured Logging & Prometheus: adr/0020-structured-logging-prometheus-metrics-and-opentelemetry-observability.md + - 0021 — Resilience4j Rate Limiting: adr/0021-resilience4j-circuit-breaker-per-user-rate-limiting.md + - 0022 — Redis Caching & Caffeine Fallback: adr/0022-redis-distributed-caching-and-caffeine-fallback.md + - 0023 — Redisson Distributed Locking: adr/0023-redis-distributed-locking-redisson.md + - 0024 — Kafka Event Streaming: adr/0024-kafka-event-streaming-spring-modulith.md + - 0025 — Production Hardening & Remediation: adr/0025-production-hardening-and-architecture-audit-remediation.md + - 0026 — Gen-AI Spend Categorization: adr/0026-gen-ai-spend-categorization-spring-ai.md + - 0027 — Virtual Threads & Pool Tuning: adr/0027-virtual-threads-and-bounded-connection-pool.md + - 0028 — Multi-Stage Containerization: adr/0028-multi-stage-containerization-docker-compose.md + - 0029 — Kubernetes Deployment Topology: adr/0029-kubernetes-deployment-topology-hpa.md + - 0030 — CI/CD Quality Gates: adr/0030-ci-cd-quality-gates-jacoco-spotbugs.md + - Governance & Releases: + - Changelog: CHANGELOG.md + - Security Policy: SECURITY.md + - Contributing Guide: CONTRIBUTING.md + - MIT License: LICENSE.md + - Quality & Telemetry: + - 📊 Code Coverage & Quality Gates: coverage.md + - 🚀 Live JaCoCo Interactive Report: https://shashankch.github.io/payflow-api/coverage-report/ + - 🔄 CI Pipeline Status: https://github.com/shashankch/payflow-api/actions/workflows/ci.yml + - 🐙 GitHub Repository: https://github.com/shashankch/payflow-api diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0a9e234 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +mkdocs>=1.6.1 +mkdocs-material>=9.5.0 +mkdocs-d2-plugin>=1.6.0 diff --git a/scripts/generate-diagrams.sh b/scripts/generate-diagrams.sh new file mode 100755 index 0000000..d872184 --- /dev/null +++ b/scripts/generate-diagrams.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# ============================================================================== +# Payflow API — D2 Diagram Compilation & Verification Automation +# Compiles declarative D2 sources in docs/diagrams/ to self-contained SVGs +# in docs/assets/diagrams/ with automatic dark/light theme support. +# ============================================================================== + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +DIAGRAMS_DIR="$REPO_ROOT/docs/diagrams" +OUTPUT_DIR="$REPO_ROOT/docs/assets/diagrams" + +# Ensure D2 CLI is installed +if ! command -v d2 &> /dev/null; then + echo "❌ Error: D2 CLI is not installed or not in PATH." + echo " Install via Homebrew: brew install d2" + echo " Install via script: curl -fsSL https://d2lang.com/install.sh | sh -s --" + exit 1 +fi + +mkdir -p "$OUTPUT_DIR" + +CHECK_MODE=false +if [[ "${1:-}" == "--check" ]]; then + CHECK_MODE=true +fi + +echo "🎨 Compiling D2 diagrams from $DIAGRAMS_DIR to $OUTPUT_DIR..." + +compiled_count=0 +for d2file in "$DIAGRAMS_DIR"/*.d2; do + [ -f "$d2file" ] || continue + name="$(basename "$d2file" .d2)" + target_svg="$OUTPUT_DIR/$name.svg" + + # Compile with theme 0 (default/light) and dark-theme 200 (slate/dark) + # ELK layout provides superior orthogonal routing and generous container padding + d2 --layout=elk \ + --elk-nodeNodeBetweenLayers=100 \ + --elk-padding="[top=80,left=50,bottom=50,right=50]" \ + --elk-edgeNodeBetweenLayers=60 \ + --theme=0 \ + --dark-theme=200 \ + "$d2file" "$target_svg" + compiled_count=$((compiled_count + 1)) + echo " ✅ Compiled: $name.d2 -> $name.svg" +done + +echo "🎉 Successfully compiled $compiled_count D2 diagrams." + +if [ "$CHECK_MODE" = true ]; then + echo "🔍 Checking for uncommitted diagram drift..." + if ! git diff --exit-code "$OUTPUT_DIR" > /dev/null; then + echo "❌ Error: Pre-compiled D2 SVGs are out of sync with docs/diagrams/*.d2 sources!" + echo " Please run './scripts/generate-diagrams.sh' locally and commit the updated SVGs." + git diff --stat "$OUTPUT_DIR" + exit 1 + fi + echo "✅ All diagram SVGs are up-to-date with zero drift." +fi