adk_payments/journal/
session_state.rs1use adk_core::identity::AdkIdentity;
2use adk_core::{AdkError, Content, ErrorCategory, ErrorComponent, Event, Result};
3use adk_session::KEY_PREFIX_APP;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use sha2::{Digest, Sha256};
7
8use crate::domain::{SafeTransactionSummary, TransactionId, TransactionRecord};
9use crate::guardrail::redact_payment_content;
10
11pub const TRANSACTION_KEY_PREFIX: &str = "payments:tx:";
12pub const ACTIVE_INDEX_KEY: &str = "payments:index:active";
13pub const COMPLETED_INDEX_KEY: &str = "payments:index:completed";
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct TransactionLocator {
19 pub identity: AdkIdentity,
20 pub transaction_id: TransactionId,
21}
22
23#[must_use]
25pub fn transaction_state_key(identity: &AdkIdentity, transaction_id: &TransactionId) -> String {
26 format!("{TRANSACTION_KEY_PREFIX}{}:{transaction_id}", identity_hash(identity))
27}
28
29#[must_use]
31pub fn active_index_state_key() -> String {
32 format!("{KEY_PREFIX_APP}{ACTIVE_INDEX_KEY}")
33}
34
35#[must_use]
37pub fn completed_index_state_key() -> String {
38 format!("{KEY_PREFIX_APP}{COMPLETED_INDEX_KEY}")
39}
40
41#[must_use]
43pub fn transaction_state_storage_key(
44 identity: &AdkIdentity,
45 transaction_id: &TransactionId,
46) -> String {
47 format!("{KEY_PREFIX_APP}{}", transaction_state_key(identity, transaction_id))
48}
49
50pub fn build_journal_event(
56 record: &TransactionRecord,
57 active: &[TransactionLocator],
58 completed: &[TransactionLocator],
59) -> Result<Event> {
60 let identity = record.session_identity.as_ref().ok_or_else(|| {
61 AdkError::new(
62 ErrorComponent::Session,
63 ErrorCategory::InvalidInput,
64 "payments.journal.identity_required",
65 "transaction journal writes require a session identity",
66 )
67 })?;
68
69 let mut event = Event::new("payments.journal");
70 event.author = "adk-payments".to_string();
71 event.set_content(summary_content(&record.safe_summary));
72 event.actions.state_delta.insert(
73 transaction_state_storage_key(identity, &record.transaction_id),
74 serialize_value(record, "payments.journal.record_serialize")?,
75 );
76 event.actions.state_delta.insert(
77 active_index_state_key(),
78 serialize_value(active, "payments.journal.active_index_serialize")?,
79 );
80 event.actions.state_delta.insert(
81 completed_index_state_key(),
82 serialize_value(completed, "payments.journal.completed_index_serialize")?,
83 );
84 Ok(event)
85}
86
87pub fn parse_record(value: Value) -> Result<TransactionRecord> {
93 serde_json::from_value(value).map_err(|err| {
94 AdkError::new(
95 ErrorComponent::Session,
96 ErrorCategory::Internal,
97 "payments.journal.record_deserialize",
98 format!("failed to deserialize stored transaction record: {err}"),
99 )
100 })
101}
102
103pub fn parse_locators(value: Option<Value>) -> Result<Vec<TransactionLocator>> {
109 match value {
110 Some(value) => serde_json::from_value(value).map_err(|err| {
111 AdkError::new(
112 ErrorComponent::Session,
113 ErrorCategory::Internal,
114 "payments.journal.index_deserialize",
115 format!("failed to deserialize stored transaction index: {err}"),
116 )
117 }),
118 None => Ok(Vec::new()),
119 }
120}
121
122fn serialize_value<T: Serialize + ?Sized>(value: &T, code: &'static str) -> Result<Value> {
123 serde_json::to_value(value).map_err(|err| {
124 AdkError::new(
125 ErrorComponent::Session,
126 ErrorCategory::Internal,
127 code,
128 format!("failed to serialize transaction journal state: {err}"),
129 )
130 })
131}
132
133fn summary_content(summary: &SafeTransactionSummary) -> Content {
134 redact_payment_content(&Content::new("system").with_text(summary.transcript_text()))
135}
136
137fn identity_hash(identity: &AdkIdentity) -> String {
138 let mut hasher = Sha256::new();
139 hasher.update(identity.app_name.as_ref().as_bytes());
140 hasher.update([0]);
141 hasher.update(identity.user_id.as_ref().as_bytes());
142 hasher.update([0]);
143 hasher.update(identity.session_id.as_ref().as_bytes());
144 hex::encode(hasher.finalize())[..24].to_string()
145}