1use chrono::NaiveDate;
13use rust_decimal::Decimal;
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
22#[serde(rename_all = "snake_case")]
23pub enum JurisdictionType {
24 #[default]
26 Federal,
27 State,
29 Local,
31 Municipal,
33 Supranational,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
39#[serde(rename_all = "snake_case")]
40pub enum TaxType {
41 #[default]
43 Vat,
44 Gst,
46 SalesTax,
48 IncomeTax,
50 WithholdingTax,
52 PayrollTax,
54 ExciseTax,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
60#[serde(rename_all = "snake_case")]
61pub enum TaxableDocumentType {
62 #[default]
64 VendorInvoice,
65 CustomerInvoice,
67 JournalEntry,
69 Payment,
71 PayrollRun,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
77#[serde(rename_all = "snake_case")]
78pub enum TaxReturnType {
79 #[default]
81 VatReturn,
82 IncomeTax,
84 WithholdingRemittance,
86 PayrollTax,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
92#[serde(rename_all = "snake_case")]
93pub enum TaxReturnStatus {
94 #[default]
96 Draft,
97 Filed,
99 Assessed,
101 Paid,
103 Amended,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
109#[serde(rename_all = "snake_case")]
110pub enum WithholdingType {
111 DividendWithholding,
113 RoyaltyWithholding,
115 #[default]
117 ServiceWithholding,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
122#[serde(rename_all = "snake_case")]
123pub enum TaxMeasurementMethod {
124 #[default]
126 MostLikelyAmount,
127 ExpectedValue,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct TaxJurisdiction {
138 pub id: String,
140 pub name: String,
142 pub country_code: String,
144 pub region_code: Option<String>,
146 pub jurisdiction_type: JurisdictionType,
148 pub parent_jurisdiction_id: Option<String>,
150 pub vat_registered: bool,
152}
153
154impl TaxJurisdiction {
155 pub fn new(
157 id: impl Into<String>,
158 name: impl Into<String>,
159 country_code: impl Into<String>,
160 jurisdiction_type: JurisdictionType,
161 ) -> Self {
162 Self {
163 id: id.into(),
164 name: name.into(),
165 country_code: country_code.into(),
166 region_code: None,
167 jurisdiction_type,
168 parent_jurisdiction_id: None,
169 vat_registered: false,
170 }
171 }
172
173 pub fn with_region_code(mut self, region_code: impl Into<String>) -> Self {
175 self.region_code = Some(region_code.into());
176 self
177 }
178
179 pub fn with_parent_jurisdiction_id(mut self, parent_id: impl Into<String>) -> Self {
181 self.parent_jurisdiction_id = Some(parent_id.into());
182 self
183 }
184
185 pub fn with_vat_registered(mut self, registered: bool) -> Self {
187 self.vat_registered = registered;
188 self
189 }
190
191 pub fn is_subnational(&self) -> bool {
193 matches!(
194 self.jurisdiction_type,
195 JurisdictionType::State | JurisdictionType::Local | JurisdictionType::Municipal
196 )
197 }
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct TaxCode {
203 pub id: String,
205 pub code: String,
207 pub description: String,
209 pub tax_type: TaxType,
211 #[serde(with = "rust_decimal::serde::str")]
213 pub rate: Decimal,
214 pub jurisdiction_id: String,
216 pub effective_date: NaiveDate,
218 pub expiry_date: Option<NaiveDate>,
220 pub is_reverse_charge: bool,
222 pub is_exempt: bool,
224}
225
226impl TaxCode {
227 #[allow(clippy::too_many_arguments)]
229 pub fn new(
230 id: impl Into<String>,
231 code: impl Into<String>,
232 description: impl Into<String>,
233 tax_type: TaxType,
234 rate: Decimal,
235 jurisdiction_id: impl Into<String>,
236 effective_date: NaiveDate,
237 ) -> Self {
238 Self {
239 id: id.into(),
240 code: code.into(),
241 description: description.into(),
242 tax_type,
243 rate,
244 jurisdiction_id: jurisdiction_id.into(),
245 effective_date,
246 expiry_date: None,
247 is_reverse_charge: false,
248 is_exempt: false,
249 }
250 }
251
252 pub fn with_expiry_date(mut self, expiry: NaiveDate) -> Self {
254 self.expiry_date = Some(expiry);
255 self
256 }
257
258 pub fn with_reverse_charge(mut self, reverse_charge: bool) -> Self {
260 self.is_reverse_charge = reverse_charge;
261 self
262 }
263
264 pub fn with_exempt(mut self, exempt: bool) -> Self {
266 self.is_exempt = exempt;
267 self
268 }
269
270 pub fn tax_amount(&self, taxable_amount: Decimal) -> Decimal {
275 if self.is_exempt {
276 return Decimal::ZERO;
277 }
278 (taxable_amount * self.rate).round_dp(2)
279 }
280
281 pub fn is_active(&self, date: NaiveDate) -> bool {
286 if date < self.effective_date {
287 return false;
288 }
289 match self.expiry_date {
290 Some(expiry) => date < expiry,
291 None => true,
292 }
293 }
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct TaxLine {
299 pub id: String,
301 pub document_type: TaxableDocumentType,
303 pub document_id: String,
305 pub line_number: u32,
307 pub tax_code_id: String,
309 pub jurisdiction_id: String,
311 #[serde(with = "rust_decimal::serde::str")]
313 pub taxable_amount: Decimal,
314 #[serde(with = "rust_decimal::serde::str")]
316 pub tax_amount: Decimal,
317 pub is_deductible: bool,
319 pub is_reverse_charge: bool,
321 pub is_self_assessed: bool,
323}
324
325impl TaxLine {
326 #[allow(clippy::too_many_arguments)]
328 pub fn new(
329 id: impl Into<String>,
330 document_type: TaxableDocumentType,
331 document_id: impl Into<String>,
332 line_number: u32,
333 tax_code_id: impl Into<String>,
334 jurisdiction_id: impl Into<String>,
335 taxable_amount: Decimal,
336 tax_amount: Decimal,
337 ) -> Self {
338 Self {
339 id: id.into(),
340 document_type,
341 document_id: document_id.into(),
342 line_number,
343 tax_code_id: tax_code_id.into(),
344 jurisdiction_id: jurisdiction_id.into(),
345 taxable_amount,
346 tax_amount,
347 is_deductible: true,
348 is_reverse_charge: false,
349 is_self_assessed: false,
350 }
351 }
352
353 pub fn with_deductible(mut self, deductible: bool) -> Self {
355 self.is_deductible = deductible;
356 self
357 }
358
359 pub fn with_reverse_charge(mut self, reverse_charge: bool) -> Self {
361 self.is_reverse_charge = reverse_charge;
362 self
363 }
364
365 pub fn with_self_assessed(mut self, self_assessed: bool) -> Self {
367 self.is_self_assessed = self_assessed;
368 self
369 }
370
371 pub fn effective_rate(&self) -> Decimal {
376 if self.taxable_amount.is_zero() {
377 Decimal::ZERO
378 } else {
379 (self.tax_amount / self.taxable_amount).round_dp(6)
380 }
381 }
382}
383
384#[derive(Debug, Clone, Serialize, Deserialize)]
386pub struct TaxReturn {
387 pub id: String,
389 pub entity_id: String,
391 pub jurisdiction_id: String,
393 pub period_start: NaiveDate,
395 pub period_end: NaiveDate,
397 pub return_type: TaxReturnType,
399 pub status: TaxReturnStatus,
401 #[serde(with = "rust_decimal::serde::str")]
403 pub total_output_tax: Decimal,
404 #[serde(with = "rust_decimal::serde::str")]
406 pub total_input_tax: Decimal,
407 #[serde(with = "rust_decimal::serde::str")]
409 pub net_payable: Decimal,
410 pub filing_deadline: NaiveDate,
412 pub actual_filing_date: Option<NaiveDate>,
414 pub is_late: bool,
416}
417
418impl TaxReturn {
419 #[allow(clippy::too_many_arguments)]
421 pub fn new(
422 id: impl Into<String>,
423 entity_id: impl Into<String>,
424 jurisdiction_id: impl Into<String>,
425 period_start: NaiveDate,
426 period_end: NaiveDate,
427 return_type: TaxReturnType,
428 total_output_tax: Decimal,
429 total_input_tax: Decimal,
430 filing_deadline: NaiveDate,
431 ) -> Self {
432 let net_payable = (total_output_tax - total_input_tax).round_dp(2);
433 Self {
434 id: id.into(),
435 entity_id: entity_id.into(),
436 jurisdiction_id: jurisdiction_id.into(),
437 period_start,
438 period_end,
439 return_type,
440 status: TaxReturnStatus::Draft,
441 total_output_tax,
442 total_input_tax,
443 net_payable,
444 filing_deadline,
445 actual_filing_date: None,
446 is_late: false,
447 }
448 }
449
450 pub fn with_filing(mut self, filing_date: NaiveDate) -> Self {
452 self.actual_filing_date = Some(filing_date);
453 self.is_late = filing_date > self.filing_deadline;
454 self.status = TaxReturnStatus::Filed;
455 self
456 }
457
458 pub fn with_status(mut self, status: TaxReturnStatus) -> Self {
460 self.status = status;
461 self
462 }
463
464 pub fn is_filed(&self) -> bool {
466 matches!(
467 self.status,
468 TaxReturnStatus::Filed | TaxReturnStatus::Assessed | TaxReturnStatus::Paid
469 )
470 }
471}
472
473#[derive(Debug, Clone, Serialize, Deserialize)]
475pub struct RateReconciliationItem {
476 pub description: String,
478 #[serde(with = "rust_decimal::serde::str")]
480 pub rate_impact: Decimal,
481}
482
483#[derive(Debug, Clone, Serialize, Deserialize)]
485pub struct TaxProvision {
486 pub id: String,
488 pub entity_id: String,
490 pub period: NaiveDate,
492 #[serde(with = "rust_decimal::serde::str")]
494 pub current_tax_expense: Decimal,
495 #[serde(with = "rust_decimal::serde::str")]
497 pub deferred_tax_asset: Decimal,
498 #[serde(with = "rust_decimal::serde::str")]
500 pub deferred_tax_liability: Decimal,
501 #[serde(with = "rust_decimal::serde::str")]
503 pub statutory_rate: Decimal,
504 #[serde(with = "rust_decimal::serde::str")]
506 pub effective_rate: Decimal,
507 pub rate_reconciliation: Vec<RateReconciliationItem>,
509}
510
511impl TaxProvision {
512 #[allow(clippy::too_many_arguments)]
514 pub fn new(
515 id: impl Into<String>,
516 entity_id: impl Into<String>,
517 period: NaiveDate,
518 current_tax_expense: Decimal,
519 deferred_tax_asset: Decimal,
520 deferred_tax_liability: Decimal,
521 statutory_rate: Decimal,
522 effective_rate: Decimal,
523 ) -> Self {
524 Self {
525 id: id.into(),
526 entity_id: entity_id.into(),
527 period,
528 current_tax_expense,
529 deferred_tax_asset,
530 deferred_tax_liability,
531 statutory_rate,
532 effective_rate,
533 rate_reconciliation: Vec::new(),
534 }
535 }
536
537 pub fn with_reconciliation_item(
539 mut self,
540 description: impl Into<String>,
541 rate_impact: Decimal,
542 ) -> Self {
543 self.rate_reconciliation.push(RateReconciliationItem {
544 description: description.into(),
545 rate_impact,
546 });
547 self
548 }
549
550 pub fn net_deferred_tax(&self) -> Decimal {
555 self.deferred_tax_asset - self.deferred_tax_liability
556 }
557}
558
559#[derive(Debug, Clone, Serialize, Deserialize)]
561pub struct UncertainTaxPosition {
562 pub id: String,
564 pub entity_id: String,
566 pub description: String,
568 #[serde(with = "rust_decimal::serde::str")]
570 pub tax_benefit: Decimal,
571 #[serde(with = "rust_decimal::serde::str")]
573 pub recognition_threshold: Decimal,
574 #[serde(with = "rust_decimal::serde::str")]
576 pub recognized_amount: Decimal,
577 pub measurement_method: TaxMeasurementMethod,
579}
580
581impl UncertainTaxPosition {
582 #[allow(clippy::too_many_arguments)]
584 pub fn new(
585 id: impl Into<String>,
586 entity_id: impl Into<String>,
587 description: impl Into<String>,
588 tax_benefit: Decimal,
589 recognition_threshold: Decimal,
590 recognized_amount: Decimal,
591 measurement_method: TaxMeasurementMethod,
592 ) -> Self {
593 Self {
594 id: id.into(),
595 entity_id: entity_id.into(),
596 description: description.into(),
597 tax_benefit,
598 recognition_threshold,
599 recognized_amount,
600 measurement_method,
601 }
602 }
603
604 pub fn unrecognized_amount(&self) -> Decimal {
606 self.tax_benefit - self.recognized_amount
607 }
608}
609
610#[derive(Debug, Clone, Serialize, Deserialize)]
612pub struct WithholdingTaxRecord {
613 pub id: String,
615 pub payment_id: String,
617 pub vendor_id: String,
619 pub withholding_type: WithholdingType,
621 #[serde(default, with = "rust_decimal::serde::str_option")]
623 pub treaty_rate: Option<Decimal>,
624 #[serde(with = "rust_decimal::serde::str")]
626 pub statutory_rate: Decimal,
627 #[serde(with = "rust_decimal::serde::str")]
629 pub applied_rate: Decimal,
630 #[serde(with = "rust_decimal::serde::str")]
632 pub base_amount: Decimal,
633 #[serde(with = "rust_decimal::serde::str")]
635 pub withheld_amount: Decimal,
636 pub certificate_number: Option<String>,
638}
639
640impl WithholdingTaxRecord {
641 #[allow(clippy::too_many_arguments)]
643 pub fn new(
644 id: impl Into<String>,
645 payment_id: impl Into<String>,
646 vendor_id: impl Into<String>,
647 withholding_type: WithholdingType,
648 statutory_rate: Decimal,
649 applied_rate: Decimal,
650 base_amount: Decimal,
651 ) -> Self {
652 let withheld_amount = (base_amount * applied_rate).round_dp(2);
653 Self {
654 id: id.into(),
655 payment_id: payment_id.into(),
656 vendor_id: vendor_id.into(),
657 withholding_type,
658 treaty_rate: None,
659 statutory_rate,
660 applied_rate,
661 base_amount,
662 withheld_amount,
663 certificate_number: None,
664 }
665 }
666
667 pub fn with_treaty_rate(mut self, rate: Decimal) -> Self {
669 self.treaty_rate = Some(rate);
670 self
671 }
672
673 pub fn with_certificate_number(mut self, number: impl Into<String>) -> Self {
675 self.certificate_number = Some(number.into());
676 self
677 }
678
679 pub fn has_treaty_benefit(&self) -> bool {
684 self.treaty_rate.is_some() && self.applied_rate < self.statutory_rate
685 }
686
687 pub fn treaty_savings(&self) -> Decimal {
691 ((self.statutory_rate - self.applied_rate) * self.base_amount).round_dp(2)
692 }
693}
694
695#[cfg(test)]
700#[allow(clippy::unwrap_used)]
701mod tests {
702 use super::*;
703 use rust_decimal_macros::dec;
704
705 #[test]
706 fn test_tax_code_creation() {
707 let code = TaxCode::new(
708 "TC-001",
709 "VAT-STD-20",
710 "Standard VAT 20%",
711 TaxType::Vat,
712 dec!(0.20),
713 "JUR-UK",
714 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
715 )
716 .with_expiry_date(NaiveDate::from_ymd_opt(2025, 12, 31).unwrap());
717
718 assert_eq!(code.tax_amount(dec!(1000.00)), dec!(200.00));
720 assert_eq!(code.tax_amount(dec!(0)), dec!(0.00));
721
722 assert!(code.is_active(NaiveDate::from_ymd_opt(2024, 6, 15).unwrap()));
724 assert!(!code.is_active(NaiveDate::from_ymd_opt(2023, 12, 31).unwrap()));
726 assert!(!code.is_active(NaiveDate::from_ymd_opt(2025, 12, 31).unwrap()));
728 assert!(!code.is_active(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap()));
730 }
731
732 #[test]
733 fn test_tax_code_exempt() {
734 let code = TaxCode::new(
735 "TC-002",
736 "VAT-EX",
737 "VAT Exempt",
738 TaxType::Vat,
739 dec!(0.20),
740 "JUR-UK",
741 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
742 )
743 .with_exempt(true);
744
745 assert_eq!(code.tax_amount(dec!(5000.00)), dec!(0));
746 }
747
748 #[test]
749 fn test_tax_line_creation() {
750 let line = TaxLine::new(
751 "TL-001",
752 TaxableDocumentType::VendorInvoice,
753 "INV-001",
754 1,
755 "TC-001",
756 "JUR-UK",
757 dec!(1000.00),
758 dec!(200.00),
759 );
760
761 assert_eq!(line.effective_rate(), dec!(0.200000));
762
763 let zero_line = TaxLine::new(
765 "TL-002",
766 TaxableDocumentType::VendorInvoice,
767 "INV-002",
768 1,
769 "TC-001",
770 "JUR-UK",
771 dec!(0),
772 dec!(0),
773 );
774 assert_eq!(zero_line.effective_rate(), dec!(0));
775 }
776
777 #[test]
778 fn test_tax_return_net_payable() {
779 let ret = TaxReturn::new(
780 "TR-001",
781 "ENT-001",
782 "JUR-UK",
783 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
784 NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
785 TaxReturnType::VatReturn,
786 dec!(50000),
787 dec!(30000),
788 NaiveDate::from_ymd_opt(2024, 4, 30).unwrap(),
789 );
790
791 assert!(!ret.is_filed());
793 assert_eq!(ret.net_payable, dec!(20000));
794
795 let filed = ret.with_filing(NaiveDate::from_ymd_opt(2024, 4, 15).unwrap());
797 assert!(filed.is_filed());
798 assert!(!filed.is_late);
799
800 let assessed = TaxReturn::new(
802 "TR-002",
803 "ENT-001",
804 "JUR-UK",
805 NaiveDate::from_ymd_opt(2024, 4, 1).unwrap(),
806 NaiveDate::from_ymd_opt(2024, 6, 30).unwrap(),
807 TaxReturnType::VatReturn,
808 dec!(60000),
809 dec!(40000),
810 NaiveDate::from_ymd_opt(2024, 7, 31).unwrap(),
811 )
812 .with_status(TaxReturnStatus::Assessed);
813 assert!(assessed.is_filed());
814
815 let paid = TaxReturn::new(
817 "TR-003",
818 "ENT-001",
819 "JUR-UK",
820 NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(),
821 NaiveDate::from_ymd_opt(2024, 9, 30).unwrap(),
822 TaxReturnType::IncomeTax,
823 dec!(100000),
824 dec!(0),
825 NaiveDate::from_ymd_opt(2024, 10, 31).unwrap(),
826 )
827 .with_status(TaxReturnStatus::Paid);
828 assert!(paid.is_filed());
829
830 let amended = TaxReturn::new(
832 "TR-004",
833 "ENT-001",
834 "JUR-UK",
835 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
836 NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
837 TaxReturnType::VatReturn,
838 dec!(50000),
839 dec!(30000),
840 NaiveDate::from_ymd_opt(2024, 4, 30).unwrap(),
841 )
842 .with_status(TaxReturnStatus::Amended);
843 assert!(!amended.is_filed());
844 }
845
846 #[test]
847 fn test_tax_provision() {
848 let provision = TaxProvision::new(
849 "TP-001",
850 "ENT-001",
851 NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
852 dec!(250000),
853 dec!(80000),
854 dec!(120000),
855 dec!(0.21),
856 dec!(0.245),
857 )
858 .with_reconciliation_item("State taxes", dec!(0.03))
859 .with_reconciliation_item("R&D credits", dec!(-0.015));
860
861 assert_eq!(provision.net_deferred_tax(), dec!(-40000));
863 assert_eq!(provision.rate_reconciliation.len(), 2);
864 }
865
866 #[test]
867 fn test_withholding_tax_record() {
868 let wht = WithholdingTaxRecord::new(
869 "WHT-001",
870 "PAY-001",
871 "V-100",
872 WithholdingType::RoyaltyWithholding,
873 dec!(0.30), dec!(0.10), dec!(100000), )
877 .with_treaty_rate(dec!(0.10))
878 .with_certificate_number("CERT-2024-001");
879
880 assert!(wht.has_treaty_benefit());
881 assert_eq!(wht.treaty_savings(), dec!(20000.00));
883 assert_eq!(wht.withheld_amount, dec!(10000.00));
884 assert_eq!(wht.certificate_number, Some("CERT-2024-001".to_string()));
885 }
886
887 #[test]
888 fn test_withholding_no_treaty() {
889 let wht = WithholdingTaxRecord::new(
890 "WHT-002",
891 "PAY-002",
892 "V-200",
893 WithholdingType::ServiceWithholding,
894 dec!(0.25),
895 dec!(0.25),
896 dec!(50000),
897 );
898
899 assert!(!wht.has_treaty_benefit());
900 assert_eq!(wht.treaty_savings(), dec!(0.00));
902 }
903
904 #[test]
905 fn test_uncertain_tax_position() {
906 let utp = UncertainTaxPosition::new(
907 "UTP-001",
908 "ENT-001",
909 "R&D credit claim for software development",
910 dec!(500000), dec!(0.50), dec!(350000), TaxMeasurementMethod::MostLikelyAmount,
914 );
915
916 assert_eq!(utp.unrecognized_amount(), dec!(150000));
918 }
919
920 #[test]
921 fn test_jurisdiction_hierarchy() {
922 let federal = TaxJurisdiction::new(
923 "JUR-US",
924 "United States - Federal",
925 "US",
926 JurisdictionType::Federal,
927 );
928 assert!(!federal.is_subnational());
929
930 let state = TaxJurisdiction::new("JUR-US-CA", "California", "US", JurisdictionType::State)
931 .with_region_code("CA")
932 .with_parent_jurisdiction_id("JUR-US");
933 assert!(state.is_subnational());
934 assert_eq!(state.region_code, Some("CA".to_string()));
935 assert_eq!(state.parent_jurisdiction_id, Some("JUR-US".to_string()));
936
937 let local = TaxJurisdiction::new(
938 "JUR-US-CA-SF",
939 "San Francisco",
940 "US",
941 JurisdictionType::Local,
942 )
943 .with_parent_jurisdiction_id("JUR-US-CA");
944 assert!(local.is_subnational());
945
946 let municipal = TaxJurisdiction::new(
947 "JUR-US-NY-NYC",
948 "New York City",
949 "US",
950 JurisdictionType::Municipal,
951 )
952 .with_parent_jurisdiction_id("JUR-US-NY");
953 assert!(municipal.is_subnational());
954
955 let supra = TaxJurisdiction::new(
956 "JUR-EU",
957 "European Union",
958 "EU",
959 JurisdictionType::Supranational,
960 );
961 assert!(!supra.is_subnational());
962 }
963
964 #[test]
965 fn test_serde_roundtrip() {
966 let code = TaxCode::new(
967 "TC-SERDE",
968 "VAT-STD-20",
969 "Standard VAT 20%",
970 TaxType::Vat,
971 dec!(0.20),
972 "JUR-UK",
973 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
974 )
975 .with_expiry_date(NaiveDate::from_ymd_opt(2025, 12, 31).unwrap())
976 .with_reverse_charge(true);
977
978 let json = serde_json::to_string_pretty(&code).unwrap();
979 let deserialized: TaxCode = serde_json::from_str(&json).unwrap();
980
981 assert_eq!(deserialized.id, code.id);
982 assert_eq!(deserialized.code, code.code);
983 assert_eq!(deserialized.rate, code.rate);
984 assert_eq!(deserialized.tax_type, code.tax_type);
985 assert_eq!(deserialized.is_reverse_charge, code.is_reverse_charge);
986 assert_eq!(deserialized.effective_date, code.effective_date);
987 assert_eq!(deserialized.expiry_date, code.expiry_date);
988 }
989
990 #[test]
991 fn test_withholding_serde_roundtrip() {
992 let wht = WithholdingTaxRecord::new(
994 "WHT-SERDE-1",
995 "PAY-001",
996 "V-001",
997 WithholdingType::RoyaltyWithholding,
998 dec!(0.30),
999 dec!(0.15),
1000 dec!(50000),
1001 )
1002 .with_treaty_rate(dec!(0.10));
1003
1004 let json = serde_json::to_string_pretty(&wht).unwrap();
1005 let deserialized: WithholdingTaxRecord = serde_json::from_str(&json).unwrap();
1006 assert_eq!(deserialized.treaty_rate, Some(dec!(0.10)));
1007 assert_eq!(deserialized.statutory_rate, dec!(0.30));
1008 assert_eq!(deserialized.applied_rate, dec!(0.15));
1009 assert_eq!(deserialized.base_amount, dec!(50000));
1010 assert_eq!(deserialized.withheld_amount, wht.withheld_amount);
1011
1012 let wht_no_treaty = WithholdingTaxRecord::new(
1014 "WHT-SERDE-2",
1015 "PAY-002",
1016 "V-002",
1017 WithholdingType::ServiceWithholding,
1018 dec!(0.30),
1019 dec!(0.30),
1020 dec!(10000),
1021 );
1022
1023 let json2 = serde_json::to_string_pretty(&wht_no_treaty).unwrap();
1024 let deserialized2: WithholdingTaxRecord = serde_json::from_str(&json2).unwrap();
1025 assert_eq!(deserialized2.treaty_rate, None);
1026 assert_eq!(deserialized2.statutory_rate, dec!(0.30));
1027 }
1028}