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)]
19#[serde(untagged)]
20pub enum InvoiceReference {
21 Url(String),
23 Object(Box<crate::message::Invoice>),
25}
26
27impl InvoiceReference {
28 pub fn is_url(&self) -> bool {
30 matches!(self, InvoiceReference::Url(_))
31 }
32
33 pub fn is_object(&self) -> bool {
35 matches!(self, InvoiceReference::Object(_))
36 }
37
38 pub fn as_url(&self) -> Option<&str> {
40 match self {
41 InvoiceReference::Url(url) => Some(url),
42 _ => None,
43 }
44 }
45
46 pub fn as_object(&self) -> Option<&crate::message::Invoice> {
48 match self {
49 InvoiceReference::Object(invoice) => Some(invoice.as_ref()),
50 _ => None,
51 }
52 }
53
54 pub fn validate(&self) -> Result<()> {
56 match self {
57 InvoiceReference::Url(url) => {
58 if url.is_empty() {
60 return Err(Error::Validation("Invoice URL cannot be empty".to_string()));
61 }
62 Ok(())
64 }
65 InvoiceReference::Object(invoice) => {
66 invoice.validate()
68 }
69 }
70 }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
80#[tap(
81 message_type = "https://tap.rsvp/schema/1.0#Payment",
82 initiator,
83 authorizable,
84 transactable
85)]
86pub struct Payment {
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub asset: Option<AssetId>,
90
91 pub amount: String,
93
94 #[serde(rename = "currency", skip_serializing_if = "Option::is_none")]
96 pub currency_code: Option<String>,
97
98 #[serde(rename = "supportedAssets", skip_serializing_if = "Option::is_none")]
100 pub supported_assets: Option<Vec<AssetId>>,
101
102 #[serde(skip_serializing_if = "Option::is_none")]
104 #[tap(participant)]
105 pub customer: Option<Party>,
106
107 #[tap(participant)]
109 pub merchant: Party,
110
111 #[serde(skip)]
113 #[tap(transaction_id)]
114 pub transaction_id: Option<String>,
115
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub memo: Option<String>,
119
120 #[serde(skip_serializing_if = "Option::is_none")]
122 pub expiry: Option<String>,
123
124 #[serde(skip_serializing_if = "Option::is_none")]
126 pub invoice: Option<InvoiceReference>,
127
128 #[serde(default)]
130 #[tap(participant_list)]
131 pub agents: Vec<Agent>,
132
133 #[serde(skip_serializing_if = "Option::is_none")]
135 #[tap(connection_id)]
136 pub connection_id: Option<String>,
137
138 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
140 pub metadata: HashMap<String, serde_json::Value>,
141}
142
143#[derive(Default)]
145pub struct PaymentBuilder {
146 asset: Option<AssetId>,
147 amount: Option<String>,
148 currency_code: Option<String>,
149 supported_assets: Option<Vec<AssetId>>,
150 customer: Option<Party>,
151 merchant: Option<Party>,
152 transaction_id: Option<String>,
153 memo: Option<String>,
154 expiry: Option<String>,
155 invoice: Option<InvoiceReference>,
156 agents: Vec<Agent>,
157 metadata: HashMap<String, serde_json::Value>,
158}
159
160impl PaymentBuilder {
161 pub fn asset(mut self, asset: AssetId) -> Self {
163 self.asset = Some(asset);
164 self
165 }
166
167 pub fn amount(mut self, amount: String) -> Self {
169 self.amount = Some(amount);
170 self
171 }
172
173 pub fn currency_code(mut self, currency_code: String) -> Self {
175 self.currency_code = Some(currency_code);
176 self
177 }
178
179 pub fn supported_assets(mut self, supported_assets: Vec<AssetId>) -> Self {
181 self.supported_assets = Some(supported_assets);
182 self
183 }
184
185 pub fn add_supported_asset(mut self, asset: AssetId) -> Self {
187 if let Some(assets) = &mut self.supported_assets {
188 assets.push(asset);
189 } else {
190 self.supported_assets = Some(vec![asset]);
191 }
192 self
193 }
194
195 pub fn customer(mut self, customer: Party) -> Self {
197 self.customer = Some(customer);
198 self
199 }
200
201 pub fn merchant(mut self, merchant: Party) -> Self {
203 self.merchant = Some(merchant);
204 self
205 }
206
207 pub fn transaction_id(mut self, transaction_id: String) -> Self {
209 self.transaction_id = Some(transaction_id);
210 self
211 }
212
213 pub fn memo(mut self, memo: String) -> Self {
215 self.memo = Some(memo);
216 self
217 }
218
219 pub fn expiry(mut self, expiry: String) -> Self {
221 self.expiry = Some(expiry);
222 self
223 }
224
225 pub fn invoice(mut self, invoice: crate::message::Invoice) -> Self {
227 self.invoice = Some(InvoiceReference::Object(Box::new(invoice)));
228 self
229 }
230
231 pub fn invoice_url(mut self, url: String) -> Self {
233 self.invoice = Some(InvoiceReference::Url(url));
234 self
235 }
236
237 pub fn add_agent(mut self, agent: Agent) -> Self {
239 self.agents.push(agent);
240 self
241 }
242
243 pub fn agents(mut self, agents: Vec<Agent>) -> Self {
245 self.agents = agents;
246 self
247 }
248
249 pub fn add_metadata(mut self, key: String, value: serde_json::Value) -> Self {
251 self.metadata.insert(key, value);
252 self
253 }
254
255 pub fn metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
257 self.metadata = metadata;
258 self
259 }
260
261 pub fn build(self) -> Payment {
267 if self.asset.is_none() && self.currency_code.is_none() {
269 panic!("Either asset or currency_code is required");
270 }
271
272 Payment {
273 asset: self.asset,
274 amount: self.amount.expect("Amount is required"),
275 currency_code: self.currency_code,
276 supported_assets: self.supported_assets,
277 customer: self.customer,
278 merchant: self.merchant.expect("Merchant is required"),
279 transaction_id: self.transaction_id,
280 memo: self.memo,
281 expiry: self.expiry,
282 invoice: self.invoice,
283 agents: self.agents,
284 connection_id: None,
285 metadata: self.metadata,
286 }
287 }
288}
289
290impl Payment {
291 pub fn with_asset(asset: AssetId, amount: String, merchant: Party, agents: Vec<Agent>) -> Self {
293 Self {
294 asset: Some(asset),
295 amount,
296 currency_code: None,
297 supported_assets: None,
298 customer: None,
299 merchant,
300 transaction_id: None,
301 memo: None,
302 expiry: None,
303 invoice: None,
304 agents,
305 connection_id: None,
306 metadata: HashMap::new(),
307 }
308 }
309
310 pub fn with_currency(
312 currency_code: String,
313 amount: String,
314 merchant: Party,
315 agents: Vec<Agent>,
316 ) -> Self {
317 Self {
318 asset: None,
319 amount,
320 currency_code: Some(currency_code),
321 supported_assets: None,
322 customer: None,
323 merchant,
324 transaction_id: None,
325 memo: None,
326 expiry: None,
327 invoice: None,
328 agents,
329 connection_id: None,
330 metadata: HashMap::new(),
331 }
332 }
333
334 pub fn with_currency_and_assets(
336 currency_code: String,
337 amount: String,
338 supported_assets: Vec<AssetId>,
339 merchant: Party,
340 agents: Vec<Agent>,
341 ) -> Self {
342 Self {
343 asset: None,
344 amount,
345 currency_code: Some(currency_code),
346 supported_assets: Some(supported_assets),
347 customer: None,
348 merchant,
349 transaction_id: None,
350 memo: None,
351 expiry: None,
352 invoice: None,
353 agents,
354 connection_id: None,
355 metadata: HashMap::new(),
356 }
357 }
358
359 pub fn validate(&self) -> Result<()> {
361 if self.asset.is_none() && self.currency_code.is_none() {
363 return Err(Error::Validation(
364 "Either asset or currency_code must be provided".to_string(),
365 ));
366 }
367
368 if let Some(asset) = &self.asset {
370 if asset.namespace().is_empty() || asset.reference().is_empty() {
371 return Err(Error::Validation("Asset ID is invalid".to_string()));
372 }
373 }
374
375 if self.amount.is_empty() {
377 return Err(Error::Validation("Amount is required".to_string()));
378 }
379
380 match self.amount.parse::<f64>() {
382 Ok(amount) if amount <= 0.0 => {
383 return Err(Error::Validation("Amount must be positive".to_string()));
384 }
385 Err(_) => {
386 return Err(Error::Validation(
387 "Amount must be a valid number".to_string(),
388 ));
389 }
390 _ => {}
391 }
392
393 if self.merchant.id().is_empty() {
395 return Err(Error::Validation("Merchant ID is required".to_string()));
396 }
397
398 if let Some(supported_assets) = &self.supported_assets {
400 if supported_assets.is_empty() {
401 return Err(Error::Validation(
402 "Supported assets list cannot be empty".to_string(),
403 ));
404 }
405
406 for (i, asset) in supported_assets.iter().enumerate() {
408 if asset.namespace().is_empty() || asset.reference().is_empty() {
409 return Err(Error::Validation(format!(
410 "Supported asset at index {} is invalid",
411 i
412 )));
413 }
414 }
415 }
416
417 if let Some(invoice) = &self.invoice {
419 if let Err(e) = invoice.validate() {
421 return Err(Error::Validation(format!(
422 "Invoice validation failed: {}",
423 e
424 )));
425 }
426 }
427
428 Ok(())
429 }
430}