# CreditFair Python Backend — Claude Code Guide

## Project Context
Migration of a Laravel 5.5 monolithic PHP backend → FastAPI (Python) using Clean Architecture + DDD.
- **Original repo (reference):** `D:\PHPAdmin\creditfairadmin`
- **Flutter frontend:** `D:\PHPAdmin\creditfair_flutter_frontend`
- **This repo:** `D:\PHPAdmin\creditfair_python_backend`

---

## Architecture: Clean Architecture + DDD

Dependencies point **inward**. Outer layers know about inner layers. Inner layers know nothing about outer layers.

```
presentation/  →  application/  →  domain/
infrastructure/  →  domain/
```

### Layer Map

| Layer | Path | Responsibility |
|-------|------|----------------|
| Domain | `src/domain/entities/` | Pure dataclasses, zero framework imports |
| Domain | `src/domain/interfaces/` | Abstract base classes (contracts) |
| Application | `src/application/use_cases/` | Orchestration logic, one class per feature |
| Infrastructure | `src/infrastructure/repositories/` | SQLAlchemy implementations of interfaces |
| Infrastructure | `src/infrastructure/models/` | SQLAlchemy ORM models |
| Presentation | `src/presentation/api/v1/` | FastAPI routers + Pydantic schemas |
| Core | `src/core/` | Security, logging, exceptions, RBAC engine |
| Config | `src/config/settings.py` | Pydantic Settings (env-based) |

---

## How to Add a New Feature (Step-by-Step)

**Example: Adding a `LoanManagement` module**

### Step 1 — Domain Entity
Create `src/domain/entities/loan.py`:
```python
from dataclasses import dataclass
from datetime import datetime
from typing import Optional

@dataclass
class Loan:
    id: int
    application_id: int
    amount: float
    status: str
    created_at: Optional[datetime] = None
```
**Rule:** No imports from FastAPI, SQLAlchemy, or any framework here.

### Step 2 — Repository Interface
Create `src/domain/interfaces/loan_repository.py`:
```python
from abc import ABC, abstractmethod
from typing import Optional, List
from src.domain.entities.loan import Loan

class LoanRepositoryInterface(ABC):
    @abstractmethod
    def get_by_id(self, loan_id: int) -> Optional[Loan]: ...

    @abstractmethod
    def get_by_application(self, application_id: int) -> List[Loan]: ...
```

### Step 3 — SQLAlchemy Model
Create `src/infrastructure/models/loan_model.py`:
```python
from sqlalchemy import Column, Integer, Float, String, DateTime
from src.infrastructure.database.connection import Base

class LoanModel(Base):
    __tablename__ = "loans"  # must match existing Laravel table name
    id = Column(Integer, primary_key=True)
    application_id = Column(Integer)
    amount = Column(Float)
    status = Column(String(50))
```

### Step 4 — Repository Implementation
Create `src/infrastructure/repositories/sql_loan_repository.py`:
```python
from sqlalchemy.orm import Session
from src.domain.entities.loan import Loan
from src.domain.interfaces.loan_repository import LoanRepositoryInterface
from src.infrastructure.models.loan_model import LoanModel

class SqlLoanRepository(LoanRepositoryInterface):
    def __init__(self, db: Session):
        self.db = db

    def get_by_id(self, loan_id: int):
        model = self.db.query(LoanModel).filter(LoanModel.id == loan_id).first()
        if not model:
            return None
        return Loan(id=model.id, application_id=model.application_id,
                    amount=model.amount, status=model.status)
```

### Step 5 — Use Case
Create `src/application/use_cases/get_loan.py`:
```python
from src.domain.interfaces.loan_repository import LoanRepositoryInterface
from src.domain.entities.loan import Loan

class GetLoanUseCase:
    def __init__(self, loan_repo: LoanRepositoryInterface):
        self.loan_repo = loan_repo

    def execute(self, loan_id: int) -> Loan:
        loan = self.loan_repo.get_by_id(loan_id)
        if not loan:
            raise ValueError(f"Loan {loan_id} not found")
        return loan
```

### Step 6 — Pydantic Schema
Create `src/presentation/api/v1/loans/schemas.py`:
```python
from pydantic import BaseModel
from datetime import datetime

class LoanResponse(BaseModel):
    id: int
    application_id: int
    amount: float
    status: str

    class Config:
        from_attributes = True
```

### Step 7 — FastAPI Router
Create `src/presentation/api/v1/loans/routes.py`:
```python
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from src.infrastructure.database.connection import get_db
from src.infrastructure.repositories.sql_loan_repository import SqlLoanRepository
from src.application.use_cases.get_loan import GetLoanUseCase
from src.presentation.api.v1.loans.schemas import LoanResponse

router = APIRouter(prefix="/loans", tags=["Loans"])

@router.get("/{loan_id}", response_model=LoanResponse)
def get_loan(loan_id: int, db: Session = Depends(get_db)):
    repo = SqlLoanRepository(db)
    use_case = GetLoanUseCase(loan_repo=repo)
    return use_case.execute(loan_id)
```

