use crate::invoice::{InvoiceLine, LineVat, VatBreakdown};
use crate::{Date, DocumentReference, InvoiceAmount};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdvancePayment {
pub gross: InvoiceAmount,
pub received_on: Option<Date>,
pub tax: Vec<VatBreakdown>,
pub reference: Option<DocumentReference>,
pub reference_date: Option<Date>,
}
impl AdvancePayment {
pub fn tax_total(&self) -> Result<InvoiceAmount, crate::AmountError> {
InvoiceAmount::checked_sum(self.tax.iter().map(|e| e.tax_amount))
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct Extensions {
pub advance_payments: Vec<AdvancePayment>,
pub sub_invoice_lines: Vec<(usize, Vec<SubInvoiceLine>)>,
pub third_party_payments: Vec<ThirdPartyPayment>,
}
impl Extensions {
#[must_use]
pub fn populated(&self) -> Vec<&'static str> {
let mut v = Vec::new();
if !self.advance_payments.is_empty() {
v.push(ADVANCE_PAYMENTS);
}
if !self.sub_invoice_lines.is_empty() {
v.push(SUB_INVOICE_LINES);
}
if !self.third_party_payments.is_empty() {
v.push(THIRD_PARTY_PAYMENTS);
}
v
}
#[must_use]
pub fn sub_lines(&self, index: usize) -> &[SubInvoiceLine] {
self.sub_invoice_lines
.iter()
.find(|(i, _)| *i == index)
.map_or(&[], |(_, v)| v.as_slice())
}
pub fn third_party_total(&self) -> Result<InvoiceAmount, crate::AmountError> {
InvoiceAmount::checked_sum(self.third_party_payments.iter().filter_map(|p| p.amount))
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.advance_payments.is_empty()
&& self.sub_invoice_lines.is_empty()
&& self.third_party_payments.is_empty()
}
}
pub const ADVANCE_PAYMENTS: &str = "BG-X-45";
pub const SUB_INVOICE_LINES: &str = "BG-DEX-01";
pub const THIRD_PARTY_PAYMENTS: &str = "BG-DEX-09";
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubInvoiceLine {
pub line: InvoiceLine,
pub vat: Option<LineVat>,
pub children: Vec<SubInvoiceLine>,
}
impl SubInvoiceLine {
pub fn total(&self) -> Result<InvoiceAmount, crate::AmountError> {
if self.children.is_empty() {
return Ok(self.line.net_amount);
}
InvoiceAmount::checked_sum(
self.children
.iter()
.map(Self::total)
.collect::<Result<Vec<_>, _>>()?,
)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ThirdPartyPayment {
pub payment_type: Option<String>,
pub amount: Option<InvoiceAmount>,
pub description: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Percentage;
use crate::invoice::Code;
fn amount(s: &str) -> InvoiceAmount {
InvoiceAmount::parse(s).unwrap()
}
#[test]
fn an_advance_states_the_tax_it_contains() {
let a = AdvancePayment {
gross: amount("446.25"),
received_on: Some(Date::parse("2026-03-31").unwrap()),
tax: vec![VatBreakdown {
taxable_amount: amount("375.00"),
tax_amount: amount("71.25"),
category: Code::new("S"),
rate: Some(Percentage::new(rust_decimal::dec!(19))),
exemption_reason: None,
exemption_reason_code: None,
}],
reference: Some(DocumentReference::new("AB-1")),
reference_date: None,
};
assert_eq!(a.tax_total().unwrap(), amount("71.25"));
assert_eq!(
a.tax[0]
.taxable_amount
.checked_add(a.tax[0].tax_amount)
.unwrap(),
a.gross
);
}
#[test]
fn populated_names_only_what_is_there() {
assert!(Extensions::default().is_empty());
assert!(Extensions::default().populated().is_empty());
}
}