Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
data.sqlite3
.idea/
78 changes: 78 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Architecture Overview

## System Structure
- `app/main.py`: FastAPI app bootstrap, startup init, and static file mounting.
- `app/db.py`: SQLite connection management and ranking operations (insert, move, delete).
- `app/schemas.py`: Pydantic request/response models.
- `app/models.py`: Lightweight domain model for applications.
- `app/routers/applications.py`: API endpoints for queue operations.
- `static/`: Frontend UI (HTML/CSS/JS) served by FastAPI.
- `tests/`: Pytest coverage for ranking workflows.

## Data Model
Single SQLite table `applications`:
- `id` (auto-increment)
- `name`
- `summary`
- `position` (1-based rank)

`position` is treated as the canonical ranking, with reindexing on insert/move/delete.

## Key Design Decisions
- Integer ranking with reindexing:
- Keeps ordering deterministic and easy to reason about.
- Avoids fractional rank drift and simplifies constraints.
- SQLite for persistence:
- Zero-config local storage fits assessment constraints.
- Simple to reset or inspect via a single file.
- Frontend as static assets served by FastAPI:
- Minimal setup and no build step.
- Keeps deployment and local run straightforward.
- Backend validates bounds, frontend clamps:
- UI clamps move requests to `[1, max]` for convenience.
- Backend still enforces range to avoid invalid data.

## Sample Commands

### List Applications
```powershell
curl http://127.0.0.1:8000/applications
```

### Insert at End
```powershell
curl -X POST http://127.0.0.1:8000/applications/insert ^
-H "Content-Type: application/json" ^
-d "{\"name\":\"Jane Doe\",\"summary\":\"Small business line of credit\",\"placement\":\"end\"}"
```

### Insert at Start
```powershell
curl -X POST http://127.0.0.1:8000/applications/insert ^
-H "Content-Type: application/json" ^
-d "{\"name\":\"Urgent Review\",\"summary\":\"Escalated request\",\"placement\":\"start\"}"
```

### Insert Between Two Items
```powershell
curl -X POST http://127.0.0.1:8000/applications/insert ^
-H "Content-Type: application/json" ^
-d "{\"name\":\"Priority Insert\",\"summary\":\"Manual triage\",\"placement\":\"between\",\"before_id\":1,\"after_id\":2}"
```

### Move (Change Rank)
```powershell
curl -X POST http://127.0.0.1:8000/applications/3/move ^
-H "Content-Type: application/json" ^
-d "{\"new_position\":1}"
```

### Delete
```powershell
curl -X DELETE http://127.0.0.1:8000/applications/3
```

## Testing Focus
- Ordering after inserts (start/end/between).
- Reindexing after moves and deletes.
- Error handling for invalid moves and unknown IDs is enforced by the API.
26 changes: 26 additions & 0 deletions RUNNING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Run Guide

## Prerequisites
- Python 3.11+ (recommended)
- pip

## Install Dependencies
```powershell
python -m pip install -r requirements.txt
```

## Run the Backend API
```powershell
python -m uvicorn app.main:app --reload
```

The API will be available at `http://127.0.0.1:8000/` and the frontend will load from the same address.

## Run Tests
```powershell
python -m pytest
```

## Notes
- SQLite data is stored in `data.sqlite3` at the repo root.
- Tests use a temporary SQLite file via the `APP_DB_PATH` environment variable.
35 changes: 35 additions & 0 deletions SPECS/credit-risk-review-queue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Feature Spec: Credit Risk Review Queue Ranking

Fulfilled via OpenAI Codex

## Goal
- Provide a full-stack app to manage and reprioritize credit application reviews by ranked order.

## Scope
- In:
- Full ranking view of all applications with rank, applicant name, and summary.
- Insert a new application at the start, end, or between two applications.
- Remove an application from the queue.
- Change an application's rank.
- Persist data in SQLite.
- Out:
- Authentication, authorization, or multi-user roles.
- External credit bureau integrations.
- Complex scoring models beyond manual ranking.

## Requirements
- Backend built with Python + FastAPI.
- Database using SQLite.
- REST endpoints to list, insert (start/end/between), move, and delete applications.
- Frontend that loads the full ranking and supports the above actions.
- Tests that cover key workflows and edge cases for ranking changes.

## Acceptance Criteria
- [ ] Viewing the main page shows the full ranking with each item's rank, applicant name, and summary.
- [ ] A user can insert a new application at the start of the list.
- [ ] A user can insert a new application at the end of the list.
- [ ] A user can insert a new application between two existing applications.
- [ ] A user can remove an application from the queue.
- [ ] A user can change the rank of an application and the list reflects the new order.
- [ ] Data persists in SQLite across server restarts.
- [ ] Tests validate list ordering, insertions, deletions, and rank changes.
Empty file added app/__init__.py
Empty file.
190 changes: 190 additions & 0 deletions app/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import os
import sqlite3
from pathlib import Path

DEFAULT_DB_PATH = Path(__file__).resolve().parent.parent / "data.sqlite3"
DB_PATH = Path(os.environ.get("APP_DB_PATH", DEFAULT_DB_PATH))


def get_connection() -> sqlite3.Connection:
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
conn.row_factory = sqlite3.Row
return conn


