use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use stateset_primitives::{CurrencyCode, PrepaymentApplicationId, PrepaymentId};
use strum::{Display, EnumString};
use uuid::Uuid;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Display, EnumString,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[non_exhaustive]
pub enum PrepaymentStatus {
#[default]
Open,
Applied,
Refunded,
Cancelled,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Display, EnumString,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[non_exhaustive]
pub enum PrepaymentTargetType {
#[default]
Bill,
PaymentObligation,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Prepayment {
pub id: PrepaymentId,
pub number: String,
pub supplier_id: Uuid,
pub amount: Decimal,
pub remaining: Decimal,
pub currency: CurrencyCode,
pub status: PrepaymentStatus,
pub method: Option<String>,
pub reference: Option<String>,
pub memo: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Prepayment {
#[must_use]
pub fn has_balance(&self) -> bool {
self.status == PrepaymentStatus::Open && self.remaining > Decimal::ZERO
}
#[must_use]
pub fn applied_amount(&self) -> Decimal {
self.amount - self.remaining
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrepaymentApplication {
pub id: PrepaymentApplicationId,
pub prepayment_id: PrepaymentId,
pub target_type: PrepaymentTargetType,
pub target_id: Uuid,
pub amount: Decimal,
pub reversed: bool,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreatePrepayment {
pub supplier_id: Uuid,
pub amount: Decimal,
pub currency: Option<CurrencyCode>,
pub method: Option<String>,
pub reference: Option<String>,
pub memo: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApplyPrepayment {
pub target_type: PrepaymentTargetType,
pub target_id: Uuid,
pub amount: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PrepaymentFilter {
pub supplier_id: Option<Uuid>,
pub status: Option<PrepaymentStatus>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
fn make(amount: Decimal, remaining: Decimal, status: PrepaymentStatus) -> Prepayment {
Prepayment {
id: PrepaymentId::new(),
number: "PRE-1".into(),
supplier_id: Uuid::nil(),
amount,
remaining,
currency: CurrencyCode::USD,
status,
method: Some("wire".into()),
reference: None,
memo: None,
created_at: Utc::now(),
updated_at: Utc::now(),
}
}
#[test]
fn has_balance_requires_open_and_positive() {
assert!(make(dec!(100), dec!(40), PrepaymentStatus::Open).has_balance());
assert!(!make(dec!(100), dec!(0), PrepaymentStatus::Open).has_balance());
assert!(!make(dec!(100), dec!(40), PrepaymentStatus::Refunded).has_balance());
}
#[test]
fn applied_amount_is_amount_minus_remaining() {
assert_eq!(make(dec!(100), dec!(30), PrepaymentStatus::Open).applied_amount(), dec!(70));
}
#[test]
fn status_and_target_roundtrip() {
for s in [
PrepaymentStatus::Open,
PrepaymentStatus::Applied,
PrepaymentStatus::Refunded,
PrepaymentStatus::Cancelled,
] {
assert_eq!(s.to_string().parse::<PrepaymentStatus>().unwrap(), s);
}
for t in [PrepaymentTargetType::Bill, PrepaymentTargetType::PaymentObligation] {
assert_eq!(t.to_string().parse::<PrepaymentTargetType>().unwrap(), t);
}
}
}