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
106 changes: 104 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,104 @@
# p4-programmers
snuc hackathon
# Fintech Decision Engine

A full-stack, AI-powered financial decision engine designed to help businesses manage cash flow, prioritize obligations, and simulate financial scenarios smartly.

This monorepo contains both the **FastAPI Backend** and the **Next.js Frontend**.

---

## Features

- **Smart Data Ingestion**: Upload bank CSVs or receipt images — OCR extracts and auto-categorizes everything.
- **Live Financial Dashboard**: See your real-time balance, runway days, and month-by-month cash flow chart.
- **Decision Engine**: AI scores every obligation by urgency, penalty, and relationship to determine who to pay first.
- **What-If Simulator**: Safely simulate payment delays without changing any real data. See the impact instantly.
- **Email Generator**: Auto-draft professional payment-delay emails in formal or friendly tones.
- **Reports & Insights**: Filter transactions by date, category, and type to understand spending patterns.

---

## Tech Stack

**Frontend:**
- [Next.js](https://nextjs.org/) (App Router)
- [Tailwind CSS v4](https://tailwindcss.com/)
- [Chart.js](https://www.chartjs.org/) + react-chartjs-2
- Axios for API requests

**Backend:**
- [FastAPI](https://fastapi.tiangolo.com/) (Python)
- SQLite database (via SQLAlchemy ORM)
- Tesseract OCR (via `pytesseract`)
- Pandas for CSV ingestion
- Passlib + Bcrypt + JWT (Authentication)

---

## Getting Started

### 1. Start the Backend API

Make sure you have Python 3.9+ installed.

```bash
cd fintech-backend

# Create and activate a virtual environment
python -m venv venv
# On Windows:
.\venv\Scripts\activate
# On Mac/Linux:
# source venv/bin/activate

# Install dependencies
pip install -r requirements.txt

# Seed the database with demo data (run this once)
python -m app.seed

# Start the FastAPI development server
uvicorn app.main:app --reload --port 8000
```
*The backend API will run at http://localhost:8000. Interactive swagger docs are available at `http://localhost:8000/docs`.*

### 2. Start the Frontend UI

Open a new terminal window.

```bash
cd fintech-frontend

# Install node dependencies
npm install

# Start the Next.js development server
npm run dev
```
*The frontend application will securely run at http://localhost:3000.*

---

## Demo Login

Use these credentials to log in and test the application with the seeded mock data:
- **Email:** `demo@fintech.com`
- **Password:** `password123`

---

## Repository Structure

```
.
├── fintech-backend/ # Python FastAPI server
│ ├── app/ # Application code (routes, services, models)
│ ├── main.py # FastAPI entry point
│ ├── seed.py # Generates mock transactions/obligations
│ └── requirements.txt # Python dependencies
└── fintech-frontend/ # Next.js UI Application
├── app/ # Next.js app router pages
├── components/ # Reusable React components
├── lib/ # API Axios setup & Auth context
└── ... # Standard Next.js config files
```
26 changes: 26 additions & 0 deletions fintech-backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# IDE / Editor
.vscode/
.idea/
*.swp
*.swo

# Database
fintech.db
*.sqlite3

# Diagnostics
*.log
1 change: 1 addition & 0 deletions fintech-backend/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""App package"""
25 changes: 25 additions & 0 deletions fintech-backend/app/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

DATABASE_URL = "sqlite:///./fintech.db"

engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()


def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()


def init_db():
"""Create all tables."""
from app.models import user, transaction, obligation, receivable, decision # noqa
Base.metadata.create_all(bind=engine)
45 changes: 45 additions & 0 deletions fintech-backend/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""FastAPI application entry point."""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from app.database import init_db
from app.routes import auth, upload, dashboard, decision, simulation, email_gen, reports

app = FastAPI(
title="Fintech Decision Engine",
description="AI-powered cash flow & financial decision system",
version="1.0.0",
)

# Allow Next.js frontend on port 3000
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000", "http://127.0.0.1:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

# Register routers
app.include_router(auth.router)
app.include_router(upload.router)
app.include_router(dashboard.router)
app.include_router(decision.router)
app.include_router(simulation.router)
app.include_router(email_gen.router)
app.include_router(reports.router)


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


@app.get("/")
def root():
return {"message": "Fintech Decision Engine API is running 🚀", "docs": "/docs"}


@app.get("/health")
def health():
return {"status": "ok"}
Empty file.
12 changes: 12 additions & 0 deletions fintech-backend/app/models/decision.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from sqlalchemy import Column, Integer, String, Text, ForeignKey
from app.database import Base


class Decision(Base):
__tablename__ = "decisions"

id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
decision_output = Column(Text, nullable=False)
reasoning = Column(Text, default="")
created_at = Column(String, nullable=False)
18 changes: 18 additions & 0 deletions fintech-backend/app/models/obligation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from sqlalchemy import Column, Integer, Float, String, ForeignKey
from app.database import Base


class Obligation(Base):
__tablename__ = "obligations"

id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
name = Column(String, nullable=False)
amount = Column(Float, nullable=False)
due_date = Column(String, nullable=False) # ISO date string
urgency = Column(Float, default=5.0) # 1-10
penalty = Column(Float, default=5.0) # 1-10
relationship = Column(Float, default=5.0) # 1-10
flexibility = Column(Float, default=5.0) # 1-10
risk_score = Column(Float, default=0.0)
status = Column(String, default="pending") # pending | paid | delayed
13 changes: 13 additions & 0 deletions fintech-backend/app/models/receivable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from sqlalchemy import Column, Integer, Float, String, ForeignKey
from app.database import Base


class Receivable(Base):
__tablename__ = "receivables"

id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
source = Column(String, nullable=False)
amount = Column(Float, nullable=False)
expected_date = Column(String, nullable=False)
status = Column(String, default="pending") # pending | received
14 changes: 14 additions & 0 deletions fintech-backend/app/models/transaction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from sqlalchemy import Column, Integer, Float, String, Date, ForeignKey
from app.database import Base


class Transaction(Base):
__tablename__ = "transactions"

id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
amount = Column(Float, nullable=False)
type = Column(String, nullable=False) # "income" | "expense"
category = Column(String, default="general")
date = Column(String, nullable=False) # stored as ISO string for SQLite
description = Column(String, default="")
11 changes: 11 additions & 0 deletions fintech-backend/app/models/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from sqlalchemy import Column, Integer, String
from app.database import Base


class User(Base):
__tablename__ = "users"

id = Column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False)
email = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False)
1 change: 1 addition & 0 deletions fintech-backend/app/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Routes package"""
49 changes: 49 additions & 0 deletions fintech-backend/app/routes/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Auth routes — POST /register, POST /login"""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from pydantic import BaseModel
from passlib.context import CryptContext
from jose import jwt
import os

from app.database import get_db
from app.models.user import User

router = APIRouter(prefix="/auth", tags=["auth"])

SECRET_KEY = os.getenv("SECRET_KEY", "fintech-secret-2026")
ALGORITHM = "HS256"
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")


class RegisterRequest(BaseModel):
name: str
email: str
password: str


class LoginRequest(BaseModel):
email: str
password: str


@router.post("/register")
def register(req: RegisterRequest, db: Session = Depends(get_db)):
existing = db.query(User).filter(User.email == req.email).first()
if existing:
raise HTTPException(status_code=400, detail="Email already registered")
hashed = pwd_context.hash(req.password)
user = User(name=req.name, email=req.email, hashed_password=hashed)
db.add(user)
db.commit()
db.refresh(user)
return {"message": "Registered successfully", "user_id": user.id}


@router.post("/login")
def login(req: LoginRequest, db: Session = Depends(get_db)):
user = db.query(User).filter(User.email == req.email).first()
if not user or not pwd_context.verify(req.password, user.hashed_password):
raise HTTPException(status_code=401, detail="Invalid credentials")
token = jwt.encode({"user_id": user.id, "email": user.email}, SECRET_KEY, algorithm=ALGORITHM)
return {"token": token, "user_id": user.id, "name": user.name}
62 changes: 62 additions & 0 deletions fintech-backend/app/routes/dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Dashboard route — GET /dashboard"""
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from collections import defaultdict

from app.database import get_db
from app.models.transaction import Transaction
from app.models.obligation import Obligation
from app.models.receivable import Receivable

router = APIRouter(tags=["dashboard"])

DEMO_USER_ID = 1


@router.get("/dashboard")
def get_dashboard(db: Session = Depends(get_db)):
transactions = db.query(Transaction).filter(Transaction.user_id == DEMO_USER_ID).all()

income = sum(t.amount for t in transactions if t.type == "income")
expense = sum(t.amount for t in transactions if t.type == "expense")
balance = income - expense

obligations = db.query(Obligation).filter(
Obligation.user_id == DEMO_USER_ID,
Obligation.status == "pending"
).all()

receivables = db.query(Receivable).filter(
Receivable.user_id == DEMO_USER_ID,
Receivable.status == "pending"
).all()

avg_daily_expense = expense / 90 if expense > 0 else 1
runway_days = round(balance / avg_daily_expense) if avg_daily_expense > 0 else 999

# Build month-by-month history for chart
monthly: dict[str, float] = defaultdict(float)
for t in sorted(transactions, key=lambda x: x.date):
month = t.date[:7] # "YYYY-MM"
if t.type == "income":
monthly[month] += t.amount
else:
monthly[month] -= t.amount

running = 0.0
history = []
for month in sorted(monthly):
running += monthly[month]
history.append({"date": month, "balance": round(running, 2)})

return {
"balance": round(balance, 2),
"income": round(income, 2),
"expense": round(expense, 2),
"runway": runway_days,
"upcoming_obligations": len(obligations),
"upcoming_amount": round(sum(o.amount for o in obligations), 2),
"incoming_receivables": len(receivables),
"incoming_amount": round(sum(r.amount for r in receivables), 2),
"history": history,
}
Loading