use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::types::SupportedCurrencies;
use crate::types::wire::wire_enum;
wire_enum! {
pub enum FundingWalletStatus {
Active => "active",
Pending => "pending",
Disabled => "disabled",
}
}
wire_enum! {
pub enum PaymentType {
SwiftWire => "swift_wire",
LocalRails => "local_rails",
}
}
wire_enum! {
pub enum FundingDirection {
Incoming => "incoming",
Outgoing => "outgoing",
}
}
wire_enum! {
pub enum FundingTransferStatus {
Pending => "PENDING",
Canceled => "CANCELED",
Executed => "EXECUTED",
Failed => "FAILED",
Complete => "COMPLETE",
}
}
wire_enum! {
pub enum FundingFeeType {
WithdrawalFee => "withdrawal_fee",
FxFee => "fx_fee",
NetworkFee => "network_fee",
DepositFee => "deposit_fee",
AchReturnFee => "ach_return_fee",
PartnerFee => "parnter_fee",
AlpacaFee => "alpaca_fee",
}
}
wire_enum! {
pub enum RoutingCodeType {
SortCode => "sort_code",
Aba => "aba",
BsbCode => "bsb_code",
InstitutionNo => "institution_no",
BankCode => "bank_code",
BranchCode => "branch_code",
Clabe => "clabe",
Cnaps => "cnaps",
Ifsc => "ifsc",
}
}
wire_enum! {
pub enum RecipientAccountType {
Checking => "checking",
Savings => "savings",
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FundingWallet {
pub account_id: Uuid,
pub status: FundingWalletStatus,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FundingWallets {
#[serde(
default,
deserialize_with = "crate::types::serde_util::null_as_default"
)]
pub funding_wallets: Vec<FundingWallet>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FundingFee {
#[serde(rename = "type")]
pub fee_type: FundingFeeType,
#[serde(with = "crate::types::decimal")]
pub amount: Decimal,
pub currency: crate::types::SupportedCurrencies,
pub payment_type: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UsdAmount {
#[serde(with = "crate::types::decimal")]
pub amount: Decimal,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FundingWalletTransfer {
#[serde(default)]
pub id: Option<Uuid>,
#[serde(default)]
pub account_id: Option<Uuid>,
#[serde(default)]
pub direction: Option<FundingDirection>,
#[serde(default)]
pub status: Option<FundingTransferStatus>,
#[serde(default)]
pub payment_type: Option<PaymentType>,
#[serde(default, with = "crate::types::option_decimal")]
pub requested_amount: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub original_amount: Option<Decimal>,
#[serde(default)]
pub original_currency: Option<SupportedCurrencies>,
#[serde(default)]
pub usd: Option<UsdAmount>,
#[serde(
default,
deserialize_with = "crate::types::serde_util::null_as_default"
)]
pub fees: Vec<FundingFee>,
#[serde(default)]
pub created_at: Option<DateTime<Utc>>,
#[serde(default)]
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FundingWalletTransfers {
#[serde(
default,
deserialize_with = "crate::types::serde_util::null_as_default"
)]
pub transfers: Vec<FundingWalletTransfer>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RecipientBank {
#[serde(default)]
pub id: Option<Uuid>,
#[serde(default)]
pub account_number: Option<String>,
#[serde(default)]
pub iban: Option<String>,
#[serde(default)]
pub bic_swift: Option<String>,
#[serde(default)]
pub routing_code: Option<String>,
#[serde(default)]
pub routing_code_type: Option<RoutingCodeType>,
#[serde(default)]
pub first_name: Option<String>,
#[serde(default)]
pub last_name: Option<String>,
#[serde(default)]
pub company_name: Option<String>,
#[serde(default)]
pub street_address: Option<String>,
#[serde(default)]
pub city: Option<String>,
#[serde(default)]
pub state_or_province: Option<String>,
#[serde(default)]
pub postal_code: Option<String>,
#[serde(default)]
pub country: Option<String>,
#[serde(default)]
pub currency: Option<crate::types::SupportedCurrencies>,
#[serde(
default,
deserialize_with = "crate::types::serde_util::null_as_default"
)]
pub payment_types: Vec<PaymentType>,
#[serde(default)]
pub created_at: Option<DateTime<Utc>>,
#[serde(default)]
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct BatchCreateFundingWalletsRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub account_ids: Option<Vec<Uuid>>,
}
impl BatchCreateFundingWalletsRequest {
#[must_use]
pub fn new(account_ids: Vec<Uuid>) -> Self {
Self {
account_ids: Some(account_ids),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetFundingDetailsRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub payment_type: Option<PaymentType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub currency: Option<crate::types::SupportedCurrencies>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreateRecipientBankRequest {
pub account_number: String,
pub bank_name: String,
pub bank_country: String,
pub currency: crate::types::SupportedCurrencies,
pub street_address: String,
pub city: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state_or_province: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub postal_code: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bank_account_holder_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub account_type: Option<RecipientAccountType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub iban: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bic_swift: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub routing_code: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub routing_code_type: Option<RoutingCodeType>,
}
impl CreateRecipientBankRequest {
#[must_use]
pub fn new(
account_number: impl Into<String>,
bank_name: impl Into<String>,
bank_country: impl Into<String>,
currency: SupportedCurrencies,
street_address: impl Into<String>,
city: impl Into<String>,
) -> Self {
Self {
account_number: account_number.into(),
bank_name: bank_name.into(),
bank_country: bank_country.into(),
currency,
street_address: street_address.into(),
city: city.into(),
state_or_province: None,
postal_code: None,
bank_account_holder_name: None,
account_type: None,
iban: None,
bic_swift: None,
routing_code: None,
routing_code_type: None,
}
}
#[must_use]
pub fn iban(mut self, iban: impl Into<String>) -> Self {
self.iban = Some(iban.into());
self
}
#[must_use]
pub fn bic_swift(mut self, bic_swift: impl Into<String>) -> Self {
self.bic_swift = Some(bic_swift.into());
self
}
#[must_use]
pub fn routing_code(mut self, code: impl Into<String>, code_type: RoutingCodeType) -> Self {
self.routing_code = Some(code.into());
self.routing_code_type = Some(code_type);
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreateWithdrawalRequest {
#[serde(
default,
with = "crate::types::option_decimal",
skip_serializing_if = "Option::is_none"
)]
pub usd_amount: Option<Decimal>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub desired_currency: Option<SupportedCurrencies>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub payment_type: Option<PaymentType>,
}
impl CreateWithdrawalRequest {
#[must_use]
pub fn new(usd_amount: Decimal, desired_currency: SupportedCurrencies) -> Self {
Self {
usd_amount: Some(usd_amount),
desired_currency: Some(desired_currency),
payment_type: None,
}
}
#[must_use]
pub fn payment_type(mut self, payment_type: PaymentType) -> Self {
self.payment_type = Some(payment_type);
self
}
pub fn validate(&self) -> crate::Result<()> {
if self
.usd_amount
.is_some_and(|amount| amount <= Decimal::ZERO)
{
return Err(crate::Error::InvalidRequest(
"usd_amount must be greater than zero".to_owned(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DemoFundingRequest {
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub amount: Option<Decimal>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub currency: Option<crate::types::SupportedCurrencies>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub receiver_account_number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub receiver_routing_code: Option<String>,
}
impl DemoFundingRequest {
#[must_use]
pub fn new(
amount: Decimal,
currency: SupportedCurrencies,
receiver_account_number: impl Into<String>,
) -> Self {
Self {
amount: Some(amount),
currency: Some(currency),
receiver_account_number: Some(receiver_account_number.into()),
receiver_routing_code: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_partner_fee_keeps_alpacas_typo() {
assert_eq!(FundingFeeType::PartnerFee.as_str(), "parnter_fee");
}
#[test]
fn complete_is_spelled_differently_here_than_in_instant_funding() {
assert_eq!(FundingTransferStatus::Complete.as_str(), "COMPLETE");
assert_eq!(
crate::broker::InstantFundingStatus::Completed.as_str(),
"COMPLETED"
);
}
#[test]
fn the_direction_is_lower_case_here_and_upper_case_on_crypto_wallets() {
assert_eq!(FundingDirection::Incoming.as_str(), "incoming");
assert_eq!(
crate::trading::TransferDirection::Incoming.as_str(),
"INCOMING"
);
}
#[test]
fn a_recipient_bank_needs_only_what_the_reference_requires() {
let bank = CreateRecipientBankRequest::new(
"12345678",
"Example Bank",
"GB",
SupportedCurrencies::Gbp,
"1 Example Street",
"London",
);
let json = serde_json::to_value(&bank).unwrap();
assert!(json.get("iban").is_none());
assert!(json.get("bic_swift").is_none());
assert!(json.get("routing_code").is_none());
}
#[test]
fn a_zero_withdrawal_is_refused_before_it_is_sent() {
let request = CreateWithdrawalRequest::new(Decimal::ZERO, SupportedCurrencies::Gbp);
assert!(request.validate().is_err());
}
}