chio_kernel/operator_report/
comptroller_surface.rs1use chio_core_types::CHIO_COMPTROLLER_SURFACE_REPORT_V1_SCHEMA;
8use chio_credit::{ExposureLedgerCurrencyPosition, ExposureLedgerReport};
9use serde::{Deserialize, Serialize};
10
11use super::{
12 BudgetUtilizationSummary, OperatorReport, OperatorReportQuery, SettlementReconciliationSummary,
13};
14
15pub const COMPTROLLER_SURFACE_REPORT_SCHEMA: &str = CHIO_COMPTROLLER_SURFACE_REPORT_V1_SCHEMA;
17
18#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
20#[serde(rename_all = "camelCase")]
21pub struct ComptrollerDecisionSummary {
22 pub allow_count: u64,
23 pub deny_count: u64,
24 pub cancelled_count: u64,
25 pub incomplete_count: u64,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct ComptrollerSurfaceSourceRefs {
32 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub operator_report_ref: Option<String>,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub exposure_ledger_ref: Option<String>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub risk_comptroller_report_ref: Option<String>,
38}
39
40#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
42#[serde(rename_all = "camelCase")]
43pub struct ComptrollerSurfaceReport {
44 pub schema: String,
45 pub generated_at: u64,
46 pub filters: OperatorReportQuery,
47 pub exposure_positions: Vec<ExposureLedgerCurrencyPosition>,
48 pub decision_summary: ComptrollerDecisionSummary,
49 pub settlement_reconciliation: SettlementReconciliationSummary,
50 pub budget_utilization: BudgetUtilizationSummary,
51 pub source_refs: ComptrollerSurfaceSourceRefs,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub execution_nonce_ref: Option<String>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub hold_ref: Option<String>,
59}
60
61impl ComptrollerSurfaceReport {
62 pub fn from_parts(operator: &OperatorReport, exposure: &ExposureLedgerReport) -> Self {
64 Self {
65 schema: COMPTROLLER_SURFACE_REPORT_SCHEMA.to_string(),
66 generated_at: operator.generated_at,
67 filters: operator.filters.clone(),
68 exposure_positions: exposure.positions.clone(),
69 decision_summary: ComptrollerDecisionSummary {
70 allow_count: operator.activity.summary.allow_count,
71 deny_count: operator.activity.summary.deny_count,
72 cancelled_count: operator.activity.summary.cancelled_count,
73 incomplete_count: operator.activity.summary.incomplete_count,
74 },
75 settlement_reconciliation: operator.settlement_reconciliation.summary.clone(),
76 budget_utilization: operator.budget_utilization.summary.clone(),
77 source_refs: ComptrollerSurfaceSourceRefs::default(),
78 execution_nonce_ref: None,
79 hold_ref: None,
80 }
81 }
82
83 pub fn validate_consistency(&self) -> Result<(), String> {
90 for position in &self.exposure_positions {
91 if position.governed_max_exposure_units == 0 {
92 continue;
93 }
94 let Some(outstanding) = position.reserved_units.checked_add(position.pending_units)
100 else {
101 return Err(format!(
102 "exposure position {} outstanding holds overflow u64 (reserved {} + pending {})",
103 position.currency, position.reserved_units, position.pending_units
104 ));
105 };
106 if outstanding > position.governed_max_exposure_units {
107 return Err(format!(
108 "exposure position {} outstanding holds {} exceed governed ceiling {}",
109 position.currency, outstanding, position.governed_max_exposure_units
110 ));
111 }
112 }
113 Ok(())
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use chio_credit::ExposureLedgerCurrencyPosition;
121
122 fn position(
123 currency: &str,
124 governed: u64,
125 reserved: u64,
126 pending: u64,
127 ) -> ExposureLedgerCurrencyPosition {
128 ExposureLedgerCurrencyPosition {
129 currency: currency.to_string(),
130 governed_max_exposure_units: governed,
131 reserved_units: reserved,
132 settled_units: 0,
133 pending_units: pending,
134 failed_units: 0,
135 provisional_loss_units: 0,
136 recovered_units: 0,
137 quoted_premium_units: 0,
138 active_quoted_premium_units: 0,
139 }
140 }
141
142 fn sample() -> ComptrollerSurfaceReport {
143 ComptrollerSurfaceReport {
144 schema: COMPTROLLER_SURFACE_REPORT_SCHEMA.to_string(),
145 generated_at: 1_700_000_000,
146 filters: OperatorReportQuery::default(),
147 exposure_positions: vec![position("USD", 4200, 1000, 200)],
148 decision_summary: ComptrollerDecisionSummary {
149 allow_count: 1,
150 deny_count: 1,
151 cancelled_count: 0,
152 incomplete_count: 0,
153 },
154 settlement_reconciliation: SettlementReconciliationSummary::default(),
155 budget_utilization: BudgetUtilizationSummary::default(),
156 source_refs: ComptrollerSurfaceSourceRefs::default(),
157 execution_nonce_ref: None,
158 hold_ref: None,
159 }
160 }
161
162 #[test]
163 fn serde_round_trip_is_camel_case() {
164 let report = sample();
165 let json = serde_json::to_string(&report).expect("serialize");
166 assert!(json.contains("\"schema\":\"chio.comptroller.surface-report.v1\""));
167 assert!(json.contains("\"generatedAt\""));
168 assert!(json.contains("\"exposurePositions\""));
169 assert!(json.contains("\"governedMaxExposureUnits\""));
170 assert!(json.contains("\"allowCount\""));
171 assert!(!json.contains("executionNonceRef"));
173 assert!(!json.contains("holdRef"));
174 let back: ComptrollerSurfaceReport = serde_json::from_str(&json).expect("deserialize");
175 assert_eq!(report, back);
176 }
177
178 #[test]
179 fn validate_consistency_accepts_coherent_positions() {
180 assert!(sample().validate_consistency().is_ok());
181 }
182
183 #[test]
184 fn validate_consistency_rejects_outstanding_over_governed_ceiling() {
185 let mut report = sample();
186 report.exposure_positions = vec![position("USD", 4200, 5000, 0)];
187 let err = report
188 .validate_consistency()
189 .expect_err("must reject over-ceiling");
190 assert!(err.contains("exceed governed ceiling"));
191 }
192
193 #[test]
194 fn validate_consistency_treats_zero_ceiling_as_no_ceiling() {
195 let mut report = sample();
196 report.exposure_positions = vec![position("USD", 0, u64::MAX / 2, u64::MAX / 2)];
197 assert!(report.validate_consistency().is_ok());
198 }
199
200 #[test]
201 fn validate_consistency_rejects_outstanding_overflow_over_ceiling() {
202 let mut report = sample();
203 report.exposure_positions = vec![position("USD", u64::MAX, u64::MAX, 1)];
207 let err = report
208 .validate_consistency()
209 .expect_err("overflowing outstanding must fail closed");
210 assert!(err.contains("overflow"), "unexpected error: {err}");
211 }
212}