use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FpslInterest {
pub customer: f64,
pub partner: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FpslLoan {
pub account_id: String,
pub account_number: String,
pub correspondent: String,
pub symbol: String,
pub quantity: i64,
pub market_value: f64,
pub collateral: f64,
pub date: NaiveDate,
#[serde(default)]
pub interest: Option<FpslInterest>,
#[serde(default)]
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FpslLoansPage {
#[serde(
default,
deserialize_with = "crate::types::serde_util::null_as_default"
)]
pub loans: Vec<FpslLoan>,
#[serde(default)]
pub next_page_token: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FpslTier {
#[serde(default)]
pub id: Option<Uuid>,
#[serde(default)]
pub tier_name: Option<String>,
#[serde(default)]
pub market: Option<String>,
#[serde(default)]
pub customer_split: Option<f64>,
#[serde(default)]
pub partner_split: Option<f64>,
#[serde(default)]
pub created_at: Option<DateTime<Utc>>,
#[serde(default)]
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FpslAnalytics {
pub account_number: String,
pub total_lending_activities: i64,
pub in_progress_lending_activities: i64,
#[serde(default)]
pub interest: Option<FpslInterest>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetFpslLoansRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub account_id: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page_token: Option<String>,
}
impl GetFpslLoansRequest {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn account_id(mut self, account_id: Uuid) -> Self {
self.account_id = Some(account_id);
self
}
pub fn between(mut self, start: NaiveDate, end: NaiveDate) -> crate::Result<Self> {
if end < start {
return Err(crate::Error::InvalidRequest(format!(
"end ({end}) is before start ({start})"
)));
}
self.start = Some(start);
self.end = Some(end);
Ok(self)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetFpslAnalyticsRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end: Option<NaiveDate>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_loan_without_an_interest_split_decodes() {
let loan: FpslLoan = serde_json::from_value(serde_json::json!({
"account_id": "550e8400-e29b-41d4-a716-446655440000",
"account_number": "123",
"correspondent": "TEST",
"symbol": "AAPL",
"quantity": 100,
"market_value": 18500.0,
"collateral": 18500.0,
"date": "2026-01-02",
}))
.unwrap();
assert_eq!(loan.interest, None);
assert_eq!(loan.quantity, 100);
}
#[test]
fn a_backwards_loan_window_is_refused() {
let start: NaiveDate = "2026-02-01".parse().unwrap();
let end: NaiveDate = "2026-01-01".parse().unwrap();
assert!(GetFpslLoansRequest::new().between(start, end).is_err());
}
}