diff --git a/README.md b/README.md index 494f1c75..a0c5ad7f 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,62 @@ -# Candidate Assessment: Spec-Driven Development With Codegen Tools - -This assessment evaluates how you use modern code generation tools (for example `5.2-Codex`, `Claude`, `Copilot`, and similar) to design, build, and test a software application using a spec-driven development pattern. You may build a frontend, a backend, or both. - -## Goals -- Build a working application with at least one meaningful feature. -- Create a testing framework to validate the application. -- Demonstrate effective use of code generation tools to accelerate delivery. -- Show clear, maintainable engineering practices. - -## Deliverables -- Application source code in this repository. -- A test suite and test harness that can be run locally. -- Documentation that explains how to run the app and the tests. - -## Scope Options -Pick one: -- Frontend-only application. -- Backend-only application. -- Full-stack application. - -Your solution should include at least one real workflow, for example: -- Create and view a resource. -- Search or filter data. -- Persist data in memory or storage. - -## Rules -- You must use a code generation tool (for example `5.2-Codex`, `Claude`, or similar). You can use multiple tools. -- You must build the application and a testing framework for it. -- The application and tests must run locally. -- Do not include secrets or credentials in this repository. - -## Evaluation Criteria -- Working product: Does the app do what it claims? -- Test coverage: Do tests cover key workflows and edge cases? -- Engineering quality: Clarity, structure, and maintainability. -- Use of codegen: How effectively you used tools to accelerate work. -- Documentation: Clear setup and run instructions. - -## What to Submit -- When you are complete, put up a Pull Request against this repository with your changes. -- A short summary of your approach and tools used in your PR submission -- Any additional information or approach that helped you. +# Banking Transaction System + +A full-stack banking application built for the spec-driven-development assessment. + +## Features +- Create bank accounts with initial balance +- Deposit and withdraw funds +- Transfer between accounts +- View transaction history +- Input validation and error handling + +## Tech Stack +- **Backend:** FastAPI (Python) +- **Database:** SQLite +- **Frontend:** HTML/CSS/JavaScript +- **Testing:** pytest, Playwright + +## Setup + +```bash +# Clone and navigate +git clone https://github.com/Farhod75/spec-driven-development.git +cd spec-driven-development + +# Create virtual environment +python -m venv venv +.\venv\Scripts\activate # Windows +# source venv/bin/activate # macOS/Linux + +# Install dependencies +pip install -r requirements.txt +playwright install chromium + +# Run the Application +uvicorn app.main:app --reload --port 8000 +Open http://localhost:8000 + +# Run Tests +# API tests +pytest tests/test_api.py -v + +# UI tests (server must be running) +pytest tests/test_ui.py -v + +# All tests +pytest -v + +API Endpoints +Method Endpoint Description +POST /accounts Create account +GET /accounts/{id} Get account details +POST /accounts/{id}/deposit Deposit funds +POST /accounts/{id}/withdraw Withdraw funds +POST /transfers Transfer between accounts +GET /accounts/{id}/transactions Get transaction history + +# AI Tools Used + +Claude (Anthropic) - Code generation and debugging + +# Author +Farhod Elbekov \ No newline at end of file diff --git a/SPECS/banking-transactions.md b/SPECS/banking-transactions.md new file mode 100644 index 00000000..d9316675 --- /dev/null +++ b/SPECS/banking-transactions.md @@ -0,0 +1,27 @@ +# Feature Spec: Banking Transactions + +## Goal +Build a simple banking system that allows users to create accounts, manage balances, and perform transactions. + +## Scope +- In: Account creation, deposits, withdrawals, transfers, transaction history, basic UI +- Out: Authentication, multi-currency, external integrations + +## Requirements +- Users can create bank accounts with initial balance +- Users can deposit money into accounts +- Users can withdraw money (with insufficient funds validation) +- Users can transfer between accounts +- Users can view transaction history +- API returns appropriate status codes and error messages + +## Acceptance Criteria +- [ ] POST /accounts creates a new account and returns account details +- [ ] GET /accounts/{id} returns account with current balance +- [ ] POST /accounts/{id}/deposit increases balance +- [ ] POST /accounts/{id}/withdraw decreases balance (fails if insufficient funds) +- [ ] POST /transfers moves money between accounts +- [ ] GET /accounts/{id}/transactions returns transaction history +- [ ] UI allows all operations above +- [ ] All API endpoints have passing tests +- [ ] UI has Playwright E2E tests \ No newline at end of file diff --git a/app/__pycache__/database.cpython-313.pyc b/app/__pycache__/database.cpython-313.pyc new file mode 100644 index 00000000..6c0e99d8 Binary files /dev/null and b/app/__pycache__/database.cpython-313.pyc differ diff --git a/app/__pycache__/main.cpython-313.pyc b/app/__pycache__/main.cpython-313.pyc new file mode 100644 index 00000000..34fb0944 Binary files /dev/null and b/app/__pycache__/main.cpython-313.pyc differ diff --git a/app/__pycache__/models.cpython-313.pyc b/app/__pycache__/models.cpython-313.pyc new file mode 100644 index 00000000..97ae2464 Binary files /dev/null and b/app/__pycache__/models.cpython-313.pyc differ diff --git a/app/database.py b/app/database.py new file mode 100644 index 00000000..a83bcdd7 --- /dev/null +++ b/app/database.py @@ -0,0 +1,32 @@ +import sqlite3 +from contextlib import contextmanager + +DATABASE = "banking.db" + +def init_db(): + with get_db() as conn: + conn.executescript(""" + CREATE TABLE IF NOT EXISTS accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + balance REAL DEFAULT 0.0 + ); + CREATE TABLE IF NOT EXISTS transactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL, + type TEXT NOT NULL, + amount REAL NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (account_id) REFERENCES accounts(id) + ); + """) + +@contextmanager +def get_db(): + conn = sqlite3.connect(DATABASE) + conn.row_factory = sqlite3.Row + try: + yield conn + conn.commit() + finally: + conn.close() \ No newline at end of file diff --git a/app/main.py b/app/main.py new file mode 100644 index 00000000..f5daba73 --- /dev/null +++ b/app/main.py @@ -0,0 +1,108 @@ +from fastapi import FastAPI, HTTPException +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse +from app.models import AccountCreate, Account, DepositWithdraw, Transfer, Transaction +from app.database import init_db, get_db + +app = FastAPI(title="Banking API") + +@app.on_event("startup") +def startup(): + init_db() + +@app.post("/accounts", response_model=Account, status_code=201) +def create_account(data: AccountCreate): + with get_db() as conn: + cursor = conn.execute( + "INSERT INTO accounts (name, balance) VALUES (?, ?)", + (data.name, data.initial_balance) + ) + account_id = cursor.lastrowid + return {"id": account_id, "name": data.name, "balance": data.initial_balance} + +@app.get("/accounts/{account_id}", response_model=Account) +def get_account(account_id: int): + with get_db() as conn: + row = conn.execute("SELECT * FROM accounts WHERE id = ?", (account_id,)).fetchone() + if not row: + raise HTTPException(status_code=404, detail="Account not found") + return dict(row) + +@app.post("/accounts/{account_id}/deposit", response_model=Account) +def deposit(account_id: int, data: DepositWithdraw): + if data.amount <= 0: + raise HTTPException(status_code=400, detail="Amount must be positive") + with get_db() as conn: + row = conn.execute("SELECT * FROM accounts WHERE id = ?", (account_id,)).fetchone() + if not row: + raise HTTPException(status_code=404, detail="Account not found") + new_balance = row["balance"] + data.amount + conn.execute("UPDATE accounts SET balance = ? WHERE id = ?", (new_balance, account_id)) + conn.execute( + "INSERT INTO transactions (account_id, type, amount) VALUES (?, ?, ?)", + (account_id, "deposit", data.amount) + ) + return {"id": account_id, "name": row["name"], "balance": new_balance} + +@app.post("/accounts/{account_id}/withdraw", response_model=Account) +def withdraw(account_id: int, data: DepositWithdraw): + if data.amount <= 0: + raise HTTPException(status_code=400, detail="Amount must be positive") + with get_db() as conn: + row = conn.execute("SELECT * FROM accounts WHERE id = ?", (account_id,)).fetchone() + if not row: + raise HTTPException(status_code=404, detail="Account not found") + if row["balance"] < data.amount: + raise HTTPException(status_code=400, detail="Insufficient funds") + new_balance = row["balance"] - data.amount + conn.execute("UPDATE accounts SET balance = ? WHERE id = ?", (new_balance, account_id)) + conn.execute( + "INSERT INTO transactions (account_id, type, amount) VALUES (?, ?, ?)", + (account_id, "withdraw", data.amount) + ) + return {"id": account_id, "name": row["name"], "balance": new_balance} + +@app.post("/transfers", response_model=dict) +def transfer(data: Transfer): + if data.amount <= 0: + raise HTTPException(status_code=400, detail="Amount must be positive") + with get_db() as conn: + from_acc = conn.execute("SELECT * FROM accounts WHERE id = ?", (data.from_account_id,)).fetchone() + to_acc = conn.execute("SELECT * FROM accounts WHERE id = ?", (data.to_account_id,)).fetchone() + if not from_acc: + raise HTTPException(status_code=404, detail="Source account not found") + if not to_acc: + raise HTTPException(status_code=404, detail="Destination account not found") + if from_acc["balance"] < data.amount: + raise HTTPException(status_code=400, detail="Insufficient funds") + + conn.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", (data.amount, data.from_account_id)) + conn.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", (data.amount, data.to_account_id)) + conn.execute( + "INSERT INTO transactions (account_id, type, amount) VALUES (?, ?, ?)", + (data.from_account_id, "transfer_out", data.amount) + ) + conn.execute( + "INSERT INTO transactions (account_id, type, amount) VALUES (?, ?, ?)", + (data.to_account_id, "transfer_in", data.amount) + ) + return {"message": "Transfer successful"} + +@app.get("/accounts/{account_id}/transactions", response_model=list[Transaction]) +def get_transactions(account_id: int): + with get_db() as conn: + row = conn.execute("SELECT * FROM accounts WHERE id = ?", (account_id,)).fetchone() + if not row: + raise HTTPException(status_code=404, detail="Account not found") + rows = conn.execute( + "SELECT * FROM transactions WHERE account_id = ? ORDER BY timestamp DESC", + (account_id,) + ).fetchall() + return [dict(r) for r in rows] + +# Serve frontend +app.mount("/static", StaticFiles(directory="frontend"), name="static") + +@app.get("/") +def serve_frontend(): + return FileResponse("frontend/index.html") \ No newline at end of file diff --git a/app/models.py b/app/models.py new file mode 100644 index 00000000..b83bef58 --- /dev/null +++ b/app/models.py @@ -0,0 +1,27 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + +class AccountCreate(BaseModel): + name: str + initial_balance: float = 0.0 + +class Account(BaseModel): + id: int + name: str + balance: float + +class DepositWithdraw(BaseModel): + amount: float + +class Transfer(BaseModel): + from_account_id: int + to_account_id: int + amount: float + +class Transaction(BaseModel): + id: int + account_id: int + type: str + amount: float + timestamp: str \ No newline at end of file diff --git a/banking.db b/banking.db new file mode 100644 index 00000000..565f933a Binary files /dev/null and b/banking.db differ diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 00000000..70f166e8 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,165 @@ + + +
+ + +