use chrono::{DateTime, NaiveDate, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::broker::enums::{
AccountEntities, AccountSubType, AccountType, BankAccountType, DocumentType, FeePaymentMethod,
FundingSource, IdentifierType, JournalEntryType, JournalStatus, PortfolioStatus, RunType,
TaxIdType, TradeDocumentType, TransferDirection, TransferTiming, TransferType,
UploadDocumentMimeType, UploadDocumentSubType, VisaType,
};
use crate::broker::models::{
Agreement, Contact, Disclosures, Identity, RebalancingCondition, TrustedContact, W8BenDocument,
Weight,
};
use crate::broker::onboarding::ActivityCategory;
use crate::error::{Error, Result};
use crate::trading::ActivityType;
use crate::trading::{AccountStatus, AssetClass};
use crate::types::Sort;
use crate::types::SupportedCurrencies;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct OrderRequest {
#[serde(flatten)]
pub order: crate::trading::OrderRequest,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub commission: Option<Decimal>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub currency: Option<SupportedCurrencies>,
}
impl OrderRequest {
#[must_use]
pub fn new(order: crate::trading::OrderRequest) -> Self {
Self {
order,
commission: None,
currency: None,
}
}
#[must_use]
pub fn commission(mut self, commission: Decimal) -> Self {
self.commission = Some(commission);
self
}
#[must_use]
pub fn currency(mut self, currency: SupportedCurrencies) -> Self {
self.currency = Some(currency);
self
}
pub fn validate(&self) -> Result<()> {
self.order.validate()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreateAccountRequest {
pub contact: Contact,
pub identity: Identity,
pub disclosures: Disclosures,
pub agreements: Vec<Agreement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub account_type: Option<AccountType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub account_sub_type: Option<AccountSubType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub documents: Option<Vec<UploadDocument>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trusted_contact: Option<TrustedContact>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub currency: Option<SupportedCurrencies>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled_assets: Option<Vec<AssetClass>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub primary_account_holder_id: Option<String>,
}
impl CreateAccountRequest {
#[must_use]
pub fn new(
contact: Contact,
identity: Identity,
disclosures: Disclosures,
agreements: Vec<Agreement>,
) -> Self {
Self {
contact,
identity,
disclosures,
agreements,
account_type: None,
account_sub_type: None,
documents: None,
trusted_contact: None,
currency: None,
enabled_assets: None,
primary_account_holder_id: None,
}
}
pub fn validate(&self) -> Result<()> {
fn require(present: bool, field: &str) -> Result<()> {
if present {
Ok(())
} else {
Err(Error::InvalidRequest(format!(
"{field} is required to open an account"
)))
}
}
require(
!self.contact.email_address.is_empty(),
"contact.email_address",
)?;
require(
!self.contact.street_address.is_empty(),
"contact.street_address",
)?;
require(self.contact.city.is_some(), "contact.city")?;
require(!self.identity.given_name.is_empty(), "identity.given_name")?;
require(
!self.identity.family_name.is_empty(),
"identity.family_name",
)?;
require(
self.identity.date_of_birth.is_some(),
"identity.date_of_birth",
)?;
require(self.identity.tax_id_type.is_some(), "identity.tax_id_type")?;
require(
self.identity.country_of_tax_residence.is_some(),
"identity.country_of_tax_residence",
)?;
require(
!self.identity.funding_source.is_empty(),
"identity.funding_source",
)?;
require(
self.disclosures.is_control_person.is_some(),
"disclosures.is_control_person",
)?;
require(
self.disclosures.is_affiliated_exchange_or_finra.is_some(),
"disclosures.is_affiliated_exchange_or_finra",
)?;
require(
self.disclosures.is_politically_exposed.is_some(),
"disclosures.is_politically_exposed",
)?;
require(
self.disclosures.immediate_family_exposed.is_some(),
"disclosures.immediate_family_exposed",
)?;
require(!self.agreements.is_empty(), "agreements")?;
for agreement in &self.agreements {
require(agreement.ip_address.is_some(), "agreements[].ip_address")?;
}
Ok(())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UpdatableContact {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub email_address: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub phone_number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub street_address: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unit: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub city: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub postal_code: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub country: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UpdatableIdentity {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub given_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub middle_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub family_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub date_of_birth: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tax_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tax_id_type: Option<TaxIdType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub country_of_citizenship: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub country_of_birth: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub country_of_tax_residence: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub funding_source: Option<Vec<FundingSource>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub visa_type: Option<VisaType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub visa_expiration_date: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub date_of_departure_from_usa: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub permanent_resident: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub number_of_dependents: Option<u32>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub annual_income_min: Option<Decimal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub annual_income_max: Option<Decimal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub liquid_net_worth_min: Option<Decimal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub liquid_net_worth_max: Option<Decimal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub total_net_worth_min: Option<Decimal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub total_net_worth_max: Option<Decimal>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UpdateAccountRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub contact: Option<UpdatableContact>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identity: Option<UpdatableIdentity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disclosures: Option<Disclosures>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trusted_contact: Option<TrustedContact>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agreements: Option<Vec<Agreement>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub primary_account_holder_id: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ListAccountsRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub query: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_after: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_before: Option<DateTime<Utc>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "crate::types::serde_util::comma_separated"
)]
pub status: Option<Vec<AccountStatus>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sort: Option<Sort>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "crate::types::serde_util::comma_separated"
)]
pub entities: Option<Vec<AccountEntities>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum CreateACHRelationshipRequest {
Manual(ManualACHRelationship),
Plaid(PlaidACHRelationship),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ManualACHRelationship {
pub account_owner_name: String,
pub bank_account_type: BankAccountType,
pub bank_account_number: String,
pub bank_routing_number: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nickname: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PlaidACHRelationship {
pub processor_token: String,
}
impl ManualACHRelationship {
#[must_use]
pub fn new(
account_owner_name: impl Into<String>,
bank_account_type: BankAccountType,
bank_account_number: impl Into<String>,
bank_routing_number: impl Into<String>,
) -> Self {
Self {
account_owner_name: account_owner_name.into(),
bank_account_type,
bank_account_number: bank_account_number.into(),
bank_routing_number: bank_routing_number.into(),
nickname: None,
}
}
#[must_use]
pub fn nickname(mut self, nickname: impl Into<String>) -> Self {
self.nickname = Some(nickname.into());
self
}
}
impl PlaidACHRelationship {
#[must_use]
pub fn new(processor_token: impl Into<String>) -> Self {
Self {
processor_token: processor_token.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreateBankRequest {
pub name: String,
pub bank_code_type: IdentifierType,
pub bank_code: String,
pub account_number: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub country: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state_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 city: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub street_address: Option<String>,
}
impl CreateBankRequest {
#[must_use]
pub fn domestic(
name: impl Into<String>,
routing_number: impl Into<String>,
account_number: impl Into<String>,
) -> Self {
Self {
name: name.into(),
bank_code_type: IdentifierType::Aba,
bank_code: routing_number.into(),
account_number: account_number.into(),
country: None,
state_province: None,
postal_code: None,
city: None,
street_address: None,
}
}
#[must_use]
pub fn international(
name: impl Into<String>,
bic: impl Into<String>,
account_number: impl Into<String>,
address: BankAddress,
) -> Self {
Self {
name: name.into(),
bank_code_type: IdentifierType::Bic,
bank_code: bic.into(),
account_number: account_number.into(),
country: Some(address.country),
state_province: Some(address.state_province),
postal_code: Some(address.postal_code),
city: Some(address.city),
street_address: Some(address.street_address),
}
}
pub fn validate(&self) -> Result<()> {
let address = [
("country", &self.country),
("state_province", &self.state_province),
("postal_code", &self.postal_code),
("city", &self.city),
("street_address", &self.street_address),
];
match self.bank_code_type {
IdentifierType::Aba => {
for (field, value) in address {
if value.is_some() {
return Err(Error::InvalidRequest(format!(
"{field} may only be set for international bank accounts"
)));
}
}
}
IdentifierType::Bic | IdentifierType::Unknown(_) => {}
}
Ok(())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct BankAddress {
pub country: String,
pub state_province: String,
pub postal_code: String,
pub city: String,
pub street_address: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum CreateTransferRequest {
Ach(CreateACHTransferRequest),
Wire(CreateBankTransferRequest),
}
impl CreateTransferRequest {
pub fn validate(&self) -> Result<()> {
let amount = match self {
Self::Ach(request) => request.amount,
Self::Wire(request) => request.amount,
};
if amount <= Decimal::ZERO {
return Err(Error::InvalidRequest(
"transfer amount must be greater than zero".to_owned(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreateACHTransferRequest {
#[serde(with = "crate::types::decimal")]
pub amount: Decimal,
pub direction: TransferDirection,
pub timing: TransferTiming,
pub relationship_id: Uuid,
pub transfer_type: TransferType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fee_payment_method: Option<FeePaymentMethod>,
}
impl CreateACHTransferRequest {
#[must_use]
pub fn new(
amount: Decimal,
direction: TransferDirection,
timing: TransferTiming,
relationship_id: Uuid,
) -> Self {
Self {
amount,
direction,
timing,
relationship_id,
transfer_type: TransferType::Ach,
fee_payment_method: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreateBankTransferRequest {
#[serde(with = "crate::types::decimal")]
pub amount: Decimal,
pub direction: TransferDirection,
pub timing: TransferTiming,
pub bank_id: Uuid,
pub transfer_type: TransferType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fee_payment_method: Option<FeePaymentMethod>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub additional_information: Option<String>,
}
impl CreateBankTransferRequest {
#[must_use]
pub fn new(
amount: Decimal,
direction: TransferDirection,
timing: TransferTiming,
bank_id: Uuid,
) -> Self {
Self {
amount,
direction,
timing,
bank_id,
transfer_type: TransferType::Wire,
fee_payment_method: None,
additional_information: None,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetTransfersRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub direction: Option<TransferDirection>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub offset: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreateJournalRequest {
pub from_account: Uuid,
pub to_account: Uuid,
pub entry_type: JournalEntryType,
#[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 symbol: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub qty: Option<Decimal>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transmitter_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transmitter_account_number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transmitter_address: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transmitter_financial_institution: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transmitter_timestamp: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub currency: Option<SupportedCurrencies>,
}
impl CreateJournalRequest {
fn base(from_account: Uuid, to_account: Uuid, entry_type: JournalEntryType) -> Self {
Self {
from_account,
to_account,
entry_type,
amount: None,
symbol: None,
qty: None,
description: None,
transmitter_name: None,
transmitter_account_number: None,
transmitter_address: None,
transmitter_financial_institution: None,
transmitter_timestamp: None,
currency: None,
}
}
#[must_use]
pub fn cash(from_account: Uuid, to_account: Uuid, amount: Decimal) -> Self {
let mut request = Self::base(from_account, to_account, JournalEntryType::Cash);
request.amount = Some(amount);
request
}
#[must_use]
pub fn security(
from_account: Uuid,
to_account: Uuid,
symbol: impl Into<String>,
qty: Decimal,
) -> Self {
let mut request = Self::base(from_account, to_account, JournalEntryType::Security);
request.symbol = Some(symbol.into());
request.qty = Some(qty);
request
}
pub fn validate(&self) -> Result<()> {
match self.entry_type {
JournalEntryType::Cash => {
if self.symbol.is_some() || self.qty.is_some() {
return Err(Error::InvalidRequest(
"symbol and qty are reserved for security journals".to_owned(),
));
}
if self.amount.is_none() {
return Err(Error::InvalidRequest(
"cash journals must carry an amount".to_owned(),
));
}
}
JournalEntryType::Security => {
if self.amount.is_some() {
return Err(Error::InvalidRequest(
"amount is reserved for cash journals".to_owned(),
));
}
if self.symbol.is_none() || self.qty.is_none() {
return Err(Error::InvalidRequest(
"security journals must carry a symbol and a qty".to_owned(),
));
}
}
JournalEntryType::Unknown(_) => {}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct BatchJournalRequestEntry {
pub to_account: Uuid,
#[serde(with = "crate::types::decimal")]
pub amount: Decimal,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transmitter_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transmitter_account_number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transmitter_address: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transmitter_financial_institution: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transmitter_timestamp: Option<String>,
}
impl BatchJournalRequestEntry {
#[must_use]
pub fn new(to_account: Uuid, amount: Decimal) -> Self {
Self {
to_account,
amount,
description: None,
transmitter_name: None,
transmitter_account_number: None,
transmitter_address: None,
transmitter_financial_institution: None,
transmitter_timestamp: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ReverseBatchJournalRequestEntry {
pub from_account: Uuid,
#[serde(with = "crate::types::decimal")]
pub amount: Decimal,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transmitter_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transmitter_account_number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transmitter_address: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transmitter_financial_institution: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transmitter_timestamp: Option<String>,
}
impl ReverseBatchJournalRequestEntry {
#[must_use]
pub fn new(from_account: Uuid, amount: Decimal) -> Self {
Self {
from_account,
amount,
description: None,
transmitter_name: None,
transmitter_account_number: None,
transmitter_address: None,
transmitter_financial_institution: None,
transmitter_timestamp: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreateBatchJournalRequest {
pub entry_type: JournalEntryType,
pub from_account: Uuid,
pub entries: Vec<BatchJournalRequestEntry>,
}
impl CreateBatchJournalRequest {
#[must_use]
pub fn new(from_account: Uuid, entries: Vec<BatchJournalRequestEntry>) -> Self {
Self {
entry_type: JournalEntryType::Cash,
from_account,
entries,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreateReverseBatchJournalRequest {
pub entry_type: JournalEntryType,
pub to_account: Uuid,
pub entries: Vec<ReverseBatchJournalRequestEntry>,
}
impl CreateReverseBatchJournalRequest {
#[must_use]
pub fn new(to_account: Uuid, entries: Vec<ReverseBatchJournalRequestEntry>) -> Self {
Self {
entry_type: JournalEntryType::Cash,
to_account,
entries,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetJournalsRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub before: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<JournalStatus>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub entry_type: Option<JournalEntryType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub to_account: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub from_account: Option<Uuid>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetTradeDocumentsRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end: Option<NaiveDate>,
#[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
pub document_type: Option<TradeDocumentType>,
}
impl GetTradeDocumentsRequest {
pub fn validate(&self) -> Result<()> {
if let (Some(start), Some(end)) = (self.start, self.end)
&& start > end
{
return Err(Error::InvalidRequest(
"start must not be after end".to_owned(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum UploadDocument {
Document(UploadDocumentRequest),
W8Ben(UploadW8BenDocumentRequest),
}
impl UploadDocument {
pub fn validate(&self) -> Result<()> {
match self {
Self::Document(document) => document.validate(),
Self::W8Ben(document) => document.validate(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UploadDocumentRequest {
pub document_type: DocumentType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub document_sub_type: Option<UploadDocumentSubType>,
pub content: String,
pub mime_type: UploadDocumentMimeType,
}
impl UploadDocumentRequest {
#[must_use]
pub fn new(
document_type: DocumentType,
content: impl Into<String>,
mime_type: UploadDocumentMimeType,
) -> Self {
Self {
document_type,
document_sub_type: None,
content: content.into(),
mime_type,
}
}
pub fn validate(&self) -> Result<()> {
if self.document_type == DocumentType::W8ben
|| self.document_sub_type == Some(UploadDocumentSubType::FormW8Ben)
{
return Err(Error::InvalidRequest(
"use UploadW8BenDocumentRequest to upload a W-8BEN".to_owned(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UploadW8BenDocumentRequest {
pub document_type: DocumentType,
pub document_sub_type: UploadDocumentSubType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_data: Option<Box<W8BenDocument>>,
pub mime_type: UploadDocumentMimeType,
}
impl UploadW8BenDocumentRequest {
#[must_use]
pub fn from_content(content: impl Into<String>, mime_type: UploadDocumentMimeType) -> Self {
Self {
document_type: DocumentType::W8ben,
document_sub_type: UploadDocumentSubType::FormW8Ben,
content: Some(content.into()),
content_data: None,
mime_type,
}
}
#[must_use]
pub fn from_fields(document: W8BenDocument) -> Self {
Self {
document_type: DocumentType::W8ben,
document_sub_type: UploadDocumentSubType::FormW8Ben,
content: None,
content_data: Some(Box::new(document)),
mime_type: UploadDocumentMimeType::Json,
}
}
pub fn validate(&self) -> Result<()> {
match (&self.content, &self.content_data) {
(None, None) => {
return Err(Error::InvalidRequest(
"a W-8BEN upload needs either content or content_data".to_owned(),
));
}
(Some(_), Some(_)) => {
return Err(Error::InvalidRequest(
"a W-8BEN upload takes content or content_data, not both".to_owned(),
));
}
(Some(_), None) => {}
(None, Some(document)) => {
if self.mime_type != UploadDocumentMimeType::Json {
return Err(Error::InvalidRequest(
"content_data must be sent as application/json".to_owned(),
));
}
document.validate()?;
}
}
if self.document_type != DocumentType::W8ben
|| self.document_sub_type != UploadDocumentSubType::FormW8Ben
{
return Err(Error::InvalidRequest(
"a W-8BEN upload must keep the W8BEN document type and sub type".to_owned(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreatePortfolioRequest {
pub name: String,
pub description: String,
pub weights: Vec<Weight>,
pub cooldown_days: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rebalance_conditions: Option<Vec<RebalancingCondition>>,
}
impl CreatePortfolioRequest {
#[must_use]
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
weights: Vec<Weight>,
cooldown_days: u32,
) -> Self {
Self {
name: name.into(),
description: description.into(),
weights,
cooldown_days,
rebalance_conditions: None,
}
}
pub fn validate(&self) -> Result<()> {
for weight in &self.weights {
weight.validate()?;
}
Ok(())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UpdatePortfolioRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub weights: Option<Vec<Weight>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cooldown_days: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rebalance_conditions: Option<Vec<RebalancingCondition>>,
}
impl UpdatePortfolioRequest {
pub fn validate(&self) -> Result<()> {
for weight in self.weights.iter().flatten() {
weight.validate()?;
}
Ok(())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetPortfoliosRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub symbol: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub portfolio_id: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<PortfolioStatus>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreateSubscriptionRequest {
pub account_id: Uuid,
pub portfolio_id: Uuid,
}
impl CreateSubscriptionRequest {
#[must_use]
pub fn new(account_id: Uuid, portfolio_id: Uuid) -> Self {
Self {
account_id,
portfolio_id,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetSubscriptionsRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub account_id: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub portfolio_id: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page_token: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreateRunRequest {
pub account_id: Uuid,
#[serde(rename = "type")]
pub run_type: RunType,
pub weights: Vec<Weight>,
}
impl CreateRunRequest {
#[must_use]
pub fn new(account_id: Uuid, run_type: RunType, weights: Vec<Weight>) -> Self {
Self {
account_id,
run_type,
weights,
}
}
pub fn validate(&self) -> Result<()> {
for weight in &self.weights {
weight.validate()?;
}
Ok(())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetRunsRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub account_id: Option<Uuid>,
#[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
pub run_type: Option<RunType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page_token: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetAccountActivitiesRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub account_id: Option<Uuid>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "crate::types::serde_util::comma_separated"
)]
pub activity_types: Option<Vec<ActivityType>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub category: Option<ActivityCategory>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub order_id: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub date: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub until: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub direction: Option<Sort>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page_size: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page_token: Option<String>,
}
impl GetAccountActivitiesRequest {
pub fn validate(&self) -> Result<()> {
if self.category.is_some() && self.activity_types.is_some() {
return Err(crate::Error::InvalidRequest(
"category cannot be combined with activity_types".to_owned(),
));
}
Ok(())
}
#[must_use]
pub fn category(mut self, category: ActivityCategory) -> Self {
self.category = Some(category);
self
}
#[must_use]
pub fn order_id(mut self, order_id: Uuid) -> Self {
self.order_id = Some(order_id);
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreateOptionExerciseRequest {
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub commission: Option<Decimal>,
}
impl CreateOptionExerciseRequest {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn commission(mut self, commission: Decimal) -> Self {
self.commission = Some(commission);
self
}
}