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#[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#[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#[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#[derive(Debug, Deserialize)]
60#[serde(rename_all = "camelCase")]
61struct CancelParams {
62 transaction_id: String,
63 #[serde(default)]
64 reason: Option<String>,
65}
66
67#[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
132struct 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, ¶ms.merchant_id, ¶ms.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
172pub fn create_checkout_tool(checkout_service: Arc<dyn MerchantCheckoutService>) -> impl Tool {
174 CreateCheckoutTool { checkout_service }
175}
176
177struct 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(¶ms.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
209pub fn update_checkout_tool(checkout_service: Arc<dyn MerchantCheckoutService>) -> impl Tool {
211 UpdateCheckoutTool { checkout_service }
212}
213
214struct 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(¶ms.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
249pub fn complete_checkout_tool(checkout_service: Arc<dyn MerchantCheckoutService>) -> impl Tool {
251 CompleteCheckoutTool { checkout_service }
252}
253
254struct 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(¶ms.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
289pub fn cancel_checkout_tool(checkout_service: Arc<dyn MerchantCheckoutService>) -> impl Tool {
291 CancelCheckoutTool { checkout_service }
292}