Skip to main content

adk_payments/tools/
checkout.rs

1use std::sync::Arc;
2
3use adk_core::identity::AdkIdentity;
4use adk_core::{AdkError, ErrorCategory, ErrorComponent, Result, Tool, ToolContext};
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::auth::{
10    CHECKOUT_CANCEL_SCOPES, CHECKOUT_COMPLETE_SCOPES, CHECKOUT_CREATE_SCOPES,
11    CHECKOUT_UPDATE_SCOPES,
12};
13use crate::domain::{
14    Cart, CommerceActor, CommerceActorRole, CommerceMode, FulfillmentSelection, MerchantRef,
15    PaymentMethodSelection, ProtocolDescriptor, ProtocolExtensions, SafeTransactionSummary,
16    TransactionId,
17};
18use crate::guardrail::redact_tool_output;
19use crate::kernel::commands::{
20    CancelCheckoutCommand, CommerceContext, CompleteCheckoutCommand, CreateCheckoutCommand,
21    UpdateCheckoutCommand,
22};
23use crate::kernel::service::MerchantCheckoutService;
24
25/// JSON parameters accepted by `payments_checkout_create`.
26#[derive(Debug, Deserialize)]
27#[serde(rename_all = "camelCase")]
28struct CreateParams {
29    merchant_id: String,
30    merchant_name: String,
31    cart: Cart,
32    #[serde(default)]
33    fulfillment: Option<FulfillmentSelection>,
34    #[serde(default)]
35    mode: Option<CommerceMode>,
36}
37
38/// JSON parameters accepted by `payments_checkout_update`.
39#[derive(Debug, Deserialize)]
40#[serde(rename_all = "camelCase")]
41struct UpdateParams {
42    transaction_id: String,
43    #[serde(default)]
44    cart: Option<Cart>,
45    #[serde(default)]
46    fulfillment: Option<FulfillmentSelection>,
47}
48
49/// JSON parameters accepted by `payments_checkout_complete`.
50#[derive(Debug, Deserialize)]
51#[serde(rename_all = "camelCase")]
52struct CompleteParams {
53    transaction_id: String,
54    #[serde(default)]
55    selected_payment_method: Option<PaymentMethodSelection>,
56}
57
58/// JSON parameters accepted by `payments_checkout_cancel`.
59#[derive(Debug, Deserialize)]
60#[serde(rename_all = "camelCase")]
61struct CancelParams {
62    transaction_id: String,
63    #[serde(default)]
64    reason: Option<String>,
65}
66
67/// Masked tool response wrapping a safe transaction summary.
68#[derive(Debug, Serialize)]
69#[serde(rename_all = "camelCase")]
70struct ToolResponse {
71    status: &'static str,
72    summary: SafeTransactionSummary,
73}
74
75fn parse_args<T: serde::de::DeserializeOwned>(tool_name: &str, args: Value) -> Result<T> {
76    serde_json::from_value(args).map_err(|err| {
77        AdkError::new(
78            ErrorComponent::Tool,
79            ErrorCategory::InvalidInput,
80            "payments.tools.invalid_args",
81            format!("invalid arguments for `{tool_name}`: {err}"),
82        )
83    })
84}
85
86fn tool_context(
87    transaction_id: &str,
88    merchant_id: &str,
89    merchant_name: &str,
90    mode: Option<CommerceMode>,
91    session_identity: Option<AdkIdentity>,
92) -> CommerceContext {
93    CommerceContext {
94        transaction_id: TransactionId::from(transaction_id),
95        session_identity,
96        actor: CommerceActor {
97            actor_id: "agent-tool".to_string(),
98            role: CommerceActorRole::AgentSurface,
99            display_name: Some("payment tool".to_string()),
100            tenant_id: None,
101            extensions: ProtocolExtensions::default(),
102        },
103        merchant_of_record: MerchantRef {
104            merchant_id: merchant_id.to_string(),
105            legal_name: merchant_name.to_string(),
106            display_name: Some(merchant_name.to_string()),
107            statement_descriptor: None,
108            country_code: None,
109            website: None,
110            extensions: ProtocolExtensions::default(),
111        },
112        payment_processor: None,
113        mode: mode.unwrap_or(CommerceMode::HumanPresent),
114        protocol: ProtocolDescriptor::new("adk-tool", Some("1.0".to_string())),
115        extensions: ProtocolExtensions::default(),
116    }
117}
118
119fn masked_response(summary: SafeTransactionSummary) -> Result<Value> {
120    let response = ToolResponse { status: "ok", summary };
121    let value = serde_json::to_value(&response).map_err(|err| {
122        AdkError::new(
123            ErrorComponent::Tool,
124            ErrorCategory::Internal,
125            "payments.tools.serialize_failed",
126            format!("failed to serialize tool response: {err}"),
127        )
128    })?;
129    Ok(redact_tool_output(&value))
130}
131
132// ---------------------------------------------------------------------------
133// Create checkout tool
134// ---------------------------------------------------------------------------
135
136struct CreateCheckoutTool {
137    checkout_service: Arc<dyn MerchantCheckoutService>,
138}
139
140#[async_trait]
141impl Tool for CreateCheckoutTool {
142    fn name(&self) -> &str {
143        "payments_checkout_create"
144    }
145
146    fn description(&self) -> &str {
147        "Create a new merchant-backed checkout session. Returns a masked transaction summary."
148    }
149
150    fn required_scopes(&self) -> &[&str] {
151        CHECKOUT_CREATE_SCOPES
152    }
153
154    async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
155        let params: CreateParams = parse_args("checkout_create", args)?;
156        let tx_id = format!(
157            "tool_tx_{:016x}",
158            std::time::SystemTime::now()
159                .duration_since(std::time::UNIX_EPOCH)
160                .unwrap_or_default()
161                .as_nanos()
162        );
163        let context =
164            tool_context(&tx_id, &params.merchant_id, &params.merchant_name, params.mode, None);
165        let command =
166            CreateCheckoutCommand { context, cart: params.cart, fulfillment: params.fulfillment };
167        let record = self.checkout_service.create_checkout(command).await?;
168        masked_response(record.safe_summary)
169    }
170}
171
172/// Creates a `payments_checkout_create` tool backed by the given checkout service.
173pub fn create_checkout_tool(checkout_service: Arc<dyn MerchantCheckoutService>) -> impl Tool {
174    CreateCheckoutTool { checkout_service }
175}
176
177// ---------------------------------------------------------------------------
178// Update checkout tool
179// ---------------------------------------------------------------------------
180
181struct UpdateCheckoutTool {
182    checkout_service: Arc<dyn MerchantCheckoutService>,
183}
184
185#[async_trait]
186impl Tool for UpdateCheckoutTool {
187    fn name(&self) -> &str {
188        "payments_checkout_update"
189    }
190
191    fn description(&self) -> &str {
192        "Update cart or fulfillment details on an existing checkout session."
193    }
194
195    fn required_scopes(&self) -> &[&str] {
196        CHECKOUT_UPDATE_SCOPES
197    }
198
199    async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
200        let params: UpdateParams = parse_args("checkout_update", args)?;
201        let context = tool_context(&params.transaction_id, "", "unknown", None, None);
202        let command =
203            UpdateCheckoutCommand { context, cart: params.cart, fulfillment: params.fulfillment };
204        let record = self.checkout_service.update_checkout(command).await?;
205        masked_response(record.safe_summary)
206    }
207}
208
209/// Creates a `payments_checkout_update` tool backed by the given checkout service.
210pub fn update_checkout_tool(checkout_service: Arc<dyn MerchantCheckoutService>) -> impl Tool {
211    UpdateCheckoutTool { checkout_service }
212}
213
214// ---------------------------------------------------------------------------
215// Complete checkout tool
216// ---------------------------------------------------------------------------
217
218struct CompleteCheckoutTool {
219    checkout_service: Arc<dyn MerchantCheckoutService>,
220}
221
222#[async_trait]
223impl Tool for CompleteCheckoutTool {
224    fn name(&self) -> &str {
225        "payments_checkout_complete"
226    }
227
228    fn description(&self) -> &str {
229        "Finalize a checkout session and produce an order. The continuation identifier is returned explicitly for follow-up."
230    }
231
232    fn required_scopes(&self) -> &[&str] {
233        CHECKOUT_COMPLETE_SCOPES
234    }
235
236    async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
237        let params: CompleteParams = parse_args("checkout_complete", args)?;
238        let context = tool_context(&params.transaction_id, "", "unknown", None, None);
239        let command = CompleteCheckoutCommand {
240            context,
241            selected_payment_method: params.selected_payment_method,
242            extensions: ProtocolExtensions::default(),
243        };
244        let record = self.checkout_service.complete_checkout(command).await?;
245        masked_response(record.safe_summary)
246    }
247}
248
249/// Creates a `payments_checkout_complete` tool backed by the given checkout service.
250pub fn complete_checkout_tool(checkout_service: Arc<dyn MerchantCheckoutService>) -> impl Tool {
251    CompleteCheckoutTool { checkout_service }
252}
253
254// ---------------------------------------------------------------------------
255// Cancel checkout tool
256// ---------------------------------------------------------------------------
257
258struct CancelCheckoutTool {
259    checkout_service: Arc<dyn MerchantCheckoutService>,
260}
261
262#[async_trait]
263impl Tool for CancelCheckoutTool {
264    fn name(&self) -> &str {
265        "payments_checkout_cancel"
266    }
267
268    fn description(&self) -> &str {
269        "Cancel an active checkout session or transaction."
270    }
271
272    fn required_scopes(&self) -> &[&str] {
273        CHECKOUT_CANCEL_SCOPES
274    }
275
276    async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
277        let params: CancelParams = parse_args("checkout_cancel", args)?;
278        let context = tool_context(&params.transaction_id, "", "unknown", None, None);
279        let command = CancelCheckoutCommand {
280            context,
281            reason: params.reason,
282            extensions: ProtocolExtensions::default(),
283        };
284        let record = self.checkout_service.cancel_checkout(command).await?;
285        masked_response(record.safe_summary)
286    }
287}
288
289/// Creates a `payments_checkout_cancel` tool backed by the given checkout service.
290pub fn cancel_checkout_tool(checkout_service: Arc<dyn MerchantCheckoutService>) -> impl Tool {
291    CancelCheckoutTool { checkout_service }
292}