1use 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}