use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tap_caip::AssetId;
use crate::error::{Error, Result};
use crate::message::agent::TapParticipant;
use crate::message::tap_message_trait::{TapMessage as TapMessageTrait, TapMessageBody};
use crate::message::{Agent, Party};
use crate::settlement_address::SettlementAddress;
use crate::TapMessage;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SupportedAsset {
Simple(AssetId),
Priced(AssetPricing),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssetPricing {
pub asset: String,
pub amount: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum InvoiceReference {
Url(String),
Object(Box<crate::message::Invoice>),
}
impl InvoiceReference {
pub fn is_url(&self) -> bool {
matches!(self, InvoiceReference::Url(_))
}
pub fn is_object(&self) -> bool {
matches!(self, InvoiceReference::Object(_))
}
pub fn as_url(&self) -> Option<&str> {
match self {
InvoiceReference::Url(url) => Some(url),
_ => None,
}
}
pub fn as_object(&self) -> Option<&crate::message::Invoice> {
match self {
InvoiceReference::Object(invoice) => Some(invoice.as_ref()),
_ => None,
}
}
pub fn validate(&self) -> Result<()> {
match self {
InvoiceReference::Url(url) => {
if url.is_empty() {
return Err(Error::Validation("Invoice URL cannot be empty".to_string()));
}
Ok(())
}
InvoiceReference::Object(invoice) => {
invoice.validate()
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
#[tap(
message_type = "https://tap.rsvp/schema/1.0#Payment",
initiator,
authorizable,
transactable
)]
pub struct Payment {
#[serde(skip_serializing_if = "Option::is_none")]
pub asset: Option<AssetId>,
pub amount: String,
#[serde(rename = "currency", skip_serializing_if = "Option::is_none")]
pub currency_code: Option<String>,
#[serde(rename = "supportedAssets", skip_serializing_if = "Option::is_none")]
pub supported_assets: Option<Vec<SupportedAsset>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[tap(participant)]
pub customer: Option<Party>,
#[tap(participant)]
pub merchant: Party,
#[serde(skip)]
#[tap(transaction_id)]
pub transaction_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memo: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expiry: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub invoice: Option<InvoiceReference>,
#[serde(default)]
#[tap(participant_list)]
pub agents: Vec<Agent>,
#[serde(skip_serializing_if = "Option::is_none")]
#[tap(connection_id)]
pub connection_id: Option<String>,
#[serde(
rename = "fallbackSettlementAddresses",
skip_serializing_if = "Option::is_none"
)]
pub fallback_settlement_addresses: Option<Vec<SettlementAddress>>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, serde_json::Value>,
}
#[derive(Default)]
pub struct PaymentBuilder {
asset: Option<AssetId>,
amount: Option<String>,
currency_code: Option<String>,
supported_assets: Option<Vec<SupportedAsset>>,
customer: Option<Party>,
merchant: Option<Party>,
transaction_id: Option<String>,
memo: Option<String>,
expiry: Option<String>,
invoice: Option<InvoiceReference>,
agents: Vec<Agent>,
fallback_settlement_addresses: Option<Vec<SettlementAddress>>,
metadata: HashMap<String, serde_json::Value>,
}
impl PaymentBuilder {
pub fn asset(mut self, asset: AssetId) -> Self {
self.asset = Some(asset);
self
}
pub fn amount(mut self, amount: String) -> Self {
self.amount = Some(amount);
self
}
pub fn currency_code(mut self, currency_code: String) -> Self {
self.currency_code = Some(currency_code);
self
}
pub fn supported_assets(mut self, supported_assets: Vec<SupportedAsset>) -> Self {
self.supported_assets = Some(supported_assets);
self
}
pub fn add_supported_asset(mut self, asset: AssetId) -> Self {
if let Some(assets) = &mut self.supported_assets {
assets.push(SupportedAsset::Simple(asset));
} else {
self.supported_assets = Some(vec![SupportedAsset::Simple(asset)]);
}
self
}
pub fn add_priced_asset(mut self, pricing: AssetPricing) -> Self {
if let Some(assets) = &mut self.supported_assets {
assets.push(SupportedAsset::Priced(pricing));
} else {
self.supported_assets = Some(vec![SupportedAsset::Priced(pricing)]);
}
self
}
pub fn customer(mut self, customer: Party) -> Self {
self.customer = Some(customer);
self
}
pub fn merchant(mut self, merchant: Party) -> Self {
self.merchant = Some(merchant);
self
}
pub fn transaction_id(mut self, transaction_id: String) -> Self {
self.transaction_id = Some(transaction_id);
self
}
pub fn memo(mut self, memo: String) -> Self {
self.memo = Some(memo);
self
}
pub fn expiry(mut self, expiry: String) -> Self {
self.expiry = Some(expiry);
self
}
pub fn invoice(mut self, invoice: crate::message::Invoice) -> Self {
self.invoice = Some(InvoiceReference::Object(Box::new(invoice)));
self
}
pub fn invoice_url(mut self, url: String) -> Self {
self.invoice = Some(InvoiceReference::Url(url));
self
}
pub fn add_agent(mut self, agent: Agent) -> Self {
self.agents.push(agent);
self
}
pub fn agents(mut self, agents: Vec<Agent>) -> Self {
self.agents = agents;
self
}
pub fn add_metadata(mut self, key: String, value: serde_json::Value) -> Self {
self.metadata.insert(key, value);
self
}
pub fn metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
self.metadata = metadata;
self
}
pub fn add_fallback_settlement_address(mut self, address: SettlementAddress) -> Self {
if let Some(addresses) = &mut self.fallback_settlement_addresses {
addresses.push(address);
} else {
self.fallback_settlement_addresses = Some(vec![address]);
}
self
}
pub fn fallback_settlement_addresses(mut self, addresses: Vec<SettlementAddress>) -> Self {
self.fallback_settlement_addresses = Some(addresses);
self
}
pub fn build(self) -> Payment {
if self.asset.is_none() && self.currency_code.is_none() {
panic!("Either asset or currency_code is required");
}
Payment {
asset: self.asset,
amount: self.amount.expect("Amount is required"),
currency_code: self.currency_code,
supported_assets: self.supported_assets,
customer: self.customer,
merchant: self.merchant.expect("Merchant is required"),
transaction_id: self.transaction_id,
memo: self.memo,
expiry: self.expiry,
invoice: self.invoice,
agents: self.agents,
connection_id: None,
fallback_settlement_addresses: self.fallback_settlement_addresses,
metadata: self.metadata,
}
}
}
impl Payment {
pub fn builder() -> PaymentBuilder {
PaymentBuilder::default()
}
pub fn with_asset(asset: AssetId, amount: String, merchant: Party, agents: Vec<Agent>) -> Self {
Self {
asset: Some(asset),
amount,
currency_code: None,
supported_assets: None,
customer: None,
merchant,
transaction_id: None,
memo: None,
expiry: None,
invoice: None,
agents,
connection_id: None,
fallback_settlement_addresses: None,
metadata: HashMap::new(),
}
}
pub fn with_currency(
currency_code: String,
amount: String,
merchant: Party,
agents: Vec<Agent>,
) -> Self {
Self {
asset: None,
amount,
currency_code: Some(currency_code),
supported_assets: None,
customer: None,
merchant,
transaction_id: None,
memo: None,
expiry: None,
invoice: None,
agents,
connection_id: None,
fallback_settlement_addresses: None,
metadata: HashMap::new(),
}
}
pub fn with_currency_and_assets(
currency_code: String,
amount: String,
supported_assets: Vec<SupportedAsset>,
merchant: Party,
agents: Vec<Agent>,
) -> Self {
Self {
asset: None,
amount,
currency_code: Some(currency_code),
supported_assets: Some(supported_assets),
customer: None,
merchant,
transaction_id: None,
memo: None,
expiry: None,
invoice: None,
agents,
connection_id: None,
fallback_settlement_addresses: None,
metadata: HashMap::new(),
}
}
pub fn validate(&self) -> Result<()> {
if self.asset.is_none() && self.currency_code.is_none() {
return Err(Error::Validation(
"Either asset or currency_code must be provided".to_string(),
));
}
if let Some(asset) = &self.asset {
if asset.namespace().is_empty() || asset.reference().is_empty() {
return Err(Error::Validation("Asset ID is invalid".to_string()));
}
}
if self.amount.is_empty() {
return Err(Error::Validation("Amount is required".to_string()));
}
match self.amount.parse::<f64>() {
Ok(amount) if !amount.is_finite() => {
return Err(Error::Validation(
"Amount must be a finite number".to_string(),
));
}
Ok(amount) if amount <= 0.0 => {
return Err(Error::Validation("Amount must be positive".to_string()));
}
Err(_) => {
return Err(Error::Validation(
"Amount must be a valid number".to_string(),
));
}
_ => {}
}
if self.merchant.id().is_empty() {
return Err(Error::Validation("Merchant ID is required".to_string()));
}
if let Some(supported_assets) = &self.supported_assets {
if supported_assets.is_empty() {
return Err(Error::Validation(
"Supported assets list cannot be empty".to_string(),
));
}
for (i, supported) in supported_assets.iter().enumerate() {
match supported {
SupportedAsset::Simple(asset) => {
if asset.namespace().is_empty() || asset.reference().is_empty() {
return Err(Error::Validation(format!(
"Supported asset at index {} is invalid",
i
)));
}
}
SupportedAsset::Priced(pricing) => {
if pricing.asset.is_empty() {
return Err(Error::Validation(format!(
"Supported asset at index {} has empty asset identifier",
i
)));
}
if pricing.amount.is_empty() {
return Err(Error::Validation(format!(
"Supported asset at index {} has empty amount",
i
)));
}
}
}
}
}
if let Some(invoice) = &self.invoice {
if let Err(e) = invoice.validate() {
return Err(Error::Validation(format!(
"Invoice validation failed: {}",
e
)));
}
}
Ok(())
}
}