# HR-ATS Backend LLM Context Prompt Copy everything below the line into any LLM session before asking it to write or edit backend code. --- You are coding inside **HR-ATS-Portal** (`backend/`). You must follow this house style **exactly**. Mirror neighboring files. Do not invent alternate patterns, layers, or response shapes. Prefer matching existing code over “cleaner” industry defaults. ## Goal Every change must look like it was written by the same author as `backend/users/` and `backend/inbox/`. ## Package layout (every domain) ``` backend// app.py # routes only — HTTP in/out views.py # service class — business logic models.py # SQLModel table + classmethod DB accessors serializers.py # hand-rolled dict builders (no Pydantic response models) plugins.py # pure helpers (hash, JWT, clean payload) — NO FastAPI imports permissions.py # OAuth2 scheme + Depends aliases (auth domains only) ``` - Bare `router = APIRouter()`; mount in `main.py` with `app.include_router(...)`. - No package `__init__.py`. Run from `backend/` so imports are top-level (`users.app`, `db_setup`). - Non-DB config: module-level `load_dotenv()` + `os.getenv(...)`. Do **not** extend `db_setup.Settings` for app secrets. ## Layer duties (non-negotiable) | Layer | Owns | Must NOT do | |---|---|---| | `app.py` | Routes, inline request Pydantic models, `JSONResponse`, HTTP token envelope via serializers, inject `session` / `CurrentUser` | Business rules, SQL, JWT crypto beyond calling plugin functions | | `views.py` | Business checks, call models, raise `HTTPException`, return ORM user (auth) or serialized dict (CRUD) | Call `serialize_token` or build login HTTP payloads | | `models.py` | Fields, queries, inserts/updates/soft-delete, `selectinload` when needed | HTTPException, FastAPI, serializers | | `serializers.py` | `serialize_*` → plain `dict` | DB, Depends | | `plugins.py` | Pure helpers; raise library errors (`jwt.*`) | Import FastAPI / raise HTTPException | | `permissions.py` | `OAuth2PasswordBearer`, `get_current_user`, `CurrentUser` alias, `require_permission` | Route handlers | ## Exact route pattern (`app.py`) - Paths are verb-in-path: `/users/create`, `/users/fetch`, `/users/login` — **not** `/auth/token`, not REST-resource-only. - Standard wrapper on every handler: ```python try: service=User(session=session) data=await service.some_method(...) return JSONResponse(content={"data":data,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) ``` - List: `{"data":items,"total":total,"status_code":200}`. By id: include `"total":1`. - Login / refresh — **service returns ORM user**; route mints tokens and serializes: ```python user=await service.authenticate_user(form_data.username,form_data.password) tokens=serialize_token(create_access_token(user),create_refresh_token(user),user) return JSONResponse(content={**tokens,"status_code":200}) ``` - Request body models stay **inline in `app.py`** (`UserCreate`, `UserUpdate`, `TokenRefresh`). Never move them into `serializers.py`. - Use `session: AsyncSession = Depends(get_session)` by default. Use `Annotated` only when required (`OAuth2PasswordRequestForm`, `CurrentUser` before other defaulted params). - Preserve tight local spacing: `service=User(session=session)`, `detail=str(e)`. Do not pretty-reformat unrelated code. ## Exact service pattern (`views.py`) ```python class User: def __init__(self,session:AsyncSession): self.session=session async def create_user(self,payload): ... return serialize_user(user) ``` - Leave service method parameters untyped (match existing). - Raise `HTTPException(status_code=...,detail="...")` for domain errors. - `authenticate_user` / `refresh_access_token` return the **Users ORM instance only**. - If you loaded via an accessor that does not `selectinload(role)`, re-fetch with `get_user_by_id` before serialization that touches `user.role`. (`get_user_by_email` and `get_user_by_id` both eager-load `role` today.) ## Exact model pattern (`models.py`) - SQLModel `table=True`; accessors as `@classmethod async def`. - Soft delete sets `is_deleted=True` and `is_active=False`. - `selectinload` relations that serializers read. - Commits happen inside write accessors (existing convention). ## Serializers - Hand-built dicts only. `str(uuid)`, `.isoformat()` for datetimes. Never include `password`. - Token response: OAuth2 fields at **root** (`access_token`, `refresh_token`, `token_type`, `expires_in`); user record under `data`. ## Auth (when touching users auth) - PyJWT access + refresh with a `type` claim; `decode_token(..., expected_type=...)` rejects mismatches. - Protect `/users/*` with `current_user: CurrentUser` except `/users/login` and `/users/refresh`. Prefer `Depends(require_permission(...))` on mutating/list routes that need a specific tag; keep `/users/me` on plain `CurrentUser` so users can discover a missing-role state. - `get_current_user`: decode access → DB by `sub` → reject missing/deleted/inactive → return `serialize_user(user, with_permissions=True)`. - Login: `OAuth2PasswordRequestForm` (username = email). `tokenUrl="users/login"` (no leading slash). - JWT `iat`/`exp` use `datetime.now(timezone.utc)` only — never naive `datetime.now()`. ## Dependencies / env - Add pins to `backend/requirements.txt` under banner comments with a trailing `# why` comment. - Put secrets in `backend/.env`; keep key names in `backend/.env.example`. ## Hard bans 1. No repository / use-case / DTO layers beyond inline request models. 2. No Pydantic response models; no alternate envelopes; no `/api/v1` prefix. 3. No `serialize_token` inside `views.py`. 4. No FastAPI imports in `plugins.py`. 5. No drive-by refactors, renames, or whole-file reformats. 6. RBAC exists in `users/permissions.py`; do not invent a second scheme. 7. Do not edit unrelated domains (`inbox/` vs `users/`) unless asked. 8. Do not add `__init__.py` to make packages. ## Workflow when adding an endpoint 1. Model accessor (if DB). 2. Service method in `views.py`. 3. `serialize_*` if new shape. 4. Route in `app.py` with the standard try/except + `JSONResponse`. 5. Add `current_user: CurrentUser` or `Depends(require_permission(...))` if the route is protected. Before finishing, re-read the touched files and confirm they still match a sibling file’s structure, naming, spacing, and response shape.