1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use tap_caip::AssetId;
10
11use crate::error::{Error, Result};
12use crate::message::agent::TapParticipant;
13use crate::message::tap_message_trait::{TapMessage as TapMessageTrait, TapMessageBody};
14use crate::message::{Agent, Party};
15use crate::TapMessage;
16
17#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
24#[tap(
25 message_type = "https://tap.rsvp/schema/1.0#Payment",
26 initiator,
27 authorizable,
28 transactable
29)]
30pub struct Payment {
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub asset: Option<AssetId>,
34
35 pub amount: String,
37
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub currency_code: Option<String>,
41
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub supported_assets: Option<Vec<AssetId>>,
45
46 #[serde(skip_serializing_if = "Option::is_none")]
48 #[tap(participant)]
49 pub customer: Option<Party>,
50
51 #[tap(participant)]
53 pub merchant: Party,
54
55 #[tap(transaction_id)]
57 pub transaction_id: String,
58
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub memo: Option<String>,
62
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub expiry: Option<String>,
66
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub invoice: Option<crate::message::Invoice>,
70
71 #[serde(default)]
73 #[tap(participant_list)]
74 pub agents: Vec<Agent>,
75
76 #[serde(skip_serializing_if = "Option::is_none")]
78 #[tap(connection_id)]
79 pub connection_id: Option<String>,
80
81 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
83 pub metadata: HashMap<String, serde_json::Value>,
84}
85
86#[derive(Default)]
88pub struct PaymentBuilder {
89 asset: Option<AssetId>,
90 amount: Option<String>,
91 currency_code: Option<String>,
92 supported_assets: Option<Vec<AssetId>>,
93 customer: Option<Party>,
94 merchant: Option<Party>,
95 transaction_id: Option<String>,
96 memo: Option<String>,
97 expiry: Option<String>,
98 invoice: Option<crate::message::Invoice>,
99 agents: Vec<Agent>,
100 metadata: HashMap<String, serde_json::Value>,
101}
102
103impl PaymentBuilder {
104 pub fn asset(mut self, asset: AssetId) -> Self {
106 self.asset = Some(asset);
107 self
108 }
109
110 pub fn amount(mut self, amount: String) -> Self {
112 self.amount = Some(amount);
113 self
114 }
115
116 pub fn currency_code(mut self, currency_code: String) -> Self {
118 self.currency_code = Some(currency_code);
119 self
120 }
121
122 pub fn supported_assets(mut self, supported_assets: Vec<AssetId>) -> Self {
124 self.supported_assets = Some(supported_assets);
125 self
126 }
127
128 pub fn add_supported_asset(mut self, asset: AssetId) -> Self {
130 if let Some(assets) = &mut self.supported_assets {
131 assets.push(asset);
132 } else {
133 self.supported_assets = Some(vec![asset]);
134 }
135 self
136 }
137
138 pub fn customer(mut self, customer: Party) -> Self {
140 self.customer = Some(customer);
141 self
142 }
143
144 pub fn merchant(mut self, merchant: Party) -> Self {
146 self.merchant = Some(merchant);
147 self
148 }
149
150 pub fn transaction_id(mut self, transaction_id: String) -> Self {
152 self.transaction_id = Some(transaction_id);
153 self
154 }
155
156 pub fn memo(mut self, memo: String) -> Self {
158 self.memo = Some(memo);
159 self
160 }
161
162 pub fn expiry(mut self, expiry: String) -> Self {
164 self.expiry = Some(expiry);
165 self
166 }
167
168 pub fn invoice(mut self, invoice: crate::message::Invoice) -> Self {
170 self.invoice = Some(invoice);
171 self
172 }
173
174 pub fn add_agent(mut self, agent: Agent) -> Self {
176 self.agents.push(agent);
177 self
178 }
179
180 pub fn agents(mut self, agents: Vec<Agent>) -> Self {
182 self.agents = agents;
183 self
184 }
185
186 pub fn add_metadata(mut self, key: String, value: serde_json::Value) -> Self {
188 self.metadata.insert(key, value);
189 self
190 }
191
192 pub fn metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
194 self.metadata = metadata;
195 self
196 }
197
198 pub fn build(self) -> Payment {
204 if self.asset.is_none() && self.currency_code.is_none() {
206 panic!("Either asset or currency_code is required");
207 }
208
209 Payment {
210 asset: self.asset,
211 amount: self.amount.expect("Amount is required"),
212 currency_code: self.currency_code,
213 supported_assets: self.supported_assets,
214 customer: self.customer,
215 merchant: self.merchant.expect("Merchant is required"),
216 transaction_id: self
217 .transaction_id
218 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
219 memo: self.memo,
220 expiry: self.expiry,
221 invoice: self.invoice,
222 agents: self.agents,
223 connection_id: None,
224 metadata: self.metadata,
225 }
226 }
227}
228
229impl Payment {
230 pub fn with_asset(asset: AssetId, amount: String, merchant: Party, agents: Vec<Agent>) -> Self {
232 Self {
233 asset: Some(asset),
234 amount,
235 currency_code: None,
236 supported_assets: None,
237 customer: None,
238 merchant,
239 transaction_id: uuid::Uuid::new_v4().to_string(),
240 memo: None,
241 expiry: None,
242 invoice: None,
243 agents,
244 connection_id: None,
245 metadata: HashMap::new(),
246 }
247 }
248
249 pub fn with_currency(
251 currency_code: String,
252 amount: String,
253 merchant: Party,
254 agents: Vec<Agent>,
255 ) -> Self {
256 Self {
257 asset: None,
258 amount,
259 currency_code: Some(currency_code),
260 supported_assets: None,
261 customer: None,
262 merchant,
263 transaction_id: uuid::Uuid::new_v4().to_string(),
264 memo: None,
265 expiry: None,
266 invoice: None,
267 agents,
268 connection_id: None,
269 metadata: HashMap::new(),
270 }
271 }
272
273 pub fn with_currency_and_assets(
275 currency_code: String,
276 amount: String,
277 supported_assets: Vec<AssetId>,
278 merchant: Party,
279 agents: Vec<Agent>,
280 ) -> Self {
281 Self {
282 asset: None,
283 amount,
284 currency_code: Some(currency_code),
285 supported_assets: Some(supported_assets),
286 customer: None,
287 merchant,
288 transaction_id: uuid::Uuid::new_v4().to_string(),
289 memo: None,
290 expiry: None,
291 invoice: None,
292 agents,
293 connection_id: None,
294 metadata: HashMap::new(),
295 }
296 }
297
298 pub fn validate(&self) -> Result<()> {
300 if self.asset.is_none() && self.currency_code.is_none() {
302 return Err(Error::Validation(
303 "Either asset or currency_code must be provided".to_string(),
304 ));
305 }
306
307 if let Some(asset) = &self.asset {
309 if asset.namespace().is_empty() || asset.reference().is_empty() {
310 return Err(Error::Validation("Asset ID is invalid".to_string()));
311 }
312 }
313
314 if self.amount.is_empty() {
316 return Err(Error::Validation("Amount is required".to_string()));
317 }
318
319 match self.amount.parse::<f64>() {
321 Ok(amount) if amount <= 0.0 => {
322 return Err(Error::Validation("Amount must be positive".to_string()));
323 }
324 Err(_) => {
325 return Err(Error::Validation(
326 "Amount must be a valid number".to_string(),
327 ));
328 }
329 _ => {}
330 }
331
332 if self.merchant.id().is_empty() {
334 return Err(Error::Validation("Merchant ID is required".to_string()));
335 }
336
337 if let Some(supported_assets) = &self.supported_assets {
339 if supported_assets.is_empty() {
340 return Err(Error::Validation(
341 "Supported assets list cannot be empty".to_string(),
342 ));
343 }
344
345 for (i, asset) in supported_assets.iter().enumerate() {
347 if asset.namespace().is_empty() || asset.reference().is_empty() {
348 return Err(Error::Validation(format!(
349 "Supported asset at index {} is invalid",
350 i
351 )));
352 }
353 }
354 }
355
356 if let Some(invoice) = &self.invoice {
358 if let Err(e) = invoice.validate() {
360 return Err(Error::Validation(format!(
361 "Invoice validation failed: {}",
362 e
363 )));
364 }
365 }
366
367 Ok(())
368 }
369}