mirror of
https://github.com/dat515-2025/Group-8.git
synced 2026-03-22 15:12:08 +01:00
feat(backend): moved mock bank to backend
This commit is contained in:
143
7project/backend/app/api/mock_bank.py
Normal file
143
7project/backend/app/api/mock_bank.py
Normal file
@@ -0,0 +1,143 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional
|
||||
import random
|
||||
|
||||
from fastapi import APIRouter, Depends, Response, status
|
||||
from pydantic import BaseModel, Field, conint, confloat, validator
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.services.db import get_async_session
|
||||
from app.services.user_service import current_active_user
|
||||
from app.models.user import User
|
||||
from app.models.transaction import Transaction
|
||||
from app.models.categories import Category
|
||||
from app.schemas.transaction import TransactionRead
|
||||
|
||||
router = APIRouter(prefix="/mock-bank", tags=["mock-bank"])
|
||||
|
||||
|
||||
class GenerateOptions(BaseModel):
|
||||
count: conint(strict=True, gt=0) = Field(default=10, description="Number of transactions to generate")
|
||||
minAmount: confloat(strict=True) = Field(default=-200.0, description="Minimum transaction amount")
|
||||
maxAmount: confloat(strict=True) = Field(default=200.0, description="Maximum transaction amount")
|
||||
startDate: Optional[str] = Field(None, description="Earliest date (YYYY-MM-DD)")
|
||||
endDate: Optional[str] = Field(None, description="Latest date (YYYY-MM-DD)")
|
||||
categoryIds: List[int] = Field(default_factory=list, description="Optional category IDs to assign randomly")
|
||||
|
||||
@validator("maxAmount")
|
||||
def _validate_amounts(cls, v, values):
|
||||
min_amt = values.get("minAmount")
|
||||
if min_amt is not None and v < min_amt:
|
||||
raise ValueError("maxAmount must be greater than or equal to minAmount")
|
||||
return v
|
||||
|
||||
@validator("endDate")
|
||||
def _validate_dates(cls, v, values):
|
||||
sd = values.get("startDate")
|
||||
if v and sd:
|
||||
try:
|
||||
ed = datetime.strptime(v, "%Y-%m-%d").date()
|
||||
st = datetime.strptime(sd, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
raise ValueError("Invalid date format, expected YYYY-MM-DD")
|
||||
if ed < st:
|
||||
raise ValueError("endDate must be greater than or equal to startDate")
|
||||
return v
|
||||
|
||||
|
||||
class GeneratedTransaction(BaseModel):
|
||||
amount: float
|
||||
date: str # YYYY-MM-DD
|
||||
category_ids: List[int] = []
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
@router.post("/generate", response_model=List[GeneratedTransaction])
|
||||
async def generate_mock_transactions(
|
||||
options: GenerateOptions,
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
# Seed randomness per user to make results less erratic across multiple calls in quick succession
|
||||
seed = int(datetime.utcnow().timestamp()) ^ int(user.id)
|
||||
rnd = random.Random(seed)
|
||||
|
||||
# Determine date range
|
||||
if options.startDate:
|
||||
start_date = datetime.strptime(options.startDate, "%Y-%m-%d").date()
|
||||
else:
|
||||
start_date = (datetime.utcnow() - timedelta(days=365)).date()
|
||||
if options.endDate:
|
||||
end_date = datetime.strptime(options.endDate, "%Y-%m-%d").date()
|
||||
else:
|
||||
end_date = datetime.utcnow().date()
|
||||
|
||||
span_days = max(0, (end_date - start_date).days)
|
||||
|
||||
results: List[GeneratedTransaction] = []
|
||||
for _ in range(options.count):
|
||||
amount = round(rnd.uniform(options.minAmount, options.maxAmount), 2)
|
||||
# Pick a random date in the inclusive range
|
||||
rand_day = rnd.randint(0, span_days) if span_days > 0 else 0
|
||||
tx_date = start_date + timedelta(days=rand_day)
|
||||
# Pick category randomly from provided list, or empty
|
||||
if options.categoryIds:
|
||||
cat = [rnd.choice(options.categoryIds)]
|
||||
else:
|
||||
cat = []
|
||||
# Optional simple description for flavor
|
||||
desc = None
|
||||
# Assemble
|
||||
results.append(GeneratedTransaction(
|
||||
amount=amount,
|
||||
date=tx_date.isoformat(),
|
||||
category_ids=cat,
|
||||
description=desc,
|
||||
))
|
||||
|
||||
return results
|
||||
|
||||
|
||||
|
||||
@router.get("/scrape", response_model=Optional[TransactionRead])
|
||||
async def scrape_mock_bank(
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
# 95% of the time: nothing to scrape
|
||||
if random.random() < 0.95:
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
# 5% chance: create a new transaction and return it
|
||||
amount = round(random.uniform(-200.0, 200.0), 2)
|
||||
tx_date = datetime.utcnow().date()
|
||||
|
||||
# Optionally attach a random category owned by this user (if any)
|
||||
res = await session.execute(select(Category).where(Category.user_id == user.id))
|
||||
user_categories = list(res.scalars())
|
||||
chosen_categories = []
|
||||
if user_categories:
|
||||
chosen_categories = [random.choice(user_categories)]
|
||||
|
||||
# Build and persist transaction
|
||||
tx = Transaction(
|
||||
amount=amount,
|
||||
description="Mock bank scrape",
|
||||
user_id=user.id,
|
||||
date=tx_date,
|
||||
)
|
||||
if chosen_categories:
|
||||
tx.categories = chosen_categories
|
||||
|
||||
session.add(tx)
|
||||
await session.commit()
|
||||
await session.refresh(tx)
|
||||
await session.refresh(tx, attribute_names=["categories"]) # ensure categories are loaded
|
||||
|
||||
return TransactionRead(
|
||||
id=tx.id,
|
||||
amount=float(tx.amount),
|
||||
description=tx.description,
|
||||
date=tx.date,
|
||||
category_ids=[c.id for c in (tx.categories or [])],
|
||||
)
|
||||
@@ -68,6 +68,8 @@ fastApi.include_router(auth_router)
|
||||
fastApi.include_router(categories_router)
|
||||
fastApi.include_router(transactions_router)
|
||||
fastApi.include_router(exchange_rates_router)
|
||||
from app.api.mock_bank import router as mock_bank_router
|
||||
fastApi.include_router(mock_bank_router)
|
||||
|
||||
for h in list(logging.root.handlers):
|
||||
logging.root.removeHandler(h)
|
||||
|
||||
@@ -195,44 +195,50 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
|
||||
setIsGenerating(true);
|
||||
setMockModalOpen(false);
|
||||
|
||||
const { count, minAmount, maxAmount, startDate, endDate, categoryIds } = options;
|
||||
const newTransactions: Transaction[] = [];
|
||||
|
||||
const startDateTime = new Date(startDate).getTime();
|
||||
const endDateTime = new Date(endDate).getTime();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// Generate random data based on user input
|
||||
const amount = parseFloat((Math.random() * (maxAmount - minAmount) + minAmount).toFixed(2));
|
||||
|
||||
const randomTime = Math.random() * (endDateTime - startDateTime) + startDateTime;
|
||||
const date = new Date(randomTime);
|
||||
const dateString = date.toISOString().split('T')[0];
|
||||
|
||||
const randomCategory = categoryIds.length > 0
|
||||
? [categoryIds[Math.floor(Math.random() * categoryIds.length)]]
|
||||
: [];
|
||||
|
||||
const payload = {
|
||||
amount,
|
||||
date: dateString,
|
||||
category_ids: randomCategory,
|
||||
};
|
||||
|
||||
try {
|
||||
const created = await createTransaction(payload);
|
||||
newTransactions.push(created);
|
||||
} catch (err) {
|
||||
console.error("Failed to create mock transaction:", err);
|
||||
alert('An error occurred while generating transactions. Check the console.');
|
||||
break;
|
||||
try {
|
||||
const base = BACKEND_URL.replace(/\/$/, '');
|
||||
const url = `${base}/mock-bank/generate`;
|
||||
const token = localStorage.getItem('token');
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(options),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text || `Failed to generate mock transactions (${res.status})`);
|
||||
}
|
||||
const generated: Array<{ amount: number; date: string; category_ids: number[]; description?: string | null }>
|
||||
= await res.json();
|
||||
|
||||
const newTransactions: Transaction[] = [];
|
||||
for (const g of generated) {
|
||||
try {
|
||||
const created = await createTransaction({
|
||||
amount: g.amount,
|
||||
date: g.date,
|
||||
category_ids: g.category_ids || [],
|
||||
description: g.description || undefined,
|
||||
});
|
||||
newTransactions.push(created);
|
||||
} catch (err) {
|
||||
console.error('Failed to create mock transaction:', err);
|
||||
// continue creating others
|
||||
}
|
||||
}
|
||||
|
||||
alert(`${newTransactions.length} mock transactions were successfully generated!`);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
alert(err?.message || 'Failed to generate mock transactions');
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
await loadAll();
|
||||
}
|
||||
|
||||
setIsGenerating(false);
|
||||
alert(`${newTransactions.length} mock transactions were successfully generated!`);
|
||||
|
||||
await loadAll();
|
||||
}
|
||||
|
||||
useEffect(() => { loadAll(); }, [startDate, endDate]);
|
||||
|
||||
Reference in New Issue
Block a user