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}