From ef26e887132fc5a6a9b55447f0578605d654bab2 Mon Sep 17 00:00:00 2001 From: ribardej Date: Tue, 11 Nov 2025 18:47:35 +0100 Subject: [PATCH] feat(backend): moved mock bank to backend --- 7project/backend/app/api/mock_bank.py | 143 ++++++++++++++++++++++ 7project/backend/app/app.py | 2 + 7project/frontend/src/pages/Dashboard.tsx | 78 ++++++------ 3 files changed, 187 insertions(+), 36 deletions(-) create mode 100644 7project/backend/app/api/mock_bank.py diff --git a/7project/backend/app/api/mock_bank.py b/7project/backend/app/api/mock_bank.py new file mode 100644 index 0000000..551756b --- /dev/null +++ b/7project/backend/app/api/mock_bank.py @@ -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 [])], + ) diff --git a/7project/backend/app/app.py b/7project/backend/app/app.py index e692435..580d12d 100644 --- a/7project/backend/app/app.py +++ b/7project/backend/app/app.py @@ -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) diff --git a/7project/frontend/src/pages/Dashboard.tsx b/7project/frontend/src/pages/Dashboard.tsx index 5252a20..d32a3f4 100644 --- a/7project/frontend/src/pages/Dashboard.tsx +++ b/7project/frontend/src/pages/Dashboard.tsx @@ -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]);