Skip to main content

stateset_db/sqlite/
tax.rs

1//! SQLite implementation of tax repository
2
3use chrono::{NaiveDate, Utc};
4use r2d2::Pool;
5use r2d2_sqlite::SqliteConnectionManager;
6use rusqlite::params;
7use rust_decimal::Decimal;
8use stateset_core::{
9    CommerceError, CreateTaxExemption, CreateTaxJurisdiction, CreateTaxRate, JurisdictionSummary,
10    LineItemTax, ProductTaxCategory, Result, TaxAddress, TaxBreakdown, TaxCalculationRequest,
11    TaxCalculationResult, TaxDetail, TaxExemption, TaxJurisdiction, TaxJurisdictionFilter, TaxRate,
12    TaxRateFilter, TaxRepository, TaxSettings,
13};
14use uuid::Uuid;
15
16use super::{
17    map_db_error, parse_date_row, parse_datetime_opt_row, parse_datetime_row,
18    parse_decimal_opt_row, parse_decimal_row, parse_enum_row, parse_json_opt_row, parse_json_row,
19    parse_uuid_opt_row, parse_uuid_row,
20};
21
22/// SQLite tax repository
23#[derive(Debug)]
24pub struct SqliteTaxRepository {
25    pool: Pool<SqliteConnectionManager>,
26}
27
28impl SqliteTaxRepository {
29    #[must_use]
30    pub const fn new(pool: Pool<SqliteConnectionManager>) -> Self {
31        Self { pool }
32    }
33
34    fn parse_date_opt(
35        value: Option<String>,
36        entity: &str,
37        field: &str,
38    ) -> rusqlite::Result<Option<NaiveDate>> {
39        match value {
40            Some(ref val) if !val.is_empty() => Ok(Some(parse_date_row(val, entity, field)?)),
41            _ => Ok(None),
42        }
43    }
44}
45
46// ============================================================================
47// Jurisdiction Operations
48// ============================================================================
49
50impl SqliteTaxRepository {
51    /// Get a jurisdiction by ID
52    pub fn get_jurisdiction(&self, id: Uuid) -> Result<Option<TaxJurisdiction>> {
53        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
54
55        let result = conn.query_row(
56            "SELECT id, parent_id, name, code, level, country_code, state_code, county, city, postal_codes, active, created_at, updated_at
57             FROM tax_jurisdictions WHERE id = ?",
58            params![id.to_string()],
59            |row| {
60                let postal_codes_json: String = row.get(9)?;
61                let postal_codes: Vec<String> =
62                    parse_json_row(&postal_codes_json, "tax_jurisdiction", "postal_codes")?;
63
64                Ok(TaxJurisdiction {
65                    id: parse_uuid_row(&row.get::<_, String>(0)?, "tax_jurisdiction", "id")?,
66                    parent_id: parse_uuid_opt_row(
67                        row.get::<_, Option<String>>(1)?,
68                        "tax_jurisdiction",
69                        "parent_id",
70                    )?,
71                    name: row.get(2)?,
72                    code: row.get(3)?,
73                    level: parse_enum_row(&row.get::<_, String>(4)?, "tax_jurisdiction", "level")?,
74                    country_code: row.get(5)?,
75                    state_code: row.get(6)?,
76                    county: row.get(7)?,
77                    city: row.get(8)?,
78                    postal_codes,
79                    active: row.get::<_, i32>(10)? != 0,
80                    created_at: parse_datetime_row(
81                        &row.get::<_, String>(11)?,
82                        "tax_jurisdiction",
83                        "created_at",
84                    )?,
85                    updated_at: parse_datetime_row(
86                        &row.get::<_, String>(12)?,
87                        "tax_jurisdiction",
88                        "updated_at",
89                    )?,
90                })
91            },
92        );
93
94        match result {
95            Ok(jurisdiction) => Ok(Some(jurisdiction)),
96            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
97            Err(e) => Err(map_db_error(e)),
98        }
99    }
100
101    /// Get a jurisdiction by code (e.g., "US-CA")
102    pub fn get_jurisdiction_by_code(&self, code: &str) -> Result<Option<TaxJurisdiction>> {
103        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
104
105        let result = conn.query_row(
106            "SELECT id, parent_id, name, code, level, country_code, state_code, county, city, postal_codes, active, created_at, updated_at
107             FROM tax_jurisdictions WHERE code = ?",
108            params![code],
109            |row| {
110                let postal_codes_json: String = row.get(9)?;
111                let postal_codes: Vec<String> =
112                    parse_json_row(&postal_codes_json, "tax_jurisdiction", "postal_codes")?;
113
114                Ok(TaxJurisdiction {
115                    id: parse_uuid_row(&row.get::<_, String>(0)?, "tax_jurisdiction", "id")?,
116                    parent_id: parse_uuid_opt_row(
117                        row.get::<_, Option<String>>(1)?,
118                        "tax_jurisdiction",
119                        "parent_id",
120                    )?,
121                    name: row.get(2)?,
122                    code: row.get(3)?,
123                    level: parse_enum_row(&row.get::<_, String>(4)?, "tax_jurisdiction", "level")?,
124                    country_code: row.get(5)?,
125                    state_code: row.get(6)?,
126                    county: row.get(7)?,
127                    city: row.get(8)?,
128                    postal_codes,
129                    active: row.get::<_, i32>(10)? != 0,
130                    created_at: parse_datetime_row(
131                        &row.get::<_, String>(11)?,
132                        "tax_jurisdiction",
133                        "created_at",
134                    )?,
135                    updated_at: parse_datetime_row(
136                        &row.get::<_, String>(12)?,
137                        "tax_jurisdiction",
138                        "updated_at",
139                    )?,
140                })
141            },
142        );
143
144        match result {
145            Ok(jurisdiction) => Ok(Some(jurisdiction)),
146            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
147            Err(e) => Err(map_db_error(e)),
148        }
149    }
150
151    /// List jurisdictions with optional filter
152    pub fn list_jurisdictions(
153        &self,
154        filter: TaxJurisdictionFilter,
155    ) -> Result<Vec<TaxJurisdiction>> {
156        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
157
158        let mut query = String::from(
159            "SELECT id, parent_id, name, code, level, country_code, state_code, county, city, postal_codes, active, created_at, updated_at
160             FROM tax_jurisdictions WHERE 1=1"
161        );
162        let mut params_vec: Vec<String> = Vec::new();
163
164        if let Some(country) = &filter.country_code {
165            query.push_str(" AND country_code = ?");
166            params_vec.push(country.clone());
167        }
168
169        if let Some(state) = &filter.state_code {
170            query.push_str(" AND state_code = ?");
171            params_vec.push(state.clone());
172        }
173
174        if let Some(level) = &filter.level {
175            query.push_str(" AND level = ?");
176            params_vec.push(level.to_string());
177        }
178
179        if filter.active_only {
180            query.push_str(" AND active = 1");
181        }
182
183        // Deterministic, backend-identical order (COALESCE so a NULL state_code
184        // sorts consistently with the Postgres backend). Must match Postgres.
185        query.push_str(" ORDER BY country_code, COALESCE(state_code, ''), level, name");
186        crate::sqlite::append_limit_offset(&mut query, filter.limit, filter.offset);
187
188        let mut stmt = conn.prepare(&query).map_err(map_db_error)?;
189        let params: Vec<&dyn rusqlite::ToSql> =
190            params_vec.iter().map(|s| s as &dyn rusqlite::ToSql).collect();
191
192        let rows = stmt
193            .query_map(params.as_slice(), |row| {
194                let postal_codes_json: String = row.get(9)?;
195                let postal_codes: Vec<String> =
196                    parse_json_row(&postal_codes_json, "tax_jurisdiction", "postal_codes")?;
197
198                Ok(TaxJurisdiction {
199                    id: parse_uuid_row(&row.get::<_, String>(0)?, "tax_jurisdiction", "id")?,
200                    parent_id: parse_uuid_opt_row(
201                        row.get::<_, Option<String>>(1)?,
202                        "tax_jurisdiction",
203                        "parent_id",
204                    )?,
205                    name: row.get(2)?,
206                    code: row.get(3)?,
207                    level: parse_enum_row(&row.get::<_, String>(4)?, "tax_jurisdiction", "level")?,
208                    country_code: row.get(5)?,
209                    state_code: row.get(6)?,
210                    county: row.get(7)?,
211                    city: row.get(8)?,
212                    postal_codes,
213                    active: row.get::<_, i32>(10)? != 0,
214                    created_at: parse_datetime_row(
215                        &row.get::<_, String>(11)?,
216                        "tax_jurisdiction",
217                        "created_at",
218                    )?,
219                    updated_at: parse_datetime_row(
220                        &row.get::<_, String>(12)?,
221                        "tax_jurisdiction",
222                        "updated_at",
223                    )?,
224                })
225            })
226            .map_err(map_db_error)?;
227
228        rows.collect::<std::result::Result<Vec<_>, _>>().map_err(map_db_error)
229    }
230
231    /// Create a new jurisdiction
232    pub fn create_jurisdiction(&self, input: CreateTaxJurisdiction) -> Result<TaxJurisdiction> {
233        let id = Uuid::new_v4();
234        let now = Utc::now();
235        let postal_codes_json = serde_json::to_string(&input.postal_codes)
236            .map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
237
238        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
239
240        conn.execute(
241            "INSERT INTO tax_jurisdictions (id, parent_id, name, code, level, country_code, state_code, county, city, postal_codes, active, created_at, updated_at)
242             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)",
243            params![
244                id.to_string(),
245                input.parent_id.map(|id| id.to_string()),
246                input.name,
247                input.code,
248                input.level.to_string(),
249                input.country_code,
250                input.state_code,
251                input.county,
252                input.city,
253                postal_codes_json,
254                now.to_rfc3339(),
255                now.to_rfc3339()
256            ],
257        ).map_err(map_db_error)?;
258
259        drop(conn);
260
261        self.get_jurisdiction(id)?.ok_or(CommerceError::NotFound)
262    }
263}
264
265// ============================================================================
266// Tax Rate Operations
267// ============================================================================
268
269impl SqliteTaxRepository {
270    /// Get a tax rate by ID
271    pub fn get_rate(&self, id: Uuid) -> Result<Option<TaxRate>> {
272        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
273
274        let result = conn.query_row(
275            "SELECT id, jurisdiction_id, tax_type, product_category, rate, name, description, is_compound, priority, threshold_min, threshold_max, fixed_amount, effective_from, effective_to, active, created_at, updated_at
276             FROM tax_rates WHERE id = ?",
277            params![id.to_string()],
278            |row| {
279                Ok(TaxRate {
280                    id: parse_uuid_row(&row.get::<_, String>(0)?, "tax_rate", "id")?,
281                    jurisdiction_id: parse_uuid_row(
282                        &row.get::<_, String>(1)?,
283                        "tax_rate",
284                        "jurisdiction_id",
285                    )?,
286                    tax_type: parse_enum_row(&row.get::<_, String>(2)?, "tax_rate", "tax_type")?,
287                    product_category: parse_enum_row(
288                        &row.get::<_, String>(3)?,
289                        "tax_rate",
290                        "product_category",
291                    )?,
292                    rate: parse_decimal_row(&row.get::<_, String>(4)?, "tax_rate", "rate")?,
293                    name: row.get(5)?,
294                    description: row.get(6)?,
295                    is_compound: row.get::<_, i32>(7)? != 0,
296                    priority: row.get(8)?,
297                    threshold_min: parse_decimal_opt_row(
298                        row.get::<_, Option<String>>(9)?,
299                        "tax_rate",
300                        "threshold_min",
301                    )?,
302                    threshold_max: parse_decimal_opt_row(
303                        row.get::<_, Option<String>>(10)?,
304                        "tax_rate",
305                        "threshold_max",
306                    )?,
307                    fixed_amount: parse_decimal_opt_row(
308                        row.get::<_, Option<String>>(11)?,
309                        "tax_rate",
310                        "fixed_amount",
311                    )?,
312                    effective_from: parse_date_row(
313                        &row.get::<_, String>(12)?,
314                        "tax_rate",
315                        "effective_from",
316                    )?,
317                    effective_to: Self::parse_date_opt(
318                        row.get::<_, Option<String>>(13)?,
319                        "tax_rate",
320                        "effective_to",
321                    )?,
322                    active: row.get::<_, i32>(14)? != 0,
323                    created_at: parse_datetime_row(&row.get::<_, String>(15)?, "tax_rate", "created_at")?,
324                    updated_at: parse_datetime_row(&row.get::<_, String>(16)?, "tax_rate", "updated_at")?,
325                })
326            },
327        );
328
329        match result {
330            Ok(rate) => Ok(Some(rate)),
331            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
332            Err(e) => Err(map_db_error(e)),
333        }
334    }
335
336    /// List tax rates with optional filter
337    pub fn list_rates(&self, filter: TaxRateFilter) -> Result<Vec<TaxRate>> {
338        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
339
340        let mut query = String::from(
341            "SELECT id, jurisdiction_id, tax_type, product_category, rate, name, description, is_compound, priority, threshold_min, threshold_max, fixed_amount, effective_from, effective_to, active, created_at, updated_at
342             FROM tax_rates WHERE 1=1"
343        );
344        let mut params_vec: Vec<String> = Vec::new();
345
346        if let Some(jurisdiction_id) = &filter.jurisdiction_id {
347            query.push_str(" AND jurisdiction_id = ?");
348            params_vec.push(jurisdiction_id.to_string());
349        }
350
351        if let Some(tax_type) = &filter.tax_type {
352            query.push_str(" AND tax_type = ?");
353            params_vec.push(tax_type.to_string());
354        }
355
356        if let Some(category) = &filter.product_category {
357            query.push_str(" AND product_category = ?");
358            params_vec.push(category.to_string());
359        }
360
361        if filter.active_only {
362            query.push_str(" AND active = 1");
363        }
364
365        if let Some(date) = &filter.effective_date {
366            query.push_str(
367                " AND effective_from <= ? AND (effective_to IS NULL OR effective_to >= ?)",
368            );
369            params_vec.push(date.to_string());
370            params_vec.push(date.to_string());
371        }
372
373        query.push_str(" ORDER BY priority, name");
374        crate::sqlite::append_limit_offset(&mut query, filter.limit, filter.offset);
375
376        let mut stmt = conn.prepare(&query).map_err(map_db_error)?;
377        let params: Vec<&dyn rusqlite::ToSql> =
378            params_vec.iter().map(|s| s as &dyn rusqlite::ToSql).collect();
379
380        let rows = stmt
381            .query_map(params.as_slice(), |row| {
382                Ok(TaxRate {
383                    id: parse_uuid_row(&row.get::<_, String>(0)?, "tax_rate", "id")?,
384                    jurisdiction_id: parse_uuid_row(
385                        &row.get::<_, String>(1)?,
386                        "tax_rate",
387                        "jurisdiction_id",
388                    )?,
389                    tax_type: parse_enum_row(&row.get::<_, String>(2)?, "tax_rate", "tax_type")?,
390                    product_category: parse_enum_row(
391                        &row.get::<_, String>(3)?,
392                        "tax_rate",
393                        "product_category",
394                    )?,
395                    rate: parse_decimal_row(&row.get::<_, String>(4)?, "tax_rate", "rate")?,
396                    name: row.get(5)?,
397                    description: row.get(6)?,
398                    is_compound: row.get::<_, i32>(7)? != 0,
399                    priority: row.get(8)?,
400                    threshold_min: parse_decimal_opt_row(
401                        row.get::<_, Option<String>>(9)?,
402                        "tax_rate",
403                        "threshold_min",
404                    )?,
405                    threshold_max: parse_decimal_opt_row(
406                        row.get::<_, Option<String>>(10)?,
407                        "tax_rate",
408                        "threshold_max",
409                    )?,
410                    fixed_amount: parse_decimal_opt_row(
411                        row.get::<_, Option<String>>(11)?,
412                        "tax_rate",
413                        "fixed_amount",
414                    )?,
415                    effective_from: parse_date_row(
416                        &row.get::<_, String>(12)?,
417                        "tax_rate",
418                        "effective_from",
419                    )?,
420                    effective_to: Self::parse_date_opt(
421                        row.get::<_, Option<String>>(13)?,
422                        "tax_rate",
423                        "effective_to",
424                    )?,
425                    active: row.get::<_, i32>(14)? != 0,
426                    created_at: parse_datetime_row(
427                        &row.get::<_, String>(15)?,
428                        "tax_rate",
429                        "created_at",
430                    )?,
431                    updated_at: parse_datetime_row(
432                        &row.get::<_, String>(16)?,
433                        "tax_rate",
434                        "updated_at",
435                    )?,
436                })
437            })
438            .map_err(map_db_error)?;
439
440        rows.collect::<std::result::Result<Vec<_>, _>>().map_err(map_db_error)
441    }
442
443    /// Get rates for a jurisdiction and product category
444    pub fn get_rates_for_address(
445        &self,
446        address: &TaxAddress,
447        category: ProductTaxCategory,
448        date: NaiveDate,
449    ) -> Result<Vec<TaxRate>> {
450        // Find applicable jurisdictions (country, state, etc.)
451        let mut jurisdiction_ids = Vec::new();
452
453        // Get country jurisdiction
454        if let Some(country) = self.get_jurisdiction_by_code(&address.country)? {
455            jurisdiction_ids.push(country.id);
456        }
457
458        // Get state jurisdiction if applicable
459        if let Some(state) = &address.state {
460            let state_code = format!("{}-{}", address.country, state);
461            if let Some(state_jurisdiction) = self.get_jurisdiction_by_code(&state_code)? {
462                jurisdiction_ids.push(state_jurisdiction.id);
463            }
464        }
465
466        if jurisdiction_ids.is_empty() {
467            return Ok(Vec::new());
468        }
469
470        // Get all applicable rates
471        let mut all_rates = Vec::new();
472        for jurisdiction_id in jurisdiction_ids {
473            let filter = TaxRateFilter {
474                jurisdiction_id: Some(jurisdiction_id),
475                product_category: Some(category),
476                active_only: true,
477                effective_date: Some(date),
478                ..Default::default()
479            };
480            let rates = self.list_rates(filter)?;
481            all_rates.extend(rates);
482        }
483
484        // Sort by priority
485        all_rates.sort_by_key(|r| r.priority);
486        Ok(all_rates)
487    }
488
489    /// Create a new tax rate
490    pub fn create_rate(&self, input: CreateTaxRate) -> Result<TaxRate> {
491        let id = Uuid::new_v4();
492        let now = Utc::now();
493
494        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
495
496        conn.execute(
497            "INSERT INTO tax_rates (id, jurisdiction_id, tax_type, product_category, rate, name, description, is_compound, priority, threshold_min, threshold_max, fixed_amount, effective_from, effective_to, active, created_at, updated_at)
498             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)",
499            params![
500                id.to_string(),
501                input.jurisdiction_id.to_string(),
502                input.tax_type.to_string(),
503                input.product_category.to_string(),
504                input.rate.to_string(),
505                input.name,
506                input.description,
507                i32::from(input.is_compound),
508                input.priority,
509                input.threshold_min.map(|d| d.to_string()),
510                input.threshold_max.map(|d| d.to_string()),
511                input.fixed_amount.map(|d| d.to_string()),
512                input.effective_from.to_string(),
513                input.effective_to.map(|d| d.to_string()),
514                now.to_rfc3339(),
515                now.to_rfc3339()
516            ],
517        ).map_err(map_db_error)?;
518
519        drop(conn);
520
521        self.get_rate(id)?.ok_or(CommerceError::NotFound)
522    }
523}
524
525// ============================================================================
526// Tax Exemption Operations
527// ============================================================================
528
529impl SqliteTaxRepository {
530    /// Get an exemption by ID
531    pub fn get_exemption(&self, id: Uuid) -> Result<Option<TaxExemption>> {
532        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
533
534        let result = conn.query_row(
535            "SELECT id, customer_id, exemption_type, certificate_number, issuing_authority, jurisdiction_ids, exempt_categories, effective_from, expires_at, verified, verified_at, notes, active, created_at, updated_at
536             FROM tax_exemptions WHERE id = ?",
537            params![id.to_string()],
538            |row| {
539                let jurisdiction_ids_json: String = row.get(5)?;
540                let raw_jurisdiction_ids: Vec<String> = parse_json_row(
541                    &jurisdiction_ids_json,
542                    "tax_exemption",
543                    "jurisdiction_ids",
544                )?;
545                let jurisdiction_ids = raw_jurisdiction_ids
546                    .into_iter()
547                    .map(|value| parse_uuid_row(&value, "tax_exemption", "jurisdiction_ids"))
548                    .collect::<rusqlite::Result<Vec<_>>>()?;
549
550                let categories_json: String = row.get(6)?;
551                let raw_categories: Vec<String> =
552                    parse_json_row(&categories_json, "tax_exemption", "exempt_categories")?;
553                let exempt_categories = raw_categories
554                    .into_iter()
555                    .map(|value| parse_enum_row(&value, "tax_exemption", "exempt_categories"))
556                    .collect::<rusqlite::Result<Vec<_>>>()?;
557
558                Ok(TaxExemption {
559                    id: parse_uuid_row(&row.get::<_, String>(0)?, "tax_exemption", "id")?,
560                    customer_id: parse_uuid_row(
561                        &row.get::<_, String>(1)?,
562                        "tax_exemption",
563                        "customer_id",
564                    )?,
565                    exemption_type: parse_enum_row(
566                        &row.get::<_, String>(2)?,
567                        "tax_exemption",
568                        "exemption_type",
569                    )?,
570                    certificate_number: row.get(3)?,
571                    issuing_authority: row.get(4)?,
572                    jurisdiction_ids,
573                    exempt_categories,
574                    effective_from: parse_date_row(
575                        &row.get::<_, String>(7)?,
576                        "tax_exemption",
577                        "effective_from",
578                    )?,
579                    expires_at: Self::parse_date_opt(
580                        row.get::<_, Option<String>>(8)?,
581                        "tax_exemption",
582                        "expires_at",
583                    )?,
584                    verified: row.get::<_, i32>(9)? != 0,
585                    verified_at: parse_datetime_opt_row(
586                        row.get::<_, Option<String>>(10)?,
587                        "tax_exemption",
588                        "verified_at",
589                    )?,
590                    notes: row.get(11)?,
591                    active: row.get::<_, i32>(12)? != 0,
592                    created_at: parse_datetime_row(
593                        &row.get::<_, String>(13)?,
594                        "tax_exemption",
595                        "created_at",
596                    )?,
597                    updated_at: parse_datetime_row(
598                        &row.get::<_, String>(14)?,
599                        "tax_exemption",
600                        "updated_at",
601                    )?,
602                })
603            },
604        );
605
606        match result {
607            Ok(exemption) => Ok(Some(exemption)),
608            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
609            Err(e) => Err(map_db_error(e)),
610        }
611    }
612
613    /// Get active exemptions for a customer
614    pub fn get_customer_exemptions(&self, customer_id: Uuid) -> Result<Vec<TaxExemption>> {
615        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
616        let today = Utc::now().date_naive().to_string();
617
618        let mut stmt = conn.prepare(
619            "SELECT id, customer_id, exemption_type, certificate_number, issuing_authority, jurisdiction_ids, exempt_categories, effective_from, expires_at, verified, verified_at, notes, active, created_at, updated_at
620             FROM tax_exemptions
621             WHERE customer_id = ? AND active = 1 AND effective_from <= ? AND (expires_at IS NULL OR expires_at >= ?)"
622        ).map_err(map_db_error)?;
623
624        let rows = stmt
625            .query_map(params![customer_id.to_string(), &today, &today], |row| {
626                let jurisdiction_ids_json: String = row.get(5)?;
627                let raw_jurisdiction_ids: Vec<String> =
628                    parse_json_row(&jurisdiction_ids_json, "tax_exemption", "jurisdiction_ids")?;
629                let jurisdiction_ids = raw_jurisdiction_ids
630                    .into_iter()
631                    .map(|value| parse_uuid_row(&value, "tax_exemption", "jurisdiction_ids"))
632                    .collect::<rusqlite::Result<Vec<_>>>()?;
633
634                let categories_json: String = row.get(6)?;
635                let raw_categories: Vec<String> =
636                    parse_json_row(&categories_json, "tax_exemption", "exempt_categories")?;
637                let exempt_categories = raw_categories
638                    .into_iter()
639                    .map(|value| parse_enum_row(&value, "tax_exemption", "exempt_categories"))
640                    .collect::<rusqlite::Result<Vec<_>>>()?;
641
642                Ok(TaxExemption {
643                    id: parse_uuid_row(&row.get::<_, String>(0)?, "tax_exemption", "id")?,
644                    customer_id: parse_uuid_row(
645                        &row.get::<_, String>(1)?,
646                        "tax_exemption",
647                        "customer_id",
648                    )?,
649                    exemption_type: parse_enum_row(
650                        &row.get::<_, String>(2)?,
651                        "tax_exemption",
652                        "exemption_type",
653                    )?,
654                    certificate_number: row.get(3)?,
655                    issuing_authority: row.get(4)?,
656                    jurisdiction_ids,
657                    exempt_categories,
658                    effective_from: parse_date_row(
659                        &row.get::<_, String>(7)?,
660                        "tax_exemption",
661                        "effective_from",
662                    )?,
663                    expires_at: Self::parse_date_opt(
664                        row.get::<_, Option<String>>(8)?,
665                        "tax_exemption",
666                        "expires_at",
667                    )?,
668                    verified: row.get::<_, i32>(9)? != 0,
669                    verified_at: parse_datetime_opt_row(
670                        row.get::<_, Option<String>>(10)?,
671                        "tax_exemption",
672                        "verified_at",
673                    )?,
674                    notes: row.get(11)?,
675                    active: row.get::<_, i32>(12)? != 0,
676                    created_at: parse_datetime_row(
677                        &row.get::<_, String>(13)?,
678                        "tax_exemption",
679                        "created_at",
680                    )?,
681                    updated_at: parse_datetime_row(
682                        &row.get::<_, String>(14)?,
683                        "tax_exemption",
684                        "updated_at",
685                    )?,
686                })
687            })
688            .map_err(map_db_error)?;
689
690        rows.collect::<std::result::Result<Vec<_>, _>>().map_err(map_db_error)
691    }
692
693    /// Create a new exemption
694    pub fn create_exemption(&self, input: CreateTaxExemption) -> Result<TaxExemption> {
695        let id = Uuid::new_v4();
696        let now = Utc::now();
697
698        let jurisdiction_ids_json = serde_json::to_string(
699            &input
700                .jurisdiction_ids
701                .iter()
702                .map(std::string::ToString::to_string)
703                .collect::<Vec<_>>(),
704        )
705        .map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
706
707        let categories_json = serde_json::to_string(
708            &input
709                .exempt_categories
710                .iter()
711                .map(std::string::ToString::to_string)
712                .collect::<Vec<_>>(),
713        )
714        .map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
715
716        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
717
718        conn.execute(
719            "INSERT INTO tax_exemptions (id, customer_id, exemption_type, certificate_number, issuing_authority, jurisdiction_ids, exempt_categories, effective_from, expires_at, verified, notes, active, created_at, updated_at)
720             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, 1, ?, ?)",
721            params![
722                id.to_string(),
723                input.customer_id.to_string(),
724                input.exemption_type.to_string(),
725                input.certificate_number,
726                input.issuing_authority,
727                jurisdiction_ids_json,
728                categories_json,
729                input.effective_from.to_string(),
730                input.expires_at.map(|d| d.to_string()),
731                input.notes,
732                now.to_rfc3339(),
733                now.to_rfc3339()
734            ],
735        ).map_err(map_db_error)?;
736
737        drop(conn);
738
739        self.get_exemption(id)?.ok_or(CommerceError::NotFound)
740    }
741}
742
743// ============================================================================
744// Tax Settings Operations
745// ============================================================================
746
747impl SqliteTaxRepository {
748    /// Get tax settings
749    pub fn get_settings(&self) -> Result<TaxSettings> {
750        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
751
752        let result = conn.query_row(
753            "SELECT id, enabled, calculation_method, compound_method, tax_shipping, tax_handling, tax_gift_wrap, origin_address, default_product_category, rounding_mode, decimal_places, validate_addresses, tax_provider, provider_credentials, created_at, updated_at
754             FROM tax_settings WHERE id = 'default'",
755            [],
756            |row| {
757                let origin_address_json: Option<String> = row.get(7)?;
758                let origin_address: Option<TaxAddress> =
759                    parse_json_opt_row(origin_address_json, "tax_settings", "origin_address")?;
760
761                let id_str: String = row.get(0)?;
762                let id = if id_str == "default" {
763                    Uuid::nil()
764                } else {
765                    parse_uuid_row(&id_str, "tax_settings", "id")?
766                };
767
768                Ok(TaxSettings {
769                    id,
770                    enabled: row.get::<_, i32>(1)? != 0,
771                    calculation_method: parse_enum_row(
772                        &row.get::<_, String>(2)?,
773                        "tax_settings",
774                        "calculation_method",
775                    )?,
776                    compound_method: parse_enum_row(
777                        &row.get::<_, String>(3)?,
778                        "tax_settings",
779                        "compound_method",
780                    )?,
781                    tax_shipping: row.get::<_, i32>(4)? != 0,
782                    tax_handling: row.get::<_, i32>(5)? != 0,
783                    tax_gift_wrap: row.get::<_, i32>(6)? != 0,
784                    origin_address,
785                    default_product_category: parse_enum_row(
786                        &row.get::<_, String>(8)?,
787                        "tax_settings",
788                        "default_product_category",
789                    )?,
790                    rounding_mode: row.get(9)?,
791                    decimal_places: row.get(10)?,
792                    validate_addresses: row.get::<_, i32>(11)? != 0,
793                    tax_provider: row.get(12)?,
794                    provider_credentials: row.get(13)?,
795                    created_at: parse_datetime_row(
796                        &row.get::<_, String>(14)?,
797                        "tax_settings",
798                        "created_at",
799                    )?,
800                    updated_at: parse_datetime_row(
801                        &row.get::<_, String>(15)?,
802                        "tax_settings",
803                        "updated_at",
804                    )?,
805                })
806            },
807        );
808
809        match result {
810            Ok(settings) => Ok(settings),
811            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(TaxSettings::default()),
812            Err(e) => Err(map_db_error(e)),
813        }
814    }
815
816    /// Update tax settings
817    pub fn update_settings(&self, settings: TaxSettings) -> Result<TaxSettings> {
818        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
819
820        let origin_address_json = settings
821            .origin_address
822            .as_ref()
823            .map(serde_json::to_string)
824            .transpose()
825            .map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
826
827        let calc_method = settings.calculation_method.to_string();
828        let compound_method = settings.compound_method.to_string();
829
830        conn.execute(
831            "INSERT INTO tax_settings (id, enabled, calculation_method, compound_method, tax_shipping, tax_handling, tax_gift_wrap, origin_address, default_product_category, rounding_mode, decimal_places, validate_addresses, tax_provider, provider_credentials, updated_at)
832             VALUES ('default', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
833             ON CONFLICT (id) DO UPDATE SET
834                enabled = excluded.enabled,
835                calculation_method = excluded.calculation_method,
836                compound_method = excluded.compound_method,
837                tax_shipping = excluded.tax_shipping,
838                tax_handling = excluded.tax_handling,
839                tax_gift_wrap = excluded.tax_gift_wrap,
840                origin_address = excluded.origin_address,
841                default_product_category = excluded.default_product_category,
842                rounding_mode = excluded.rounding_mode,
843                decimal_places = excluded.decimal_places,
844                validate_addresses = excluded.validate_addresses,
845                tax_provider = excluded.tax_provider,
846                provider_credentials = excluded.provider_credentials,
847                updated_at = excluded.updated_at",
848            params![
849                i32::from(settings.enabled),
850                calc_method,
851                compound_method,
852                i32::from(settings.tax_shipping),
853                i32::from(settings.tax_handling),
854                i32::from(settings.tax_gift_wrap),
855                origin_address_json,
856                settings.default_product_category.to_string(),
857                settings.rounding_mode,
858                settings.decimal_places,
859                i32::from(settings.validate_addresses),
860                settings.tax_provider,
861                settings.provider_credentials
862            ],
863        ).map_err(map_db_error)?;
864
865        self.get_settings()
866    }
867}
868
869// ============================================================================
870// Tax Calculation
871// ============================================================================
872
873impl SqliteTaxRepository {
874    /// Calculate tax for a request
875    pub fn calculate_tax(&self, request: TaxCalculationRequest) -> Result<TaxCalculationResult> {
876        let settings = self.get_settings()?;
877        let now = Utc::now();
878        let transaction_date = request.transaction_date.unwrap_or_else(|| now.date_naive());
879
880        // Check for customer exemptions
881        let exemptions = if let Some(customer_id) = request.customer_id {
882            self.get_customer_exemptions(customer_id)?
883        } else {
884            Vec::new()
885        };
886
887        let mut subtotal = Decimal::ZERO;
888        let mut total_tax = Decimal::ZERO;
889        let mut line_item_taxes = Vec::new();
890        let mut tax_breakdown: Vec<TaxBreakdown> = Vec::new();
891        let mut jurisdictions_map = std::collections::HashMap::new();
892
893        // Calculate tax for each line item
894        for item in &request.line_items {
895            let mut line_amount = item.unit_price * item.quantity - item.discount_amount;
896            if line_amount < Decimal::ZERO {
897                line_amount = Decimal::ZERO;
898            }
899            subtotal += line_amount;
900
901            // Check if item is exempt due to customer exemption
902            let is_exempt = exemptions.iter().any(|e| {
903                e.exempt_categories.is_empty() || e.exempt_categories.contains(&item.tax_category)
904            });
905
906            if is_exempt || item.tax_category == ProductTaxCategory::Exempt {
907                line_item_taxes.push(LineItemTax {
908                    line_item_id: item.id.clone(),
909                    taxable_amount: line_amount,
910                    tax_amount: Decimal::ZERO,
911                    effective_rate: Decimal::ZERO,
912                    is_exempt: true,
913                    exemption_reason: Some("Customer exemption".to_string()),
914                    tax_details: Vec::new(),
915                });
916                continue;
917            }
918
919            // Get applicable tax rates
920            let rates = self.get_rates_for_address(
921                &request.shipping_address,
922                item.tax_category,
923                transaction_date,
924            )?;
925
926            let mut line_tax = Decimal::ZERO;
927            let mut line_tax_details = Vec::new();
928
929            for rate in &rates {
930                if line_amount <= Decimal::ZERO {
931                    continue;
932                }
933
934                if let Some(min) = rate.threshold_min {
935                    if line_amount < min {
936                        continue;
937                    }
938                }
939
940                let capped_base = match rate.threshold_max {
941                    Some(max) if line_amount > max => max,
942                    _ => line_amount,
943                };
944
945                if capped_base <= Decimal::ZERO {
946                    continue;
947                }
948
949                let taxable_amount = if rate.fixed_amount.is_some() {
950                    capped_base
951                } else if rate.is_compound {
952                    // Compound tax is applied on (subtotal + previous taxes)
953                    capped_base + line_tax
954                } else {
955                    capped_base
956                };
957
958                let rate_tax = if let Some(fixed) = rate.fixed_amount {
959                    fixed
960                } else {
961                    taxable_amount * rate.rate
962                };
963
964                line_tax += rate_tax;
965
966                // Get jurisdiction info
967                if let Some(jurisdiction) = self.get_jurisdiction(rate.jurisdiction_id)? {
968                    jurisdictions_map.entry(jurisdiction.id).or_insert_with(|| {
969                        JurisdictionSummary {
970                            id: jurisdiction.id,
971                            name: jurisdiction.name.clone(),
972                            code: jurisdiction.code.clone(),
973                            level: jurisdiction.level,
974                            total_rate: Decimal::ZERO,
975                            total_tax: Decimal::ZERO,
976                        }
977                    });
978
979                    if let Some(summary) = jurisdictions_map.get_mut(&jurisdiction.id) {
980                        summary.total_rate += rate.rate;
981                        summary.total_tax += rate_tax;
982                    }
983
984                    // Add to breakdown
985                    if let Some(existing) = tax_breakdown.iter_mut().find(|b| {
986                        b.jurisdiction_id == jurisdiction.id && b.tax_type == rate.tax_type
987                    }) {
988                        existing.taxable_amount += taxable_amount;
989                        existing.tax_amount += rate_tax;
990                    } else {
991                        tax_breakdown.push(TaxBreakdown {
992                            jurisdiction_id: jurisdiction.id,
993                            jurisdiction_name: jurisdiction.name.clone(),
994                            tax_type: rate.tax_type,
995                            rate_name: rate.name.clone(),
996                            rate: rate.rate,
997                            taxable_amount,
998                            tax_amount: rate_tax,
999                            is_compound: rate.is_compound,
1000                        });
1001                    }
1002
1003                    line_tax_details.push(TaxDetail {
1004                        tax_type: rate.tax_type,
1005                        jurisdiction_name: jurisdiction.name,
1006                        rate: rate.rate,
1007                        amount: rate_tax,
1008                    });
1009                }
1010            }
1011
1012            let effective_rate =
1013                if line_amount.is_zero() { Decimal::ZERO } else { line_tax / line_amount };
1014
1015            total_tax += line_tax;
1016            line_item_taxes.push(LineItemTax {
1017                line_item_id: item.id.clone(),
1018                taxable_amount: line_amount,
1019                tax_amount: line_tax,
1020                effective_rate,
1021                is_exempt: false,
1022                exemption_reason: None,
1023                tax_details: line_tax_details,
1024            });
1025        }
1026
1027        // Calculate shipping tax if applicable
1028        let mut shipping_tax = Decimal::ZERO;
1029        if settings.tax_shipping {
1030            if let Some(mut shipping_amount) = request.shipping_amount {
1031                if shipping_amount < Decimal::ZERO {
1032                    shipping_amount = Decimal::ZERO;
1033                }
1034
1035                let shipping_rates = self.get_rates_for_address(
1036                    &request.shipping_address,
1037                    ProductTaxCategory::Standard,
1038                    transaction_date,
1039                )?;
1040                for rate in &shipping_rates {
1041                    if shipping_amount <= Decimal::ZERO {
1042                        continue;
1043                    }
1044
1045                    if let Some(min) = rate.threshold_min {
1046                        if shipping_amount < min {
1047                            continue;
1048                        }
1049                    }
1050
1051                    let capped_base = match rate.threshold_max {
1052                        Some(max) if shipping_amount > max => max,
1053                        _ => shipping_amount,
1054                    };
1055
1056                    if capped_base <= Decimal::ZERO {
1057                        continue;
1058                    }
1059
1060                    let taxable_amount = if rate.fixed_amount.is_some() {
1061                        capped_base
1062                    } else if rate.is_compound {
1063                        capped_base + shipping_tax
1064                    } else {
1065                        capped_base
1066                    };
1067
1068                    let rate_tax = if let Some(fixed) = rate.fixed_amount {
1069                        fixed
1070                    } else {
1071                        taxable_amount * rate.rate
1072                    };
1073
1074                    shipping_tax += rate_tax;
1075
1076                    if let Some(jurisdiction) = self.get_jurisdiction(rate.jurisdiction_id)? {
1077                        jurisdictions_map.entry(jurisdiction.id).or_insert_with(|| {
1078                            JurisdictionSummary {
1079                                id: jurisdiction.id,
1080                                name: jurisdiction.name.clone(),
1081                                code: jurisdiction.code.clone(),
1082                                level: jurisdiction.level,
1083                                total_rate: Decimal::ZERO,
1084                                total_tax: Decimal::ZERO,
1085                            }
1086                        });
1087
1088                        if let Some(summary) = jurisdictions_map.get_mut(&jurisdiction.id) {
1089                            summary.total_rate += rate.rate;
1090                            summary.total_tax += rate_tax;
1091                        }
1092
1093                        if let Some(existing) = tax_breakdown.iter_mut().find(|b| {
1094                            b.jurisdiction_id == jurisdiction.id && b.tax_type == rate.tax_type
1095                        }) {
1096                            existing.taxable_amount += taxable_amount;
1097                            existing.tax_amount += rate_tax;
1098                        } else {
1099                            tax_breakdown.push(TaxBreakdown {
1100                                jurisdiction_id: jurisdiction.id,
1101                                jurisdiction_name: jurisdiction.name.clone(),
1102                                tax_type: rate.tax_type,
1103                                rate_name: rate.name.clone(),
1104                                rate: rate.rate,
1105                                taxable_amount,
1106                                tax_amount: rate_tax,
1107                                is_compound: rate.is_compound,
1108                            });
1109                        }
1110                    }
1111                }
1112
1113                total_tax += shipping_tax;
1114            }
1115        }
1116
1117        // Round tax using the configured rounding mode.
1118        let decimal_places = settings.decimal_places as u32;
1119        let strategy = settings.rounding_strategy();
1120        let total_tax = total_tax.round_dp_with_strategy(decimal_places, strategy);
1121        let shipping_tax = shipping_tax.round_dp_with_strategy(decimal_places, strategy);
1122
1123        let total = subtotal + total_tax + request.shipping_amount.unwrap_or_default();
1124
1125        // Emit jurisdictions in a stable order (by code, then id) rather than
1126        // the HashMap's iteration order, which varies run-to-run and between the
1127        // SQLite and Postgres backends — the tax result must be deterministic.
1128        let mut jurisdictions: Vec<JurisdictionSummary> = jurisdictions_map.into_values().collect();
1129        jurisdictions.sort_by(|a, b| a.code.cmp(&b.code).then_with(|| a.id.cmp(&b.id)));
1130
1131        Ok(TaxCalculationResult {
1132            id: Uuid::new_v4(),
1133            total_tax,
1134            subtotal,
1135            total,
1136            shipping_tax,
1137            tax_breakdown,
1138            line_item_taxes,
1139            exemptions_applied: !exemptions.is_empty(),
1140            exemption_details: None, // Could populate if needed
1141            jurisdictions,
1142            calculated_at: now,
1143            is_estimate: true,
1144        })
1145    }
1146
1147    /// Save a tax calculation to the database
1148    pub fn save_calculation(
1149        &self,
1150        result: &TaxCalculationResult,
1151        order_id: Option<Uuid>,
1152        cart_id: Option<Uuid>,
1153        customer_id: Option<Uuid>,
1154        address: &TaxAddress,
1155        currency: &str,
1156    ) -> Result<()> {
1157        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
1158
1159        let address_json = serde_json::to_string(address)
1160            .map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
1161        let line_items_json = serde_json::to_string(&result.line_item_taxes)
1162            .map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
1163        let breakdown_json = serde_json::to_string(&result.tax_breakdown)
1164            .map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
1165        let exemption_json = result
1166            .exemption_details
1167            .as_ref()
1168            .map(serde_json::to_string)
1169            .transpose()
1170            .map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
1171
1172        conn.execute(
1173            "INSERT INTO tax_calculations (id, order_id, cart_id, customer_id, subtotal, total_tax, shipping_tax, total, currency, shipping_address, line_items, tax_breakdown, exemptions_applied, exemption_details, is_estimate, calculated_at)
1174             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1175            params![
1176                result.id.to_string(),
1177                order_id.map(|id| id.to_string()),
1178                cart_id.map(|id| id.to_string()),
1179                customer_id.map(|id| id.to_string()),
1180                result.subtotal.to_string(),
1181                result.total_tax.to_string(),
1182                result.shipping_tax.to_string(),
1183                result.total.to_string(),
1184                currency,
1185                address_json,
1186                line_items_json,
1187                breakdown_json,
1188                i32::from(result.exemptions_applied),
1189                exemption_json,
1190                i32::from(result.is_estimate),
1191                result.calculated_at.to_rfc3339()
1192            ],
1193        ).map_err(map_db_error)?;
1194
1195        Ok(())
1196    }
1197}
1198
1199impl TaxRepository for SqliteTaxRepository {
1200    fn create_jurisdiction(&self, input: CreateTaxJurisdiction) -> Result<TaxJurisdiction> {
1201        Self::create_jurisdiction(self, input)
1202    }
1203
1204    fn get_jurisdiction(&self, id: Uuid) -> Result<Option<TaxJurisdiction>> {
1205        Self::get_jurisdiction(self, id)
1206    }
1207
1208    fn get_jurisdiction_by_code(&self, code: &str) -> Result<Option<TaxJurisdiction>> {
1209        Self::get_jurisdiction_by_code(self, code)
1210    }
1211
1212    fn list_jurisdictions(&self, filter: TaxJurisdictionFilter) -> Result<Vec<TaxJurisdiction>> {
1213        Self::list_jurisdictions(self, filter)
1214    }
1215
1216    fn create_rate(&self, input: CreateTaxRate) -> Result<TaxRate> {
1217        Self::create_rate(self, input)
1218    }
1219
1220    fn get_rate(&self, id: Uuid) -> Result<Option<TaxRate>> {
1221        Self::get_rate(self, id)
1222    }
1223
1224    fn list_rates(&self, filter: TaxRateFilter) -> Result<Vec<TaxRate>> {
1225        Self::list_rates(self, filter)
1226    }
1227
1228    fn get_rates_for_address(
1229        &self,
1230        address: &TaxAddress,
1231        category: ProductTaxCategory,
1232        date: chrono::NaiveDate,
1233    ) -> Result<Vec<TaxRate>> {
1234        Self::get_rates_for_address(self, address, category, date)
1235    }
1236
1237    fn create_exemption(&self, input: CreateTaxExemption) -> Result<TaxExemption> {
1238        Self::create_exemption(self, input)
1239    }
1240
1241    fn get_exemption(&self, id: Uuid) -> Result<Option<TaxExemption>> {
1242        Self::get_exemption(self, id)
1243    }
1244
1245    fn get_customer_exemptions(&self, customer_id: Uuid) -> Result<Vec<TaxExemption>> {
1246        Self::get_customer_exemptions(self, customer_id)
1247    }
1248
1249    fn get_settings(&self) -> Result<TaxSettings> {
1250        Self::get_settings(self)
1251    }
1252
1253    fn update_settings(&self, settings: TaxSettings) -> Result<TaxSettings> {
1254        Self::update_settings(self, settings)
1255    }
1256
1257    fn calculate_tax(&self, request: TaxCalculationRequest) -> Result<TaxCalculationResult> {
1258        Self::calculate_tax(self, request)
1259    }
1260
1261    fn save_calculation(
1262        &self,
1263        result: &TaxCalculationResult,
1264        order_id: Option<Uuid>,
1265        cart_id: Option<Uuid>,
1266        customer_id: Option<Uuid>,
1267        address: &TaxAddress,
1268        currency: &str,
1269    ) -> Result<()> {
1270        Self::save_calculation(self, result, order_id, cart_id, customer_id, address, currency)
1271    }
1272}
1273
1274#[cfg(test)]
1275mod tests {
1276    use super::*;
1277    use crate::SqliteDatabase;
1278    use chrono::NaiveDate;
1279    use rust_decimal_macros::dec;
1280    use stateset_core::{
1281        CreateTaxExemption, CreateTaxJurisdiction, CreateTaxRate, CurrencyCode, ExemptionType,
1282        JurisdictionLevel, ProductTaxCategory, TaxAddress, TaxCalculationRequest,
1283        TaxJurisdictionFilter, TaxLineItem, TaxRateFilter, TaxType,
1284    };
1285
1286    fn fresh_repo() -> SqliteTaxRepository {
1287        SqliteDatabase::in_memory().expect("in-memory").tax()
1288    }
1289
1290    fn make_state_jur(repo: &SqliteTaxRepository, state: &str) -> TaxJurisdiction {
1291        // Use ZZ-* prefix so we don't collide with the 50 pre-seeded US states.
1292        repo.create_jurisdiction(CreateTaxJurisdiction {
1293            parent_id: None,
1294            name: format!("{state} Test State"),
1295            code: format!("ZZ-{state}"),
1296            level: JurisdictionLevel::State,
1297            country_code: "ZZ".into(),
1298            state_code: Some(state.into()),
1299            county: None,
1300            city: None,
1301            postal_codes: vec![],
1302        })
1303        .expect("create jurisdiction")
1304    }
1305
1306    fn make_rate(repo: &SqliteTaxRepository, jur_id: Uuid, rate: Decimal) -> TaxRate {
1307        repo.create_rate(CreateTaxRate {
1308            jurisdiction_id: jur_id,
1309            tax_type: TaxType::SalesTax,
1310            product_category: ProductTaxCategory::Standard,
1311            rate,
1312            name: "Sales Tax".into(),
1313            description: None,
1314            is_compound: false,
1315            priority: 1,
1316            threshold_min: None,
1317            threshold_max: None,
1318            fixed_amount: None,
1319            effective_from: NaiveDate::from_ymd_opt(2026, 1, 1).expect("date"),
1320            effective_to: None,
1321        })
1322        .expect("create rate")
1323    }
1324
1325    #[test]
1326    fn create_jurisdiction_round_trips() {
1327        let repo = fresh_repo();
1328        let j = make_state_jur(&repo, "CA");
1329        assert_eq!(j.code, "ZZ-CA");
1330        assert_eq!(j.level, JurisdictionLevel::State);
1331
1332        let by_id = repo.get_jurisdiction(j.id).expect("ok").expect("found");
1333        assert_eq!(by_id.id, j.id);
1334        let by_code = repo.get_jurisdiction_by_code("ZZ-CA").expect("ok").expect("found");
1335        assert_eq!(by_code.id, j.id);
1336        assert!(repo.get_jurisdiction_by_code("missing-xyz").expect("ok").is_none());
1337    }
1338
1339    #[test]
1340    fn list_jurisdictions_applies_limit_and_offset() {
1341        let repo = fresh_repo();
1342        make_state_jur(&repo, "AA");
1343        make_state_jur(&repo, "BB");
1344        make_state_jur(&repo, "CC");
1345
1346        let base =
1347            || TaxJurisdictionFilter { country_code: Some("ZZ".into()), ..Default::default() };
1348        let all = repo.list_jurisdictions(base()).expect("list all");
1349        assert_eq!(all.len(), 3);
1350
1351        let page = repo
1352            .list_jurisdictions(TaxJurisdictionFilter { limit: Some(2), ..base() })
1353            .expect("limited");
1354        assert_eq!(page.len(), 2, "limit must bound the result set");
1355        assert_eq!(page[0].id, all[0].id);
1356
1357        let rest = repo
1358            .list_jurisdictions(TaxJurisdictionFilter { limit: Some(2), offset: Some(2), ..base() })
1359            .expect("offset");
1360        assert_eq!(rest.len(), 1);
1361        assert_eq!(rest[0].id, all[2].id);
1362    }
1363
1364    #[test]
1365    fn list_rates_applies_limit_and_offset() {
1366        let repo = fresh_repo();
1367        let jur = make_state_jur(&repo, "LR");
1368        for _ in 0..3 {
1369            make_rate(&repo, jur.id, dec!(0.05));
1370        }
1371
1372        let base = || TaxRateFilter { jurisdiction_id: Some(jur.id), ..Default::default() };
1373        let all = repo.list_rates(base()).expect("list all");
1374        assert_eq!(all.len(), 3);
1375
1376        let page = repo.list_rates(TaxRateFilter { limit: Some(2), ..base() }).expect("limited");
1377        assert_eq!(page.len(), 2, "limit must bound the result set");
1378
1379        let rest = repo
1380            .list_rates(TaxRateFilter { limit: Some(2), offset: Some(2), ..base() })
1381            .expect("offset");
1382        assert_eq!(rest.len(), 1);
1383    }
1384
1385    #[test]
1386    fn list_jurisdictions_filters_by_country_and_state() {
1387        let repo = fresh_repo();
1388        make_state_jur(&repo, "AA");
1389        make_state_jur(&repo, "BB");
1390        repo.create_jurisdiction(CreateTaxJurisdiction {
1391            parent_id: None,
1392            name: "Test BC".into(),
1393            code: "YY-BC".into(),
1394            level: JurisdictionLevel::State,
1395            country_code: "YY".into(),
1396            state_code: Some("BC".into()),
1397            county: None,
1398            city: None,
1399            postal_codes: vec![],
1400        })
1401        .expect("yy-bc");
1402
1403        let zz = repo
1404            .list_jurisdictions(TaxJurisdictionFilter {
1405                country_code: Some("ZZ".into()),
1406                ..Default::default()
1407            })
1408            .expect("zz");
1409        assert_eq!(zz.len(), 2);
1410
1411        let zz_aa = repo
1412            .list_jurisdictions(TaxJurisdictionFilter {
1413                country_code: Some("ZZ".into()),
1414                state_code: Some("AA".into()),
1415                ..Default::default()
1416            })
1417            .expect("aa");
1418        assert_eq!(zz_aa.len(), 1);
1419        assert_eq!(zz_aa[0].state_code.as_deref(), Some("AA"));
1420    }
1421
1422    #[test]
1423    fn list_jurisdictions_orders_by_country_then_state() {
1424        let repo = fresh_repo();
1425        // Two jurisdictions in different countries where ordering by country and
1426        // ordering by name DISAGREE, so the sort key is observable. Postgres used
1427        // to order only by (level, name), which would put XB (Apple) first.
1428        repo.create_jurisdiction(CreateTaxJurisdiction {
1429            parent_id: None,
1430            name: "Zebra".into(),
1431            code: "XA-1".into(),
1432            level: JurisdictionLevel::State,
1433            country_code: "XA".into(),
1434            state_code: Some("X1".into()),
1435            county: None,
1436            city: None,
1437            postal_codes: vec![],
1438        })
1439        .expect("xa");
1440        repo.create_jurisdiction(CreateTaxJurisdiction {
1441            parent_id: None,
1442            name: "Apple".into(),
1443            code: "XB-1".into(),
1444            level: JurisdictionLevel::State,
1445            country_code: "XB".into(),
1446            state_code: Some("X2".into()),
1447            county: None,
1448            city: None,
1449            postal_codes: vec![],
1450        })
1451        .expect("xb");
1452
1453        let all = repo.list_jurisdictions(TaxJurisdictionFilter::default()).expect("list");
1454        let mine: Vec<&str> = all
1455            .iter()
1456            .filter(|j| j.country_code == "XA" || j.country_code == "XB")
1457            .map(|j| j.country_code.as_str())
1458            .collect();
1459        assert_eq!(mine, vec!["XA", "XB"], "must order by country_code, not by name");
1460    }
1461
1462    #[test]
1463    fn create_rate_round_trips() {
1464        let repo = fresh_repo();
1465        let j = make_state_jur(&repo, "CA");
1466        let r = make_rate(&repo, j.id, dec!(0.0725));
1467        assert_eq!(r.rate, dec!(0.0725));
1468        let by_id = repo.get_rate(r.id).expect("ok").expect("found");
1469        assert_eq!(by_id.id, r.id);
1470    }
1471
1472    fn single_item_request(
1473        country: &str,
1474        state: &str,
1475        unit_price: Decimal,
1476    ) -> TaxCalculationRequest {
1477        TaxCalculationRequest {
1478            line_items: vec![TaxLineItem {
1479                id: "line-1".into(),
1480                sku: None,
1481                product_id: None,
1482                quantity: dec!(1),
1483                unit_price,
1484                discount_amount: Decimal::ZERO,
1485                tax_category: ProductTaxCategory::Standard,
1486                tax_code: None,
1487                description: None,
1488            }],
1489            shipping_address: TaxAddress {
1490                line1: None,
1491                line2: None,
1492                city: None,
1493                state: Some(state.into()),
1494                postal_code: None,
1495                country: country.into(),
1496            },
1497            billing_address: None,
1498            customer_id: None,
1499            shipping_amount: None,
1500            currency: CurrencyCode::USD,
1501            transaction_date: Some(NaiveDate::from_ymd_opt(2026, 6, 1).expect("date")),
1502            prices_include_tax: false,
1503        }
1504    }
1505
1506    #[test]
1507    fn calculate_tax_honors_configured_rounding_mode() {
1508        // A $2.50 line at 5% yields exactly $0.125 of tax — a rounding midpoint.
1509        // half_up rounds it to 0.13; half_even (banker's, required by some
1510        // jurisdictions) rounds to 0.12 because the retained digit (2) is even.
1511        // The configured rounding_mode must actually change the result.
1512        let repo = fresh_repo();
1513        let j = make_state_jur(&repo, "CA");
1514        make_rate(&repo, j.id, dec!(0.05));
1515        let request = single_item_request("ZZ", "CA", dec!(2.50));
1516
1517        let mut settings = repo.get_settings().expect("settings");
1518        settings.rounding_mode = "half_even".into();
1519        repo.update_settings(settings).expect("update settings");
1520        let even = repo.calculate_tax(request.clone()).expect("calc");
1521        assert_eq!(
1522            even.total_tax,
1523            dec!(0.12),
1524            "half_even must round $0.125 down to 0.12 (retained digit is even): {even:?}"
1525        );
1526
1527        let mut settings = repo.get_settings().expect("settings");
1528        settings.rounding_mode = "half_up".into();
1529        repo.update_settings(settings).expect("update settings");
1530        let up = repo.calculate_tax(request).expect("calc");
1531        assert_eq!(up.total_tax, dec!(0.13), "half_up must round $0.125 up to 0.13: {up:?}");
1532    }
1533
1534    #[test]
1535    fn list_rates_filters_by_jurisdiction() {
1536        let repo = fresh_repo();
1537        let j_a = make_state_jur(&repo, "AA");
1538        let j_b = make_state_jur(&repo, "BB");
1539        make_rate(&repo, j_a.id, dec!(0.0725));
1540        make_rate(&repo, j_a.id, dec!(0.01));
1541        make_rate(&repo, j_b.id, dec!(0.04));
1542
1543        let a_rates = repo
1544            .list_rates(TaxRateFilter { jurisdiction_id: Some(j_a.id), ..Default::default() })
1545            .expect("a");
1546        assert_eq!(a_rates.len(), 2);
1547    }
1548
1549    #[test]
1550    fn calculate_tax_returns_jurisdictions_in_stable_order() {
1551        // The jurisdictions summary must be emitted in a stable, backend-agnostic
1552        // order (by code) rather than the tax engine's internal HashMap order,
1553        // so the same input always produces the same result.
1554        let repo = fresh_repo();
1555        // Country-level ZZ plus state-level ZZ-CA, each taxing the item.
1556        let country = repo
1557            .create_jurisdiction(CreateTaxJurisdiction {
1558                parent_id: None,
1559                name: "ZZ Country".into(),
1560                code: "ZZ".into(),
1561                level: JurisdictionLevel::Country,
1562                country_code: "ZZ".into(),
1563                state_code: None,
1564                county: None,
1565                city: None,
1566                postal_codes: vec![],
1567            })
1568            .expect("create country");
1569        make_rate(&repo, country.id, dec!(0.05));
1570        let state = make_state_jur(&repo, "CA"); // code ZZ-CA
1571        make_rate(&repo, state.id, dec!(0.03));
1572
1573        let result =
1574            repo.calculate_tax(single_item_request("ZZ", "CA", dec!(100.00))).expect("calc");
1575
1576        let codes: Vec<&str> = result.jurisdictions.iter().map(|j| j.code.as_str()).collect();
1577        assert_eq!(
1578            codes,
1579            vec!["ZZ", "ZZ-CA"],
1580            "jurisdictions must be returned in stable code order: {codes:?}"
1581        );
1582        assert!(
1583            result.jurisdictions.windows(2).all(|w| w[0].code <= w[1].code),
1584            "jurisdictions must be sorted by code"
1585        );
1586    }
1587
1588    #[test]
1589    fn create_exemption_requires_existing_customer() {
1590        // tax_exemptions has a FK to customers — creating against a nonexistent
1591        // customer must surface a clear validation error rather than silently
1592        // inserting an orphan row.
1593        let repo = fresh_repo();
1594        let j = make_state_jur(&repo, "AA");
1595        let result = repo.create_exemption(CreateTaxExemption {
1596            customer_id: Uuid::new_v4(),
1597            exemption_type: ExemptionType::Resale,
1598            certificate_number: Some("RES-12345".into()),
1599            issuing_authority: None,
1600            jurisdiction_ids: vec![j.id],
1601            exempt_categories: vec![ProductTaxCategory::Standard],
1602            effective_from: NaiveDate::from_ymd_opt(2026, 1, 1).expect("date"),
1603            expires_at: Some(NaiveDate::from_ymd_opt(2027, 1, 1).expect("date")),
1604            notes: None,
1605        });
1606        assert!(result.is_err(), "expected FK rejection for unknown customer");
1607    }
1608
1609    #[test]
1610    fn get_customer_exemptions_unknown_customer_is_empty() {
1611        let repo = fresh_repo();
1612        let exemptions = repo.get_customer_exemptions(Uuid::new_v4()).expect("ok");
1613        assert!(exemptions.is_empty());
1614    }
1615
1616    #[test]
1617    fn get_settings_returns_defaults() {
1618        let repo = fresh_repo();
1619        let _ = repo.get_settings().expect("settings");
1620    }
1621
1622    #[test]
1623    fn get_jurisdiction_unknown_id_returns_none() {
1624        let repo = fresh_repo();
1625        assert!(repo.get_jurisdiction(Uuid::new_v4()).expect("ok").is_none());
1626    }
1627
1628    #[test]
1629    fn get_rate_unknown_id_returns_none() {
1630        let repo = fresh_repo();
1631        assert!(repo.get_rate(Uuid::new_v4()).expect("ok").is_none());
1632    }
1633
1634    #[test]
1635    fn get_exemption_unknown_id_returns_none() {
1636        let repo = fresh_repo();
1637        assert!(repo.get_exemption(Uuid::new_v4()).expect("ok").is_none());
1638    }
1639}