Skip to main content

chio_store_sqlite/receipt_store/
liability_claims.rs

1use super::*;
2
3impl SqliteReceiptStore {
4    pub fn record_liability_claim_package(
5        &mut self,
6        claim: &SignedLiabilityClaimPackage,
7    ) -> Result<(), ReceiptStoreError> {
8        if !claim
9            .verify_signature()
10            .map_err(|error| ReceiptStoreError::Canonical(error.to_string()))?
11        {
12            return Err(ReceiptStoreError::Conflict(
13                "liability claim package signature verification failed".to_string(),
14            ));
15        }
16        claim.body.validate().map_err(ReceiptStoreError::Conflict)?;
17
18        let artifact = &claim.body;
19        let mut connection = self.connection()?;
20        let tx = connection.transaction()?;
21        let existing = tx
22            .query_row(
23                "SELECT claim_id FROM liability_claim_packages WHERE claim_id = ?1",
24                params![artifact.claim_id],
25                |row| row.get::<_, String>(0),
26            )
27            .optional()?;
28        if existing.is_some() {
29            return Err(ReceiptStoreError::Conflict(format!(
30                "liability claim package `{}` already exists",
31                artifact.claim_id
32            )));
33        }
34
35        let stored_bound_raw_json = tx
36            .query_row(
37                "SELECT raw_json
38                 FROM liability_bound_coverages
39                 WHERE bound_coverage_id = ?1",
40                params![artifact.bound_coverage.body.bound_coverage_id],
41                |row| row.get::<_, String>(0),
42            )
43            .optional()?
44            .ok_or_else(|| {
45                ReceiptStoreError::NotFound(format!(
46                    "liability bound coverage `{}` not found",
47                    artifact.bound_coverage.body.bound_coverage_id
48                ))
49            })?;
50        let stored_bound: SignedLiabilityBoundCoverage =
51            serde_json::from_str(&stored_bound_raw_json)?;
52        if stored_bound.body != artifact.bound_coverage.body {
53            return Err(ReceiptStoreError::Conflict(
54                "liability claim package bound_coverage does not match the persisted bound coverage"
55                    .to_string(),
56            ));
57        }
58
59        let stored_bond_raw_json = tx
60            .query_row(
61                "SELECT raw_json
62                 FROM credit_bonds
63                 WHERE bond_id = ?1",
64                params![artifact.bond.body.bond_id],
65                |row| row.get::<_, String>(0),
66            )
67            .optional()?
68            .ok_or_else(|| {
69                ReceiptStoreError::NotFound(format!(
70                    "credit bond `{}` not found",
71                    artifact.bond.body.bond_id
72                ))
73            })?;
74        let stored_bond: SignedCreditBond = serde_json::from_str(&stored_bond_raw_json)?;
75        if stored_bond.body != artifact.bond.body {
76            return Err(ReceiptStoreError::Conflict(
77                "liability claim package bond does not match the persisted credit bond".to_string(),
78            ));
79        }
80
81        let stored_loss_raw_json = tx
82            .query_row(
83                "SELECT raw_json
84                 FROM credit_loss_lifecycle
85                 WHERE event_id = ?1",
86                params![artifact.loss_event.body.event_id],
87                |row| row.get::<_, String>(0),
88            )
89            .optional()?
90            .ok_or_else(|| {
91                ReceiptStoreError::NotFound(format!(
92                    "credit loss lifecycle event `{}` not found",
93                    artifact.loss_event.body.event_id
94                ))
95            })?;
96        let stored_loss: SignedCreditLossLifecycle = serde_json::from_str(&stored_loss_raw_json)?;
97        if stored_loss.body != artifact.loss_event.body {
98            return Err(ReceiptStoreError::Conflict(
99                "liability claim package loss_event does not match the persisted credit loss lifecycle event"
100                    .to_string(),
101            ));
102        }
103
104        for receipt_id in &artifact.receipt_ids {
105            let exists = tx
106                .query_row(
107                    "SELECT 1 FROM chio_tool_receipts WHERE receipt_id = ?1",
108                    params![receipt_id],
109                    |row| row.get::<_, i64>(0),
110                )
111                .optional()?;
112            if exists.is_none() {
113                return Err(ReceiptStoreError::NotFound(format!(
114                    "receipt {receipt_id} does not exist"
115                )));
116            }
117        }
118
119        tx.execute(
120            "INSERT INTO liability_claim_packages (
121                claim_id, issued_at, provider_id, policy_number, jurisdiction, subject_key,
122                claim_event_at, raw_json, signer_key, signature
123             ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
124            params![
125                artifact.claim_id,
126                artifact.issued_at as i64,
127                artifact
128                    .bound_coverage
129                    .body
130                    .placement
131                    .body
132                    .quote_response
133                    .body
134                    .quote_request
135                    .body
136                    .provider_policy
137                    .provider_id,
138                artifact.bound_coverage.body.policy_number,
139                artifact
140                    .bound_coverage
141                    .body
142                    .placement
143                    .body
144                    .quote_response
145                    .body
146                    .quote_request
147                    .body
148                    .provider_policy
149                    .jurisdiction,
150                artifact
151                    .bound_coverage
152                    .body
153                    .placement
154                    .body
155                    .quote_response
156                    .body
157                    .quote_request
158                    .body
159                    .risk_package
160                    .body
161                    .subject_key,
162                artifact.claim_event_at as i64,
163                serde_json::to_string(claim)?,
164                claim.signer_key.to_hex(),
165                claim.signature.to_hex(),
166            ],
167        )?;
168
169        tx.commit()?;
170        Ok(())
171    }
172
173    pub fn record_liability_claim_response(
174        &mut self,
175        response: &SignedLiabilityClaimResponse,
176    ) -> Result<(), ReceiptStoreError> {
177        if !response
178            .verify_signature()
179            .map_err(|error| ReceiptStoreError::Canonical(error.to_string()))?
180        {
181            return Err(ReceiptStoreError::Conflict(
182                "liability claim response signature verification failed".to_string(),
183            ));
184        }
185        response
186            .body
187            .validate()
188            .map_err(ReceiptStoreError::Conflict)?;
189
190        let artifact = &response.body;
191        let mut connection = self.connection()?;
192        let tx = connection.transaction()?;
193        let existing = tx
194            .query_row(
195                "SELECT claim_response_id
196                 FROM liability_claim_responses
197                 WHERE claim_response_id = ?1",
198                params![artifact.claim_response_id],
199                |row| row.get::<_, String>(0),
200            )
201            .optional()?;
202        if existing.is_some() {
203            return Err(ReceiptStoreError::Conflict(format!(
204                "liability claim response `{}` already exists",
205                artifact.claim_response_id
206            )));
207        }
208
209        let stored_claim_raw_json = tx
210            .query_row(
211                "SELECT raw_json
212                 FROM liability_claim_packages
213                 WHERE claim_id = ?1",
214                params![artifact.claim.body.claim_id],
215                |row| row.get::<_, String>(0),
216            )
217            .optional()?
218            .ok_or_else(|| {
219                ReceiptStoreError::NotFound(format!(
220                    "liability claim package `{}` not found",
221                    artifact.claim.body.claim_id
222                ))
223            })?;
224        let stored_claim: SignedLiabilityClaimPackage =
225            serde_json::from_str(&stored_claim_raw_json)?;
226        if stored_claim.body != artifact.claim.body {
227            return Err(ReceiptStoreError::Conflict(
228                "liability claim response claim does not match the persisted claim package"
229                    .to_string(),
230            ));
231        }
232
233        tx.execute(
234            "INSERT INTO liability_claim_responses (
235                claim_response_id, issued_at, claim_id, provider_id, disposition,
236                raw_json, signer_key, signature
237             ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
238            params![
239                artifact.claim_response_id,
240                artifact.issued_at as i64,
241                artifact.claim.body.claim_id,
242                artifact
243                    .claim
244                    .body
245                    .bound_coverage
246                    .body
247                    .placement
248                    .body
249                    .quote_response
250                    .body
251                    .quote_request
252                    .body
253                    .provider_policy
254                    .provider_id,
255                serde_json::to_string(&artifact.disposition)?,
256                serde_json::to_string(response)?,
257                response.signer_key.to_hex(),
258                response.signature.to_hex(),
259            ],
260        )?;
261
262        tx.commit()?;
263        Ok(())
264    }
265
266    pub fn record_liability_claim_dispute(
267        &mut self,
268        dispute: &SignedLiabilityClaimDispute,
269    ) -> Result<(), ReceiptStoreError> {
270        if !dispute
271            .verify_signature()
272            .map_err(|error| ReceiptStoreError::Canonical(error.to_string()))?
273        {
274            return Err(ReceiptStoreError::Conflict(
275                "liability claim dispute signature verification failed".to_string(),
276            ));
277        }
278        dispute
279            .body
280            .validate()
281            .map_err(ReceiptStoreError::Conflict)?;
282
283        let artifact = &dispute.body;
284        let mut connection = self.connection()?;
285        let tx = connection.transaction()?;
286        let existing = tx
287            .query_row(
288                "SELECT dispute_id FROM liability_claim_disputes WHERE dispute_id = ?1",
289                params![artifact.dispute_id],
290                |row| row.get::<_, String>(0),
291            )
292            .optional()?;
293        if existing.is_some() {
294            return Err(ReceiptStoreError::Conflict(format!(
295                "liability claim dispute `{}` already exists",
296                artifact.dispute_id
297            )));
298        }
299
300        let stored_response_raw_json = tx
301            .query_row(
302                "SELECT raw_json
303                 FROM liability_claim_responses
304                 WHERE claim_response_id = ?1",
305                params![artifact.provider_response.body.claim_response_id],
306                |row| row.get::<_, String>(0),
307            )
308            .optional()?
309            .ok_or_else(|| {
310                ReceiptStoreError::NotFound(format!(
311                    "liability claim response `{}` not found",
312                    artifact.provider_response.body.claim_response_id
313                ))
314            })?;
315        let stored_response: SignedLiabilityClaimResponse =
316            serde_json::from_str(&stored_response_raw_json)?;
317        if stored_response.body != artifact.provider_response.body {
318            return Err(ReceiptStoreError::Conflict(
319                "liability claim dispute provider_response does not match the persisted claim response"
320                    .to_string(),
321            ));
322        }
323
324        tx.execute(
325            "INSERT INTO liability_claim_disputes (
326                dispute_id, issued_at, claim_id, claim_response_id, provider_id,
327                raw_json, signer_key, signature
328             ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
329            params![
330                artifact.dispute_id,
331                artifact.issued_at as i64,
332                artifact.provider_response.body.claim.body.claim_id,
333                artifact.provider_response.body.claim_response_id,
334                artifact
335                    .provider_response
336                    .body
337                    .claim
338                    .body
339                    .bound_coverage
340                    .body
341                    .placement
342                    .body
343                    .quote_response
344                    .body
345                    .quote_request
346                    .body
347                    .provider_policy
348                    .provider_id,
349                serde_json::to_string(dispute)?,
350                dispute.signer_key.to_hex(),
351                dispute.signature.to_hex(),
352            ],
353        )?;
354
355        tx.commit()?;
356        Ok(())
357    }
358
359    pub fn record_liability_claim_adjudication(
360        &mut self,
361        adjudication: &SignedLiabilityClaimAdjudication,
362    ) -> Result<(), ReceiptStoreError> {
363        if !adjudication
364            .verify_signature()
365            .map_err(|error| ReceiptStoreError::Canonical(error.to_string()))?
366        {
367            return Err(ReceiptStoreError::Conflict(
368                "liability claim adjudication signature verification failed".to_string(),
369            ));
370        }
371        adjudication
372            .body
373            .validate()
374            .map_err(ReceiptStoreError::Conflict)?;
375
376        let artifact = &adjudication.body;
377        let mut connection = self.connection()?;
378        let tx = connection.transaction()?;
379        let existing = tx
380            .query_row(
381                "SELECT adjudication_id
382                 FROM liability_claim_adjudications
383                 WHERE adjudication_id = ?1",
384                params![artifact.adjudication_id],
385                |row| row.get::<_, String>(0),
386            )
387            .optional()?;
388        if existing.is_some() {
389            return Err(ReceiptStoreError::Conflict(format!(
390                "liability claim adjudication `{}` already exists",
391                artifact.adjudication_id
392            )));
393        }
394
395        let stored_dispute_raw_json = tx
396            .query_row(
397                "SELECT raw_json
398                 FROM liability_claim_disputes
399                 WHERE dispute_id = ?1",
400                params![artifact.dispute.body.dispute_id],
401                |row| row.get::<_, String>(0),
402            )
403            .optional()?
404            .ok_or_else(|| {
405                ReceiptStoreError::NotFound(format!(
406                    "liability claim dispute `{}` not found",
407                    artifact.dispute.body.dispute_id
408                ))
409            })?;
410        let stored_dispute: SignedLiabilityClaimDispute =
411            serde_json::from_str(&stored_dispute_raw_json)?;
412        if stored_dispute.body != artifact.dispute.body {
413            return Err(ReceiptStoreError::Conflict(
414                "liability claim adjudication dispute does not match the persisted claim dispute"
415                    .to_string(),
416            ));
417        }
418
419        tx.execute(
420            "INSERT INTO liability_claim_adjudications (
421                adjudication_id, issued_at, claim_id, dispute_id, outcome,
422                raw_json, signer_key, signature
423             ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
424            params![
425                artifact.adjudication_id,
426                artifact.issued_at as i64,
427                artifact
428                    .dispute
429                    .body
430                    .provider_response
431                    .body
432                    .claim
433                    .body
434                    .claim_id,
435                artifact.dispute.body.dispute_id,
436                serde_json::to_string(&artifact.outcome)?,
437                serde_json::to_string(adjudication)?,
438                adjudication.signer_key.to_hex(),
439                adjudication.signature.to_hex(),
440            ],
441        )?;
442
443        tx.commit()?;
444        Ok(())
445    }
446
447    pub fn record_liability_claim_payout_instruction(
448        &mut self,
449        payout_instruction: &SignedLiabilityClaimPayoutInstruction,
450    ) -> Result<(), ReceiptStoreError> {
451        if !payout_instruction
452            .verify_signature()
453            .map_err(|error| ReceiptStoreError::Canonical(error.to_string()))?
454        {
455            return Err(ReceiptStoreError::Conflict(
456                "liability claim payout instruction signature verification failed".to_string(),
457            ));
458        }
459        payout_instruction
460            .body
461            .validate()
462            .map_err(ReceiptStoreError::Conflict)?;
463
464        let artifact = &payout_instruction.body;
465        let mut connection = self.connection()?;
466        let tx = connection.transaction()?;
467        let existing = tx
468            .query_row(
469                "SELECT payout_instruction_id
470                 FROM liability_claim_payout_instructions
471                 WHERE payout_instruction_id = ?1",
472                params![artifact.payout_instruction_id],
473                |row| row.get::<_, String>(0),
474            )
475            .optional()?;
476        if existing.is_some() {
477            return Err(ReceiptStoreError::Conflict(format!(
478                "liability claim payout instruction `{}` already exists",
479                artifact.payout_instruction_id
480            )));
481        }
482
483        let stored_adjudication_raw_json = tx
484            .query_row(
485                "SELECT raw_json
486                 FROM liability_claim_adjudications
487                 WHERE adjudication_id = ?1",
488                params![artifact.adjudication.body.adjudication_id],
489                |row| row.get::<_, String>(0),
490            )
491            .optional()?
492            .ok_or_else(|| {
493                ReceiptStoreError::NotFound(format!(
494                    "liability claim adjudication `{}` not found",
495                    artifact.adjudication.body.adjudication_id
496                ))
497            })?;
498        let stored_adjudication: SignedLiabilityClaimAdjudication =
499            serde_json::from_str(&stored_adjudication_raw_json)?;
500        if stored_adjudication.body != artifact.adjudication.body {
501            return Err(ReceiptStoreError::Conflict(
502                "liability claim payout instruction adjudication does not match the persisted adjudication"
503                    .to_string(),
504            ));
505        }
506
507        let claim_id = artifact
508            .adjudication
509            .body
510            .dispute
511            .body
512            .provider_response
513            .body
514            .claim
515            .body
516            .claim_id
517            .clone();
518
519        tx.execute(
520            "INSERT INTO liability_claim_payout_instructions (
521                payout_instruction_id, issued_at, claim_id, adjudication_id,
522                payout_amount_units, payout_amount_currency,
523                raw_json, signer_key, signature
524             ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
525            params![
526                artifact.payout_instruction_id,
527                artifact.issued_at as i64,
528                claim_id,
529                artifact.adjudication.body.adjudication_id,
530                artifact.payout_amount.units as i64,
531                artifact.payout_amount.currency,
532                serde_json::to_string(payout_instruction)?,
533                payout_instruction.signer_key.to_hex(),
534                payout_instruction.signature.to_hex(),
535            ],
536        )?;
537
538        tx.commit()?;
539        Ok(())
540    }
541
542    pub fn record_liability_claim_payout_receipt(
543        &mut self,
544        payout_receipt: &SignedLiabilityClaimPayoutReceipt,
545    ) -> Result<(), ReceiptStoreError> {
546        if !payout_receipt
547            .verify_signature()
548            .map_err(|error| ReceiptStoreError::Canonical(error.to_string()))?
549        {
550            return Err(ReceiptStoreError::Conflict(
551                "liability claim payout receipt signature verification failed".to_string(),
552            ));
553        }
554        payout_receipt
555            .body
556            .validate()
557            .map_err(ReceiptStoreError::Conflict)?;
558
559        let artifact = &payout_receipt.body;
560        let mut connection = self.connection()?;
561        let tx = connection.transaction()?;
562        let existing = tx
563            .query_row(
564                "SELECT payout_receipt_id
565                 FROM liability_claim_payout_receipts
566                 WHERE payout_receipt_id = ?1",
567                params![artifact.payout_receipt_id],
568                |row| row.get::<_, String>(0),
569            )
570            .optional()?;
571        if existing.is_some() {
572            return Err(ReceiptStoreError::Conflict(format!(
573                "liability claim payout receipt `{}` already exists",
574                artifact.payout_receipt_id
575            )));
576        }
577
578        let stored_instruction_raw_json = tx
579            .query_row(
580                "SELECT raw_json
581                 FROM liability_claim_payout_instructions
582                 WHERE payout_instruction_id = ?1",
583                params![artifact.payout_instruction.body.payout_instruction_id],
584                |row| row.get::<_, String>(0),
585            )
586            .optional()?
587            .ok_or_else(|| {
588                ReceiptStoreError::NotFound(format!(
589                    "liability claim payout instruction `{}` not found",
590                    artifact.payout_instruction.body.payout_instruction_id
591                ))
592            })?;
593        let stored_instruction: SignedLiabilityClaimPayoutInstruction =
594            serde_json::from_str(&stored_instruction_raw_json)?;
595        if stored_instruction.body != artifact.payout_instruction.body {
596            return Err(ReceiptStoreError::Conflict(
597                "liability claim payout receipt payout_instruction does not match the persisted payout instruction"
598                    .to_string(),
599            ));
600        }
601
602        let claim_id = artifact
603            .payout_instruction
604            .body
605            .adjudication
606            .body
607            .dispute
608            .body
609            .provider_response
610            .body
611            .claim
612            .body
613            .claim_id
614            .clone();
615
616        tx.execute(
617            "INSERT INTO liability_claim_payout_receipts (
618                payout_receipt_id, issued_at, claim_id, payout_instruction_id,
619                reconciliation_state, raw_json, signer_key, signature
620             ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
621            params![
622                artifact.payout_receipt_id,
623                artifact.issued_at as i64,
624                claim_id,
625                artifact.payout_instruction.body.payout_instruction_id,
626                serde_json::to_string(&artifact.reconciliation_state)?,
627                serde_json::to_string(payout_receipt)?,
628                payout_receipt.signer_key.to_hex(),
629                payout_receipt.signature.to_hex(),
630            ],
631        )?;
632
633        tx.commit()?;
634        Ok(())
635    }
636
637    pub fn record_liability_claim_settlement_instruction(
638        &mut self,
639        settlement_instruction: &SignedLiabilityClaimSettlementInstruction,
640    ) -> Result<(), ReceiptStoreError> {
641        if !settlement_instruction
642            .verify_signature()
643            .map_err(|error| ReceiptStoreError::Canonical(error.to_string()))?
644        {
645            return Err(ReceiptStoreError::Conflict(
646                "liability claim settlement instruction signature verification failed".to_string(),
647            ));
648        }
649        settlement_instruction
650            .body
651            .validate()
652            .map_err(ReceiptStoreError::Conflict)?;
653
654        let artifact = &settlement_instruction.body;
655        let mut connection = self.connection()?;
656        let tx = connection.transaction()?;
657        let existing = tx
658            .query_row(
659                "SELECT settlement_instruction_id
660                 FROM liability_claim_settlement_instructions
661                 WHERE settlement_instruction_id = ?1",
662                params![artifact.settlement_instruction_id],
663                |row| row.get::<_, String>(0),
664            )
665            .optional()?;
666        if existing.is_some() {
667            return Err(ReceiptStoreError::Conflict(format!(
668                "liability claim settlement instruction `{}` already exists",
669                artifact.settlement_instruction_id
670            )));
671        }
672
673        let stored_payout_receipt_raw_json = tx
674            .query_row(
675                "SELECT raw_json
676                 FROM liability_claim_payout_receipts
677                 WHERE payout_receipt_id = ?1",
678                params![artifact.payout_receipt.body.payout_receipt_id],
679                |row| row.get::<_, String>(0),
680            )
681            .optional()?
682            .ok_or_else(|| {
683                ReceiptStoreError::NotFound(format!(
684                    "liability claim payout receipt `{}` not found",
685                    artifact.payout_receipt.body.payout_receipt_id
686                ))
687            })?;
688        let stored_payout_receipt: SignedLiabilityClaimPayoutReceipt =
689            serde_json::from_str(&stored_payout_receipt_raw_json)?;
690        if stored_payout_receipt.body != artifact.payout_receipt.body {
691            return Err(ReceiptStoreError::Conflict(
692                "liability claim settlement instruction payout_receipt does not match the persisted payout receipt"
693                    .to_string(),
694            ));
695        }
696
697        let claim_id = artifact
698            .payout_receipt
699            .body
700            .payout_instruction
701            .body
702            .adjudication
703            .body
704            .dispute
705            .body
706            .provider_response
707            .body
708            .claim
709            .body
710            .claim_id
711            .clone();
712
713        tx.execute(
714            "INSERT INTO liability_claim_settlement_instructions (
715                settlement_instruction_id, issued_at, claim_id, payout_receipt_id,
716                settlement_kind, payer_role, payer_id, payee_role, payee_id,
717                settlement_amount_units, settlement_amount_currency,
718                raw_json, signer_key, signature
719             ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
720            params![
721                artifact.settlement_instruction_id,
722                artifact.issued_at as i64,
723                claim_id,
724                artifact.payout_receipt.body.payout_receipt_id,
725                serde_json::to_string(&artifact.settlement_kind)?,
726                serde_json::to_string(&artifact.topology.payer.role)?,
727                artifact.topology.payer.party_id,
728                serde_json::to_string(&artifact.topology.payee.role)?,
729                artifact.topology.payee.party_id,
730                artifact.settlement_amount.units as i64,
731                artifact.settlement_amount.currency,
732                serde_json::to_string(settlement_instruction)?,
733                settlement_instruction.signer_key.to_hex(),
734                settlement_instruction.signature.to_hex(),
735            ],
736        )?;
737
738        tx.commit()?;
739        Ok(())
740    }
741
742    pub fn record_liability_claim_settlement_receipt(
743        &mut self,
744        settlement_receipt: &SignedLiabilityClaimSettlementReceipt,
745    ) -> Result<(), ReceiptStoreError> {
746        if !settlement_receipt
747            .verify_signature()
748            .map_err(|error| ReceiptStoreError::Canonical(error.to_string()))?
749        {
750            return Err(ReceiptStoreError::Conflict(
751                "liability claim settlement receipt signature verification failed".to_string(),
752            ));
753        }
754        settlement_receipt
755            .body
756            .validate()
757            .map_err(ReceiptStoreError::Conflict)?;
758
759        let artifact = &settlement_receipt.body;
760        let mut connection = self.connection()?;
761        let tx = connection.transaction()?;
762        let existing = tx
763            .query_row(
764                "SELECT settlement_receipt_id
765                 FROM liability_claim_settlement_receipts
766                 WHERE settlement_receipt_id = ?1",
767                params![artifact.settlement_receipt_id],
768                |row| row.get::<_, String>(0),
769            )
770            .optional()?;
771        if existing.is_some() {
772            return Err(ReceiptStoreError::Conflict(format!(
773                "liability claim settlement receipt `{}` already exists",
774                artifact.settlement_receipt_id
775            )));
776        }
777
778        let stored_instruction_raw_json = tx
779            .query_row(
780                "SELECT raw_json
781                 FROM liability_claim_settlement_instructions
782                 WHERE settlement_instruction_id = ?1",
783                params![
784                    artifact
785                        .settlement_instruction
786                        .body
787                        .settlement_instruction_id
788                ],
789                |row| row.get::<_, String>(0),
790            )
791            .optional()?
792            .ok_or_else(|| {
793                ReceiptStoreError::NotFound(format!(
794                    "liability claim settlement instruction `{}` not found",
795                    artifact
796                        .settlement_instruction
797                        .body
798                        .settlement_instruction_id
799                ))
800            })?;
801        let stored_instruction: SignedLiabilityClaimSettlementInstruction =
802            serde_json::from_str(&stored_instruction_raw_json)?;
803        if stored_instruction.body != artifact.settlement_instruction.body {
804            return Err(ReceiptStoreError::Conflict(
805                "liability claim settlement receipt settlement_instruction does not match the persisted settlement instruction"
806                    .to_string(),
807            ));
808        }
809
810        let claim_id = artifact
811            .settlement_instruction
812            .body
813            .payout_receipt
814            .body
815            .payout_instruction
816            .body
817            .adjudication
818            .body
819            .dispute
820            .body
821            .provider_response
822            .body
823            .claim
824            .body
825            .claim_id
826            .clone();
827
828        tx.execute(
829            "INSERT INTO liability_claim_settlement_receipts (
830                settlement_receipt_id, issued_at, claim_id, settlement_instruction_id,
831                reconciliation_state, raw_json, signer_key, signature
832             ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
833            params![
834                artifact.settlement_receipt_id,
835                artifact.issued_at as i64,
836                claim_id,
837                artifact
838                    .settlement_instruction
839                    .body
840                    .settlement_instruction_id,
841                serde_json::to_string(&artifact.reconciliation_state)?,
842                serde_json::to_string(settlement_receipt)?,
843                settlement_receipt.signer_key.to_hex(),
844                settlement_receipt.signature.to_hex(),
845            ],
846        )?;
847
848        tx.commit()?;
849        Ok(())
850    }
851
852    pub fn query_liability_claim_workflows(
853        &self,
854        query: &LiabilityClaimWorkflowQuery,
855    ) -> Result<LiabilityClaimWorkflowReport, ReceiptStoreError> {
856        let normalized = query.normalized();
857        let connection = self.connection()?;
858        let mut statement = connection.prepare(
859            "SELECT raw_json
860             FROM liability_claim_packages
861             ORDER BY issued_at DESC, claim_id DESC",
862        )?;
863        let rows = statement.query_map([], |row| row.get::<_, String>(0))?;
864
865        let mut matching_claims = 0_u64;
866        let mut provider_responses = 0_u64;
867        let mut accepted_responses = 0_u64;
868        let mut denied_responses = 0_u64;
869        let mut disputes = 0_u64;
870        let mut adjudications = 0_u64;
871        let mut payout_instructions = 0_u64;
872        let mut payout_receipts = 0_u64;
873        let mut matched_payout_receipts = 0_u64;
874        let mut mismatched_payout_receipts = 0_u64;
875        let mut settlement_instructions = 0_u64;
876        let mut settlement_receipts = 0_u64;
877        let mut matched_settlement_receipts = 0_u64;
878        let mut mismatched_settlement_receipts = 0_u64;
879        let mut counterparty_mismatch_settlement_receipts = 0_u64;
880        let mut claims = Vec::new();
881
882        for row in rows {
883            let raw_json = row?;
884            let claim: SignedLiabilityClaimPackage = serde_json::from_str(&raw_json)?;
885            if !liability_claim_workflow_matches_query(&claim, &normalized) {
886                continue;
887            }
888            matching_claims += 1;
889
890            let provider_response = self
891                .connection()?
892                .query_row(
893                    "SELECT raw_json
894                     FROM liability_claim_responses
895                     WHERE claim_id = ?1
896                     ORDER BY issued_at DESC, claim_response_id DESC
897                     LIMIT 1",
898                    params![claim.body.claim_id],
899                    |row| row.get::<_, String>(0),
900                )
901                .optional()?
902                .map(|raw_json| serde_json::from_str::<SignedLiabilityClaimResponse>(&raw_json))
903                .transpose()?;
904            if let Some(response) = provider_response.as_ref() {
905                provider_responses += 1;
906                match response.body.disposition {
907                    LiabilityClaimResponseDisposition::Accepted => accepted_responses += 1,
908                    LiabilityClaimResponseDisposition::Denied => denied_responses += 1,
909                    LiabilityClaimResponseDisposition::Acknowledged => {}
910                }
911            }
912
913            let dispute = self
914                .connection()?
915                .query_row(
916                    "SELECT raw_json
917                     FROM liability_claim_disputes
918                     WHERE claim_id = ?1
919                     ORDER BY issued_at DESC, dispute_id DESC
920                     LIMIT 1",
921                    params![claim.body.claim_id],
922                    |row| row.get::<_, String>(0),
923                )
924                .optional()?
925                .map(|raw_json| serde_json::from_str::<SignedLiabilityClaimDispute>(&raw_json))
926                .transpose()?;
927            if dispute.is_some() {
928                disputes += 1;
929            }
930
931            let adjudication = self
932                .connection()?
933                .query_row(
934                    "SELECT raw_json
935                     FROM liability_claim_adjudications
936                     WHERE claim_id = ?1
937                     ORDER BY issued_at DESC, adjudication_id DESC
938                     LIMIT 1",
939                    params![claim.body.claim_id],
940                    |row| row.get::<_, String>(0),
941                )
942                .optional()?
943                .map(|raw_json| serde_json::from_str::<SignedLiabilityClaimAdjudication>(&raw_json))
944                .transpose()?;
945            if adjudication.is_some() {
946                adjudications += 1;
947            }
948
949            let payout_instruction = self
950                .connection()?
951                .query_row(
952                    "SELECT raw_json
953                     FROM liability_claim_payout_instructions
954                     WHERE claim_id = ?1
955                     ORDER BY issued_at DESC, payout_instruction_id DESC
956                     LIMIT 1",
957                    params![claim.body.claim_id],
958                    |row| row.get::<_, String>(0),
959                )
960                .optional()?
961                .map(|raw_json| {
962                    serde_json::from_str::<SignedLiabilityClaimPayoutInstruction>(&raw_json)
963                })
964                .transpose()?;
965            if payout_instruction.is_some() {
966                payout_instructions += 1;
967            }
968
969            let payout_receipt = self
970                .connection()?
971                .query_row(
972                    "SELECT raw_json
973                     FROM liability_claim_payout_receipts
974                     WHERE claim_id = ?1
975                     ORDER BY issued_at DESC, payout_receipt_id DESC
976                     LIMIT 1",
977                    params![claim.body.claim_id],
978                    |row| row.get::<_, String>(0),
979                )
980                .optional()?
981                .map(|raw_json| {
982                    serde_json::from_str::<SignedLiabilityClaimPayoutReceipt>(&raw_json)
983                })
984                .transpose()?;
985            if let Some(receipt) = payout_receipt.as_ref() {
986                payout_receipts += 1;
987                match receipt.body.reconciliation_state {
988                    LiabilityClaimPayoutReconciliationState::Matched => {
989                        matched_payout_receipts += 1;
990                    }
991                    LiabilityClaimPayoutReconciliationState::AmountMismatch => {
992                        mismatched_payout_receipts += 1;
993                    }
994                }
995            }
996
997            let settlement_instruction = self
998                .connection()?
999                .query_row(
1000                    "SELECT raw_json
1001                     FROM liability_claim_settlement_instructions
1002                     WHERE claim_id = ?1
1003                     ORDER BY issued_at DESC, settlement_instruction_id DESC
1004                     LIMIT 1",
1005                    params![claim.body.claim_id],
1006                    |row| row.get::<_, String>(0),
1007                )
1008                .optional()?
1009                .map(|raw_json| {
1010                    serde_json::from_str::<SignedLiabilityClaimSettlementInstruction>(&raw_json)
1011                })
1012                .transpose()?;
1013            if settlement_instruction.is_some() {
1014                settlement_instructions += 1;
1015            }
1016
1017            let settlement_receipt = self
1018                .connection()?
1019                .query_row(
1020                    "SELECT raw_json
1021                     FROM liability_claim_settlement_receipts
1022                     WHERE claim_id = ?1
1023                     ORDER BY issued_at DESC, settlement_receipt_id DESC
1024                     LIMIT 1",
1025                    params![claim.body.claim_id],
1026                    |row| row.get::<_, String>(0),
1027                )
1028                .optional()?
1029                .map(|raw_json| {
1030                    serde_json::from_str::<SignedLiabilityClaimSettlementReceipt>(&raw_json)
1031                })
1032                .transpose()?;
1033            if let Some(receipt) = settlement_receipt.as_ref() {
1034                settlement_receipts += 1;
1035                match receipt.body.reconciliation_state {
1036                    LiabilityClaimSettlementReconciliationState::Matched => {
1037                        matched_settlement_receipts += 1;
1038                    }
1039                    LiabilityClaimSettlementReconciliationState::AmountMismatch => {
1040                        mismatched_settlement_receipts += 1;
1041                    }
1042                    LiabilityClaimSettlementReconciliationState::CounterpartyMismatch => {
1043                        counterparty_mismatch_settlement_receipts += 1;
1044                    }
1045                }
1046            }
1047
1048            if claims.len() < normalized.limit_or_default() {
1049                claims.push(LiabilityClaimWorkflowRow {
1050                    claim,
1051                    provider_response,
1052                    dispute,
1053                    adjudication,
1054                    payout_instruction,
1055                    payout_receipt,
1056                    settlement_instruction,
1057                    settlement_receipt,
1058                });
1059            }
1060        }
1061
1062        Ok(LiabilityClaimWorkflowReport {
1063            schema: LIABILITY_CLAIM_WORKFLOW_REPORT_SCHEMA.to_string(),
1064            generated_at: unix_now(),
1065            query: normalized,
1066            summary: LiabilityClaimWorkflowSummary {
1067                matching_claims,
1068                returned_claims: claims.len() as u64,
1069                provider_responses,
1070                accepted_responses,
1071                denied_responses,
1072                disputes,
1073                adjudications,
1074                payout_instructions,
1075                payout_receipts,
1076                matched_payout_receipts,
1077                mismatched_payout_receipts,
1078                settlement_instructions,
1079                settlement_receipts,
1080                matched_settlement_receipts,
1081                mismatched_settlement_receipts,
1082                counterparty_mismatch_settlement_receipts,
1083            },
1084            claims,
1085        })
1086    }
1087}