fts_core/models/
auth.rs

1use super::demand;
2use crate::models::{BidderId, Bound, DemandCurve, Group, ProductId, map_wrapper, uuid_wrapper};
3use serde::{Deserialize, Serialize};
4use std::hash::Hash;
5use thiserror::Error;
6use time::OffsetDateTime;
7use utoipa::ToSchema;
8
9uuid_wrapper!(AuthId);
10
11/// An authorization defines a portfolio and associates some data. This data
12/// describes any trading constraints, as well as a default demand curve to
13/// associate to the portfolio.
14#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)]
15#[serde(try_from = "RawAuthorization", into = "RawAuthorization")]
16pub struct AuthData {
17    /// The demand curve to associate to the portfolio
18    pub demand: DemandCurve,
19
20    /// A minimum amount of trade to preserve (always enforced against the authorization's contemporaneous amount of trade)
21    #[schema(value_type = Option<f64>)]
22    pub min_trade: f64,
23
24    /// A maximum amount of trade to preserve (always enforced against the authorization's contemporaneous amount of trade)
25    #[schema(value_type = Option<f64>)]
26    pub max_trade: f64,
27}
28
29impl AuthData {
30    /// Creates a new AuthData with the specified constraints.
31    pub fn new(
32        demand: DemandCurve,
33        min_trade: f64,
34        max_trade: f64,
35    ) -> Result<Self, ValidationError> {
36        if min_trade.is_nan() || max_trade.is_nan() {
37            return Err(ValidationError::NAN);
38        }
39        if min_trade > max_trade {
40            return Err(ValidationError::INFEASIBLETRADE);
41        }
42
43        Ok(Self {
44            demand,
45            min_trade,
46            max_trade,
47        })
48    }
49}
50
51/// An enumeration of the ways authorization data may be invalid
52#[derive(Debug, Error)]
53pub enum ValidationError {
54    /// Error when any constraint value is NaN
55    #[error("NaN value encountered")]
56    NAN,
57    /// Error when demand curve is invalid
58    #[error("Invalid demand curve: {0:?}")]
59    DEMAND(demand::ValidationError),
60    /// Error when min_trade > max_trade
61    #[error("Trade restriction is infeasible")]
62    INFEASIBLETRADE,
63}
64
65/// The "DTO" type for AuthData. Omitted values default to the appropriately signed infinity.
66///
67/// This provides a user-friendly interface for specifying auth constraints, allowing
68/// missing values to default to appropriate infinities.
69#[derive(Serialize, Deserialize)]
70pub struct RawAuthorization {
71    pub demand: demand::RawDemandCurve,
72    pub min_trade: Bound,
73    pub max_trade: Bound,
74}
75
76impl TryFrom<RawAuthorization> for AuthData {
77    type Error = ValidationError;
78
79    fn try_from(value: RawAuthorization) -> Result<Self, Self::Error> {
80        AuthData::new(
81            value
82                .demand
83                .try_into()
84                .map_err(|err| ValidationError::DEMAND(err))?,
85            value.min_trade.or_neg_inf(),
86            value.max_trade.or_pos_inf(),
87        )
88    }
89}
90
91impl From<AuthData> for RawAuthorization {
92    fn from(value: AuthData) -> Self {
93        Self {
94            demand: value.demand.into(),
95            min_trade: value.min_trade.into(),
96            max_trade: value.max_trade.into(),
97        }
98    }
99}
100
101/// A record of the authorization's data at the time it was updated or defined
102///
103/// This provides historical versioning of auth constraints, allowing the system
104/// to track changes to auth parameters over time.
105#[derive(Serialize, Deserialize, PartialEq, ToSchema, Debug)]
106pub struct AuthHistoryRecord {
107    /// The authorization constraints, or None if the auth was deactivated
108    pub data: Option<AuthData>,
109    /// The timestamp when this version was created
110    #[serde(with = "time::serde::rfc3339")]
111    pub version: OffsetDateTime,
112}
113
114/// A full description of an authorization
115///
116/// An AuthRecord combines all the information needed to define an authorization:
117/// - Who owns it (bidder_id)
118/// - What it trades (portfolio)
119/// - How it can be traded (data)
120/// - The current accumulated trade (trade)
121#[derive(Serialize, Deserialize, PartialEq, ToSchema, Debug)]
122pub struct AuthRecord {
123    /// The responsible bidder's id
124    pub bidder_id: BidderId,
125
126    /// A unique id for the auth
127    pub auth_id: AuthId,
128
129    /// The portfolio associated to the auth. Due to the expected size, this portfolio may be omitted from certain endpoints.
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub portfolio: Option<Portfolio>,
132
133    /// The constraint data for the authorization
134    pub data: Option<AuthData>,
135
136    /// The "last-modified-or-created" time as recorded by the system
137    #[serde(with = "time::serde::rfc3339")]
138    pub version: OffsetDateTime,
139
140    /// The amount of cumulative trade associated to this authorization, as-of the request time
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub trade: Option<f64>,
143}
144
145map_wrapper!(Portfolio, ProductId, f64);
146
147impl AuthRecord {
148    /// Converts this auth record into a solver-compatible format.
149    ///
150    /// This method applies the time scale to rate-based constraints and computes
151    /// the appropriate trade bounds for the auction, taking into account the
152    /// current accumulated trade.
153    pub fn into_solver(
154        self,
155        scale: f64,
156    ) -> Option<(
157        Portfolio,
158        fts_solver::DemandCurve<AuthId, Group, Vec<fts_solver::Point>>,
159    )> {
160        let trade = self.trade.unwrap_or_default();
161        if let Some(data) = self.data {
162            let (min_rate, max_rate) = data.demand.domain();
163            let min_trade = (data.min_trade - trade).max(min_rate * scale).min(0.0);
164            let max_trade = (data.max_trade - trade).min(max_rate * scale).max(0.0);
165
166            Some((
167                self.portfolio.unwrap_or_default(),
168                fts_solver::DemandCurve {
169                    domain: (min_trade, max_trade),
170                    group: std::iter::once((self.auth_id, 1.0)).collect(),
171                    points: data.demand.as_solver(scale),
172                },
173            ))
174        } else {
175            None
176        }
177    }
178}