Skip to main content

chio_credit/
risk_reports.rs

1use serde::{Deserialize, Serialize};
2
3use crate::appraisal::AttestationVerifierFamily;
4use crate::capability::{runtime_attestation::RuntimeAssuranceTier, scope::MonetaryAmount};
5use crate::receipt::{economics::SettlementStatus, lineage::SignedExportEnvelope};
6use crate::underwriting::{UnderwritingCertificationState, UnderwritingComplianceEvidence};
7use crate::{
8    bounded_limit_or_default, CapitalExecutionAuthorityStep, CapitalExecutionObservation,
9    CapitalExecutionRail, CapitalExecutionReconciledState, CapitalExecutionWindow,
10    CreditBondLifecycleState, CreditFacilityDisposition, CreditFacilityLifecycleState,
11    CreditFacilityReport, CreditFacilityTerms, CreditScorecardBand,
12    CreditScorecardEvidenceReference, CreditScorecardSummary, ExposureLedgerEvidenceReference,
13    ExposureLedgerQuery, SignedCreditScorecardReport, SignedExposureLedgerReport,
14    MAX_CREDIT_BACKTEST_WINDOW_LIMIT, MAX_CREDIT_LOSS_LIFECYCLE_LIST_LIMIT,
15    MAX_CREDIT_PROVIDER_LOSS_LIMIT, MAX_EXPOSURE_LEDGER_DECISION_LIMIT,
16    MAX_EXPOSURE_LEDGER_RECEIPT_LIMIT,
17};
18
19#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
20#[serde(rename_all = "snake_case")]
21pub enum CreditLossLifecycleEventKind {
22    Delinquency,
23    Recovery,
24    ReserveRelease,
25    ReserveSlash,
26    WriteOff,
27}
28
29#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
30#[serde(rename_all = "snake_case")]
31pub enum CreditLossLifecycleReasonCode {
32    ActiveBondRequired,
33    BondNotActive,
34    DelinquencyEvidenceMissing,
35    OutstandingDelinquencyRequired,
36    AmountRequired,
37    AmountCurrencyMismatch,
38    AmountExceedsOutstandingDelinquency,
39    OutstandingExposureBlocksReserveRelease,
40    ReserveAlreadyReleased,
41    DelinquencyRecorded,
42    RecoveryRecorded,
43    ReserveReleased,
44    ReserveSlashed,
45    WriteOffRecorded,
46}
47
48#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
49#[serde(rename_all = "snake_case")]
50pub enum CreditReserveControlExecutionState {
51    PendingExecution,
52    Executed,
53}
54
55#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
56#[serde(rename_all = "snake_case")]
57pub enum CreditReserveControlAppealState {
58    Unsupported,
59    Open,
60    Closed,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
64#[serde(rename_all = "camelCase")]
65pub struct CreditLossLifecycleQuery {
66    pub bond_id: String,
67    pub event_kind: CreditLossLifecycleEventKind,
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub amount: Option<MonetaryAmount>,
70}
71
72impl CreditLossLifecycleQuery {
73    pub fn validate(&self) -> Result<(), String> {
74        if self.bond_id.trim().is_empty() {
75            return Err("credit loss lifecycle requests require --bond-id".to_string());
76        }
77        if self.amount.as_ref().is_some_and(|amount| amount.units == 0) {
78            return Err("credit loss lifecycle amounts must be greater than zero".to_string());
79        }
80        Ok(())
81    }
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
85#[serde(rename_all = "camelCase")]
86pub struct CreditLossLifecycleSummary {
87    pub bond_id: String,
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub facility_id: Option<String>,
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub capability_id: Option<String>,
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    pub agent_subject: Option<String>,
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub tool_server: Option<String>,
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub tool_name: Option<String>,
98    pub current_bond_lifecycle_state: CreditBondLifecycleState,
99    pub projected_bond_lifecycle_state: CreditBondLifecycleState,
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub current_delinquent_amount: Option<MonetaryAmount>,
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub current_recovered_amount: Option<MonetaryAmount>,
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub current_written_off_amount: Option<MonetaryAmount>,
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub current_released_reserve_amount: Option<MonetaryAmount>,
108    #[serde(default, skip_serializing_if = "Option::is_none")]
109    pub current_slashed_reserve_amount: Option<MonetaryAmount>,
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    pub outstanding_delinquent_amount: Option<MonetaryAmount>,
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    pub releaseable_reserve_amount: Option<MonetaryAmount>,
114    #[serde(default, skip_serializing_if = "Option::is_none")]
115    pub reserve_control_source_id: Option<String>,
116    #[serde(default, skip_serializing_if = "Option::is_none")]
117    pub execution_state: Option<CreditReserveControlExecutionState>,
118    #[serde(default, skip_serializing_if = "Option::is_none")]
119    pub appeal_state: Option<CreditReserveControlAppealState>,
120    #[serde(default, skip_serializing_if = "Option::is_none")]
121    pub appeal_window_ends_at: Option<u64>,
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    pub event_amount: Option<MonetaryAmount>,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
127#[serde(rename_all = "camelCase")]
128pub struct CreditLossLifecycleFinding {
129    pub code: CreditLossLifecycleReasonCode,
130    pub description: String,
131    #[serde(default, skip_serializing_if = "Vec::is_empty")]
132    pub evidence_refs: Vec<CreditScorecardEvidenceReference>,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
136#[serde(rename_all = "camelCase")]
137pub struct CreditLossLifecycleSupportBoundary {
138    pub immutable_lifecycle_authoritative: bool,
139    pub bond_lifecycle_projection_authoritative: bool,
140    pub external_claim_adjudication_supported: bool,
141    pub automatic_capital_execution_supported: bool,
142    pub reserve_control_execution_supported: bool,
143    pub appeal_window_supported: bool,
144}
145
146impl Default for CreditLossLifecycleSupportBoundary {
147    fn default() -> Self {
148        Self {
149            immutable_lifecycle_authoritative: true,
150            bond_lifecycle_projection_authoritative: true,
151            external_claim_adjudication_supported: false,
152            automatic_capital_execution_supported: false,
153            reserve_control_execution_supported: true,
154            appeal_window_supported: true,
155        }
156    }
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
160#[serde(rename_all = "camelCase")]
161pub struct CreditLossLifecycleReport {
162    pub schema: String,
163    pub generated_at: u64,
164    pub query: CreditLossLifecycleQuery,
165    pub summary: CreditLossLifecycleSummary,
166    pub support_boundary: CreditLossLifecycleSupportBoundary,
167    #[serde(default, skip_serializing_if = "Vec::is_empty")]
168    pub findings: Vec<CreditLossLifecycleFinding>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
172#[serde(rename_all = "camelCase")]
173pub struct CreditLossLifecycleArtifact {
174    pub schema: String,
175    pub event_id: String,
176    pub issued_at: u64,
177    pub bond_id: String,
178    pub event_kind: CreditLossLifecycleEventKind,
179    pub projected_bond_lifecycle_state: CreditBondLifecycleState,
180    #[serde(default, skip_serializing_if = "Option::is_none")]
181    pub reserve_control_source_id: Option<String>,
182    #[serde(default, skip_serializing_if = "Vec::is_empty")]
183    pub authority_chain: Vec<CapitalExecutionAuthorityStep>,
184    #[serde(default, skip_serializing_if = "Option::is_none")]
185    pub execution_window: Option<CapitalExecutionWindow>,
186    #[serde(default, skip_serializing_if = "Option::is_none")]
187    pub rail: Option<CapitalExecutionRail>,
188    #[serde(default, skip_serializing_if = "Option::is_none")]
189    pub observed_execution: Option<CapitalExecutionObservation>,
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub reconciled_state: Option<CapitalExecutionReconciledState>,
192    #[serde(default, skip_serializing_if = "Option::is_none")]
193    pub execution_state: Option<CreditReserveControlExecutionState>,
194    #[serde(default, skip_serializing_if = "Option::is_none")]
195    pub appeal_state: Option<CreditReserveControlAppealState>,
196    #[serde(default, skip_serializing_if = "Option::is_none")]
197    pub appeal_window_ends_at: Option<u64>,
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    pub description: Option<String>,
200    pub report: CreditLossLifecycleReport,
201}
202
203pub type SignedCreditLossLifecycle = SignedExportEnvelope<CreditLossLifecycleArtifact>;
204
205#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
206#[serde(rename_all = "camelCase")]
207pub struct CreditLossLifecycleListQuery {
208    #[serde(default, skip_serializing_if = "Option::is_none")]
209    pub event_id: Option<String>,
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub bond_id: Option<String>,
212    #[serde(default, skip_serializing_if = "Option::is_none")]
213    pub facility_id: Option<String>,
214    #[serde(default, skip_serializing_if = "Option::is_none")]
215    pub capability_id: Option<String>,
216    #[serde(default, skip_serializing_if = "Option::is_none")]
217    pub agent_subject: Option<String>,
218    #[serde(default, skip_serializing_if = "Option::is_none")]
219    pub tool_server: Option<String>,
220    #[serde(default, skip_serializing_if = "Option::is_none")]
221    pub tool_name: Option<String>,
222    #[serde(default, skip_serializing_if = "Option::is_none")]
223    pub event_kind: Option<CreditLossLifecycleEventKind>,
224    #[serde(default, skip_serializing_if = "Option::is_none")]
225    pub limit: Option<usize>,
226}
227
228impl Default for CreditLossLifecycleListQuery {
229    fn default() -> Self {
230        Self {
231            event_id: None,
232            bond_id: None,
233            facility_id: None,
234            capability_id: None,
235            agent_subject: None,
236            tool_server: None,
237            tool_name: None,
238            event_kind: None,
239            limit: Some(50),
240        }
241    }
242}
243
244impl CreditLossLifecycleListQuery {
245    #[must_use]
246    pub fn limit_or_default(&self) -> usize {
247        bounded_limit_or_default(self.limit, 50, MAX_CREDIT_LOSS_LIFECYCLE_LIST_LIMIT)
248    }
249
250    #[must_use]
251    pub fn normalized(&self) -> Self {
252        let mut normalized = self.clone();
253        normalized.limit = Some(self.limit_or_default());
254        normalized
255    }
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
259#[serde(rename_all = "camelCase")]
260pub struct CreditLossLifecycleRow {
261    pub event: SignedCreditLossLifecycle,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
265#[serde(rename_all = "camelCase")]
266pub struct CreditLossLifecycleListSummary {
267    pub matching_events: u64,
268    pub returned_events: u64,
269    pub delinquency_events: u64,
270    pub recovery_events: u64,
271    pub reserve_release_events: u64,
272    pub reserve_slash_events: u64,
273    pub write_off_events: u64,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
277#[serde(rename_all = "camelCase")]
278pub struct CreditLossLifecycleListReport {
279    pub schema: String,
280    pub generated_at: u64,
281    pub query: CreditLossLifecycleListQuery,
282    pub summary: CreditLossLifecycleListSummary,
283    pub events: Vec<CreditLossLifecycleRow>,
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
287#[serde(rename_all = "camelCase")]
288pub struct CreditBacktestQuery {
289    #[serde(default, skip_serializing_if = "Option::is_none")]
290    pub capability_id: Option<String>,
291    #[serde(default, skip_serializing_if = "Option::is_none")]
292    pub agent_subject: Option<String>,
293    #[serde(default, skip_serializing_if = "Option::is_none")]
294    pub tool_server: Option<String>,
295    #[serde(default, skip_serializing_if = "Option::is_none")]
296    pub tool_name: Option<String>,
297    #[serde(default, skip_serializing_if = "Option::is_none")]
298    pub since: Option<u64>,
299    #[serde(default, skip_serializing_if = "Option::is_none")]
300    pub until: Option<u64>,
301    #[serde(default, skip_serializing_if = "Option::is_none")]
302    pub receipt_limit: Option<usize>,
303    #[serde(default, skip_serializing_if = "Option::is_none")]
304    pub decision_limit: Option<usize>,
305    #[serde(default, skip_serializing_if = "Option::is_none")]
306    pub window_seconds: Option<u64>,
307    #[serde(default, skip_serializing_if = "Option::is_none")]
308    pub window_count: Option<usize>,
309    #[serde(default, skip_serializing_if = "Option::is_none")]
310    pub stale_after_seconds: Option<u64>,
311}
312
313impl Default for CreditBacktestQuery {
314    fn default() -> Self {
315        Self {
316            capability_id: None,
317            agent_subject: None,
318            tool_server: None,
319            tool_name: None,
320            since: None,
321            until: None,
322            receipt_limit: Some(100),
323            decision_limit: Some(50),
324            window_seconds: Some(7 * 86_400),
325            window_count: Some(4),
326            stale_after_seconds: Some(30 * 86_400),
327        }
328    }
329}
330
331impl CreditBacktestQuery {
332    #[must_use]
333    pub fn receipt_limit_or_default(&self) -> usize {
334        bounded_limit_or_default(self.receipt_limit, 100, MAX_EXPOSURE_LEDGER_RECEIPT_LIMIT)
335    }
336
337    #[must_use]
338    pub fn decision_limit_or_default(&self) -> usize {
339        bounded_limit_or_default(self.decision_limit, 50, MAX_EXPOSURE_LEDGER_DECISION_LIMIT)
340    }
341
342    #[must_use]
343    pub fn window_seconds_or_default(&self) -> u64 {
344        self.window_seconds.unwrap_or(7 * 86_400).max(1)
345    }
346
347    #[must_use]
348    pub fn window_count_or_default(&self) -> usize {
349        bounded_limit_or_default(self.window_count, 4, MAX_CREDIT_BACKTEST_WINDOW_LIMIT)
350    }
351
352    #[must_use]
353    pub fn stale_after_seconds_or_default(&self) -> u64 {
354        self.stale_after_seconds.unwrap_or(30 * 86_400).max(1)
355    }
356
357    #[must_use]
358    pub fn normalized(&self) -> Self {
359        let mut normalized = self.clone();
360        normalized.receipt_limit = Some(self.receipt_limit_or_default());
361        normalized.decision_limit = Some(self.decision_limit_or_default());
362        normalized.window_seconds = Some(self.window_seconds_or_default());
363        normalized.window_count = Some(self.window_count_or_default());
364        normalized.stale_after_seconds = Some(self.stale_after_seconds_or_default());
365        normalized
366    }
367
368    #[must_use]
369    pub fn exposure_query(&self) -> ExposureLedgerQuery {
370        ExposureLedgerQuery {
371            capability_id: self.capability_id.clone(),
372            agent_subject: self.agent_subject.clone(),
373            tool_server: self.tool_server.clone(),
374            tool_name: self.tool_name.clone(),
375            since: self.since,
376            until: self.until,
377            receipt_limit: self.receipt_limit,
378            decision_limit: self.decision_limit,
379        }
380    }
381
382    pub fn validate(&self) -> Result<(), String> {
383        self.exposure_query().validate()?;
384        if self.agent_subject.is_none() {
385            return Err(
386                "credit backtests require --agent-subject because scorecards and facilities are subject-scoped"
387                    .to_string(),
388            );
389        }
390        Ok(())
391    }
392}
393
394#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
395#[serde(rename_all = "snake_case")]
396pub enum CreditBacktestReasonCode {
397    ScoreBandShift,
398    FacilityDispositionShift,
399    MixedCurrencyBook,
400    StaleEvidence,
401    FacilityOverUtilization,
402    PendingSettlementBacklog,
403    FailedSettlementBacklog,
404    MissingRuntimeAssurance,
405    CertificationNotActive,
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
409#[serde(rename_all = "camelCase")]
410pub struct CreditBacktestWindow {
411    pub index: u64,
412    pub window_started_at: u64,
413    pub window_ended_at: u64,
414    #[serde(default, skip_serializing_if = "Option::is_none")]
415    pub newest_receipt_at: Option<u64>,
416    #[serde(default, skip_serializing_if = "Option::is_none")]
417    pub expected_band: Option<CreditScorecardBand>,
418    #[serde(default, skip_serializing_if = "Option::is_none")]
419    pub expected_disposition: Option<CreditFacilityDisposition>,
420    pub simulated_scorecard: CreditScorecardSummary,
421    pub simulated_disposition: CreditFacilityDisposition,
422    #[serde(default, skip_serializing_if = "Option::is_none")]
423    pub simulated_terms: Option<CreditFacilityTerms>,
424    pub stale_evidence: bool,
425    #[serde(default, skip_serializing_if = "Option::is_none")]
426    pub utilization_bps: Option<u32>,
427    #[serde(default, skip_serializing_if = "Vec::is_empty")]
428    pub reason_codes: Vec<CreditBacktestReasonCode>,
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
432#[serde(rename_all = "camelCase")]
433pub struct CreditBacktestSummary {
434    pub windows_evaluated: u64,
435    pub drift_windows: u64,
436    pub score_band_changes: u64,
437    pub facility_disposition_changes: u64,
438    pub manual_review_windows: u64,
439    pub denied_windows: u64,
440    pub stale_evidence_windows: u64,
441    pub mixed_currency_windows: u64,
442    pub over_utilized_windows: u64,
443}
444
445#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
446#[serde(rename_all = "camelCase")]
447pub struct CreditBacktestReport {
448    pub schema: String,
449    pub generated_at: u64,
450    pub query: CreditBacktestQuery,
451    pub summary: CreditBacktestSummary,
452    pub windows: Vec<CreditBacktestWindow>,
453}
454
455#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
456#[serde(rename_all = "camelCase")]
457pub struct CreditProviderRiskPackageQuery {
458    #[serde(default, skip_serializing_if = "Option::is_none")]
459    pub capability_id: Option<String>,
460    #[serde(default, skip_serializing_if = "Option::is_none")]
461    pub agent_subject: Option<String>,
462    #[serde(default, skip_serializing_if = "Option::is_none")]
463    pub tool_server: Option<String>,
464    #[serde(default, skip_serializing_if = "Option::is_none")]
465    pub tool_name: Option<String>,
466    #[serde(default, skip_serializing_if = "Option::is_none")]
467    pub since: Option<u64>,
468    #[serde(default, skip_serializing_if = "Option::is_none")]
469    pub until: Option<u64>,
470    #[serde(default, skip_serializing_if = "Option::is_none")]
471    pub receipt_limit: Option<usize>,
472    #[serde(default, skip_serializing_if = "Option::is_none")]
473    pub decision_limit: Option<usize>,
474    #[serde(default, skip_serializing_if = "Option::is_none")]
475    pub recent_loss_limit: Option<usize>,
476}
477
478impl Default for CreditProviderRiskPackageQuery {
479    fn default() -> Self {
480        Self {
481            capability_id: None,
482            agent_subject: None,
483            tool_server: None,
484            tool_name: None,
485            since: None,
486            until: None,
487            receipt_limit: Some(100),
488            decision_limit: Some(50),
489            recent_loss_limit: Some(10),
490        }
491    }
492}
493
494impl CreditProviderRiskPackageQuery {
495    #[must_use]
496    pub fn recent_loss_limit_or_default(&self) -> usize {
497        bounded_limit_or_default(self.recent_loss_limit, 10, MAX_CREDIT_PROVIDER_LOSS_LIMIT)
498    }
499
500    #[must_use]
501    pub fn normalized(&self) -> Self {
502        let mut normalized = self.clone();
503        normalized.receipt_limit = Some(bounded_limit_or_default(
504            self.receipt_limit,
505            100,
506            MAX_EXPOSURE_LEDGER_RECEIPT_LIMIT,
507        ));
508        normalized.decision_limit = Some(bounded_limit_or_default(
509            self.decision_limit,
510            50,
511            MAX_EXPOSURE_LEDGER_DECISION_LIMIT,
512        ));
513        normalized.recent_loss_limit = Some(self.recent_loss_limit_or_default());
514        normalized
515    }
516
517    #[must_use]
518    pub fn exposure_query(&self) -> ExposureLedgerQuery {
519        ExposureLedgerQuery {
520            capability_id: self.capability_id.clone(),
521            agent_subject: self.agent_subject.clone(),
522            tool_server: self.tool_server.clone(),
523            tool_name: self.tool_name.clone(),
524            since: self.since,
525            until: self.until,
526            receipt_limit: self.receipt_limit,
527            decision_limit: self.decision_limit,
528        }
529    }
530
531    pub fn validate(&self) -> Result<(), String> {
532        self.exposure_query().validate()?;
533        if self.agent_subject.is_none() {
534            return Err(
535                "provider risk packages require --agent-subject because the package is subject-scoped"
536                    .to_string(),
537            );
538        }
539        Ok(())
540    }
541}
542
543#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
544#[serde(rename_all = "camelCase")]
545pub struct CreditRecentLossEntry {
546    pub receipt_id: String,
547    pub observed_at: u64,
548    pub settlement_status: SettlementStatus,
549    #[serde(default, skip_serializing_if = "Option::is_none")]
550    pub financial_amount: Option<MonetaryAmount>,
551    #[serde(default, skip_serializing_if = "Option::is_none")]
552    pub provisional_loss_amount: Option<MonetaryAmount>,
553    #[serde(default, skip_serializing_if = "Option::is_none")]
554    pub recovered_amount: Option<MonetaryAmount>,
555    #[serde(default, skip_serializing_if = "Vec::is_empty")]
556    pub evidence_refs: Vec<ExposureLedgerEvidenceReference>,
557}
558
559#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
560#[serde(rename_all = "camelCase")]
561pub struct CreditRecentLossSummary {
562    pub matching_loss_events: u64,
563    pub returned_loss_events: u64,
564    pub failed_settlement_events: u64,
565    pub provisional_loss_events: u64,
566    pub recovered_events: u64,
567}
568
569#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
570#[serde(rename_all = "camelCase")]
571pub struct CreditRecentLossHistory {
572    pub summary: CreditRecentLossSummary,
573    pub entries: Vec<CreditRecentLossEntry>,
574}
575
576#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
577#[serde(rename_all = "camelCase")]
578pub struct CreditRuntimeAssuranceState {
579    pub governed_receipts: u64,
580    pub runtime_assurance_receipts: u64,
581    #[serde(default, skip_serializing_if = "Option::is_none")]
582    pub highest_tier: Option<RuntimeAssuranceTier>,
583    #[serde(default, skip_serializing_if = "Option::is_none")]
584    pub latest_schema: Option<String>,
585    #[serde(default, skip_serializing_if = "Option::is_none")]
586    pub latest_verifier_family: Option<AttestationVerifierFamily>,
587    #[serde(default, skip_serializing_if = "Option::is_none")]
588    pub latest_verifier: Option<String>,
589    #[serde(default, skip_serializing_if = "Option::is_none")]
590    pub latest_evidence_sha256: Option<String>,
591    #[serde(default, skip_serializing_if = "Vec::is_empty")]
592    pub observed_verifier_families: Vec<AttestationVerifierFamily>,
593    pub stale: bool,
594}
595
596#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
597#[serde(rename_all = "camelCase")]
598pub struct CreditCertificationState {
599    pub required: bool,
600    #[serde(default, skip_serializing_if = "Option::is_none")]
601    pub state: Option<UnderwritingCertificationState>,
602    #[serde(default, skip_serializing_if = "Option::is_none")]
603    pub artifact_id: Option<String>,
604    #[serde(default, skip_serializing_if = "Option::is_none")]
605    pub checked_at: Option<u64>,
606    #[serde(default, skip_serializing_if = "Option::is_none")]
607    pub published_at: Option<u64>,
608}
609
610#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
611#[serde(rename_all = "camelCase")]
612pub struct CreditProviderRiskPackageSupportBoundary {
613    pub signed_exposure_authoritative: bool,
614    pub signed_scorecard_authoritative: bool,
615    pub facility_policy_authoritative: bool,
616    pub compliance_score_reference_supported: bool,
617    pub external_capital_review_supported: bool,
618    pub autonomous_pricing_supported: bool,
619    pub liability_market_supported: bool,
620}
621
622impl Default for CreditProviderRiskPackageSupportBoundary {
623    fn default() -> Self {
624        Self {
625            signed_exposure_authoritative: true,
626            signed_scorecard_authoritative: true,
627            facility_policy_authoritative: true,
628            compliance_score_reference_supported: true,
629            external_capital_review_supported: true,
630            autonomous_pricing_supported: false,
631            liability_market_supported: false,
632        }
633    }
634}
635
636#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
637#[serde(rename_all = "camelCase")]
638pub struct CreditProviderFacilitySnapshot {
639    pub facility_id: String,
640    pub issued_at: u64,
641    pub expires_at: u64,
642    pub disposition: CreditFacilityDisposition,
643    pub lifecycle_state: CreditFacilityLifecycleState,
644    #[serde(default, skip_serializing_if = "Option::is_none")]
645    pub credit_limit: Option<MonetaryAmount>,
646    #[serde(default, skip_serializing_if = "Option::is_none")]
647    pub supersedes_facility_id: Option<String>,
648    pub signer_key: String,
649}
650
651#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
652#[serde(rename_all = "camelCase")]
653pub struct CreditProviderRiskPackage {
654    pub schema: String,
655    pub generated_at: u64,
656    pub subject_key: String,
657    pub filters: CreditProviderRiskPackageQuery,
658    pub support_boundary: CreditProviderRiskPackageSupportBoundary,
659    pub exposure: SignedExposureLedgerReport,
660    pub scorecard: SignedCreditScorecardReport,
661    pub facility_report: CreditFacilityReport,
662    #[serde(default, skip_serializing_if = "Option::is_none")]
663    pub compliance_score: Option<UnderwritingComplianceEvidence>,
664    #[serde(default, skip_serializing_if = "Option::is_none")]
665    pub latest_facility: Option<CreditProviderFacilitySnapshot>,
666    #[serde(default, skip_serializing_if = "Option::is_none")]
667    pub runtime_assurance: Option<CreditRuntimeAssuranceState>,
668    pub certification: CreditCertificationState,
669    pub recent_loss_history: CreditRecentLossHistory,
670    #[serde(default, skip_serializing_if = "Vec::is_empty")]
671    pub evidence_refs: Vec<CreditScorecardEvidenceReference>,
672}
673
674pub type SignedCreditProviderRiskPackage = SignedExportEnvelope<CreditProviderRiskPackage>;