7.9 KiB
7.9 KiB
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
# 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):
- schemas/ — Pydantic models (single source of truth)
- solver/ — Three nested iterations: tariff (brentq) → debt sizing → IDC
- generation/ — Solar, wind, BESS simulation
- dispatch/ — Hybrid RTC scheduling, MCP settlement
- commercial/ — PPA revenue, DSM, charges, losses
- capex/ — CostItem catalog + IDC calculation
- financial/ — P&L, cash flow, balance sheet
- debt/ — Sizing, sculpting, schedule, DSCR compliance
- 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):
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):
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)
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:
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:
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-networkmust exist on Dokploy server (external network)- Traefik uses
websecureendpoint (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 |