"""Per-session settings (reserves, FX rates) + global header-mapping rules.""" from __future__ import annotations from datetime import date from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy.orm import Session as OrmSession from ...core.column_map import FIELD_ORDER, normalize_header from ...db import models from ..deps import db_dep, get_session_or_404, to_dict router = APIRouter(prefix="/api/sessions", tags=["settings"]) rules_router = APIRouter(prefix="/api/mapping-rules", tags=["mapping"]) class RuleIn(BaseModel): header: str # raw or normalized source header field: str # canonical internal field @rules_router.get("") def list_rules(db: OrmSession = Depends(db_dep)) -> dict: rows = db.query(models.MappingRule).order_by(models.MappingRule.normalized_header).all() return { "fields": FIELD_ORDER, "rules": [{"id": r.id, "normalized_header": r.normalized_header, "field": r.field} for r in rows], } @rules_router.put("") def upsert_rules(items: list[RuleIn], db: OrmSession = Depends(db_dep)) -> dict: """Save admin header-alias rules; applied to every future parse (no code change needed).""" for it in items: if it.field not in FIELD_ORDER: raise HTTPException(400, f"Unknown field '{it.field}'.") norm = normalize_header(it.header) if not norm: raise HTTPException(400, "Empty header.") row = db.query(models.MappingRule).filter( models.MappingRule.normalized_header == norm).first() if row is None: db.add(models.MappingRule(normalized_header=norm, field=it.field)) else: row.field = it.field db.commit() return list_rules(db) @rules_router.delete("/{rule_id}") def delete_rule(rule_id: int, db: OrmSession = Depends(db_dep)) -> dict: db.query(models.MappingRule).filter(models.MappingRule.id == rule_id).delete() db.commit() return list_rules(db) class ReserveIn(BaseModel): marketplace: str account_type: str amount: float = 0.0 class FxIn(BaseModel): marketplace: str currency: str = "USD" rate: float = 1.0 source: str = "manual" rate_date: date | None = None @router.get("/{session_id}/reserves") def get_reserves(session_id: int, db: OrmSession = Depends(db_dep)) -> list[dict]: get_session_or_404(session_id, db) rows = db.query(models.Reserve).filter(models.Reserve.session_id == session_id).all() return [to_dict(r, ["marketplace", "account_type", "amount"]) for r in rows] @router.put("/{session_id}/reserves") def put_reserves(session_id: int, items: list[ReserveIn], db: OrmSession = Depends(db_dep)) -> list[dict]: get_session_or_404(session_id, db) db.query(models.Reserve).filter(models.Reserve.session_id == session_id).delete() for it in items: db.add(models.Reserve(session_id=session_id, marketplace=it.marketplace, account_type=it.account_type, amount=it.amount)) db.commit() return get_reserves(session_id, db) @router.get("/{session_id}/fx") def get_fx(session_id: int, db: OrmSession = Depends(db_dep)) -> list[dict]: get_session_or_404(session_id, db) rows = db.query(models.FxRate).filter(models.FxRate.session_id == session_id).all() return [to_dict(r, ["marketplace", "currency", "rate", "source", "rate_date"]) for r in rows] @router.put("/{session_id}/fx") def put_fx(session_id: int, items: list[FxIn], db: OrmSession = Depends(db_dep)) -> list[dict]: get_session_or_404(session_id, db) db.query(models.FxRate).filter(models.FxRate.session_id == session_id).delete() for it in items: db.add(models.FxRate(session_id=session_id, marketplace=it.marketplace, currency=it.currency, rate=it.rate, source=it.source, rate_date=it.rate_date)) db.commit() return get_fx(session_id, db)