# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview REmodel is a Python calculation engine + FastAPI backend + Next.js frontend for Indian renewable energy (Solar + Wind + BESS) project finance modeling. It computes optimal flat tariff and full 25-year project financials for hybrid RTC RE projects. ## Prerequisites - Python ≥ 3.12, Poetry ≥ 2.0 - Node.js ≥ 20, pnpm ≥ 10 - Docker (for Redis) ## Common Commands ```bash # Full stack make setup # Install all deps (Poetry + pnpm) make dev # Start Redis, API, Arq worker, web dev server make test # Run pytest + jest make lint # ruff + mypy + tsc + eslint make clean # Remove build artefacts # Single test (Python) cd packages/engine && poetry run pytest tests/unit/test_xxx.py::test_name -v # Individual packages cd packages/engine && poetry run mypy src/ cd packages/api && poetry run uvicorn remodel_api.main:app --reload --port 8000 cd packages/web && pnpm dev ``` ## Architecture ``` packages/web (Next.js App Router) │ REST + SSE packages/api (FastAPI + Arq + SQLite) │ Python import packages/engine (Pydantic + NumPy + SciPy) ``` ## Key Context - **Domain**: Indian RE bidding — PPA tariff bids, SECI/DISCOM auctions, 15-20% target equity IRR, D:E ≤ 75:25, DSCR ≥ 1.20 - **Hybrid RTC**: Solar + Wind + BESS (battery firming), 57.64% RTC CUF commitment, DSM penalties - **Tax**: Section 115BAA → 22% + cess = 25.17% - **Currency**: INR Crore (Cr) = 10 million, Lakh = 100 thousand ## Working Agreements - Always read PROJECT.md and active SPRINT_XX.md at session start - One task per commit, format: `[S2-T03] Implement IDC fixed-point solver` - Excel parity is sacred — debug diffs, don't bump tolerances - Type strict (mypy strict), no Any except at JSON boundaries - Pydantic for all I/O, no raw dicts crossing module boundaries - No magic numbers — defaults in catalog/defaults.py - Comments explain WHY, not WHAT ## Engine Structure Key modules (read in this order for domain understanding): 1. **schemas/** — Pydantic models (single source of truth) 2. **solver/** — Three nested iterations: tariff (brentq) → debt sizing → IDC 3. **generation/** — Solar, wind, BESS simulation 4. **dispatch/** — Hybrid RTC scheduling, MCP settlement 5. **commercial/** — PPA revenue, DSM, charges, losses 6. **capex/** — CostItem catalog + IDC calculation 7. **financial/** — P&L, cash flow, balance sheet 8. **debt/** — Sizing, sculpting, schedule, DSCR compliance 9. **irr/** — Equity/project IRR metrics ## API Structure - **routers/** — REST endpoints (scenarios, sensitivities, templates) - **workers/** — Arq async tasks (run via Redis queue) - **db/** — SQLAlchemy models + migrations - **main.py** — FastAPI app factory ## Deployment (Dokploy) ### Docker Configuration **API Dockerfile** (`packages/api/Dockerfile`): ```dockerfile FROM python:3.12-slim WORKDIR /app RUN pip install poetry COPY packages /app/packages WORKDIR /app/packages/api RUN poetry install --no-interaction ENV VENV_PATH=/root/.cache/pypoetry/virtualenvs/remodel-api-cufy8KWC-py3.12/bin ENV PATH=$VENV_PATH:$PATH ENV PYTHONPATH=/app/packages/engine/src:/app/packages/api/src WORKDIR /app/packages/api EXPOSE 8000 CMD ["uvicorn", "remodel_api.main:app", "--host", "0.0.0.0", "--port", "8000"] ``` **Web Dockerfile** (`packages/web/Dockerfile`): ```dockerfile FROM node:22-alpine AS builder WORKDIR /app RUN corepack enable && corepack prepare pnpm@latest --activate COPY package.json pnpm-lock.yaml ./ COPY . . RUN test -f .env.local || echo "# placeholder" > .env.local RUN pnpm install --frozen-lockfile --ignore-scripts RUN pnpm build FROM node:22-alpine WORKDIR /app RUN corepack enable && corepack prepare pnpm@latest --activate COPY --from=builder /app/.next ./.next COPY --from=builder /app/public ./public COPY --from=builder /app/package.json . COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/.env.local ./.env.local ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 EXPOSE 3000 CMD ["/app/node_modules/.bin/next", "start"] ``` ### docker-compose.yml (Dokploy) ```yaml services: redis: image: redis:7-alpine restart: unless-stopped volumes: - redis_data:/data networks: - internal api: build: context: . dockerfile: packages/api/Dockerfile restart: unless-stopped environment: - DATABASE_URL=sqlite+aiosqlite:///./remodel.db - REDIS_URL=redis://redis:6379 depends_on: - redis labels: - "traefik.enable=true" - "traefik.docker.network=web" - "traefik.http.routers.api.rule=Host(`model.manohargupta.com`) && PathPrefix(`/api`)" - "traefik.http.routers.api.entrypoints=websecure" - "traefik.http.routers.api.tls.certresolver=letsencrypt" - "traefik.http.services.api.loadbalancer.server.port=8000" networks: - internal - web worker: build: context: . dockerfile: packages/api/Dockerfile command: python -m arq remodel_api.workers.main.WorkerSettings restart: unless-stopped environment: - DATABASE_URL=sqlite+aiosqlite:///./remodel.db - REDIS_URL=redis://redis:6379 depends_on: - redis networks: - internal web: build: context: ./packages/web dockerfile: Dockerfile restart: unless-stopped environment: - NEXT_PUBLIC_API_URL=https://model.manohargupta.com/api labels: - "traefik.enable=true" - "traefik.docker.network=web" - "traefik.http.routers.web.rule=Host(`model.manohargupta.com`)" - "traefik.http.routers.web.entrypoints=websecure" - "traefik.http.routers.web.tls.certresolver=letsencrypt" - "traefik.http.services.web.loadbalancer.server.port=3000" networks: - web networks: internal: internal: true web: name: dokploy-network external: true volumes: redis_data: ``` ### Environment Variables | Variable | Description | Required | |----------|-------------|----------| | `DATABASE_URL` | SQLite with async driver | `sqlite+aiosqlite:///./remodel.db` | | `REDIS_URL` | Redis connection string | `redis://redis:6379` | | `NEXT_PUBLIC_API_URL` | Public API URL for web | `https://model.manohargupta.com/api` | ### CORS Configuration In `packages/api/src/remodel_api/main.py`: ```python from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:3000", "https://model.manohargupta.com"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) ``` ### Config Settings In `packages/api/src/remodel_api/config.py`: ```python class Settings(BaseSettings): model_config = SettingsConfigDict(env_prefix="", extra="ignore") database_url: str = "sqlite+aiosqlite:///./remodel.db" redis_url: str = "redis://localhost:6379" ``` **Important**: No prefix for env vars - they must match docker-compose exactly. ### Network Requirements - `dokploy-network` must exist on Dokploy server (external network) - Traefik uses `websecure` endpoint (HTTPS with Let's Encrypt TLS) ## Deployment Issues & Fixes | Issue | Cause | Fix | |-------|-------|-----| | Poetry pyproject.toml not found | COPY syntax wrong in Dockerfile | Copy entire `packages/` directory | | `--no-venv-seeding` flag error | Old Poetry version | Remove the flag | | Missing `.env.local` | Not in git | Create placeholder in builder stage | | `uvicorn` not in PATH | Poetry venv not in PATH | Set `ENV PATH=$VENV_PATH:$PATH` | | `next` not found | pnpm stores bins differently | Use absolute path `/app/node_modules/.bin/next` | | 504 Gateway Timeout | Redis/DB not accessible | Check network, restart containers | | Worker can't connect to Redis | Wrong env prefix | Use `REDIS_URL` not `REMODEL_REDIS_URL` | | SQLite async error | Wrong driver | Use `sqlite+aiosqlite://` not `sqlite://` | | CORS blocked | Origin mismatch | Add production domain to allow_origins |