use chrono::{DateTime, NaiveDate, Utc};
use rust_decimal::{Decimal, RoundingStrategy};
use serde::{Deserialize, Serialize};
use stateset_primitives::{CurrencyCode, ProductId};
use strum::{Display, EnumString};
use uuid::Uuid;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString, Serialize, Deserialize, Default,
)]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum TaxType {
#[default]
SalesTax,
Vat,
Gst,
Hst,
Pst,
Qst,
ConsumptionTax,
Custom,
}
impl TaxType {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::SalesTax => "sales_tax",
Self::Vat => "vat",
Self::Gst => "gst",
Self::Hst => "hst",
Self::Pst => "pst",
Self::Qst => "qst",
Self::ConsumptionTax => "consumption_tax",
Self::Custom => "custom",
}
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize, Default,
)]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum TaxCalculationMethod {
#[default]
Exclusive,
Inclusive,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum TaxCompoundMethod {
#[default]
Combined,
Compound,
Separate,
}
impl std::fmt::Display for TaxCompoundMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Combined => f.write_str("combined"),
Self::Compound => f.write_str("compound"),
Self::Separate => f.write_str("separate"),
}
}
}
impl std::str::FromStr for TaxCompoundMethod {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"combined" => Ok(Self::Combined),
"compound" => Ok(Self::Compound),
"separate" => Ok(Self::Separate),
_ => Err(format!("Unknown tax compound method: {s}")),
}
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize, Default,
)]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ProductTaxCategory {
#[default]
Standard,
Reduced,
SuperReduced,
ZeroRated,
Exempt,
Digital,
Clothing,
Food,
PreparedFood,
Medical,
Educational,
Luxury,
}
impl ProductTaxCategory {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Standard => "standard",
Self::Reduced => "reduced",
Self::SuperReduced => "super_reduced",
Self::ZeroRated => "zero_rated",
Self::Exempt => "exempt",
Self::Digital => "digital",
Self::Clothing => "clothing",
Self::Food => "food",
Self::PreparedFood => "prepared_food",
Self::Medical => "medical",
Self::Educational => "educational",
Self::Luxury => "luxury",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ExemptionType {
Resale,
NonProfit,
Government,
Educational,
Religious,
Medical,
Manufacturing,
Agricultural,
Export,
Diplomatic,
Other,
}
impl std::fmt::Display for ExemptionType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Resale => f.write_str("resale"),
Self::NonProfit => f.write_str("non_profit"),
Self::Government => f.write_str("government"),
Self::Educational => f.write_str("educational"),
Self::Religious => f.write_str("religious"),
Self::Medical => f.write_str("medical"),
Self::Manufacturing => f.write_str("manufacturing"),
Self::Agricultural => f.write_str("agricultural"),
Self::Export => f.write_str("export"),
Self::Diplomatic => f.write_str("diplomatic"),
Self::Other => f.write_str("other"),
}
}
}
impl std::str::FromStr for ExemptionType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"resale" => Ok(Self::Resale),
"non_profit" | "nonprofit" | "non-profit" => Ok(Self::NonProfit),
"government" => Ok(Self::Government),
"educational" => Ok(Self::Educational),
"religious" => Ok(Self::Religious),
"medical" => Ok(Self::Medical),
"manufacturing" => Ok(Self::Manufacturing),
"agricultural" => Ok(Self::Agricultural),
"export" => Ok(Self::Export),
"diplomatic" => Ok(Self::Diplomatic),
"other" => Ok(Self::Other),
_ => Err(format!("Unknown exemption type: {s}")),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaxJurisdiction {
pub id: Uuid,
pub parent_id: Option<Uuid>,
pub name: String,
pub code: String,
pub level: JurisdictionLevel,
pub country_code: String,
pub state_code: Option<String>,
pub county: Option<String>,
pub city: Option<String>,
pub postal_codes: Vec<String>,
pub active: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum JurisdictionLevel {
#[default]
Country,
State,
County,
City,
District,
Special,
}
impl std::fmt::Display for JurisdictionLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Country => f.write_str("country"),
Self::State => f.write_str("state"),
Self::County => f.write_str("county"),
Self::City => f.write_str("city"),
Self::District => f.write_str("district"),
Self::Special => f.write_str("special"),
}
}
}
impl std::str::FromStr for JurisdictionLevel {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"country" => Ok(Self::Country),
"state" => Ok(Self::State),
"county" => Ok(Self::County),
"city" => Ok(Self::City),
"district" => Ok(Self::District),
"special" => Ok(Self::Special),
_ => Err(format!("Unknown jurisdiction level: {s}")),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaxRate {
pub id: Uuid,
pub jurisdiction_id: Uuid,
pub tax_type: TaxType,
pub product_category: ProductTaxCategory,
pub rate: Decimal,
pub name: String,
pub description: Option<String>,
pub is_compound: bool,
pub priority: i32,
pub threshold_min: Option<Decimal>,
pub threshold_max: Option<Decimal>,
pub fixed_amount: Option<Decimal>,
pub effective_from: NaiveDate,
pub effective_to: Option<NaiveDate>,
pub active: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaxExemption {
pub id: Uuid,
pub customer_id: Uuid,
pub exemption_type: ExemptionType,
pub certificate_number: Option<String>,
pub issuing_authority: Option<String>,
pub jurisdiction_ids: Vec<Uuid>,
pub exempt_categories: Vec<ProductTaxCategory>,
pub effective_from: NaiveDate,
pub expires_at: Option<NaiveDate>,
pub verified: bool,
pub verified_at: Option<DateTime<Utc>>,
pub notes: Option<String>,
pub active: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TaxCalculationRequest {
pub line_items: Vec<TaxLineItem>,
pub shipping_address: TaxAddress,
pub billing_address: Option<TaxAddress>,
pub customer_id: Option<Uuid>,
pub shipping_amount: Option<Decimal>,
#[serde(default = "default_currency")]
pub currency: CurrencyCode,
pub transaction_date: Option<NaiveDate>,
pub prices_include_tax: bool,
}
const fn default_currency() -> CurrencyCode {
CurrencyCode::USD
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaxLineItem {
pub id: String,
pub sku: Option<String>,
pub product_id: Option<ProductId>,
pub quantity: Decimal,
pub unit_price: Decimal,
pub discount_amount: Decimal,
pub tax_category: ProductTaxCategory,
pub tax_code: Option<String>,
pub description: Option<String>,
}
impl Default for TaxLineItem {
fn default() -> Self {
Self {
id: String::new(),
sku: None,
product_id: None,
quantity: Decimal::ONE,
unit_price: Decimal::ZERO,
discount_amount: Decimal::ZERO,
tax_category: ProductTaxCategory::Standard,
tax_code: None,
description: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TaxAddress {
pub line1: Option<String>,
pub line2: Option<String>,
pub city: Option<String>,
pub state: Option<String>,
pub postal_code: Option<String>,
pub country: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaxCalculationResult {
pub id: Uuid,
pub total_tax: Decimal,
pub subtotal: Decimal,
pub total: Decimal,
pub shipping_tax: Decimal,
pub tax_breakdown: Vec<TaxBreakdown>,
pub line_item_taxes: Vec<LineItemTax>,
pub exemptions_applied: bool,
pub exemption_details: Option<ExemptionDetails>,
pub jurisdictions: Vec<JurisdictionSummary>,
pub calculated_at: DateTime<Utc>,
pub is_estimate: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaxBreakdown {
pub jurisdiction_id: Uuid,
pub jurisdiction_name: String,
pub tax_type: TaxType,
pub rate_name: String,
pub rate: Decimal,
pub taxable_amount: Decimal,
pub tax_amount: Decimal,
pub is_compound: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LineItemTax {
pub line_item_id: String,
pub taxable_amount: Decimal,
pub tax_amount: Decimal,
pub effective_rate: Decimal,
pub is_exempt: bool,
pub exemption_reason: Option<String>,
pub tax_details: Vec<TaxDetail>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaxDetail {
pub tax_type: TaxType,
pub jurisdiction_name: String,
pub rate: Decimal,
pub amount: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExemptionDetails {
pub exemption_id: Uuid,
pub exemption_type: ExemptionType,
pub certificate_number: Option<String>,
pub amount_exempt: Decimal,
pub tax_saved: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JurisdictionSummary {
pub id: Uuid,
pub name: String,
pub code: String,
pub level: JurisdictionLevel,
pub total_rate: Decimal,
pub total_tax: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaxSettings {
pub id: Uuid,
pub enabled: bool,
pub calculation_method: TaxCalculationMethod,
pub compound_method: TaxCompoundMethod,
pub tax_shipping: bool,
pub tax_handling: bool,
pub tax_gift_wrap: bool,
pub origin_address: Option<TaxAddress>,
pub default_product_category: ProductTaxCategory,
pub rounding_mode: String,
pub decimal_places: i32,
pub validate_addresses: bool,
pub tax_provider: Option<String>,
pub provider_credentials: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Default for TaxSettings {
fn default() -> Self {
Self {
id: Uuid::new_v4(),
enabled: true,
calculation_method: TaxCalculationMethod::Exclusive,
compound_method: TaxCompoundMethod::Combined,
tax_shipping: true,
tax_handling: true,
tax_gift_wrap: true,
origin_address: None,
default_product_category: ProductTaxCategory::Standard,
rounding_mode: "half_up".to_string(),
decimal_places: 2,
validate_addresses: false,
tax_provider: None,
provider_credentials: None,
created_at: Utc::now(),
updated_at: Utc::now(),
}
}
}
impl TaxSettings {
#[must_use]
pub fn rounding_strategy(&self) -> RoundingStrategy {
match self.rounding_mode.trim().to_ascii_lowercase().as_str() {
"half_even" | "half_to_even" | "bankers" | "banker" => {
RoundingStrategy::MidpointNearestEven
}
"half_down" => RoundingStrategy::MidpointTowardZero,
"up" | "away_from_zero" => RoundingStrategy::AwayFromZero,
"down" | "truncate" | "toward_zero" => RoundingStrategy::ToZero,
"ceil" | "ceiling" => RoundingStrategy::ToPositiveInfinity,
"floor" => RoundingStrategy::ToNegativeInfinity,
_ => RoundingStrategy::MidpointAwayFromZero,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CreateTaxJurisdiction {
pub parent_id: Option<Uuid>,
pub name: String,
pub code: String,
pub level: JurisdictionLevel,
pub country_code: String,
pub state_code: Option<String>,
pub county: Option<String>,
pub city: Option<String>,
pub postal_codes: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateTaxRate {
pub jurisdiction_id: Uuid,
pub tax_type: TaxType,
pub product_category: ProductTaxCategory,
pub rate: Decimal,
pub name: String,
pub description: Option<String>,
pub is_compound: bool,
pub priority: i32,
pub threshold_min: Option<Decimal>,
pub threshold_max: Option<Decimal>,
pub fixed_amount: Option<Decimal>,
pub effective_from: NaiveDate,
pub effective_to: Option<NaiveDate>,
}
impl Default for CreateTaxRate {
fn default() -> Self {
Self {
jurisdiction_id: Uuid::nil(),
tax_type: TaxType::SalesTax,
product_category: ProductTaxCategory::Standard,
rate: Decimal::ZERO,
name: String::new(),
description: None,
is_compound: false,
priority: 0,
threshold_min: None,
threshold_max: None,
fixed_amount: None,
effective_from: Utc::now().date_naive(),
effective_to: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateTaxExemption {
pub customer_id: Uuid,
pub exemption_type: ExemptionType,
pub certificate_number: Option<String>,
pub issuing_authority: Option<String>,
pub jurisdiction_ids: Vec<Uuid>,
pub exempt_categories: Vec<ProductTaxCategory>,
pub effective_from: NaiveDate,
pub expires_at: Option<NaiveDate>,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TaxRateFilter {
pub jurisdiction_id: Option<Uuid>,
pub tax_type: Option<TaxType>,
pub product_category: Option<ProductTaxCategory>,
pub active_only: bool,
pub effective_date: Option<NaiveDate>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TaxJurisdictionFilter {
pub country_code: Option<String>,
pub state_code: Option<String>,
pub level: Option<JurisdictionLevel>,
pub active_only: bool,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UsStateTaxInfo {
pub state_code: String,
pub state_name: String,
pub state_rate: Decimal,
pub has_local_taxes: bool,
pub origin_based: bool,
pub tax_shipping: bool,
pub tax_clothing: bool,
pub tax_food: bool,
pub tax_digital: bool,
}
#[must_use]
pub fn get_us_state_tax_info(state_code: &str) -> Option<UsStateTaxInfo> {
match state_code.to_uppercase().as_str() {
"AL" => Some(UsStateTaxInfo {
state_code: "AL".into(),
state_name: "Alabama".into(),
state_rate: Decimal::new(4, 2), has_local_taxes: true,
origin_based: false,
tax_shipping: true,
tax_clothing: true,
tax_food: true,
tax_digital: true,
}),
"AK" => Some(UsStateTaxInfo {
state_code: "AK".into(),
state_name: "Alaska".into(),
state_rate: Decimal::ZERO, has_local_taxes: true,
origin_based: false,
tax_shipping: false,
tax_clothing: false,
tax_food: false,
tax_digital: false,
}),
"AZ" => Some(UsStateTaxInfo {
state_code: "AZ".into(),
state_name: "Arizona".into(),
state_rate: Decimal::new(56, 3), has_local_taxes: true,
origin_based: true,
tax_shipping: true,
tax_clothing: true,
tax_food: false,
tax_digital: true,
}),
"CA" => Some(UsStateTaxInfo {
state_code: "CA".into(),
state_name: "California".into(),
state_rate: Decimal::new(725, 4), has_local_taxes: true,
origin_based: true,
tax_shipping: false,
tax_clothing: true,
tax_food: false,
tax_digital: false,
}),
"CO" => Some(UsStateTaxInfo {
state_code: "CO".into(),
state_name: "Colorado".into(),
state_rate: Decimal::new(29, 3), has_local_taxes: true,
origin_based: false,
tax_shipping: true,
tax_clothing: true,
tax_food: false,
tax_digital: true,
}),
"DE" => Some(UsStateTaxInfo {
state_code: "DE".into(),
state_name: "Delaware".into(),
state_rate: Decimal::ZERO, has_local_taxes: false,
origin_based: false,
tax_shipping: false,
tax_clothing: false,
tax_food: false,
tax_digital: false,
}),
"FL" => Some(UsStateTaxInfo {
state_code: "FL".into(),
state_name: "Florida".into(),
state_rate: Decimal::new(6, 2), has_local_taxes: true,
origin_based: false,
tax_shipping: true,
tax_clothing: true,
tax_food: false,
tax_digital: true,
}),
"MT" => Some(UsStateTaxInfo {
state_code: "MT".into(),
state_name: "Montana".into(),
state_rate: Decimal::ZERO, has_local_taxes: false,
origin_based: false,
tax_shipping: false,
tax_clothing: false,
tax_food: false,
tax_digital: false,
}),
"NH" => Some(UsStateTaxInfo {
state_code: "NH".into(),
state_name: "New Hampshire".into(),
state_rate: Decimal::ZERO, has_local_taxes: false,
origin_based: false,
tax_shipping: false,
tax_clothing: false,
tax_food: false,
tax_digital: false,
}),
"NY" => Some(UsStateTaxInfo {
state_code: "NY".into(),
state_name: "New York".into(),
state_rate: Decimal::new(4, 2), has_local_taxes: true,
origin_based: false,
tax_shipping: true,
tax_clothing: false, tax_food: false,
tax_digital: true,
}),
"OR" => Some(UsStateTaxInfo {
state_code: "OR".into(),
state_name: "Oregon".into(),
state_rate: Decimal::ZERO, has_local_taxes: false,
origin_based: false,
tax_shipping: false,
tax_clothing: false,
tax_food: false,
tax_digital: false,
}),
"TX" => Some(UsStateTaxInfo {
state_code: "TX".into(),
state_name: "Texas".into(),
state_rate: Decimal::new(625, 4), has_local_taxes: true,
origin_based: true,
tax_shipping: true,
tax_clothing: true,
tax_food: false,
tax_digital: true,
}),
"WA" => Some(UsStateTaxInfo {
state_code: "WA".into(),
state_name: "Washington".into(),
state_rate: Decimal::new(65, 3), has_local_taxes: true,
origin_based: false,
tax_shipping: true,
tax_clothing: true,
tax_food: false,
tax_digital: true,
}),
_ => None,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EuVatInfo {
pub country_code: String,
pub country_name: String,
pub standard_rate: Decimal,
pub reduced_rate: Option<Decimal>,
pub super_reduced_rate: Option<Decimal>,
pub parking_rate: Option<Decimal>,
}
#[must_use]
pub fn get_eu_vat_info(country_code: &str) -> Option<EuVatInfo> {
match country_code.to_uppercase().as_str() {
"AT" => Some(EuVatInfo {
country_code: "AT".into(),
country_name: "Austria".into(),
standard_rate: Decimal::new(20, 2),
reduced_rate: Some(Decimal::new(10, 2)),
super_reduced_rate: None,
parking_rate: Some(Decimal::new(13, 2)),
}),
"BE" => Some(EuVatInfo {
country_code: "BE".into(),
country_name: "Belgium".into(),
standard_rate: Decimal::new(21, 2),
reduced_rate: Some(Decimal::new(12, 2)),
super_reduced_rate: Some(Decimal::new(6, 2)),
parking_rate: Some(Decimal::new(12, 2)),
}),
"DE" => Some(EuVatInfo {
country_code: "DE".into(),
country_name: "Germany".into(),
standard_rate: Decimal::new(19, 2),
reduced_rate: Some(Decimal::new(7, 2)),
super_reduced_rate: None,
parking_rate: None,
}),
"ES" => Some(EuVatInfo {
country_code: "ES".into(),
country_name: "Spain".into(),
standard_rate: Decimal::new(21, 2),
reduced_rate: Some(Decimal::new(10, 2)),
super_reduced_rate: Some(Decimal::new(4, 2)),
parking_rate: None,
}),
"FR" => Some(EuVatInfo {
country_code: "FR".into(),
country_name: "France".into(),
standard_rate: Decimal::new(20, 2),
reduced_rate: Some(Decimal::new(10, 2)),
super_reduced_rate: Some(Decimal::new(55, 3)), parking_rate: None,
}),
"GB" => Some(EuVatInfo {
country_code: "GB".into(),
country_name: "United Kingdom".into(),
standard_rate: Decimal::new(20, 2),
reduced_rate: Some(Decimal::new(5, 2)),
super_reduced_rate: None,
parking_rate: None,
}),
"IE" => Some(EuVatInfo {
country_code: "IE".into(),
country_name: "Ireland".into(),
standard_rate: Decimal::new(23, 2),
reduced_rate: Some(Decimal::new(135, 3)), super_reduced_rate: Some(Decimal::new(48, 3)), parking_rate: Some(Decimal::new(135, 3)),
}),
"IT" => Some(EuVatInfo {
country_code: "IT".into(),
country_name: "Italy".into(),
standard_rate: Decimal::new(22, 2),
reduced_rate: Some(Decimal::new(10, 2)),
super_reduced_rate: Some(Decimal::new(4, 2)),
parking_rate: None,
}),
"NL" => Some(EuVatInfo {
country_code: "NL".into(),
country_name: "Netherlands".into(),
standard_rate: Decimal::new(21, 2),
reduced_rate: Some(Decimal::new(9, 2)),
super_reduced_rate: None,
parking_rate: None,
}),
"SE" => Some(EuVatInfo {
country_code: "SE".into(),
country_name: "Sweden".into(),
standard_rate: Decimal::new(25, 2),
reduced_rate: Some(Decimal::new(12, 2)),
super_reduced_rate: Some(Decimal::new(6, 2)),
parking_rate: None,
}),
_ => None,
}
}
pub const EU_MEMBER_STATES: &[&str] = &[
"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE", "IT", "LV",
"LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE",
];
#[must_use]
pub fn is_eu_member(country_code: &str) -> bool {
EU_MEMBER_STATES.contains(&country_code.to_uppercase().as_str())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CanadianTaxInfo {
pub province_code: String,
pub province_name: String,
pub gst_rate: Decimal,
pub pst_rate: Option<Decimal>,
pub hst_rate: Option<Decimal>,
pub qst_rate: Option<Decimal>,
pub total_rate: Decimal,
}
#[must_use]
pub fn get_canadian_tax_info(province_code: &str) -> Option<CanadianTaxInfo> {
let gst = Decimal::new(5, 2);
match province_code.to_uppercase().as_str() {
"AB" => Some(CanadianTaxInfo {
province_code: "AB".into(),
province_name: "Alberta".into(),
gst_rate: gst,
pst_rate: None,
hst_rate: None,
qst_rate: None,
total_rate: gst,
}),
"BC" => Some(CanadianTaxInfo {
province_code: "BC".into(),
province_name: "British Columbia".into(),
gst_rate: gst,
pst_rate: Some(Decimal::new(7, 2)),
hst_rate: None,
qst_rate: None,
total_rate: Decimal::new(12, 2),
}),
"ON" => Some(CanadianTaxInfo {
province_code: "ON".into(),
province_name: "Ontario".into(),
gst_rate: Decimal::ZERO, pst_rate: None,
hst_rate: Some(Decimal::new(13, 2)),
qst_rate: None,
total_rate: Decimal::new(13, 2),
}),
"QC" => Some(CanadianTaxInfo {
province_code: "QC".into(),
province_name: "Quebec".into(),
gst_rate: gst,
pst_rate: None,
hst_rate: None,
qst_rate: Some(Decimal::new(9975, 4)), total_rate: Decimal::new(14975, 4),
}),
"SK" => Some(CanadianTaxInfo {
province_code: "SK".into(),
province_name: "Saskatchewan".into(),
gst_rate: gst,
pst_rate: Some(Decimal::new(6, 2)),
hst_rate: None,
qst_rate: None,
total_rate: Decimal::new(11, 2),
}),
"MB" => Some(CanadianTaxInfo {
province_code: "MB".into(),
province_name: "Manitoba".into(),
gst_rate: gst,
pst_rate: Some(Decimal::new(7, 2)),
hst_rate: None,
qst_rate: None,
total_rate: Decimal::new(12, 2),
}),
"NS" => Some(CanadianTaxInfo {
province_code: "NS".into(),
province_name: "Nova Scotia".into(),
gst_rate: Decimal::ZERO,
pst_rate: None,
hst_rate: Some(Decimal::new(15, 2)),
qst_rate: None,
total_rate: Decimal::new(15, 2),
}),
"NB" => Some(CanadianTaxInfo {
province_code: "NB".into(),
province_name: "New Brunswick".into(),
gst_rate: Decimal::ZERO,
pst_rate: None,
hst_rate: Some(Decimal::new(15, 2)),
qst_rate: None,
total_rate: Decimal::new(15, 2),
}),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn tax_type_from_str() {
assert_eq!(TaxType::from_str("sales_tax").unwrap(), TaxType::SalesTax);
assert!(TaxType::from_str("unknown").is_err());
}
#[test]
fn tax_calculation_method_from_str() {
assert_eq!(
TaxCalculationMethod::from_str("inclusive").unwrap(),
TaxCalculationMethod::Inclusive
);
assert!(TaxCalculationMethod::from_str("other").is_err());
}
#[test]
fn tax_compound_method_from_str() {
assert_eq!(TaxCompoundMethod::from_str("combined").unwrap(), TaxCompoundMethod::Combined);
assert!(TaxCompoundMethod::from_str("other").is_err());
}
#[test]
fn product_tax_category_from_str() {
assert_eq!(
ProductTaxCategory::from_str("super_reduced").unwrap(),
ProductTaxCategory::SuperReduced
);
assert!(ProductTaxCategory::from_str("other").is_err());
}
#[test]
fn exemption_type_from_str() {
assert_eq!(ExemptionType::from_str("non_profit").unwrap(), ExemptionType::NonProfit);
assert!(ExemptionType::from_str("unknown").is_err());
}
#[test]
fn jurisdiction_level_from_str() {
assert_eq!(JurisdictionLevel::from_str("state").unwrap(), JurisdictionLevel::State);
assert!(JurisdictionLevel::from_str("unknown").is_err());
}
#[test]
fn tax_settings_rounding_strategy_maps_modes() {
use rust_decimal::RoundingStrategy;
let strat = |mode: &str| {
let s = TaxSettings { rounding_mode: mode.to_string(), ..Default::default() };
s.rounding_strategy()
};
assert_eq!(strat("half_up"), RoundingStrategy::MidpointAwayFromZero);
assert_eq!(strat("half_even"), RoundingStrategy::MidpointNearestEven);
assert_eq!(strat("bankers"), RoundingStrategy::MidpointNearestEven);
assert_eq!(strat("half_down"), RoundingStrategy::MidpointTowardZero);
assert_eq!(strat("up"), RoundingStrategy::AwayFromZero);
assert_eq!(strat("down"), RoundingStrategy::ToZero);
assert_eq!(strat("truncate"), RoundingStrategy::ToZero);
assert_eq!(strat("ceil"), RoundingStrategy::ToPositiveInfinity);
assert_eq!(strat("floor"), RoundingStrategy::ToNegativeInfinity);
assert_eq!(strat(" Half_Even "), RoundingStrategy::MidpointNearestEven);
assert_eq!(strat("wat"), RoundingStrategy::MidpointAwayFromZero);
assert_eq!(
TaxSettings::default().rounding_strategy(),
RoundingStrategy::MidpointAwayFromZero
);
}
}