fts_core/models/
cost.rs

1use super::demand;
2use crate::models::{AuthId, BidderId, DemandCurve, map_wrapper, uuid_wrapper};
3use serde::{Deserialize, Serialize};
4use std::hash::Hash;
5use time::OffsetDateTime;
6use utoipa::ToSchema;
7
8// A simple newtype for a Uuid
9uuid_wrapper!(CostId);
10
11/// A cost defines a group and associates some data to it. This data
12/// describes the utility implied by trades in the form of a demand curve.
13#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)]
14#[serde(try_from = "RawCost", into = "RawCost")]
15pub struct CostData {
16    /// The demand curve to associate to the portfolio
17    pub demand: DemandCurve,
18}
19
20impl CostData {
21    /// Creates a new AuthData with the specified constraints.
22    pub fn new(demand: DemandCurve) -> Self {
23        Self { demand }
24    }
25}
26
27/// The "DTO" type for CostData.
28#[derive(Serialize, Deserialize)]
29pub struct RawCost {
30    pub demand: demand::RawDemandCurve,
31}
32
33impl TryFrom<RawCost> for CostData {
34    type Error = demand::ValidationError;
35
36    fn try_from(value: RawCost) -> Result<Self, Self::Error> {
37        Ok(CostData::new(value.demand.try_into()?))
38    }
39}
40
41impl From<CostData> for RawCost {
42    fn from(value: CostData) -> Self {
43        Self {
44            demand: value.demand.into(),
45        }
46    }
47}
48
49/// Controls whether cost group details should be included in API responses
50///
51/// Since cost groups can be large, this enum allows API endpoints to optionally
52/// exclude group details from responses to reduce payload size.
53#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
54#[serde(rename_all = "lowercase")]
55pub enum GroupDisplay {
56    /// Exclude group details from the response
57    Exclude,
58    /// Include group details in the response
59    Include,
60}
61
62impl Default for GroupDisplay {
63    fn default() -> Self {
64        Self::Exclude
65    }
66}
67
68/// A record of the cost's data at the time it was updated or defined
69///
70/// This provides historical versioning of cost data, allowing the system
71/// to track changes to cost definitions over time.
72#[derive(Serialize, Deserialize, PartialEq, ToSchema, Debug)]
73pub struct CostHistoryRecord {
74    /// The cost data, or None if the cost was deactivated
75    pub data: Option<CostData>,
76    /// The timestamp when this version was created
77    #[serde(with = "time::serde::rfc3339")]
78    pub version: OffsetDateTime,
79}
80
81/// A full description of a cost
82///
83/// A CostRecord combines all the information needed to define a cost:
84/// - Who owns it (bidder_id)
85/// - Which auths it applies to (group)
86/// - The utility function (data)
87#[derive(Serialize, Deserialize, PartialEq, Debug, ToSchema)]
88pub struct CostRecord {
89    /// The responsible bidder's id
90    pub bidder_id: BidderId,
91
92    /// A unique id for the cost
93    pub cost_id: CostId,
94
95    /// The group associated to the cost. Because it is not always required, some endpoints may omit its definition.
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    #[schema(value_type = Option<std::collections::HashMap<AuthId, f64>>)]
98    pub group: Option<Group>,
99
100    /// The utility for the cost
101    pub data: Option<CostData>,
102
103    /// The "last-modified-or-created" time as recorded by the system
104    #[serde(with = "time::serde::rfc3339")]
105    pub version: OffsetDateTime,
106}
107
108map_wrapper!(Group, AuthId, f64);
109
110impl CostRecord {
111    /// Converts this cost record into a solver-compatible format.
112    ///
113    /// This method transforms the cost record into the appropriate solver structures,
114    /// applying the time scale to rate-based constraints as needed.
115    pub fn into_solver(
116        self,
117        scale: f64,
118    ) -> Option<fts_solver::DemandCurve<AuthId, Group, Vec<fts_solver::Point>>> {
119        if let Some(data) = self.data {
120            let group = self.group.unwrap_or_default();
121            let points = match data.demand {
122                DemandCurve::Curve(curve) => curve.as_solver(scale),
123                DemandCurve::Constant(constant) => constant.as_solver(scale),
124            };
125            let domain = (
126                points.first().map(|pt| pt.quantity).unwrap_or_default(),
127                points.last().map(|pt| pt.quantity).unwrap_or_default(),
128            );
129
130            Some(fts_solver::DemandCurve {
131                domain,
132                group,
133                points,
134            })
135        } else {
136            None
137        }
138    }
139}