You've already forked AstralRinth
forked from didirus/AstralRinth
Mural Pay integration (#4520)
* wip: muralpay integration * Basic Mural Pay API bindings * Fix clippy * use dotenvy in muralpay example * Refactor payout creation code * wip: muralpay payout requests * Mural Pay payouts work * Fix clippy * add mural pay fees API * Work on payout fee API * Fees API for more payment methods * Fix CI * Temporarily disable Venmo and PayPal methods from frontend * wip: counterparties * Start on counterparties and payment methods API * Mural Pay multiple methods when fetching * Don't send supported_countries to frontend * Add countries to muralpay fiat methods * Compile fix * Add exchange rate info to fees endpoint * Add fees to premium Tremendous options * Add delivery email field to Tremendous payouts * Add Tremendous product category to payout methods * Add bank details API to muralpay * Fix CI * Fix CI * Remove prepaid visa, compute fees properly for Tremendous methods * Add more details to Tremendous errors * Add fees to Mural * Payout history route and bank details * Re-add legacy PayPal/Venmo options for US * move the mural bank details route * Add utoipa support to payout endpoints * address some PR comments * add CORS to new utoipa routes * Immediately approve mural payouts * Add currency support to Tremendous payouts * Currency forex * add forex to tremendous fee request * Add Mural balance to bank balance info * Add more Tremendous currencies support * Transaction payouts available use the correct date * Address my own review comment * Address PR comments * Change Mural withdrawal limit to 3k * maybe fix tremendous gift cards * Change how Mural minimum withdrawals are calculated * Tweak min/max withdrawal values --------- Co-authored-by: Calum H. <contact@cal.engineer> Co-authored-by: Alejandro González <me@alegon.dev>
This commit is contained in:
43
packages/muralpay/Cargo.toml
Normal file
43
packages/muralpay/Cargo.toml
Normal file
@@ -0,0 +1,43 @@
|
||||
[package]
|
||||
name = "muralpay"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
description = "Mural Pay API"
|
||||
repository = "https://github.com/modrinth/code/"
|
||||
license = "MIT"
|
||||
keywords = []
|
||||
categories = ["api-bindings"]
|
||||
|
||||
[dependencies]
|
||||
bytes = { workspace = true }
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
derive_more = { workspace = true, features = [
|
||||
"deref",
|
||||
"display",
|
||||
"error",
|
||||
"from",
|
||||
] }
|
||||
reqwest = { workspace = true, features = ["default-tls", "http2", "json"] }
|
||||
rust_decimal = { workspace = true, features = ["macros"] }
|
||||
rust_iso3166 = { workspace = true }
|
||||
secrecy = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_with = { workspace = true }
|
||||
strum = { workspace = true, features = ["derive"] }
|
||||
utoipa = { workspace = true, features = ["uuid"], optional = true }
|
||||
uuid = { workspace = true, features = ["serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
color-eyre = { workspace = true }
|
||||
dotenvy = { workspace = true }
|
||||
eyre = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tracing-subscriber = { workspace = true }
|
||||
|
||||
[features]
|
||||
utoipa = ["dep:utoipa"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
5
packages/muralpay/README.md
Normal file
5
packages/muralpay/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
Rust API bindings for the [Mural Pay API](https://developers.muralpay.com/docs/getting-started).
|
||||
|
||||
# Useful links
|
||||
|
||||
- [Mural Pay API Reference](https://developers.muralpay.com/reference/)
|
||||
321
packages/muralpay/examples/muralpay.rs
Normal file
321
packages/muralpay/examples/muralpay.rs
Normal file
@@ -0,0 +1,321 @@
|
||||
use std::{env, fmt::Debug, io};
|
||||
|
||||
use eyre::{Result, WrapErr, eyre};
|
||||
use muralpay::{
|
||||
AccountId, CounterpartyId, CreatePayout, CreatePayoutDetails, Dob,
|
||||
FiatAccountType, FiatAndRailCode, FiatAndRailDetails, FiatFeeRequest,
|
||||
FiatPayoutFee, MuralPay, PayoutMethodId, PayoutRecipientInfo,
|
||||
PhysicalAddress, TokenAmount, TokenFeeRequest, TokenPayoutFee, UsdSymbol,
|
||||
};
|
||||
use rust_decimal::{Decimal, dec};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, clap::Parser)]
|
||||
struct Args {
|
||||
#[arg(short, long)]
|
||||
output: Option<OutputFormat>,
|
||||
#[clap(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
enum Command {
|
||||
/// Account listing and management
|
||||
Account {
|
||||
#[command(subcommand)]
|
||||
command: AccountCommand,
|
||||
},
|
||||
/// Payouts and payout requests
|
||||
Payout {
|
||||
#[command(subcommand)]
|
||||
command: PayoutCommand,
|
||||
},
|
||||
/// Counterparty management
|
||||
Counterparty {
|
||||
#[command(subcommand)]
|
||||
command: CounterpartyCommand,
|
||||
},
|
||||
/// Payout method management
|
||||
PayoutMethod {
|
||||
#[command(subcommand)]
|
||||
command: PayoutMethodCommand,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
enum AccountCommand {
|
||||
/// List all accounts
|
||||
#[clap(alias = "ls")]
|
||||
List,
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
enum PayoutCommand {
|
||||
/// List all payout requests
|
||||
#[clap(alias = "ls")]
|
||||
List,
|
||||
/// Create a payout request
|
||||
Create {
|
||||
/// ID of the Mural account to send from
|
||||
source_account_id: AccountId,
|
||||
/// Description for this payout request
|
||||
memo: Option<String>,
|
||||
},
|
||||
/// Get fees for a transaction
|
||||
Fees {
|
||||
#[command(subcommand)]
|
||||
command: PayoutFeesCommand,
|
||||
},
|
||||
/// Get bank details for a fiat and rail code
|
||||
BankDetails {
|
||||
/// Fiat and rail code to fetch bank details for
|
||||
fiat_and_rail_code: FiatAndRailCode,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
enum PayoutFeesCommand {
|
||||
/// Get fees for a token-to-fiat transaction
|
||||
Token {
|
||||
amount: Decimal,
|
||||
fiat_and_rail_code: FiatAndRailCode,
|
||||
},
|
||||
/// Get fees for a fiat-to-token transaction
|
||||
Fiat {
|
||||
amount: Decimal,
|
||||
fiat_and_rail_code: FiatAndRailCode,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
enum CounterpartyCommand {
|
||||
/// List all counterparties
|
||||
#[clap(alias = "ls")]
|
||||
List,
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
enum PayoutMethodCommand {
|
||||
/// List payout methods for a counterparty
|
||||
#[clap(alias = "ls")]
|
||||
List {
|
||||
/// ID of the counterparty
|
||||
counterparty_id: CounterpartyId,
|
||||
},
|
||||
/// Delete a payout method
|
||||
Delete {
|
||||
/// ID of the counterparty
|
||||
counterparty_id: CounterpartyId,
|
||||
/// ID of the payout method to delete
|
||||
payout_method_id: PayoutMethodId,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
|
||||
enum OutputFormat {
|
||||
Json,
|
||||
JsonMin,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
_ = dotenvy::dotenv();
|
||||
color_eyre::install().expect("failed to install `color-eyre`");
|
||||
tracing_subscriber::fmt().init();
|
||||
|
||||
let args = <Args as clap::Parser>::parse();
|
||||
let of = args.output;
|
||||
|
||||
let api_url = env::var("MURALPAY_API_URL")
|
||||
.unwrap_or_else(|_| muralpay::SANDBOX_API_URL.to_string());
|
||||
let api_key = env::var("MURALPAY_API_KEY").wrap_err("no API key")?;
|
||||
let transfer_api_key = env::var("MURALPAY_TRANSFER_API_KEY").ok();
|
||||
|
||||
let muralpay = MuralPay::new(api_url, api_key, transfer_api_key);
|
||||
|
||||
match args.command {
|
||||
Command::Account {
|
||||
command: AccountCommand::List,
|
||||
} => run(of, muralpay.get_all_accounts().await?),
|
||||
Command::Payout {
|
||||
command: PayoutCommand::List,
|
||||
} => run(of, muralpay.search_payout_requests(None, None).await?),
|
||||
Command::Payout {
|
||||
command:
|
||||
PayoutCommand::Create {
|
||||
source_account_id,
|
||||
memo,
|
||||
},
|
||||
} => run(
|
||||
of,
|
||||
create_payout_request(
|
||||
&muralpay,
|
||||
source_account_id,
|
||||
memo.as_deref(),
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
Command::Payout {
|
||||
command:
|
||||
PayoutCommand::Fees {
|
||||
command:
|
||||
PayoutFeesCommand::Token {
|
||||
amount,
|
||||
fiat_and_rail_code,
|
||||
},
|
||||
},
|
||||
} => run(
|
||||
of,
|
||||
get_fees_for_token_amount(&muralpay, amount, fiat_and_rail_code)
|
||||
.await?,
|
||||
),
|
||||
Command::Payout {
|
||||
command:
|
||||
PayoutCommand::Fees {
|
||||
command:
|
||||
PayoutFeesCommand::Fiat {
|
||||
amount,
|
||||
fiat_and_rail_code,
|
||||
},
|
||||
},
|
||||
} => run(
|
||||
of,
|
||||
get_fees_for_fiat_amount(&muralpay, amount, fiat_and_rail_code)
|
||||
.await?,
|
||||
),
|
||||
Command::Payout {
|
||||
command: PayoutCommand::BankDetails { fiat_and_rail_code },
|
||||
} => run(of, muralpay.get_bank_details(&[fiat_and_rail_code]).await?),
|
||||
Command::Counterparty {
|
||||
command: CounterpartyCommand::List,
|
||||
} => run(of, list_counterparties(&muralpay).await?),
|
||||
Command::PayoutMethod {
|
||||
command: PayoutMethodCommand::List { counterparty_id },
|
||||
} => run(
|
||||
of,
|
||||
muralpay
|
||||
.search_payout_methods(counterparty_id, None)
|
||||
.await?,
|
||||
),
|
||||
Command::PayoutMethod {
|
||||
command:
|
||||
PayoutMethodCommand::Delete {
|
||||
counterparty_id,
|
||||
payout_method_id,
|
||||
},
|
||||
} => run(
|
||||
of,
|
||||
muralpay
|
||||
.delete_payout_method(counterparty_id, payout_method_id)
|
||||
.await?,
|
||||
),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_payout_request(
|
||||
muralpay: &MuralPay,
|
||||
source_account_id: AccountId,
|
||||
memo: Option<&str>,
|
||||
) -> Result<()> {
|
||||
muralpay
|
||||
.create_payout_request(
|
||||
source_account_id,
|
||||
memo,
|
||||
&[CreatePayout {
|
||||
amount: TokenAmount {
|
||||
token_amount: dec!(2.00),
|
||||
token_symbol: muralpay::USDC.into(),
|
||||
},
|
||||
payout_details: CreatePayoutDetails::Fiat {
|
||||
bank_name: "Foo Bank".into(),
|
||||
bank_account_owner: "John Smith".into(),
|
||||
developer_fee: None,
|
||||
fiat_and_rail_details: FiatAndRailDetails::Usd {
|
||||
symbol: UsdSymbol::Usd,
|
||||
account_type: FiatAccountType::Checking,
|
||||
bank_account_number: "123456789".into(),
|
||||
// idk what the format is, https://wise.com/us/routing-number/bank/us-bank
|
||||
bank_routing_number: "071004200".into(),
|
||||
},
|
||||
},
|
||||
recipient_info: PayoutRecipientInfo::Individual {
|
||||
first_name: "John".into(),
|
||||
last_name: "Smith".into(),
|
||||
email: "john.smith@example.com".into(),
|
||||
date_of_birth: Dob::new(1970, 1, 1).unwrap(),
|
||||
physical_address: PhysicalAddress {
|
||||
address1: "1234 Elm Street".into(),
|
||||
address2: Some("Apt 56B".into()),
|
||||
country: rust_iso3166::US,
|
||||
state: "CA".into(),
|
||||
city: "Springfield".into(),
|
||||
zip: "90001".into(),
|
||||
},
|
||||
},
|
||||
supporting_details: None,
|
||||
}],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_fees_for_token_amount(
|
||||
muralpay: &MuralPay,
|
||||
amount: Decimal,
|
||||
fiat_and_rail_code: FiatAndRailCode,
|
||||
) -> Result<TokenPayoutFee> {
|
||||
let fees = muralpay
|
||||
.get_fees_for_token_amount(&[TokenFeeRequest {
|
||||
amount: TokenAmount {
|
||||
token_amount: amount,
|
||||
token_symbol: muralpay::USDC.into(),
|
||||
},
|
||||
fiat_and_rail_code,
|
||||
}])
|
||||
.await?;
|
||||
let fee = fees
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| eyre!("no fee results returned"))?;
|
||||
Ok(fee)
|
||||
}
|
||||
|
||||
async fn get_fees_for_fiat_amount(
|
||||
muralpay: &MuralPay,
|
||||
amount: Decimal,
|
||||
fiat_and_rail_code: FiatAndRailCode,
|
||||
) -> Result<FiatPayoutFee> {
|
||||
let fees = muralpay
|
||||
.get_fees_for_fiat_amount(&[FiatFeeRequest {
|
||||
fiat_amount: amount,
|
||||
token_symbol: muralpay::USDC.into(),
|
||||
fiat_and_rail_code,
|
||||
}])
|
||||
.await?;
|
||||
let fee = fees
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| eyre!("no fee results returned"))?;
|
||||
Ok(fee)
|
||||
}
|
||||
|
||||
async fn list_counterparties(muralpay: &MuralPay) -> Result<()> {
|
||||
let _counterparties = muralpay.search_counterparties(None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run<T: Debug + Serialize>(output_format: Option<OutputFormat>, value: T) {
|
||||
match output_format {
|
||||
None => {
|
||||
println!("{value:#?}");
|
||||
}
|
||||
Some(OutputFormat::Json) => {
|
||||
_ = serde_json::to_writer_pretty(io::stdout(), &value)
|
||||
}
|
||||
Some(OutputFormat::JsonMin) => {
|
||||
_ = serde_json::to_writer(io::stdout(), &value);
|
||||
}
|
||||
}
|
||||
}
|
||||
236
packages/muralpay/src/account.rs
Normal file
236
packages/muralpay/src/account.rs
Normal file
@@ -0,0 +1,236 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use derive_more::{Deref, Display};
|
||||
use rust_decimal::Decimal;
|
||||
use secrecy::ExposeSecret;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
Blockchain, FiatAmount, MuralError, MuralPay, TokenAmount, WalletDetails,
|
||||
util::RequestExt,
|
||||
};
|
||||
|
||||
impl MuralPay {
|
||||
pub async fn get_all_accounts(&self) -> Result<Vec<Account>, MuralError> {
|
||||
self.http_get(|base| format!("{base}/api/accounts"))
|
||||
.send_mural()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_account(
|
||||
&self,
|
||||
id: AccountId,
|
||||
) -> Result<Account, MuralError> {
|
||||
self.http_get(|base| format!("{base}/api/accounts/{id}"))
|
||||
.send_mural()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_account(
|
||||
&self,
|
||||
name: impl AsRef<str>,
|
||||
description: Option<impl AsRef<str>>,
|
||||
) -> Result<Account, MuralError> {
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Body<'a> {
|
||||
name: &'a str,
|
||||
description: Option<&'a str>,
|
||||
}
|
||||
|
||||
let body = Body {
|
||||
name: name.as_ref(),
|
||||
description: description.as_ref().map(|x| x.as_ref()),
|
||||
};
|
||||
|
||||
self.http
|
||||
.post(format!("{}/api/accounts", self.api_url))
|
||||
.bearer_auth(self.api_key.expose_secret())
|
||||
.json(&body)
|
||||
.send_mural()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
Deref,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[display("{}", _0.hyphenated())]
|
||||
pub struct AccountId(pub Uuid);
|
||||
|
||||
impl FromStr for AccountId {
|
||||
type Err = <Uuid as FromStr>::Err;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
s.parse::<Uuid>().map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Account {
|
||||
pub id: AccountId,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub is_api_enabled: bool,
|
||||
pub status: AccountStatus,
|
||||
pub account_details: Option<AccountDetails>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum AccountStatus {
|
||||
Initializing,
|
||||
Active,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AccountDetails {
|
||||
pub wallet_details: WalletDetails,
|
||||
pub balances: Vec<TokenAmount>,
|
||||
pub payin_methods: Vec<PayinMethod>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PayinMethod {
|
||||
pub status: PayinMethodStatus,
|
||||
pub supported_destination_tokens: Vec<DestinationToken>,
|
||||
pub payin_rail_details: PayinRailDetails,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum PayinMethodStatus {
|
||||
Activated,
|
||||
Deactivated,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DestinationToken {
|
||||
pub fees: Fees,
|
||||
pub token: Token,
|
||||
pub transaction_minimum: Option<FiatAmount>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Fees {
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
pub variable_fee_percentage: Decimal,
|
||||
pub fixed_transaction_fee: Option<FiatAmount>,
|
||||
#[serde(with = "rust_decimal::serde::float_option", default)]
|
||||
pub developer_fee_percentage: Option<Decimal>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Token {
|
||||
pub symbol: String,
|
||||
pub blockchain: Blockchain,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum PayinRailDetails {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Usd {
|
||||
currency: UsdCurrency,
|
||||
payin_rails: Vec<String>,
|
||||
bank_beneficiary_name: String,
|
||||
bank_beneficiary_address: String,
|
||||
bank_name: String,
|
||||
bank_address: String,
|
||||
bank_routing_number: String,
|
||||
bank_account_number: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Eur {
|
||||
currency: EurCurrency,
|
||||
payin_rail: EurPayinRail,
|
||||
bank_name: String,
|
||||
bank_address: String,
|
||||
account_holder_name: String,
|
||||
iban: String,
|
||||
bic: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Cop {
|
||||
currency: CopCurrency,
|
||||
payin_rail: CopPayinRail,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
BlockchainDeposit {
|
||||
deposit_token: DepositToken,
|
||||
sender_address: Option<String>,
|
||||
destination_address: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum UsdCurrency {
|
||||
Usd,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum EurCurrency {
|
||||
Eur,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum EurPayinRail {
|
||||
Sepa,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum CopCurrency {
|
||||
Cop,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum CopPayinRail {
|
||||
Pse,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum DepositToken {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
UsdtTron { contract_address: String },
|
||||
}
|
||||
169
packages/muralpay/src/counterparty.rs
Normal file
169
packages/muralpay/src/counterparty.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use derive_more::{Deref, Display};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
MuralError, MuralPay, PhysicalAddress, SearchParams, SearchResponse,
|
||||
util::RequestExt,
|
||||
};
|
||||
|
||||
impl MuralPay {
|
||||
pub async fn search_counterparties(
|
||||
&self,
|
||||
params: Option<SearchParams<CounterpartyId>>,
|
||||
) -> Result<SearchResponse<CounterpartyId, Counterparty>, MuralError> {
|
||||
self.http_post(|base| format!("{base}/api/counterparties/search"))
|
||||
.query(¶ms.map(|p| p.to_query()).unwrap_or_default())
|
||||
.send_mural()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_counterparty(
|
||||
&self,
|
||||
id: CounterpartyId,
|
||||
) -> Result<Counterparty, MuralError> {
|
||||
self.http_get(|base| {
|
||||
format!("{base}/api/counterparties/counterparty/{id}")
|
||||
})
|
||||
.send_mural()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_counterparty(
|
||||
&self,
|
||||
counterparty: &CreateCounterparty,
|
||||
) -> Result<Counterparty, MuralError> {
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Body<'a> {
|
||||
counterparty: &'a CreateCounterparty,
|
||||
}
|
||||
|
||||
let body = Body { counterparty };
|
||||
|
||||
self.http_post(|base| format!("{base}/api/counterparties"))
|
||||
.json(&body)
|
||||
.send_mural()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update_counterparty(
|
||||
&self,
|
||||
id: CounterpartyId,
|
||||
counterparty: &UpdateCounterparty,
|
||||
) -> Result<Counterparty, MuralError> {
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Body<'a> {
|
||||
counterparty: &'a UpdateCounterparty,
|
||||
}
|
||||
|
||||
let body = Body { counterparty };
|
||||
|
||||
self.http_put(|base| {
|
||||
format!("{base}/api/counterparties/counterparty/{id}")
|
||||
})
|
||||
.json(&body)
|
||||
.send_mural()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
Deref,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[display("{}", _0.hyphenated())]
|
||||
pub struct CounterpartyId(pub Uuid);
|
||||
|
||||
impl FromStr for CounterpartyId {
|
||||
type Err = <Uuid as FromStr>::Err;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
s.parse::<Uuid>().map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Counterparty {
|
||||
pub id: CounterpartyId,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub alias: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub kind: CounterpartyKind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum CounterpartyKind {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Individual {
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
email: String,
|
||||
physical_address: PhysicalAddress,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Business {
|
||||
name: String,
|
||||
email: String,
|
||||
physical_address: PhysicalAddress,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum CreateCounterparty {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Individual {
|
||||
alias: Option<String>,
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
email: String,
|
||||
physical_address: PhysicalAddress,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Business {
|
||||
alias: Option<String>,
|
||||
name: String,
|
||||
email: String,
|
||||
physical_address: PhysicalAddress,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum UpdateCounterparty {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Individual {
|
||||
alias: Option<String>,
|
||||
first_name: Option<String>,
|
||||
last_name: Option<String>,
|
||||
email: Option<String>,
|
||||
physical_address: Option<PhysicalAddress>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Business {
|
||||
alias: Option<String>,
|
||||
name: Option<String>,
|
||||
email: Option<String>,
|
||||
physical_address: Option<PhysicalAddress>,
|
||||
},
|
||||
}
|
||||
117
packages/muralpay/src/error.rs
Normal file
117
packages/muralpay/src/error.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use std::{collections::HashMap, fmt};
|
||||
|
||||
use bytes::Bytes;
|
||||
use derive_more::{Display, Error, From};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Display, Error, From)]
|
||||
pub enum MuralError {
|
||||
#[display("API error")]
|
||||
Api(ApiError),
|
||||
#[display("request error")]
|
||||
Request(reqwest::Error),
|
||||
#[display("failed to decode response\n{json:?}")]
|
||||
#[from(skip)]
|
||||
Decode {
|
||||
source: serde_json::Error,
|
||||
json: Bytes,
|
||||
},
|
||||
#[display("failed to decode error response\n{json:?}")]
|
||||
#[from(skip)]
|
||||
DecodeError {
|
||||
source: serde_json::Error,
|
||||
json: Bytes,
|
||||
},
|
||||
}
|
||||
|
||||
pub type Result<T, E = MuralError> = std::result::Result<T, E>;
|
||||
|
||||
#[derive(Debug, Display, Error, From)]
|
||||
pub enum TransferError {
|
||||
#[display("no transfer API key")]
|
||||
NoTransferKey,
|
||||
#[display("API error")]
|
||||
Api(Box<ApiError>),
|
||||
#[display("request error")]
|
||||
Request(reqwest::Error),
|
||||
#[display("failed to decode response\n{json:?}")]
|
||||
#[from(skip)]
|
||||
Decode {
|
||||
source: serde_json::Error,
|
||||
json: Bytes,
|
||||
},
|
||||
#[display("failed to decode error response\n{json:?}")]
|
||||
#[from(skip)]
|
||||
DecodeError {
|
||||
source: serde_json::Error,
|
||||
json: Bytes,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<MuralError> for TransferError {
|
||||
fn from(value: MuralError) -> Self {
|
||||
match value {
|
||||
MuralError::Api(x) => Self::Api(Box::new(x)),
|
||||
MuralError::Request(x) => Self::Request(x),
|
||||
MuralError::Decode { source, json } => {
|
||||
Self::Decode { source, json }
|
||||
}
|
||||
MuralError::DecodeError { source, json } => {
|
||||
Self::DecodeError { source, json }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Error)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApiError {
|
||||
pub error_instance_id: Uuid,
|
||||
pub name: String,
|
||||
pub message: String,
|
||||
#[serde(deserialize_with = "one_or_many")]
|
||||
#[serde(default)]
|
||||
pub details: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub params: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
fn one_or_many<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum OneOrMany {
|
||||
One(String),
|
||||
Many(Vec<String>),
|
||||
}
|
||||
|
||||
match OneOrMany::deserialize(deserializer)? {
|
||||
OneOrMany::One(s) => Ok(vec![s]),
|
||||
OneOrMany::Many(v) => Ok(v),
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ApiError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut lines = vec![self.message.to_string()];
|
||||
|
||||
if !self.details.is_empty() {
|
||||
lines.push("details:".into());
|
||||
lines.extend(self.details.iter().map(|s| format!("- {s}")));
|
||||
}
|
||||
|
||||
if !self.params.is_empty() {
|
||||
lines.push("params:".into());
|
||||
lines
|
||||
.extend(self.params.iter().map(|(k, v)| format!("- {k}: {v}")));
|
||||
}
|
||||
|
||||
lines.push(format!("error name: {}", self.name));
|
||||
lines.push(format!("error instance id: {}", self.error_instance_id));
|
||||
|
||||
write!(f, "{}", lines.join("\n"))
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,31 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
mod account;
|
||||
mod counterparty;
|
||||
mod error;
|
||||
mod organization;
|
||||
mod payout;
|
||||
mod payout_method;
|
||||
mod serde_iso3166;
|
||||
mod util;
|
||||
|
||||
pub use {account::*, error::*, organization::*, payout::*};
|
||||
pub use {
|
||||
account::*, counterparty::*, error::*, organization::*, payout::*,
|
||||
payout_method::*,
|
||||
};
|
||||
|
||||
use rust_decimal::Decimal;
|
||||
use secrecy::SecretString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Deref;
|
||||
use std::{ops::Deref, str::FromStr};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub const API_URL: &str = "https://api.muralpay.com";
|
||||
pub const SANDBOX_API_URL: &str = "https://api-staging.muralpay.com";
|
||||
|
||||
/// Default token symbol for [`TokenAmount::token_symbol`] values.
|
||||
pub const USDC: &str = "USDC";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MuralPay {
|
||||
pub http: reqwest::Client,
|
||||
@@ -41,6 +50,7 @@ impl MuralPay {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum Blockchain {
|
||||
Ethereum,
|
||||
@@ -49,7 +59,10 @@ pub enum Blockchain {
|
||||
Celo,
|
||||
}
|
||||
|
||||
crate::util::display_as_serialize!(Blockchain);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum CurrencyCode {
|
||||
Usd,
|
||||
@@ -65,7 +78,20 @@ pub enum CurrencyCode {
|
||||
Zar,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
crate::util::display_as_serialize!(CurrencyCode);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum FiatAccountType {
|
||||
Checking,
|
||||
Savings,
|
||||
}
|
||||
|
||||
crate::util::display_as_serialize!(FiatAccountType);
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, strum::EnumIter)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum FiatAndRailCode {
|
||||
Usd,
|
||||
@@ -84,7 +110,18 @@ pub enum FiatAndRailCode {
|
||||
UsdPanama,
|
||||
}
|
||||
|
||||
crate::util::display_as_serialize!(FiatAndRailCode);
|
||||
|
||||
impl FromStr for FiatAndRailCode {
|
||||
type Err = serde_json::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
serde_json::from_value(serde_json::Value::String(s.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WalletDetails {
|
||||
pub blockchain: Blockchain,
|
||||
@@ -92,15 +129,19 @@ pub struct WalletDetails {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TokenAmount {
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
pub token_amount: Decimal,
|
||||
pub token_symbol: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FiatAmount {
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
pub fiat_amount: Decimal,
|
||||
pub fiat_currency_code: CurrencyCode,
|
||||
}
|
||||
@@ -126,6 +167,7 @@ impl<Id: Deref<Target = Uuid> + Clone> SearchParams<Id> {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SearchResponse<Id, T> {
|
||||
pub total: u64,
|
||||
|
||||
277
packages/muralpay/src/organization.rs
Normal file
277
packages/muralpay/src/organization.rs
Normal file
@@ -0,0 +1,277 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use derive_more::{Deref, Display};
|
||||
use secrecy::ExposeSecret;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
CurrencyCode, MuralError, MuralPay, SearchResponse, util::RequestExt,
|
||||
};
|
||||
|
||||
impl MuralPay {
|
||||
pub async fn search_organizations(
|
||||
&self,
|
||||
req: SearchRequest,
|
||||
) -> Result<SearchResponse<OrganizationId, Organization>, MuralError> {
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Body {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
filter: Option<Filter>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Filter {
|
||||
#[serde(rename = "type")]
|
||||
ty: FilterType,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FilterType {
|
||||
Name,
|
||||
}
|
||||
|
||||
let query = [
|
||||
req.limit.map(|limit| ("limit", limit.to_string())),
|
||||
req.next_id
|
||||
.map(|next_id| ("nextId", next_id.hyphenated().to_string())),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let body = Body {
|
||||
filter: req.name.map(|name| Filter {
|
||||
ty: FilterType::Name,
|
||||
name,
|
||||
}),
|
||||
};
|
||||
|
||||
self.http_post(|base| format!("{base}/api/organizations/search"))
|
||||
.bearer_auth(self.api_key.expose_secret())
|
||||
.query(&query)
|
||||
.json(&body)
|
||||
.send_mural()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_organization(
|
||||
&self,
|
||||
id: OrganizationId,
|
||||
) -> Result<Organization, MuralError> {
|
||||
self.http_post(|base| format!("{base}/api/organizations/{id}"))
|
||||
.send_mural()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
Deref,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[display("{}", _0.hyphenated())]
|
||||
pub struct OrganizationId(pub Uuid);
|
||||
|
||||
impl FromStr for OrganizationId {
|
||||
type Err = <Uuid as FromStr>::Err;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
s.parse::<Uuid>().map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
pub struct SearchRequest {
|
||||
pub limit: Option<u64>,
|
||||
pub next_id: Option<Uuid>,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum Organization {
|
||||
Individual(Individual),
|
||||
Business(Business),
|
||||
EndUserCustodialIndividual(EndUserCustodialIndividual),
|
||||
EndUserCustodialBusiness(EndUserCustodialBusiness),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Individual {
|
||||
pub id: OrganizationId,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub tos_status: TosStatus,
|
||||
pub kyc_status: KycStatus,
|
||||
pub currency_capabilities: Vec<CurrencyCapability>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Business {
|
||||
pub id: OrganizationId,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub name: String,
|
||||
pub tos_status: TosStatus,
|
||||
pub kyc_status: KycStatus,
|
||||
pub currency_capabilities: Vec<CurrencyCapability>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EndUserCustodialIndividual {
|
||||
pub id: OrganizationId,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub approver: Approver,
|
||||
pub tos_status: TosStatus,
|
||||
pub kyc_status: KycStatus,
|
||||
pub currency_capabilities: Vec<CurrencyCapability>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EndUserCustodialBusiness {
|
||||
pub id: OrganizationId,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub name: String,
|
||||
pub approver: Approver,
|
||||
pub tos_status: TosStatus,
|
||||
pub kyc_status: KycStatus,
|
||||
pub currency_capabilities: Vec<CurrencyCapability>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Approver {
|
||||
pub id: Uuid,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
pub auth_methods: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum TosStatus {
|
||||
NotAccepted,
|
||||
NeedsReview,
|
||||
Accepted,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum KycStatus {
|
||||
Inactive,
|
||||
Pending,
|
||||
Approved {
|
||||
approved_at: DateTime<Utc>,
|
||||
},
|
||||
Errored {
|
||||
details: String,
|
||||
errored_at: DateTime<Utc>,
|
||||
},
|
||||
Rejected {
|
||||
reason: String,
|
||||
rejected_at: DateTime<Utc>,
|
||||
},
|
||||
PreValidationFailed {
|
||||
failed_validation_reason: FailedValidationReason,
|
||||
failed_validation_at: DateTime<Utc>,
|
||||
},
|
||||
NeedsUpdate {
|
||||
needs_update_reason: String,
|
||||
verification_status_updated_at: DateTime<Utc>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum FailedValidationReason {
|
||||
DocumentPrevalidationFailed {
|
||||
document_id: String,
|
||||
failed_validation_reason: String,
|
||||
},
|
||||
UltimateBeneficialOwnerPrevalidationFailed {
|
||||
ultimate_beneficial_owner_id: String,
|
||||
failed_validation_reason: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CurrencyCapability {
|
||||
pub fiat_and_rail_code: String,
|
||||
pub currency_code: CurrencyCode,
|
||||
pub deposit_status: TransactionCapabilityStatus,
|
||||
pub pay_out_status: TransactionCapabilityStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum TransactionCapabilityStatus {
|
||||
TermsOfService {
|
||||
details: String,
|
||||
},
|
||||
#[serde(rename = "awaitingKYC")]
|
||||
AwaitingKyc {
|
||||
details: String,
|
||||
},
|
||||
Enabled,
|
||||
Rejected {
|
||||
reason: RejectedReason,
|
||||
details: String,
|
||||
},
|
||||
Disabled {
|
||||
reason: DisabledReason,
|
||||
details: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum RejectedReason {
|
||||
KycFailed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum DisabledReason {
|
||||
CapabilityUnavailable,
|
||||
ProcessingError,
|
||||
}
|
||||
825
packages/muralpay/src/payout.rs
Normal file
825
packages/muralpay/src/payout.rs
Normal file
@@ -0,0 +1,825 @@
|
||||
#![cfg_attr(
|
||||
feature = "utoipa",
|
||||
expect(
|
||||
clippy::large_stack_arrays,
|
||||
reason = "due to `utoipa::ToSchema` derive"
|
||||
)
|
||||
)]
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use derive_more::{Deref, Display, Error, From};
|
||||
use rust_decimal::Decimal;
|
||||
use rust_iso3166::CountryCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
AccountId, Blockchain, FiatAccountType, FiatAmount, FiatAndRailCode,
|
||||
MuralError, MuralPay, SearchParams, SearchResponse, TokenAmount,
|
||||
TransferError, WalletDetails, util::RequestExt,
|
||||
};
|
||||
|
||||
impl MuralPay {
|
||||
pub async fn search_payout_requests(
|
||||
&self,
|
||||
filter: Option<PayoutStatusFilter>,
|
||||
params: Option<SearchParams<PayoutRequestId>>,
|
||||
) -> Result<SearchResponse<PayoutRequestId, PayoutRequest>, MuralError>
|
||||
{
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Body {
|
||||
filter: Option<PayoutStatusFilter>,
|
||||
}
|
||||
|
||||
let body = Body { filter };
|
||||
|
||||
self.http_post(|base| format!("{base}/api/payouts/search"))
|
||||
.query(¶ms.map(|p| p.to_query()).unwrap_or_default())
|
||||
.json(&body)
|
||||
.send_mural()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_payout_request(
|
||||
&self,
|
||||
id: PayoutRequestId,
|
||||
) -> Result<PayoutRequest, MuralError> {
|
||||
self.http_get(|base| format!("{base}/api/payouts/{id}"))
|
||||
.send_mural()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_fees_for_token_amount(
|
||||
&self,
|
||||
token_fee_requests: &[TokenFeeRequest],
|
||||
) -> Result<Vec<TokenPayoutFee>, MuralError> {
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Body<'a> {
|
||||
token_fee_requests: &'a [TokenFeeRequest],
|
||||
}
|
||||
|
||||
let body = Body { token_fee_requests };
|
||||
|
||||
self.http_post(|base| format!("{base}/api/payouts/fees/token-to-fiat"))
|
||||
.json(&body)
|
||||
.send_mural()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_fees_for_fiat_amount(
|
||||
&self,
|
||||
fiat_fee_requests: &[FiatFeeRequest],
|
||||
) -> Result<Vec<FiatPayoutFee>, MuralError> {
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Body<'a> {
|
||||
fiat_fee_requests: &'a [FiatFeeRequest],
|
||||
}
|
||||
|
||||
let body = Body { fiat_fee_requests };
|
||||
|
||||
self.http_post(|base| format!("{base}/api/payouts/fees/fiat-to-token"))
|
||||
.json(&body)
|
||||
.send_mural()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_payout_request(
|
||||
&self,
|
||||
source_account_id: AccountId,
|
||||
memo: Option<impl AsRef<str>>,
|
||||
payouts: &[CreatePayout],
|
||||
) -> Result<PayoutRequest, MuralError> {
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Body<'a> {
|
||||
source_account_id: AccountId,
|
||||
memo: Option<&'a str>,
|
||||
payouts: &'a [CreatePayout],
|
||||
}
|
||||
|
||||
let body = Body {
|
||||
source_account_id,
|
||||
memo: memo.as_ref().map(|x| x.as_ref()),
|
||||
payouts,
|
||||
};
|
||||
|
||||
self.http_post(|base| format!("{base}/api/payouts/payout"))
|
||||
.json(&body)
|
||||
.send_mural()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn execute_payout_request(
|
||||
&self,
|
||||
id: PayoutRequestId,
|
||||
) -> Result<PayoutRequest, TransferError> {
|
||||
self.http_post(|base| format!("{base}/api/payouts/payout/{id}/execute"))
|
||||
.transfer_auth(self)?
|
||||
.send_mural()
|
||||
.await
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
pub async fn cancel_payout_request(
|
||||
&self,
|
||||
id: PayoutRequestId,
|
||||
) -> Result<PayoutRequest, TransferError> {
|
||||
self.http_post(|base| format!("{base}/api/payouts/payout/{id}/cancel"))
|
||||
.transfer_auth(self)?
|
||||
.send_mural()
|
||||
.await
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
pub async fn get_bank_details(
|
||||
&self,
|
||||
fiat_currency_and_rail: &[FiatAndRailCode],
|
||||
) -> Result<BankDetailsResponse, MuralError> {
|
||||
let query = fiat_currency_and_rail
|
||||
.iter()
|
||||
.map(|code| ("fiatCurrencyAndRail", code.to_string()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.http_get(|base| format!("{base}/api/payouts/bank-details"))
|
||||
.query(&query)
|
||||
.send_mural()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
Deref,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[display("{}", _0.hyphenated())]
|
||||
pub struct PayoutRequestId(pub Uuid);
|
||||
|
||||
impl FromStr for PayoutRequestId {
|
||||
type Err = <Uuid as FromStr>::Err;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
s.parse::<Uuid>().map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
Deref,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[display("{}", _0.hyphenated())]
|
||||
pub struct PayoutId(pub Uuid);
|
||||
|
||||
impl FromStr for PayoutId {
|
||||
type Err = <Uuid as FromStr>::Err;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
s.parse::<Uuid>().map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum PayoutStatusFilter {
|
||||
PayoutStatus { statuses: Vec<String> },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PayoutRequest {
|
||||
pub id: PayoutRequestId,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub source_account_id: AccountId,
|
||||
pub transaction_hash: Option<String>,
|
||||
pub memo: Option<String>,
|
||||
pub status: PayoutStatus,
|
||||
pub payouts: Vec<Payout>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum PayoutStatus {
|
||||
AwaitingExecution,
|
||||
Canceled,
|
||||
Pending,
|
||||
Executed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Payout {
|
||||
pub id: PayoutId,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub amount: TokenAmount,
|
||||
pub details: PayoutDetails,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum PayoutDetails {
|
||||
Fiat(FiatPayoutDetails),
|
||||
Blockchain(BlockchainPayoutDetails),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FiatPayoutDetails {
|
||||
pub fiat_and_rail_code: FiatAndRailCode,
|
||||
pub fiat_payout_status: FiatPayoutStatus,
|
||||
pub fiat_amount: FiatAmount,
|
||||
pub transaction_fee: TokenAmount,
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
pub exchange_fee_percentage: Decimal,
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
pub exchange_rate: Decimal,
|
||||
pub fee_total: TokenAmount,
|
||||
pub developer_fee: Option<DeveloperFee>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
pub enum FiatPayoutStatus {
|
||||
Created,
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Pending {
|
||||
initiated_at: DateTime<Utc>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
OnHold {
|
||||
initiated_at: DateTime<Utc>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Completed {
|
||||
initiated_at: DateTime<Utc>,
|
||||
completed_at: DateTime<Utc>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Failed {
|
||||
initiated_at: DateTime<Utc>,
|
||||
reason: String,
|
||||
error_code: FiatPayoutErrorCode,
|
||||
},
|
||||
Canceled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum FiatPayoutErrorCode {
|
||||
Unknown,
|
||||
AccountNumberIncorrect,
|
||||
RejectedByBank,
|
||||
AccountTypeIncorrect,
|
||||
AccountClosed,
|
||||
BeneficiaryDocumentationIncorrect,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeveloperFee {
|
||||
#[serde(with = "rust_decimal::serde::float_option", default)]
|
||||
pub developer_fee_percentage: Option<Decimal>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BlockchainPayoutDetails {
|
||||
pub wallet_address: String,
|
||||
pub blockchain: Blockchain,
|
||||
pub status: BlockchainPayoutStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum BlockchainPayoutStatus {
|
||||
AwaitingExecution,
|
||||
Pending,
|
||||
Executed,
|
||||
Failed,
|
||||
Canceled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreatePayout {
|
||||
pub amount: TokenAmount,
|
||||
pub payout_details: CreatePayoutDetails,
|
||||
pub recipient_info: PayoutRecipientInfo,
|
||||
pub supporting_details: Option<SupportingDetails>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum CreatePayoutDetails {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Fiat {
|
||||
bank_name: String,
|
||||
bank_account_owner: String,
|
||||
developer_fee: Option<DeveloperFee>,
|
||||
fiat_and_rail_details: FiatAndRailDetails,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Blockchain { wallet_details: WalletDetails },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
pub enum FiatAndRailDetails {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Usd {
|
||||
symbol: UsdSymbol,
|
||||
account_type: FiatAccountType,
|
||||
bank_account_number: String,
|
||||
bank_routing_number: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Cop {
|
||||
symbol: CopSymbol,
|
||||
phone_number: String,
|
||||
account_type: FiatAccountType,
|
||||
bank_account_number: String,
|
||||
document_number: String,
|
||||
document_type: DocumentType,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Ars {
|
||||
symbol: ArsSymbol,
|
||||
bank_account_number: String,
|
||||
document_number: String,
|
||||
bank_account_number_type: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Eur {
|
||||
symbol: EurSymbol,
|
||||
iban: String,
|
||||
swift_bic: String,
|
||||
#[serde(with = "crate::serde_iso3166")]
|
||||
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
|
||||
country: CountryCode,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Mxn {
|
||||
symbol: MxnSymbol,
|
||||
bank_account_number: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Brl {
|
||||
symbol: BrlSymbol,
|
||||
pix_account_type: PixAccountType,
|
||||
pix_email: String,
|
||||
pix_phone: String,
|
||||
branch_code: String,
|
||||
document_number: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Clp {
|
||||
symbol: ClpSymbol,
|
||||
account_type: FiatAccountType,
|
||||
bank_account_number: String,
|
||||
document_type: DocumentType,
|
||||
document_number: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Pen {
|
||||
symbol: PenSymbol,
|
||||
document_number: String,
|
||||
document_type: DocumentType,
|
||||
bank_account_number: String,
|
||||
account_type: FiatAccountType,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Bob {
|
||||
symbol: BobSymbol,
|
||||
bank_account_number: String,
|
||||
document_number: String,
|
||||
document_type: DocumentType,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Crc {
|
||||
symbol: CrcSymbol,
|
||||
iban: String,
|
||||
document_number: String,
|
||||
document_type: DocumentType,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Zar {
|
||||
symbol: ZarSymbol,
|
||||
account_type: FiatAccountType,
|
||||
bank_account_number: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
UsdPeru {
|
||||
symbol: UsdSymbol,
|
||||
account_type: FiatAccountType,
|
||||
bank_account_number: String,
|
||||
document_number: String,
|
||||
document_type: DocumentType,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
UsdChina {
|
||||
symbol: UsdSymbol,
|
||||
bank_name: String,
|
||||
account_type: FiatAccountType,
|
||||
bank_account_number: String,
|
||||
document_number: String,
|
||||
document_type: DocumentType,
|
||||
phone_number: String,
|
||||
address: String,
|
||||
swift_bic: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl FiatAndRailDetails {
|
||||
pub fn code(&self) -> FiatAndRailCode {
|
||||
match self {
|
||||
Self::Usd { .. } => FiatAndRailCode::Usd,
|
||||
Self::Cop { .. } => FiatAndRailCode::Cop,
|
||||
Self::Ars { .. } => FiatAndRailCode::Ars,
|
||||
Self::Eur { .. } => FiatAndRailCode::Eur,
|
||||
Self::Mxn { .. } => FiatAndRailCode::Mxn,
|
||||
Self::Brl { .. } => FiatAndRailCode::Brl,
|
||||
Self::Clp { .. } => FiatAndRailCode::Clp,
|
||||
Self::Pen { .. } => FiatAndRailCode::Pen,
|
||||
Self::Bob { .. } => FiatAndRailCode::Bob,
|
||||
Self::Crc { .. } => FiatAndRailCode::Crc,
|
||||
Self::Zar { .. } => FiatAndRailCode::Zar,
|
||||
Self::UsdPeru { .. } => FiatAndRailCode::UsdPeru,
|
||||
Self::UsdChina { .. } => FiatAndRailCode::UsdChina,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum UsdSymbol {
|
||||
Usd,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum CopSymbol {
|
||||
Cop,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum ArsSymbol {
|
||||
Ars,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum EurSymbol {
|
||||
Eur,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum MxnSymbol {
|
||||
Mxn,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum BrlSymbol {
|
||||
Brl,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum ClpSymbol {
|
||||
Clp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum PenSymbol {
|
||||
Pen,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum BobSymbol {
|
||||
Bob,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum CrcSymbol {
|
||||
Crc,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum ZarSymbol {
|
||||
Zar,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum DocumentType {
|
||||
NationalId,
|
||||
Passport,
|
||||
ResidentId,
|
||||
Ruc,
|
||||
TaxId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum PixAccountType {
|
||||
Phone,
|
||||
Email,
|
||||
Document,
|
||||
BankAccount,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, From)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum PayoutRecipientInfo {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Individual {
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
email: String,
|
||||
date_of_birth: Dob,
|
||||
physical_address: PhysicalAddress,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Business {
|
||||
name: String,
|
||||
email: String,
|
||||
physical_address: PhysicalAddress,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, SerializeDisplay, DeserializeFromStr)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[display("{year:04}-{month:02}-{day:02}")]
|
||||
pub struct Dob {
|
||||
year: u16,
|
||||
month: u8,
|
||||
day: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Error)]
|
||||
pub enum InvalidDob {
|
||||
#[display("must be three segments separated by `-`")]
|
||||
NotThreeSegments,
|
||||
#[display("year is not an integer")]
|
||||
YearNotInt,
|
||||
#[display("month is not an integer")]
|
||||
MonthNotInt,
|
||||
#[display("day is not an integer")]
|
||||
DayNotInt,
|
||||
#[display("year out of range")]
|
||||
YearRange,
|
||||
#[display("month out of range")]
|
||||
MonthRange,
|
||||
#[display("day out of range")]
|
||||
DayRange,
|
||||
}
|
||||
|
||||
impl Dob {
|
||||
pub fn new(year: u16, month: u8, day: u8) -> Result<Self, InvalidDob> {
|
||||
if !(1000..10000).contains(&year) {
|
||||
return Err(InvalidDob::YearRange);
|
||||
}
|
||||
if month > 12 {
|
||||
return Err(InvalidDob::MonthRange);
|
||||
}
|
||||
if day > 31 {
|
||||
return Err(InvalidDob::DayRange);
|
||||
}
|
||||
Ok(Self { year, month, day })
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Dob {
|
||||
type Err = InvalidDob;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let [year, month, day] = s
|
||||
.split('-')
|
||||
.collect::<Vec<_>>()
|
||||
.try_into()
|
||||
.map_err(|_| InvalidDob::NotThreeSegments)?;
|
||||
let year = year.parse::<u16>().map_err(|_| InvalidDob::YearNotInt)?;
|
||||
let month = month.parse::<u8>().map_err(|_| InvalidDob::MonthNotInt)?;
|
||||
let day = day.parse::<u8>().map_err(|_| InvalidDob::DayNotInt)?;
|
||||
Self::new(year, month, day)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PhysicalAddress {
|
||||
pub address1: String,
|
||||
pub address2: Option<String>,
|
||||
#[serde(with = "crate::serde_iso3166")]
|
||||
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
|
||||
pub country: CountryCode,
|
||||
pub state: String,
|
||||
pub city: String,
|
||||
pub zip: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SupportingDetails {
|
||||
pub supporting_document: Option<String>, // data:image/jpeg;base64,...
|
||||
pub payout_purpose: Option<PayoutPurpose>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum PayoutPurpose {
|
||||
VendorPayment,
|
||||
Payroll,
|
||||
TaxPayment,
|
||||
RentLeasePayment,
|
||||
SupplierPayment,
|
||||
PersonalGift,
|
||||
FamilySupport,
|
||||
CharitableDonation,
|
||||
ExpenseReimbursement,
|
||||
BillUtilityPayment,
|
||||
TravelExpenses,
|
||||
InvestmentContribution,
|
||||
CashWithdrawal,
|
||||
RealEstatePurchase,
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TokenFeeRequest {
|
||||
pub amount: TokenAmount,
|
||||
pub fiat_and_rail_code: FiatAndRailCode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum TokenPayoutFee {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Success {
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
exchange_rate: Decimal,
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
exchange_fee_percentage: Decimal,
|
||||
fiat_and_rail_code: FiatAndRailCode,
|
||||
transaction_fee: TokenAmount,
|
||||
min_transaction_value: TokenAmount,
|
||||
estimated_fiat_amount: FiatAmount,
|
||||
token_amount: TokenAmount,
|
||||
fee_total: TokenAmount,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Error {
|
||||
token_amount: TokenAmount,
|
||||
message: String,
|
||||
fiat_and_rail_code: FiatAndRailCode,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FiatFeeRequest {
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
pub fiat_amount: Decimal,
|
||||
pub token_symbol: String,
|
||||
pub fiat_and_rail_code: FiatAndRailCode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum FiatPayoutFee {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Success {
|
||||
token_symbol: String,
|
||||
fiat_amount: FiatAmount,
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
exchange_rate: Decimal,
|
||||
#[serde(with = "rust_decimal::serde::float")]
|
||||
exchange_fee_percentage: Decimal,
|
||||
fiat_and_rail_code: FiatAndRailCode,
|
||||
transaction_fee: TokenAmount,
|
||||
min_transaction_value: TokenAmount,
|
||||
estimated_token_amount_required: TokenAmount,
|
||||
fee_total: TokenAmount,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Error {
|
||||
message: String,
|
||||
fiat_and_rail_code: FiatAndRailCode,
|
||||
token_symbol: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BankDetailsResponse {
|
||||
pub bank_details: CurrenciesBankDetails,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct CurrenciesBankDetails {
|
||||
#[serde(default)]
|
||||
pub usd: CurrencyBankDetails,
|
||||
#[serde(default)]
|
||||
pub cop: CurrencyBankDetails,
|
||||
#[serde(default)]
|
||||
pub ars: CurrencyBankDetails,
|
||||
#[serde(default)]
|
||||
pub eur: CurrencyBankDetails,
|
||||
#[serde(default)]
|
||||
pub mxn: CurrencyBankDetails,
|
||||
#[serde(default)]
|
||||
pub brl: CurrencyBankDetails,
|
||||
#[serde(default)]
|
||||
pub clp: CurrencyBankDetails,
|
||||
#[serde(default)]
|
||||
pub pen: CurrencyBankDetails,
|
||||
#[serde(default)]
|
||||
pub bob: CurrencyBankDetails,
|
||||
#[serde(default)]
|
||||
pub crc: CurrencyBankDetails,
|
||||
#[serde(default)]
|
||||
pub zar: CurrencyBankDetails,
|
||||
#[serde(default)]
|
||||
pub usd_peru: CurrencyBankDetails,
|
||||
#[serde(default)]
|
||||
pub usd_china: CurrencyBankDetails,
|
||||
#[serde(default)]
|
||||
pub usd_panama: CurrencyBankDetails,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CurrencyBankDetails {
|
||||
pub bank_names: Vec<String>,
|
||||
}
|
||||
439
packages/muralpay/src/payout_method.rs
Normal file
439
packages/muralpay/src/payout_method.rs
Normal file
@@ -0,0 +1,439 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use derive_more::{Deref, Display, Error};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::DeserializeFromStr;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
ArsSymbol, BobSymbol, BrlSymbol, ClpSymbol, CopSymbol, CounterpartyId,
|
||||
CrcSymbol, DocumentType, EurSymbol, FiatAccountType, MuralError, MuralPay,
|
||||
MxnSymbol, PenSymbol, SearchParams, SearchResponse, UsdSymbol,
|
||||
WalletDetails, ZarSymbol, util::RequestExt,
|
||||
};
|
||||
|
||||
impl MuralPay {
|
||||
pub async fn search_payout_methods(
|
||||
&self,
|
||||
counterparty_id: CounterpartyId,
|
||||
params: Option<SearchParams<PayoutMethodId>>,
|
||||
) -> Result<SearchResponse<PayoutMethodId, PayoutMethod>, MuralError> {
|
||||
self.http_post(|base| {
|
||||
format!(
|
||||
"{base}/api/counterparties/{counterparty_id}/payout-methods/search"
|
||||
)
|
||||
})
|
||||
.query(¶ms.map(|p| p.to_query()).unwrap_or_default())
|
||||
.send_mural()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_payout_method(
|
||||
&self,
|
||||
counterparty_id: CounterpartyId,
|
||||
payout_method_id: PayoutMethodId,
|
||||
) -> Result<PayoutMethod, MuralError> {
|
||||
self.http_get(|base| format!("{base}/api/counterparties/{counterparty_id}/payout-methods/{payout_method_id}"))
|
||||
.send_mural()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_payout_method(
|
||||
&self,
|
||||
counterparty_id: CounterpartyId,
|
||||
alias: impl AsRef<str>,
|
||||
payout_method: &PayoutMethodDetails,
|
||||
) -> Result<PayoutMethod, MuralError> {
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Body<'a> {
|
||||
alias: &'a str,
|
||||
payout_method: &'a PayoutMethodDetails,
|
||||
}
|
||||
|
||||
let body = Body {
|
||||
alias: alias.as_ref(),
|
||||
payout_method,
|
||||
};
|
||||
|
||||
self.http_post(|base| {
|
||||
format!(
|
||||
"{base}/api/counterparties/{counterparty_id}/payout-methods"
|
||||
)
|
||||
})
|
||||
.json(&body)
|
||||
.send_mural()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_payout_method(
|
||||
&self,
|
||||
counterparty_id: CounterpartyId,
|
||||
payout_method_id: PayoutMethodId,
|
||||
) -> Result<(), MuralError> {
|
||||
self.http_delete(|base| format!("{base}/api/counterparties/{counterparty_id}/payout-methods/{payout_method_id}"))
|
||||
.send_mural()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum PayoutMethodDocumentType {
|
||||
NationalId,
|
||||
Passport,
|
||||
ResidentId,
|
||||
Ruc,
|
||||
TaxId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum PayoutMethodPixAccountType {
|
||||
Phone,
|
||||
Email,
|
||||
Document,
|
||||
BankAccount,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
Deref,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[display("{}", _0.hyphenated())]
|
||||
pub struct PayoutMethodId(pub Uuid);
|
||||
|
||||
impl FromStr for PayoutMethodId {
|
||||
type Err = <Uuid as FromStr>::Err;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
s.parse::<Uuid>().map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, DeserializeFromStr)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
pub struct TruncatedString(String);
|
||||
|
||||
const TRUNCATED_LEN: usize = 4;
|
||||
|
||||
#[derive(Debug, Display, Error)]
|
||||
#[display("expected {TRUNCATED_LEN} characters, got {num_chars}")]
|
||||
pub struct InvalidTruncated {
|
||||
pub num_chars: usize,
|
||||
}
|
||||
|
||||
impl FromStr for TruncatedString {
|
||||
type Err = InvalidTruncated;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let num_chars = s.chars().count();
|
||||
if num_chars == TRUNCATED_LEN {
|
||||
Ok(Self(s.to_string()))
|
||||
} else {
|
||||
Err(InvalidTruncated { num_chars })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PayoutMethod {
|
||||
pub id: PayoutMethodId,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub counterparty_id: CounterpartyId,
|
||||
pub alias: String,
|
||||
pub payout_method: PayoutMethodDetails,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum PayoutMethodDetails {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Usd { details: UsdPayoutDetails },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Ars { details: ArsPayoutDetails },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Brl { details: BrlPayoutDetails },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Cop { details: CopPayoutDetails },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Eur { details: EurPayoutDetails },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Mxn { details: MxnPayoutDetails },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Clp { details: ClpPayoutDetails },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Pen { details: PenPayoutDetails },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Bob { details: BobPayoutDetails },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Crc { details: CrcPayoutDetails },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Zar { details: ZarPayoutDetails },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
BlockchainWallet { details: WalletDetails },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum UsdPayoutDetails {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
UsdDomestic {
|
||||
symbol: UsdSymbol,
|
||||
account_type: FiatAccountType,
|
||||
transfer_type: UsdTransferType,
|
||||
bank_name: String,
|
||||
bank_account_number_truncated: TruncatedString,
|
||||
bank_routing_number_truncated: TruncatedString,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
UsdPeru {
|
||||
symbol: UsdSymbol,
|
||||
account_type: FiatAccountType,
|
||||
document_type: DocumentType,
|
||||
bank_name: String,
|
||||
bank_account_number_truncated: TruncatedString,
|
||||
document_number_truncated: TruncatedString,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
UsdChina {
|
||||
symbol: UsdSymbol,
|
||||
account_type: FiatAccountType,
|
||||
document_type: DocumentType,
|
||||
bank_name: String,
|
||||
bank_account_number_truncated: TruncatedString,
|
||||
document_number_truncated: TruncatedString,
|
||||
swift_bic_truncated: TruncatedString,
|
||||
phone_number_truncated: TruncatedString,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
UsdPanama {
|
||||
symbol: UsdSymbol,
|
||||
account_type: FiatAccountType,
|
||||
document_type: DocumentType,
|
||||
bank_name: String,
|
||||
bank_account_number_truncated: TruncatedString,
|
||||
document_number_truncated: TruncatedString,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum UsdTransferType {
|
||||
Ach,
|
||||
Wire,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum ArsPayoutDetails {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ArsAlias {
|
||||
symbol: ArsSymbol,
|
||||
bank_name: String,
|
||||
alias_truncated: TruncatedString,
|
||||
document_number_truncated: TruncatedString,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ArsAccountNumber {
|
||||
symbol: ArsSymbol,
|
||||
bank_account_number_type: ArsBankAccountNumberType,
|
||||
bank_name: String,
|
||||
bank_account_number_truncated: TruncatedString,
|
||||
document_number_truncated: TruncatedString,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum ArsBankAccountNumberType {
|
||||
Cvu,
|
||||
Cbu,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum BrlPayoutDetails {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
PixPhone {
|
||||
symbol: BrlSymbol,
|
||||
full_legal_name: String,
|
||||
bank_name: String,
|
||||
phone_number_truncated: TruncatedString,
|
||||
document_number_truncated: TruncatedString,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
PixEmail {
|
||||
symbol: BrlSymbol,
|
||||
full_legal_name: String,
|
||||
bank_name: String,
|
||||
email_truncated: TruncatedString,
|
||||
document_number_truncated: TruncatedString,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
PixDocument {
|
||||
symbol: BrlSymbol,
|
||||
full_legal_name: String,
|
||||
bank_name: String,
|
||||
document_number_truncated: TruncatedString,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
PixBankAccount {
|
||||
symbol: BrlSymbol,
|
||||
full_legal_name: String,
|
||||
bank_name: String,
|
||||
bank_account_number_truncated: TruncatedString,
|
||||
document_number_truncated: TruncatedString,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Wire {
|
||||
symbol: BrlSymbol,
|
||||
account_type: FiatAccountType,
|
||||
full_legal_name: String,
|
||||
bank_name: String,
|
||||
account_number_truncated: TruncatedString,
|
||||
bank_branch_truncated: TruncatedString,
|
||||
document_number_truncated: TruncatedString,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum CopPayoutDetails {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
CopDomestic {
|
||||
symbol: CopSymbol,
|
||||
account_type: FiatAccountType,
|
||||
document_type: DocumentType,
|
||||
bank_name: String,
|
||||
phone_number_truncated: TruncatedString,
|
||||
document_number_truncated: TruncatedString,
|
||||
bank_account_number_truncated: TruncatedString,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum EurPayoutDetails {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
EurSepa {
|
||||
symbol: EurSymbol,
|
||||
country: String,
|
||||
bank_name: String,
|
||||
iban_truncated: TruncatedString,
|
||||
swift_bic_truncated: TruncatedString,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum MxnPayoutDetails {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MxnDomestic {
|
||||
symbol: MxnSymbol,
|
||||
bank_name: String,
|
||||
bank_account_number_truncated: TruncatedString,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum ClpPayoutDetails {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ClpDomestic {
|
||||
clp: ClpSymbol,
|
||||
account_type: FiatAccountType,
|
||||
document_type: DocumentType,
|
||||
bank_name: String,
|
||||
bank_account_number_truncated: TruncatedString,
|
||||
document_number_truncated: TruncatedString,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum PenPayoutDetails {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
PenDomestic {
|
||||
symbol: PenSymbol,
|
||||
document_type: DocumentType,
|
||||
account_type: FiatAccountType,
|
||||
bank_name: String,
|
||||
document_number_truncated: TruncatedString,
|
||||
bank_account_number_truncated: TruncatedString,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum BobPayoutDetails {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
BobDomestic {
|
||||
symbol: BobSymbol,
|
||||
document_type: DocumentType,
|
||||
bank_name: String,
|
||||
bank_account_number_truncated: TruncatedString,
|
||||
document_number_truncated: TruncatedString,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum CrcPayoutDetails {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
CrcDomestic {
|
||||
symbol: CrcSymbol,
|
||||
document_type: DocumentType,
|
||||
bank_name: String,
|
||||
iban_truncated: TruncatedString,
|
||||
document_number_truncated: TruncatedString,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum ZarPayoutDetails {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ZarDomestic {
|
||||
symbol: ZarSymbol,
|
||||
account_type: FiatAccountType,
|
||||
bank_name: String,
|
||||
bank_account_number_truncated: TruncatedString,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreatePayoutMethod {
|
||||
pub alias: String,
|
||||
pub payout_method: PayoutMethodDetails,
|
||||
}
|
||||
24
packages/muralpay/src/serde_iso3166.rs
Normal file
24
packages/muralpay/src/serde_iso3166.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use serde::{Deserialize, de::Error};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use rust_iso3166::CountryCode;
|
||||
|
||||
pub fn serialize<S: serde::Serializer>(
|
||||
v: &CountryCode,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_str(v.alpha2)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D: serde::Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<CountryCode, D::Error> {
|
||||
<Cow<'_, str>>::deserialize(deserializer).and_then(|country_code| {
|
||||
rust_iso3166::ALPHA2_MAP
|
||||
.get(&country_code)
|
||||
.copied()
|
||||
.ok_or_else(|| {
|
||||
D::Error::custom("invalid ISO 3166 alpha-2 country code")
|
||||
})
|
||||
})
|
||||
}
|
||||
100
packages/muralpay/src/util.rs
Normal file
100
packages/muralpay/src/util.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use reqwest::{IntoUrl, RequestBuilder};
|
||||
use secrecy::ExposeSecret;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use crate::{ApiError, MuralError, MuralPay, TransferError};
|
||||
|
||||
impl MuralPay {
|
||||
fn http_req(
|
||||
&self,
|
||||
make_req: impl FnOnce() -> RequestBuilder,
|
||||
) -> RequestBuilder {
|
||||
make_req()
|
||||
.bearer_auth(self.api_key.expose_secret())
|
||||
.header("accept", "application/json")
|
||||
.header("content-type", "application/json")
|
||||
}
|
||||
|
||||
pub(crate) fn http_get<U: IntoUrl>(
|
||||
&self,
|
||||
make_url: impl FnOnce(&str) -> U,
|
||||
) -> RequestBuilder {
|
||||
self.http_req(|| self.http.get(make_url(&self.api_url)))
|
||||
}
|
||||
|
||||
pub(crate) fn http_post<U: IntoUrl>(
|
||||
&self,
|
||||
make_url: impl FnOnce(&str) -> U,
|
||||
) -> RequestBuilder {
|
||||
self.http_req(|| self.http.post(make_url(&self.api_url)))
|
||||
}
|
||||
|
||||
pub(crate) fn http_put<U: IntoUrl>(
|
||||
&self,
|
||||
make_url: impl FnOnce(&str) -> U,
|
||||
) -> RequestBuilder {
|
||||
self.http_req(|| self.http.put(make_url(&self.api_url)))
|
||||
}
|
||||
|
||||
pub(crate) fn http_delete<U: IntoUrl>(
|
||||
&self,
|
||||
make_url: impl FnOnce(&str) -> U,
|
||||
) -> RequestBuilder {
|
||||
self.http_req(|| self.http.delete(make_url(&self.api_url)))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RequestExt: Sized {
|
||||
fn transfer_auth(self, client: &MuralPay) -> Result<Self, TransferError>;
|
||||
|
||||
async fn send_mural<T: DeserializeOwned>(self) -> crate::Result<T>;
|
||||
}
|
||||
|
||||
const HEADER_TRANSFER_API_KEY: &str = "transfer-api-key";
|
||||
|
||||
impl RequestExt for reqwest::RequestBuilder {
|
||||
fn transfer_auth(self, client: &MuralPay) -> Result<Self, TransferError> {
|
||||
let transfer_api_key = client
|
||||
.transfer_api_key
|
||||
.as_ref()
|
||||
.ok_or(TransferError::NoTransferKey)?;
|
||||
|
||||
Ok(self
|
||||
.header(HEADER_TRANSFER_API_KEY, transfer_api_key.expose_secret()))
|
||||
}
|
||||
|
||||
async fn send_mural<T: DeserializeOwned>(self) -> crate::Result<T> {
|
||||
let resp = self.send().await?;
|
||||
let status = resp.status();
|
||||
if status.is_client_error() || status.is_server_error() {
|
||||
let json = resp.bytes().await?;
|
||||
let err = serde_json::from_slice::<ApiError>(&json)
|
||||
.map_err(|source| MuralError::DecodeError { source, json })?;
|
||||
Err(MuralError::Api(err))
|
||||
} else {
|
||||
let json = resp.bytes().await?;
|
||||
let t = serde_json::from_slice::<T>(&json)
|
||||
.map_err(|source| MuralError::Decode { source, json })?;
|
||||
Ok(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! display_as_serialize {
|
||||
($T:ty) => {
|
||||
const _: () = {
|
||||
use std::fmt;
|
||||
|
||||
impl fmt::Display for $T {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let value =
|
||||
serde_json::to_value(self).map_err(|_| fmt::Error)?;
|
||||
let value = value.as_str().ok_or(fmt::Error)?;
|
||||
write!(f, "{value}")
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use display_as_serialize;
|
||||
Reference in New Issue
Block a user