use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use stateset_primitives::{CurrencyCode, VendorCreditApplicationId, VendorCreditId};
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 VendorCreditStatus {
#[default]
Open,
Applied,
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 VendorCreditTargetType {
#[default]
Bill,
PaymentObligation,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VendorCredit {
pub id: VendorCreditId,
pub number: String,
pub supplier_id: Uuid,
pub vendor_return_id: Option<Uuid>,
pub amount: Decimal,
pub remaining: Decimal,
pub currency: CurrencyCode,
pub status: VendorCreditStatus,
pub memo: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl VendorCredit {
#[must_use]
pub fn has_balance(&self) -> bool {
self.status == VendorCreditStatus::Open && self.remaining > Decimal::ZERO
}
#[must_use]
pub fn applied_amount(&self) -> Decimal {
self.amount - self.remaining
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VendorCreditApplication {
pub id: VendorCreditApplicationId,
pub vendor_credit_id: VendorCreditId,
pub target_type: VendorCreditTargetType,
pub target_id: Uuid,
pub amount: Decimal,
pub reversed: bool,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateVendorCredit {
pub supplier_id: Uuid,
pub vendor_return_id: Option<Uuid>,
pub amount: Decimal,
pub currency: Option<CurrencyCode>,
pub memo: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApplyVendorCredit {
pub target_type: VendorCreditTargetType,
pub target_id: Uuid,
pub amount: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct VendorCreditFilter {
pub supplier_id: Option<Uuid>,
pub status: Option<VendorCreditStatus>,
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: VendorCreditStatus) -> VendorCredit {
VendorCredit {
id: VendorCreditId::new(),
number: "VC-1".into(),
supplier_id: Uuid::nil(),
vendor_return_id: None,
amount,
remaining,
currency: CurrencyCode::USD,
status,
memo: None,
created_at: Utc::now(),
updated_at: Utc::now(),
}
}
#[test]
fn has_balance_requires_open_and_positive_remaining() {
assert!(make(dec!(100), dec!(40), VendorCreditStatus::Open).has_balance());
assert!(!make(dec!(100), dec!(0), VendorCreditStatus::Open).has_balance());
assert!(!make(dec!(100), dec!(40), VendorCreditStatus::Applied).has_balance());
assert!(!make(dec!(100), dec!(40), VendorCreditStatus::Cancelled).has_balance());
}
#[test]
fn applied_amount_is_amount_minus_remaining() {
assert_eq!(make(dec!(100), dec!(30), VendorCreditStatus::Open).applied_amount(), dec!(70));
}
#[test]
fn status_and_target_roundtrip() {
for s in
[VendorCreditStatus::Open, VendorCreditStatus::Applied, VendorCreditStatus::Cancelled]
{
assert_eq!(s.to_string().parse::<VendorCreditStatus>().unwrap(), s);
}
for t in [VendorCreditTargetType::Bill, VendorCreditTargetType::PaymentObligation] {
assert_eq!(t.to_string().parse::<VendorCreditTargetType>().unwrap(), t);
}
}
}