1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use tap_caip::AssetId;
10
11use crate::didcomm::PlainMessage;
12use crate::error::{Error, Result};
13use crate::impl_tap_message;
14use crate::message::tap_message_trait::{Authorizable, Connectable, TapMessageBody};
15use crate::message::{Authorize, Participant, Policy, RemoveAgent, ReplaceAgent, UpdatePolicies};
16use chrono::Utc;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct Payment {
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub asset: Option<AssetId>,
29
30 pub amount: String,
32
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub currency_code: Option<String>,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub supported_assets: Option<Vec<AssetId>>,
40
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub customer: Option<Participant>,
44
45 pub merchant: Participant,
47
48 pub transaction_id: String,
50
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub memo: Option<String>,
54
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub expiry: Option<String>,
58
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub invoice: Option<crate::message::Invoice>,
62
63 #[serde(default)]
65 pub agents: Vec<Participant>,
66
67 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
69 pub metadata: HashMap<String, serde_json::Value>,
70}
71
72#[derive(Default)]
74pub struct PaymentBuilder {
75 asset: Option<AssetId>,
76 amount: Option<String>,
77 currency_code: Option<String>,
78 supported_assets: Option<Vec<AssetId>>,
79 customer: Option<Participant>,
80 merchant: Option<Participant>,
81 transaction_id: Option<String>,
82 memo: Option<String>,
83 expiry: Option<String>,
84 invoice: Option<crate::message::Invoice>,
85 agents: Vec<Participant>,
86 metadata: HashMap<String, serde_json::Value>,
87}
88
89impl PaymentBuilder {
90 pub fn asset(mut self, asset: AssetId) -> Self {
92 self.asset = Some(asset);
93 self
94 }
95
96 pub fn amount(mut self, amount: String) -> Self {
98 self.amount = Some(amount);
99 self
100 }
101
102 pub fn currency_code(mut self, currency_code: String) -> Self {
104 self.currency_code = Some(currency_code);
105 self
106 }
107
108 pub fn supported_assets(mut self, supported_assets: Vec<AssetId>) -> Self {
110 self.supported_assets = Some(supported_assets);
111 self
112 }
113
114 pub fn add_supported_asset(mut self, asset: AssetId) -> Self {
116 if let Some(assets) = &mut self.supported_assets {
117 assets.push(asset);
118 } else {
119 self.supported_assets = Some(vec![asset]);
120 }
121 self
122 }
123
124 pub fn customer(mut self, customer: Participant) -> Self {
126 self.customer = Some(customer);
127 self
128 }
129
130 pub fn merchant(mut self, merchant: Participant) -> Self {
132 self.merchant = Some(merchant);
133 self
134 }
135
136 pub fn transaction_id(mut self, transaction_id: String) -> Self {
138 self.transaction_id = Some(transaction_id);
139 self
140 }
141
142 pub fn memo(mut self, memo: String) -> Self {
144 self.memo = Some(memo);
145 self
146 }
147
148 pub fn expiry(mut self, expiry: String) -> Self {
150 self.expiry = Some(expiry);
151 self
152 }
153
154 pub fn invoice(mut self, invoice: crate::message::Invoice) -> Self {
156 self.invoice = Some(invoice);
157 self
158 }
159
160 pub fn add_agent(mut self, agent: Participant) -> Self {
162 self.agents.push(agent);
163 self
164 }
165
166 pub fn agents(mut self, agents: Vec<Participant>) -> Self {
168 self.agents = agents;
169 self
170 }
171
172 pub fn add_metadata(mut self, key: String, value: serde_json::Value) -> Self {
174 self.metadata.insert(key, value);
175 self
176 }
177
178 pub fn metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
180 self.metadata = metadata;
181 self
182 }
183
184 pub fn build(self) -> Payment {
190 if self.asset.is_none() && self.currency_code.is_none() {
192 panic!("Either asset or currency_code is required");
193 }
194
195 Payment {
196 asset: self.asset,
197 amount: self.amount.expect("Amount is required"),
198 currency_code: self.currency_code,
199 supported_assets: self.supported_assets,
200 customer: self.customer,
201 merchant: self.merchant.expect("Merchant is required"),
202 transaction_id: self
203 .transaction_id
204 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
205 memo: self.memo,
206 expiry: self.expiry,
207 invoice: self.invoice,
208 agents: self.agents,
209 metadata: self.metadata,
210 }
211 }
212}
213
214impl Connectable for Payment {
215 fn with_connection(&mut self, connect_id: &str) -> &mut Self {
216 self.metadata.insert(
218 "connect_id".to_string(),
219 serde_json::Value::String(connect_id.to_string()),
220 );
221 self
222 }
223
224 fn has_connection(&self) -> bool {
225 self.metadata.contains_key("connect_id")
226 }
227
228 fn connection_id(&self) -> Option<&str> {
229 self.metadata.get("connect_id").and_then(|v| v.as_str())
230 }
231}
232
233impl TapMessageBody for Payment {
234 fn message_type() -> &'static str {
235 "https://tap.rsvp/schema/1.0#payment"
236 }
237
238 fn validate(&self) -> Result<()> {
239 if self.asset.is_none() && self.currency_code.is_none() {
241 return Err(Error::Validation(
242 "Either asset or currency_code must be provided".to_string(),
243 ));
244 }
245
246 if let Some(asset) = &self.asset {
248 if asset.namespace().is_empty() || asset.reference().is_empty() {
249 return Err(Error::Validation("Asset ID is invalid".to_string()));
250 }
251 }
252
253 if self.amount.is_empty() {
255 return Err(Error::Validation("Amount is required".to_string()));
256 }
257
258 match self.amount.parse::<f64>() {
260 Ok(amount) if amount <= 0.0 => {
261 return Err(Error::Validation("Amount must be positive".to_string()));
262 }
263 Err(_) => {
264 return Err(Error::Validation(
265 "Amount must be a valid number".to_string(),
266 ));
267 }
268 _ => {}
269 }
270
271 if self.merchant.id.is_empty() {
273 return Err(Error::Validation("Merchant ID is required".to_string()));
274 }
275
276 if let Some(supported_assets) = &self.supported_assets {
278 if supported_assets.is_empty() {
279 return Err(Error::Validation(
280 "Supported assets list cannot be empty".to_string(),
281 ));
282 }
283
284 for (i, asset) in supported_assets.iter().enumerate() {
286 if asset.namespace().is_empty() || asset.reference().is_empty() {
287 return Err(Error::Validation(format!(
288 "Supported asset at index {} is invalid",
289 i
290 )));
291 }
292 }
293 }
294
295 if let Some(invoice) = &self.invoice {
297 if let Err(e) = invoice.validate() {
299 return Err(Error::Validation(format!(
300 "Invoice validation failed: {}",
301 e
302 )));
303 }
304 }
305
306 Ok(())
307 }
308
309 fn to_didcomm(&self, from_did: &str) -> Result<PlainMessage> {
310 let mut body_json =
312 serde_json::to_value(self).map_err(|e| Error::SerializationError(e.to_string()))?;
313
314 if let Some(body_obj) = body_json.as_object_mut() {
316 body_obj.insert(
317 "@type".to_string(),
318 serde_json::Value::String(Self::message_type().to_string()),
319 );
320 }
321
322 let mut agent_dids = Vec::new();
324
325 agent_dids.push(self.merchant.id.clone());
327
328 if let Some(customer) = &self.customer {
330 agent_dids.push(customer.id.clone());
331 }
332
333 for agent in &self.agents {
335 agent_dids.push(agent.id.clone());
336 }
337
338 agent_dids.sort();
340 agent_dids.dedup();
341
342 agent_dids.retain(|did| did != from_did);
344
345 let now = Utc::now().timestamp() as u64;
346
347 let pthid = self
349 .connection_id()
350 .map(|connect_id| connect_id.to_string());
351
352 let expires_time = self.expiry.as_ref().and_then(|expiry| {
354 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(expiry) {
356 Some(dt.timestamp() as u64)
357 } else {
358 None
359 }
360 });
361
362 let message = PlainMessage {
364 id: uuid::Uuid::new_v4().to_string(),
365 typ: "application/didcomm-plain+json".to_string(),
366 type_: Self::message_type().to_string(),
367 body: body_json,
368 from: from_did.to_string(),
369 to: agent_dids,
370 thid: Some(self.transaction_id.clone()),
371 pthid,
372 created_time: Some(now),
373 expires_time,
374 extra_headers: std::collections::HashMap::new(),
375 from_prior: None,
376 attachments: None,
377 };
378
379 Ok(message)
380 }
381
382 fn from_didcomm(message: &PlainMessage) -> Result<Self> {
383 if message.type_ != Self::message_type() {
385 return Err(Error::InvalidMessageType(format!(
386 "Expected {} but got {}",
387 Self::message_type(),
388 message.type_
389 )));
390 }
391
392 let payment: Payment = serde_json::from_value(message.body.clone())
394 .map_err(|e| Error::SerializationError(e.to_string()))?;
395
396 Ok(payment)
397 }
398}
399
400impl Authorizable for Payment {
401 fn authorize(&self, note: Option<String>) -> Authorize {
402 Authorize {
403 transaction_id: self.transaction_id.clone(),
404 note,
405 }
406 }
407
408 fn update_policies(&self, transaction_id: String, policies: Vec<Policy>) -> UpdatePolicies {
409 UpdatePolicies {
410 transaction_id,
411 policies,
412 }
413 }
414
415 fn replace_agent(
416 &self,
417 transaction_id: String,
418 original_agent: String,
419 replacement: Participant,
420 ) -> ReplaceAgent {
421 ReplaceAgent {
422 transaction_id,
423 original: original_agent,
424 replacement,
425 }
426 }
427
428 fn remove_agent(&self, transaction_id: String, agent: String) -> RemoveAgent {
429 RemoveAgent {
430 transaction_id,
431 agent,
432 }
433 }
434}
435
436impl Payment {
437 pub fn with_asset(
439 asset: AssetId,
440 amount: String,
441 merchant: Participant,
442 agents: Vec<Participant>,
443 ) -> Self {
444 Self {
445 asset: Some(asset),
446 amount,
447 currency_code: None,
448 supported_assets: None,
449 customer: None,
450 merchant,
451 transaction_id: uuid::Uuid::new_v4().to_string(),
452 memo: None,
453 expiry: None,
454 invoice: None,
455 agents,
456 metadata: HashMap::new(),
457 }
458 }
459
460 pub fn with_currency(
462 currency_code: String,
463 amount: String,
464 merchant: Participant,
465 agents: Vec<Participant>,
466 ) -> Self {
467 Self {
468 asset: None,
469 amount,
470 currency_code: Some(currency_code),
471 supported_assets: None,
472 customer: None,
473 merchant,
474 transaction_id: uuid::Uuid::new_v4().to_string(),
475 memo: None,
476 expiry: None,
477 invoice: None,
478 agents,
479 metadata: HashMap::new(),
480 }
481 }
482
483 pub fn with_currency_and_assets(
485 currency_code: String,
486 amount: String,
487 supported_assets: Vec<AssetId>,
488 merchant: Participant,
489 agents: Vec<Participant>,
490 ) -> Self {
491 Self {
492 asset: None,
493 amount,
494 currency_code: Some(currency_code),
495 supported_assets: Some(supported_assets),
496 customer: None,
497 merchant,
498 transaction_id: uuid::Uuid::new_v4().to_string(),
499 memo: None,
500 expiry: None,
501 invoice: None,
502 agents,
503 metadata: HashMap::new(),
504 }
505 }
506}
507
508impl_tap_message!(Payment);