Skip to main content

stateset_embedded/
tax.rs

1//! Tax calculation operations
2//!
3//! Provides multi-jurisdiction tax calculation with support for:
4//! - US sales tax (state, county, city)
5//! - EU VAT (standard, reduced, zero-rated)
6//! - Canadian GST/HST/PST/QST
7//! - Customer exemptions (resale, non-profit, etc.)
8
9use chrono::NaiveDate;
10use rust_decimal::Decimal;
11use stateset_core::{
12    CreateTaxExemption, CreateTaxJurisdiction, CreateTaxRate, ProductTaxCategory, Result,
13    TaxAddress, TaxCalculationRequest, TaxCalculationResult, TaxExemption, TaxJurisdiction,
14    TaxJurisdictionFilter, TaxLineItem, TaxRate, TaxRateFilter, TaxSettings,
15};
16use stateset_db::Database;
17use std::sync::Arc;
18use uuid::Uuid;
19
20/// Tax calculation and management interface.
21///
22/// Provides tax rate management, exemption handling, and tax calculation
23/// for commerce operations.
24///
25/// # Example
26///
27/// ```rust,ignore
28/// use stateset_embedded::{Commerce, TaxAddress, TaxCalculationRequest, TaxLineItem, ProductTaxCategory};
29/// use rust_decimal_macros::dec;
30///
31/// let commerce = Commerce::new("./store.db")?;
32///
33/// // Calculate tax for a transaction
34/// let result = commerce.tax().calculate(TaxCalculationRequest {
35///     line_items: vec![TaxLineItem {
36///         id: "item-1".into(),
37///         quantity: dec!(2),
38///         unit_price: dec!(29.99),
39///         tax_category: ProductTaxCategory::Standard,
40///         ..Default::default()
41///     }],
42///     shipping_address: TaxAddress {
43///         country: "US".into(),
44///         state: Some("CA".into()),
45///         postal_code: Some("90210".into()),
46///         ..Default::default()
47///     },
48///     ..Default::default()
49/// })?;
50///
51/// println!("Subtotal: ${}", result.subtotal);
52/// println!("Tax: ${}", result.total_tax);
53/// println!("Total: ${}", result.total);
54/// # Ok::<(), stateset_embedded::CommerceError>(())
55/// ```
56pub struct Tax {
57    db: Arc<dyn Database>,
58}
59
60impl std::fmt::Debug for Tax {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        f.debug_struct("Tax").finish_non_exhaustive()
63    }
64}
65
66impl Tax {
67    pub(crate) fn new(db: Arc<dyn Database>) -> Self {
68        Self { db }
69    }
70
71    // ========================================================================
72    // Tax Calculation
73    // ========================================================================
74
75    /// Calculate tax for a transaction.
76    ///
77    /// Given line items and shipping address, calculates applicable taxes
78    /// based on jurisdiction rules, product categories, and customer exemptions.
79    ///
80    /// # Example
81    ///
82    /// ```rust,ignore
83    /// # use stateset_embedded::*;
84    /// use rust_decimal_macros::dec;
85    ///
86    /// # let commerce = Commerce::new(":memory:")?;
87    /// let result = commerce.tax().calculate(TaxCalculationRequest {
88    ///     line_items: vec![TaxLineItem {
89    ///         id: "item-1".into(),
90    ///         quantity: dec!(2),
91    ///         unit_price: dec!(29.99),
92    ///         tax_category: ProductTaxCategory::Standard,
93    ///         ..Default::default()
94    ///     }],
95    ///     shipping_address: TaxAddress {
96    ///         country: "US".into(),
97    ///         state: Some("CA".into()),
98    ///         ..Default::default()
99    ///     },
100    ///     ..Default::default()
101    /// })?;
102    ///
103    /// println!("Tax breakdown:");
104    /// for breakdown in &result.tax_breakdown {
105    ///     println!("  {}: {}% = ${}", breakdown.rate_name, breakdown.rate * dec!(100), breakdown.tax_amount);
106    /// }
107    /// # Ok::<(), CommerceError>(())
108    /// ```
109    pub fn calculate(&self, request: TaxCalculationRequest) -> Result<TaxCalculationResult> {
110        self.db.tax().calculate_tax(request)
111    }
112
113    /// Calculate tax for a single item (convenience method).
114    ///
115    /// # Example
116    ///
117    /// ```rust,ignore
118    /// # use stateset_embedded::*;
119    /// use rust_decimal_macros::dec;
120    ///
121    /// # let commerce = Commerce::new(":memory:")?;
122    /// let tax = commerce.tax().calculate_for_item(
123    ///     dec!(99.99),                    // unit price
124    ///     dec!(2),                        // quantity
125    ///     ProductTaxCategory::Standard,    // category
126    ///     &TaxAddress {
127    ///         country: "US".into(),
128    ///         state: Some("TX".into()),
129    ///         ..Default::default()
130    ///     },
131    /// )?;
132    ///
133    /// println!("Tax: ${}", tax);
134    /// # Ok::<(), CommerceError>(())
135    /// ```
136    pub fn calculate_for_item(
137        &self,
138        unit_price: Decimal,
139        quantity: Decimal,
140        category: ProductTaxCategory,
141        shipping_address: &TaxAddress,
142    ) -> Result<Decimal> {
143        let request = TaxCalculationRequest {
144            line_items: vec![TaxLineItem {
145                id: "single".into(),
146                quantity,
147                unit_price,
148                tax_category: category,
149                ..Default::default()
150            }],
151            shipping_address: shipping_address.clone(),
152            ..Default::default()
153        };
154
155        let result = self.calculate(request)?;
156        Ok(result.total_tax)
157    }
158
159    /// Get the effective tax rate for an address and category.
160    ///
161    /// Returns the combined tax rate that would apply to a standard purchase.
162    ///
163    /// # Example
164    ///
165    /// ```rust,ignore
166    /// # use stateset_embedded::*;
167    /// # let commerce = Commerce::new(":memory:")?;
168    /// let rate = commerce.tax().get_effective_rate(
169    ///     &TaxAddress {
170    ///         country: "US".into(),
171    ///         state: Some("CA".into()),
172    ///         city: Some("Los Angeles".into()),
173    ///         ..Default::default()
174    ///     },
175    ///     ProductTaxCategory::Standard,
176    /// )?;
177    ///
178    /// println!("Effective tax rate: {}%", rate * rust_decimal_macros::dec!(100));
179    /// # Ok::<(), CommerceError>(())
180    /// ```
181    pub fn get_effective_rate(
182        &self,
183        address: &TaxAddress,
184        category: ProductTaxCategory,
185    ) -> Result<Decimal> {
186        let today = chrono::Utc::now().date_naive();
187        let rates = self.db.tax().get_rates_for_address(address, category, today)?;
188
189        let total_rate: Decimal = rates.iter().filter(|r| !r.is_compound).map(|r| r.rate).sum();
190
191        Ok(total_rate)
192    }
193
194    // ========================================================================
195    // Jurisdiction Operations
196    // ========================================================================
197
198    /// Get a tax jurisdiction by ID.
199    pub fn get_jurisdiction(&self, id: Uuid) -> Result<Option<TaxJurisdiction>> {
200        self.db.tax().get_jurisdiction(id)
201    }
202
203    /// Get a tax jurisdiction by code (e.g., "US-CA").
204    ///
205    /// # Example
206    ///
207    /// ```rust,ignore
208    /// # use stateset_embedded::*;
209    /// # let commerce = Commerce::new(":memory:")?;
210    /// if let Some(jurisdiction) = commerce.tax().get_jurisdiction_by_code("US-CA")? {
211    ///     println!("{}: {}", jurisdiction.code, jurisdiction.name);
212    /// }
213    /// # Ok::<(), CommerceError>(())
214    /// ```
215    pub fn get_jurisdiction_by_code(&self, code: &str) -> Result<Option<TaxJurisdiction>> {
216        self.db.tax().get_jurisdiction_by_code(code)
217    }
218
219    /// List tax jurisdictions with optional filtering.
220    ///
221    /// # Example
222    ///
223    /// ```rust,ignore
224    /// # use stateset_embedded::*;
225    /// # let commerce = Commerce::new(":memory:")?;
226    /// // List all US state jurisdictions
227    /// let states = commerce.tax().list_jurisdictions(TaxJurisdictionFilter {
228    ///     country_code: Some("US".into()),
229    ///     level: Some(JurisdictionLevel::State),
230    ///     active_only: true,
231    ///     ..Default::default()
232    /// })?;
233    ///
234    /// for state in states {
235    ///     println!("{}: {}", state.code, state.name);
236    /// }
237    /// # Ok::<(), CommerceError>(())
238    /// ```
239    pub fn list_jurisdictions(
240        &self,
241        filter: TaxJurisdictionFilter,
242    ) -> Result<Vec<TaxJurisdiction>> {
243        self.db.tax().list_jurisdictions(filter)
244    }
245
246    /// Create a new tax jurisdiction.
247    ///
248    /// # Example
249    ///
250    /// ```rust,ignore
251    /// # use stateset_embedded::*;
252    /// # let commerce = Commerce::new(":memory:")?;
253    /// let jurisdiction = commerce.tax().create_jurisdiction(CreateTaxJurisdiction {
254    ///     name: "Los Angeles".into(),
255    ///     code: "US-CA-LA".into(),
256    ///     level: JurisdictionLevel::City,
257    ///     country_code: "US".into(),
258    ///     state_code: Some("CA".into()),
259    ///     city: Some("Los Angeles".into()),
260    ///     ..Default::default()
261    /// })?;
262    /// # Ok::<(), CommerceError>(())
263    /// ```
264    pub fn create_jurisdiction(&self, input: CreateTaxJurisdiction) -> Result<TaxJurisdiction> {
265        self.db.tax().create_jurisdiction(input)
266    }
267
268    // ========================================================================
269    // Tax Rate Operations
270    // ========================================================================
271
272    /// Get a tax rate by ID.
273    pub fn get_rate(&self, id: Uuid) -> Result<Option<TaxRate>> {
274        self.db.tax().get_rate(id)
275    }
276
277    /// List tax rates with optional filtering.
278    ///
279    /// # Example
280    ///
281    /// ```rust,ignore
282    /// # use stateset_embedded::*;
283    /// # let commerce = Commerce::new(":memory:")?;
284    /// // Get all active rates for a jurisdiction
285    /// let rates = commerce.tax().list_rates(TaxRateFilter {
286    ///     jurisdiction_id: Some(jurisdiction_id),
287    ///     active_only: true,
288    ///     ..Default::default()
289    /// })?;
290    ///
291    /// for rate in rates {
292    ///     println!("{}: {}%", rate.name, rate.rate * rust_decimal_macros::dec!(100));
293    /// }
294    /// # Ok::<(), CommerceError>(())
295    /// ```
296    pub fn list_rates(&self, filter: TaxRateFilter) -> Result<Vec<TaxRate>> {
297        self.db.tax().list_rates(filter)
298    }
299
300    /// Create a new tax rate.
301    ///
302    /// # Example
303    ///
304    /// ```rust,ignore
305    /// # use stateset_embedded::*;
306    /// use rust_decimal_macros::dec;
307    ///
308    /// # let commerce = Commerce::new(":memory:")?;
309    /// let rate = commerce.tax().create_rate(CreateTaxRate {
310    ///     jurisdiction_id,
311    ///     tax_type: TaxType::SalesTax,
312    ///     product_category: ProductTaxCategory::Standard,
313    ///     rate: dec!(0.0825),  // 8.25%
314    ///     name: "City Sales Tax".into(),
315    ///     effective_from: chrono::Utc::now().date_naive(),
316    ///     ..Default::default()
317    /// })?;
318    /// # Ok::<(), CommerceError>(())
319    /// ```
320    pub fn create_rate(&self, input: CreateTaxRate) -> Result<TaxRate> {
321        self.db.tax().create_rate(input)
322    }
323
324    /// Get rates for a specific address and product category.
325    ///
326    /// Returns all applicable tax rates sorted by priority.
327    pub fn get_rates_for_address(
328        &self,
329        address: &TaxAddress,
330        category: ProductTaxCategory,
331        date: NaiveDate,
332    ) -> Result<Vec<TaxRate>> {
333        self.db.tax().get_rates_for_address(address, category, date)
334    }
335
336    // ========================================================================
337    // Exemption Operations
338    // ========================================================================
339
340    /// Get an exemption by ID.
341    pub fn get_exemption(&self, id: Uuid) -> Result<Option<TaxExemption>> {
342        self.db.tax().get_exemption(id)
343    }
344
345    /// Get active exemptions for a customer.
346    ///
347    /// # Example
348    ///
349    /// ```rust,ignore
350    /// # use stateset_embedded::*;
351    /// # let commerce = Commerce::new(":memory:")?;
352    /// let exemptions = commerce.tax().get_customer_exemptions(customer_id)?;
353    ///
354    /// for exemption in exemptions {
355    ///     println!("Type: {:?}", exemption.exemption_type);
356    ///     if let Some(cert) = &exemption.certificate_number {
357    ///         println!("Certificate: {}", cert);
358    ///     }
359    /// }
360    /// # Ok::<(), CommerceError>(())
361    /// ```
362    pub fn get_customer_exemptions(&self, customer_id: Uuid) -> Result<Vec<TaxExemption>> {
363        self.db.tax().get_customer_exemptions(customer_id)
364    }
365
366    /// Create a tax exemption for a customer.
367    ///
368    /// # Example
369    ///
370    /// ```rust,ignore
371    /// # use stateset_embedded::*;
372    /// # let commerce = Commerce::new(":memory:")?;
373    /// let exemption = commerce.tax().create_exemption(CreateTaxExemption {
374    ///     customer_id,
375    ///     exemption_type: ExemptionType::Resale,
376    ///     certificate_number: Some("RS-12345".into()),
377    ///     issuing_authority: Some("California".into()),
378    ///     effective_from: chrono::Utc::now().date_naive(),
379    ///     expires_at: Some(chrono::Utc::now().date_naive() + chrono::Duration::days(365)),
380    ///     ..Default::default()
381    /// })?;
382    /// # Ok::<(), CommerceError>(())
383    /// ```
384    pub fn create_exemption(&self, input: CreateTaxExemption) -> Result<TaxExemption> {
385        self.db.tax().create_exemption(input)
386    }
387
388    /// Check if a customer has an active exemption.
389    ///
390    /// # Example
391    ///
392    /// ```rust,ignore
393    /// # use stateset_embedded::*;
394    /// # let commerce = Commerce::new(":memory:")?;
395    /// if commerce.tax().customer_is_exempt(customer_id)? {
396    ///     println!("Customer has tax exemption");
397    /// }
398    /// # Ok::<(), CommerceError>(())
399    /// ```
400    pub fn customer_is_exempt(&self, customer_id: Uuid) -> Result<bool> {
401        let exemptions = self.get_customer_exemptions(customer_id)?;
402        Ok(!exemptions.is_empty())
403    }
404
405    // ========================================================================
406    // Settings Operations
407    // ========================================================================
408
409    /// Get tax settings.
410    ///
411    /// # Example
412    ///
413    /// ```rust,ignore
414    /// # use stateset_embedded::*;
415    /// # let commerce = Commerce::new(":memory:")?;
416    /// let settings = commerce.tax().get_settings()?;
417    ///
418    /// println!("Tax enabled: {}", settings.enabled);
419    /// println!("Tax shipping: {}", settings.tax_shipping);
420    /// println!("Calculation method: {:?}", settings.calculation_method);
421    /// # Ok::<(), CommerceError>(())
422    /// ```
423    pub fn get_settings(&self) -> Result<TaxSettings> {
424        self.db.tax().get_settings()
425    }
426
427    /// Update tax settings.
428    ///
429    /// # Example
430    ///
431    /// ```rust,ignore
432    /// # use stateset_embedded::*;
433    /// # let commerce = Commerce::new(":memory:")?;
434    /// let mut settings = commerce.tax().get_settings()?;
435    /// settings.tax_shipping = true;
436    /// settings.decimal_places = 2;
437    ///
438    /// commerce.tax().update_settings(settings)?;
439    /// # Ok::<(), CommerceError>(())
440    /// ```
441    pub fn update_settings(&self, settings: TaxSettings) -> Result<TaxSettings> {
442        self.db.tax().update_settings(settings)
443    }
444
445    /// Enable or disable tax calculation.
446    pub fn set_enabled(&self, enabled: bool) -> Result<TaxSettings> {
447        let mut settings = self.get_settings()?;
448        settings.enabled = enabled;
449        self.update_settings(settings)
450    }
451
452    /// Check if tax calculation is enabled.
453    pub fn is_enabled(&self) -> Result<bool> {
454        let settings = self.get_settings()?;
455        Ok(settings.enabled)
456    }
457
458    // ========================================================================
459    // Helper Methods
460    // ========================================================================
461
462    /// Get US state tax information.
463    ///
464    /// Returns pre-configured tax information for a US state.
465    ///
466    /// # Example
467    ///
468    /// ```rust,ignore
469    /// # use stateset_embedded::*;
470    /// if let Some(info) = stateset_core::get_us_state_tax_info("CA") {
471    ///     println!("California state rate: {}%", info.state_rate * rust_decimal_macros::dec!(100));
472    ///     println!("Has local taxes: {}", info.has_local_taxes);
473    ///     println!("Tax shipping: {}", info.tax_shipping);
474    /// }
475    /// ```
476    #[must_use]
477    pub fn get_us_state_info(state_code: &str) -> Option<stateset_core::UsStateTaxInfo> {
478        stateset_core::get_us_state_tax_info(state_code)
479    }
480
481    /// Get EU VAT information.
482    ///
483    /// Returns pre-configured VAT rates for an EU country.
484    ///
485    /// # Example
486    ///
487    /// ```rust,ignore
488    /// # use stateset_embedded::*;
489    /// if let Some(info) = stateset_core::get_eu_vat_info("DE") {
490    ///     println!("Germany standard VAT: {}%", info.standard_rate * rust_decimal_macros::dec!(100));
491    ///     if let Some(reduced) = info.reduced_rate {
492    ///         println!("Reduced rate: {}%", reduced * rust_decimal_macros::dec!(100));
493    ///     }
494    /// }
495    /// ```
496    #[must_use]
497    pub fn get_eu_vat_info(country_code: &str) -> Option<stateset_core::EuVatInfo> {
498        stateset_core::get_eu_vat_info(country_code)
499    }
500
501    /// Get Canadian tax information.
502    ///
503    /// Returns pre-configured tax rates for a Canadian province.
504    ///
505    /// # Example
506    ///
507    /// ```rust,ignore
508    /// # use stateset_embedded::*;
509    /// if let Some(info) = stateset_core::get_canadian_tax_info("ON") {
510    ///     println!("Ontario total rate: {}%", info.total_rate * rust_decimal_macros::dec!(100));
511    ///     if let Some(hst) = info.hst_rate {
512    ///         println!("HST: {}%", hst * rust_decimal_macros::dec!(100));
513    ///     }
514    /// }
515    /// ```
516    #[must_use]
517    pub fn get_canadian_tax_info(province_code: &str) -> Option<stateset_core::CanadianTaxInfo> {
518        stateset_core::get_canadian_tax_info(province_code)
519    }
520
521    /// Check if a country is in the EU.
522    #[must_use]
523    pub fn is_eu_country(country_code: &str) -> bool {
524        stateset_core::is_eu_member(country_code)
525    }
526}