Skip to main content

chio_market/
lib.rs

1pub use chio_appraisal as appraisal;
2pub use chio_core_types::{capability, crypto, receipt};
3pub use chio_credit as credit;
4pub use chio_underwriting as underwriting;
5
6pub mod insurance_flow;
7pub use insurance_flow::{
8    quote_and_bind, BoundPolicy, ClaimDecision, ClaimDenialReason, ClaimEvidence, ClaimSettlement,
9    ClaimSettlementRequest, ClaimSettlementSink, CoverageLimit, InsuranceFlowError, PolicyStatus,
10    PremiumSource, ReceiptEvidenceSource, ReceiptFingerprint, ResolvedReceiptEvidence,
11    StaticPremiumSource,
12};
13
14use std::collections::BTreeSet;
15
16use serde::{Deserialize, Serialize};
17
18use crate::capability::MonetaryAmount;
19use crate::credit::{
20    CapitalBookSourceKind, CapitalExecutionAuthorityStep, CapitalExecutionInstructionAction,
21    CapitalExecutionObservation, CapitalExecutionRail, CapitalExecutionReconciledState,
22    CapitalExecutionRole, CapitalExecutionWindow, CreditFacilityDisposition,
23    CreditFacilityLifecycleState, SignedCapitalBookReport, SignedCapitalExecutionInstruction,
24    SignedCreditBond, SignedCreditFacility, SignedCreditLossLifecycle,
25    SignedCreditProviderRiskPackage, SignedExposureLedgerReport,
26};
27use crate::receipt::SignedExportEnvelope;
28use crate::underwriting::{
29    SignedUnderwritingDecision, UnderwritingBudgetAction, UnderwritingDecisionLifecycleState,
30    UnderwritingReviewState,
31};
32
33pub const LIABILITY_PROVIDER_ARTIFACT_SCHEMA: &str = "chio.market.provider.v1";
34pub const LIABILITY_PROVIDER_LIST_REPORT_SCHEMA: &str = "chio.market.provider-list.v1";
35pub const LIABILITY_PROVIDER_RESOLUTION_REPORT_SCHEMA: &str = "chio.market.provider-resolution.v1";
36pub const LIABILITY_QUOTE_REQUEST_ARTIFACT_SCHEMA: &str = "chio.market.quote-request.v1";
37pub const LIABILITY_QUOTE_RESPONSE_ARTIFACT_SCHEMA: &str = "chio.market.quote-response.v1";
38pub const LIABILITY_PRICING_AUTHORITY_ARTIFACT_SCHEMA: &str = "chio.market.pricing-authority.v1";
39pub const LIABILITY_PLACEMENT_ARTIFACT_SCHEMA: &str = "chio.market.placement.v1";
40pub const LIABILITY_BOUND_COVERAGE_ARTIFACT_SCHEMA: &str = "chio.market.bound-coverage.v1";
41pub const LIABILITY_AUTO_BIND_DECISION_ARTIFACT_SCHEMA: &str = "chio.market.auto-bind.v1";
42pub const LIABILITY_MARKET_WORKFLOW_REPORT_SCHEMA: &str = "chio.market.workflow-list.v1";
43pub const LIABILITY_CLAIM_PACKAGE_ARTIFACT_SCHEMA: &str = "chio.market.claim-package.v1";
44pub const LIABILITY_CLAIM_RESPONSE_ARTIFACT_SCHEMA: &str = "chio.market.claim-response.v1";
45pub const LIABILITY_CLAIM_DISPUTE_ARTIFACT_SCHEMA: &str = "chio.market.claim-dispute.v1";
46pub const LIABILITY_CLAIM_ADJUDICATION_ARTIFACT_SCHEMA: &str = "chio.market.claim-adjudication.v1";
47pub const LIABILITY_CLAIM_PAYOUT_INSTRUCTION_ARTIFACT_SCHEMA: &str =
48    "chio.market.claim-payout-instruction.v1";
49pub const LIABILITY_CLAIM_PAYOUT_RECEIPT_ARTIFACT_SCHEMA: &str =
50    "chio.market.claim-payout-receipt.v1";
51pub const LIABILITY_CLAIM_SETTLEMENT_INSTRUCTION_ARTIFACT_SCHEMA: &str =
52    "chio.market.claim-settlement-instruction.v1";
53pub const LIABILITY_CLAIM_SETTLEMENT_RECEIPT_ARTIFACT_SCHEMA: &str =
54    "chio.market.claim-settlement-receipt.v1";
55pub const LIABILITY_CLAIM_WORKFLOW_REPORT_SCHEMA: &str = "chio.market.claim-workflow-list.v1";
56pub const MAX_LIABILITY_PROVIDER_LIST_LIMIT: usize = 100;
57pub const MAX_LIABILITY_MARKET_WORKFLOW_LIMIT: usize = 100;
58pub const MAX_LIABILITY_CLAIM_WORKFLOW_LIMIT: usize = 100;
59
60#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
61#[serde(rename_all = "snake_case")]
62pub enum LiabilityProviderType {
63    AdmittedCarrier,
64    SurplusLine,
65    Captive,
66    RiskPool,
67}
68
69#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
70#[serde(rename_all = "snake_case")]
71pub enum LiabilityCoverageClass {
72    ToolExecution,
73    DataBreach,
74    FinancialLoss,
75    ProfessionalLiability,
76    RegulatoryResponse,
77}
78
79#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
80#[serde(rename_all = "snake_case")]
81pub enum LiabilityEvidenceRequirement {
82    BehavioralFeed,
83    UnderwritingDecision,
84    CreditProviderRiskPackage,
85    RuntimeAttestationAppraisal,
86    CertificationArtifact,
87    CreditBond,
88    AuthorizationReviewPack,
89}
90
91#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
92#[serde(rename_all = "snake_case")]
93pub enum LiabilityProviderLifecycleState {
94    Active,
95    Suspended,
96    Superseded,
97    Retired,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
101#[serde(rename_all = "camelCase")]
102pub struct LiabilityProviderProvenance {
103    pub configured_by: String,
104    pub configured_at: u64,
105    pub source_ref: String,
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub change_reason: Option<String>,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
111#[serde(rename_all = "camelCase")]
112pub struct LiabilityJurisdictionPolicy {
113    pub jurisdiction: String,
114    pub coverage_classes: Vec<LiabilityCoverageClass>,
115    pub supported_currencies: Vec<String>,
116    #[serde(default, skip_serializing_if = "Vec::is_empty")]
117    pub required_evidence: Vec<LiabilityEvidenceRequirement>,
118    #[serde(default, skip_serializing_if = "Option::is_none")]
119    pub max_coverage_amount: Option<MonetaryAmount>,
120    pub claims_supported: bool,
121    pub quote_ttl_seconds: u64,
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    pub notes: Option<String>,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
127#[serde(rename_all = "camelCase")]
128pub struct LiabilityProviderSupportBoundary {
129    pub curated_registry_only: bool,
130    pub automatic_trust_admission: bool,
131    pub permissionless_federation_supported: bool,
132    pub bound_coverage_supported: bool,
133}
134
135impl Default for LiabilityProviderSupportBoundary {
136    fn default() -> Self {
137        Self {
138            curated_registry_only: true,
139            automatic_trust_admission: false,
140            permissionless_federation_supported: false,
141            bound_coverage_supported: false,
142        }
143    }
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
147#[serde(rename_all = "camelCase")]
148pub struct LiabilityProviderReport {
149    pub schema: String,
150    pub provider_id: String,
151    pub display_name: String,
152    pub provider_type: LiabilityProviderType,
153    #[serde(default, skip_serializing_if = "Option::is_none")]
154    pub provider_url: Option<String>,
155    pub lifecycle_state: LiabilityProviderLifecycleState,
156    pub support_boundary: LiabilityProviderSupportBoundary,
157    pub policies: Vec<LiabilityJurisdictionPolicy>,
158    pub provenance: LiabilityProviderProvenance,
159}
160
161impl LiabilityProviderReport {
162    pub fn validate(&self) -> Result<(), String> {
163        if self.provider_id.trim().is_empty() {
164            return Err("provider_id must not be empty".to_string());
165        }
166        if self.display_name.trim().is_empty() {
167            return Err("display_name must not be empty".to_string());
168        }
169        if self.provenance.configured_by.trim().is_empty() {
170            return Err("provenance.configured_by must not be empty".to_string());
171        }
172        if self.provenance.source_ref.trim().is_empty() {
173            return Err("provenance.source_ref must not be empty".to_string());
174        }
175        if let Some(provider_url) = self.provider_url.as_deref() {
176            if !(provider_url.starts_with("http://") || provider_url.starts_with("https://")) {
177                return Err("provider_url must start with http:// or https://".to_string());
178            }
179        }
180        if self.policies.is_empty() {
181            return Err("providers require at least one jurisdiction policy".to_string());
182        }
183
184        let mut seen_jurisdictions = BTreeSet::new();
185        for policy in &self.policies {
186            if policy.jurisdiction.trim().is_empty() {
187                return Err("jurisdiction policies require a non-empty jurisdiction".to_string());
188            }
189            let normalized_jurisdiction = policy.jurisdiction.trim().to_ascii_lowercase();
190            if !seen_jurisdictions.insert(normalized_jurisdiction) {
191                return Err(format!(
192                    "provider `{}` defines duplicate jurisdiction policy `{}`",
193                    self.provider_id, policy.jurisdiction
194                ));
195            }
196            if policy.coverage_classes.is_empty() {
197                return Err(format!(
198                    "jurisdiction policy `{}` requires at least one coverage class",
199                    policy.jurisdiction
200                ));
201            }
202            if policy.supported_currencies.is_empty() {
203                return Err(format!(
204                    "jurisdiction policy `{}` requires at least one supported currency",
205                    policy.jurisdiction
206                ));
207            }
208            if policy.quote_ttl_seconds == 0 {
209                return Err(format!(
210                    "jurisdiction policy `{}` requires quote_ttl_seconds greater than zero",
211                    policy.jurisdiction
212                ));
213            }
214            let mut seen_coverage = BTreeSet::new();
215            for coverage_class in &policy.coverage_classes {
216                if !seen_coverage.insert(*coverage_class) {
217                    return Err(format!(
218                        "jurisdiction policy `{}` defines duplicate coverage class `{:?}`",
219                        policy.jurisdiction, coverage_class
220                    ));
221                }
222            }
223            let mut seen_currencies = BTreeSet::new();
224            for currency in &policy.supported_currencies {
225                let normalized_currency = currency.trim().to_ascii_uppercase();
226                if normalized_currency.len() != 3
227                    || !normalized_currency
228                        .chars()
229                        .all(|character| character.is_ascii_uppercase())
230                {
231                    return Err(format!(
232                        "jurisdiction policy `{}` contains invalid currency `{}`",
233                        policy.jurisdiction, currency
234                    ));
235                }
236                if !seen_currencies.insert(normalized_currency) {
237                    return Err(format!(
238                        "jurisdiction policy `{}` contains duplicate currency `{}`",
239                        policy.jurisdiction, currency
240                    ));
241                }
242            }
243        }
244
245        Ok(())
246    }
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
250#[serde(rename_all = "camelCase")]
251pub struct LiabilityProviderArtifact {
252    pub schema: String,
253    pub provider_record_id: String,
254    pub issued_at: u64,
255    pub lifecycle_state: LiabilityProviderLifecycleState,
256    #[serde(default, skip_serializing_if = "Option::is_none")]
257    pub supersedes_provider_record_id: Option<String>,
258    pub report: LiabilityProviderReport,
259}
260
261pub type SignedLiabilityProvider = SignedExportEnvelope<LiabilityProviderArtifact>;
262
263#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
264#[serde(rename_all = "camelCase")]
265pub struct LiabilityProviderListQuery {
266    #[serde(default, skip_serializing_if = "Option::is_none")]
267    pub provider_id: Option<String>,
268    #[serde(default, skip_serializing_if = "Option::is_none")]
269    pub jurisdiction: Option<String>,
270    #[serde(default, skip_serializing_if = "Option::is_none")]
271    pub coverage_class: Option<LiabilityCoverageClass>,
272    #[serde(default, skip_serializing_if = "Option::is_none")]
273    pub currency: Option<String>,
274    #[serde(default, skip_serializing_if = "Option::is_none")]
275    pub lifecycle_state: Option<LiabilityProviderLifecycleState>,
276    #[serde(default, skip_serializing_if = "Option::is_none")]
277    pub limit: Option<usize>,
278}
279
280impl Default for LiabilityProviderListQuery {
281    fn default() -> Self {
282        Self {
283            provider_id: None,
284            jurisdiction: None,
285            coverage_class: None,
286            currency: None,
287            lifecycle_state: None,
288            limit: Some(50),
289        }
290    }
291}
292
293impl LiabilityProviderListQuery {
294    #[must_use]
295    pub fn limit_or_default(&self) -> usize {
296        self.limit
297            .unwrap_or(50)
298            .clamp(1, MAX_LIABILITY_PROVIDER_LIST_LIMIT)
299    }
300
301    #[must_use]
302    pub fn normalized(&self) -> Self {
303        let mut normalized = self.clone();
304        normalized.limit = Some(self.limit_or_default());
305        normalized.currency = self
306            .currency
307            .as_ref()
308            .map(|currency| currency.trim().to_ascii_uppercase());
309        normalized.jurisdiction = self
310            .jurisdiction
311            .as_ref()
312            .map(|jurisdiction| jurisdiction.trim().to_ascii_lowercase());
313        normalized
314    }
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
318#[serde(rename_all = "camelCase")]
319pub struct LiabilityProviderRow {
320    pub provider: SignedLiabilityProvider,
321    pub lifecycle_state: LiabilityProviderLifecycleState,
322    #[serde(default, skip_serializing_if = "Option::is_none")]
323    pub superseded_by_provider_record_id: Option<String>,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
327#[serde(rename_all = "camelCase")]
328pub struct LiabilityProviderListSummary {
329    pub matching_providers: u64,
330    pub returned_providers: u64,
331    pub active_providers: u64,
332    pub suspended_providers: u64,
333    pub superseded_providers: u64,
334    pub retired_providers: u64,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
338#[serde(rename_all = "camelCase")]
339pub struct LiabilityProviderListReport {
340    pub schema: String,
341    pub generated_at: u64,
342    pub query: LiabilityProviderListQuery,
343    pub summary: LiabilityProviderListSummary,
344    pub providers: Vec<LiabilityProviderRow>,
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
348#[serde(rename_all = "camelCase")]
349pub struct LiabilityProviderResolutionQuery {
350    pub provider_id: String,
351    pub jurisdiction: String,
352    pub coverage_class: LiabilityCoverageClass,
353    pub currency: String,
354}
355
356impl LiabilityProviderResolutionQuery {
357    pub fn validate(&self) -> Result<(), String> {
358        if self.provider_id.trim().is_empty() {
359            return Err("provider_id must not be empty".to_string());
360        }
361        if self.jurisdiction.trim().is_empty() {
362            return Err("jurisdiction must not be empty".to_string());
363        }
364        let currency = self.currency.trim().to_ascii_uppercase();
365        if currency.len() != 3
366            || !currency
367                .chars()
368                .all(|character| character.is_ascii_uppercase())
369        {
370            return Err("currency must be a three-letter uppercase ISO-style code".to_string());
371        }
372        Ok(())
373    }
374
375    #[must_use]
376    pub fn normalized(&self) -> Self {
377        Self {
378            provider_id: self.provider_id.trim().to_string(),
379            jurisdiction: self.jurisdiction.trim().to_ascii_lowercase(),
380            coverage_class: self.coverage_class,
381            currency: self.currency.trim().to_ascii_uppercase(),
382        }
383    }
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
387#[serde(rename_all = "camelCase")]
388pub struct LiabilityProviderResolutionReport {
389    pub schema: String,
390    pub generated_at: u64,
391    pub query: LiabilityProviderResolutionQuery,
392    pub provider: SignedLiabilityProvider,
393    pub matched_policy: LiabilityJurisdictionPolicy,
394    pub support_boundary: LiabilityProviderSupportBoundary,
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
398#[serde(rename_all = "snake_case")]
399pub enum LiabilityQuoteDisposition {
400    Quoted,
401    Declined,
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
405#[serde(rename_all = "camelCase")]
406pub struct LiabilityProviderPolicyReference {
407    pub provider_id: String,
408    pub provider_record_id: String,
409    pub display_name: String,
410    pub jurisdiction: String,
411    pub coverage_class: LiabilityCoverageClass,
412    pub currency: String,
413    pub required_evidence: Vec<LiabilityEvidenceRequirement>,
414    #[serde(default, skip_serializing_if = "Option::is_none")]
415    pub max_coverage_amount: Option<MonetaryAmount>,
416    pub claims_supported: bool,
417    pub quote_ttl_seconds: u64,
418    pub bound_coverage_supported: bool,
419}
420
421impl LiabilityProviderPolicyReference {
422    pub fn validate(&self) -> Result<(), String> {
423        if self.provider_id.trim().is_empty() {
424            return Err("provider policy reference requires provider_id".to_string());
425        }
426        if self.provider_record_id.trim().is_empty() {
427            return Err("provider policy reference requires provider_record_id".to_string());
428        }
429        if self.display_name.trim().is_empty() {
430            return Err("provider policy reference requires display_name".to_string());
431        }
432        if self.jurisdiction.trim().is_empty() {
433            return Err("provider policy reference requires jurisdiction".to_string());
434        }
435        validate_currency_code(&self.currency, "provider policy reference currency")?;
436        if self.quote_ttl_seconds == 0 {
437            return Err(
438                "provider policy reference requires quote_ttl_seconds greater than zero"
439                    .to_string(),
440            );
441        }
442        if let Some(max_coverage_amount) = self.max_coverage_amount.as_ref() {
443            if max_coverage_amount.units == 0 {
444                return Err(
445                    "provider policy reference max_coverage_amount must be greater than zero"
446                        .to_string(),
447                );
448            }
449            if max_coverage_amount.currency.trim().to_ascii_uppercase() != self.currency {
450                return Err("provider policy reference max_coverage_amount currency must match policy currency".to_string());
451            }
452        }
453        Ok(())
454    }
455}
456
457#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
458#[serde(rename_all = "snake_case")]
459pub enum LiabilityPricingAuthorityEnvelopeKind {
460    ProviderDelegate,
461    RegulatedRole,
462}
463
464#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
465#[serde(rename_all = "camelCase")]
466pub struct LiabilityPricingAuthorityEnvelope {
467    pub kind: LiabilityPricingAuthorityEnvelopeKind,
468    pub delegate_id: String,
469    #[serde(default, skip_serializing_if = "Option::is_none")]
470    pub regulated_role: Option<String>,
471    #[serde(default, skip_serializing_if = "Option::is_none")]
472    pub authority_chain_ref: Option<String>,
473}
474
475impl LiabilityPricingAuthorityEnvelope {
476    pub fn validate(&self) -> Result<(), String> {
477        if self.delegate_id.trim().is_empty() {
478            return Err("pricing authority envelope requires delegate_id".to_string());
479        }
480        if matches!(
481            self.kind,
482            LiabilityPricingAuthorityEnvelopeKind::RegulatedRole
483        ) && self
484            .regulated_role
485            .as_deref()
486            .is_none_or(|value| value.trim().is_empty())
487        {
488            return Err(
489                "regulated-role pricing authority envelopes require regulated_role".to_string(),
490            );
491        }
492        Ok(())
493    }
494}
495
496#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
497#[serde(rename_all = "camelCase")]
498pub struct LiabilityQuoteRequestArtifact {
499    pub schema: String,
500    pub quote_request_id: String,
501    pub issued_at: u64,
502    pub provider_policy: LiabilityProviderPolicyReference,
503    pub requested_coverage_amount: MonetaryAmount,
504    pub requested_effective_from: u64,
505    pub requested_effective_until: u64,
506    pub risk_package: SignedCreditProviderRiskPackage,
507    #[serde(default, skip_serializing_if = "Option::is_none")]
508    pub notes: Option<String>,
509}
510
511impl LiabilityQuoteRequestArtifact {
512    pub fn validate(&self) -> Result<(), String> {
513        self.provider_policy.validate()?;
514        validate_positive_money(
515            &self.requested_coverage_amount,
516            "quote request requested_coverage_amount",
517        )?;
518        if self
519            .requested_coverage_amount
520            .currency
521            .trim()
522            .to_ascii_uppercase()
523            != self.provider_policy.currency
524        {
525            return Err(
526                "quote request requested_coverage_amount currency must match provider policy currency"
527                    .to_string(),
528            );
529        }
530        if self.requested_effective_until <= self.requested_effective_from {
531            return Err("quote request effective window must have end after start".to_string());
532        }
533        if !self.risk_package.verify_signature().map_err(|error| {
534            format!("quote request risk package signature verification failed: {error}")
535        })? {
536            return Err("quote request risk package signature verification failed".to_string());
537        }
538        if self.risk_package.body.subject_key.trim().is_empty() {
539            return Err("quote request risk package subject_key must not be empty".to_string());
540        }
541        if let Some(max_coverage_amount) = self.provider_policy.max_coverage_amount.as_ref() {
542            if self.requested_coverage_amount.units > max_coverage_amount.units {
543                return Err(
544                    "quote request requested_coverage_amount exceeds provider max_coverage_amount"
545                        .to_string(),
546                );
547            }
548        }
549        Ok(())
550    }
551}
552
553pub type SignedLiabilityQuoteRequest = SignedExportEnvelope<LiabilityQuoteRequestArtifact>;
554
555#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
556#[serde(rename_all = "camelCase")]
557pub struct LiabilityQuoteTerms {
558    pub quoted_coverage_amount: MonetaryAmount,
559    pub quoted_premium_amount: MonetaryAmount,
560    #[serde(default, skip_serializing_if = "Option::is_none")]
561    pub quoted_deductible_amount: Option<MonetaryAmount>,
562    pub expires_at: u64,
563}
564
565impl LiabilityQuoteTerms {
566    fn validate_for_request(
567        &self,
568        request: &LiabilityQuoteRequestArtifact,
569        issued_at: u64,
570    ) -> Result<(), String> {
571        validate_positive_money(
572            &self.quoted_coverage_amount,
573            "quote response quoted_coverage_amount",
574        )?;
575        validate_positive_money(
576            &self.quoted_premium_amount,
577            "quote response quoted_premium_amount",
578        )?;
579        if let Some(quoted_deductible_amount) = self.quoted_deductible_amount.as_ref() {
580            validate_positive_money(
581                quoted_deductible_amount,
582                "quote response quoted_deductible_amount",
583            )?;
584            if quoted_deductible_amount
585                .currency
586                .trim()
587                .to_ascii_uppercase()
588                != request.provider_policy.currency
589            {
590                return Err(
591                    "quote response quoted_deductible_amount currency must match provider policy currency"
592                        .to_string(),
593                );
594            }
595        }
596        if self
597            .quoted_coverage_amount
598            .currency
599            .trim()
600            .to_ascii_uppercase()
601            != request.provider_policy.currency
602        {
603            return Err(
604                "quote response quoted_coverage_amount currency must match provider policy currency"
605                    .to_string(),
606            );
607        }
608        if self
609            .quoted_premium_amount
610            .currency
611            .trim()
612            .to_ascii_uppercase()
613            != request.provider_policy.currency
614        {
615            return Err(
616                "quote response quoted_premium_amount currency must match provider policy currency"
617                    .to_string(),
618            );
619        }
620        if self.expires_at <= issued_at {
621            return Err("quote response expires_at must be after issuance".to_string());
622        }
623        if self.expires_at
624            > request
625                .issued_at
626                .saturating_add(request.provider_policy.quote_ttl_seconds)
627        {
628            return Err("quote response expires_at exceeds provider policy quote TTL".to_string());
629        }
630        Ok(())
631    }
632}
633
634#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
635#[serde(rename_all = "camelCase")]
636pub struct LiabilityQuoteResponseArtifact {
637    pub schema: String,
638    pub quote_response_id: String,
639    pub issued_at: u64,
640    pub quote_request: SignedLiabilityQuoteRequest,
641    pub provider_quote_ref: String,
642    pub disposition: LiabilityQuoteDisposition,
643    #[serde(default, skip_serializing_if = "Option::is_none")]
644    pub supersedes_quote_response_id: Option<String>,
645    #[serde(default, skip_serializing_if = "Option::is_none")]
646    pub quoted_terms: Option<LiabilityQuoteTerms>,
647    #[serde(default, skip_serializing_if = "Option::is_none")]
648    pub decline_reason: Option<String>,
649}
650
651impl LiabilityQuoteResponseArtifact {
652    pub fn validate(&self) -> Result<(), String> {
653        if !self.quote_request.verify_signature().map_err(|error| {
654            format!("quote response quote_request signature verification failed: {error}")
655        })? {
656            return Err("quote response quote_request signature verification failed".to_string());
657        }
658        self.quote_request.body.validate()?;
659        if self.provider_quote_ref.trim().is_empty() {
660            return Err("quote response requires provider_quote_ref".to_string());
661        }
662        match self.disposition {
663            LiabilityQuoteDisposition::Quoted => {
664                let quoted_terms = self
665                    .quoted_terms
666                    .as_ref()
667                    .ok_or_else(|| "quoted quote responses require quoted_terms".to_string())?;
668                quoted_terms.validate_for_request(&self.quote_request.body, self.issued_at)?;
669                if self.decline_reason.is_some() {
670                    return Err("quoted quote responses cannot include decline_reason".to_string());
671                }
672            }
673            LiabilityQuoteDisposition::Declined => {
674                if self.quoted_terms.is_some() {
675                    return Err("declined quote responses cannot include quoted_terms".to_string());
676                }
677                if self
678                    .decline_reason
679                    .as_deref()
680                    .is_none_or(|value| value.trim().is_empty())
681                {
682                    return Err("declined quote responses require decline_reason".to_string());
683                }
684            }
685        }
686        Ok(())
687    }
688}
689
690pub type SignedLiabilityQuoteResponse = SignedExportEnvelope<LiabilityQuoteResponseArtifact>;
691
692#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
693#[serde(rename_all = "camelCase")]
694pub struct LiabilityPricingAuthorityArtifact {
695    pub schema: String,
696    pub authority_id: String,
697    pub issued_at: u64,
698    pub quote_request: SignedLiabilityQuoteRequest,
699    pub provider_policy: LiabilityProviderPolicyReference,
700    pub facility: SignedCreditFacility,
701    pub underwriting_decision: SignedUnderwritingDecision,
702    pub capital_book: SignedCapitalBookReport,
703    pub envelope: LiabilityPricingAuthorityEnvelope,
704    pub max_coverage_amount: MonetaryAmount,
705    pub max_premium_amount: MonetaryAmount,
706    pub expires_at: u64,
707    pub auto_bind_enabled: bool,
708    #[serde(default, skip_serializing_if = "Option::is_none")]
709    pub notes: Option<String>,
710}
711
712impl LiabilityPricingAuthorityArtifact {
713    pub fn validate(&self) -> Result<(), String> {
714        if !self.quote_request.verify_signature().map_err(|error| {
715            format!("pricing authority quote_request signature verification failed: {error}")
716        })? {
717            return Err(
718                "pricing authority quote_request signature verification failed".to_string(),
719            );
720        }
721        if !self.facility.verify_signature().map_err(|error| {
722            format!("pricing authority facility signature verification failed: {error}")
723        })? {
724            return Err("pricing authority facility signature verification failed".to_string());
725        }
726        if !self
727            .underwriting_decision
728            .verify_signature()
729            .map_err(|error| {
730                format!(
731                    "pricing authority underwriting decision signature verification failed: {error}"
732                )
733            })?
734        {
735            return Err(
736                "pricing authority underwriting decision signature verification failed".to_string(),
737            );
738        }
739        if !self.capital_book.verify_signature().map_err(|error| {
740            format!("pricing authority capital book signature verification failed: {error}")
741        })? {
742            return Err("pricing authority capital book signature verification failed".to_string());
743        }
744        self.quote_request.body.validate()?;
745        self.provider_policy.validate()?;
746        self.envelope.validate()?;
747        if self.provider_policy != self.quote_request.body.provider_policy {
748            return Err(
749                "pricing authority provider_policy must match the quote request provider_policy"
750                    .to_string(),
751            );
752        }
753        validate_positive_money(
754            &self.max_coverage_amount,
755            "pricing authority max_coverage_amount",
756        )?;
757        validate_positive_money(
758            &self.max_premium_amount,
759            "pricing authority max_premium_amount",
760        )?;
761        if self
762            .max_coverage_amount
763            .currency
764            .trim()
765            .to_ascii_uppercase()
766            != self.provider_policy.currency
767        {
768            return Err(
769                "pricing authority max_coverage_amount currency must match provider policy currency"
770                    .to_string(),
771            );
772        }
773        if self.max_premium_amount.currency.trim().to_ascii_uppercase()
774            != self.provider_policy.currency
775        {
776            return Err(
777                "pricing authority max_premium_amount currency must match provider policy currency"
778                    .to_string(),
779            );
780        }
781        if self.expires_at <= self.issued_at {
782            return Err("pricing authority expires_at must be after issuance".to_string());
783        }
784        if self.expires_at
785            > self
786                .quote_request
787                .body
788                .issued_at
789                .saturating_add(self.provider_policy.quote_ttl_seconds)
790        {
791            return Err(
792                "pricing authority expires_at exceeds provider policy quote TTL".to_string(),
793            );
794        }
795        if self.facility.body.lifecycle_state != CreditFacilityLifecycleState::Active {
796            return Err("pricing authority requires an active facility".to_string());
797        }
798        if self.facility.body.report.disposition != CreditFacilityDisposition::Grant {
799            return Err("pricing authority requires a granted facility".to_string());
800        }
801        let facility_terms = self
802            .facility
803            .body
804            .report
805            .terms
806            .as_ref()
807            .ok_or_else(|| "pricing authority requires facility terms".to_string())?;
808        if facility_terms
809            .credit_limit
810            .currency
811            .trim()
812            .to_ascii_uppercase()
813            != self.provider_policy.currency
814        {
815            return Err(
816                "pricing authority facility credit limit currency must match provider policy currency"
817                    .to_string(),
818            );
819        }
820        if self.max_coverage_amount.units > facility_terms.credit_limit.units {
821            return Err(
822                "pricing authority max_coverage_amount exceeds facility credit limit".to_string(),
823            );
824        }
825        if let Some(max_coverage_amount) = self.provider_policy.max_coverage_amount.as_ref() {
826            if self.max_coverage_amount.units > max_coverage_amount.units {
827                return Err(
828                    "pricing authority max_coverage_amount exceeds provider max_coverage_amount"
829                        .to_string(),
830                );
831            }
832        }
833        if self.underwriting_decision.body.lifecycle_state
834            != UnderwritingDecisionLifecycleState::Active
835        {
836            return Err("pricing authority requires an active underwriting decision".to_string());
837        }
838        if self.underwriting_decision.body.review_state != UnderwritingReviewState::Approved {
839            return Err("pricing authority requires an approved underwriting decision".to_string());
840        }
841        if matches!(
842            self.underwriting_decision.body.budget.action,
843            UnderwritingBudgetAction::Hold | UnderwritingBudgetAction::Deny
844        ) {
845            return Err(
846                "pricing authority requires underwriting budget action preserve or reduce"
847                    .to_string(),
848            );
849        }
850        if let Some(quoted_amount) = self
851            .underwriting_decision
852            .body
853            .premium
854            .quoted_amount
855            .as_ref()
856        {
857            if quoted_amount.currency.trim().to_ascii_uppercase() != self.provider_policy.currency {
858                return Err(
859                    "pricing authority underwriting premium currency must match provider policy currency"
860                        .to_string(),
861                );
862            }
863            if self.max_premium_amount.units > quoted_amount.units {
864                return Err(
865                    "pricing authority max_premium_amount exceeds underwriting quoted premium"
866                        .to_string(),
867                );
868            }
869        }
870        let subject_key = self
871            .quote_request
872            .body
873            .risk_package
874            .body
875            .subject_key
876            .as_str();
877        if self.capital_book.body.subject_key != subject_key {
878            return Err(
879                "pricing authority capital book subject must match the quote request subject"
880                    .to_string(),
881            );
882        }
883        if self.capital_book.body.summary.mixed_currency_book {
884            return Err(
885                "pricing authority cannot be issued against a mixed-currency capital book"
886                    .to_string(),
887            );
888        }
889        let facility_source = self
890            .capital_book
891            .body
892            .sources
893            .iter()
894            .find(|source| {
895                source.facility_id.as_deref() == Some(self.facility.body.facility_id.as_str())
896            })
897            .ok_or_else(|| {
898                "pricing authority capital book must include the referenced facility source"
899                    .to_string()
900            })?;
901        if facility_source.currency.trim().to_ascii_uppercase() != self.provider_policy.currency {
902            return Err(
903                "pricing authority capital book source currency must match provider policy currency"
904                    .to_string(),
905            );
906        }
907        if let Some(committed_amount) = facility_source.committed_amount.as_ref() {
908            let available_units = committed_amount
909                .units
910                .saturating_sub(
911                    facility_source
912                        .disbursed_amount
913                        .as_ref()
914                        .map_or(0, |amount| amount.units),
915                )
916                .saturating_sub(
917                    facility_source
918                        .impaired_amount
919                        .as_ref()
920                        .map_or(0, |amount| amount.units),
921                );
922            if self.max_coverage_amount.units > available_units {
923                return Err(
924                    "pricing authority max_coverage_amount exceeds capital book available committed amount"
925                        .to_string(),
926                );
927            }
928        }
929        if self.auto_bind_enabled
930            && (!self.provider_policy.bound_coverage_supported
931                || !self.provider_policy.claims_supported)
932        {
933            return Err(
934                "pricing authority cannot enable auto_bind because the provider policy does not support bound coverage and claims"
935                    .to_string(),
936            );
937        }
938        Ok(())
939    }
940}
941
942pub type SignedLiabilityPricingAuthority = SignedExportEnvelope<LiabilityPricingAuthorityArtifact>;
943
944#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
945#[serde(rename_all = "camelCase")]
946pub struct LiabilityPlacementArtifact {
947    pub schema: String,
948    pub placement_id: String,
949    pub issued_at: u64,
950    pub quote_response: SignedLiabilityQuoteResponse,
951    pub selected_coverage_amount: MonetaryAmount,
952    pub selected_premium_amount: MonetaryAmount,
953    pub effective_from: u64,
954    pub effective_until: u64,
955    #[serde(default, skip_serializing_if = "Option::is_none")]
956    pub placement_ref: Option<String>,
957    #[serde(default, skip_serializing_if = "Option::is_none")]
958    pub notes: Option<String>,
959}
960
961impl LiabilityPlacementArtifact {
962    pub fn validate(&self) -> Result<(), String> {
963        if !self.quote_response.verify_signature().map_err(|error| {
964            format!("placement quote_response signature verification failed: {error}")
965        })? {
966            return Err("placement quote_response signature verification failed".to_string());
967        }
968        self.quote_response.body.validate()?;
969        let quote_request = &self.quote_response.body.quote_request.body;
970        let quoted_terms = self
971            .quote_response
972            .body
973            .quoted_terms
974            .as_ref()
975            .ok_or_else(|| "placements require a quoted quote response".to_string())?;
976        if self.quote_response.body.disposition != LiabilityQuoteDisposition::Quoted {
977            return Err("placements require a quoted quote response".to_string());
978        }
979        validate_positive_money(
980            &self.selected_coverage_amount,
981            "placement selected_coverage_amount",
982        )?;
983        validate_positive_money(
984            &self.selected_premium_amount,
985            "placement selected_premium_amount",
986        )?;
987        if self.selected_coverage_amount != quote_request.requested_coverage_amount {
988            return Err(
989                "placement selected_coverage_amount must match the quote request requested_coverage_amount"
990                    .to_string(),
991            );
992        }
993        if self.selected_coverage_amount != quoted_terms.quoted_coverage_amount {
994            return Err(
995                "placement selected_coverage_amount must match the quoted coverage amount"
996                    .to_string(),
997            );
998        }
999        if self.selected_premium_amount != quoted_terms.quoted_premium_amount {
1000            return Err(
1001                "placement selected_premium_amount must match the quoted premium amount"
1002                    .to_string(),
1003            );
1004        }
1005        if self.effective_from != quote_request.requested_effective_from
1006            || self.effective_until != quote_request.requested_effective_until
1007        {
1008            return Err(
1009                "placement effective window must match the quote request effective window"
1010                    .to_string(),
1011            );
1012        }
1013        if self.effective_until <= self.effective_from {
1014            return Err("placement effective window must have end after start".to_string());
1015        }
1016        if self.issued_at > quoted_terms.expires_at {
1017            return Err("placement cannot be issued after the quote expires".to_string());
1018        }
1019        Ok(())
1020    }
1021}
1022
1023pub type SignedLiabilityPlacement = SignedExportEnvelope<LiabilityPlacementArtifact>;
1024
1025#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1026#[serde(rename_all = "camelCase")]
1027pub struct LiabilityBoundCoverageArtifact {
1028    pub schema: String,
1029    pub bound_coverage_id: String,
1030    pub issued_at: u64,
1031    pub placement: SignedLiabilityPlacement,
1032    pub policy_number: String,
1033    #[serde(default, skip_serializing_if = "Option::is_none")]
1034    pub carrier_reference: Option<String>,
1035    pub bound_at: u64,
1036    pub effective_from: u64,
1037    pub effective_until: u64,
1038    pub coverage_amount: MonetaryAmount,
1039    pub premium_amount: MonetaryAmount,
1040}
1041
1042impl LiabilityBoundCoverageArtifact {
1043    pub fn validate(&self) -> Result<(), String> {
1044        if !self.placement.verify_signature().map_err(|error| {
1045            format!("bound coverage placement signature verification failed: {error}")
1046        })? {
1047            return Err("bound coverage placement signature verification failed".to_string());
1048        }
1049        self.placement.body.validate()?;
1050        let quote_request = &self.placement.body.quote_response.body.quote_request.body;
1051        if self.policy_number.trim().is_empty() {
1052            return Err("bound coverage requires policy_number".to_string());
1053        }
1054        if self.bound_at < self.placement.body.issued_at {
1055            return Err("bound coverage bound_at cannot precede placement issuance".to_string());
1056        }
1057        if self.effective_from != self.placement.body.effective_from
1058            || self.effective_until != self.placement.body.effective_until
1059        {
1060            return Err(
1061                "bound coverage effective window must match the placement effective window"
1062                    .to_string(),
1063            );
1064        }
1065        if self.effective_until <= self.effective_from {
1066            return Err("bound coverage effective window must have end after start".to_string());
1067        }
1068        if self.coverage_amount != self.placement.body.selected_coverage_amount {
1069            return Err(
1070                "bound coverage coverage_amount must match the placement selected_coverage_amount"
1071                    .to_string(),
1072            );
1073        }
1074        if self.premium_amount != self.placement.body.selected_premium_amount {
1075            return Err(
1076                "bound coverage premium_amount must match the placement selected_premium_amount"
1077                    .to_string(),
1078            );
1079        }
1080        if !quote_request.provider_policy.bound_coverage_supported {
1081            return Err(
1082                "bound coverage cannot be issued because the provider policy does not support bound coverage"
1083                    .to_string(),
1084            );
1085        }
1086        if !quote_request.provider_policy.claims_supported {
1087            return Err(
1088                "bound coverage cannot be issued because the provider policy does not support claims"
1089                    .to_string(),
1090            );
1091        }
1092        Ok(())
1093    }
1094}
1095
1096pub type SignedLiabilityBoundCoverage = SignedExportEnvelope<LiabilityBoundCoverageArtifact>;
1097
1098#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1099#[serde(rename_all = "snake_case")]
1100pub enum LiabilityAutoBindDisposition {
1101    AutoBound,
1102    ManualReview,
1103    Denied,
1104}
1105
1106#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1107#[serde(rename_all = "snake_case")]
1108pub enum LiabilityAutoBindReasonCode {
1109    AuthorityExpired,
1110    QuoteExpired,
1111    AutoBindDisabled,
1112    CoverageExceedsAuthority,
1113    PremiumExceedsAuthority,
1114    CapitalUnavailable,
1115}
1116
1117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1118#[serde(rename_all = "camelCase")]
1119pub struct LiabilityAutoBindFinding {
1120    pub code: LiabilityAutoBindReasonCode,
1121    pub description: String,
1122}
1123
1124#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1125#[serde(rename_all = "camelCase")]
1126pub struct LiabilityAutoBindDecisionArtifact {
1127    pub schema: String,
1128    pub decision_id: String,
1129    pub issued_at: u64,
1130    pub authority: SignedLiabilityPricingAuthority,
1131    pub quote_response: SignedLiabilityQuoteResponse,
1132    pub disposition: LiabilityAutoBindDisposition,
1133    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1134    pub findings: Vec<LiabilityAutoBindFinding>,
1135    #[serde(default, skip_serializing_if = "Option::is_none")]
1136    pub placement: Option<SignedLiabilityPlacement>,
1137    #[serde(default, skip_serializing_if = "Option::is_none")]
1138    pub bound_coverage: Option<SignedLiabilityBoundCoverage>,
1139}
1140
1141impl LiabilityAutoBindDecisionArtifact {
1142    pub fn validate(&self) -> Result<(), String> {
1143        if !self.authority.verify_signature().map_err(|error| {
1144            format!("auto-bind authority signature verification failed: {error}")
1145        })? {
1146            return Err("auto-bind authority signature verification failed".to_string());
1147        }
1148        if !self.quote_response.verify_signature().map_err(|error| {
1149            format!("auto-bind quote_response signature verification failed: {error}")
1150        })? {
1151            return Err("auto-bind quote_response signature verification failed".to_string());
1152        }
1153        self.authority.body.validate()?;
1154        self.quote_response.body.validate()?;
1155        if self.authority.body.quote_request.body.quote_request_id
1156            != self.quote_response.body.quote_request.body.quote_request_id
1157        {
1158            return Err(
1159                "auto-bind authority quote_request_id must match the quote response quote_request_id"
1160                    .to_string(),
1161            );
1162        }
1163        if self.authority.body.provider_policy
1164            != self.quote_response.body.quote_request.body.provider_policy
1165        {
1166            return Err(
1167                "auto-bind authority provider_policy must match the quote response provider_policy"
1168                    .to_string(),
1169            );
1170        }
1171        match self.disposition {
1172            LiabilityAutoBindDisposition::AutoBound => {
1173                let placement = self
1174                    .placement
1175                    .as_ref()
1176                    .ok_or_else(|| "auto-bound decisions require placement".to_string())?;
1177                let bound_coverage = self
1178                    .bound_coverage
1179                    .as_ref()
1180                    .ok_or_else(|| "auto-bound decisions require bound_coverage".to_string())?;
1181                if !placement.verify_signature().map_err(|error| {
1182                    format!("auto-bind placement signature verification failed: {error}")
1183                })? {
1184                    return Err("auto-bind placement signature verification failed".to_string());
1185                }
1186                if !bound_coverage.verify_signature().map_err(|error| {
1187                    format!("auto-bind bound coverage signature verification failed: {error}")
1188                })? {
1189                    return Err(
1190                        "auto-bind bound coverage signature verification failed".to_string()
1191                    );
1192                }
1193                placement.body.validate()?;
1194                bound_coverage.body.validate()?;
1195                if placement.body.quote_response.body != self.quote_response.body {
1196                    return Err(
1197                        "auto-bind placement quote_response must match the decision quote_response"
1198                            .to_string(),
1199                    );
1200                }
1201                if bound_coverage.body.placement.body != placement.body {
1202                    return Err(
1203                        "auto-bind bound coverage placement must match the decision placement"
1204                            .to_string(),
1205                    );
1206                }
1207            }
1208            LiabilityAutoBindDisposition::ManualReview | LiabilityAutoBindDisposition::Denied => {
1209                if self.placement.is_some() || self.bound_coverage.is_some() {
1210                    return Err(
1211                        "manual-review and denied auto-bind decisions cannot embed issued placement or bound coverage"
1212                            .to_string(),
1213                    );
1214                }
1215            }
1216        }
1217        Ok(())
1218    }
1219}
1220
1221pub type SignedLiabilityAutoBindDecision = SignedExportEnvelope<LiabilityAutoBindDecisionArtifact>;
1222
1223#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1224#[serde(rename_all = "camelCase")]
1225pub struct LiabilityMarketWorkflowQuery {
1226    #[serde(default, skip_serializing_if = "Option::is_none")]
1227    pub quote_request_id: Option<String>,
1228    #[serde(default, skip_serializing_if = "Option::is_none")]
1229    pub provider_id: Option<String>,
1230    #[serde(default, skip_serializing_if = "Option::is_none")]
1231    pub agent_subject: Option<String>,
1232    #[serde(default, skip_serializing_if = "Option::is_none")]
1233    pub jurisdiction: Option<String>,
1234    #[serde(default, skip_serializing_if = "Option::is_none")]
1235    pub coverage_class: Option<LiabilityCoverageClass>,
1236    #[serde(default, skip_serializing_if = "Option::is_none")]
1237    pub currency: Option<String>,
1238    #[serde(default, skip_serializing_if = "Option::is_none")]
1239    pub limit: Option<usize>,
1240}
1241
1242impl Default for LiabilityMarketWorkflowQuery {
1243    fn default() -> Self {
1244        Self {
1245            quote_request_id: None,
1246            provider_id: None,
1247            agent_subject: None,
1248            jurisdiction: None,
1249            coverage_class: None,
1250            currency: None,
1251            limit: Some(50),
1252        }
1253    }
1254}
1255
1256impl LiabilityMarketWorkflowQuery {
1257    #[must_use]
1258    pub fn limit_or_default(&self) -> usize {
1259        self.limit
1260            .unwrap_or(50)
1261            .clamp(1, MAX_LIABILITY_MARKET_WORKFLOW_LIMIT)
1262    }
1263
1264    #[must_use]
1265    pub fn normalized(&self) -> Self {
1266        let mut normalized = self.clone();
1267        normalized.limit = Some(self.limit_or_default());
1268        normalized.provider_id = self
1269            .provider_id
1270            .as_ref()
1271            .map(|value| value.trim().to_string());
1272        normalized.quote_request_id = self
1273            .quote_request_id
1274            .as_ref()
1275            .map(|value| value.trim().to_string());
1276        normalized.agent_subject = self
1277            .agent_subject
1278            .as_ref()
1279            .map(|value| value.trim().to_string());
1280        normalized.jurisdiction = self
1281            .jurisdiction
1282            .as_ref()
1283            .map(|value| value.trim().to_ascii_lowercase());
1284        normalized.currency = self
1285            .currency
1286            .as_ref()
1287            .map(|value| value.trim().to_ascii_uppercase());
1288        normalized
1289    }
1290}
1291
1292#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1293#[serde(rename_all = "camelCase")]
1294pub struct LiabilityMarketWorkflowRow {
1295    pub quote_request: SignedLiabilityQuoteRequest,
1296    #[serde(default, skip_serializing_if = "Option::is_none")]
1297    pub latest_quote_response: Option<SignedLiabilityQuoteResponse>,
1298    #[serde(default, skip_serializing_if = "Option::is_none")]
1299    pub pricing_authority: Option<SignedLiabilityPricingAuthority>,
1300    #[serde(default, skip_serializing_if = "Option::is_none")]
1301    pub latest_auto_bind_decision: Option<SignedLiabilityAutoBindDecision>,
1302    #[serde(default, skip_serializing_if = "Option::is_none")]
1303    pub placement: Option<SignedLiabilityPlacement>,
1304    #[serde(default, skip_serializing_if = "Option::is_none")]
1305    pub bound_coverage: Option<SignedLiabilityBoundCoverage>,
1306}
1307
1308#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1309#[serde(rename_all = "camelCase")]
1310pub struct LiabilityMarketWorkflowSummary {
1311    pub matching_requests: u64,
1312    pub returned_requests: u64,
1313    pub quote_responses: u64,
1314    pub quoted_responses: u64,
1315    pub declined_responses: u64,
1316    pub pricing_authorities: u64,
1317    pub auto_bind_decisions: u64,
1318    pub auto_bound_decisions: u64,
1319    pub manual_review_decisions: u64,
1320    pub denied_decisions: u64,
1321    pub placements: u64,
1322    pub bound_coverages: u64,
1323}
1324
1325#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1326#[serde(rename_all = "camelCase")]
1327pub struct LiabilityMarketWorkflowReport {
1328    pub schema: String,
1329    pub generated_at: u64,
1330    pub query: LiabilityMarketWorkflowQuery,
1331    pub summary: LiabilityMarketWorkflowSummary,
1332    pub workflows: Vec<LiabilityMarketWorkflowRow>,
1333}
1334
1335#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1336#[serde(rename_all = "snake_case")]
1337pub enum LiabilityClaimEvidenceKind {
1338    BoundCoverage,
1339    ExposureLedger,
1340    CreditBond,
1341    CreditLossLifecycle,
1342    Receipt,
1343    ClaimResponse,
1344    ClaimDispute,
1345}
1346
1347#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1348#[serde(rename_all = "camelCase")]
1349pub struct LiabilityClaimEvidenceReference {
1350    pub kind: LiabilityClaimEvidenceKind,
1351    pub reference_id: String,
1352    #[serde(default, skip_serializing_if = "Option::is_none")]
1353    pub observed_at: Option<u64>,
1354    #[serde(default, skip_serializing_if = "Option::is_none")]
1355    pub locator: Option<String>,
1356}
1357
1358#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1359#[serde(rename_all = "snake_case")]
1360pub enum LiabilityClaimResponseDisposition {
1361    Acknowledged,
1362    Accepted,
1363    Denied,
1364}
1365
1366#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1367#[serde(rename_all = "snake_case")]
1368pub enum LiabilityClaimAdjudicationOutcome {
1369    ClaimUpheld,
1370    ProviderUpheld,
1371    PartialSettlement,
1372}
1373
1374#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1375#[serde(rename_all = "camelCase")]
1376pub struct LiabilityClaimPackageArtifact {
1377    pub schema: String,
1378    pub claim_id: String,
1379    pub issued_at: u64,
1380    pub bound_coverage: SignedLiabilityBoundCoverage,
1381    pub exposure: SignedExposureLedgerReport,
1382    pub bond: SignedCreditBond,
1383    pub loss_event: SignedCreditLossLifecycle,
1384    pub claimant: String,
1385    pub claim_event_at: u64,
1386    pub claim_amount: MonetaryAmount,
1387    #[serde(default, skip_serializing_if = "Option::is_none")]
1388    pub claim_ref: Option<String>,
1389    pub narrative: String,
1390    pub receipt_ids: Vec<String>,
1391    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1392    pub evidence_refs: Vec<LiabilityClaimEvidenceReference>,
1393}
1394
1395impl LiabilityClaimPackageArtifact {
1396    pub fn validate(&self) -> Result<(), String> {
1397        if self.claimant.trim().is_empty() {
1398            return Err("claim packages require a non-empty claimant".to_string());
1399        }
1400        if self.narrative.trim().is_empty() {
1401            return Err("claim packages require a non-empty narrative".to_string());
1402        }
1403        if self.receipt_ids.is_empty() {
1404            return Err("claim packages require at least one receipt reference".to_string());
1405        }
1406        let mut deduped_receipts = BTreeSet::new();
1407        for receipt_id in &self.receipt_ids {
1408            if receipt_id.trim().is_empty() {
1409                return Err("claim receipt references must be non-empty".to_string());
1410            }
1411            if !deduped_receipts.insert(receipt_id.trim().to_string()) {
1412                return Err("claim receipt references must be unique".to_string());
1413            }
1414        }
1415        validate_positive_money(&self.claim_amount, "claim_amount")?;
1416        let coverage = &self.bound_coverage.body.coverage_amount;
1417        if self.claim_amount.currency != coverage.currency {
1418            return Err("claim_amount currency must match bound coverage currency".to_string());
1419        }
1420        if self.claim_amount.units > coverage.units {
1421            return Err("claim_amount cannot exceed bound coverage amount".to_string());
1422        }
1423        if self.claim_event_at < self.bound_coverage.body.effective_from
1424            || self.claim_event_at > self.bound_coverage.body.effective_until
1425        {
1426            return Err(
1427                "claim_event_at must fall within the bound coverage effective window".to_string(),
1428            );
1429        }
1430        if self.exposure.body.summary.mixed_currency_book {
1431            return Err(
1432                "claim packages require exposure evidence without mixed-currency ambiguity"
1433                    .to_string(),
1434            );
1435        }
1436        let subject_key = &self
1437            .bound_coverage
1438            .body
1439            .placement
1440            .body
1441            .quote_response
1442            .body
1443            .quote_request
1444            .body
1445            .risk_package
1446            .body
1447            .subject_key;
1448        if self
1449            .exposure
1450            .body
1451            .filters
1452            .agent_subject
1453            .as_ref()
1454            .is_some_and(|agent_subject| agent_subject != subject_key)
1455        {
1456            return Err(
1457                "claim exposure evidence must match the bound coverage subject".to_string(),
1458            );
1459        }
1460        if self
1461            .bond
1462            .body
1463            .report
1464            .filters
1465            .agent_subject
1466            .as_ref()
1467            .is_some_and(|agent_subject| agent_subject != subject_key)
1468        {
1469            return Err("claim bond evidence must match the bound coverage subject".to_string());
1470        }
1471        if self.loss_event.body.bond_id != self.bond.body.bond_id {
1472            return Err("claim loss evidence must reference the same bond".to_string());
1473        }
1474        if self
1475            .loss_event
1476            .body
1477            .report
1478            .summary
1479            .agent_subject
1480            .as_ref()
1481            .is_some_and(|agent_subject| agent_subject != subject_key)
1482        {
1483            return Err("claim loss evidence must match the bound coverage subject".to_string());
1484        }
1485        Ok(())
1486    }
1487}
1488
1489pub type SignedLiabilityClaimPackage = SignedExportEnvelope<LiabilityClaimPackageArtifact>;
1490
1491#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1492#[serde(rename_all = "camelCase")]
1493pub struct LiabilityClaimResponseArtifact {
1494    pub schema: String,
1495    pub claim_response_id: String,
1496    pub issued_at: u64,
1497    pub claim: SignedLiabilityClaimPackage,
1498    pub provider_response_ref: String,
1499    pub disposition: LiabilityClaimResponseDisposition,
1500    #[serde(default, skip_serializing_if = "Option::is_none")]
1501    pub covered_amount: Option<MonetaryAmount>,
1502    #[serde(default, skip_serializing_if = "Option::is_none")]
1503    pub response_note: Option<String>,
1504    #[serde(default, skip_serializing_if = "Option::is_none")]
1505    pub denial_reason: Option<String>,
1506    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1507    pub evidence_refs: Vec<LiabilityClaimEvidenceReference>,
1508}
1509
1510impl LiabilityClaimResponseArtifact {
1511    pub fn validate(&self) -> Result<(), String> {
1512        self.claim.body.validate()?;
1513        if self.provider_response_ref.trim().is_empty() {
1514            return Err("claim responses require a non-empty provider_response_ref".to_string());
1515        }
1516        match self.disposition {
1517            LiabilityClaimResponseDisposition::Acknowledged => {
1518                if self.covered_amount.is_some() {
1519                    return Err(
1520                        "acknowledged claim responses cannot include covered_amount".to_string()
1521                    );
1522                }
1523                if self.denial_reason.is_some() {
1524                    return Err(
1525                        "acknowledged claim responses cannot include denial_reason".to_string()
1526                    );
1527                }
1528            }
1529            LiabilityClaimResponseDisposition::Accepted => {
1530                let covered_amount = self
1531                    .covered_amount
1532                    .as_ref()
1533                    .ok_or_else(|| "accepted claim responses require covered_amount".to_string())?;
1534                validate_positive_money(covered_amount, "covered_amount")?;
1535                if covered_amount.currency != self.claim.body.claim_amount.currency {
1536                    return Err(
1537                        "covered_amount currency must match claim_amount currency".to_string()
1538                    );
1539                }
1540                if covered_amount.units > self.claim.body.claim_amount.units {
1541                    return Err("covered_amount cannot exceed claim_amount".to_string());
1542                }
1543                if self.denial_reason.is_some() {
1544                    return Err("accepted claim responses cannot include denial_reason".to_string());
1545                }
1546            }
1547            LiabilityClaimResponseDisposition::Denied => {
1548                if self.covered_amount.is_some() {
1549                    return Err("denied claim responses cannot include covered_amount".to_string());
1550                }
1551                if self
1552                    .denial_reason
1553                    .as_ref()
1554                    .is_none_or(|reason| reason.trim().is_empty())
1555                {
1556                    return Err("denied claim responses require denial_reason".to_string());
1557                }
1558            }
1559        }
1560        Ok(())
1561    }
1562}
1563
1564pub type SignedLiabilityClaimResponse = SignedExportEnvelope<LiabilityClaimResponseArtifact>;
1565
1566#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1567#[serde(rename_all = "camelCase")]
1568pub struct LiabilityClaimDisputeArtifact {
1569    pub schema: String,
1570    pub dispute_id: String,
1571    pub issued_at: u64,
1572    pub provider_response: SignedLiabilityClaimResponse,
1573    pub opened_by: String,
1574    pub reason: String,
1575    #[serde(default, skip_serializing_if = "Option::is_none")]
1576    pub note: Option<String>,
1577    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1578    pub evidence_refs: Vec<LiabilityClaimEvidenceReference>,
1579}
1580
1581impl LiabilityClaimDisputeArtifact {
1582    pub fn validate(&self) -> Result<(), String> {
1583        self.provider_response.body.validate()?;
1584        if self.opened_by.trim().is_empty() {
1585            return Err("claim disputes require a non-empty opened_by".to_string());
1586        }
1587        if self.reason.trim().is_empty() {
1588            return Err("claim disputes require a non-empty reason".to_string());
1589        }
1590        let partially_accepted = self.provider_response.body.disposition
1591            == LiabilityClaimResponseDisposition::Accepted
1592            && self
1593                .provider_response
1594                .body
1595                .covered_amount
1596                .as_ref()
1597                .is_some_and(|amount| {
1598                    amount.units < self.provider_response.body.claim.body.claim_amount.units
1599                });
1600        if self.provider_response.body.disposition != LiabilityClaimResponseDisposition::Denied
1601            && !partially_accepted
1602        {
1603            return Err(
1604                "claim disputes require a denied or partially accepted provider response"
1605                    .to_string(),
1606            );
1607        }
1608        Ok(())
1609    }
1610}
1611
1612pub type SignedLiabilityClaimDispute = SignedExportEnvelope<LiabilityClaimDisputeArtifact>;
1613
1614#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1615#[serde(rename_all = "camelCase")]
1616pub struct LiabilityClaimAdjudicationArtifact {
1617    pub schema: String,
1618    pub adjudication_id: String,
1619    pub issued_at: u64,
1620    pub dispute: SignedLiabilityClaimDispute,
1621    pub adjudicator: String,
1622    pub outcome: LiabilityClaimAdjudicationOutcome,
1623    #[serde(default, skip_serializing_if = "Option::is_none")]
1624    pub awarded_amount: Option<MonetaryAmount>,
1625    #[serde(default, skip_serializing_if = "Option::is_none")]
1626    pub note: Option<String>,
1627    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1628    pub evidence_refs: Vec<LiabilityClaimEvidenceReference>,
1629}
1630
1631impl LiabilityClaimAdjudicationArtifact {
1632    pub fn validate(&self) -> Result<(), String> {
1633        self.dispute.body.validate()?;
1634        if self.adjudicator.trim().is_empty() {
1635            return Err("claim adjudications require a non-empty adjudicator".to_string());
1636        }
1637        let claim_amount = &self
1638            .dispute
1639            .body
1640            .provider_response
1641            .body
1642            .claim
1643            .body
1644            .claim_amount;
1645        match self.outcome {
1646            LiabilityClaimAdjudicationOutcome::ClaimUpheld => {
1647                let awarded_amount = self.awarded_amount.as_ref().ok_or_else(|| {
1648                    "claim_upheld adjudications require awarded_amount".to_string()
1649                })?;
1650                validate_positive_money(awarded_amount, "awarded_amount")?;
1651                if awarded_amount.currency != claim_amount.currency {
1652                    return Err(
1653                        "awarded_amount currency must match claim_amount currency".to_string()
1654                    );
1655                }
1656                if awarded_amount.units > claim_amount.units {
1657                    return Err("awarded_amount cannot exceed claim_amount".to_string());
1658                }
1659            }
1660            LiabilityClaimAdjudicationOutcome::ProviderUpheld => {
1661                if self.awarded_amount.is_some() {
1662                    return Err(
1663                        "provider_upheld adjudications cannot include awarded_amount".to_string(),
1664                    );
1665                }
1666            }
1667            LiabilityClaimAdjudicationOutcome::PartialSettlement => {
1668                let awarded_amount = self.awarded_amount.as_ref().ok_or_else(|| {
1669                    "partial_settlement adjudications require awarded_amount".to_string()
1670                })?;
1671                validate_positive_money(awarded_amount, "awarded_amount")?;
1672                if awarded_amount.currency != claim_amount.currency {
1673                    return Err(
1674                        "awarded_amount currency must match claim_amount currency".to_string()
1675                    );
1676                }
1677                if awarded_amount.units >= claim_amount.units {
1678                    return Err(
1679                        "partial_settlement awarded_amount must be less than claim_amount"
1680                            .to_string(),
1681                    );
1682                }
1683            }
1684        }
1685        Ok(())
1686    }
1687}
1688
1689pub type SignedLiabilityClaimAdjudication =
1690    SignedExportEnvelope<LiabilityClaimAdjudicationArtifact>;
1691
1692fn liability_claim_adjudication_payable_amount(
1693    adjudication: &LiabilityClaimAdjudicationArtifact,
1694) -> Result<&MonetaryAmount, String> {
1695    match adjudication.outcome {
1696        LiabilityClaimAdjudicationOutcome::ClaimUpheld
1697        | LiabilityClaimAdjudicationOutcome::PartialSettlement => {
1698            adjudication.awarded_amount.as_ref().ok_or_else(|| {
1699                "claim payout instructions require adjudications with awarded_amount".to_string()
1700            })
1701        }
1702        LiabilityClaimAdjudicationOutcome::ProviderUpheld => {
1703            Err("claim payout instructions require a payable adjudication outcome".to_string())
1704        }
1705    }
1706}
1707
1708#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1709#[serde(rename_all = "snake_case")]
1710pub enum LiabilityClaimPayoutReconciliationState {
1711    Matched,
1712    AmountMismatch,
1713}
1714
1715#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1716#[serde(rename_all = "snake_case")]
1717pub enum LiabilityClaimSettlementKind {
1718    RecoveryClearing,
1719    ReinsuranceReimbursement,
1720    FacilityReimbursement,
1721}
1722
1723#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1724#[serde(rename_all = "snake_case")]
1725pub enum LiabilityClaimSettlementReconciliationState {
1726    Matched,
1727    AmountMismatch,
1728    CounterpartyMismatch,
1729}
1730
1731#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1732#[serde(rename_all = "camelCase")]
1733pub struct LiabilityClaimSettlementRoleBinding {
1734    pub role: CapitalExecutionRole,
1735    pub party_id: String,
1736    #[serde(default, skip_serializing_if = "Option::is_none")]
1737    pub jurisdiction: Option<String>,
1738    #[serde(default, skip_serializing_if = "Option::is_none")]
1739    pub note: Option<String>,
1740}
1741
1742impl LiabilityClaimSettlementRoleBinding {
1743    fn validate(&self, field_name: &str) -> Result<(), String> {
1744        if self.party_id.trim().is_empty() {
1745            return Err(format!("{field_name} requires a non-empty party_id"));
1746        }
1747        Ok(())
1748    }
1749}
1750
1751#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1752#[serde(rename_all = "camelCase")]
1753pub struct LiabilityClaimSettlementRoleTopology {
1754    pub payer: LiabilityClaimSettlementRoleBinding,
1755    pub payee: LiabilityClaimSettlementRoleBinding,
1756    #[serde(default, skip_serializing_if = "Option::is_none")]
1757    pub beneficiary: Option<LiabilityClaimSettlementRoleBinding>,
1758}
1759
1760impl LiabilityClaimSettlementRoleTopology {
1761    fn validate(&self) -> Result<(), String> {
1762        self.payer.validate("settlement topology payer")?;
1763        self.payee.validate("settlement topology payee")?;
1764        if self.payer.role == self.payee.role && self.payer.party_id == self.payee.party_id {
1765            return Err("settlement topology payer and payee must not be identical".to_string());
1766        }
1767        if let Some(beneficiary) = self.beneficiary.as_ref() {
1768            beneficiary.validate("settlement topology beneficiary")?;
1769        }
1770        Ok(())
1771    }
1772}
1773
1774#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1775#[serde(rename_all = "camelCase")]
1776pub struct LiabilityClaimPayoutInstructionArtifact {
1777    pub schema: String,
1778    pub payout_instruction_id: String,
1779    pub issued_at: u64,
1780    pub adjudication: SignedLiabilityClaimAdjudication,
1781    pub capital_instruction: SignedCapitalExecutionInstruction,
1782    pub payout_amount: MonetaryAmount,
1783    #[serde(default, skip_serializing_if = "Option::is_none")]
1784    pub note: Option<String>,
1785}
1786
1787impl LiabilityClaimPayoutInstructionArtifact {
1788    pub fn validate(&self) -> Result<(), String> {
1789        self.adjudication.body.validate()?;
1790        if !self
1791            .capital_instruction
1792            .verify_signature()
1793            .map_err(|error| error.to_string())?
1794        {
1795            return Err(
1796                "claim payout instruction capital_instruction signature verification failed"
1797                    .to_string(),
1798            );
1799        }
1800        validate_positive_money(&self.payout_amount, "payout_amount")?;
1801        let awarded_amount = liability_claim_adjudication_payable_amount(&self.adjudication.body)?;
1802        if &self.payout_amount != awarded_amount {
1803            return Err(
1804                "claim payout instruction payout_amount must match adjudication awarded_amount"
1805                    .to_string(),
1806            );
1807        }
1808        let capital_instruction = &self.capital_instruction.body;
1809        if capital_instruction.action != CapitalExecutionInstructionAction::TransferFunds {
1810            return Err(
1811                "claim payout instructions require capital_instruction action transfer_funds"
1812                    .to_string(),
1813            );
1814        }
1815        if capital_instruction.source_kind != CapitalBookSourceKind::FacilityCommitment {
1816            return Err(
1817                "claim payout instructions require capital_instruction source_kind facility_commitment"
1818                    .to_string(),
1819            );
1820        }
1821        let intended_amount = capital_instruction.amount.as_ref().ok_or_else(|| {
1822            "claim payout instructions require capital_instruction amount".to_string()
1823        })?;
1824        if intended_amount != &self.payout_amount {
1825            return Err(
1826                "claim payout instruction capital_instruction amount must match payout_amount"
1827                    .to_string(),
1828            );
1829        }
1830        let subject_key = &self
1831            .adjudication
1832            .body
1833            .dispute
1834            .body
1835            .provider_response
1836            .body
1837            .claim
1838            .body
1839            .bound_coverage
1840            .body
1841            .placement
1842            .body
1843            .quote_response
1844            .body
1845            .quote_request
1846            .body
1847            .risk_package
1848            .body
1849            .subject_key;
1850        if &capital_instruction.subject_key != subject_key {
1851            return Err(
1852                "claim payout instruction capital_instruction subject_key must match the claim subject"
1853                    .to_string(),
1854            );
1855        }
1856        if capital_instruction.execution_window.not_after <= self.issued_at {
1857            return Err(
1858                "claim payout instructions require a non-stale capital_instruction execution window"
1859                    .to_string(),
1860            );
1861        }
1862        if capital_instruction.reconciled_state != CapitalExecutionReconciledState::NotObserved
1863            || capital_instruction.observed_execution.is_some()
1864        {
1865            return Err(
1866                "claim payout instructions require an unreconciled capital_instruction so payout receipts stay explicit"
1867                    .to_string(),
1868            );
1869        }
1870        Ok(())
1871    }
1872}
1873
1874pub type SignedLiabilityClaimPayoutInstruction =
1875    SignedExportEnvelope<LiabilityClaimPayoutInstructionArtifact>;
1876
1877#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1878#[serde(rename_all = "camelCase")]
1879pub struct LiabilityClaimPayoutReceiptArtifact {
1880    pub schema: String,
1881    pub payout_receipt_id: String,
1882    pub issued_at: u64,
1883    pub payout_instruction: SignedLiabilityClaimPayoutInstruction,
1884    pub payout_receipt_ref: String,
1885    pub reconciliation_state: LiabilityClaimPayoutReconciliationState,
1886    pub observed_execution: crate::credit::CapitalExecutionObservation,
1887    #[serde(default, skip_serializing_if = "Option::is_none")]
1888    pub note: Option<String>,
1889}
1890
1891impl LiabilityClaimPayoutReceiptArtifact {
1892    pub fn validate(&self) -> Result<(), String> {
1893        self.payout_instruction.body.validate()?;
1894        if self.payout_receipt_ref.trim().is_empty() {
1895            return Err("claim payout receipts require a non-empty payout_receipt_ref".to_string());
1896        }
1897        if self
1898            .observed_execution
1899            .external_reference_id
1900            .trim()
1901            .is_empty()
1902        {
1903            return Err(
1904                "claim payout receipts require a non-empty observed_execution external_reference_id"
1905                    .to_string(),
1906            );
1907        }
1908        validate_positive_money(
1909            &self.observed_execution.amount,
1910            "claim payout receipt observed_execution amount",
1911        )?;
1912        if self.observed_execution.amount.currency
1913            != self.payout_instruction.body.payout_amount.currency
1914        {
1915            return Err(
1916                "claim payout receipt observed_execution amount currency must match payout_amount"
1917                    .to_string(),
1918            );
1919        }
1920        let execution_window = &self
1921            .payout_instruction
1922            .body
1923            .capital_instruction
1924            .body
1925            .execution_window;
1926        if self.observed_execution.observed_at < execution_window.not_before
1927            || self.observed_execution.observed_at > execution_window.not_after
1928        {
1929            return Err(
1930                "claim payout receipt observed_execution timestamp falls outside the payout instruction execution window"
1931                    .to_string(),
1932            );
1933        }
1934        match self.reconciliation_state {
1935            LiabilityClaimPayoutReconciliationState::Matched => {
1936                if self.observed_execution.amount != self.payout_instruction.body.payout_amount {
1937                    return Err(
1938                        "matched claim payout receipts require observed_execution amount to match payout_amount"
1939                            .to_string(),
1940                    );
1941                }
1942            }
1943            LiabilityClaimPayoutReconciliationState::AmountMismatch => {
1944                if self.observed_execution.amount == self.payout_instruction.body.payout_amount {
1945                    return Err(
1946                        "amount_mismatch claim payout receipts require observed_execution amount to differ from payout_amount"
1947                            .to_string(),
1948                    );
1949                }
1950            }
1951        }
1952        Ok(())
1953    }
1954}
1955
1956pub type SignedLiabilityClaimPayoutReceipt =
1957    SignedExportEnvelope<LiabilityClaimPayoutReceiptArtifact>;
1958
1959#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1960#[serde(rename_all = "camelCase")]
1961pub struct LiabilityClaimSettlementInstructionArtifact {
1962    pub schema: String,
1963    pub settlement_instruction_id: String,
1964    pub issued_at: u64,
1965    pub payout_receipt: SignedLiabilityClaimPayoutReceipt,
1966    pub capital_book: SignedCapitalBookReport,
1967    pub settlement_kind: LiabilityClaimSettlementKind,
1968    pub settlement_amount: MonetaryAmount,
1969    pub topology: LiabilityClaimSettlementRoleTopology,
1970    pub authority_chain: Vec<CapitalExecutionAuthorityStep>,
1971    pub execution_window: CapitalExecutionWindow,
1972    pub rail: CapitalExecutionRail,
1973    #[serde(default, skip_serializing_if = "Option::is_none")]
1974    pub settlement_reference: Option<String>,
1975    #[serde(default, skip_serializing_if = "Option::is_none")]
1976    pub note: Option<String>,
1977}
1978
1979impl LiabilityClaimSettlementInstructionArtifact {
1980    pub fn validate(&self) -> Result<(), String> {
1981        self.payout_receipt.body.validate()?;
1982        if !self
1983            .capital_book
1984            .verify_signature()
1985            .map_err(|error| error.to_string())?
1986        {
1987            return Err(
1988                "claim settlement instruction capital_book signature verification failed"
1989                    .to_string(),
1990            );
1991        }
1992        validate_positive_money(&self.settlement_amount, "settlement_amount")?;
1993        self.topology.validate()?;
1994        if self.payout_receipt.body.reconciliation_state
1995            != LiabilityClaimPayoutReconciliationState::Matched
1996        {
1997            return Err(
1998                "claim settlement instructions require a matched payout_receipt".to_string(),
1999            );
2000        }
2001        if self.settlement_amount.currency
2002            != self
2003                .payout_receipt
2004                .body
2005                .payout_instruction
2006                .body
2007                .payout_amount
2008                .currency
2009        {
2010            return Err(
2011                "claim settlement instruction settlement_amount currency must match payout_amount"
2012                    .to_string(),
2013            );
2014        }
2015        if self.settlement_amount.units
2016            > self
2017                .payout_receipt
2018                .body
2019                .payout_instruction
2020                .body
2021                .payout_amount
2022                .units
2023        {
2024            return Err(
2025                "claim settlement instruction settlement_amount cannot exceed payout_amount"
2026                    .to_string(),
2027            );
2028        }
2029        let subject_key = &self
2030            .payout_receipt
2031            .body
2032            .payout_instruction
2033            .body
2034            .adjudication
2035            .body
2036            .dispute
2037            .body
2038            .provider_response
2039            .body
2040            .claim
2041            .body
2042            .bound_coverage
2043            .body
2044            .placement
2045            .body
2046            .quote_response
2047            .body
2048            .quote_request
2049            .body
2050            .risk_package
2051            .body
2052            .subject_key;
2053        if self.capital_book.body.subject_key != *subject_key {
2054            return Err(
2055                "claim settlement instruction capital_book subject_key must match the claim subject"
2056                    .to_string(),
2057            );
2058        }
2059        if self.capital_book.body.summary.mixed_currency_book {
2060            return Err(
2061                "claim settlement instructions require a capital_book without mixed-currency ambiguity"
2062                    .to_string(),
2063            );
2064        }
2065        if self.authority_chain.is_empty() {
2066            return Err(
2067                "claim settlement instructions require at least one authority_chain step"
2068                    .to_string(),
2069            );
2070        }
2071        if self.rail.rail_id.trim().is_empty() {
2072            return Err("claim settlement instructions require rail.rail_id".to_string());
2073        }
2074        if self.rail.custody_provider_id.trim().is_empty() {
2075            return Err(
2076                "claim settlement instructions require rail.custody_provider_id".to_string(),
2077            );
2078        }
2079        if self.execution_window.not_before > self.execution_window.not_after {
2080            return Err(
2081                "claim settlement instructions require execution_window.not_before <= not_after"
2082                    .to_string(),
2083            );
2084        }
2085        if self.execution_window.not_after <= self.issued_at {
2086            return Err(
2087                "claim settlement instructions require a non-stale execution_window".to_string(),
2088            );
2089        }
2090        let mut payer_role_present = false;
2091        let mut custodian_present = false;
2092        for step in &self.authority_chain {
2093            if step.principal_id.trim().is_empty() {
2094                return Err(
2095                    "claim settlement authority_chain principal_id cannot be empty".to_string(),
2096                );
2097            }
2098            if step.approved_at > step.expires_at {
2099                return Err(
2100                    "claim settlement authority_chain requires approved_at <= expires_at"
2101                        .to_string(),
2102                );
2103            }
2104            if step.expires_at < self.issued_at {
2105                return Err(format!(
2106                    "claim settlement authority step `{}` is stale at issuance time",
2107                    step.principal_id
2108                ));
2109            }
2110            if step.expires_at < self.execution_window.not_after {
2111                return Err(format!(
2112                    "claim settlement authority step `{}` expires before the execution window closes",
2113                    step.principal_id
2114                ));
2115            }
2116            if step.role == self.topology.payer.role {
2117                payer_role_present = true;
2118            }
2119            if step.role == CapitalExecutionRole::Custodian
2120                && step.principal_id == self.rail.custody_provider_id
2121            {
2122                custodian_present = true;
2123            }
2124        }
2125        if !payer_role_present {
2126            return Err(
2127                "claim settlement authority_chain is missing payer-role approval".to_string(),
2128            );
2129        }
2130        if !custodian_present {
2131            return Err(
2132                "claim settlement authority_chain is missing the custody-provider execution step"
2133                    .to_string(),
2134            );
2135        }
2136        Ok(())
2137    }
2138}
2139
2140pub type SignedLiabilityClaimSettlementInstruction =
2141    SignedExportEnvelope<LiabilityClaimSettlementInstructionArtifact>;
2142
2143#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2144#[serde(rename_all = "camelCase")]
2145pub struct LiabilityClaimSettlementReceiptArtifact {
2146    pub schema: String,
2147    pub settlement_receipt_id: String,
2148    pub issued_at: u64,
2149    pub settlement_instruction: SignedLiabilityClaimSettlementInstruction,
2150    pub settlement_receipt_ref: String,
2151    pub reconciliation_state: LiabilityClaimSettlementReconciliationState,
2152    pub observed_execution: CapitalExecutionObservation,
2153    pub observed_payer_id: String,
2154    pub observed_payee_id: String,
2155    #[serde(default, skip_serializing_if = "Option::is_none")]
2156    pub note: Option<String>,
2157}
2158
2159impl LiabilityClaimSettlementReceiptArtifact {
2160    pub fn validate(&self) -> Result<(), String> {
2161        self.settlement_instruction.body.validate()?;
2162        if self.settlement_receipt_ref.trim().is_empty() {
2163            return Err(
2164                "claim settlement receipts require a non-empty settlement_receipt_ref".to_string(),
2165            );
2166        }
2167        if self.observed_payer_id.trim().is_empty() {
2168            return Err(
2169                "claim settlement receipts require a non-empty observed_payer_id".to_string(),
2170            );
2171        }
2172        if self.observed_payee_id.trim().is_empty() {
2173            return Err(
2174                "claim settlement receipts require a non-empty observed_payee_id".to_string(),
2175            );
2176        }
2177        if self
2178            .observed_execution
2179            .external_reference_id
2180            .trim()
2181            .is_empty()
2182        {
2183            return Err(
2184                "claim settlement receipts require a non-empty observed_execution external_reference_id"
2185                    .to_string(),
2186            );
2187        }
2188        validate_positive_money(
2189            &self.observed_execution.amount,
2190            "claim settlement receipt observed_execution amount",
2191        )?;
2192        if self.observed_execution.amount.currency
2193            != self.settlement_instruction.body.settlement_amount.currency
2194        {
2195            return Err(
2196                "claim settlement receipt observed_execution amount currency must match settlement_amount"
2197                    .to_string(),
2198            );
2199        }
2200        let execution_window = &self.settlement_instruction.body.execution_window;
2201        if self.observed_execution.observed_at < execution_window.not_before
2202            || self.observed_execution.observed_at > execution_window.not_after
2203        {
2204            return Err(
2205                "claim settlement receipt observed_execution timestamp falls outside the settlement execution window"
2206                    .to_string(),
2207            );
2208        }
2209        let expected_payer = &self.settlement_instruction.body.topology.payer.party_id;
2210        let expected_payee = &self.settlement_instruction.body.topology.payee.party_id;
2211        match self.reconciliation_state {
2212            LiabilityClaimSettlementReconciliationState::Matched => {
2213                if self.observed_execution.amount
2214                    != self.settlement_instruction.body.settlement_amount
2215                {
2216                    return Err(
2217                        "matched claim settlement receipts require observed_execution amount to match settlement_amount"
2218                            .to_string(),
2219                    );
2220                }
2221                if &self.observed_payer_id != expected_payer
2222                    || &self.observed_payee_id != expected_payee
2223                {
2224                    return Err(
2225                        "matched claim settlement receipts require observed payer/payee to match the settlement topology"
2226                            .to_string(),
2227                    );
2228                }
2229            }
2230            LiabilityClaimSettlementReconciliationState::AmountMismatch => {
2231                if self.observed_execution.amount
2232                    == self.settlement_instruction.body.settlement_amount
2233                {
2234                    return Err(
2235                        "amount_mismatch claim settlement receipts require observed_execution amount to differ from settlement_amount"
2236                            .to_string(),
2237                    );
2238                }
2239                if &self.observed_payer_id != expected_payer
2240                    || &self.observed_payee_id != expected_payee
2241                {
2242                    return Err(
2243                        "amount_mismatch claim settlement receipts still require observed payer/payee to match the settlement topology"
2244                            .to_string(),
2245                    );
2246                }
2247            }
2248            LiabilityClaimSettlementReconciliationState::CounterpartyMismatch => {
2249                if &self.observed_payer_id == expected_payer
2250                    && &self.observed_payee_id == expected_payee
2251                {
2252                    return Err(
2253                        "counterparty_mismatch claim settlement receipts require at least one observed counterparty to differ from the settlement topology"
2254                            .to_string(),
2255                    );
2256                }
2257            }
2258        }
2259        Ok(())
2260    }
2261}
2262
2263pub type SignedLiabilityClaimSettlementReceipt =
2264    SignedExportEnvelope<LiabilityClaimSettlementReceiptArtifact>;
2265
2266#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2267#[serde(rename_all = "camelCase")]
2268pub struct LiabilityClaimWorkflowQuery {
2269    #[serde(default, skip_serializing_if = "Option::is_none")]
2270    pub claim_id: Option<String>,
2271    #[serde(default, skip_serializing_if = "Option::is_none")]
2272    pub provider_id: Option<String>,
2273    #[serde(default, skip_serializing_if = "Option::is_none")]
2274    pub agent_subject: Option<String>,
2275    #[serde(default, skip_serializing_if = "Option::is_none")]
2276    pub jurisdiction: Option<String>,
2277    #[serde(default, skip_serializing_if = "Option::is_none")]
2278    pub policy_number: Option<String>,
2279    #[serde(default, skip_serializing_if = "Option::is_none")]
2280    pub limit: Option<usize>,
2281}
2282
2283impl Default for LiabilityClaimWorkflowQuery {
2284    fn default() -> Self {
2285        Self {
2286            claim_id: None,
2287            provider_id: None,
2288            agent_subject: None,
2289            jurisdiction: None,
2290            policy_number: None,
2291            limit: Some(50),
2292        }
2293    }
2294}
2295
2296impl LiabilityClaimWorkflowQuery {
2297    #[must_use]
2298    pub fn limit_or_default(&self) -> usize {
2299        self.limit
2300            .unwrap_or(50)
2301            .clamp(1, MAX_LIABILITY_CLAIM_WORKFLOW_LIMIT)
2302    }
2303
2304    #[must_use]
2305    pub fn normalized(&self) -> Self {
2306        let mut normalized = self.clone();
2307        normalized.limit = Some(self.limit_or_default());
2308        normalized.claim_id = self.claim_id.as_ref().map(|value| value.trim().to_string());
2309        normalized.provider_id = self
2310            .provider_id
2311            .as_ref()
2312            .map(|value| value.trim().to_string());
2313        normalized.agent_subject = self
2314            .agent_subject
2315            .as_ref()
2316            .map(|value| value.trim().to_string());
2317        normalized.jurisdiction = self
2318            .jurisdiction
2319            .as_ref()
2320            .map(|value| value.trim().to_ascii_lowercase());
2321        normalized.policy_number = self
2322            .policy_number
2323            .as_ref()
2324            .map(|value| value.trim().to_string());
2325        normalized
2326    }
2327}
2328
2329#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2330#[serde(rename_all = "camelCase")]
2331pub struct LiabilityClaimWorkflowRow {
2332    pub claim: SignedLiabilityClaimPackage,
2333    #[serde(default, skip_serializing_if = "Option::is_none")]
2334    pub provider_response: Option<SignedLiabilityClaimResponse>,
2335    #[serde(default, skip_serializing_if = "Option::is_none")]
2336    pub dispute: Option<SignedLiabilityClaimDispute>,
2337    #[serde(default, skip_serializing_if = "Option::is_none")]
2338    pub adjudication: Option<SignedLiabilityClaimAdjudication>,
2339    #[serde(default, skip_serializing_if = "Option::is_none")]
2340    pub payout_instruction: Option<SignedLiabilityClaimPayoutInstruction>,
2341    #[serde(default, skip_serializing_if = "Option::is_none")]
2342    pub payout_receipt: Option<SignedLiabilityClaimPayoutReceipt>,
2343    #[serde(default, skip_serializing_if = "Option::is_none")]
2344    pub settlement_instruction: Option<SignedLiabilityClaimSettlementInstruction>,
2345    #[serde(default, skip_serializing_if = "Option::is_none")]
2346    pub settlement_receipt: Option<SignedLiabilityClaimSettlementReceipt>,
2347}
2348
2349#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2350#[serde(rename_all = "camelCase")]
2351pub struct LiabilityClaimWorkflowSummary {
2352    pub matching_claims: u64,
2353    pub returned_claims: u64,
2354    pub provider_responses: u64,
2355    pub accepted_responses: u64,
2356    pub denied_responses: u64,
2357    pub disputes: u64,
2358    pub adjudications: u64,
2359    pub payout_instructions: u64,
2360    pub payout_receipts: u64,
2361    pub matched_payout_receipts: u64,
2362    pub mismatched_payout_receipts: u64,
2363    pub settlement_instructions: u64,
2364    pub settlement_receipts: u64,
2365    pub matched_settlement_receipts: u64,
2366    pub mismatched_settlement_receipts: u64,
2367    pub counterparty_mismatch_settlement_receipts: u64,
2368}
2369
2370#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2371#[serde(rename_all = "camelCase")]
2372pub struct LiabilityClaimWorkflowReport {
2373    pub schema: String,
2374    pub generated_at: u64,
2375    pub query: LiabilityClaimWorkflowQuery,
2376    pub summary: LiabilityClaimWorkflowSummary,
2377    pub claims: Vec<LiabilityClaimWorkflowRow>,
2378}
2379
2380fn validate_currency_code(value: &str, field_name: &str) -> Result<(), String> {
2381    let currency = value.trim().to_ascii_uppercase();
2382    if currency.len() != 3
2383        || !currency
2384            .chars()
2385            .all(|character| character.is_ascii_uppercase())
2386    {
2387        return Err(format!(
2388            "{field_name} must be a three-letter uppercase ISO-style code"
2389        ));
2390    }
2391    Ok(())
2392}
2393
2394fn validate_positive_money(amount: &MonetaryAmount, field_name: &str) -> Result<(), String> {
2395    if amount.units == 0 {
2396        return Err(format!("{field_name} must be greater than zero"));
2397    }
2398    validate_currency_code(&amount.currency, &format!("{field_name} currency"))?;
2399    Ok(())
2400}
2401
2402#[cfg(test)]
2403mod tests {
2404    use super::*;
2405
2406    fn sample_report() -> LiabilityProviderReport {
2407        LiabilityProviderReport {
2408            schema: LIABILITY_PROVIDER_ARTIFACT_SCHEMA.to_string(),
2409            provider_id: "carrier-alpha".to_string(),
2410            display_name: "Carrier Alpha".to_string(),
2411            provider_type: LiabilityProviderType::AdmittedCarrier,
2412            provider_url: Some("https://carrier.example.com".to_string()),
2413            lifecycle_state: LiabilityProviderLifecycleState::Active,
2414            support_boundary: LiabilityProviderSupportBoundary::default(),
2415            policies: vec![LiabilityJurisdictionPolicy {
2416                jurisdiction: "us-ny".to_string(),
2417                coverage_classes: vec![LiabilityCoverageClass::ToolExecution],
2418                supported_currencies: vec!["USD".to_string()],
2419                required_evidence: vec![LiabilityEvidenceRequirement::CreditProviderRiskPackage],
2420                max_coverage_amount: Some(MonetaryAmount {
2421                    units: 50_000,
2422                    currency: "USD".to_string(),
2423                }),
2424                claims_supported: true,
2425                quote_ttl_seconds: 3_600,
2426                notes: None,
2427            }],
2428            provenance: LiabilityProviderProvenance {
2429                configured_by: "operator".to_string(),
2430                configured_at: 1_700_000_000,
2431                source_ref: "compliance-runbook".to_string(),
2432                change_reason: None,
2433            },
2434        }
2435    }
2436
2437    fn sample_risk_package() -> SignedCreditProviderRiskPackage {
2438        let keypair = crate::crypto::Keypair::generate();
2439        let exposure = crate::credit::SignedExposureLedgerReport::sign(
2440            crate::credit::ExposureLedgerReport {
2441                schema: crate::credit::EXPOSURE_LEDGER_SCHEMA.to_string(),
2442                generated_at: 1,
2443                filters: crate::credit::ExposureLedgerQuery {
2444                    agent_subject: Some("subject-1".to_string()),
2445                    ..crate::credit::ExposureLedgerQuery::default()
2446                },
2447                support_boundary: crate::credit::ExposureLedgerSupportBoundary::default(),
2448                summary: crate::credit::ExposureLedgerSummary {
2449                    matching_receipts: 1,
2450                    returned_receipts: 1,
2451                    matching_decisions: 0,
2452                    returned_decisions: 0,
2453                    active_decisions: 0,
2454                    superseded_decisions: 0,
2455                    actionable_receipts: 0,
2456                    pending_settlement_receipts: 0,
2457                    failed_settlement_receipts: 0,
2458                    currencies: vec!["USD".to_string()],
2459                    mixed_currency_book: false,
2460                    truncated_receipts: false,
2461                    truncated_decisions: false,
2462                },
2463                positions: vec![crate::credit::ExposureLedgerCurrencyPosition {
2464                    currency: "USD".to_string(),
2465                    governed_max_exposure_units: 4_000,
2466                    reserved_units: 0,
2467                    settled_units: 4_000,
2468                    pending_units: 0,
2469                    failed_units: 0,
2470                    provisional_loss_units: 0,
2471                    recovered_units: 0,
2472                    quoted_premium_units: 0,
2473                    active_quoted_premium_units: 0,
2474                }],
2475                receipts: Vec::new(),
2476                decisions: Vec::new(),
2477            },
2478            &keypair,
2479        )
2480        .expect("sign exposure");
2481        let scorecard = crate::credit::SignedCreditScorecardReport::sign(
2482            crate::credit::CreditScorecardReport {
2483                schema: crate::credit::CREDIT_SCORECARD_SCHEMA.to_string(),
2484                generated_at: 2,
2485                filters: crate::credit::ExposureLedgerQuery {
2486                    agent_subject: Some("subject-1".to_string()),
2487                    ..crate::credit::ExposureLedgerQuery::default()
2488                },
2489                support_boundary: crate::credit::CreditScorecardSupportBoundary::default(),
2490                summary: crate::credit::CreditScorecardSummary {
2491                    matching_receipts: 1,
2492                    returned_receipts: 1,
2493                    matching_decisions: 0,
2494                    returned_decisions: 0,
2495                    currencies: vec!["USD".to_string()],
2496                    mixed_currency_book: false,
2497                    confidence: crate::credit::CreditScorecardConfidence::High,
2498                    band: crate::credit::CreditScorecardBand::Prime,
2499                    overall_score: 0.95,
2500                    anomaly_count: 0,
2501                    probationary: false,
2502                },
2503                reputation: crate::credit::CreditScorecardReputationContext {
2504                    effective_score: 0.95,
2505                    probationary: false,
2506                    resolved_tier: None,
2507                    imported_signal_count: 0,
2508                    accepted_imported_signal_count: 0,
2509                },
2510                positions: exposure.body.positions.clone(),
2511                probation: crate::credit::CreditScorecardProbationStatus {
2512                    probationary: false,
2513                    reasons: Vec::new(),
2514                    receipt_count: 1,
2515                    span_days: 1,
2516                    target_receipt_count: 1,
2517                    target_span_days: 1,
2518                },
2519                dimensions: Vec::new(),
2520                anomalies: Vec::new(),
2521            },
2522            &keypair,
2523        )
2524        .expect("sign scorecard");
2525
2526        SignedCreditProviderRiskPackage::sign(
2527            crate::credit::CreditProviderRiskPackage {
2528                schema: crate::credit::CREDIT_PROVIDER_RISK_PACKAGE_SCHEMA.to_string(),
2529                generated_at: 3,
2530                subject_key: "subject-1".to_string(),
2531                filters: crate::credit::CreditProviderRiskPackageQuery {
2532                    agent_subject: Some("subject-1".to_string()),
2533                    ..crate::credit::CreditProviderRiskPackageQuery::default()
2534                },
2535                support_boundary: crate::credit::CreditProviderRiskPackageSupportBoundary::default(
2536                ),
2537                exposure,
2538                scorecard,
2539                facility_report: crate::credit::CreditFacilityReport {
2540                    schema: crate::credit::CREDIT_FACILITY_REPORT_SCHEMA.to_string(),
2541                    generated_at: 3,
2542                    filters: crate::credit::ExposureLedgerQuery {
2543                        agent_subject: Some("subject-1".to_string()),
2544                        ..crate::credit::ExposureLedgerQuery::default()
2545                    },
2546                    scorecard: crate::credit::CreditScorecardSummary {
2547                        matching_receipts: 1,
2548                        returned_receipts: 1,
2549                        matching_decisions: 0,
2550                        returned_decisions: 0,
2551                        currencies: vec!["USD".to_string()],
2552                        mixed_currency_book: false,
2553                        confidence: crate::credit::CreditScorecardConfidence::High,
2554                        band: crate::credit::CreditScorecardBand::Prime,
2555                        overall_score: 0.95,
2556                        anomaly_count: 0,
2557                        probationary: false,
2558                    },
2559                    disposition: crate::credit::CreditFacilityDisposition::Grant,
2560                    prerequisites: crate::credit::CreditFacilityPrerequisites {
2561                        minimum_runtime_assurance_tier:
2562                            crate::capability::RuntimeAssuranceTier::Verified,
2563                        runtime_assurance_met: true,
2564                        certification_required: false,
2565                        certification_met: true,
2566                        manual_review_required: false,
2567                    },
2568                    support_boundary: crate::credit::CreditFacilitySupportBoundary::default(),
2569                    terms: Some(crate::credit::CreditFacilityTerms {
2570                        credit_limit: MonetaryAmount {
2571                            units: 4_000,
2572                            currency: "USD".to_string(),
2573                        },
2574                        utilization_ceiling_bps: 8_000,
2575                        reserve_ratio_bps: 1_500,
2576                        concentration_cap_bps: 3_000,
2577                        ttl_seconds: 86_400,
2578                        capital_source:
2579                            crate::credit::CreditFacilityCapitalSource::OperatorInternal,
2580                    }),
2581                    findings: Vec::new(),
2582                },
2583                compliance_score: None,
2584                latest_facility: Some(crate::credit::CreditProviderFacilitySnapshot {
2585                    facility_id: "cfd-1".to_string(),
2586                    issued_at: 3,
2587                    expires_at: 4,
2588                    disposition: crate::credit::CreditFacilityDisposition::Grant,
2589                    lifecycle_state: crate::credit::CreditFacilityLifecycleState::Active,
2590                    credit_limit: Some(MonetaryAmount {
2591                        units: 4_000,
2592                        currency: "USD".to_string(),
2593                    }),
2594                    supersedes_facility_id: None,
2595                    signer_key: keypair.public_key().to_hex(),
2596                }),
2597                runtime_assurance: Some(crate::credit::CreditRuntimeAssuranceState {
2598                    governed_receipts: 1,
2599                    runtime_assurance_receipts: 1,
2600                    highest_tier: Some(crate::capability::RuntimeAssuranceTier::Verified),
2601                    latest_schema: Some("chio.runtime-attestation.azure-maa.jwt.v1".to_string()),
2602                    latest_verifier_family: Some(
2603                        crate::appraisal::AttestationVerifierFamily::AzureMaa,
2604                    ),
2605                    latest_verifier: Some("verifier.chio".to_string()),
2606                    latest_evidence_sha256: Some("sha256-runtime".to_string()),
2607                    observed_verifier_families: vec![
2608                        crate::appraisal::AttestationVerifierFamily::AzureMaa,
2609                    ],
2610                    stale: false,
2611                }),
2612                certification: crate::credit::CreditCertificationState {
2613                    required: false,
2614                    state: None,
2615                    artifact_id: None,
2616                    checked_at: None,
2617                    published_at: None,
2618                },
2619                recent_loss_history: crate::credit::CreditRecentLossHistory {
2620                    summary: crate::credit::CreditRecentLossSummary {
2621                        matching_loss_events: 0,
2622                        returned_loss_events: 0,
2623                        failed_settlement_events: 0,
2624                        provisional_loss_events: 0,
2625                        recovered_events: 0,
2626                    },
2627                    entries: Vec::new(),
2628                },
2629                evidence_refs: Vec::new(),
2630            },
2631            &keypair,
2632        )
2633        .expect("sign risk package")
2634    }
2635
2636    fn sign_export<T>(body: T) -> SignedExportEnvelope<T>
2637    where
2638        T: serde::Serialize + Clone,
2639    {
2640        let keypair = crate::crypto::Keypair::generate();
2641        SignedExportEnvelope::sign(body, &keypair).expect("sign export")
2642    }
2643
2644    fn usd(units: u64) -> MonetaryAmount {
2645        MonetaryAmount {
2646            units,
2647            currency: "USD".to_string(),
2648        }
2649    }
2650
2651    fn sample_provider_policy() -> LiabilityProviderPolicyReference {
2652        let report = sample_report();
2653        let policy = &report.policies[0];
2654        LiabilityProviderPolicyReference {
2655            provider_id: report.provider_id,
2656            provider_record_id: "lpr-1".to_string(),
2657            display_name: report.display_name,
2658            jurisdiction: policy.jurisdiction.clone(),
2659            coverage_class: policy.coverage_classes[0],
2660            currency: "USD".to_string(),
2661            required_evidence: policy.required_evidence.clone(),
2662            max_coverage_amount: policy.max_coverage_amount.clone(),
2663            claims_supported: policy.claims_supported,
2664            quote_ttl_seconds: policy.quote_ttl_seconds,
2665            bound_coverage_supported: true,
2666        }
2667    }
2668
2669    fn sample_quote_request_artifact() -> LiabilityQuoteRequestArtifact {
2670        LiabilityQuoteRequestArtifact {
2671            schema: LIABILITY_QUOTE_REQUEST_ARTIFACT_SCHEMA.to_string(),
2672            quote_request_id: "lqr-1".to_string(),
2673            issued_at: 1_700_000_000,
2674            provider_policy: sample_provider_policy(),
2675            requested_coverage_amount: usd(10_000),
2676            requested_effective_from: 1_700_010_000,
2677            requested_effective_until: 1_700_020_000,
2678            risk_package: sample_risk_package(),
2679            notes: Some("initial market inquiry".to_string()),
2680        }
2681    }
2682
2683    fn sample_quote_response_artifact(
2684        quote_request: SignedLiabilityQuoteRequest,
2685    ) -> LiabilityQuoteResponseArtifact {
2686        LiabilityQuoteResponseArtifact {
2687            schema: LIABILITY_QUOTE_RESPONSE_ARTIFACT_SCHEMA.to_string(),
2688            quote_response_id: "lqp-1".to_string(),
2689            issued_at: quote_request.body.issued_at + 120,
2690            quote_request,
2691            provider_quote_ref: "carrier-alpha-quote".to_string(),
2692            disposition: LiabilityQuoteDisposition::Quoted,
2693            supersedes_quote_response_id: None,
2694            quoted_terms: Some(LiabilityQuoteTerms {
2695                quoted_coverage_amount: usd(10_000),
2696                quoted_premium_amount: usd(500),
2697                quoted_deductible_amount: Some(usd(1_000)),
2698                expires_at: 1_700_003_000,
2699            }),
2700            decline_reason: None,
2701        }
2702    }
2703
2704    fn sample_credit_scorecard_summary() -> crate::credit::CreditScorecardSummary {
2705        crate::credit::CreditScorecardSummary {
2706            matching_receipts: 2,
2707            returned_receipts: 2,
2708            matching_decisions: 1,
2709            returned_decisions: 1,
2710            currencies: vec!["USD".to_string()],
2711            mixed_currency_book: false,
2712            confidence: crate::credit::CreditScorecardConfidence::High,
2713            band: crate::credit::CreditScorecardBand::Prime,
2714            overall_score: 0.94,
2715            anomaly_count: 0,
2716            probationary: false,
2717        }
2718    }
2719
2720    fn sample_credit_facility() -> crate::credit::SignedCreditFacility {
2721        sign_export(crate::credit::CreditFacilityArtifact {
2722            schema: crate::credit::CREDIT_FACILITY_ARTIFACT_SCHEMA.to_string(),
2723            facility_id: "cfd-1".to_string(),
2724            issued_at: 1_700_000_100,
2725            expires_at: 1_700_086_500,
2726            lifecycle_state: crate::credit::CreditFacilityLifecycleState::Active,
2727            supersedes_facility_id: None,
2728            report: crate::credit::CreditFacilityReport {
2729                schema: crate::credit::CREDIT_FACILITY_REPORT_SCHEMA.to_string(),
2730                generated_at: 1_700_000_090,
2731                filters: crate::credit::ExposureLedgerQuery {
2732                    agent_subject: Some("subject-1".to_string()),
2733                    ..crate::credit::ExposureLedgerQuery::default()
2734                },
2735                scorecard: sample_credit_scorecard_summary(),
2736                disposition: crate::credit::CreditFacilityDisposition::Grant,
2737                prerequisites: crate::credit::CreditFacilityPrerequisites {
2738                    minimum_runtime_assurance_tier:
2739                        crate::capability::RuntimeAssuranceTier::Verified,
2740                    runtime_assurance_met: true,
2741                    certification_required: false,
2742                    certification_met: true,
2743                    manual_review_required: false,
2744                },
2745                support_boundary: crate::credit::CreditFacilitySupportBoundary::default(),
2746                terms: Some(crate::credit::CreditFacilityTerms {
2747                    credit_limit: usd(12_000),
2748                    utilization_ceiling_bps: 8_000,
2749                    reserve_ratio_bps: 1_500,
2750                    concentration_cap_bps: 3_000,
2751                    ttl_seconds: 86_400,
2752                    capital_source: crate::credit::CreditFacilityCapitalSource::OperatorInternal,
2753                }),
2754                findings: Vec::new(),
2755            },
2756        })
2757    }
2758
2759    fn sample_underwriting_input() -> crate::underwriting::UnderwritingPolicyInput {
2760        crate::underwriting::UnderwritingPolicyInput {
2761            schema: crate::underwriting::UNDERWRITING_POLICY_INPUT_SCHEMA.to_string(),
2762            generated_at: 1_700_000_120,
2763            filters: crate::underwriting::UnderwritingPolicyInputQuery {
2764                agent_subject: Some("subject-1".to_string()),
2765                ..crate::underwriting::UnderwritingPolicyInputQuery::default()
2766            },
2767            taxonomy: crate::underwriting::UnderwritingRiskTaxonomy::default(),
2768            receipts: crate::underwriting::UnderwritingReceiptEvidence {
2769                matching_receipts: 2,
2770                returned_receipts: 2,
2771                allow_count: 2,
2772                deny_count: 0,
2773                cancelled_count: 0,
2774                incomplete_count: 0,
2775                governed_receipts: 2,
2776                approval_receipts: 1,
2777                approved_receipts: 1,
2778                call_chain_receipts: 0,
2779                runtime_assurance_receipts: 1,
2780                pending_settlement_receipts: 0,
2781                failed_settlement_receipts: 0,
2782                actionable_settlement_receipts: 0,
2783                metered_receipts: 0,
2784                actionable_metered_receipts: 0,
2785                shared_evidence_reference_count: 0,
2786                shared_evidence_proof_required_count: 0,
2787                receipt_refs: Vec::new(),
2788            },
2789            reputation: Some(crate::underwriting::UnderwritingReputationEvidence {
2790                subject_key: "subject-1".to_string(),
2791                effective_score: 0.94,
2792                probationary: false,
2793                resolved_tier: Some("prime".to_string()),
2794                imported_signal_count: 0,
2795                accepted_imported_signal_count: 0,
2796            }),
2797            certification: Some(crate::underwriting::UnderwritingCertificationEvidence {
2798                tool_server_id: "server-1".to_string(),
2799                state: crate::underwriting::UnderwritingCertificationState::Active,
2800                artifact_id: Some("cert-1".to_string()),
2801                verdict: Some("pass".to_string()),
2802                checked_at: Some(1_700_000_110),
2803                published_at: Some(1_700_000_111),
2804            }),
2805            runtime_assurance: Some(crate::underwriting::UnderwritingRuntimeAssuranceEvidence {
2806                governed_receipts: 2,
2807                runtime_assurance_receipts: 1,
2808                highest_tier: Some(crate::capability::RuntimeAssuranceTier::Verified),
2809                latest_schema: Some("chio.runtime-attestation.enterprise.v1".to_string()),
2810                latest_verifier_family: Some(
2811                    crate::appraisal::AttestationVerifierFamily::EnterpriseVerifier,
2812                ),
2813                latest_verifier: Some("verifier.chio".to_string()),
2814                latest_evidence_sha256: Some("sha256-attest".to_string()),
2815                observed_verifier_families: vec![
2816                    crate::appraisal::AttestationVerifierFamily::EnterpriseVerifier,
2817                ],
2818            }),
2819            compliance_score: None,
2820            signals: Vec::new(),
2821        }
2822    }
2823
2824    fn sample_underwriting_decision() -> crate::underwriting::SignedUnderwritingDecision {
2825        sign_export(crate::underwriting::UnderwritingDecisionArtifact {
2826            schema: crate::underwriting::UNDERWRITING_DECISION_ARTIFACT_SCHEMA.to_string(),
2827            decision_id: "uwd-1".to_string(),
2828            issued_at: 1_700_000_130,
2829            evaluation: crate::underwriting::UnderwritingDecisionReport {
2830                schema: crate::underwriting::UNDERWRITING_DECISION_REPORT_SCHEMA.to_string(),
2831                generated_at: 1_700_000_129,
2832                policy: crate::underwriting::UnderwritingDecisionPolicy::default(),
2833                outcome: crate::underwriting::UnderwritingDecisionOutcome::Approve,
2834                risk_class: crate::underwriting::UnderwritingRiskClass::Baseline,
2835                suggested_ceiling_factor: Some(1.0),
2836                findings: Vec::new(),
2837                input: sample_underwriting_input(),
2838            },
2839            lifecycle_state: crate::underwriting::UnderwritingDecisionLifecycleState::Active,
2840            review_state: crate::underwriting::UnderwritingReviewState::Approved,
2841            supersedes_decision_id: None,
2842            budget: crate::underwriting::UnderwritingBudgetRecommendation {
2843                action: crate::underwriting::UnderwritingBudgetAction::Preserve,
2844                ceiling_factor: Some(1.0),
2845                rationale: "approved under baseline risk profile".to_string(),
2846            },
2847            premium: crate::underwriting::UnderwritingPremiumQuote {
2848                state: crate::underwriting::UnderwritingPremiumState::Quoted,
2849                basis_points: Some(500),
2850                quoted_amount: Some(usd(500)),
2851                rationale: "5% premium quote".to_string(),
2852            },
2853        })
2854    }
2855
2856    fn sample_capital_book() -> crate::credit::SignedCapitalBookReport {
2857        sign_export(crate::credit::CapitalBookReport {
2858            schema: crate::credit::CAPITAL_BOOK_REPORT_SCHEMA.to_string(),
2859            generated_at: 1_700_000_140,
2860            query: crate::credit::CapitalBookQuery {
2861                agent_subject: Some("subject-1".to_string()),
2862                ..crate::credit::CapitalBookQuery::default()
2863            },
2864            subject_key: "subject-1".to_string(),
2865            support_boundary: crate::credit::CapitalBookSupportBoundary::default(),
2866            summary: crate::credit::CapitalBookSummary {
2867                matching_receipts: 2,
2868                returned_receipts: 2,
2869                matching_facilities: 1,
2870                returned_facilities: 1,
2871                matching_bonds: 1,
2872                returned_bonds: 1,
2873                matching_loss_events: 1,
2874                returned_loss_events: 1,
2875                currencies: vec!["USD".to_string()],
2876                mixed_currency_book: false,
2877                funding_sources: 1,
2878                ledger_events: 0,
2879                truncated_receipts: false,
2880                truncated_facilities: false,
2881                truncated_bonds: false,
2882                truncated_loss_events: false,
2883            },
2884            sources: vec![crate::credit::CapitalBookSource {
2885                source_id: "facility-source-1".to_string(),
2886                kind: crate::credit::CapitalBookSourceKind::FacilityCommitment,
2887                owner_role: crate::credit::CapitalBookRole::OperatorTreasury,
2888                counterparty_role: crate::credit::CapitalBookRole::AgentCounterparty,
2889                counterparty_id: "subject-1".to_string(),
2890                currency: "USD".to_string(),
2891                jurisdiction: Some("us-ny".to_string()),
2892                capital_source: Some(crate::credit::CreditFacilityCapitalSource::OperatorInternal),
2893                facility_id: Some("cfd-1".to_string()),
2894                bond_id: None,
2895                committed_amount: Some(usd(12_000)),
2896                held_amount: None,
2897                drawn_amount: None,
2898                disbursed_amount: Some(usd(1_000)),
2899                released_amount: None,
2900                repaid_amount: None,
2901                impaired_amount: Some(usd(1_000)),
2902                description: "facility commitment".to_string(),
2903            }],
2904            events: Vec::new(),
2905        })
2906    }
2907
2908    fn sample_exposure_report() -> crate::credit::SignedExposureLedgerReport {
2909        sign_export(crate::credit::ExposureLedgerReport {
2910            schema: crate::credit::EXPOSURE_LEDGER_SCHEMA.to_string(),
2911            generated_at: 1_700_010_350,
2912            filters: crate::credit::ExposureLedgerQuery {
2913                agent_subject: Some("subject-1".to_string()),
2914                ..crate::credit::ExposureLedgerQuery::default()
2915            },
2916            support_boundary: crate::credit::ExposureLedgerSupportBoundary::default(),
2917            summary: crate::credit::ExposureLedgerSummary {
2918                matching_receipts: 2,
2919                returned_receipts: 2,
2920                matching_decisions: 1,
2921                returned_decisions: 1,
2922                active_decisions: 1,
2923                superseded_decisions: 0,
2924                actionable_receipts: 0,
2925                pending_settlement_receipts: 0,
2926                failed_settlement_receipts: 0,
2927                currencies: vec!["USD".to_string()],
2928                mixed_currency_book: false,
2929                truncated_receipts: false,
2930                truncated_decisions: false,
2931            },
2932            positions: vec![crate::credit::ExposureLedgerCurrencyPosition {
2933                currency: "USD".to_string(),
2934                governed_max_exposure_units: 10_000,
2935                reserved_units: 0,
2936                settled_units: 10_000,
2937                pending_units: 0,
2938                failed_units: 0,
2939                provisional_loss_units: 0,
2940                recovered_units: 0,
2941                quoted_premium_units: 500,
2942                active_quoted_premium_units: 500,
2943            }],
2944            receipts: Vec::new(),
2945            decisions: Vec::new(),
2946        })
2947    }
2948
2949    fn sample_credit_bond() -> crate::credit::SignedCreditBond {
2950        sign_export(crate::credit::CreditBondArtifact {
2951            schema: crate::credit::CREDIT_BOND_ARTIFACT_SCHEMA.to_string(),
2952            bond_id: "bond-1".to_string(),
2953            issued_at: 1_700_010_360,
2954            expires_at: 1_700_096_760,
2955            lifecycle_state: crate::credit::CreditBondLifecycleState::Active,
2956            supersedes_bond_id: None,
2957            report: crate::credit::CreditBondReport {
2958                schema: crate::credit::CREDIT_BOND_REPORT_SCHEMA.to_string(),
2959                generated_at: 1_700_010_359,
2960                filters: crate::credit::ExposureLedgerQuery {
2961                    agent_subject: Some("subject-1".to_string()),
2962                    ..crate::credit::ExposureLedgerQuery::default()
2963                },
2964                exposure: sample_exposure_report().body.summary.clone(),
2965                scorecard: sample_credit_scorecard_summary(),
2966                disposition: crate::credit::CreditBondDisposition::Lock,
2967                prerequisites: crate::credit::CreditBondPrerequisites {
2968                    active_facility_required: true,
2969                    active_facility_met: true,
2970                    runtime_assurance_met: true,
2971                    certification_required: false,
2972                    certification_met: true,
2973                    currency_coherent: true,
2974                },
2975                support_boundary: crate::credit::CreditBondSupportBoundary::default(),
2976                latest_facility_id: Some("cfd-1".to_string()),
2977                terms: Some(crate::credit::CreditBondTerms {
2978                    facility_id: "cfd-1".to_string(),
2979                    credit_limit: usd(12_000),
2980                    collateral_amount: usd(6_000),
2981                    reserve_requirement_amount: usd(3_000),
2982                    outstanding_exposure_amount: usd(9_000),
2983                    reserve_ratio_bps: 1_500,
2984                    coverage_ratio_bps: 12_000,
2985                    capital_source: crate::credit::CreditFacilityCapitalSource::OperatorInternal,
2986                }),
2987                findings: Vec::new(),
2988            },
2989        })
2990    }
2991
2992    fn sample_credit_loss_lifecycle() -> crate::credit::SignedCreditLossLifecycle {
2993        sign_export(crate::credit::CreditLossLifecycleArtifact {
2994            schema: crate::credit::CREDIT_LOSS_LIFECYCLE_ARTIFACT_SCHEMA.to_string(),
2995            event_id: "loss-1".to_string(),
2996            issued_at: 1_700_010_370,
2997            bond_id: "bond-1".to_string(),
2998            event_kind: crate::credit::CreditLossLifecycleEventKind::Delinquency,
2999            projected_bond_lifecycle_state: crate::credit::CreditBondLifecycleState::Active,
3000            reserve_control_source_id: None,
3001            authority_chain: Vec::new(),
3002            execution_window: None,
3003            rail: None,
3004            observed_execution: None,
3005            reconciled_state: None,
3006            execution_state: None,
3007            appeal_state: None,
3008            appeal_window_ends_at: None,
3009            description: Some("claim loss marker".to_string()),
3010            report: crate::credit::CreditLossLifecycleReport {
3011                schema: crate::credit::CREDIT_LOSS_LIFECYCLE_REPORT_SCHEMA.to_string(),
3012                generated_at: 1_700_010_369,
3013                query: crate::credit::CreditLossLifecycleQuery {
3014                    bond_id: "bond-1".to_string(),
3015                    event_kind: crate::credit::CreditLossLifecycleEventKind::Delinquency,
3016                    amount: Some(usd(1_000)),
3017                },
3018                summary: crate::credit::CreditLossLifecycleSummary {
3019                    bond_id: "bond-1".to_string(),
3020                    facility_id: Some("cfd-1".to_string()),
3021                    capability_id: Some("cap-1".to_string()),
3022                    agent_subject: Some("subject-1".to_string()),
3023                    tool_server: Some("server-1".to_string()),
3024                    tool_name: Some("tool-a".to_string()),
3025                    current_bond_lifecycle_state: crate::credit::CreditBondLifecycleState::Active,
3026                    projected_bond_lifecycle_state: crate::credit::CreditBondLifecycleState::Active,
3027                    current_delinquent_amount: Some(usd(1_000)),
3028                    current_recovered_amount: None,
3029                    current_written_off_amount: None,
3030                    current_released_reserve_amount: None,
3031                    current_slashed_reserve_amount: None,
3032                    outstanding_delinquent_amount: Some(usd(1_000)),
3033                    releaseable_reserve_amount: Some(usd(2_000)),
3034                    reserve_control_source_id: None,
3035                    execution_state: None,
3036                    appeal_state: None,
3037                    appeal_window_ends_at: None,
3038                    event_amount: Some(usd(1_000)),
3039                },
3040                support_boundary: crate::credit::CreditLossLifecycleSupportBoundary::default(),
3041                findings: Vec::new(),
3042            },
3043        })
3044    }
3045
3046    #[derive(Clone)]
3047    struct MarketFixtures {
3048        quote_response: SignedLiabilityQuoteResponse,
3049        pricing_authority: SignedLiabilityPricingAuthority,
3050        placement: SignedLiabilityPlacement,
3051        bound_coverage: SignedLiabilityBoundCoverage,
3052        claim_package: SignedLiabilityClaimPackage,
3053        claim_response: SignedLiabilityClaimResponse,
3054        claim_dispute: SignedLiabilityClaimDispute,
3055        claim_adjudication: SignedLiabilityClaimAdjudication,
3056        payout_instruction: SignedLiabilityClaimPayoutInstruction,
3057        payout_receipt: SignedLiabilityClaimPayoutReceipt,
3058        settlement_instruction: SignedLiabilityClaimSettlementInstruction,
3059        settlement_receipt: SignedLiabilityClaimSettlementReceipt,
3060    }
3061
3062    fn sample_market_fixtures() -> MarketFixtures {
3063        let quote_request = sign_export(sample_quote_request_artifact());
3064        let quote_response = sign_export(sample_quote_response_artifact(quote_request.clone()));
3065        let capital_book = sample_capital_book();
3066        let pricing_authority = sign_export(LiabilityPricingAuthorityArtifact {
3067            schema: LIABILITY_PRICING_AUTHORITY_ARTIFACT_SCHEMA.to_string(),
3068            authority_id: "lpa-1".to_string(),
3069            issued_at: 1_700_000_150,
3070            quote_request: quote_request.clone(),
3071            provider_policy: quote_request.body.provider_policy.clone(),
3072            facility: sample_credit_facility(),
3073            underwriting_decision: sample_underwriting_decision(),
3074            capital_book: capital_book.clone(),
3075            envelope: LiabilityPricingAuthorityEnvelope {
3076                kind: LiabilityPricingAuthorityEnvelopeKind::ProviderDelegate,
3077                delegate_id: "pricing-delegate-1".to_string(),
3078                regulated_role: None,
3079                authority_chain_ref: Some("auth-chain-1".to_string()),
3080            },
3081            max_coverage_amount: usd(10_000),
3082            max_premium_amount: usd(500),
3083            expires_at: 1_700_002_000,
3084            auto_bind_enabled: true,
3085            notes: Some("carrier delegated pricing authority".to_string()),
3086        });
3087        let placement = sign_export(LiabilityPlacementArtifact {
3088            schema: LIABILITY_PLACEMENT_ARTIFACT_SCHEMA.to_string(),
3089            placement_id: "lpl-1".to_string(),
3090            issued_at: 1_700_000_160,
3091            quote_response: quote_response.clone(),
3092            selected_coverage_amount: usd(10_000),
3093            selected_premium_amount: usd(500),
3094            effective_from: quote_response
3095                .body
3096                .quote_request
3097                .body
3098                .requested_effective_from,
3099            effective_until: quote_response
3100                .body
3101                .quote_request
3102                .body
3103                .requested_effective_until,
3104            placement_ref: Some("placement-ref-1".to_string()),
3105            notes: None,
3106        });
3107        let bound_coverage = sign_export(LiabilityBoundCoverageArtifact {
3108            schema: LIABILITY_BOUND_COVERAGE_ARTIFACT_SCHEMA.to_string(),
3109            bound_coverage_id: "lbc-1".to_string(),
3110            issued_at: 1_700_000_170,
3111            placement: placement.clone(),
3112            policy_number: "POL-Chio-1".to_string(),
3113            carrier_reference: Some("carrier-ref-1".to_string()),
3114            bound_at: 1_700_000_171,
3115            effective_from: placement.body.effective_from,
3116            effective_until: placement.body.effective_until,
3117            coverage_amount: placement.body.selected_coverage_amount.clone(),
3118            premium_amount: placement.body.selected_premium_amount.clone(),
3119        });
3120        let claim_package = sign_export(LiabilityClaimPackageArtifact {
3121            schema: LIABILITY_CLAIM_PACKAGE_ARTIFACT_SCHEMA.to_string(),
3122            claim_id: "clm-1".to_string(),
3123            issued_at: 1_700_010_400,
3124            bound_coverage: bound_coverage.clone(),
3125            exposure: sample_exposure_report(),
3126            bond: sample_credit_bond(),
3127            loss_event: sample_credit_loss_lifecycle(),
3128            claimant: "subject-1".to_string(),
3129            claim_event_at: 1_700_010_500,
3130            claim_amount: usd(9_000),
3131            claim_ref: Some("claim-ref-1".to_string()),
3132            narrative: "tool execution loss".to_string(),
3133            receipt_ids: vec!["rcpt-1".to_string(), "rcpt-2".to_string()],
3134            evidence_refs: Vec::new(),
3135        });
3136        let claim_response = sign_export(LiabilityClaimResponseArtifact {
3137            schema: LIABILITY_CLAIM_RESPONSE_ARTIFACT_SCHEMA.to_string(),
3138            claim_response_id: "clr-1".to_string(),
3139            issued_at: 1_700_010_600,
3140            claim: claim_package.clone(),
3141            provider_response_ref: "provider-claim-1".to_string(),
3142            disposition: LiabilityClaimResponseDisposition::Accepted,
3143            covered_amount: Some(usd(7_000)),
3144            response_note: Some("partial acceptance".to_string()),
3145            denial_reason: None,
3146            evidence_refs: Vec::new(),
3147        });
3148        let claim_dispute = sign_export(LiabilityClaimDisputeArtifact {
3149            schema: LIABILITY_CLAIM_DISPUTE_ARTIFACT_SCHEMA.to_string(),
3150            dispute_id: "cld-1".to_string(),
3151            issued_at: 1_700_010_700,
3152            provider_response: claim_response.clone(),
3153            opened_by: "subject-1".to_string(),
3154            reason: "remaining uncovered amount disputed".to_string(),
3155            note: None,
3156            evidence_refs: Vec::new(),
3157        });
3158        let claim_adjudication = sign_export(LiabilityClaimAdjudicationArtifact {
3159            schema: LIABILITY_CLAIM_ADJUDICATION_ARTIFACT_SCHEMA.to_string(),
3160            adjudication_id: "cla-1".to_string(),
3161            issued_at: 1_700_010_800,
3162            dispute: claim_dispute.clone(),
3163            adjudicator: "arbiter.chio".to_string(),
3164            outcome: LiabilityClaimAdjudicationOutcome::PartialSettlement,
3165            awarded_amount: Some(usd(6_000)),
3166            note: Some("partial settlement ordered".to_string()),
3167            evidence_refs: Vec::new(),
3168        });
3169        let capital_instruction = sign_export(crate::credit::CapitalExecutionInstructionArtifact {
3170            schema: crate::credit::CAPITAL_EXECUTION_INSTRUCTION_ARTIFACT_SCHEMA.to_string(),
3171            instruction_id: "cei-1".to_string(),
3172            issued_at: 1_700_010_850,
3173            query: crate::credit::CapitalBookQuery {
3174                agent_subject: Some("subject-1".to_string()),
3175                ..crate::credit::CapitalBookQuery::default()
3176            },
3177            subject_key: "subject-1".to_string(),
3178            source_id: "facility-source-1".to_string(),
3179            source_kind: crate::credit::CapitalBookSourceKind::FacilityCommitment,
3180            governed_receipt_id: Some("rc-1".to_string()),
3181            completion_flow_row_id: Some("economic-completion-flow:rc-1".to_string()),
3182            action: crate::credit::CapitalExecutionInstructionAction::TransferFunds,
3183            owner_role: crate::credit::CapitalExecutionRole::FacilityProvider,
3184            counterparty_role: crate::credit::CapitalExecutionRole::AgentCounterparty,
3185            counterparty_id: "subject-1".to_string(),
3186            amount: Some(usd(6_000)),
3187            authority_chain: Vec::new(),
3188            execution_window: crate::credit::CapitalExecutionWindow {
3189                not_before: 1_700_010_850,
3190                not_after: 1_700_011_200,
3191            },
3192            rail: crate::credit::CapitalExecutionRail {
3193                kind: crate::credit::CapitalExecutionRailKind::Api,
3194                rail_id: "rail-1".to_string(),
3195                custody_provider_id: "custody-1".to_string(),
3196                source_account_ref: None,
3197                destination_account_ref: None,
3198                jurisdiction: Some("us-ny".to_string()),
3199            },
3200            intended_state: crate::credit::CapitalExecutionIntendedState::PendingExecution,
3201            reconciled_state: crate::credit::CapitalExecutionReconciledState::NotObserved,
3202            related_instruction_id: None,
3203            observed_execution: None,
3204            support_boundary: crate::credit::CapitalExecutionInstructionSupportBoundary::default(),
3205            evidence_refs: Vec::new(),
3206            description: "claim payout transfer".to_string(),
3207        });
3208        let payout_instruction = sign_export(LiabilityClaimPayoutInstructionArtifact {
3209            schema: LIABILITY_CLAIM_PAYOUT_INSTRUCTION_ARTIFACT_SCHEMA.to_string(),
3210            payout_instruction_id: "cpi-1".to_string(),
3211            issued_at: 1_700_010_900,
3212            adjudication: claim_adjudication.clone(),
3213            capital_instruction: capital_instruction.clone(),
3214            payout_amount: usd(6_000),
3215            note: None,
3216        });
3217        let payout_receipt = sign_export(LiabilityClaimPayoutReceiptArtifact {
3218            schema: LIABILITY_CLAIM_PAYOUT_RECEIPT_ARTIFACT_SCHEMA.to_string(),
3219            payout_receipt_id: "cpr-1".to_string(),
3220            issued_at: 1_700_011_000,
3221            payout_instruction: payout_instruction.clone(),
3222            payout_receipt_ref: "payout-receipt-1".to_string(),
3223            reconciliation_state: LiabilityClaimPayoutReconciliationState::Matched,
3224            observed_execution: crate::credit::CapitalExecutionObservation {
3225                observed_at: 1_700_011_000,
3226                external_reference_id: "exec-1".to_string(),
3227                amount: usd(6_000),
3228            },
3229            note: None,
3230        });
3231        let settlement_instruction = sign_export(LiabilityClaimSettlementInstructionArtifact {
3232            schema: LIABILITY_CLAIM_SETTLEMENT_INSTRUCTION_ARTIFACT_SCHEMA.to_string(),
3233            settlement_instruction_id: "csi-1".to_string(),
3234            issued_at: 1_700_011_100,
3235            payout_receipt: payout_receipt.clone(),
3236            capital_book: capital_book.clone(),
3237            settlement_kind: LiabilityClaimSettlementKind::FacilityReimbursement,
3238            settlement_amount: usd(5_000),
3239            topology: LiabilityClaimSettlementRoleTopology {
3240                payer: LiabilityClaimSettlementRoleBinding {
3241                    role: crate::credit::CapitalExecutionRole::FacilityProvider,
3242                    party_id: "facility-provider-1".to_string(),
3243                    jurisdiction: Some("us-ny".to_string()),
3244                    note: None,
3245                },
3246                payee: LiabilityClaimSettlementRoleBinding {
3247                    role: crate::credit::CapitalExecutionRole::AgentCounterparty,
3248                    party_id: "subject-1".to_string(),
3249                    jurisdiction: Some("us-ny".to_string()),
3250                    note: None,
3251                },
3252                beneficiary: None,
3253            },
3254            authority_chain: vec![
3255                crate::credit::CapitalExecutionAuthorityStep {
3256                    role: crate::credit::CapitalExecutionRole::FacilityProvider,
3257                    principal_id: "facility-provider-1".to_string(),
3258                    approved_at: 1_700_011_050,
3259                    expires_at: 1_700_011_600,
3260                    note: None,
3261                },
3262                crate::credit::CapitalExecutionAuthorityStep {
3263                    role: crate::credit::CapitalExecutionRole::Custodian,
3264                    principal_id: "custody-1".to_string(),
3265                    approved_at: 1_700_011_050,
3266                    expires_at: 1_700_011_600,
3267                    note: None,
3268                },
3269            ],
3270            execution_window: crate::credit::CapitalExecutionWindow {
3271                not_before: 1_700_011_100,
3272                not_after: 1_700_011_500,
3273            },
3274            rail: crate::credit::CapitalExecutionRail {
3275                kind: crate::credit::CapitalExecutionRailKind::Ach,
3276                rail_id: "ach-1".to_string(),
3277                custody_provider_id: "custody-1".to_string(),
3278                source_account_ref: None,
3279                destination_account_ref: None,
3280                jurisdiction: Some("us-ny".to_string()),
3281            },
3282            settlement_reference: Some("settle-1".to_string()),
3283            note: None,
3284        });
3285        let settlement_receipt = sign_export(LiabilityClaimSettlementReceiptArtifact {
3286            schema: LIABILITY_CLAIM_SETTLEMENT_RECEIPT_ARTIFACT_SCHEMA.to_string(),
3287            settlement_receipt_id: "csr-1".to_string(),
3288            issued_at: 1_700_011_200,
3289            settlement_instruction: settlement_instruction.clone(),
3290            settlement_receipt_ref: "settlement-receipt-1".to_string(),
3291            reconciliation_state: LiabilityClaimSettlementReconciliationState::Matched,
3292            observed_execution: crate::credit::CapitalExecutionObservation {
3293                observed_at: 1_700_011_200,
3294                external_reference_id: "settle-exec-1".to_string(),
3295                amount: usd(5_000),
3296            },
3297            observed_payer_id: "facility-provider-1".to_string(),
3298            observed_payee_id: "subject-1".to_string(),
3299            note: None,
3300        });
3301
3302        MarketFixtures {
3303            quote_response,
3304            pricing_authority,
3305            placement,
3306            bound_coverage,
3307            claim_package,
3308            claim_response,
3309            claim_dispute,
3310            claim_adjudication,
3311            payout_instruction,
3312            payout_receipt,
3313            settlement_instruction,
3314            settlement_receipt,
3315        }
3316    }
3317
3318    #[test]
3319    fn liability_provider_report_rejects_duplicate_jurisdictions() {
3320        let mut report = sample_report();
3321        report.policies.push(report.policies[0].clone());
3322        let error = report
3323            .validate()
3324            .expect_err("duplicate jurisdiction rejected");
3325        assert!(error.contains("duplicate jurisdiction policy"));
3326    }
3327
3328    #[test]
3329    fn liability_provider_report_rejects_invalid_currency() {
3330        let mut report = sample_report();
3331        report.policies[0].supported_currencies = vec!["usdollars".to_string()];
3332        let error = report.validate().expect_err("invalid currency rejected");
3333        assert!(error.contains("invalid currency"));
3334    }
3335
3336    #[test]
3337    fn liability_provider_resolution_query_normalizes_fields() {
3338        let query = LiabilityProviderResolutionQuery {
3339            provider_id: " carrier-alpha ".to_string(),
3340            jurisdiction: "US-NY".to_string(),
3341            coverage_class: LiabilityCoverageClass::ToolExecution,
3342            currency: "usd".to_string(),
3343        }
3344        .normalized();
3345
3346        assert_eq!(query.provider_id, "carrier-alpha");
3347        assert_eq!(query.jurisdiction, "us-ny");
3348        assert_eq!(query.currency, "USD");
3349    }
3350
3351    #[test]
3352    fn liability_quote_request_rejects_currency_mismatch() {
3353        let report = sample_report();
3354        let request = LiabilityQuoteRequestArtifact {
3355            schema: LIABILITY_QUOTE_REQUEST_ARTIFACT_SCHEMA.to_string(),
3356            quote_request_id: "lqr-test".to_string(),
3357            issued_at: 1_700_000_000,
3358            provider_policy: LiabilityProviderPolicyReference {
3359                provider_id: report.provider_id.clone(),
3360                provider_record_id: "lpr-test".to_string(),
3361                display_name: report.display_name.clone(),
3362                jurisdiction: "us-ny".to_string(),
3363                coverage_class: LiabilityCoverageClass::ToolExecution,
3364                currency: "USD".to_string(),
3365                required_evidence: vec![LiabilityEvidenceRequirement::CreditProviderRiskPackage],
3366                max_coverage_amount: Some(MonetaryAmount {
3367                    units: 50_000,
3368                    currency: "USD".to_string(),
3369                }),
3370                claims_supported: true,
3371                quote_ttl_seconds: 3_600,
3372                bound_coverage_supported: true,
3373            },
3374            requested_coverage_amount: MonetaryAmount {
3375                units: 10_000,
3376                currency: "EUR".to_string(),
3377            },
3378            requested_effective_from: 1_700_010_000,
3379            requested_effective_until: 1_700_020_000,
3380            risk_package: sample_risk_package(),
3381            notes: None,
3382        };
3383
3384        let error = request.validate().expect_err("currency mismatch rejected");
3385        assert!(error.contains("currency must match provider policy currency"));
3386    }
3387
3388    #[test]
3389    fn liability_market_workflow_query_normalizes_fields() {
3390        let query = LiabilityMarketWorkflowQuery {
3391            quote_request_id: Some(" q-1 ".to_string()),
3392            provider_id: Some(" carrier-alpha ".to_string()),
3393            agent_subject: Some(" subject-1 ".to_string()),
3394            jurisdiction: Some("US-NY".to_string()),
3395            coverage_class: Some(LiabilityCoverageClass::ToolExecution),
3396            currency: Some("usd".to_string()),
3397            limit: Some(500),
3398        }
3399        .normalized();
3400
3401        assert_eq!(query.quote_request_id.as_deref(), Some("q-1"));
3402        assert_eq!(query.provider_id.as_deref(), Some("carrier-alpha"));
3403        assert_eq!(query.agent_subject.as_deref(), Some("subject-1"));
3404        assert_eq!(query.jurisdiction.as_deref(), Some("us-ny"));
3405        assert_eq!(query.currency.as_deref(), Some("USD"));
3406        assert_eq!(query.limit, Some(MAX_LIABILITY_MARKET_WORKFLOW_LIMIT));
3407    }
3408
3409    #[test]
3410    fn liability_provider_list_query_normalizes_and_clamps_fields() {
3411        let query = LiabilityProviderListQuery {
3412            provider_id: Some("carrier-alpha".to_string()),
3413            jurisdiction: Some(" US-NY ".to_string()),
3414            coverage_class: Some(LiabilityCoverageClass::ToolExecution),
3415            currency: Some(" usd ".to_string()),
3416            lifecycle_state: Some(LiabilityProviderLifecycleState::Active),
3417            limit: Some(500),
3418        }
3419        .normalized();
3420
3421        assert_eq!(query.jurisdiction.as_deref(), Some("us-ny"));
3422        assert_eq!(query.currency.as_deref(), Some("USD"));
3423        assert_eq!(query.limit, Some(MAX_LIABILITY_PROVIDER_LIST_LIMIT));
3424    }
3425
3426    #[test]
3427    fn liability_provider_resolution_query_rejects_invalid_currency() {
3428        let error = LiabilityProviderResolutionQuery {
3429            provider_id: "carrier-alpha".to_string(),
3430            jurisdiction: "us-ny".to_string(),
3431            coverage_class: LiabilityCoverageClass::ToolExecution,
3432            currency: "usdollars".to_string(),
3433        }
3434        .validate()
3435        .expect_err("invalid currency rejected");
3436
3437        assert!(error.contains("three-letter uppercase"));
3438    }
3439
3440    #[test]
3441    fn liability_pricing_authority_envelope_requires_regulated_role() {
3442        let error = LiabilityPricingAuthorityEnvelope {
3443            kind: LiabilityPricingAuthorityEnvelopeKind::RegulatedRole,
3444            delegate_id: "delegate-1".to_string(),
3445            regulated_role: None,
3446            authority_chain_ref: None,
3447        }
3448        .validate()
3449        .expect_err("regulated role required");
3450
3451        assert!(error.contains("regulated_role"));
3452    }
3453
3454    #[test]
3455    fn liability_quote_response_validates_quoted_terms_path() {
3456        let fixtures = sample_market_fixtures();
3457        assert!(fixtures.quote_response.body.validate().is_ok());
3458    }
3459
3460    #[test]
3461    fn liability_quote_response_declined_requires_reason() {
3462        let fixtures = sample_market_fixtures();
3463        let mut response = fixtures.quote_response.body.clone();
3464        response.disposition = LiabilityQuoteDisposition::Declined;
3465        response.quoted_terms = None;
3466        response.decline_reason = Some("   ".to_string());
3467
3468        let error = response
3469            .validate()
3470            .expect_err("declined response requires reason");
3471        assert!(error.contains("declined quote responses require decline_reason"));
3472    }
3473
3474    #[test]
3475    fn liability_pricing_authority_validates_happy_path() {
3476        let fixtures = sample_market_fixtures();
3477        assert!(fixtures.pricing_authority.body.validate().is_ok());
3478    }
3479
3480    #[test]
3481    fn liability_pricing_authority_rejects_auto_bind_without_claim_support() {
3482        let fixtures = sample_market_fixtures();
3483        let mut authority = fixtures.pricing_authority.body.clone();
3484        let mut quote_request = authority.quote_request.body.clone();
3485        quote_request.provider_policy.claims_supported = false;
3486        authority.quote_request = sign_export(quote_request);
3487        authority.provider_policy = authority.quote_request.body.provider_policy.clone();
3488
3489        let error = authority
3490            .validate()
3491            .expect_err("auto-bind requires claim support");
3492        assert!(error.contains("cannot enable auto_bind"));
3493    }
3494
3495    #[test]
3496    fn liability_placement_rejects_expired_quote() {
3497        let fixtures = sample_market_fixtures();
3498        let mut placement = fixtures.placement.body.clone();
3499        placement.issued_at = placement
3500            .quote_response
3501            .body
3502            .quoted_terms
3503            .as_ref()
3504            .expect("quoted terms")
3505            .expires_at
3506            + 1;
3507
3508        let error = placement.validate().expect_err("expired quote rejected");
3509        assert!(error.contains("cannot be issued after the quote expires"));
3510    }
3511
3512    #[test]
3513    fn liability_bound_coverage_rejects_provider_without_bound_coverage() {
3514        let fixtures = sample_market_fixtures();
3515        let mut coverage = fixtures.bound_coverage.body.clone();
3516        let mut placement = coverage.placement.body.clone();
3517        let mut quote_response = placement.quote_response.body.clone();
3518        let mut quote_request = quote_response.quote_request.body.clone();
3519        quote_request.provider_policy.bound_coverage_supported = false;
3520        quote_response.quote_request = sign_export(quote_request);
3521        placement.quote_response = sign_export(quote_response);
3522        coverage.placement = sign_export(placement);
3523
3524        let error = coverage
3525            .validate()
3526            .expect_err("provider must support bound coverage");
3527        assert!(error.contains("does not support bound coverage"));
3528    }
3529
3530    #[test]
3531    fn liability_auto_bind_decision_validates_auto_bound_flow() {
3532        let fixtures = sample_market_fixtures();
3533        let decision = LiabilityAutoBindDecisionArtifact {
3534            schema: LIABILITY_AUTO_BIND_DECISION_ARTIFACT_SCHEMA.to_string(),
3535            decision_id: "abd-1".to_string(),
3536            issued_at: 1_700_000_180,
3537            authority: fixtures.pricing_authority,
3538            quote_response: fixtures.quote_response,
3539            disposition: LiabilityAutoBindDisposition::AutoBound,
3540            findings: Vec::new(),
3541            placement: Some(fixtures.placement),
3542            bound_coverage: Some(fixtures.bound_coverage),
3543        };
3544
3545        assert!(decision.validate().is_ok());
3546    }
3547
3548    #[test]
3549    fn liability_auto_bind_decision_rejects_manual_review_with_embedded_artifacts() {
3550        let fixtures = sample_market_fixtures();
3551        let decision = LiabilityAutoBindDecisionArtifact {
3552            schema: LIABILITY_AUTO_BIND_DECISION_ARTIFACT_SCHEMA.to_string(),
3553            decision_id: "abd-1".to_string(),
3554            issued_at: 1_700_000_180,
3555            authority: fixtures.pricing_authority,
3556            quote_response: fixtures.quote_response,
3557            disposition: LiabilityAutoBindDisposition::ManualReview,
3558            findings: Vec::new(),
3559            placement: Some(fixtures.placement),
3560            bound_coverage: Some(fixtures.bound_coverage),
3561        };
3562
3563        let error = decision
3564            .validate()
3565            .expect_err("manual review cannot embed issued artifacts");
3566        assert!(error.contains("cannot embed issued placement or bound coverage"));
3567    }
3568
3569    #[test]
3570    fn liability_claim_package_rejects_duplicate_receipts() {
3571        let fixtures = sample_market_fixtures();
3572        let mut claim = fixtures.claim_package.body.clone();
3573        claim.receipt_ids = vec!["rcpt-1".to_string(), "rcpt-1".to_string()];
3574
3575        let error = claim
3576            .validate()
3577            .expect_err("duplicate receipt ids rejected");
3578        assert!(error.contains("receipt references must be unique"));
3579    }
3580
3581    #[test]
3582    fn liability_claim_response_rejects_denied_without_reason() {
3583        let fixtures = sample_market_fixtures();
3584        let mut response = fixtures.claim_response.body.clone();
3585        response.disposition = LiabilityClaimResponseDisposition::Denied;
3586        response.covered_amount = None;
3587        response.denial_reason = None;
3588
3589        let error = response
3590            .validate()
3591            .expect_err("denied responses require reason");
3592        assert!(error.contains("denied claim responses require denial_reason"));
3593    }
3594
3595    #[test]
3596    fn liability_claim_dispute_rejects_fully_accepted_response() {
3597        let fixtures = sample_market_fixtures();
3598        let mut dispute = fixtures.claim_dispute.body.clone();
3599        dispute.provider_response.body.covered_amount = Some(
3600            dispute
3601                .provider_response
3602                .body
3603                .claim
3604                .body
3605                .claim_amount
3606                .clone(),
3607        );
3608
3609        let error = dispute
3610            .validate()
3611            .expect_err("fully accepted response cannot be disputed");
3612        assert!(error.contains("denied or partially accepted"));
3613    }
3614
3615    #[test]
3616    fn liability_claim_adjudication_rejects_partial_settlement_at_full_amount() {
3617        let fixtures = sample_market_fixtures();
3618        let mut adjudication = fixtures.claim_adjudication.body.clone();
3619        adjudication.awarded_amount = Some(
3620            adjudication
3621                .dispute
3622                .body
3623                .provider_response
3624                .body
3625                .claim
3626                .body
3627                .claim_amount
3628                .clone(),
3629        );
3630
3631        let error = adjudication
3632            .validate()
3633            .expect_err("partial settlement must be less than full claim");
3634        assert!(error.contains("must be less than claim_amount"));
3635    }
3636
3637    #[test]
3638    fn liability_claim_workflow_query_normalizes_and_clamps_fields() {
3639        let query = LiabilityClaimWorkflowQuery {
3640            claim_id: Some(" clm-1 ".to_string()),
3641            provider_id: Some(" carrier-alpha ".to_string()),
3642            agent_subject: Some(" subject-1 ".to_string()),
3643            jurisdiction: Some("US-NY".to_string()),
3644            policy_number: Some(" POL-Chio-1 ".to_string()),
3645            limit: Some(500),
3646        }
3647        .normalized();
3648
3649        assert_eq!(query.claim_id.as_deref(), Some("clm-1"));
3650        assert_eq!(query.provider_id.as_deref(), Some("carrier-alpha"));
3651        assert_eq!(query.agent_subject.as_deref(), Some("subject-1"));
3652        assert_eq!(query.jurisdiction.as_deref(), Some("us-ny"));
3653        assert_eq!(query.policy_number.as_deref(), Some("POL-Chio-1"));
3654        assert_eq!(query.limit, Some(MAX_LIABILITY_CLAIM_WORKFLOW_LIMIT));
3655    }
3656
3657    #[test]
3658    fn liability_claim_payout_instruction_validates_transfer_flow() {
3659        let fixtures = sample_market_fixtures();
3660        assert!(fixtures.payout_instruction.body.validate().is_ok());
3661    }
3662
3663    #[test]
3664    fn liability_claim_payout_instruction_rejects_observed_capital_instruction() {
3665        let fixtures = sample_market_fixtures();
3666        let mut payout = fixtures.payout_instruction.body.clone();
3667        let mut capital_instruction = payout.capital_instruction.body.clone();
3668        capital_instruction.observed_execution = Some(crate::credit::CapitalExecutionObservation {
3669            observed_at: 1_700_011_000,
3670            external_reference_id: "exec-early".to_string(),
3671            amount: usd(6_000),
3672        });
3673        capital_instruction.reconciled_state =
3674            crate::credit::CapitalExecutionReconciledState::Matched;
3675        payout.capital_instruction = sign_export(capital_instruction);
3676
3677        let error = payout
3678            .validate()
3679            .expect_err("observed capital instruction should be rejected");
3680        assert!(error.contains("require an unreconciled capital_instruction"));
3681    }
3682
3683    #[test]
3684    fn liability_claim_payout_receipt_rejects_matched_amount_mismatch() {
3685        let fixtures = sample_market_fixtures();
3686        let mut receipt = fixtures.payout_receipt.body.clone();
3687        receipt.observed_execution.amount = usd(5_500);
3688
3689        let error = receipt
3690            .validate()
3691            .expect_err("matched payouts require identical amount");
3692        assert!(error.contains("observed_execution amount to match payout_amount"));
3693    }
3694
3695    #[test]
3696    fn liability_claim_settlement_instruction_validates_topology_and_authority_chain() {
3697        let fixtures = sample_market_fixtures();
3698        assert!(fixtures.settlement_instruction.body.validate().is_ok());
3699    }
3700
3701    #[test]
3702    fn liability_claim_settlement_instruction_rejects_missing_custodian_approval() {
3703        let fixtures = sample_market_fixtures();
3704        let mut instruction = fixtures.settlement_instruction.body.clone();
3705        instruction
3706            .authority_chain
3707            .retain(|step| step.role != crate::credit::CapitalExecutionRole::Custodian);
3708
3709        let error = instruction
3710            .validate()
3711            .expect_err("custodian approval required");
3712        assert!(error.contains("missing the custody-provider execution step"));
3713    }
3714
3715    #[test]
3716    fn liability_claim_settlement_receipt_rejects_counterparty_match_in_mismatch_state() {
3717        let fixtures = sample_market_fixtures();
3718        let mut receipt = fixtures.settlement_receipt.body.clone();
3719        receipt.reconciliation_state =
3720            LiabilityClaimSettlementReconciliationState::CounterpartyMismatch;
3721
3722        let error = receipt
3723            .validate()
3724            .expect_err("counterparty mismatch requires differing counterparties");
3725        assert!(error.contains("require at least one observed counterparty to differ"));
3726    }
3727}