use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use stateset_primitives::{CurrencyCode, CustomerId, InvoiceId, OrderId, OrderItemId, ProductId};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, Serialize, Deserialize, Default)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum InvoiceStatus {
#[default]
Draft,
Sent,
Viewed,
PartiallyPaid,
Paid,
Overdue,
Voided,
WrittenOff,
Disputed,
}
impl std::str::FromStr for InvoiceStatus {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"draft" => Ok(Self::Draft),
"sent" => Ok(Self::Sent),
"viewed" => Ok(Self::Viewed),
"partially_paid" => Ok(Self::PartiallyPaid),
"paid" => Ok(Self::Paid),
"overdue" => Ok(Self::Overdue),
"voided" => Ok(Self::Voided),
"written_off" => Ok(Self::WrittenOff),
"disputed" => Ok(Self::Disputed),
_ => Err(format!("Unknown invoice status: {}", s)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, Serialize, Deserialize, Default)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum InvoiceType {
#[default]
Standard,
CreditMemo,
DebitMemo,
Proforma,
Recurring,
Final,
}
impl std::str::FromStr for InvoiceType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"standard" => Ok(Self::Standard),
"credit_memo" | "credit_note" => Ok(Self::CreditMemo),
"debit_memo" | "debit_note" => Ok(Self::DebitMemo),
"proforma" => Ok(Self::Proforma),
"recurring" => Ok(Self::Recurring),
"final" => Ok(Self::Final),
_ => Err(format!("Unknown invoice type: {}", s)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Invoice {
pub id: InvoiceId,
pub invoice_number: String,
pub customer_id: CustomerId,
pub order_id: Option<OrderId>,
pub status: InvoiceStatus,
pub invoice_type: InvoiceType,
pub invoice_date: DateTime<Utc>,
pub due_date: DateTime<Utc>,
pub payment_terms: Option<String>,
pub currency: CurrencyCode,
pub billing_name: Option<String>,
pub billing_email: Option<String>,
pub billing_address: Option<String>,
pub billing_city: Option<String>,
pub billing_state: Option<String>,
pub billing_postal_code: Option<String>,
pub billing_country: Option<String>,
pub subtotal: Decimal,
pub discount_amount: Decimal,
pub discount_percent: Option<Decimal>,
pub tax_amount: Decimal,
pub tax_rate: Option<Decimal>,
pub shipping_amount: Decimal,
pub total: Decimal,
pub amount_paid: Decimal,
pub balance_due: Decimal,
pub po_number: Option<String>,
pub notes: Option<String>,
pub terms: Option<String>,
pub footer: Option<String>,
pub sent_at: Option<DateTime<Utc>>,
pub viewed_at: Option<DateTime<Utc>>,
pub paid_at: Option<DateTime<Utc>>,
pub voided_at: Option<DateTime<Utc>>,
pub items: Vec<InvoiceItem>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Invoice {
pub fn is_overdue(&self) -> bool {
if self.status == InvoiceStatus::Paid || self.status == InvoiceStatus::Voided {
return false;
}
Utc::now() > self.due_date
}
pub fn days_until_due(&self) -> i64 {
(self.due_date - Utc::now()).num_days()
}
pub fn calculate_balance(&self) -> Decimal {
self.total - self.amount_paid
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InvoiceItem {
pub id: Uuid,
pub invoice_id: InvoiceId,
pub order_item_id: Option<OrderItemId>,
pub product_id: Option<ProductId>,
pub sku: Option<String>,
pub description: String,
pub quantity: Decimal,
pub unit_of_measure: Option<String>,
pub unit_price: Decimal,
pub discount_amount: Decimal,
pub tax_amount: Decimal,
pub line_total: Decimal,
pub sort_order: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CreateInvoice {
pub customer_id: CustomerId,
pub order_id: Option<OrderId>,
pub invoice_type: Option<InvoiceType>,
pub invoice_date: Option<DateTime<Utc>>,
pub due_date: Option<DateTime<Utc>>,
pub days_until_due: Option<i32>,
pub payment_terms: Option<String>,
pub currency: Option<CurrencyCode>,
pub billing_name: Option<String>,
pub billing_email: Option<String>,
pub billing_address: Option<String>,
pub billing_city: Option<String>,
pub billing_state: Option<String>,
pub billing_postal_code: Option<String>,
pub billing_country: Option<String>,
pub discount_amount: Option<Decimal>,
pub discount_percent: Option<Decimal>,
pub tax_amount: Option<Decimal>,
pub tax_rate: Option<Decimal>,
pub shipping_amount: Option<Decimal>,
pub po_number: Option<String>,
pub notes: Option<String>,
pub terms: Option<String>,
pub footer: Option<String>,
pub items: Vec<CreateInvoiceItem>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CreateInvoiceItem {
pub order_item_id: Option<OrderItemId>,
pub product_id: Option<ProductId>,
pub sku: Option<String>,
pub description: String,
pub quantity: Decimal,
pub unit_of_measure: Option<String>,
pub unit_price: Decimal,
pub discount_amount: Option<Decimal>,
pub tax_amount: Option<Decimal>,
pub sort_order: Option<i32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UpdateInvoice {
pub due_date: Option<DateTime<Utc>>,
pub payment_terms: Option<String>,
pub billing_name: Option<String>,
pub billing_email: Option<String>,
pub billing_address: Option<String>,
pub billing_city: Option<String>,
pub billing_state: Option<String>,
pub billing_postal_code: Option<String>,
pub billing_country: Option<String>,
pub discount_amount: Option<Decimal>,
pub discount_percent: Option<Decimal>,
pub tax_amount: Option<Decimal>,
pub tax_rate: Option<Decimal>,
pub shipping_amount: Option<Decimal>,
pub po_number: Option<String>,
pub notes: Option<String>,
pub terms: Option<String>,
pub footer: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RecordInvoicePayment {
pub amount: Decimal,
pub payment_id: Option<Uuid>,
pub payment_method: Option<String>,
pub reference: Option<String>,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct InvoiceFilter {
pub customer_id: Option<CustomerId>,
pub order_id: Option<OrderId>,
pub status: Option<InvoiceStatus>,
pub invoice_type: Option<InvoiceType>,
pub overdue_only: Option<bool>,
pub from_date: Option<DateTime<Utc>>,
pub to_date: Option<DateTime<Utc>>,
pub due_from: Option<DateTime<Utc>>,
pub due_to: Option<DateTime<Utc>>,
pub min_total: Option<Decimal>,
pub max_total: Option<Decimal>,
pub min_balance: Option<Decimal>,
pub invoice_number: Option<String>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
pub fn generate_invoice_number() -> String {
let now = chrono::Utc::now();
let short_id = &uuid::Uuid::new_v4().simple().to_string()[..8];
format!("INV-{}-{short_id}", now.format("%Y%m%d%H%M%S%3f"))
}
#[cfg(test)]
mod tests {
use super::generate_invoice_number;
#[test]
fn generated_invoice_numbers_include_entropy_suffix() {
let first = generate_invoice_number();
let second = generate_invoice_number();
assert!(first.starts_with("INV-"));
assert!(first.len() > "INV-20260101120000000".len());
assert_ne!(first, second);
}
}