Skip to main content

adk_payments/journal/
session_state.rs

1use 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/// Locates one journal entry by session identity plus canonical transaction ID.
16#[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/// Returns the app-scoped session-state key for one transaction record.
24#[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/// Returns the app-scoped state key used to store unresolved transaction locators.
30#[must_use]
31pub fn active_index_state_key() -> String {
32    format!("{KEY_PREFIX_APP}{ACTIVE_INDEX_KEY}")
33}
34
35/// Returns the app-scoped state key used to store completed transaction locators.
36#[must_use]
37pub fn completed_index_state_key() -> String {
38    format!("{KEY_PREFIX_APP}{COMPLETED_INDEX_KEY}")
39}
40
41/// Returns the app-scoped state key used to store one transaction record.
42#[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
50/// Builds the safe event that mirrors journal state into session storage.
51///
52/// # Errors
53///
54/// Returns an error if the journal state cannot be serialized.
55pub 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
87/// Parses one serialized transaction record from session state.
88///
89/// # Errors
90///
91/// Returns an error if the stored value is not a valid serialized transaction record.
92pub 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
103/// Parses one serialized locator index from session state.
104///
105/// # Errors
106///
107/// Returns an error if the stored value is not a valid locator list.
108pub 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}