use chrono::{DateTime, NaiveDate, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::broker::settlements::TransmitterInfo;
use crate::types::wire::wire_enum;
wire_enum! {
pub enum InstantFundingStatus {
Pending => "PENDING",
Canceled => "CANCELED",
Executed => "EXECUTED",
Failed => "FAILED",
Completed => "COMPLETED",
}
}
wire_enum! {
pub enum InstantFundingFeeType {
Partner => "partner",
Alpaca => "alpaca",
}
}
wire_enum! {
pub enum InstantFundingSortBy {
CreatedAt => "created_at",
Amount => "amount",
Deadline => "deadline",
}
}
wire_enum! {
pub enum SortOrder {
Asc => "ASC",
Desc => "DESC",
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct InstantFundingFee {
pub id: Uuid,
#[serde(rename = "type")]
pub fee_type: InstantFundingFeeType,
#[serde(with = "crate::types::decimal")]
pub amount: Decimal,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct InstantFundingInterest {
pub id: Uuid,
pub date: NaiveDate,
#[serde(with = "crate::types::decimal")]
pub amount: Decimal,
pub status: InstantFundingStatus,
pub created_at: DateTime<Utc>,
#[serde(default)]
pub reconciled_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct InstantFunding {
pub id: Uuid,
pub account_no: String,
pub source_account_no: String,
#[serde(with = "crate::types::decimal")]
pub amount: Decimal,
#[serde(with = "crate::types::decimal")]
pub remaining_payable: Decimal,
#[serde(with = "crate::types::decimal")]
pub total_interest: Decimal,
pub status: InstantFundingStatus,
pub system_date: NaiveDate,
pub deadline: NaiveDate,
#[serde(
default,
deserialize_with = "crate::types::serde_util::null_as_default"
)]
pub fees: Vec<InstantFundingFee>,
#[serde(
default,
deserialize_with = "crate::types::serde_util::null_as_default"
)]
pub interests: Vec<InstantFundingInterest>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct InstantFundingLimits {
#[serde(default, with = "crate::types::option_decimal")]
pub amount_limit: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub amount_in_use: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub amount_available: Option<Decimal>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AccountInstantFundingLimits {
pub account_no: String,
#[serde(with = "crate::types::decimal")]
pub amount_limit: Decimal,
#[serde(with = "crate::types::decimal")]
pub amount_in_use: Decimal,
#[serde(with = "crate::types::decimal")]
pub amount_available: Decimal,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct InstantFundingReport {
pub account_no: String,
pub system_date: NaiveDate,
pub deadline: NaiveDate,
#[serde(with = "crate::types::decimal")]
pub total_amount_owed: Decimal,
#[serde(with = "crate::types::decimal")]
pub total_interest_penalty: Decimal,
#[serde(
default,
deserialize_with = "crate::types::serde_util::null_as_default"
)]
pub instant_funding_transfers: Vec<InstantFunding>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetInstantFundingRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<InstantFundingStatus>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub system_date: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deadline: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub before: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sort_by: Option<InstantFundingSortBy>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sort_order: Option<SortOrder>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub offset: Option<u32>,
}
impl GetInstantFundingRequest {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn status(mut self, status: InstantFundingStatus) -> Self {
self.status = Some(status);
self
}
#[must_use]
pub fn sort(mut self, sort_by: InstantFundingSortBy, order: SortOrder) -> Self {
self.sort_by = Some(sort_by);
self.sort_order = Some(order);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreateInstantFundingRequest {
pub account_no: String,
pub source_account_no: String,
#[serde(with = "crate::types::decimal")]
pub amount: Decimal,
}
impl CreateInstantFundingRequest {
pub fn new(
account_no: impl Into<String>,
source_account_no: impl Into<String>,
amount: Decimal,
) -> Self {
Self {
account_no: account_no.into(),
source_account_no: source_account_no.into(),
amount,
}
}
pub fn validate(&self) -> crate::Result<()> {
if self.amount <= Decimal::ZERO {
return Err(crate::Error::InvalidRequest(
"amount must be greater than zero".to_owned(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SettlementTransfer {
pub instant_transfer_id: Uuid,
pub transmitter_info: TransmitterInfo,
}
impl SettlementTransfer {
#[must_use]
pub fn new(instant_transfer_id: Uuid, transmitter_info: TransmitterInfo) -> Self {
Self {
instant_transfer_id,
transmitter_info,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreateInstantFundingSettlementRequest {
pub transfers: Vec<SettlementTransfer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub additional_info: Option<String>,
}
impl CreateInstantFundingSettlementRequest {
#[must_use]
pub fn new(transfers: Vec<SettlementTransfer>) -> Self {
Self {
transfers,
additional_info: None,
}
}
pub fn validate(&self) -> crate::Result<()> {
if self.transfers.is_empty() {
return Err(crate::Error::InvalidRequest(
"a settlement must name at least one transfer".to_owned(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetInstantFundingReportRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub report_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub system_date: Option<NaiveDate>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetAccountLimitsRequest {
#[serde(serialize_with = "crate::types::serde_util::comma_separated_required")]
pub account_numbers: Vec<String>,
}
impl GetAccountLimitsRequest {
#[must_use]
pub fn new(account_numbers: Vec<String>) -> Self {
Self { account_numbers }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_zero_advance_is_refused_before_it_is_sent() {
let request = CreateInstantFundingRequest::new("acct", "source", Decimal::ZERO);
assert!(request.validate().is_err());
let request = CreateInstantFundingRequest::new("acct", "source", Decimal::ONE);
assert!(request.validate().is_ok());
}
#[test]
fn a_settlement_must_settle_something() {
assert!(
CreateInstantFundingSettlementRequest::new(Vec::new())
.validate()
.is_err()
);
}
#[test]
fn the_sort_order_is_upper_case_on_this_family_alone() {
assert_eq!(SortOrder::Asc.as_str(), "ASC");
assert_eq!(crate::types::Sort::Asc.as_str(), "asc");
}
#[test]
fn an_advance_with_no_fees_or_interest_yet_decodes() {
let funding: InstantFunding = serde_json::from_value(serde_json::json!({
"id": "550e8400-e29b-41d4-a716-446655440000",
"account_no": "123",
"source_account_no": "456",
"amount": "1000.00",
"remaining_payable": "1000.00",
"total_interest": "0",
"status": "EXECUTED",
"system_date": "2026-01-02",
"deadline": "2026-01-05",
"created_at": "2026-01-02T15:04:05Z",
}))
.unwrap();
assert!(funding.fees.is_empty());
assert!(funding.interests.is_empty());
}
}