//! Kur ve portföy uç noktaları (hanenin tüm üyeleri): //! //! | Yöntem | Yol | İş | //! |---|---|---| //! | GET | `/api/portfolio` | varlıklar + güncel kur, değer, kâr/zarar + toplamlar + son kur çekimi | //! | POST | `/api/rates` | elle kur (hanenin; aynı gün tekrar → güncellenir) | //! | DELETE | `/api/rates/{id}` | elle kuru sil → otomatik kura dön | use axum::{ Json, extract::{Path, State}, http::StatusCode, }; use mongodb::bson::{DateTime, Uuid}; use serde::{Deserialize, Serialize}; use ts_rs::TS; use validator::Validate; use crate::{ auth::AuthUser, error::{AppError, AppResult}, extract::ValidJson, models::rate::Rate, routes::asset::AssetDto, services::rates, }; #[derive(Serialize, TS)] #[ts(export, rename = "Rate")] pub struct RateDto { id: String, code: String, /// TL × 1e6, birim başına #[ts(type = "number")] rate_e6: i64, /// `tcmb` · `xaus` · `manual` #[ts(type = "\"tcmb\" | \"xaus\" | \"manual\"")] source: String, rate_date: String, /// ISO 8601 fetched_at: String, is_manual: bool, } fn iso(d: DateTime) -> String { d.try_to_rfc3339_string().unwrap_or_default() } impl From for RateDto { fn from(r: Rate) -> Self { Self { id: r.id.to_string(), is_manual: r.is_manual(), code: r.code, rate_e6: r.rate_e6, source: r.source, rate_date: r.rate_date, fetched_at: iso(r.fetched_at), } } } #[derive(Serialize, TS)] #[ts(export, rename = "PortfolioItem")] struct ItemDto { asset: AssetDto, /// TL'de ve kur yoksa `null` rate: Option, /// Güncel değer (kuruş); kur yoksa `null` #[ts(type = "number | null")] value: Option, /// Değer − eldeki maliyet (kuruş) #[ts(type = "number | null")] unrealized_pl: Option, } #[derive(Serialize, TS)] #[ts(export, rename = "Portfolio")] pub struct PortfolioDto { items: Vec, /// Eldeki maliyet toplamı (kuruş) #[ts(type = "number")] cost_total: i64, /// Kuru olanların güncel değeri (kuruş) #[ts(type = "number")] value_total: i64, /// Kuru olanlarda değer − maliyet #[ts(type = "number")] unrealized_pl_total: i64, #[ts(type = "number")] realized_pl_total: i64, /// Elde olup kuru bulunmayan varlık sayısı (toplama girmedi) unpriced: u32, /// Son otomatik kur çekimi (ISO); hiç yoksa `null` last_fetch: Option, } pub async fn portfolio( State(s): State, auth: AuthUser, ) -> AppResult> { let p = rates::portfolio(&s.db, auth.household_id).await?; let mut dto = PortfolioDto { items: Vec::with_capacity(p.items.len()), cost_total: 0, value_total: 0, unrealized_pl_total: 0, realized_pl_total: 0, unpriced: 0, last_fetch: p.last_fetch.map(iso), }; for v in p.items { let h = v.holding; dto.cost_total += h.cost; dto.realized_pl_total += h.realized_pl; let unrealized = v.value.map(|x| x - h.cost); match v.value { Some(x) => { dto.value_total += x; dto.unrealized_pl_total += x - h.cost; } None if h.quantity_e6 > 0 => dto.unpriced += 1, None => {} } dto.items.push(ItemDto { asset: AssetDto::new(v.asset, h, v.tx_count), rate: v.rate.map(RateDto::from), value: v.value, unrealized_pl: unrealized, }); } Ok(Json(dto)) } #[derive(Deserialize, Validate, TS)] #[ts(export, rename = "RateCreateRequest", optional_fields)] pub struct CreateRequest { /// Varlık kodu (`XAU_CEYREK`, `USD`…) code: String, /// TL × 1e6, birim başına #[ts(type = "number")] rate_e6: i64, /// Boşsa bugün rate_date: Option, } pub async fn create( State(s): State, auth: AuthUser, ValidJson(req): ValidJson, ) -> AppResult<(StatusCode, Json)> { let r = rates::set_manual( &s.db, auth.household_id, &req.code, req.rate_e6, req.rate_date.as_deref(), ) .await?; Ok((StatusCode::CREATED, Json(r.into()))) } pub async fn delete( State(s): State, auth: AuthUser, Path(id): Path, ) -> AppResult { let id = Uuid::parse_str(&id).map_err(|_| AppError::NotFound)?; rates::delete_manual(&s.db, auth.household_id, id).await?; Ok(StatusCode::NO_CONTENT) }