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}