Skip to main content

chio_store_sqlite/receipt_store/reports/
economic.rs

1// Economic receipt projection and completion-flow report queries.
2
3use super::*;
4
5impl SqliteReceiptStore {
6    pub fn query_economic_receipt_projection_report(
7        &self,
8        query: &OperatorReportQuery,
9    ) -> Result<EconomicReceiptProjectionReport, ReceiptStoreError> {
10        require_admin_receipt_read_context(
11            query.read_context.as_ref(),
12            "economic receipt projection report",
13        )?;
14        let capability_id = query.capability_id.as_deref();
15        let tool_server = query.tool_server.as_deref();
16        let tool_name = query.tool_name.as_deref();
17        let since = query.since.map(|value| value as i64);
18        let until = query.until.map(|value| value as i64);
19        let agent_subject = query.agent_subject.as_deref();
20        let row_limit = query.economic_limit_or_default();
21
22        let matching_receipts = self.connection()?.query_row(
23            r#"
24            SELECT COUNT(*)
25            FROM chio_tool_receipts r
26            LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
27            WHERE json_type(r.raw_json, '$.metadata.governed_transaction.economic_authorization') = 'object'
28              AND (?1 IS NULL OR r.capability_id = ?1)
29              AND (?2 IS NULL OR r.tool_server = ?2)
30              AND (?3 IS NULL OR r.tool_name = ?3)
31              AND (?4 IS NULL OR r.timestamp >= ?4)
32              AND (?5 IS NULL OR r.timestamp <= ?5)
33              AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
34            "#,
35            params![
36                capability_id,
37                tool_server,
38                tool_name,
39                since,
40                until,
41                agent_subject
42            ],
43            |row| row.get::<_, i64>(0),
44        )?
45        .max(0) as u64;
46
47        let rows_sql = r#"
48            SELECT
49                r.seq,
50                r.receipt_id,
51                r.timestamp,
52                r.capability_id,
53                COALESCE(r.subject_key, cl.subject_key),
54                r.tool_server,
55                r.tool_name,
56                COALESCE(sr.reconciliation_state, 'open'),
57                sr.note,
58                sr.updated_at,
59                COALESCE(mbr.reconciliation_state, 'open'),
60                mbr.note,
61                mbr.updated_at,
62                mbr.adapter_kind,
63                mbr.evidence_id,
64                mbr.observed_units,
65                mbr.billed_cost_units,
66                mbr.billed_cost_currency,
67                mbr.evidence_sha256,
68                mbr.recorded_at,
69                r.raw_json
70            FROM chio_tool_receipts r
71            LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
72            LEFT JOIN settlement_reconciliations sr ON r.receipt_id = sr.receipt_id
73            LEFT JOIN metered_billing_reconciliations mbr ON r.receipt_id = mbr.receipt_id
74            WHERE json_type(r.raw_json, '$.metadata.governed_transaction.economic_authorization') = 'object'
75              AND (?1 IS NULL OR r.capability_id = ?1)
76              AND (?2 IS NULL OR r.tool_server = ?2)
77              AND (?3 IS NULL OR r.tool_name = ?3)
78              AND (?4 IS NULL OR r.timestamp >= ?4)
79              AND (?5 IS NULL OR r.timestamp <= ?5)
80              AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
81            ORDER BY r.timestamp DESC, r.seq DESC
82            LIMIT ?7
83        "#;
84
85        let connection = self.connection()?;
86        let mut stmt = connection.prepare(rows_sql)?;
87        let rows = stmt.query_map(
88            params![
89                capability_id,
90                tool_server,
91                tool_name,
92                since,
93                until,
94                agent_subject,
95                row_limit as i64
96            ],
97            |row| {
98                Ok((
99                    row.get::<_, i64>(0)?,
100                    row.get::<_, String>(1)?,
101                    row.get::<_, i64>(2)?,
102                    row.get::<_, String>(3)?,
103                    row.get::<_, Option<String>>(4)?,
104                    row.get::<_, String>(5)?,
105                    row.get::<_, String>(6)?,
106                    row.get::<_, String>(7)?,
107                    row.get::<_, Option<String>>(8)?,
108                    row.get::<_, Option<i64>>(9)?,
109                    row.get::<_, String>(10)?,
110                    row.get::<_, Option<String>>(11)?,
111                    row.get::<_, Option<i64>>(12)?,
112                    row.get::<_, Option<String>>(13)?,
113                    row.get::<_, Option<String>>(14)?,
114                    row.get::<_, Option<i64>>(15)?,
115                    row.get::<_, Option<i64>>(16)?,
116                    row.get::<_, Option<String>>(17)?,
117                    row.get::<_, Option<String>>(18)?,
118                    row.get::<_, Option<i64>>(19)?,
119                    row.get::<_, String>(20)?,
120                ))
121            },
122        )?;
123
124        let mut receipts = Vec::new();
125        let mut metered_receipts = 0_u64;
126        let mut pending_settlement_receipts = 0_u64;
127        let mut failed_settlement_receipts = 0_u64;
128        let mut settlement_actionable_receipts = 0_u64;
129        let mut metering_actionable_receipts = 0_u64;
130        let mut metering_evidence_missing_receipts = 0_u64;
131        let mut metering_financial_mismatch_receipts = 0_u64;
132
133        for row in rows {
134            let (
135                seq,
136                receipt_id,
137                timestamp,
138                capability_id,
139                subject_key,
140                tool_server,
141                tool_name,
142                settlement_reconciliation_state_text,
143                settlement_note,
144                settlement_updated_at,
145                metering_reconciliation_state_text,
146                metering_note,
147                metering_updated_at,
148                adapter_kind,
149                evidence_id,
150                observed_units,
151                billed_cost_units,
152                billed_cost_currency,
153                evidence_sha256,
154                recorded_at,
155                raw_json,
156            ) = row?;
157            let receipt = decode_verified_chio_receipt(
158                &raw_json,
159                "persisted tool receipt",
160                Some(seq.max(0) as u64),
161            )?;
162            let governed = extract_governed_transaction_metadata(&receipt).ok_or_else(|| {
163                ReceiptStoreError::Canonical(format!(
164                    "receipt {} is missing governed transaction metadata",
165                    receipt.id
166                ))
167            })?;
168            let economic_authorization = governed
169                .economic_authorization
170                .clone()
171                .or_else(|| extract_economic_authorization_metadata(&receipt))
172                .ok_or_else(|| {
173                    ReceiptStoreError::Canonical(format!(
174                        "receipt {} is missing economic authorization metadata",
175                        receipt.id
176                    ))
177                })?;
178            let financial = extract_financial_metadata(&receipt);
179            let settlement_reconciliation_state =
180                parse_settlement_reconciliation_state(&settlement_reconciliation_state_text)?;
181            let settlement = EconomicReceiptSettlementProjection {
182                settlement_status: economic_authorization.settlement.settlement_status.clone(),
183                reconciliation_state: settlement_reconciliation_state,
184                action_required: settlement_reconciliation_action_required(
185                    economic_authorization.settlement.settlement_status.clone(),
186                    settlement_reconciliation_state,
187                ),
188                note: settlement_note,
189                updated_at: settlement_updated_at.map(|value| value.max(0) as u64),
190            };
191            if settlement.settlement_status == SettlementStatus::Pending {
192                pending_settlement_receipts = pending_settlement_receipts.saturating_add(1);
193            }
194            if settlement.settlement_status == SettlementStatus::Failed {
195                failed_settlement_receipts = failed_settlement_receipts.saturating_add(1);
196            }
197            if settlement.action_required {
198                settlement_actionable_receipts = settlement_actionable_receipts.saturating_add(1);
199            }
200
201            let metering = if economic_authorization.metering.is_some() {
202                metered_receipts = metered_receipts.saturating_add(1);
203                let governed_metering = governed.metered_billing.as_ref().ok_or_else(|| {
204                    ReceiptStoreError::Canonical(format!(
205                        "receipt {} has economic metering metadata without governed metered billing context",
206                        receipt.id
207                    ))
208                })?;
209                let evidence = metered_billing_evidence_record_from_columns(
210                    adapter_kind,
211                    evidence_id,
212                    observed_units,
213                    billed_cost_units,
214                    billed_cost_currency,
215                    evidence_sha256,
216                    recorded_at,
217                );
218                let reconciliation_state = parse_metered_billing_reconciliation_state(
219                    &metering_reconciliation_state_text,
220                )?;
221                let analysis = analyze_metered_billing_reconciliation(
222                    governed_metering,
223                    financial.as_ref(),
224                    evidence.as_ref(),
225                    reconciliation_state,
226                );
227                if analysis.action_required {
228                    metering_actionable_receipts = metering_actionable_receipts.saturating_add(1);
229                }
230                if analysis.evidence_missing {
231                    metering_evidence_missing_receipts =
232                        metering_evidence_missing_receipts.saturating_add(1);
233                }
234                if analysis.financial_mismatch {
235                    metering_financial_mismatch_receipts =
236                        metering_financial_mismatch_receipts.saturating_add(1);
237                }
238                Some(EconomicReceiptMeteringProjection {
239                    reconciliation_state,
240                    action_required: analysis.action_required,
241                    evidence_missing: analysis.evidence_missing,
242                    exceeds_quoted_units: analysis.exceeds_quoted_units,
243                    exceeds_max_billed_units: analysis.exceeds_max_billed_units,
244                    exceeds_quoted_cost: analysis.exceeds_quoted_cost,
245                    financial_mismatch: analysis.financial_mismatch,
246                    evidence,
247                    note: metering_note,
248                    updated_at: metering_updated_at.map(|value| value.max(0) as u64),
249                })
250            } else {
251                None
252            };
253
254            receipts.push(EconomicReceiptProjectionRow {
255                receipt_id,
256                timestamp: timestamp.max(0) as u64,
257                capability_id,
258                subject_key,
259                tool_server,
260                tool_name,
261                economic_authorization,
262                budget_authority: receipt.financial_budget_authority_metadata(),
263                settlement,
264                metering,
265            });
266        }
267
268        Ok(EconomicReceiptProjectionReport {
269            summary: EconomicReceiptProjectionSummary {
270                matching_receipts,
271                returned_receipts: receipts.len() as u64,
272                metered_receipts,
273                pending_settlement_receipts,
274                failed_settlement_receipts,
275                settlement_actionable_receipts,
276                metering_actionable_receipts,
277                metering_evidence_missing_receipts,
278                metering_financial_mismatch_receipts,
279                truncated: matching_receipts > receipts.len() as u64,
280            },
281            receipts,
282        })
283    }
284
285    pub fn query_economic_completion_flow_report(
286        &self,
287        query: &ExposureLedgerQuery,
288        read_context: ReceiptReadContext,
289    ) -> Result<EconomicCompletionFlowReport, ReceiptStoreError> {
290        let normalized = query.normalized();
291        if let Err(message) = normalized.validate() {
292            return Err(ReceiptStoreError::Conflict(message));
293        }
294
295        let economic_receipts =
296            self.query_economic_receipt_projection_report(&OperatorReportQuery {
297                capability_id: normalized.capability_id.clone(),
298                agent_subject: normalized.agent_subject.clone(),
299                tool_server: normalized.tool_server.clone(),
300                tool_name: normalized.tool_name.clone(),
301                since: normalized.since,
302                until: normalized.until,
303                economic_limit: normalized.receipt_limit,
304                read_context: Some(read_context),
305                ..OperatorReportQuery::default()
306            })?;
307        let underwriting_decisions =
308            self.query_underwriting_decisions(&UnderwritingDecisionQuery {
309                decision_id: None,
310                capability_id: normalized.capability_id.clone(),
311                agent_subject: normalized.agent_subject.clone(),
312                tool_server: normalized.tool_server.clone(),
313                tool_name: normalized.tool_name.clone(),
314                outcome: None,
315                lifecycle_state: None,
316                appeal_status: None,
317                limit: normalized.decision_limit,
318            })?;
319        let credit_facilities = self.query_credit_facilities(&CreditFacilityListQuery {
320            facility_id: None,
321            capability_id: normalized.capability_id.clone(),
322            agent_subject: normalized.agent_subject.clone(),
323            tool_server: normalized.tool_server.clone(),
324            tool_name: normalized.tool_name.clone(),
325            disposition: None,
326            lifecycle_state: None,
327            limit: normalized.decision_limit,
328        })?;
329        let credit_bonds = self.query_credit_bonds(&CreditBondListQuery {
330            bond_id: None,
331            facility_id: None,
332            capability_id: normalized.capability_id.clone(),
333            agent_subject: normalized.agent_subject.clone(),
334            tool_server: normalized.tool_server.clone(),
335            tool_name: normalized.tool_name.clone(),
336            disposition: None,
337            lifecycle_state: None,
338            limit: normalized.decision_limit,
339        })?;
340
341        let latest_underwriting = underwriting_decisions
342            .decisions
343            .iter()
344            .find(|row| row.lifecycle_state == UnderwritingDecisionLifecycleState::Active)
345            .or_else(|| underwriting_decisions.decisions.first());
346        let latest_credit_facility = credit_facilities
347            .facilities
348            .iter()
349            .find(|row| row.lifecycle_state == CreditFacilityLifecycleState::Active)
350            .or_else(|| credit_facilities.facilities.first());
351        let latest_credit_bond = credit_bonds
352            .bonds
353            .iter()
354            .find(|row| row.lifecycle_state == CreditBondLifecycleState::Active)
355            .or_else(|| credit_bonds.bonds.first());
356
357        Ok(EconomicCompletionFlowReport {
358            schema: ECONOMIC_COMPLETION_FLOW_SCHEMA.to_string(),
359            generated_at: unix_now(),
360            filters: normalized,
361            summary: EconomicCompletionFlowSummary {
362                matching_receipts: economic_receipts.summary.matching_receipts,
363                returned_receipts: economic_receipts.summary.returned_receipts,
364                matching_underwriting_decisions: underwriting_decisions.summary.matching_decisions,
365                returned_underwriting_decisions: underwriting_decisions.summary.returned_decisions,
366                matching_credit_facilities: credit_facilities.summary.matching_facilities,
367                returned_credit_facilities: credit_facilities.summary.returned_facilities,
368                matching_credit_bonds: credit_bonds.summary.matching_bonds,
369                returned_credit_bonds: credit_bonds.summary.returned_bonds,
370                pending_settlement_receipts: economic_receipts.summary.pending_settlement_receipts,
371                failed_settlement_receipts: economic_receipts.summary.failed_settlement_receipts,
372                metering_actionable_receipts: economic_receipts
373                    .summary
374                    .metering_actionable_receipts,
375                latest_underwriting_decision_id: latest_underwriting
376                    .map(|row| row.decision.body.decision_id.clone()),
377                latest_underwriting_outcome: latest_underwriting
378                    .map(|row| row.decision.body.evaluation.outcome),
379                latest_credit_facility_id: latest_credit_facility
380                    .map(|row| row.facility.body.facility_id.clone()),
381                latest_credit_facility_disposition: latest_credit_facility
382                    .map(|row| row.facility.body.report.disposition),
383                latest_credit_bond_id: latest_credit_bond.map(|row| row.bond.body.bond_id.clone()),
384                latest_credit_bond_disposition: latest_credit_bond
385                    .map(|row| row.bond.body.report.disposition),
386            },
387            economic_receipts,
388            underwriting_decisions,
389            credit_facilities,
390            credit_bonds,
391        })
392    }
393}