def init_db() -> None:
conn = get_connection()
try:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS applications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
summary TEXT NOT NULL,
position INTEGER NOT NULL
)
"""
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_applications_position ON applications(position)"
)
conn.commit()
finally:
conn.close()


def fetch_all_applications() -> list[sqlite3.Row]:
conn = get_connection()
try:
rows = conn.execute(
"SELECT id, name, summary, position FROM applications ORDER BY position ASC"
).fetchall()
return rows
finally:
conn.close()


def get_application_by_id(application_id: int) -> sqlite3.Row | None:
conn = get_connection()
try:
row = conn.execute(
"SELECT id, name, summary, position FROM applications WHERE id = ?",
(application_id,),
).fetchone()
return row
finally:
conn.close()


def get_max_position(conn: sqlite3.Connection) -> int:
row = conn.execute("SELECT MAX(position) AS max_pos FROM applications").fetchone()
if row is None or row["max_pos"] is None:
return 0
return int(row["max_pos"])


def insert_application_at_position(name: str, summary: str, position: int) -> int:
conn = get_connection()
try:
conn.execute(
"UPDATE applications SET position = position + 1 WHERE position >= ?",
(position,),
)
cursor = conn.execute(
"INSERT INTO applications (name, summary, position) VALUES (?, ?, ?)",
(name, summary, position),
)
conn.commit()
return int(cursor.lastrowid)
finally:
conn.close()


def insert_application_start(name: str, summary: str) -> int:
return insert_application_at_position(name, summary, 1)


def insert_application_end(name: str, summary: str) -> int:
conn = get_connection()
try:
position = get_max_position(conn) + 1
cursor = conn.execute(
"INSERT INTO applications (name, summary, position) VALUES (?, ?, ?)",
(name, summary, position),
)
conn.commit()
return int(cursor.lastrowid)
finally:
conn.close()


def insert_application_between(
name: str, summary: str, before_id: int, after_id: int
) -> int:
conn = get_connection()
try:
before_row = conn.execute(
"SELECT position FROM applications WHERE id = ?", (before_id,)
).fetchone()
after_row = conn.execute(
"SELECT position FROM applications WHERE id = ?", (after_id,)
).fetchone()
if before_row is None or after_row is None:
raise ValueError("before_id or after_id not found")

before_pos = int(before_row["position"])
after_pos = int(after_row["position"])
if before_pos >= after_pos:
raise ValueError("before_id must be ranked before after_id")

insert_pos = before_pos + 1
conn.execute(
"UPDATE applications SET position = position + 1 WHERE position >= ?",
(insert_pos,),
)
cursor = conn.execute(
"INSERT INTO applications (name, summary, position) VALUES (?, ?, ?)",
(name, summary, insert_pos),
)
conn.commit()
return int(cursor.lastrowid)
finally:
conn.close()


def delete_application(application_id: int) -> None:
conn = get_connection()
try:
row = conn.execute(
"SELECT position FROM applications WHERE id = ?", (application_id,)
).fetchone()
if row is None:
raise ValueError("application not found")
position = int(row["position"])
conn.execute("DELETE FROM applications WHERE id = ?", (application_id,))
conn.execute(
"UPDATE applications SET position = position - 1 WHERE position > ?",
(position,),
)
conn.commit()
finally:
conn.close()


def move_application(application_id: int, new_position: int) -> None:
conn = get_connection()
try:
row = conn.execute(
"SELECT position FROM applications WHERE id = ?", (application_id,)
).fetchone()
if row is None:
raise ValueError("application not found")

current_position = int(row["position"])
max_position = get_max_position(conn)
if new_position < 1 or new_position > max_position:
raise ValueError("new_position out of range")

if new_position == current_position:
return

if new_position < current_position:
conn.execute(
"UPDATE applications SET position = position + 1 "
"WHERE position >= ? AND position < ?",
(new_position, current_position),
)
else:
conn.execute(
"UPDATE applications SET position = position - 1 "
"WHERE position > ? AND position <= ?",
(current_position, new_position),
)

conn.execute(
"UPDATE applications SET position = ? WHERE id = ?",
(new_position, application_id),
)
conn.commit()
finally:
conn.close()
29 changes: 29 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from fastapi import FastAPI
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles

from app.db import init_db
from app.routers.applications import router as applications_router

app = FastAPI(title="Credit Risk Review Queue")


@app.on_event("startup")
def on_startup() -> None:
init_db()


app.mount("/static", StaticFiles(directory="static"), name="static")


@app.get("/health")
def health() -> dict:
return {"status": "ok"}


@app.get("/", include_in_schema=False)
def index() -> FileResponse:
return FileResponse("static/index.html")


app.include_router(applications_router)
19 changes: 19 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from dataclasses import dataclass
import sqlite3


@dataclass(frozen=True)
class Application:
id: int
name: str
summary: str
position: int

@classmethod
def from_row(cls, row: sqlite3.Row) -> "Application":
return cls(
id=int(row["id"]),
name=str(row["name"]),
summary=str(row["summary"]),
position=int(row["position"]),
)
3 changes: 3 additions & 0 deletions app/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from app.routers.applications import router as applications_router

__all__ = ["applications_router"]
Loading