1use super::*;
2
3use chio_core::capability::GovernedProvenanceEvidenceClass;
4use chio_kernel::evidence_export::EvidenceLineageReferences;
5use chio_kernel::operator_report::GovernedTransactionDiagnostics;
6
7#[derive(Debug, Clone)]
8struct GovernedTransactionProjection {
9 strong: GovernedTransactionReceiptMetadata,
10 diagnostics: Option<GovernedTransactionDiagnostics>,
11}
12
13#[derive(Debug, Clone, Default)]
14struct PersistedLineageReferences {
15 statement_id: Option<String>,
16 session_anchor_id: Option<String>,
17}
18
19fn extract_governed_transaction_diagnostics(
20 receipt: &ChioReceipt,
21) -> Option<GovernedTransactionDiagnostics> {
22 let diagnostics = receipt
23 .metadata
24 .as_ref()
25 .and_then(|metadata| metadata.get("governed_transaction_diagnostics"))
26 .cloned()
27 .and_then(|value| serde_json::from_value::<GovernedTransactionDiagnostics>(value).ok())
28 .unwrap_or_default();
29
30 (!diagnostics.is_empty()).then_some(diagnostics)
31}
32
33fn sanitize_governed_transaction_projection(
34 receipt: &ChioReceipt,
35 governed: &GovernedTransactionReceiptMetadata,
36) -> GovernedTransactionProjection {
37 let mut strong = governed.clone();
38 let mut diagnostics = extract_governed_transaction_diagnostics(receipt).unwrap_or_default();
39
40 if let Some(call_chain) = strong
41 .call_chain
42 .clone()
43 .filter(|call_chain| call_chain.evidence_class == GovernedProvenanceEvidenceClass::Asserted)
44 {
45 strong.call_chain = None;
46 if diagnostics.asserted_call_chain.is_none() {
47 diagnostics.asserted_call_chain = Some(call_chain);
48 }
49 }
50
51 if diagnostics.lineage_references.session_anchor_id.is_none() {
52 diagnostics.lineage_references.session_anchor_id = strong
53 .call_chain
54 .as_ref()
55 .and_then(|call_chain| call_chain.session_anchor_id.clone())
56 .or_else(|| {
57 diagnostics
58 .asserted_call_chain
59 .as_ref()
60 .and_then(|call_chain| call_chain.session_anchor_id.clone())
61 });
62 }
63 if diagnostics
64 .lineage_references
65 .receipt_lineage_statement_id
66 .is_none()
67 {
68 diagnostics.lineage_references.receipt_lineage_statement_id = strong
69 .call_chain
70 .as_ref()
71 .and_then(|call_chain| call_chain.receipt_lineage_statement_id.clone())
72 .or_else(|| {
73 diagnostics
74 .asserted_call_chain
75 .as_ref()
76 .and_then(|call_chain| call_chain.receipt_lineage_statement_id.clone())
77 });
78 }
79
80 GovernedTransactionProjection {
81 strong,
82 diagnostics: (!diagnostics.is_empty()).then_some(diagnostics),
83 }
84}
85
86fn apply_persisted_lineage_references(
87 projection: &mut GovernedTransactionProjection,
88 persisted: &PersistedLineageReferences,
89) {
90 if persisted.statement_id.is_none() && persisted.session_anchor_id.is_none() {
91 return;
92 }
93
94 if let Some(call_chain) = projection.strong.call_chain.as_mut() {
95 if call_chain.session_anchor_id.is_none() {
96 call_chain.session_anchor_id = persisted.session_anchor_id.clone();
97 }
98 if call_chain.receipt_lineage_statement_id.is_none() {
99 call_chain.receipt_lineage_statement_id = persisted.statement_id.clone();
100 }
101 }
102
103 let diagnostics = projection
104 .diagnostics
105 .get_or_insert_with(GovernedTransactionDiagnostics::default);
106 if let Some(call_chain) = diagnostics.asserted_call_chain.as_mut() {
107 if call_chain.session_anchor_id.is_none() {
108 call_chain.session_anchor_id = persisted.session_anchor_id.clone();
109 }
110 if call_chain.receipt_lineage_statement_id.is_none() {
111 call_chain.receipt_lineage_statement_id = persisted.statement_id.clone();
112 }
113 }
114 if diagnostics.lineage_references.session_anchor_id.is_none() {
115 diagnostics.lineage_references.session_anchor_id = persisted.session_anchor_id.clone();
116 }
117 if diagnostics
118 .lineage_references
119 .receipt_lineage_statement_id
120 .is_none()
121 {
122 diagnostics.lineage_references.receipt_lineage_statement_id =
123 persisted.statement_id.clone();
124 }
125 if diagnostics.is_empty() {
126 projection.diagnostics = None;
127 }
128}
129
130fn call_chain_evidence_class(
131 projection: &GovernedTransactionProjection,
132) -> Option<GovernedProvenanceEvidenceClass> {
133 projection
134 .strong
135 .call_chain
136 .as_ref()
137 .map(|call_chain| call_chain.evidence_class)
138 .or_else(|| {
139 projection
140 .diagnostics
141 .as_ref()
142 .and_then(|diagnostics| diagnostics.asserted_call_chain.as_ref())
143 .map(|call_chain| call_chain.evidence_class)
144 })
145}
146
147fn lineage_references_from_projection(
148 projection: &GovernedTransactionProjection,
149) -> Option<&EvidenceLineageReferences> {
150 projection
151 .diagnostics
152 .as_ref()
153 .map(|diagnostics| &diagnostics.lineage_references)
154 .filter(|references| !references.is_empty())
155}
156
157fn authorization_transaction_context_from_projection(
158 projection: &GovernedTransactionProjection,
159) -> chio_kernel::operator_report::GovernedAuthorizationTransactionContext {
160 let mut transaction_context =
161 authorization_transaction_context_from_governed_metadata(&projection.strong);
162 if transaction_context.call_chain.is_none() {
163 transaction_context.call_chain = projection
164 .diagnostics
165 .as_ref()
166 .and_then(|diagnostics| diagnostics.asserted_call_chain.clone());
167 }
168 transaction_context
169}
170
171impl SqliteReceiptStore {
172 pub fn query_receipt_analytics(
173 &self,
174 query: &ReceiptAnalyticsQuery,
175 ) -> Result<ReceiptAnalyticsResponse, ReceiptStoreError> {
176 let group_limit = query
177 .group_limit
178 .unwrap_or(50)
179 .clamp(1, MAX_ANALYTICS_GROUP_LIMIT);
180 let time_bucket = query.time_bucket.unwrap_or(AnalyticsTimeBucket::Day);
181 let bucket_width = time_bucket.width_secs() as i64;
182
183 let capability_id = query.capability_id.as_deref();
184 let tool_server = query.tool_server.as_deref();
185 let tool_name = query.tool_name.as_deref();
186 let since = query.since.map(|value| value as i64);
187 let until = query.until.map(|value| value as i64);
188 let agent_subject = query.agent_subject.as_deref();
189
190 let summary_sql = r#"
191 SELECT
192 COUNT(*) AS total_receipts,
193 COALESCE(SUM(CASE WHEN r.decision_kind = 'allow' THEN 1 ELSE 0 END), 0) AS allow_count,
194 COALESCE(SUM(CASE WHEN r.decision_kind = 'deny' THEN 1 ELSE 0 END), 0) AS deny_count,
195 COALESCE(SUM(CASE WHEN r.decision_kind = 'cancelled' THEN 1 ELSE 0 END), 0) AS cancelled_count,
196 COALESCE(SUM(CASE WHEN r.decision_kind = 'incomplete' THEN 1 ELSE 0 END), 0) AS incomplete_count,
197 COALESCE(SUM(CAST(COALESCE(json_extract(r.raw_json, '$.metadata.financial.cost_charged'), 0) AS INTEGER)), 0) AS total_cost_charged,
198 COALESCE(SUM(CAST(COALESCE(json_extract(r.raw_json, '$.metadata.financial.attempted_cost'), 0) AS INTEGER)), 0) AS total_attempted_cost
199 FROM chio_tool_receipts r
200 LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
201 WHERE (?1 IS NULL OR r.capability_id = ?1)
202 AND (?2 IS NULL OR r.tool_server = ?2)
203 AND (?3 IS NULL OR r.tool_name = ?3)
204 AND (?4 IS NULL OR r.timestamp >= ?4)
205 AND (?5 IS NULL OR r.timestamp <= ?5)
206 AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
207 "#;
208 let summary = self.connection()?.query_row(
209 summary_sql,
210 params![
211 capability_id,
212 tool_server,
213 tool_name,
214 since,
215 until,
216 agent_subject
217 ],
218 |row| {
219 Ok(ReceiptAnalyticsMetrics::from_raw(
220 row.get::<_, i64>(0)?.max(0) as u64,
221 row.get::<_, i64>(1)?.max(0) as u64,
222 row.get::<_, i64>(2)?.max(0) as u64,
223 row.get::<_, i64>(3)?.max(0) as u64,
224 row.get::<_, i64>(4)?.max(0) as u64,
225 row.get::<_, i64>(5)?.max(0) as u64,
226 row.get::<_, i64>(6)?.max(0) as u64,
227 ))
228 },
229 )?;
230
231 let by_agent_sql = r#"
232 SELECT
233 COALESCE(r.subject_key, cl.subject_key) AS subject_key,
234 COUNT(*) AS total_receipts,
235 COALESCE(SUM(CASE WHEN r.decision_kind = 'allow' THEN 1 ELSE 0 END), 0) AS allow_count,
236 COALESCE(SUM(CASE WHEN r.decision_kind = 'deny' THEN 1 ELSE 0 END), 0) AS deny_count,
237 COALESCE(SUM(CASE WHEN r.decision_kind = 'cancelled' THEN 1 ELSE 0 END), 0) AS cancelled_count,
238 COALESCE(SUM(CASE WHEN r.decision_kind = 'incomplete' THEN 1 ELSE 0 END), 0) AS incomplete_count,
239 COALESCE(SUM(CAST(COALESCE(json_extract(r.raw_json, '$.metadata.financial.cost_charged'), 0) AS INTEGER)), 0) AS total_cost_charged,
240 COALESCE(SUM(CAST(COALESCE(json_extract(r.raw_json, '$.metadata.financial.attempted_cost'), 0) AS INTEGER)), 0) AS total_attempted_cost
241 FROM chio_tool_receipts r
242 LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
243 WHERE (?1 IS NULL OR r.capability_id = ?1)
244 AND (?2 IS NULL OR r.tool_server = ?2)
245 AND (?3 IS NULL OR r.tool_name = ?3)
246 AND (?4 IS NULL OR r.timestamp >= ?4)
247 AND (?5 IS NULL OR r.timestamp <= ?5)
248 AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
249 AND COALESCE(r.subject_key, cl.subject_key) IS NOT NULL
250 GROUP BY COALESCE(r.subject_key, cl.subject_key)
251 ORDER BY total_receipts DESC, subject_key ASC
252 LIMIT ?7
253 "#;
254 let by_agent = self
255 .connection()?
256 .prepare(by_agent_sql)?
257 .query_map(
258 params![
259 capability_id,
260 tool_server,
261 tool_name,
262 since,
263 until,
264 agent_subject,
265 group_limit as i64
266 ],
267 |row| {
268 Ok(AgentAnalyticsRow {
269 subject_key: row.get(0)?,
270 metrics: ReceiptAnalyticsMetrics::from_raw(
271 row.get::<_, i64>(1)?.max(0) as u64,
272 row.get::<_, i64>(2)?.max(0) as u64,
273 row.get::<_, i64>(3)?.max(0) as u64,
274 row.get::<_, i64>(4)?.max(0) as u64,
275 row.get::<_, i64>(5)?.max(0) as u64,
276 row.get::<_, i64>(6)?.max(0) as u64,
277 row.get::<_, i64>(7)?.max(0) as u64,
278 ),
279 })
280 },
281 )?
282 .collect::<Result<Vec<_>, _>>()?;
283
284 let by_tool_sql = r#"
285 SELECT
286 r.tool_server,
287 r.tool_name,
288 COUNT(*) AS total_receipts,
289 COALESCE(SUM(CASE WHEN r.decision_kind = 'allow' THEN 1 ELSE 0 END), 0) AS allow_count,
290 COALESCE(SUM(CASE WHEN r.decision_kind = 'deny' THEN 1 ELSE 0 END), 0) AS deny_count,
291 COALESCE(SUM(CASE WHEN r.decision_kind = 'cancelled' THEN 1 ELSE 0 END), 0) AS cancelled_count,
292 COALESCE(SUM(CASE WHEN r.decision_kind = 'incomplete' THEN 1 ELSE 0 END), 0) AS incomplete_count,
293 COALESCE(SUM(CAST(COALESCE(json_extract(r.raw_json, '$.metadata.financial.cost_charged'), 0) AS INTEGER)), 0) AS total_cost_charged,
294 COALESCE(SUM(CAST(COALESCE(json_extract(r.raw_json, '$.metadata.financial.attempted_cost'), 0) AS INTEGER)), 0) AS total_attempted_cost
295 FROM chio_tool_receipts r
296 LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
297 WHERE (?1 IS NULL OR r.capability_id = ?1)
298 AND (?2 IS NULL OR r.tool_server = ?2)
299 AND (?3 IS NULL OR r.tool_name = ?3)
300 AND (?4 IS NULL OR r.timestamp >= ?4)
301 AND (?5 IS NULL OR r.timestamp <= ?5)
302 AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
303 GROUP BY r.tool_server, r.tool_name
304 ORDER BY total_receipts DESC, r.tool_server ASC, r.tool_name ASC
305 LIMIT ?7
306 "#;
307 let by_tool = self
308 .connection()?
309 .prepare(by_tool_sql)?
310 .query_map(
311 params![
312 capability_id,
313 tool_server,
314 tool_name,
315 since,
316 until,
317 agent_subject,
318 group_limit as i64
319 ],
320 |row| {
321 Ok(ToolAnalyticsRow {
322 tool_server: row.get(0)?,
323 tool_name: row.get(1)?,
324 metrics: ReceiptAnalyticsMetrics::from_raw(
325 row.get::<_, i64>(2)?.max(0) as u64,
326 row.get::<_, i64>(3)?.max(0) as u64,
327 row.get::<_, i64>(4)?.max(0) as u64,
328 row.get::<_, i64>(5)?.max(0) as u64,
329 row.get::<_, i64>(6)?.max(0) as u64,
330 row.get::<_, i64>(7)?.max(0) as u64,
331 row.get::<_, i64>(8)?.max(0) as u64,
332 ),
333 })
334 },
335 )?
336 .collect::<Result<Vec<_>, _>>()?;
337
338 let by_time_sql = r#"
339 SELECT
340 CAST((r.timestamp / ?7) * ?7 AS INTEGER) AS bucket_start,
341 COUNT(*) AS total_receipts,
342 COALESCE(SUM(CASE WHEN r.decision_kind = 'allow' THEN 1 ELSE 0 END), 0) AS allow_count,
343 COALESCE(SUM(CASE WHEN r.decision_kind = 'deny' THEN 1 ELSE 0 END), 0) AS deny_count,
344 COALESCE(SUM(CASE WHEN r.decision_kind = 'cancelled' THEN 1 ELSE 0 END), 0) AS cancelled_count,
345 COALESCE(SUM(CASE WHEN r.decision_kind = 'incomplete' THEN 1 ELSE 0 END), 0) AS incomplete_count,
346 COALESCE(SUM(CAST(COALESCE(json_extract(r.raw_json, '$.metadata.financial.cost_charged'), 0) AS INTEGER)), 0) AS total_cost_charged,
347 COALESCE(SUM(CAST(COALESCE(json_extract(r.raw_json, '$.metadata.financial.attempted_cost'), 0) AS INTEGER)), 0) AS total_attempted_cost
348 FROM chio_tool_receipts r
349 LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
350 WHERE (?1 IS NULL OR r.capability_id = ?1)
351 AND (?2 IS NULL OR r.tool_server = ?2)
352 AND (?3 IS NULL OR r.tool_name = ?3)
353 AND (?4 IS NULL OR r.timestamp >= ?4)
354 AND (?5 IS NULL OR r.timestamp <= ?5)
355 AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
356 GROUP BY bucket_start
357 ORDER BY bucket_start ASC
358 LIMIT ?8
359 "#;
360 let by_time = self
361 .connection()?
362 .prepare(by_time_sql)?
363 .query_map(
364 params![
365 capability_id,
366 tool_server,
367 tool_name,
368 since,
369 until,
370 agent_subject,
371 bucket_width,
372 group_limit as i64
373 ],
374 |row| {
375 let bucket_start = row.get::<_, i64>(0)?.max(0) as u64;
376 Ok(TimeAnalyticsRow {
377 bucket_start,
378 bucket_end: bucket_start
379 .saturating_add(bucket_width.max(1) as u64)
380 .saturating_sub(1),
381 metrics: ReceiptAnalyticsMetrics::from_raw(
382 row.get::<_, i64>(1)?.max(0) as u64,
383 row.get::<_, i64>(2)?.max(0) as u64,
384 row.get::<_, i64>(3)?.max(0) as u64,
385 row.get::<_, i64>(4)?.max(0) as u64,
386 row.get::<_, i64>(5)?.max(0) as u64,
387 row.get::<_, i64>(6)?.max(0) as u64,
388 row.get::<_, i64>(7)?.max(0) as u64,
389 ),
390 })
391 },
392 )?
393 .collect::<Result<Vec<_>, _>>()?;
394
395 Ok(ReceiptAnalyticsResponse {
396 summary,
397 by_agent,
398 by_tool,
399 by_time,
400 })
401 }
402
403 pub fn query_cost_attribution_report(
404 &self,
405 query: &CostAttributionQuery,
406 ) -> Result<CostAttributionReport, ReceiptStoreError> {
407 let limit = query
408 .limit
409 .unwrap_or(100)
410 .clamp(1, MAX_COST_ATTRIBUTION_LIMIT);
411 let capability_id = query.capability_id.as_deref();
412 let tool_server = query.tool_server.as_deref();
413 let tool_name = query.tool_name.as_deref();
414 let since = query.since.map(|value| value as i64);
415 let until = query.until.map(|value| value as i64);
416 let agent_subject = query.agent_subject.as_deref();
417
418 let count_sql = r#"
419 SELECT COUNT(*)
420 FROM chio_tool_receipts r
421 LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
422 WHERE (?1 IS NULL OR r.capability_id = ?1)
423 AND (?2 IS NULL OR r.tool_server = ?2)
424 AND (?3 IS NULL OR r.tool_name = ?3)
425 AND (?4 IS NULL OR r.timestamp >= ?4)
426 AND (?5 IS NULL OR r.timestamp <= ?5)
427 AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
428 AND json_type(r.raw_json, '$.metadata.financial') = 'object'
429 "#;
430
431 let matching_receipts = self
432 .connection()?
433 .query_row(
434 count_sql,
435 params![
436 capability_id,
437 tool_server,
438 tool_name,
439 since,
440 until,
441 agent_subject
442 ],
443 |row| row.get::<_, i64>(0),
444 )
445 .map(|value| value.max(0) as u64)?;
446
447 let data_sql = r#"
448 SELECT r.seq, r.raw_json
449 FROM chio_tool_receipts r
450 LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
451 WHERE (?1 IS NULL OR r.capability_id = ?1)
452 AND (?2 IS NULL OR r.tool_server = ?2)
453 AND (?3 IS NULL OR r.tool_name = ?3)
454 AND (?4 IS NULL OR r.timestamp >= ?4)
455 AND (?5 IS NULL OR r.timestamp <= ?5)
456 AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
457 AND json_type(r.raw_json, '$.metadata.financial') = 'object'
458 ORDER BY r.seq ASC
459 "#;
460
461 let rows = self
462 .connection()?
463 .prepare(data_sql)?
464 .query_map(
465 params![
466 capability_id,
467 tool_server,
468 tool_name,
469 since,
470 until,
471 agent_subject
472 ],
473 |row| {
474 Ok((
475 row.get::<_, i64>(0)?.max(0) as u64,
476 row.get::<_, String>(1)?,
477 ))
478 },
479 )?
480 .collect::<Result<Vec<_>, _>>()?;
481
482 let mut receipts = Vec::with_capacity(rows.len().min(limit));
483 let mut by_root = BTreeMap::<String, RootAggregate>::new();
484 let mut by_leaf = BTreeMap::<(String, String), LeafAggregate>::new();
485 let mut distinct_roots = BTreeSet::new();
486 let mut distinct_leaves = BTreeSet::new();
487 let mut total_cost_charged = 0_u64;
488 let mut total_attempted_cost = 0_u64;
489 let mut max_delegation_depth = 0_u64;
490 let mut lineage_gap_count = 0_u64;
491
492 for (seq, raw_json) in rows {
493 let receipt =
494 decode_verified_chio_receipt(&raw_json, "persisted tool receipt", Some(seq))?;
495 let Some(financial) = extract_financial_metadata(&receipt) else {
496 continue;
497 };
498 let attribution = extract_receipt_attribution(&receipt);
499 let chain_snapshots = self
500 .get_combined_delegation_chain(&receipt.capability_id)
501 .unwrap_or_default();
502 let lineage_complete = chain_is_complete(&receipt.capability_id, &chain_snapshots);
503 if !lineage_complete {
504 lineage_gap_count = lineage_gap_count.saturating_add(1);
505 }
506
507 let chain = chain_snapshots
508 .iter()
509 .map(|snapshot| CostAttributionChainHop {
510 capability_id: snapshot.capability_id.clone(),
511 subject_key: snapshot.subject_key.clone(),
512 issuer_key: snapshot.issuer_key.clone(),
513 delegation_depth: snapshot.delegation_depth,
514 parent_capability_id: snapshot.parent_capability_id.clone(),
515 })
516 .collect::<Vec<_>>();
517
518 let root_subject_key = chain_snapshots
519 .first()
520 .map(|snapshot| snapshot.subject_key.clone())
521 .or_else(|| Some(financial.root_budget_holder.clone()));
522 let leaf_subject_key = attribution.subject_key.clone().or_else(|| {
523 chain_snapshots
524 .last()
525 .map(|snapshot| snapshot.subject_key.clone())
526 });
527 let attempted_cost = financial.attempted_cost.unwrap_or(0);
528 let decision = decision_kind(&receipt.decision).to_string();
529
530 total_cost_charged = total_cost_charged.saturating_add(financial.cost_charged);
531 total_attempted_cost = total_attempted_cost.saturating_add(attempted_cost);
532 max_delegation_depth = max_delegation_depth.max(financial.delegation_depth as u64);
533
534 if let Some(root_key) = root_subject_key.clone() {
535 distinct_roots.insert(root_key.clone());
536 let root_entry = by_root.entry(root_key.clone()).or_default();
537 root_entry.receipt_count = root_entry.receipt_count.saturating_add(1);
538 root_entry.total_cost_charged = root_entry
539 .total_cost_charged
540 .saturating_add(financial.cost_charged);
541 root_entry.total_attempted_cost = root_entry
542 .total_attempted_cost
543 .saturating_add(attempted_cost);
544 root_entry.max_delegation_depth = root_entry
545 .max_delegation_depth
546 .max(financial.delegation_depth as u64);
547
548 if let Some(leaf_key) = leaf_subject_key.clone() {
549 root_entry.leaf_subjects.insert(leaf_key.clone());
550 let leaf_entry = by_leaf.entry((root_key, leaf_key)).or_default();
551 leaf_entry.receipt_count = leaf_entry.receipt_count.saturating_add(1);
552 leaf_entry.total_cost_charged = leaf_entry
553 .total_cost_charged
554 .saturating_add(financial.cost_charged);
555 leaf_entry.total_attempted_cost = leaf_entry
556 .total_attempted_cost
557 .saturating_add(attempted_cost);
558 leaf_entry.max_delegation_depth = leaf_entry
559 .max_delegation_depth
560 .max(financial.delegation_depth as u64);
561 }
562 }
563
564 if let Some(leaf_key) = leaf_subject_key.clone() {
565 distinct_leaves.insert(leaf_key);
566 }
567
568 if receipts.len() < limit {
569 receipts.push(CostAttributionReceiptRow {
570 seq,
571 receipt_id: receipt.id.clone(),
572 timestamp: receipt.timestamp,
573 capability_id: receipt.capability_id.clone(),
574 tool_server: receipt.tool_server.clone(),
575 tool_name: receipt.tool_name.clone(),
576 decision_kind: decision,
577 root_subject_key,
578 leaf_subject_key,
579 grant_index: Some(financial.grant_index),
580 delegation_depth: financial.delegation_depth as u64,
581 cost_charged: financial.cost_charged,
582 attempted_cost: financial.attempted_cost,
583 currency: financial.currency.clone(),
584 budget_total: Some(financial.budget_total),
585 budget_remaining: Some(financial.budget_remaining),
586 settlement_status: Some(financial.settlement_status),
587 payment_reference: financial.payment_reference.clone(),
588 budget_authority: receipt.financial_budget_authority_metadata(),
589 lineage_complete,
590 chain,
591 });
592 }
593 }
594
595 let mut by_root = by_root
596 .into_iter()
597 .map(|(root_subject_key, aggregate)| RootCostAttributionRow {
598 root_subject_key,
599 receipt_count: aggregate.receipt_count,
600 total_cost_charged: aggregate.total_cost_charged,
601 total_attempted_cost: aggregate.total_attempted_cost,
602 distinct_leaf_subjects: aggregate.leaf_subjects.len() as u64,
603 max_delegation_depth: aggregate.max_delegation_depth,
604 })
605 .collect::<Vec<_>>();
606 by_root.sort_by(|left, right| {
607 right
608 .total_cost_charged
609 .cmp(&left.total_cost_charged)
610 .then_with(|| right.receipt_count.cmp(&left.receipt_count))
611 .then_with(|| left.root_subject_key.cmp(&right.root_subject_key))
612 });
613
614 let mut by_leaf = by_leaf
615 .into_iter()
616 .map(
617 |((root_subject_key, leaf_subject_key), aggregate)| LeafCostAttributionRow {
618 root_subject_key,
619 leaf_subject_key,
620 receipt_count: aggregate.receipt_count,
621 total_cost_charged: aggregate.total_cost_charged,
622 total_attempted_cost: aggregate.total_attempted_cost,
623 max_delegation_depth: aggregate.max_delegation_depth,
624 },
625 )
626 .collect::<Vec<_>>();
627 by_leaf.sort_by(|left, right| {
628 right
629 .total_cost_charged
630 .cmp(&left.total_cost_charged)
631 .then_with(|| right.receipt_count.cmp(&left.receipt_count))
632 .then_with(|| left.root_subject_key.cmp(&right.root_subject_key))
633 .then_with(|| left.leaf_subject_key.cmp(&right.leaf_subject_key))
634 });
635
636 Ok(CostAttributionReport {
637 summary: CostAttributionSummary {
638 matching_receipts,
639 returned_receipts: receipts.len() as u64,
640 total_cost_charged,
641 total_attempted_cost,
642 max_delegation_depth,
643 distinct_root_subjects: distinct_roots.len() as u64,
644 distinct_leaf_subjects: distinct_leaves.len() as u64,
645 lineage_gap_count,
646 truncated: matching_receipts > receipts.len() as u64,
647 },
648 by_root,
649 by_leaf,
650 receipts,
651 })
652 }
653
654 pub fn query_shared_evidence_report(
655 &self,
656 query: &SharedEvidenceQuery,
657 ) -> Result<SharedEvidenceReferenceReport, ReceiptStoreError> {
658 let limit = query.limit_or_default();
659 let capability_id = query.capability_id.as_deref();
660 let tool_server = query.tool_server.as_deref();
661 let tool_name = query.tool_name.as_deref();
662 let since = query.since.map(|value| value as i64);
663 let until = query.until.map(|value| value as i64);
664 let agent_subject = query.agent_subject.as_deref();
665 let issuer = query.issuer.as_deref();
666 let partner = query.partner.as_deref();
667
668 let rows = self
669 .connection()?
670 .prepare(
671 r#"
672 SELECT r.receipt_id, r.timestamp, r.capability_id, r.decision_kind
673 FROM chio_tool_receipts r
674 LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
675 WHERE (?1 IS NULL OR r.capability_id = ?1)
676 AND (?2 IS NULL OR r.tool_server = ?2)
677 AND (?3 IS NULL OR r.tool_name = ?3)
678 AND (?4 IS NULL OR r.timestamp >= ?4)
679 AND (?5 IS NULL OR r.timestamp <= ?5)
680 AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
681 ORDER BY r.seq ASC
682 "#,
683 )?
684 .query_map(
685 params![
686 capability_id,
687 tool_server,
688 tool_name,
689 since,
690 until,
691 agent_subject
692 ],
693 |row| {
694 Ok((
695 row.get::<_, String>(0)?,
696 row.get::<_, i64>(1)?.max(0) as u64,
697 row.get::<_, String>(2)?,
698 row.get::<_, String>(3)?,
699 ))
700 },
701 )?
702 .collect::<Result<Vec<_>, _>>()?;
703
704 let mut share_cache = BTreeMap::<String, Option<FederatedEvidenceShareSummary>>::new();
705 let mut references = BTreeMap::<(String, String), SharedEvidenceReferenceRow>::new();
706 let mut matched_local_receipts = BTreeSet::<String>::new();
707
708 for (receipt_id, timestamp, local_capability_id, decision) in rows {
709 let chain = self.get_combined_delegation_chain(&local_capability_id)?;
710 if chain.is_empty() {
711 continue;
712 }
713
714 let mut matched_this_receipt = false;
715 for (index, snapshot) in chain.iter().enumerate() {
716 let share = match share_cache.get(&snapshot.capability_id) {
717 Some(cached) => cached.clone(),
718 None => {
719 let loaded = self
720 .get_federated_share_for_capability(&snapshot.capability_id)?
721 .map(|(share, _)| share);
722 share_cache.insert(snapshot.capability_id.clone(), loaded.clone());
723 loaded
724 }
725 };
726 let Some(share) = share else {
727 continue;
728 };
729 if issuer.is_some_and(|expected| share.issuer != expected) {
730 continue;
731 }
732 if partner.is_some_and(|expected| share.partner != expected) {
733 continue;
734 }
735
736 let local_anchor_capability_id =
737 chain.iter().skip(index + 1).find_map(|candidate| {
738 match share_cache.get(&candidate.capability_id) {
739 Some(Some(_)) => None,
740 Some(None) => Some(candidate.capability_id.clone()),
741 None => {
742 let loaded = self
743 .get_federated_share_for_capability(&candidate.capability_id)
744 .ok()
745 .and_then(|value| value.map(|(share, _)| share));
746 share_cache.insert(candidate.capability_id.clone(), loaded.clone());
747 if loaded.is_some() {
748 None
749 } else {
750 Some(candidate.capability_id.clone())
751 }
752 }
753 }
754 });
755
756 let key = (share.share_id.clone(), snapshot.capability_id.clone());
757 let entry = references
758 .entry(key)
759 .or_insert_with(|| SharedEvidenceReferenceRow {
760 share: share.clone(),
761 capability_id: snapshot.capability_id.clone(),
762 subject_key: snapshot.subject_key.clone(),
763 issuer_key: snapshot.issuer_key.clone(),
764 delegation_depth: snapshot.delegation_depth,
765 parent_capability_id: snapshot.parent_capability_id.clone(),
766 local_anchor_capability_id: local_anchor_capability_id.clone(),
767 matched_local_receipts: 0,
768 allow_count: 0,
769 deny_count: 0,
770 cancelled_count: 0,
771 incomplete_count: 0,
772 first_seen: Some(timestamp),
773 last_seen: Some(timestamp),
774 });
775
776 entry.local_anchor_capability_id = entry
777 .local_anchor_capability_id
778 .clone()
779 .or(local_anchor_capability_id);
780 entry.matched_local_receipts = entry.matched_local_receipts.saturating_add(1);
781 entry.first_seen = Some(
782 entry
783 .first_seen
784 .map_or(timestamp, |value| value.min(timestamp)),
785 );
786 entry.last_seen = Some(
787 entry
788 .last_seen
789 .map_or(timestamp, |value| value.max(timestamp)),
790 );
791 match decision.as_str() {
792 "allow" => entry.allow_count = entry.allow_count.saturating_add(1),
793 "deny" => entry.deny_count = entry.deny_count.saturating_add(1),
794 "cancelled" => entry.cancelled_count = entry.cancelled_count.saturating_add(1),
795 _ => entry.incomplete_count = entry.incomplete_count.saturating_add(1),
796 }
797 matched_this_receipt = true;
798 }
799
800 if matched_this_receipt {
801 matched_local_receipts.insert(receipt_id);
802 }
803 }
804
805 let mut returned_references = references.into_values().collect::<Vec<_>>();
806 returned_references.sort_by(|left, right| {
807 right
808 .matched_local_receipts
809 .cmp(&left.matched_local_receipts)
810 .then_with(|| right.last_seen.cmp(&left.last_seen))
811 .then_with(|| right.share.imported_at.cmp(&left.share.imported_at))
812 .then_with(|| left.share.share_id.cmp(&right.share.share_id))
813 .then_with(|| left.capability_id.cmp(&right.capability_id))
814 });
815
816 let mut distinct_shares = BTreeMap::<String, FederatedEvidenceShareSummary>::new();
817 let mut distinct_remote_subjects = BTreeSet::<String>::new();
818 for reference in &returned_references {
819 distinct_shares
820 .entry(reference.share.share_id.clone())
821 .or_insert_with(|| reference.share.clone());
822 distinct_remote_subjects.insert(reference.subject_key.clone());
823 }
824
825 let matching_references = returned_references.len() as u64;
826 let truncated = returned_references.len() > limit;
827 if truncated {
828 returned_references.truncate(limit);
829 }
830
831 Ok(SharedEvidenceReferenceReport {
832 summary: SharedEvidenceReferenceSummary {
833 matching_shares: distinct_shares.len() as u64,
834 matching_references,
835 matching_local_receipts: matched_local_receipts.len() as u64,
836 remote_tool_receipts: distinct_shares
837 .values()
838 .map(|share| share.tool_receipts)
839 .sum(),
840 remote_lineage_records: distinct_shares
841 .values()
842 .map(|share| share.capability_lineage)
843 .sum(),
844 distinct_remote_subjects: distinct_remote_subjects.len() as u64,
845 proof_required_shares: distinct_shares
846 .values()
847 .filter(|share| share.require_proofs)
848 .count() as u64,
849 truncated,
850 },
851 references: returned_references,
852 })
853 }
854
855 pub fn query_compliance_report(
856 &self,
857 query: &OperatorReportQuery,
858 ) -> Result<ComplianceReport, ReceiptStoreError> {
859 let capability_id = query.capability_id.as_deref();
860 let tool_server = query.tool_server.as_deref();
861 let tool_name = query.tool_name.as_deref();
862 let since = query.since.map(|value| value as i64);
863 let until = query.until.map(|value| value as i64);
864 let agent_subject = query.agent_subject.as_deref();
865
866 let summary_sql = r#"
867 SELECT
868 COUNT(*) AS matching_receipts,
869 COALESCE(SUM(
870 CASE
871 WHEN EXISTS(
872 SELECT 1
873 FROM kernel_checkpoints kc
874 WHERE r.seq BETWEEN kc.batch_start_seq AND kc.batch_end_seq
875 ) THEN 1
876 ELSE 0
877 END
878 ), 0) AS evidence_ready_receipts,
879 COALESCE(SUM(CASE WHEN cl.capability_id IS NOT NULL THEN 1 ELSE 0 END), 0) AS lineage_covered_receipts,
880 COALESCE(SUM(
881 CASE
882 WHEN json_extract(r.raw_json, '$.metadata.financial.settlement_status') = 'pending' THEN 1
883 ELSE 0
884 END
885 ), 0) AS pending_settlement_receipts,
886 COALESCE(SUM(
887 CASE
888 WHEN json_extract(r.raw_json, '$.metadata.financial.settlement_status') = 'failed' THEN 1
889 ELSE 0
890 END
891 ), 0) AS failed_settlement_receipts
892 FROM chio_tool_receipts r
893 LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
894 WHERE (?1 IS NULL OR r.capability_id = ?1)
895 AND (?2 IS NULL OR r.tool_server = ?2)
896 AND (?3 IS NULL OR r.tool_name = ?3)
897 AND (?4 IS NULL OR r.timestamp >= ?4)
898 AND (?5 IS NULL OR r.timestamp <= ?5)
899 AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
900 "#;
901
902 let (
903 matching_receipts,
904 evidence_ready_receipts,
905 lineage_covered_receipts,
906 pending_settlement_receipts,
907 failed_settlement_receipts,
908 ) = self.connection()?.query_row(
909 summary_sql,
910 params![
911 capability_id,
912 tool_server,
913 tool_name,
914 since,
915 until,
916 agent_subject
917 ],
918 |row| {
919 Ok((
920 row.get::<_, i64>(0)?.max(0) as u64,
921 row.get::<_, i64>(1)?.max(0) as u64,
922 row.get::<_, i64>(2)?.max(0) as u64,
923 row.get::<_, i64>(3)?.max(0) as u64,
924 row.get::<_, i64>(4)?.max(0) as u64,
925 ))
926 },
927 )?;
928
929 let uncheckpointed_receipts = matching_receipts.saturating_sub(evidence_ready_receipts);
930 let lineage_gap_receipts = matching_receipts.saturating_sub(lineage_covered_receipts);
931 let export_query = query.to_evidence_export_query();
932
933 Ok(ComplianceReport {
934 matching_receipts,
935 evidence_ready_receipts,
936 uncheckpointed_receipts,
937 checkpoint_coverage_rate: ratio_option(evidence_ready_receipts, matching_receipts),
938 lineage_covered_receipts,
939 lineage_gap_receipts,
940 lineage_coverage_rate: ratio_option(lineage_covered_receipts, matching_receipts),
941 pending_settlement_receipts,
942 failed_settlement_receipts,
943 direct_evidence_export_supported: query.direct_evidence_export_supported(),
944 child_receipt_scope: export_query.child_receipt_scope(),
945 proofs_complete: uncheckpointed_receipts == 0,
946 export_query: export_query.clone(),
947 export_scope_note: compliance_export_scope_note(query, &export_query),
948 })
949 }
950
951 pub fn upsert_settlement_reconciliation(
952 &self,
953 receipt_id: &str,
954 reconciliation_state: SettlementReconciliationState,
955 note: Option<&str>,
956 ) -> Result<i64, ReceiptStoreError> {
957 let exists = self
958 .connection()?
959 .query_row(
960 "SELECT 1 FROM chio_tool_receipts WHERE receipt_id = ?1",
961 params![receipt_id],
962 |row| row.get::<_, i64>(0),
963 )
964 .optional()?;
965 if exists.is_none() {
966 return Err(ReceiptStoreError::NotFound(format!(
967 "receipt {receipt_id} does not exist"
968 )));
969 }
970
971 let updated_at = unix_timestamp_now_i64();
972 self.connection()?.execute(
973 r#"
974 INSERT INTO settlement_reconciliations (
975 receipt_id,
976 reconciliation_state,
977 note,
978 updated_at
979 ) VALUES (?1, ?2, ?3, ?4)
980 ON CONFLICT(receipt_id) DO UPDATE SET
981 reconciliation_state = excluded.reconciliation_state,
982 note = excluded.note,
983 updated_at = excluded.updated_at
984 "#,
985 params![
986 receipt_id,
987 settlement_reconciliation_state_text(reconciliation_state),
988 note,
989 updated_at
990 ],
991 )?;
992
993 Ok(updated_at)
994 }
995
996 pub fn upsert_metered_billing_reconciliation(
997 &self,
998 receipt_id: &str,
999 evidence: &MeteredBillingEvidenceRecord,
1000 reconciliation_state: MeteredBillingReconciliationState,
1001 note: Option<&str>,
1002 ) -> Result<i64, ReceiptStoreError> {
1003 let (seq, raw_json) = self
1004 .connection()?
1005 .query_row(
1006 "SELECT seq, raw_json FROM chio_tool_receipts WHERE receipt_id = ?1",
1007 params![receipt_id],
1008 |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
1009 )
1010 .optional()?
1011 .ok_or_else(|| {
1012 ReceiptStoreError::NotFound(format!("receipt {receipt_id} does not exist"))
1013 })?;
1014 let receipt = decode_verified_chio_receipt(
1015 &raw_json,
1016 "persisted tool receipt",
1017 Some(seq.max(0) as u64),
1018 )?;
1019 let governed = extract_governed_transaction_metadata(&receipt).ok_or_else(|| {
1020 ReceiptStoreError::Conflict(format!(
1021 "receipt {receipt_id} does not carry governed transaction metadata"
1022 ))
1023 })?;
1024 if governed.metered_billing.is_none() {
1025 return Err(ReceiptStoreError::Conflict(format!(
1026 "receipt {receipt_id} does not carry metered billing context"
1027 )));
1028 }
1029
1030 let existing_receipt = self
1031 .connection()?
1032 .query_row(
1033 r#"
1034 SELECT receipt_id
1035 FROM metered_billing_reconciliations
1036 WHERE adapter_kind = ?1 AND evidence_id = ?2
1037 "#,
1038 params![
1039 &evidence.usage_evidence.evidence_kind,
1040 &evidence.usage_evidence.evidence_id
1041 ],
1042 |row| row.get::<_, String>(0),
1043 )
1044 .optional()?;
1045 if let Some(existing_receipt) = existing_receipt {
1046 if existing_receipt != receipt_id {
1047 return Err(ReceiptStoreError::Conflict(format!(
1048 "metered billing evidence {}/{} is already attached to receipt {}",
1049 evidence.usage_evidence.evidence_kind,
1050 evidence.usage_evidence.evidence_id,
1051 existing_receipt
1052 )));
1053 }
1054 }
1055
1056 let updated_at = unix_timestamp_now_i64();
1057 self.connection()?.execute(
1058 r#"
1059 INSERT INTO metered_billing_reconciliations (
1060 receipt_id,
1061 adapter_kind,
1062 evidence_id,
1063 observed_units,
1064 billed_cost_units,
1065 billed_cost_currency,
1066 evidence_sha256,
1067 recorded_at,
1068 reconciliation_state,
1069 note,
1070 updated_at
1071 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)
1072 ON CONFLICT(receipt_id) DO UPDATE SET
1073 adapter_kind = excluded.adapter_kind,
1074 evidence_id = excluded.evidence_id,
1075 observed_units = excluded.observed_units,
1076 billed_cost_units = excluded.billed_cost_units,
1077 billed_cost_currency = excluded.billed_cost_currency,
1078 evidence_sha256 = excluded.evidence_sha256,
1079 recorded_at = excluded.recorded_at,
1080 reconciliation_state = excluded.reconciliation_state,
1081 note = excluded.note,
1082 updated_at = excluded.updated_at
1083 "#,
1084 params![
1085 receipt_id,
1086 &evidence.usage_evidence.evidence_kind,
1087 &evidence.usage_evidence.evidence_id,
1088 evidence.usage_evidence.observed_units as i64,
1089 evidence.billed_cost.units as i64,
1090 &evidence.billed_cost.currency,
1091 evidence.usage_evidence.evidence_sha256.as_deref(),
1092 evidence.recorded_at as i64,
1093 metered_billing_reconciliation_state_text(reconciliation_state),
1094 note,
1095 updated_at
1096 ],
1097 )?;
1098
1099 Ok(updated_at)
1100 }
1101
1102 pub fn query_metered_billing_reconciliation_report(
1103 &self,
1104 query: &OperatorReportQuery,
1105 ) -> Result<MeteredBillingReconciliationReport, ReceiptStoreError> {
1106 let capability_id = query.capability_id.as_deref();
1107 let tool_server = query.tool_server.as_deref();
1108 let tool_name = query.tool_name.as_deref();
1109 let since = query.since.map(|value| value as i64);
1110 let until = query.until.map(|value| value as i64);
1111 let agent_subject = query.agent_subject.as_deref();
1112 let row_limit = query.metered_limit_or_default();
1113
1114 let summary = self.query_metered_billing_summary(query)?;
1115
1116 let rows_sql = r#"
1117 SELECT
1118 r.seq,
1119 r.raw_json,
1120 COALESCE(r.subject_key, cl.subject_key),
1121 mbr.adapter_kind,
1122 mbr.evidence_id,
1123 mbr.observed_units,
1124 mbr.billed_cost_units,
1125 mbr.billed_cost_currency,
1126 mbr.evidence_sha256,
1127 mbr.recorded_at,
1128 COALESCE(mbr.reconciliation_state, 'open'),
1129 mbr.note,
1130 mbr.updated_at
1131 FROM chio_tool_receipts r
1132 LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
1133 LEFT JOIN metered_billing_reconciliations mbr ON r.receipt_id = mbr.receipt_id
1134 WHERE json_type(r.raw_json, '$.metadata.governed_transaction.metered_billing') = 'object'
1135 AND (?1 IS NULL OR r.capability_id = ?1)
1136 AND (?2 IS NULL OR r.tool_server = ?2)
1137 AND (?3 IS NULL OR r.tool_name = ?3)
1138 AND (?4 IS NULL OR r.timestamp >= ?4)
1139 AND (?5 IS NULL OR r.timestamp <= ?5)
1140 AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
1141 ORDER BY r.timestamp DESC, r.seq DESC
1142 LIMIT ?7
1143 "#;
1144
1145 let connection = self.connection()?;
1146 let mut stmt = connection.prepare(rows_sql)?;
1147 let rows = stmt.query_map(
1148 params![
1149 capability_id,
1150 tool_server,
1151 tool_name,
1152 since,
1153 until,
1154 agent_subject,
1155 row_limit as i64
1156 ],
1157 |row| {
1158 Ok((
1159 row.get::<_, i64>(0)?,
1160 row.get::<_, String>(1)?,
1161 row.get::<_, Option<String>>(2)?,
1162 row.get::<_, Option<String>>(3)?,
1163 row.get::<_, Option<String>>(4)?,
1164 row.get::<_, Option<i64>>(5)?,
1165 row.get::<_, Option<i64>>(6)?,
1166 row.get::<_, Option<String>>(7)?,
1167 row.get::<_, Option<String>>(8)?,
1168 row.get::<_, Option<i64>>(9)?,
1169 row.get::<_, String>(10)?,
1170 row.get::<_, Option<String>>(11)?,
1171 row.get::<_, Option<i64>>(12)?,
1172 ))
1173 },
1174 )?;
1175
1176 let mut receipts = Vec::new();
1177 for row in rows {
1178 let (
1179 seq,
1180 raw_json,
1181 subject_key,
1182 adapter_kind,
1183 evidence_id,
1184 observed_units,
1185 billed_cost_units,
1186 billed_cost_currency,
1187 evidence_sha256,
1188 recorded_at,
1189 reconciliation_state_text,
1190 note,
1191 updated_at,
1192 ) = row?;
1193 let receipt = decode_verified_chio_receipt(
1194 &raw_json,
1195 "persisted tool receipt",
1196 Some(seq.max(0) as u64),
1197 )?;
1198 let governed = extract_governed_transaction_metadata(&receipt).ok_or_else(|| {
1199 ReceiptStoreError::Canonical(format!(
1200 "receipt {} is missing governed transaction metadata",
1201 receipt.id
1202 ))
1203 })?;
1204 let metered = governed.metered_billing.ok_or_else(|| {
1205 ReceiptStoreError::Canonical(format!(
1206 "receipt {} is missing metered billing metadata",
1207 receipt.id
1208 ))
1209 })?;
1210 let financial = extract_financial_metadata(&receipt);
1211 let evidence = metered_billing_evidence_record_from_columns(
1212 adapter_kind,
1213 evidence_id,
1214 observed_units,
1215 billed_cost_units,
1216 billed_cost_currency,
1217 evidence_sha256,
1218 recorded_at,
1219 );
1220 let reconciliation_state =
1221 parse_metered_billing_reconciliation_state(&reconciliation_state_text)?;
1222 let analysis = analyze_metered_billing_reconciliation(
1223 &metered,
1224 financial.as_ref(),
1225 evidence.as_ref(),
1226 reconciliation_state,
1227 );
1228 let budget_authority = receipt.financial_budget_authority_metadata();
1229
1230 receipts.push(MeteredBillingReconciliationRow {
1231 receipt_id: receipt.id,
1232 timestamp: receipt.timestamp,
1233 capability_id: receipt.capability_id,
1234 subject_key,
1235 tool_server: receipt.tool_server,
1236 tool_name: receipt.tool_name,
1237 settlement_mode: metered.settlement_mode,
1238 provider: metered.quote.provider.clone(),
1239 quote_id: metered.quote.quote_id.clone(),
1240 billing_unit: metered.quote.billing_unit.clone(),
1241 quoted_units: metered.quote.quoted_units,
1242 quoted_cost: metered.quote.quoted_cost.clone(),
1243 max_billed_units: metered.max_billed_units,
1244 financial_cost_charged: financial.as_ref().map(|value| value.cost_charged),
1245 financial_currency: financial.as_ref().map(|value| value.currency.clone()),
1246 budget_authority,
1247 evidence,
1248 reconciliation_state,
1249 action_required: analysis.action_required,
1250 evidence_missing: analysis.evidence_missing,
1251 exceeds_quoted_units: analysis.exceeds_quoted_units,
1252 exceeds_max_billed_units: analysis.exceeds_max_billed_units,
1253 exceeds_quoted_cost: analysis.exceeds_quoted_cost,
1254 financial_mismatch: analysis.financial_mismatch,
1255 note,
1256 updated_at: updated_at.map(|value| value.max(0) as u64),
1257 });
1258 }
1259
1260 Ok(MeteredBillingReconciliationReport {
1261 summary: MeteredBillingReconciliationSummary {
1262 matching_receipts: summary.metered_receipts,
1263 returned_receipts: receipts.len() as u64,
1264 evidence_attached_receipts: summary.evidence_attached_receipts,
1265 missing_evidence_receipts: summary.missing_evidence_receipts,
1266 over_quoted_units_receipts: summary.over_quoted_units_receipts,
1267 over_max_billed_units_receipts: summary.over_max_billed_units_receipts,
1268 over_quoted_cost_receipts: summary.over_quoted_cost_receipts,
1269 financial_mismatch_receipts: summary.financial_mismatch_receipts,
1270 actionable_receipts: summary.actionable_receipts,
1271 reconciled_receipts: summary.reconciled_receipts,
1272 truncated: summary.metered_receipts > receipts.len() as u64,
1273 },
1274 receipts,
1275 })
1276 }
1277
1278 pub fn query_economic_receipt_projection_report(
1279 &self,
1280 query: &OperatorReportQuery,
1281 ) -> Result<EconomicReceiptProjectionReport, ReceiptStoreError> {
1282 let capability_id = query.capability_id.as_deref();
1283 let tool_server = query.tool_server.as_deref();
1284 let tool_name = query.tool_name.as_deref();
1285 let since = query.since.map(|value| value as i64);
1286 let until = query.until.map(|value| value as i64);
1287 let agent_subject = query.agent_subject.as_deref();
1288 let row_limit = query.economic_limit_or_default();
1289
1290 let matching_receipts = self.connection()?.query_row(
1291 r#"
1292 SELECT COUNT(*)
1293 FROM chio_tool_receipts r
1294 LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
1295 WHERE json_type(r.raw_json, '$.metadata.governed_transaction.economic_authorization') = 'object'
1296 AND (?1 IS NULL OR r.capability_id = ?1)
1297 AND (?2 IS NULL OR r.tool_server = ?2)
1298 AND (?3 IS NULL OR r.tool_name = ?3)
1299 AND (?4 IS NULL OR r.timestamp >= ?4)
1300 AND (?5 IS NULL OR r.timestamp <= ?5)
1301 AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
1302 "#,
1303 params![
1304 capability_id,
1305 tool_server,
1306 tool_name,
1307 since,
1308 until,
1309 agent_subject
1310 ],
1311 |row| row.get::<_, i64>(0),
1312 )?
1313 .max(0) as u64;
1314
1315 let rows_sql = r#"
1316 SELECT
1317 r.seq,
1318 r.receipt_id,
1319 r.timestamp,
1320 r.capability_id,
1321 COALESCE(r.subject_key, cl.subject_key),
1322 r.tool_server,
1323 r.tool_name,
1324 COALESCE(sr.reconciliation_state, 'open'),
1325 sr.note,
1326 sr.updated_at,
1327 COALESCE(mbr.reconciliation_state, 'open'),
1328 mbr.note,
1329 mbr.updated_at,
1330 mbr.adapter_kind,
1331 mbr.evidence_id,
1332 mbr.observed_units,
1333 mbr.billed_cost_units,
1334 mbr.billed_cost_currency,
1335 mbr.evidence_sha256,
1336 mbr.recorded_at,
1337 r.raw_json
1338 FROM chio_tool_receipts r
1339 LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
1340 LEFT JOIN settlement_reconciliations sr ON r.receipt_id = sr.receipt_id
1341 LEFT JOIN metered_billing_reconciliations mbr ON r.receipt_id = mbr.receipt_id
1342 WHERE json_type(r.raw_json, '$.metadata.governed_transaction.economic_authorization') = 'object'
1343 AND (?1 IS NULL OR r.capability_id = ?1)
1344 AND (?2 IS NULL OR r.tool_server = ?2)
1345 AND (?3 IS NULL OR r.tool_name = ?3)
1346 AND (?4 IS NULL OR r.timestamp >= ?4)
1347 AND (?5 IS NULL OR r.timestamp <= ?5)
1348 AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
1349 ORDER BY r.timestamp DESC, r.seq DESC
1350 LIMIT ?7
1351 "#;
1352
1353 let connection = self.connection()?;
1354 let mut stmt = connection.prepare(rows_sql)?;
1355 let rows = stmt.query_map(
1356 params![
1357 capability_id,
1358 tool_server,
1359 tool_name,
1360 since,
1361 until,
1362 agent_subject,
1363 row_limit as i64
1364 ],
1365 |row| {
1366 Ok((
1367 row.get::<_, i64>(0)?,
1368 row.get::<_, String>(1)?,
1369 row.get::<_, i64>(2)?,
1370 row.get::<_, String>(3)?,
1371 row.get::<_, Option<String>>(4)?,
1372 row.get::<_, String>(5)?,
1373 row.get::<_, String>(6)?,
1374 row.get::<_, String>(7)?,
1375 row.get::<_, Option<String>>(8)?,
1376 row.get::<_, Option<i64>>(9)?,
1377 row.get::<_, String>(10)?,
1378 row.get::<_, Option<String>>(11)?,
1379 row.get::<_, Option<i64>>(12)?,
1380 row.get::<_, Option<String>>(13)?,
1381 row.get::<_, Option<String>>(14)?,
1382 row.get::<_, Option<i64>>(15)?,
1383 row.get::<_, Option<i64>>(16)?,
1384 row.get::<_, Option<String>>(17)?,
1385 row.get::<_, Option<String>>(18)?,
1386 row.get::<_, Option<i64>>(19)?,
1387 row.get::<_, String>(20)?,
1388 ))
1389 },
1390 )?;
1391
1392 let mut receipts = Vec::new();
1393 let mut metered_receipts = 0_u64;
1394 let mut pending_settlement_receipts = 0_u64;
1395 let mut failed_settlement_receipts = 0_u64;
1396 let mut settlement_actionable_receipts = 0_u64;
1397 let mut metering_actionable_receipts = 0_u64;
1398 let mut metering_evidence_missing_receipts = 0_u64;
1399 let mut metering_financial_mismatch_receipts = 0_u64;
1400
1401 for row in rows {
1402 let (
1403 seq,
1404 receipt_id,
1405 timestamp,
1406 capability_id,
1407 subject_key,
1408 tool_server,
1409 tool_name,
1410 settlement_reconciliation_state_text,
1411 settlement_note,
1412 settlement_updated_at,
1413 metering_reconciliation_state_text,
1414 metering_note,
1415 metering_updated_at,
1416 adapter_kind,
1417 evidence_id,
1418 observed_units,
1419 billed_cost_units,
1420 billed_cost_currency,
1421 evidence_sha256,
1422 recorded_at,
1423 raw_json,
1424 ) = row?;
1425 let receipt = decode_verified_chio_receipt(
1426 &raw_json,
1427 "persisted tool receipt",
1428 Some(seq.max(0) as u64),
1429 )?;
1430 let governed = extract_governed_transaction_metadata(&receipt).ok_or_else(|| {
1431 ReceiptStoreError::Canonical(format!(
1432 "receipt {} is missing governed transaction metadata",
1433 receipt.id
1434 ))
1435 })?;
1436 let economic_authorization = governed
1437 .economic_authorization
1438 .clone()
1439 .or_else(|| extract_economic_authorization_metadata(&receipt))
1440 .ok_or_else(|| {
1441 ReceiptStoreError::Canonical(format!(
1442 "receipt {} is missing economic authorization metadata",
1443 receipt.id
1444 ))
1445 })?;
1446 let financial = extract_financial_metadata(&receipt);
1447 let settlement_reconciliation_state =
1448 parse_settlement_reconciliation_state(&settlement_reconciliation_state_text)?;
1449 let settlement = EconomicReceiptSettlementProjection {
1450 settlement_status: economic_authorization.settlement.settlement_status.clone(),
1451 reconciliation_state: settlement_reconciliation_state,
1452 action_required: settlement_reconciliation_action_required(
1453 economic_authorization.settlement.settlement_status.clone(),
1454 settlement_reconciliation_state,
1455 ),
1456 note: settlement_note,
1457 updated_at: settlement_updated_at.map(|value| value.max(0) as u64),
1458 };
1459 if settlement.settlement_status == SettlementStatus::Pending {
1460 pending_settlement_receipts = pending_settlement_receipts.saturating_add(1);
1461 }
1462 if settlement.settlement_status == SettlementStatus::Failed {
1463 failed_settlement_receipts = failed_settlement_receipts.saturating_add(1);
1464 }
1465 if settlement.action_required {
1466 settlement_actionable_receipts = settlement_actionable_receipts.saturating_add(1);
1467 }
1468
1469 let metering = if economic_authorization.metering.is_some() {
1470 metered_receipts = metered_receipts.saturating_add(1);
1471 let governed_metering = governed.metered_billing.as_ref().ok_or_else(|| {
1472 ReceiptStoreError::Canonical(format!(
1473 "receipt {} has economic metering metadata without governed metered billing context",
1474 receipt.id
1475 ))
1476 })?;
1477 let evidence = metered_billing_evidence_record_from_columns(
1478 adapter_kind,
1479 evidence_id,
1480 observed_units,
1481 billed_cost_units,
1482 billed_cost_currency,
1483 evidence_sha256,
1484 recorded_at,
1485 );
1486 let reconciliation_state = parse_metered_billing_reconciliation_state(
1487 &metering_reconciliation_state_text,
1488 )?;
1489 let analysis = analyze_metered_billing_reconciliation(
1490 governed_metering,
1491 financial.as_ref(),
1492 evidence.as_ref(),
1493 reconciliation_state,
1494 );
1495 if analysis.action_required {
1496 metering_actionable_receipts = metering_actionable_receipts.saturating_add(1);
1497 }
1498 if analysis.evidence_missing {
1499 metering_evidence_missing_receipts =
1500 metering_evidence_missing_receipts.saturating_add(1);
1501 }
1502 if analysis.financial_mismatch {
1503 metering_financial_mismatch_receipts =
1504 metering_financial_mismatch_receipts.saturating_add(1);
1505 }
1506 Some(EconomicReceiptMeteringProjection {
1507 reconciliation_state,
1508 action_required: analysis.action_required,
1509 evidence_missing: analysis.evidence_missing,
1510 exceeds_quoted_units: analysis.exceeds_quoted_units,
1511 exceeds_max_billed_units: analysis.exceeds_max_billed_units,
1512 exceeds_quoted_cost: analysis.exceeds_quoted_cost,
1513 financial_mismatch: analysis.financial_mismatch,
1514 evidence,
1515 note: metering_note,
1516 updated_at: metering_updated_at.map(|value| value.max(0) as u64),
1517 })
1518 } else {
1519 None
1520 };
1521
1522 receipts.push(EconomicReceiptProjectionRow {
1523 receipt_id,
1524 timestamp: timestamp.max(0) as u64,
1525 capability_id,
1526 subject_key,
1527 tool_server,
1528 tool_name,
1529 economic_authorization,
1530 budget_authority: receipt.financial_budget_authority_metadata(),
1531 settlement,
1532 metering,
1533 });
1534 }
1535
1536 Ok(EconomicReceiptProjectionReport {
1537 summary: EconomicReceiptProjectionSummary {
1538 matching_receipts,
1539 returned_receipts: receipts.len() as u64,
1540 metered_receipts,
1541 pending_settlement_receipts,
1542 failed_settlement_receipts,
1543 settlement_actionable_receipts,
1544 metering_actionable_receipts,
1545 metering_evidence_missing_receipts,
1546 metering_financial_mismatch_receipts,
1547 truncated: matching_receipts > receipts.len() as u64,
1548 },
1549 receipts,
1550 })
1551 }
1552
1553 pub fn query_economic_completion_flow_report(
1554 &self,
1555 query: &ExposureLedgerQuery,
1556 ) -> Result<EconomicCompletionFlowReport, ReceiptStoreError> {
1557 let normalized = query.normalized();
1558 if let Err(message) = normalized.validate() {
1559 return Err(ReceiptStoreError::Conflict(message));
1560 }
1561
1562 let economic_receipts =
1563 self.query_economic_receipt_projection_report(&OperatorReportQuery {
1564 capability_id: normalized.capability_id.clone(),
1565 agent_subject: normalized.agent_subject.clone(),
1566 tool_server: normalized.tool_server.clone(),
1567 tool_name: normalized.tool_name.clone(),
1568 since: normalized.since,
1569 until: normalized.until,
1570 economic_limit: normalized.receipt_limit,
1571 ..OperatorReportQuery::default()
1572 })?;
1573 let underwriting_decisions =
1574 self.query_underwriting_decisions(&UnderwritingDecisionQuery {
1575 decision_id: None,
1576 capability_id: normalized.capability_id.clone(),
1577 agent_subject: normalized.agent_subject.clone(),
1578 tool_server: normalized.tool_server.clone(),
1579 tool_name: normalized.tool_name.clone(),
1580 outcome: None,
1581 lifecycle_state: None,
1582 appeal_status: None,
1583 limit: normalized.decision_limit,
1584 })?;
1585 let credit_facilities = self.query_credit_facilities(&CreditFacilityListQuery {
1586 facility_id: None,
1587 capability_id: normalized.capability_id.clone(),
1588 agent_subject: normalized.agent_subject.clone(),
1589 tool_server: normalized.tool_server.clone(),
1590 tool_name: normalized.tool_name.clone(),
1591 disposition: None,
1592 lifecycle_state: None,
1593 limit: normalized.decision_limit,
1594 })?;
1595 let credit_bonds = self.query_credit_bonds(&CreditBondListQuery {
1596 bond_id: None,
1597 facility_id: None,
1598 capability_id: normalized.capability_id.clone(),
1599 agent_subject: normalized.agent_subject.clone(),
1600 tool_server: normalized.tool_server.clone(),
1601 tool_name: normalized.tool_name.clone(),
1602 disposition: None,
1603 lifecycle_state: None,
1604 limit: normalized.decision_limit,
1605 })?;
1606
1607 let latest_underwriting = underwriting_decisions
1608 .decisions
1609 .iter()
1610 .find(|row| row.lifecycle_state == UnderwritingDecisionLifecycleState::Active)
1611 .or_else(|| underwriting_decisions.decisions.first());
1612 let latest_credit_facility = credit_facilities
1613 .facilities
1614 .iter()
1615 .find(|row| row.lifecycle_state == CreditFacilityLifecycleState::Active)
1616 .or_else(|| credit_facilities.facilities.first());
1617 let latest_credit_bond = credit_bonds
1618 .bonds
1619 .iter()
1620 .find(|row| row.lifecycle_state == CreditBondLifecycleState::Active)
1621 .or_else(|| credit_bonds.bonds.first());
1622
1623 Ok(EconomicCompletionFlowReport {
1624 schema: ECONOMIC_COMPLETION_FLOW_SCHEMA.to_string(),
1625 generated_at: unix_now(),
1626 filters: normalized,
1627 summary: EconomicCompletionFlowSummary {
1628 matching_receipts: economic_receipts.summary.matching_receipts,
1629 returned_receipts: economic_receipts.summary.returned_receipts,
1630 matching_underwriting_decisions: underwriting_decisions.summary.matching_decisions,
1631 returned_underwriting_decisions: underwriting_decisions.summary.returned_decisions,
1632 matching_credit_facilities: credit_facilities.summary.matching_facilities,
1633 returned_credit_facilities: credit_facilities.summary.returned_facilities,
1634 matching_credit_bonds: credit_bonds.summary.matching_bonds,
1635 returned_credit_bonds: credit_bonds.summary.returned_bonds,
1636 pending_settlement_receipts: economic_receipts.summary.pending_settlement_receipts,
1637 failed_settlement_receipts: economic_receipts.summary.failed_settlement_receipts,
1638 metering_actionable_receipts: economic_receipts
1639 .summary
1640 .metering_actionable_receipts,
1641 latest_underwriting_decision_id: latest_underwriting
1642 .map(|row| row.decision.body.decision_id.clone()),
1643 latest_underwriting_outcome: latest_underwriting
1644 .map(|row| row.decision.body.evaluation.outcome),
1645 latest_credit_facility_id: latest_credit_facility
1646 .map(|row| row.facility.body.facility_id.clone()),
1647 latest_credit_facility_disposition: latest_credit_facility
1648 .map(|row| row.facility.body.report.disposition),
1649 latest_credit_bond_id: latest_credit_bond.map(|row| row.bond.body.bond_id.clone()),
1650 latest_credit_bond_disposition: latest_credit_bond
1651 .map(|row| row.bond.body.report.disposition),
1652 },
1653 economic_receipts,
1654 underwriting_decisions,
1655 credit_facilities,
1656 credit_bonds,
1657 })
1658 }
1659
1660 pub fn query_settlement_reconciliation_report(
1661 &self,
1662 query: &OperatorReportQuery,
1663 ) -> Result<SettlementReconciliationReport, ReceiptStoreError> {
1664 let capability_id = query.capability_id.as_deref();
1665 let tool_server = query.tool_server.as_deref();
1666 let tool_name = query.tool_name.as_deref();
1667 let since = query.since.map(|value| value as i64);
1668 let until = query.until.map(|value| value as i64);
1669 let agent_subject = query.agent_subject.as_deref();
1670 let row_limit = query.settlement_limit_or_default();
1671
1672 let summary_sql = r#"
1673 SELECT
1674 COUNT(*) AS matching_receipts,
1675 COALESCE(SUM(
1676 CASE
1677 WHEN json_extract(r.raw_json, '$.metadata.financial.settlement_status') = 'pending' THEN 1
1678 ELSE 0
1679 END
1680 ), 0) AS pending_receipts,
1681 COALESCE(SUM(
1682 CASE
1683 WHEN json_extract(r.raw_json, '$.metadata.financial.settlement_status') = 'failed' THEN 1
1684 ELSE 0
1685 END
1686 ), 0) AS failed_receipts,
1687 COALESCE(SUM(
1688 CASE
1689 WHEN COALESCE(sr.reconciliation_state, 'open') NOT IN ('reconciled', 'ignored') THEN 1
1690 ELSE 0
1691 END
1692 ), 0) AS actionable_receipts,
1693 COALESCE(SUM(
1694 CASE
1695 WHEN COALESCE(sr.reconciliation_state, 'open') = 'reconciled' THEN 1
1696 ELSE 0
1697 END
1698 ), 0) AS reconciled_receipts
1699 FROM chio_tool_receipts r
1700 LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
1701 LEFT JOIN settlement_reconciliations sr ON r.receipt_id = sr.receipt_id
1702 WHERE json_extract(r.raw_json, '$.metadata.financial.settlement_status') IN ('pending', 'failed')
1703 AND (?1 IS NULL OR r.capability_id = ?1)
1704 AND (?2 IS NULL OR r.tool_server = ?2)
1705 AND (?3 IS NULL OR r.tool_name = ?3)
1706 AND (?4 IS NULL OR r.timestamp >= ?4)
1707 AND (?5 IS NULL OR r.timestamp <= ?5)
1708 AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
1709 "#;
1710
1711 let (
1712 matching_receipts,
1713 pending_receipts,
1714 failed_receipts,
1715 actionable_receipts,
1716 reconciled_receipts,
1717 ) = self.connection()?.query_row(
1718 summary_sql,
1719 params![
1720 capability_id,
1721 tool_server,
1722 tool_name,
1723 since,
1724 until,
1725 agent_subject
1726 ],
1727 |row| {
1728 Ok((
1729 row.get::<_, i64>(0)?.max(0) as u64,
1730 row.get::<_, i64>(1)?.max(0) as u64,
1731 row.get::<_, i64>(2)?.max(0) as u64,
1732 row.get::<_, i64>(3)?.max(0) as u64,
1733 row.get::<_, i64>(4)?.max(0) as u64,
1734 ))
1735 },
1736 )?;
1737
1738 let rows_sql = r#"
1739 SELECT
1740 r.seq,
1741 r.receipt_id,
1742 r.timestamp,
1743 r.capability_id,
1744 COALESCE(r.subject_key, cl.subject_key),
1745 r.tool_server,
1746 r.tool_name,
1747 json_extract(r.raw_json, '$.metadata.financial.payment_reference'),
1748 json_extract(r.raw_json, '$.metadata.financial.settlement_status'),
1749 CAST(json_extract(r.raw_json, '$.metadata.financial.cost_charged') AS INTEGER),
1750 json_extract(r.raw_json, '$.metadata.financial.currency'),
1751 COALESCE(sr.reconciliation_state, 'open'),
1752 sr.note,
1753 sr.updated_at,
1754 r.raw_json
1755 FROM chio_tool_receipts r
1756 LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
1757 LEFT JOIN settlement_reconciliations sr ON r.receipt_id = sr.receipt_id
1758 WHERE json_extract(r.raw_json, '$.metadata.financial.settlement_status') IN ('pending', 'failed')
1759 AND (?1 IS NULL OR r.capability_id = ?1)
1760 AND (?2 IS NULL OR r.tool_server = ?2)
1761 AND (?3 IS NULL OR r.tool_name = ?3)
1762 AND (?4 IS NULL OR r.timestamp >= ?4)
1763 AND (?5 IS NULL OR r.timestamp <= ?5)
1764 AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
1765 ORDER BY r.timestamp DESC, r.seq DESC
1766 LIMIT ?7
1767 "#;
1768
1769 let connection = self.connection()?;
1770 let mut stmt = connection.prepare(rows_sql)?;
1771 let rows = stmt.query_map(
1772 params![
1773 capability_id,
1774 tool_server,
1775 tool_name,
1776 since,
1777 until,
1778 agent_subject,
1779 row_limit as i64
1780 ],
1781 |row| {
1782 Ok((
1783 row.get::<_, i64>(0)?,
1784 row.get::<_, String>(1)?,
1785 row.get::<_, i64>(2)?,
1786 row.get::<_, String>(3)?,
1787 row.get::<_, Option<String>>(4)?,
1788 row.get::<_, String>(5)?,
1789 row.get::<_, String>(6)?,
1790 row.get::<_, Option<String>>(7)?,
1791 row.get::<_, String>(8)?,
1792 row.get::<_, Option<i64>>(9)?,
1793 row.get::<_, Option<String>>(10)?,
1794 row.get::<_, String>(11)?,
1795 row.get::<_, Option<String>>(12)?,
1796 row.get::<_, Option<i64>>(13)?,
1797 row.get::<_, String>(14)?,
1798 ))
1799 },
1800 )?;
1801
1802 let mut receipts = Vec::new();
1803 for row in rows {
1804 let (
1805 seq,
1806 receipt_id,
1807 timestamp,
1808 capability_id,
1809 subject_key,
1810 tool_server,
1811 tool_name,
1812 payment_reference,
1813 settlement_status_text,
1814 cost_charged,
1815 currency,
1816 reconciliation_state_text,
1817 note,
1818 updated_at,
1819 raw_json,
1820 ) = row?;
1821 let receipt = decode_verified_chio_receipt(
1822 &raw_json,
1823 "persisted tool receipt",
1824 Some(seq.max(0) as u64),
1825 )?;
1826 let settlement_status = parse_settlement_status(&settlement_status_text)?;
1827 let reconciliation_state =
1828 parse_settlement_reconciliation_state(&reconciliation_state_text)?;
1829 let action_required = settlement_reconciliation_action_required(
1830 settlement_status.clone(),
1831 reconciliation_state,
1832 );
1833 receipts.push(SettlementReconciliationRow {
1834 receipt_id,
1835 timestamp: timestamp.max(0) as u64,
1836 capability_id,
1837 subject_key,
1838 tool_server,
1839 tool_name,
1840 payment_reference,
1841 settlement_status,
1842 cost_charged: cost_charged.map(|value| value.max(0) as u64),
1843 currency,
1844 budget_authority: receipt.financial_budget_authority_metadata(),
1845 reconciliation_state,
1846 action_required,
1847 note,
1848 updated_at: updated_at.map(|value| value.max(0) as u64),
1849 });
1850 }
1851
1852 Ok(SettlementReconciliationReport {
1853 summary: SettlementReconciliationSummary {
1854 matching_receipts,
1855 returned_receipts: receipts.len() as u64,
1856 pending_receipts,
1857 failed_receipts,
1858 actionable_receipts,
1859 reconciled_receipts,
1860 truncated: matching_receipts > receipts.len() as u64,
1861 },
1862 receipts,
1863 })
1864 }
1865
1866 pub fn query_authorization_context_report(
1867 &self,
1868 query: &OperatorReportQuery,
1869 ) -> Result<AuthorizationContextReport, ReceiptStoreError> {
1870 let capability_id = query.capability_id.as_deref();
1871 let tool_server = query.tool_server.as_deref();
1872 let tool_name = query.tool_name.as_deref();
1873 let since = query.since.map(|value| value as i64);
1874 let until = query.until.map(|value| value as i64);
1875 let agent_subject = query.agent_subject.as_deref();
1876 let row_limit = query.authorization_limit_or_default();
1877
1878 let summary_sql = r#"
1879 SELECT
1880 COUNT(*),
1881 COALESCE(SUM(
1882 CASE
1883 WHEN json_type(r.raw_json, '$.metadata.governed_transaction.approval') = 'object' THEN 1
1884 ELSE 0
1885 END
1886 ), 0),
1887 COALESCE(SUM(
1888 CASE
1889 WHEN json_extract(r.raw_json, '$.metadata.governed_transaction.approval.approved') = 1 THEN 1
1890 ELSE 0
1891 END
1892 ), 0),
1893 COALESCE(SUM(
1894 CASE
1895 WHEN json_type(r.raw_json, '$.metadata.governed_transaction.commerce') = 'object' THEN 1
1896 ELSE 0
1897 END
1898 ), 0),
1899 COALESCE(SUM(
1900 CASE
1901 WHEN json_type(r.raw_json, '$.metadata.governed_transaction.metered_billing') = 'object' THEN 1
1902 ELSE 0
1903 END
1904 ), 0),
1905 COALESCE(SUM(
1906 CASE
1907 WHEN json_type(r.raw_json, '$.metadata.governed_transaction.runtime_assurance') = 'object' THEN 1
1908 ELSE 0
1909 END
1910 ), 0),
1911 COALESCE(SUM(
1912 CASE
1913 WHEN json_type(r.raw_json, '$.metadata.governed_transaction.call_chain') = 'object' THEN 1
1914 ELSE 0
1915 END
1916 ), 0),
1917 COALESCE(SUM(
1918 CASE
1919 WHEN json_type(r.raw_json, '$.metadata.governed_transaction.max_amount') = 'object' THEN 1
1920 ELSE 0
1921 END
1922 ), 0)
1923 FROM chio_tool_receipts r
1924 LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
1925 WHERE json_type(r.raw_json, '$.metadata.governed_transaction') = 'object'
1926 AND (?1 IS NULL OR r.capability_id = ?1)
1927 AND (?2 IS NULL OR r.tool_server = ?2)
1928 AND (?3 IS NULL OR r.tool_name = ?3)
1929 AND (?4 IS NULL OR r.timestamp >= ?4)
1930 AND (?5 IS NULL OR r.timestamp <= ?5)
1931 AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
1932 "#;
1933
1934 let (
1935 matching_receipts,
1936 approval_receipts,
1937 approved_receipts,
1938 commerce_receipts,
1939 metered_billing_receipts,
1940 runtime_assurance_receipts,
1941 _call_chain_receipts,
1942 max_amount_receipts,
1943 ) = self.connection()?.query_row(
1944 summary_sql,
1945 params![
1946 capability_id,
1947 tool_server,
1948 tool_name,
1949 since,
1950 until,
1951 agent_subject
1952 ],
1953 |row| {
1954 Ok((
1955 row.get::<_, i64>(0)?.max(0) as u64,
1956 row.get::<_, i64>(1)?.max(0) as u64,
1957 row.get::<_, i64>(2)?.max(0) as u64,
1958 row.get::<_, i64>(3)?.max(0) as u64,
1959 row.get::<_, i64>(4)?.max(0) as u64,
1960 row.get::<_, i64>(5)?.max(0) as u64,
1961 row.get::<_, i64>(6)?.max(0) as u64,
1962 row.get::<_, i64>(7)?.max(0) as u64,
1963 ))
1964 },
1965 )?;
1966
1967 let rows_sql = r#"
1968 SELECT
1969 r.seq,
1970 r.raw_json,
1971 r.subject_key,
1972 r.issuer_key,
1973 cl.subject_key,
1974 cl.issuer_key,
1975 r.grant_index,
1976 cl.grants_json,
1977 rls.statement_id,
1978 rls.session_anchor_id
1979 FROM chio_tool_receipts r
1980 LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
1981 LEFT JOIN receipt_lineage_statements rls ON r.receipt_id = rls.receipt_id
1982 WHERE json_type(r.raw_json, '$.metadata.governed_transaction') = 'object'
1983 AND (?1 IS NULL OR r.capability_id = ?1)
1984 AND (?2 IS NULL OR r.tool_server = ?2)
1985 AND (?3 IS NULL OR r.tool_name = ?3)
1986 AND (?4 IS NULL OR r.timestamp >= ?4)
1987 AND (?5 IS NULL OR r.timestamp <= ?5)
1988 AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
1989 ORDER BY r.timestamp DESC, r.seq DESC
1990 "#;
1991
1992 let connection = self.connection()?;
1993 let mut stmt = connection.prepare(rows_sql)?;
1994 let rows = stmt.query_map(
1995 params![
1996 capability_id,
1997 tool_server,
1998 tool_name,
1999 since,
2000 until,
2001 agent_subject
2002 ],
2003 |row| {
2004 Ok((
2005 row.get::<_, i64>(0)?,
2006 row.get::<_, String>(1)?,
2007 row.get::<_, Option<String>>(2)?,
2008 row.get::<_, Option<String>>(3)?,
2009 row.get::<_, Option<String>>(4)?,
2010 row.get::<_, Option<String>>(5)?,
2011 row.get::<_, Option<i64>>(6)?,
2012 row.get::<_, Option<String>>(7)?,
2013 row.get::<_, Option<String>>(8)?,
2014 row.get::<_, Option<String>>(9)?,
2015 ))
2016 },
2017 )?;
2018
2019 let mut sender_bound_receipts = 0_u64;
2020 let mut dpop_bound_receipts = 0_u64;
2021 let mut runtime_assurance_bound_receipts = 0_u64;
2022 let mut delegated_sender_bound_receipts = 0_u64;
2023 let mut call_chain_receipts = 0_u64;
2024 let mut asserted_call_chain_receipts = 0_u64;
2025 let mut observed_call_chain_receipts = 0_u64;
2026 let mut verified_call_chain_receipts = 0_u64;
2027 let mut session_anchor_receipts = 0_u64;
2028 let mut request_lineage_receipts = 0_u64;
2029 let mut receipt_lineage_statement_receipts = 0_u64;
2030 let mut receipts = Vec::new();
2031 for row in rows {
2032 let (
2033 seq,
2034 raw_json,
2035 receipt_subject_key,
2036 receipt_issuer_key,
2037 lineage_subject_key,
2038 lineage_issuer_key,
2039 persisted_grant_index,
2040 grants_json,
2041 persisted_statement_id,
2042 persisted_session_anchor_id,
2043 ) = row?;
2044 let receipt = decode_verified_chio_receipt(
2045 &raw_json,
2046 "persisted tool receipt",
2047 Some(seq.max(0) as u64),
2048 )?;
2049 let governed = extract_governed_transaction_metadata(&receipt).ok_or_else(|| {
2050 ReceiptStoreError::Canonical(format!(
2051 "receipt {} is missing governed transaction metadata",
2052 receipt.id
2053 ))
2054 })?;
2055 let mut projection = sanitize_governed_transaction_projection(&receipt, &governed);
2056 apply_persisted_lineage_references(
2057 &mut projection,
2058 &PersistedLineageReferences {
2059 statement_id: persisted_statement_id,
2060 session_anchor_id: persisted_session_anchor_id,
2061 },
2062 );
2063 let attribution = extract_receipt_attribution(&receipt);
2064 let transaction_context =
2065 authorization_transaction_context_from_projection(&projection);
2066 let sender_constraint = derive_authorization_sender_constraint(
2067 &receipt.id,
2068 AuthorizationSenderConstraintArgs {
2069 tool_server: &receipt.tool_server,
2070 tool_name: &receipt.tool_name,
2071 receipt_subject_key: receipt_subject_key.as_deref(),
2072 receipt_issuer_key: receipt_issuer_key.as_deref(),
2073 lineage_subject_key: lineage_subject_key.as_deref(),
2074 lineage_issuer_key: lineage_issuer_key.as_deref(),
2075 grant_index: attribution
2076 .grant_index
2077 .or_else(|| persisted_grant_index.map(|value| value.max(0) as u32)),
2078 grants_json: grants_json.as_deref(),
2079 },
2080 &transaction_context,
2081 )?;
2082
2083 let authorization_row = AuthorizationContextRow {
2084 receipt_id: receipt.id,
2085 timestamp: receipt.timestamp,
2086 capability_id: receipt.capability_id,
2087 subject_key: Some(sender_constraint.subject_key.clone()),
2088 tool_server: receipt.tool_server,
2089 tool_name: receipt.tool_name,
2090 decision: receipt.decision,
2091 authorization_details: authorization_details_from_governed_metadata(
2092 &projection.strong,
2093 ),
2094 transaction_context,
2095 governed_transaction_diagnostics: projection.diagnostics.clone(),
2096 sender_constraint,
2097 };
2098 validate_chio_oauth_authorization_row(&authorization_row)?;
2099 if let Some(evidence_class) = call_chain_evidence_class(&projection) {
2100 call_chain_receipts += 1;
2101 match evidence_class {
2102 GovernedProvenanceEvidenceClass::Asserted => {
2103 asserted_call_chain_receipts += 1;
2104 }
2105 GovernedProvenanceEvidenceClass::Observed => {
2106 observed_call_chain_receipts += 1;
2107 }
2108 GovernedProvenanceEvidenceClass::Verified => {
2109 verified_call_chain_receipts += 1;
2110 }
2111 }
2112 }
2113 if let Some(lineage_references) = lineage_references_from_projection(&projection) {
2114 if lineage_references.session_anchor_id.is_some() {
2115 session_anchor_receipts += 1;
2116 }
2117 if lineage_references.request_lineage_id.is_some() {
2118 request_lineage_receipts += 1;
2119 }
2120 if lineage_references.receipt_lineage_statement_id.is_some() {
2121 receipt_lineage_statement_receipts += 1;
2122 }
2123 }
2124 sender_bound_receipts += 1;
2125 if authorization_row.sender_constraint.proof_required {
2126 dpop_bound_receipts += 1;
2127 }
2128 if authorization_row.sender_constraint.runtime_assurance_bound {
2129 runtime_assurance_bound_receipts += 1;
2130 }
2131 if authorization_row
2132 .sender_constraint
2133 .delegated_call_chain_bound
2134 {
2135 delegated_sender_bound_receipts += 1;
2136 }
2137 if receipts.len() < row_limit {
2138 receipts.push(authorization_row);
2139 }
2140 }
2141
2142 Ok(AuthorizationContextReport {
2143 schema: CHIO_OAUTH_AUTHORIZATION_CONTEXT_REPORT_SCHEMA.to_string(),
2144 profile: ChioOAuthAuthorizationProfile::default(),
2145 summary: AuthorizationContextSummary {
2146 matching_receipts,
2147 returned_receipts: receipts.len() as u64,
2148 approval_receipts,
2149 approved_receipts,
2150 commerce_receipts,
2151 metered_billing_receipts,
2152 runtime_assurance_receipts,
2153 call_chain_receipts,
2154 asserted_call_chain_receipts,
2155 observed_call_chain_receipts,
2156 verified_call_chain_receipts,
2157 max_amount_receipts,
2158 sender_bound_receipts,
2159 dpop_bound_receipts,
2160 runtime_assurance_bound_receipts,
2161 delegated_sender_bound_receipts,
2162 session_anchor_receipts,
2163 request_lineage_receipts,
2164 receipt_lineage_statement_receipts,
2165 truncated: matching_receipts > receipts.len() as u64,
2166 },
2167 receipts,
2168 })
2169 }
2170
2171 pub fn authorization_profile_metadata_report(&self) -> ChioOAuthAuthorizationMetadataReport {
2172 ChioOAuthAuthorizationMetadataReport {
2173 schema: CHIO_OAUTH_AUTHORIZATION_METADATA_SCHEMA.to_string(),
2174 generated_at: unix_now(),
2175 profile: ChioOAuthAuthorizationProfile::default(),
2176 report_schema: CHIO_OAUTH_AUTHORIZATION_CONTEXT_REPORT_SCHEMA.to_string(),
2177 discovery: ChioOAuthAuthorizationDiscoveryMetadata {
2178 protected_resource_metadata_paths: vec![
2179 "/.well-known/oauth-protected-resource".to_string(),
2180 "/.well-known/oauth-protected-resource/mcp".to_string(),
2181 ],
2182 authorization_server_metadata_path_template:
2183 "/.well-known/oauth-authorization-server/{issuer-path}".to_string(),
2184 discovery_informational_only: true,
2185 },
2186 support_boundary: ChioOAuthAuthorizationSupportBoundary {
2187 governed_receipts_authoritative: true,
2188 hosted_request_time_authorization_supported: true,
2189 resource_indicator_binding_supported: true,
2190 sender_constrained_projection: true,
2191 runtime_assurance_projection: true,
2192 delegated_call_chain_projection: true,
2193 generic_token_issuance_supported: false,
2194 oidc_identity_assertions_supported: false,
2195 mtls_transport_binding_in_profile: false,
2196 approval_tokens_runtime_authorization_supported: false,
2197 capabilities_runtime_authorization_supported: false,
2198 reviewer_evidence_runtime_authorization_supported: false,
2199 },
2200 example_mapping: ChioOAuthAuthorizationExampleMapping {
2201 authorization_detail_types: vec![
2202 "type".to_string(),
2203 "locations".to_string(),
2204 "actions".to_string(),
2205 "purpose".to_string(),
2206 "maxAmount".to_string(),
2207 "commerce".to_string(),
2208 "meteredBilling".to_string(),
2209 ],
2210 transaction_context_fields: ChioOAuthAuthorizationProfile::default()
2211 .transaction_context_fields,
2212 sender_constraint_fields: vec![
2213 "subjectKey".to_string(),
2214 "subjectKeySource".to_string(),
2215 "issuerKey".to_string(),
2216 "issuerKeySource".to_string(),
2217 "matchedGrantIndex".to_string(),
2218 "proofRequired".to_string(),
2219 "proofType".to_string(),
2220 "proofSchema".to_string(),
2221 "runtimeAssuranceBound".to_string(),
2222 "delegatedCallChainBound".to_string(),
2223 ],
2224 },
2225 }
2226 }
2227
2228 pub fn query_authorization_review_pack(
2229 &self,
2230 query: &OperatorReportQuery,
2231 ) -> Result<ChioOAuthAuthorizationReviewPack, ReceiptStoreError> {
2232 let authorization_context = self.query_authorization_context_report(query)?;
2233 let metadata = self.authorization_profile_metadata_report();
2234 let mut records = Vec::with_capacity(authorization_context.receipts.len());
2235
2236 for row in authorization_context.receipts {
2237 let (seq, raw_json) = self.connection()?.query_row(
2238 "SELECT seq, raw_json FROM chio_tool_receipts WHERE receipt_id = ?1",
2239 params![row.receipt_id.as_str()],
2240 |db_row| Ok((db_row.get::<_, i64>(0)?, db_row.get::<_, String>(1)?)),
2241 )?;
2242 let signed_receipt = decode_verified_chio_receipt(
2243 &raw_json,
2244 "persisted tool receipt",
2245 Some(seq.max(0) as u64),
2246 )?;
2247 let governed_transaction = extract_governed_transaction_metadata(&signed_receipt)
2248 .ok_or_else(|| {
2249 ReceiptStoreError::Canonical(format!(
2250 "receipt {} is missing governed transaction metadata",
2251 signed_receipt.id
2252 ))
2253 })?;
2254 let mut projection =
2255 sanitize_governed_transaction_projection(&signed_receipt, &governed_transaction);
2256 if let Some(diagnostics) = row.governed_transaction_diagnostics.as_ref() {
2257 apply_persisted_lineage_references(
2258 &mut projection,
2259 &PersistedLineageReferences {
2260 statement_id: diagnostics
2261 .lineage_references
2262 .receipt_lineage_statement_id
2263 .clone(),
2264 session_anchor_id: diagnostics.lineage_references.session_anchor_id.clone(),
2265 },
2266 );
2267 }
2268 records.push(ChioOAuthAuthorizationReviewPackRecord {
2269 receipt_id: row.receipt_id.clone(),
2270 capability_id: row.capability_id.clone(),
2271 authorization_context: row,
2272 governed_transaction: projection.strong,
2273 governed_transaction_diagnostics: projection.diagnostics,
2274 signed_receipt,
2275 });
2276 }
2277
2278 Ok(ChioOAuthAuthorizationReviewPack {
2279 schema: CHIO_OAUTH_AUTHORIZATION_REVIEW_PACK_SCHEMA.to_string(),
2280 generated_at: unix_now(),
2281 filters: query.clone(),
2282 metadata,
2283 summary: ChioOAuthAuthorizationReviewPackSummary {
2284 matching_receipts: authorization_context.summary.matching_receipts,
2285 returned_receipts: records.len() as u64,
2286 dpop_required_receipts: authorization_context.summary.dpop_bound_receipts,
2287 runtime_assurance_receipts: authorization_context
2288 .summary
2289 .runtime_assurance_bound_receipts,
2290 delegated_call_chain_receipts: authorization_context.summary.call_chain_receipts,
2291 asserted_call_chain_receipts: authorization_context
2292 .summary
2293 .asserted_call_chain_receipts,
2294 observed_call_chain_receipts: authorization_context
2295 .summary
2296 .observed_call_chain_receipts,
2297 verified_call_chain_receipts: authorization_context
2298 .summary
2299 .verified_call_chain_receipts,
2300 session_anchor_receipts: authorization_context.summary.session_anchor_receipts,
2301 request_lineage_receipts: authorization_context.summary.request_lineage_receipts,
2302 receipt_lineage_statement_receipts: authorization_context
2303 .summary
2304 .receipt_lineage_statement_receipts,
2305 truncated: authorization_context.summary.truncated,
2306 },
2307 records,
2308 })
2309 }
2310
2311 pub fn query_behavioral_feed_receipts(
2312 &self,
2313 query: &BehavioralFeedQuery,
2314 ) -> Result<
2315 (
2316 BehavioralFeedSettlementSummary,
2317 BehavioralFeedGovernedActionSummary,
2318 BehavioralFeedMeteredBillingSummary,
2319 BehavioralFeedReceiptSelection,
2320 ),
2321 ReceiptStoreError,
2322 > {
2323 let operator_query = query.to_operator_report_query();
2324 let capability_id = operator_query.capability_id.as_deref();
2325 let tool_server = operator_query.tool_server.as_deref();
2326 let tool_name = operator_query.tool_name.as_deref();
2327 let since = operator_query.since.map(|value| value as i64);
2328 let until = operator_query.until.map(|value| value as i64);
2329 let agent_subject = operator_query.agent_subject.as_deref();
2330
2331 let summary_sql = r#"
2332 SELECT
2333 COALESCE(SUM(
2334 CASE
2335 WHEN COALESCE(json_extract(r.raw_json, '$.metadata.financial.settlement_status'), 'not_applicable') = 'pending' THEN 1
2336 ELSE 0
2337 END
2338 ), 0),
2339 COALESCE(SUM(
2340 CASE
2341 WHEN COALESCE(json_extract(r.raw_json, '$.metadata.financial.settlement_status'), 'not_applicable') = 'settled' THEN 1
2342 ELSE 0
2343 END
2344 ), 0),
2345 COALESCE(SUM(
2346 CASE
2347 WHEN COALESCE(json_extract(r.raw_json, '$.metadata.financial.settlement_status'), 'not_applicable') = 'failed' THEN 1
2348 ELSE 0
2349 END
2350 ), 0),
2351 COALESCE(SUM(
2352 CASE
2353 WHEN COALESCE(json_extract(r.raw_json, '$.metadata.financial.settlement_status'), 'not_applicable') = 'not_applicable' THEN 1
2354 ELSE 0
2355 END
2356 ), 0),
2357 COALESCE(SUM(
2358 CASE
2359 WHEN COALESCE(json_extract(r.raw_json, '$.metadata.financial.settlement_status'), 'not_applicable') IN ('pending', 'failed')
2360 AND COALESCE(sr.reconciliation_state, 'open') NOT IN ('reconciled', 'ignored')
2361 THEN 1
2362 ELSE 0
2363 END
2364 ), 0),
2365 COALESCE(SUM(
2366 CASE
2367 WHEN COALESCE(sr.reconciliation_state, 'open') = 'reconciled' THEN 1
2368 ELSE 0
2369 END
2370 ), 0),
2371 COALESCE(SUM(
2372 CASE
2373 WHEN json_type(r.raw_json, '$.metadata.governed_transaction') IS NOT NULL THEN 1
2374 ELSE 0
2375 END
2376 ), 0),
2377 COALESCE(SUM(
2378 CASE
2379 WHEN json_type(r.raw_json, '$.metadata.governed_transaction.approval') IS NOT NULL THEN 1
2380 ELSE 0
2381 END
2382 ), 0),
2383 COALESCE(SUM(
2384 CASE
2385 WHEN json_extract(r.raw_json, '$.metadata.governed_transaction.approval.approved') = 1 THEN 1
2386 ELSE 0
2387 END
2388 ), 0),
2389 COALESCE(SUM(
2390 CASE
2391 WHEN json_type(r.raw_json, '$.metadata.governed_transaction.commerce') IS NOT NULL THEN 1
2392 ELSE 0
2393 END
2394 ), 0),
2395 COALESCE(SUM(
2396 CASE
2397 WHEN json_type(r.raw_json, '$.metadata.governed_transaction.max_amount') IS NOT NULL THEN 1
2398 ELSE 0
2399 END
2400 ), 0)
2401 FROM chio_tool_receipts r
2402 LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
2403 LEFT JOIN settlement_reconciliations sr ON r.receipt_id = sr.receipt_id
2404 WHERE (?1 IS NULL OR r.capability_id = ?1)
2405 AND (?2 IS NULL OR r.tool_server = ?2)
2406 AND (?3 IS NULL OR r.tool_name = ?3)
2407 AND (?4 IS NULL OR r.timestamp >= ?4)
2408 AND (?5 IS NULL OR r.timestamp <= ?5)
2409 AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
2410 "#;
2411
2412 let (settlements, governed_actions) = self.connection()?.query_row(
2413 summary_sql,
2414 params![
2415 capability_id,
2416 tool_server,
2417 tool_name,
2418 since,
2419 until,
2420 agent_subject
2421 ],
2422 |row| {
2423 Ok((
2424 BehavioralFeedSettlementSummary {
2425 pending_receipts: row.get::<_, i64>(0)?.max(0) as u64,
2426 settled_receipts: row.get::<_, i64>(1)?.max(0) as u64,
2427 failed_receipts: row.get::<_, i64>(2)?.max(0) as u64,
2428 not_applicable_receipts: row.get::<_, i64>(3)?.max(0) as u64,
2429 actionable_receipts: row.get::<_, i64>(4)?.max(0) as u64,
2430 reconciled_receipts: row.get::<_, i64>(5)?.max(0) as u64,
2431 },
2432 BehavioralFeedGovernedActionSummary {
2433 governed_receipts: row.get::<_, i64>(6)?.max(0) as u64,
2434 approval_receipts: row.get::<_, i64>(7)?.max(0) as u64,
2435 approved_receipts: row.get::<_, i64>(8)?.max(0) as u64,
2436 commerce_receipts: row.get::<_, i64>(9)?.max(0) as u64,
2437 max_amount_receipts: row.get::<_, i64>(10)?.max(0) as u64,
2438 },
2439 ))
2440 },
2441 )?;
2442 let metered_billing = self.query_metered_billing_summary(&operator_query)?;
2443
2444 let row_limit = query.receipt_limit_or_default();
2445 let count_sql = r#"
2446 SELECT COUNT(*)
2447 FROM chio_tool_receipts r
2448 LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
2449 WHERE (?1 IS NULL OR r.capability_id = ?1)
2450 AND (?2 IS NULL OR r.tool_server = ?2)
2451 AND (?3 IS NULL OR r.tool_name = ?3)
2452 AND (?4 IS NULL OR r.timestamp >= ?4)
2453 AND (?5 IS NULL OR r.timestamp <= ?5)
2454 AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
2455 "#;
2456 let matching_receipts = self
2457 .connection()?
2458 .query_row(
2459 count_sql,
2460 params![
2461 capability_id,
2462 tool_server,
2463 tool_name,
2464 since,
2465 until,
2466 agent_subject
2467 ],
2468 |row| row.get::<_, i64>(0),
2469 )
2470 .map(|value| value.max(0) as u64)?;
2471
2472 let rows_sql = r#"
2473 SELECT r.seq, r.raw_json
2474 FROM chio_tool_receipts r
2475 LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
2476 WHERE (?1 IS NULL OR r.capability_id = ?1)
2477 AND (?2 IS NULL OR r.tool_server = ?2)
2478 AND (?3 IS NULL OR r.tool_name = ?3)
2479 AND (?4 IS NULL OR r.timestamp >= ?4)
2480 AND (?5 IS NULL OR r.timestamp <= ?5)
2481 AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
2482 ORDER BY r.timestamp DESC, r.seq DESC
2483 LIMIT ?7
2484 "#;
2485 let connection = self.connection()?;
2486 let mut stmt = connection.prepare(rows_sql)?;
2487 let rows = stmt.query_map(
2488 params![
2489 capability_id,
2490 tool_server,
2491 tool_name,
2492 since,
2493 until,
2494 agent_subject,
2495 row_limit as i64,
2496 ],
2497 |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
2498 )?;
2499 let mut receipts = Vec::with_capacity(row_limit);
2500 for row in rows {
2501 let (seq, raw_json) = row?;
2502 let receipt = decode_verified_chio_receipt(
2503 &raw_json,
2504 "persisted tool receipt",
2505 Some(seq.max(0) as u64),
2506 )?;
2507 receipts.push(self.behavioral_feed_receipt_row_from_receipt(receipt)?);
2508 }
2509
2510 Ok((
2511 settlements,
2512 governed_actions,
2513 metered_billing,
2514 BehavioralFeedReceiptSelection {
2515 matching_receipts,
2516 receipts,
2517 },
2518 ))
2519 }
2520
2521 pub fn query_recent_credit_loss_receipts(
2522 &self,
2523 query: &BehavioralFeedQuery,
2524 limit: usize,
2525 ) -> Result<(u64, Vec<BehavioralFeedReceiptRow>), ReceiptStoreError> {
2526 let operator_query = query.to_operator_report_query();
2527 let capability_id = operator_query.capability_id.as_deref();
2528 let tool_server = operator_query.tool_server.as_deref();
2529 let tool_name = operator_query.tool_name.as_deref();
2530 let since = operator_query.since.map(|value| value as i64);
2531 let until = operator_query.until.map(|value| value as i64);
2532 let agent_subject = operator_query.agent_subject.as_deref();
2533 let row_limit = limit.max(1);
2534
2535 let count_sql = r#"
2536 SELECT COUNT(*)
2537 FROM chio_tool_receipts r
2538 LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
2539 LEFT JOIN settlement_reconciliations sr ON r.receipt_id = sr.receipt_id
2540 WHERE (?1 IS NULL OR r.capability_id = ?1)
2541 AND (?2 IS NULL OR r.tool_server = ?2)
2542 AND (?3 IS NULL OR r.tool_name = ?3)
2543 AND (?4 IS NULL OR r.timestamp >= ?4)
2544 AND (?5 IS NULL OR r.timestamp <= ?5)
2545 AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
2546 AND (
2547 COALESCE(json_extract(r.raw_json, '$.metadata.financial.settlement_status'), 'not_applicable') = 'failed'
2548 OR (
2549 COALESCE(json_extract(r.raw_json, '$.metadata.financial.settlement_status'), 'not_applicable') IN ('pending', 'failed')
2550 AND COALESCE(sr.reconciliation_state, 'open') NOT IN ('reconciled', 'ignored')
2551 )
2552 )
2553 "#;
2554
2555 let matching_loss_events = self
2556 .connection()?
2557 .query_row(
2558 count_sql,
2559 params![
2560 capability_id,
2561 tool_server,
2562 tool_name,
2563 since,
2564 until,
2565 agent_subject
2566 ],
2567 |row| row.get::<_, i64>(0),
2568 )
2569 .map(|value| value.max(0) as u64)?;
2570
2571 let rows_sql = r#"
2572 SELECT r.seq, r.raw_json
2573 FROM chio_tool_receipts r
2574 LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
2575 LEFT JOIN settlement_reconciliations sr ON r.receipt_id = sr.receipt_id
2576 WHERE (?1 IS NULL OR r.capability_id = ?1)
2577 AND (?2 IS NULL OR r.tool_server = ?2)
2578 AND (?3 IS NULL OR r.tool_name = ?3)
2579 AND (?4 IS NULL OR r.timestamp >= ?4)
2580 AND (?5 IS NULL OR r.timestamp <= ?5)
2581 AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
2582 AND (
2583 COALESCE(json_extract(r.raw_json, '$.metadata.financial.settlement_status'), 'not_applicable') = 'failed'
2584 OR (
2585 COALESCE(json_extract(r.raw_json, '$.metadata.financial.settlement_status'), 'not_applicable') IN ('pending', 'failed')
2586 AND COALESCE(sr.reconciliation_state, 'open') NOT IN ('reconciled', 'ignored')
2587 )
2588 )
2589 ORDER BY r.timestamp DESC, r.seq DESC
2590 LIMIT ?7
2591 "#;
2592
2593 let connection = self.connection()?;
2594 let mut stmt = connection.prepare(rows_sql)?;
2595 let rows = stmt.query_map(
2596 params![
2597 capability_id,
2598 tool_server,
2599 tool_name,
2600 since,
2601 until,
2602 agent_subject,
2603 row_limit as i64
2604 ],
2605 |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
2606 )?;
2607
2608 let mut receipts = Vec::new();
2609 for row in rows {
2610 let (seq, raw_json) = row?;
2611 let receipt = decode_verified_chio_receipt(
2612 &raw_json,
2613 "persisted tool receipt",
2614 Some(seq.max(0) as u64),
2615 )?;
2616 receipts.push(self.behavioral_feed_receipt_row_from_receipt(receipt)?);
2617 }
2618
2619 Ok((matching_loss_events, receipts))
2620 }
2621
2622 pub(crate) fn behavioral_feed_receipt_row_from_receipt(
2623 &self,
2624 receipt: ChioReceipt,
2625 ) -> Result<BehavioralFeedReceiptRow, ReceiptStoreError> {
2626 let attribution = extract_receipt_attribution(&receipt);
2627 let lineage = if attribution.subject_key.is_none() || attribution.issuer_key.is_none() {
2628 self.get_combined_lineage(&receipt.capability_id)?
2629 } else {
2630 None
2631 };
2632 let financial = extract_financial_metadata(&receipt);
2633 let governed = extract_governed_transaction_metadata(&receipt);
2634 let governed_projection = governed
2635 .as_ref()
2636 .map(|metadata| sanitize_governed_transaction_projection(&receipt, metadata));
2637 let metered_reconciliation = governed
2638 .as_ref()
2639 .and_then(|metadata| metadata.metered_billing.as_ref())
2640 .map(|metered| {
2641 let evidence = self.load_metered_billing_evidence_record(&receipt.id)?;
2642 let reconciliation_state = self
2643 .connection()?
2644 .query_row(
2645 r#"
2646 SELECT COALESCE(reconciliation_state, 'open')
2647 FROM metered_billing_reconciliations
2648 WHERE receipt_id = ?1
2649 "#,
2650 params![&receipt.id],
2651 |row| row.get::<_, String>(0),
2652 )
2653 .optional()?
2654 .map(|value| parse_metered_billing_reconciliation_state(&value))
2655 .transpose()?
2656 .unwrap_or(MeteredBillingReconciliationState::Open);
2657 let analysis = analyze_metered_billing_reconciliation(
2658 metered,
2659 financial.as_ref(),
2660 evidence.as_ref(),
2661 reconciliation_state,
2662 );
2663 Ok::<BehavioralFeedMeteredBillingRow, ReceiptStoreError>(
2664 BehavioralFeedMeteredBillingRow {
2665 reconciliation_state,
2666 action_required: analysis.action_required,
2667 evidence_missing: analysis.evidence_missing,
2668 exceeds_quoted_units: analysis.exceeds_quoted_units,
2669 exceeds_max_billed_units: analysis.exceeds_max_billed_units,
2670 exceeds_quoted_cost: analysis.exceeds_quoted_cost,
2671 financial_mismatch: analysis.financial_mismatch,
2672 evidence,
2673 },
2674 )
2675 })
2676 .transpose()?;
2677 let settlement_status = financial
2678 .as_ref()
2679 .map(|metadata| metadata.settlement_status.clone())
2680 .unwrap_or(SettlementStatus::NotApplicable);
2681 let reconciliation_state = self
2682 .connection()?
2683 .query_row(
2684 r#"
2685 SELECT COALESCE(reconciliation_state, 'open')
2686 FROM settlement_reconciliations
2687 WHERE receipt_id = ?1
2688 "#,
2689 params![receipt.id],
2690 |row| row.get::<_, String>(0),
2691 )
2692 .optional()?
2693 .map(|value| parse_settlement_reconciliation_state(&value))
2694 .transpose()?
2695 .unwrap_or(SettlementReconciliationState::Open);
2696 let action_required = settlement_reconciliation_action_required(
2697 settlement_status.clone(),
2698 reconciliation_state,
2699 );
2700 let budget_authority = receipt.financial_budget_authority_metadata();
2701
2702 Ok(BehavioralFeedReceiptRow {
2703 receipt_id: receipt.id,
2704 timestamp: receipt.timestamp,
2705 capability_id: receipt.capability_id,
2706 subject_key: attribution.subject_key.or_else(|| {
2707 lineage
2708 .as_ref()
2709 .map(|snapshot| snapshot.subject_key.clone())
2710 }),
2711 issuer_key: attribution
2712 .issuer_key
2713 .or_else(|| lineage.as_ref().map(|snapshot| snapshot.issuer_key.clone())),
2714 tool_server: receipt.tool_server,
2715 tool_name: receipt.tool_name,
2716 decision: receipt.decision,
2717 settlement_status,
2718 reconciliation_state,
2719 action_required,
2720 cost_charged: financial.as_ref().map(|metadata| metadata.cost_charged),
2721 attempted_cost: financial
2722 .as_ref()
2723 .and_then(|metadata| metadata.attempted_cost),
2724 currency: financial.as_ref().map(|metadata| metadata.currency.clone()),
2725 budget_authority,
2726 governed: governed_projection
2727 .as_ref()
2728 .map(|projection| projection.strong.clone()),
2729 governed_transaction_diagnostics: governed_projection
2730 .and_then(|projection| projection.diagnostics),
2731 metered_reconciliation,
2732 })
2733 }
2734
2735 pub(crate) fn query_metered_billing_summary(
2736 &self,
2737 query: &OperatorReportQuery,
2738 ) -> Result<BehavioralFeedMeteredBillingSummary, ReceiptStoreError> {
2739 let capability_id = query.capability_id.as_deref();
2740 let tool_server = query.tool_server.as_deref();
2741 let tool_name = query.tool_name.as_deref();
2742 let since = query.since.map(|value| value as i64);
2743 let until = query.until.map(|value| value as i64);
2744 let agent_subject = query.agent_subject.as_deref();
2745
2746 let summary_sql = r#"
2747 SELECT
2748 COUNT(*) AS matching_receipts,
2749 COALESCE(SUM(CASE WHEN mbr.receipt_id IS NOT NULL THEN 1 ELSE 0 END), 0),
2750 COALESCE(SUM(CASE WHEN mbr.receipt_id IS NULL THEN 1 ELSE 0 END), 0),
2751 COALESCE(SUM(
2752 CASE
2753 WHEN mbr.receipt_id IS NOT NULL
2754 AND mbr.observed_units > CAST(json_extract(r.raw_json, '$.metadata.governed_transaction.metered_billing.quote.quotedUnits') AS INTEGER)
2755 THEN 1
2756 ELSE 0
2757 END
2758 ), 0),
2759 COALESCE(SUM(
2760 CASE
2761 WHEN mbr.receipt_id IS NOT NULL
2762 AND json_extract(r.raw_json, '$.metadata.governed_transaction.metered_billing.maxBilledUnits') IS NOT NULL
2763 AND mbr.observed_units > CAST(json_extract(r.raw_json, '$.metadata.governed_transaction.metered_billing.maxBilledUnits') AS INTEGER)
2764 THEN 1
2765 ELSE 0
2766 END
2767 ), 0),
2768 COALESCE(SUM(
2769 CASE
2770 WHEN mbr.receipt_id IS NOT NULL
2771 AND (
2772 mbr.billed_cost_currency != json_extract(r.raw_json, '$.metadata.governed_transaction.metered_billing.quote.quotedCost.currency')
2773 OR mbr.billed_cost_units > CAST(json_extract(r.raw_json, '$.metadata.governed_transaction.metered_billing.quote.quotedCost.units') AS INTEGER)
2774 )
2775 THEN 1
2776 ELSE 0
2777 END
2778 ), 0),
2779 COALESCE(SUM(
2780 CASE
2781 WHEN mbr.receipt_id IS NOT NULL
2782 AND json_type(r.raw_json, '$.metadata.financial') = 'object'
2783 AND (
2784 mbr.billed_cost_currency != json_extract(r.raw_json, '$.metadata.financial.currency')
2785 OR mbr.billed_cost_units != CAST(json_extract(r.raw_json, '$.metadata.financial.cost_charged') AS INTEGER)
2786 )
2787 THEN 1
2788 ELSE 0
2789 END
2790 ), 0),
2791 COALESCE(SUM(
2792 CASE
2793 WHEN COALESCE(mbr.reconciliation_state, 'open') = 'reconciled' THEN 1
2794 ELSE 0
2795 END
2796 ), 0),
2797 COALESCE(SUM(
2798 CASE
2799 WHEN COALESCE(mbr.reconciliation_state, 'open') NOT IN ('reconciled', 'ignored')
2800 AND (
2801 mbr.receipt_id IS NULL
2802 OR mbr.observed_units > CAST(json_extract(r.raw_json, '$.metadata.governed_transaction.metered_billing.quote.quotedUnits') AS INTEGER)
2803 OR (
2804 json_extract(r.raw_json, '$.metadata.governed_transaction.metered_billing.maxBilledUnits') IS NOT NULL
2805 AND mbr.observed_units > CAST(json_extract(r.raw_json, '$.metadata.governed_transaction.metered_billing.maxBilledUnits') AS INTEGER)
2806 )
2807 OR mbr.billed_cost_currency != json_extract(r.raw_json, '$.metadata.governed_transaction.metered_billing.quote.quotedCost.currency')
2808 OR mbr.billed_cost_units > CAST(json_extract(r.raw_json, '$.metadata.governed_transaction.metered_billing.quote.quotedCost.units') AS INTEGER)
2809 OR (
2810 json_type(r.raw_json, '$.metadata.financial') = 'object'
2811 AND (
2812 mbr.billed_cost_currency != json_extract(r.raw_json, '$.metadata.financial.currency')
2813 OR mbr.billed_cost_units != CAST(json_extract(r.raw_json, '$.metadata.financial.cost_charged') AS INTEGER)
2814 )
2815 )
2816 )
2817 THEN 1
2818 ELSE 0
2819 END
2820 ), 0)
2821 FROM chio_tool_receipts r
2822 LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
2823 LEFT JOIN metered_billing_reconciliations mbr ON r.receipt_id = mbr.receipt_id
2824 WHERE json_type(r.raw_json, '$.metadata.governed_transaction.metered_billing') = 'object'
2825 AND (?1 IS NULL OR r.capability_id = ?1)
2826 AND (?2 IS NULL OR r.tool_server = ?2)
2827 AND (?3 IS NULL OR r.tool_name = ?3)
2828 AND (?4 IS NULL OR r.timestamp >= ?4)
2829 AND (?5 IS NULL OR r.timestamp <= ?5)
2830 AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
2831 "#;
2832
2833 let (
2834 metered_receipts,
2835 evidence_attached_receipts,
2836 missing_evidence_receipts,
2837 over_quoted_units_receipts,
2838 over_max_billed_units_receipts,
2839 over_quoted_cost_receipts,
2840 financial_mismatch_receipts,
2841 reconciled_receipts,
2842 actionable_receipts,
2843 ) = self.connection()?.query_row(
2844 summary_sql,
2845 params![
2846 capability_id,
2847 tool_server,
2848 tool_name,
2849 since,
2850 until,
2851 agent_subject
2852 ],
2853 |row| {
2854 Ok((
2855 row.get::<_, i64>(0)?.max(0) as u64,
2856 row.get::<_, i64>(1)?.max(0) as u64,
2857 row.get::<_, i64>(2)?.max(0) as u64,
2858 row.get::<_, i64>(3)?.max(0) as u64,
2859 row.get::<_, i64>(4)?.max(0) as u64,
2860 row.get::<_, i64>(5)?.max(0) as u64,
2861 row.get::<_, i64>(6)?.max(0) as u64,
2862 row.get::<_, i64>(7)?.max(0) as u64,
2863 row.get::<_, i64>(8)?.max(0) as u64,
2864 ))
2865 },
2866 )?;
2867
2868 Ok(BehavioralFeedMeteredBillingSummary {
2869 metered_receipts,
2870 evidence_attached_receipts,
2871 missing_evidence_receipts,
2872 over_quoted_units_receipts,
2873 over_max_billed_units_receipts,
2874 over_quoted_cost_receipts,
2875 financial_mismatch_receipts,
2876 actionable_receipts,
2877 reconciled_receipts,
2878 })
2879 }
2880
2881 pub(crate) fn load_metered_billing_evidence_record(
2882 &self,
2883 receipt_id: &str,
2884 ) -> Result<Option<MeteredBillingEvidenceRecord>, ReceiptStoreError> {
2885 self.connection()?
2886 .query_row(
2887 r#"
2888 SELECT
2889 adapter_kind,
2890 evidence_id,
2891 observed_units,
2892 billed_cost_units,
2893 billed_cost_currency,
2894 evidence_sha256,
2895 recorded_at
2896 FROM metered_billing_reconciliations
2897 WHERE receipt_id = ?1
2898 "#,
2899 params![receipt_id],
2900 |row| {
2901 Ok((
2902 row.get::<_, String>(0)?,
2903 row.get::<_, String>(1)?,
2904 row.get::<_, i64>(2)?,
2905 row.get::<_, i64>(3)?,
2906 row.get::<_, String>(4)?,
2907 row.get::<_, Option<String>>(5)?,
2908 row.get::<_, i64>(6)?,
2909 ))
2910 },
2911 )
2912 .optional()?
2913 .map(
2914 |(
2915 adapter_kind,
2916 evidence_id,
2917 observed_units,
2918 billed_cost_units,
2919 billed_cost_currency,
2920 evidence_sha256,
2921 recorded_at,
2922 )| {
2923 Ok(MeteredBillingEvidenceRecord {
2924 usage_evidence: chio_core::receipt::MeteredUsageEvidenceReceiptMetadata {
2925 evidence_kind: adapter_kind,
2926 evidence_id,
2927 observed_units: observed_units.max(0) as u64,
2928 evidence_sha256,
2929 },
2930 billed_cost: chio_core::capability::MonetaryAmount {
2931 units: billed_cost_units.max(0) as u64,
2932 currency: billed_cost_currency,
2933 },
2934 recorded_at: recorded_at.max(0) as u64,
2935 })
2936 },
2937 )
2938 .transpose()
2939 }
2940}