### Step 8 — Register Router in main.py
```python
from src.presentation.api.v1.loans.routes import router as loans_router
app.include_router(loans_router, prefix="/api/v1")
```

---

## SOLID Rules (Non-Negotiable)

| Principle | Rule | Anti-Pattern to Avoid |
|-----------|------|----------------------|
| SRP | One class = one reason to change | Fat controller doing DB + HTTP + business logic |
| OCP | Add new use case; don't modify existing | Editing `authenticate_user.py` to add Google login |
| LSP | Any repo impl is substitutable | Use case importing `SqlUserRepository` directly |
| ISP | Narrow interfaces per bounded context | One giant `RepositoryInterface` for everything |
| DIP | Use cases depend on interfaces, not implementations | `use_case.py` importing from `infrastructure/` |

---

## Coding Conventions

- **Files:** `snake_case.py`
- **Classes:** `PascalCase`
- **Functions/variables:** `snake_case`
- **Type hints:** Required on all function signatures
- **Imports order:** stdlib → third-party → local (`from src.xxx`)
- **Docstrings:** Required on all classes; optional on simple methods
- **Custom exceptions:** Define in `src/core/exceptions.py`, raise from use cases

---

## RBAC Integration

All protected endpoints use the RBAC dependency:
```python
from src.core.rbac.rbac_dependency import require_permission

@router.get("/sensitive-data")
def get_data(user=Depends(require_permission("can_view_loan_details"))):
    ...
```

Permissions are defined in `src/core/rbac/permissions_registry.py`.
Roles are configured in `src/core/rbac/roles_config.yaml`.

---

## Database Notes

- The existing MySQL database from Laravel is **read-only initially**
- Table names match Laravel's snake_case plural convention (e.g., `loan_applications`, `users`)
- **Never rename or alter existing columns** — the Laravel app may still be in use
- When reading Laravel migrations for column names: `/Users/creditfair/Sites/creditfair/database/migrations/`

---

## What Has Been Migrated

- [x] User authentication (email/password + Microsoft OAuth)
- [x] JWT token generation with embedded RBAC roles
- [x] RBAC engine with role definitions (YAML-based)
- [x] Application list + get by ID endpoints
- [x] Application partial update
- [x] Role management endpoints
- [x] Health check
- [x] Centralized logging, security, exceptions

## What Still Needs Migration

Use `/Users/creditfair/Sites/creditfair/routes/api.php` as the source of truth for remaining endpoints.

- [ ] Application creation / submission flow
- [ ] Document upload (AWS S3 integration)
- [ ] OTP verification (Exotel)
- [ ] Payment webhooks (Razorpay)
- [ ] Loan hypothecation flows (ABFL, Avanse)
- [ ] Excel export (Maatwebsite → openpyxl/xlsxwriter)
- [ ] Email notifications (Laravel Mailable → SMTP/SendGrid)
- [ ] Background jobs (Laravel Queue → Celery or FastAPI BackgroundTasks)
- [ ] Advanced filtering/search on applications

---

## Key Reference Files in Original Laravel Repo

When migrating a feature, always read the original code first:

| Feature | Laravel File |
|---------|-------------|
| API routes | `/Users/creditfair/Sites/creditfair/routes/api.php` |
| Main API controller | `/Users/creditfair/Sites/creditfair/app/Http/Controllers/ApiController.php` |
| Models | `/Users/creditfair/Sites/creditfair/app/Model/` |
| Jobs (background) | `/Users/creditfair/Sites/creditfair/app/Jobs/` |
| Mail | `/Users/creditfair/Sites/creditfair/app/Mail/` |
| External integrations | `/Users/creditfair/Sites/creditfair/app/ABFL/`, `app/Avanse/`, `app/Exotel/` |
| DB migrations | `/Users/creditfair/Sites/creditfair/database/migrations/` |

---

## Running the Backend

```bash
cd /Users/creditfair/Sites/creditfair_python_backend
source venv/bin/activate
uvicorn src.main:app --reload --port 8000
```

API docs: http://localhost:8000/docs

---

## Environment Variables (.env)

Key vars needed:
```
DATABASE_URL=mysql+pymysql://user:pass@localhost/creditfair
SECRET_KEY=your-jwt-secret
MICROSOFT_CLIENT_ID=...
MICROSOFT_CLIENT_SECRET=...
FRONTEND_URL=http://localhost:3000
ENVIRONMENT=development
```
