use std::collections::HashMap;
use chrono::NaiveDate;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use super::graph_properties::{GraphPropertyValue, ToNodeProperties};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum JurisdictionType {
#[default]
Federal,
State,
Local,
Municipal,
Supranational,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum TaxType {
#[default]
Vat,
Gst,
SalesTax,
IncomeTax,
WithholdingTax,
PayrollTax,
ExciseTax,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum TaxableDocumentType {
#[default]
VendorInvoice,
CustomerInvoice,
JournalEntry,
Payment,
PayrollRun,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum TaxReturnType {
#[default]
VatReturn,
IncomeTax,
WithholdingRemittance,
PayrollTax,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum TaxReturnStatus {
#[default]
Draft,
Filed,
Assessed,
Paid,
Amended,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum WithholdingType {
DividendWithholding,
RoyaltyWithholding,
#[default]
ServiceWithholding,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum TaxMeasurementMethod {
#[default]
MostLikelyAmount,
ExpectedValue,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaxJurisdiction {
pub id: String,
pub name: String,
pub country_code: String,
pub region_code: Option<String>,
pub jurisdiction_type: JurisdictionType,
pub parent_jurisdiction_id: Option<String>,
pub vat_registered: bool,
}
impl TaxJurisdiction {
pub fn new(
id: impl Into<String>,
name: impl Into<String>,
country_code: impl Into<String>,
jurisdiction_type: JurisdictionType,
) -> Self {
Self {
id: id.into(),
name: name.into(),
country_code: country_code.into(),
region_code: None,
jurisdiction_type,
parent_jurisdiction_id: None,
vat_registered: false,
}
}
pub fn with_region_code(mut self, region_code: impl Into<String>) -> Self {
self.region_code = Some(region_code.into());
self
}
pub fn with_parent_jurisdiction_id(mut self, parent_id: impl Into<String>) -> Self {
self.parent_jurisdiction_id = Some(parent_id.into());
self
}
pub fn with_vat_registered(mut self, registered: bool) -> Self {
self.vat_registered = registered;
self
}
pub fn is_subnational(&self) -> bool {
matches!(
self.jurisdiction_type,
JurisdictionType::State | JurisdictionType::Local | JurisdictionType::Municipal
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaxCode {
pub id: String,
pub code: String,
pub description: String,
pub tax_type: TaxType,
#[serde(with = "crate::serde_decimal")]
pub rate: Decimal,
pub jurisdiction_id: String,
pub effective_date: NaiveDate,
pub expiry_date: Option<NaiveDate>,
pub is_reverse_charge: bool,
pub is_exempt: bool,
}
impl TaxCode {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: impl Into<String>,
code: impl Into<String>,
description: impl Into<String>,
tax_type: TaxType,
rate: Decimal,
jurisdiction_id: impl Into<String>,
effective_date: NaiveDate,
) -> Self {
Self {
id: id.into(),
code: code.into(),
description: description.into(),
tax_type,
rate,
jurisdiction_id: jurisdiction_id.into(),
effective_date,
expiry_date: None,
is_reverse_charge: false,
is_exempt: false,
}
}
pub fn with_expiry_date(mut self, expiry: NaiveDate) -> Self {
self.expiry_date = Some(expiry);
self
}
pub fn with_reverse_charge(mut self, reverse_charge: bool) -> Self {
self.is_reverse_charge = reverse_charge;
self
}
pub fn with_exempt(mut self, exempt: bool) -> Self {
self.is_exempt = exempt;
self
}
pub fn tax_amount(&self, taxable_amount: Decimal) -> Decimal {
if self.is_exempt {
return Decimal::ZERO;
}
(taxable_amount * self.rate).round_dp(2)
}
pub fn is_active(&self, date: NaiveDate) -> bool {
if date < self.effective_date {
return false;
}
match self.expiry_date {
Some(expiry) => date < expiry,
None => true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaxLine {
pub id: String,
pub document_type: TaxableDocumentType,
pub document_id: String,
pub line_number: u32,
pub tax_code_id: String,
pub jurisdiction_id: String,
#[serde(with = "crate::serde_decimal")]
pub taxable_amount: Decimal,
#[serde(with = "crate::serde_decimal")]
pub tax_amount: Decimal,
pub is_deductible: bool,
pub is_reverse_charge: bool,
pub is_self_assessed: bool,
}
impl TaxLine {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: impl Into<String>,
document_type: TaxableDocumentType,
document_id: impl Into<String>,
line_number: u32,
tax_code_id: impl Into<String>,
jurisdiction_id: impl Into<String>,
taxable_amount: Decimal,
tax_amount: Decimal,
) -> Self {
Self {
id: id.into(),
document_type,
document_id: document_id.into(),
line_number,
tax_code_id: tax_code_id.into(),
jurisdiction_id: jurisdiction_id.into(),
taxable_amount,
tax_amount,
is_deductible: true,
is_reverse_charge: false,
is_self_assessed: false,
}
}
pub fn with_deductible(mut self, deductible: bool) -> Self {
self.is_deductible = deductible;
self
}
pub fn with_reverse_charge(mut self, reverse_charge: bool) -> Self {
self.is_reverse_charge = reverse_charge;
self
}
pub fn with_self_assessed(mut self, self_assessed: bool) -> Self {
self.is_self_assessed = self_assessed;
self
}
pub fn effective_rate(&self) -> Decimal {
if self.taxable_amount.is_zero() {
Decimal::ZERO
} else {
(self.tax_amount / self.taxable_amount).round_dp(6)
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaxReturn {
pub id: String,
pub entity_id: String,
pub jurisdiction_id: String,
pub period_start: NaiveDate,
pub period_end: NaiveDate,
pub return_type: TaxReturnType,
pub status: TaxReturnStatus,
#[serde(with = "crate::serde_decimal")]
pub total_output_tax: Decimal,
#[serde(with = "crate::serde_decimal")]
pub total_input_tax: Decimal,
#[serde(with = "crate::serde_decimal")]
pub net_payable: Decimal,
pub filing_deadline: NaiveDate,
pub actual_filing_date: Option<NaiveDate>,
pub is_late: bool,
}
impl TaxReturn {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: impl Into<String>,
entity_id: impl Into<String>,
jurisdiction_id: impl Into<String>,
period_start: NaiveDate,
period_end: NaiveDate,
return_type: TaxReturnType,
total_output_tax: Decimal,
total_input_tax: Decimal,
filing_deadline: NaiveDate,
) -> Self {
let net_payable = (total_output_tax - total_input_tax).round_dp(2);
Self {
id: id.into(),
entity_id: entity_id.into(),
jurisdiction_id: jurisdiction_id.into(),
period_start,
period_end,
return_type,
status: TaxReturnStatus::Draft,
total_output_tax,
total_input_tax,
net_payable,
filing_deadline,
actual_filing_date: None,
is_late: false,
}
}
pub fn with_filing(mut self, filing_date: NaiveDate) -> Self {
self.actual_filing_date = Some(filing_date);
self.is_late = filing_date > self.filing_deadline;
self.status = TaxReturnStatus::Filed;
self
}
pub fn with_status(mut self, status: TaxReturnStatus) -> Self {
self.status = status;
self
}
pub fn is_filed(&self) -> bool {
matches!(
self.status,
TaxReturnStatus::Filed | TaxReturnStatus::Assessed | TaxReturnStatus::Paid
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateReconciliationItem {
pub description: String,
#[serde(with = "crate::serde_decimal")]
pub rate_impact: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaxProvision {
pub id: String,
pub entity_id: String,
pub period: NaiveDate,
#[serde(with = "crate::serde_decimal")]
pub current_tax_expense: Decimal,
#[serde(with = "crate::serde_decimal")]
pub deferred_tax_asset: Decimal,
#[serde(with = "crate::serde_decimal")]
pub deferred_tax_liability: Decimal,
#[serde(with = "crate::serde_decimal")]
pub statutory_rate: Decimal,
#[serde(with = "crate::serde_decimal")]
pub effective_rate: Decimal,
pub rate_reconciliation: Vec<RateReconciliationItem>,
}
impl TaxProvision {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: impl Into<String>,
entity_id: impl Into<String>,
period: NaiveDate,
current_tax_expense: Decimal,
deferred_tax_asset: Decimal,
deferred_tax_liability: Decimal,
statutory_rate: Decimal,
effective_rate: Decimal,
) -> Self {
Self {
id: id.into(),
entity_id: entity_id.into(),
period,
current_tax_expense,
deferred_tax_asset,
deferred_tax_liability,
statutory_rate,
effective_rate,
rate_reconciliation: Vec::new(),
}
}
pub fn with_reconciliation_item(
mut self,
description: impl Into<String>,
rate_impact: Decimal,
) -> Self {
self.rate_reconciliation.push(RateReconciliationItem {
description: description.into(),
rate_impact,
});
self
}
pub fn net_deferred_tax(&self) -> Decimal {
self.deferred_tax_asset - self.deferred_tax_liability
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UncertainTaxPosition {
pub id: String,
pub entity_id: String,
pub description: String,
#[serde(with = "crate::serde_decimal")]
pub tax_benefit: Decimal,
#[serde(with = "crate::serde_decimal")]
pub recognition_threshold: Decimal,
#[serde(with = "crate::serde_decimal")]
pub recognized_amount: Decimal,
pub measurement_method: TaxMeasurementMethod,
}
impl UncertainTaxPosition {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: impl Into<String>,
entity_id: impl Into<String>,
description: impl Into<String>,
tax_benefit: Decimal,
recognition_threshold: Decimal,
recognized_amount: Decimal,
measurement_method: TaxMeasurementMethod,
) -> Self {
Self {
id: id.into(),
entity_id: entity_id.into(),
description: description.into(),
tax_benefit,
recognition_threshold,
recognized_amount,
measurement_method,
}
}
pub fn unrecognized_amount(&self) -> Decimal {
self.tax_benefit - self.recognized_amount
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WithholdingTaxRecord {
pub id: String,
pub payment_id: String,
pub vendor_id: String,
pub withholding_type: WithholdingType,
#[serde(default, with = "crate::serde_decimal::option")]
pub treaty_rate: Option<Decimal>,
#[serde(with = "crate::serde_decimal")]
pub statutory_rate: Decimal,
#[serde(with = "crate::serde_decimal")]
pub applied_rate: Decimal,
#[serde(with = "crate::serde_decimal")]
pub base_amount: Decimal,
#[serde(with = "crate::serde_decimal")]
pub withheld_amount: Decimal,
pub certificate_number: Option<String>,
}
impl WithholdingTaxRecord {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: impl Into<String>,
payment_id: impl Into<String>,
vendor_id: impl Into<String>,
withholding_type: WithholdingType,
statutory_rate: Decimal,
applied_rate: Decimal,
base_amount: Decimal,
) -> Self {
let withheld_amount = (base_amount * applied_rate).round_dp(2);
Self {
id: id.into(),
payment_id: payment_id.into(),
vendor_id: vendor_id.into(),
withholding_type,
treaty_rate: None,
statutory_rate,
applied_rate,
base_amount,
withheld_amount,
certificate_number: None,
}
}
pub fn with_treaty_rate(mut self, rate: Decimal) -> Self {
self.treaty_rate = Some(rate);
self
}
pub fn with_certificate_number(mut self, number: impl Into<String>) -> Self {
self.certificate_number = Some(number.into());
self
}
pub fn has_treaty_benefit(&self) -> bool {
self.treaty_rate.is_some() && self.applied_rate < self.statutory_rate
}
pub fn treaty_savings(&self) -> Decimal {
((self.statutory_rate - self.applied_rate) * self.base_amount).round_dp(2)
}
}
impl ToNodeProperties for TaxJurisdiction {
fn node_type_name(&self) -> &'static str {
"tax_jurisdiction"
}
fn node_type_code(&self) -> u16 {
410
}
fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
let mut p = HashMap::new();
p.insert("code".into(), GraphPropertyValue::String(self.id.clone()));
p.insert("name".into(), GraphPropertyValue::String(self.name.clone()));
p.insert(
"country".into(),
GraphPropertyValue::String(self.country_code.clone()),
);
if let Some(ref rc) = self.region_code {
p.insert("region".into(), GraphPropertyValue::String(rc.clone()));
}
p.insert(
"jurisdictionType".into(),
GraphPropertyValue::String(format!("{:?}", self.jurisdiction_type)),
);
p.insert(
"isActive".into(),
GraphPropertyValue::Bool(self.vat_registered),
);
p
}
}
impl ToNodeProperties for TaxCode {
fn node_type_name(&self) -> &'static str {
"tax_code"
}
fn node_type_code(&self) -> u16 {
411
}
fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
let mut p = HashMap::new();
p.insert("code".into(), GraphPropertyValue::String(self.code.clone()));
p.insert(
"description".into(),
GraphPropertyValue::String(self.description.clone()),
);
p.insert("rate".into(), GraphPropertyValue::Decimal(self.rate));
p.insert(
"taxType".into(),
GraphPropertyValue::String(format!("{:?}", self.tax_type)),
);
p.insert(
"jurisdiction".into(),
GraphPropertyValue::String(self.jurisdiction_id.clone()),
);
p.insert("isActive".into(), GraphPropertyValue::Bool(!self.is_exempt));
p.insert(
"isReverseCharge".into(),
GraphPropertyValue::Bool(self.is_reverse_charge),
);
p
}
}
impl ToNodeProperties for TaxLine {
fn node_type_name(&self) -> &'static str {
"tax_line"
}
fn node_type_code(&self) -> u16 {
412
}
fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
let mut p = HashMap::new();
p.insert(
"returnId".into(),
GraphPropertyValue::String(self.document_id.clone()),
);
p.insert(
"lineNumber".into(),
GraphPropertyValue::Int(self.line_number as i64),
);
p.insert(
"description".into(),
GraphPropertyValue::String(format!("{:?}", self.document_type)),
);
p.insert(
"amount".into(),
GraphPropertyValue::Decimal(self.tax_amount),
);
p.insert(
"taxableAmount".into(),
GraphPropertyValue::Decimal(self.taxable_amount),
);
p.insert(
"taxCode".into(),
GraphPropertyValue::String(self.tax_code_id.clone()),
);
p.insert(
"jurisdiction".into(),
GraphPropertyValue::String(self.jurisdiction_id.clone()),
);
p.insert(
"isDeductible".into(),
GraphPropertyValue::Bool(self.is_deductible),
);
p
}
}
impl ToNodeProperties for TaxReturn {
fn node_type_name(&self) -> &'static str {
"tax_return"
}
fn node_type_code(&self) -> u16 {
413
}
fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
let mut p = HashMap::new();
p.insert(
"entityCode".into(),
GraphPropertyValue::String(self.entity_id.clone()),
);
p.insert(
"period".into(),
GraphPropertyValue::String(format!("{}..{}", self.period_start, self.period_end)),
);
p.insert(
"jurisdiction".into(),
GraphPropertyValue::String(self.jurisdiction_id.clone()),
);
p.insert(
"filingType".into(),
GraphPropertyValue::String(format!("{:?}", self.return_type)),
);
p.insert(
"status".into(),
GraphPropertyValue::String(format!("{:?}", self.status)),
);
p.insert(
"totalTax".into(),
GraphPropertyValue::Decimal(self.total_output_tax),
);
p.insert(
"taxPaid".into(),
GraphPropertyValue::Decimal(self.total_input_tax),
);
p.insert(
"balanceDue".into(),
GraphPropertyValue::Decimal(self.net_payable),
);
p.insert(
"dueDate".into(),
GraphPropertyValue::Date(self.filing_deadline),
);
p.insert("isLate".into(), GraphPropertyValue::Bool(self.is_late));
p
}
}
impl ToNodeProperties for TaxProvision {
fn node_type_name(&self) -> &'static str {
"tax_provision"
}
fn node_type_code(&self) -> u16 {
414
}
fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
let mut p = HashMap::new();
p.insert(
"entityCode".into(),
GraphPropertyValue::String(self.entity_id.clone()),
);
p.insert("period".into(), GraphPropertyValue::Date(self.period));
p.insert(
"totalProvision".into(),
GraphPropertyValue::Decimal(self.current_tax_expense),
);
p.insert(
"deferredAsset".into(),
GraphPropertyValue::Decimal(self.deferred_tax_asset),
);
p.insert(
"deferredLiability".into(),
GraphPropertyValue::Decimal(self.deferred_tax_liability),
);
p.insert(
"statutoryRate".into(),
GraphPropertyValue::Decimal(self.statutory_rate),
);
p.insert(
"effectiveRate".into(),
GraphPropertyValue::Decimal(self.effective_rate),
);
p
}
}
impl ToNodeProperties for WithholdingTaxRecord {
fn node_type_name(&self) -> &'static str {
"withholding_tax_record"
}
fn node_type_code(&self) -> u16 {
415
}
fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
let mut p = HashMap::new();
p.insert(
"paymentId".into(),
GraphPropertyValue::String(self.payment_id.clone()),
);
p.insert(
"vendorId".into(),
GraphPropertyValue::String(self.vendor_id.clone()),
);
p.insert(
"taxCode".into(),
GraphPropertyValue::String(format!("{:?}", self.withholding_type)),
);
p.insert(
"grossAmount".into(),
GraphPropertyValue::Decimal(self.base_amount),
);
p.insert(
"withholdingRate".into(),
GraphPropertyValue::Decimal(self.applied_rate),
);
p.insert(
"withholdingAmount".into(),
GraphPropertyValue::Decimal(self.withheld_amount),
);
p.insert(
"treatyApplied".into(),
GraphPropertyValue::Bool(self.treaty_rate.is_some()),
);
if let Some(ref cn) = self.certificate_number {
p.insert(
"certificateNumber".into(),
GraphPropertyValue::String(cn.clone()),
);
}
p
}
}
impl ToNodeProperties for UncertainTaxPosition {
fn node_type_name(&self) -> &'static str {
"uncertain_tax_position"
}
fn node_type_code(&self) -> u16 {
416
}
fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
let mut p = HashMap::new();
p.insert(
"entityCode".into(),
GraphPropertyValue::String(self.entity_id.clone()),
);
p.insert(
"description".into(),
GraphPropertyValue::String(self.description.clone()),
);
p.insert(
"amount".into(),
GraphPropertyValue::Decimal(self.tax_benefit),
);
p.insert(
"probability".into(),
GraphPropertyValue::Decimal(self.recognition_threshold),
);
p.insert(
"reserveAmount".into(),
GraphPropertyValue::Decimal(self.recognized_amount),
);
p.insert(
"measurementMethod".into(),
GraphPropertyValue::String(format!("{:?}", self.measurement_method)),
);
p
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_tax_code_creation() {
let code = TaxCode::new(
"TC-001",
"VAT-STD-20",
"Standard VAT 20%",
TaxType::Vat,
dec!(0.20),
"JUR-UK",
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
)
.with_expiry_date(NaiveDate::from_ymd_opt(2025, 12, 31).unwrap());
assert_eq!(code.tax_amount(dec!(1000.00)), dec!(200.00));
assert_eq!(code.tax_amount(dec!(0)), dec!(0.00));
assert!(code.is_active(NaiveDate::from_ymd_opt(2024, 6, 15).unwrap()));
assert!(!code.is_active(NaiveDate::from_ymd_opt(2023, 12, 31).unwrap()));
assert!(!code.is_active(NaiveDate::from_ymd_opt(2025, 12, 31).unwrap()));
assert!(!code.is_active(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap()));
}
#[test]
fn test_tax_code_exempt() {
let code = TaxCode::new(
"TC-002",
"VAT-EX",
"VAT Exempt",
TaxType::Vat,
dec!(0.20),
"JUR-UK",
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
)
.with_exempt(true);
assert_eq!(code.tax_amount(dec!(5000.00)), dec!(0));
}
#[test]
fn test_tax_line_creation() {
let line = TaxLine::new(
"TL-001",
TaxableDocumentType::VendorInvoice,
"INV-001",
1,
"TC-001",
"JUR-UK",
dec!(1000.00),
dec!(200.00),
);
assert_eq!(line.effective_rate(), dec!(0.200000));
let zero_line = TaxLine::new(
"TL-002",
TaxableDocumentType::VendorInvoice,
"INV-002",
1,
"TC-001",
"JUR-UK",
dec!(0),
dec!(0),
);
assert_eq!(zero_line.effective_rate(), dec!(0));
}
#[test]
fn test_tax_return_net_payable() {
let ret = TaxReturn::new(
"TR-001",
"ENT-001",
"JUR-UK",
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
TaxReturnType::VatReturn,
dec!(50000),
dec!(30000),
NaiveDate::from_ymd_opt(2024, 4, 30).unwrap(),
);
assert!(!ret.is_filed());
assert_eq!(ret.net_payable, dec!(20000));
let filed = ret.with_filing(NaiveDate::from_ymd_opt(2024, 4, 15).unwrap());
assert!(filed.is_filed());
assert!(!filed.is_late);
let assessed = TaxReturn::new(
"TR-002",
"ENT-001",
"JUR-UK",
NaiveDate::from_ymd_opt(2024, 4, 1).unwrap(),
NaiveDate::from_ymd_opt(2024, 6, 30).unwrap(),
TaxReturnType::VatReturn,
dec!(60000),
dec!(40000),
NaiveDate::from_ymd_opt(2024, 7, 31).unwrap(),
)
.with_status(TaxReturnStatus::Assessed);
assert!(assessed.is_filed());
let paid = TaxReturn::new(
"TR-003",
"ENT-001",
"JUR-UK",
NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(),
NaiveDate::from_ymd_opt(2024, 9, 30).unwrap(),
TaxReturnType::IncomeTax,
dec!(100000),
dec!(0),
NaiveDate::from_ymd_opt(2024, 10, 31).unwrap(),
)
.with_status(TaxReturnStatus::Paid);
assert!(paid.is_filed());
let amended = TaxReturn::new(
"TR-004",
"ENT-001",
"JUR-UK",
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
TaxReturnType::VatReturn,
dec!(50000),
dec!(30000),
NaiveDate::from_ymd_opt(2024, 4, 30).unwrap(),
)
.with_status(TaxReturnStatus::Amended);
assert!(!amended.is_filed());
}
#[test]
fn test_tax_provision() {
let provision = TaxProvision::new(
"TP-001",
"ENT-001",
NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
dec!(250000),
dec!(80000),
dec!(120000),
dec!(0.21),
dec!(0.245),
)
.with_reconciliation_item("State taxes", dec!(0.03))
.with_reconciliation_item("R&D credits", dec!(-0.015));
assert_eq!(provision.net_deferred_tax(), dec!(-40000));
assert_eq!(provision.rate_reconciliation.len(), 2);
}
#[test]
fn test_withholding_tax_record() {
let wht = WithholdingTaxRecord::new(
"WHT-001",
"PAY-001",
"V-100",
WithholdingType::RoyaltyWithholding,
dec!(0.30), dec!(0.10), dec!(100000), )
.with_treaty_rate(dec!(0.10))
.with_certificate_number("CERT-2024-001");
assert!(wht.has_treaty_benefit());
assert_eq!(wht.treaty_savings(), dec!(20000.00));
assert_eq!(wht.withheld_amount, dec!(10000.00));
assert_eq!(wht.certificate_number, Some("CERT-2024-001".to_string()));
}
#[test]
fn test_withholding_no_treaty() {
let wht = WithholdingTaxRecord::new(
"WHT-002",
"PAY-002",
"V-200",
WithholdingType::ServiceWithholding,
dec!(0.25),
dec!(0.25),
dec!(50000),
);
assert!(!wht.has_treaty_benefit());
assert_eq!(wht.treaty_savings(), dec!(0.00));
}
#[test]
fn test_uncertain_tax_position() {
let utp = UncertainTaxPosition::new(
"UTP-001",
"ENT-001",
"R&D credit claim for software development",
dec!(500000), dec!(0.50), dec!(350000), TaxMeasurementMethod::MostLikelyAmount,
);
assert_eq!(utp.unrecognized_amount(), dec!(150000));
}
#[test]
fn test_jurisdiction_hierarchy() {
let federal = TaxJurisdiction::new(
"JUR-US",
"United States - Federal",
"US",
JurisdictionType::Federal,
);
assert!(!federal.is_subnational());
let state = TaxJurisdiction::new("JUR-US-CA", "California", "US", JurisdictionType::State)
.with_region_code("CA")
.with_parent_jurisdiction_id("JUR-US");
assert!(state.is_subnational());
assert_eq!(state.region_code, Some("CA".to_string()));
assert_eq!(state.parent_jurisdiction_id, Some("JUR-US".to_string()));
let local = TaxJurisdiction::new(
"JUR-US-CA-SF",
"San Francisco",
"US",
JurisdictionType::Local,
)
.with_parent_jurisdiction_id("JUR-US-CA");
assert!(local.is_subnational());
let municipal = TaxJurisdiction::new(
"JUR-US-NY-NYC",
"New York City",
"US",
JurisdictionType::Municipal,
)
.with_parent_jurisdiction_id("JUR-US-NY");
assert!(municipal.is_subnational());
let supra = TaxJurisdiction::new(
"JUR-EU",
"European Union",
"EU",
JurisdictionType::Supranational,
);
assert!(!supra.is_subnational());
}
#[test]
fn test_serde_roundtrip() {
let code = TaxCode::new(
"TC-SERDE",
"VAT-STD-20",
"Standard VAT 20%",
TaxType::Vat,
dec!(0.20),
"JUR-UK",
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
)
.with_expiry_date(NaiveDate::from_ymd_opt(2025, 12, 31).unwrap())
.with_reverse_charge(true);
let json = serde_json::to_string_pretty(&code).unwrap();
let deserialized: TaxCode = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.id, code.id);
assert_eq!(deserialized.code, code.code);
assert_eq!(deserialized.rate, code.rate);
assert_eq!(deserialized.tax_type, code.tax_type);
assert_eq!(deserialized.is_reverse_charge, code.is_reverse_charge);
assert_eq!(deserialized.effective_date, code.effective_date);
assert_eq!(deserialized.expiry_date, code.expiry_date);
}
#[test]
fn test_withholding_serde_roundtrip() {
let wht = WithholdingTaxRecord::new(
"WHT-SERDE-1",
"PAY-001",
"V-001",
WithholdingType::RoyaltyWithholding,
dec!(0.30),
dec!(0.15),
dec!(50000),
)
.with_treaty_rate(dec!(0.10));
let json = serde_json::to_string_pretty(&wht).unwrap();
let deserialized: WithholdingTaxRecord = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.treaty_rate, Some(dec!(0.10)));
assert_eq!(deserialized.statutory_rate, dec!(0.30));
assert_eq!(deserialized.applied_rate, dec!(0.15));
assert_eq!(deserialized.base_amount, dec!(50000));
assert_eq!(deserialized.withheld_amount, wht.withheld_amount);
let wht_no_treaty = WithholdingTaxRecord::new(
"WHT-SERDE-2",
"PAY-002",
"V-002",
WithholdingType::ServiceWithholding,
dec!(0.30),
dec!(0.30),
dec!(10000),
);
let json2 = serde_json::to_string_pretty(&wht_no_treaty).unwrap();
let deserialized2: WithholdingTaxRecord = serde_json::from_str(&json2).unwrap();
assert_eq!(deserialized2.treaty_rate, None);
assert_eq!(deserialized2.statutory_rate, dec!(0.30));
}
}