1use std::collections::HashMap;
13
14use chrono::NaiveDate;
15use rust_decimal::Decimal;
16use serde::{Deserialize, Serialize};
17
18use super::graph_properties::{GraphPropertyValue, ToNodeProperties};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
26#[serde(rename_all = "snake_case")]
27pub enum JurisdictionType {
28 #[default]
30 Federal,
31 State,
33 Local,
35 Municipal,
37 Supranational,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
43#[serde(rename_all = "snake_case")]
44pub enum TaxType {
45 #[default]
47 Vat,
48 Gst,
50 SalesTax,
52 IncomeTax,
54 WithholdingTax,
56 PayrollTax,
58 ExciseTax,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
64#[serde(rename_all = "snake_case")]
65pub enum TaxableDocumentType {
66 #[default]
68 VendorInvoice,
69 CustomerInvoice,
71 JournalEntry,
73 Payment,
75 PayrollRun,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
81#[serde(rename_all = "snake_case")]
82pub enum TaxReturnType {
83 #[default]
85 VatReturn,
86 IncomeTax,
88 WithholdingRemittance,
90 PayrollTax,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
96#[serde(rename_all = "snake_case")]
97pub enum TaxReturnStatus {
98 #[default]
100 Draft,
101 Filed,
103 Assessed,
105 Paid,
107 Amended,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
113#[serde(rename_all = "snake_case")]
114pub enum WithholdingType {
115 DividendWithholding,
117 RoyaltyWithholding,
119 #[default]
121 ServiceWithholding,
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
126#[serde(rename_all = "snake_case")]
127pub enum TaxMeasurementMethod {
128 #[default]
130 MostLikelyAmount,
131 ExpectedValue,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct TaxJurisdiction {
142 pub id: String,
144 pub name: String,
146 pub country_code: String,
148 pub region_code: Option<String>,
150 pub jurisdiction_type: JurisdictionType,
152 pub parent_jurisdiction_id: Option<String>,
154 pub vat_registered: bool,
156}
157
158impl TaxJurisdiction {
159 pub fn new(
161 id: impl Into<String>,
162 name: impl Into<String>,
163 country_code: impl Into<String>,
164 jurisdiction_type: JurisdictionType,
165 ) -> Self {
166 Self {
167 id: id.into(),
168 name: name.into(),
169 country_code: country_code.into(),
170 region_code: None,
171 jurisdiction_type,
172 parent_jurisdiction_id: None,
173 vat_registered: false,
174 }
175 }
176
177 pub fn with_region_code(mut self, region_code: impl Into<String>) -> Self {
179 self.region_code = Some(region_code.into());
180 self
181 }
182
183 pub fn with_parent_jurisdiction_id(mut self, parent_id: impl Into<String>) -> Self {
185 self.parent_jurisdiction_id = Some(parent_id.into());
186 self
187 }
188
189 pub fn with_vat_registered(mut self, registered: bool) -> Self {
191 self.vat_registered = registered;
192 self
193 }
194
195 pub fn is_subnational(&self) -> bool {
197 matches!(
198 self.jurisdiction_type,
199 JurisdictionType::State | JurisdictionType::Local | JurisdictionType::Municipal
200 )
201 }
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct TaxCode {
207 pub id: String,
209 pub code: String,
211 pub description: String,
213 pub tax_type: TaxType,
215 #[serde(with = "rust_decimal::serde::str")]
217 pub rate: Decimal,
218 pub jurisdiction_id: String,
220 pub effective_date: NaiveDate,
222 pub expiry_date: Option<NaiveDate>,
224 pub is_reverse_charge: bool,
226 pub is_exempt: bool,
228}
229
230impl TaxCode {
231 #[allow(clippy::too_many_arguments)]
233 pub fn new(
234 id: impl Into<String>,
235 code: impl Into<String>,
236 description: impl Into<String>,
237 tax_type: TaxType,
238 rate: Decimal,
239 jurisdiction_id: impl Into<String>,
240 effective_date: NaiveDate,
241 ) -> Self {
242 Self {
243 id: id.into(),
244 code: code.into(),
245 description: description.into(),
246 tax_type,
247 rate,
248 jurisdiction_id: jurisdiction_id.into(),
249 effective_date,
250 expiry_date: None,
251 is_reverse_charge: false,
252 is_exempt: false,
253 }
254 }
255
256 pub fn with_expiry_date(mut self, expiry: NaiveDate) -> Self {
258 self.expiry_date = Some(expiry);
259 self
260 }
261
262 pub fn with_reverse_charge(mut self, reverse_charge: bool) -> Self {
264 self.is_reverse_charge = reverse_charge;
265 self
266 }
267
268 pub fn with_exempt(mut self, exempt: bool) -> Self {
270 self.is_exempt = exempt;
271 self
272 }
273
274 pub fn tax_amount(&self, taxable_amount: Decimal) -> Decimal {
279 if self.is_exempt {
280 return Decimal::ZERO;
281 }
282 (taxable_amount * self.rate).round_dp(2)
283 }
284
285 pub fn is_active(&self, date: NaiveDate) -> bool {
290 if date < self.effective_date {
291 return false;
292 }
293 match self.expiry_date {
294 Some(expiry) => date < expiry,
295 None => true,
296 }
297 }
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct TaxLine {
303 pub id: String,
305 pub document_type: TaxableDocumentType,
307 pub document_id: String,
309 pub line_number: u32,
311 pub tax_code_id: String,
313 pub jurisdiction_id: String,
315 #[serde(with = "rust_decimal::serde::str")]
317 pub taxable_amount: Decimal,
318 #[serde(with = "rust_decimal::serde::str")]
320 pub tax_amount: Decimal,
321 pub is_deductible: bool,
323 pub is_reverse_charge: bool,
325 pub is_self_assessed: bool,
327}
328
329impl TaxLine {
330 #[allow(clippy::too_many_arguments)]
332 pub fn new(
333 id: impl Into<String>,
334 document_type: TaxableDocumentType,
335 document_id: impl Into<String>,
336 line_number: u32,
337 tax_code_id: impl Into<String>,
338 jurisdiction_id: impl Into<String>,
339 taxable_amount: Decimal,
340 tax_amount: Decimal,
341 ) -> Self {
342 Self {
343 id: id.into(),
344 document_type,
345 document_id: document_id.into(),
346 line_number,
347 tax_code_id: tax_code_id.into(),
348 jurisdiction_id: jurisdiction_id.into(),
349 taxable_amount,
350 tax_amount,
351 is_deductible: true,
352 is_reverse_charge: false,
353 is_self_assessed: false,
354 }
355 }
356
357 pub fn with_deductible(mut self, deductible: bool) -> Self {
359 self.is_deductible = deductible;
360 self
361 }
362
363 pub fn with_reverse_charge(mut self, reverse_charge: bool) -> Self {
365 self.is_reverse_charge = reverse_charge;
366 self
367 }
368
369 pub fn with_self_assessed(mut self, self_assessed: bool) -> Self {
371 self.is_self_assessed = self_assessed;
372 self
373 }
374
375 pub fn effective_rate(&self) -> Decimal {
380 if self.taxable_amount.is_zero() {
381 Decimal::ZERO
382 } else {
383 (self.tax_amount / self.taxable_amount).round_dp(6)
384 }
385 }
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct TaxReturn {
391 pub id: String,
393 pub entity_id: String,
395 pub jurisdiction_id: String,
397 pub period_start: NaiveDate,
399 pub period_end: NaiveDate,
401 pub return_type: TaxReturnType,
403 pub status: TaxReturnStatus,
405 #[serde(with = "rust_decimal::serde::str")]
407 pub total_output_tax: Decimal,
408 #[serde(with = "rust_decimal::serde::str")]
410 pub total_input_tax: Decimal,
411 #[serde(with = "rust_decimal::serde::str")]
413 pub net_payable: Decimal,
414 pub filing_deadline: NaiveDate,
416 pub actual_filing_date: Option<NaiveDate>,
418 pub is_late: bool,
420}
421
422impl TaxReturn {
423 #[allow(clippy::too_many_arguments)]
425 pub fn new(
426 id: impl Into<String>,
427 entity_id: impl Into<String>,
428 jurisdiction_id: impl Into<String>,
429 period_start: NaiveDate,
430 period_end: NaiveDate,
431 return_type: TaxReturnType,
432 total_output_tax: Decimal,
433 total_input_tax: Decimal,
434 filing_deadline: NaiveDate,
435 ) -> Self {
436 let net_payable = (total_output_tax - total_input_tax).round_dp(2);
437 Self {
438 id: id.into(),
439 entity_id: entity_id.into(),
440 jurisdiction_id: jurisdiction_id.into(),
441 period_start,
442 period_end,
443 return_type,
444 status: TaxReturnStatus::Draft,
445 total_output_tax,
446 total_input_tax,
447 net_payable,
448 filing_deadline,
449 actual_filing_date: None,
450 is_late: false,
451 }
452 }
453
454 pub fn with_filing(mut self, filing_date: NaiveDate) -> Self {
456 self.actual_filing_date = Some(filing_date);
457 self.is_late = filing_date > self.filing_deadline;
458 self.status = TaxReturnStatus::Filed;
459 self
460 }
461
462 pub fn with_status(mut self, status: TaxReturnStatus) -> Self {
464 self.status = status;
465 self
466 }
467
468 pub fn is_filed(&self) -> bool {
470 matches!(
471 self.status,
472 TaxReturnStatus::Filed | TaxReturnStatus::Assessed | TaxReturnStatus::Paid
473 )
474 }
475}
476
477#[derive(Debug, Clone, Serialize, Deserialize)]
479pub struct RateReconciliationItem {
480 pub description: String,
482 #[serde(with = "rust_decimal::serde::str")]
484 pub rate_impact: Decimal,
485}
486
487#[derive(Debug, Clone, Serialize, Deserialize)]
489pub struct TaxProvision {
490 pub id: String,
492 pub entity_id: String,
494 pub period: NaiveDate,
496 #[serde(with = "rust_decimal::serde::str")]
498 pub current_tax_expense: Decimal,
499 #[serde(with = "rust_decimal::serde::str")]
501 pub deferred_tax_asset: Decimal,
502 #[serde(with = "rust_decimal::serde::str")]
504 pub deferred_tax_liability: Decimal,
505 #[serde(with = "rust_decimal::serde::str")]
507 pub statutory_rate: Decimal,
508 #[serde(with = "rust_decimal::serde::str")]
510 pub effective_rate: Decimal,
511 pub rate_reconciliation: Vec<RateReconciliationItem>,
513}
514
515impl TaxProvision {
516 #[allow(clippy::too_many_arguments)]
518 pub fn new(
519 id: impl Into<String>,
520 entity_id: impl Into<String>,
521 period: NaiveDate,
522 current_tax_expense: Decimal,
523 deferred_tax_asset: Decimal,
524 deferred_tax_liability: Decimal,
525 statutory_rate: Decimal,
526 effective_rate: Decimal,
527 ) -> Self {
528 Self {
529 id: id.into(),
530 entity_id: entity_id.into(),
531 period,
532 current_tax_expense,
533 deferred_tax_asset,
534 deferred_tax_liability,
535 statutory_rate,
536 effective_rate,
537 rate_reconciliation: Vec::new(),
538 }
539 }
540
541 pub fn with_reconciliation_item(
543 mut self,
544 description: impl Into<String>,
545 rate_impact: Decimal,
546 ) -> Self {
547 self.rate_reconciliation.push(RateReconciliationItem {
548 description: description.into(),
549 rate_impact,
550 });
551 self
552 }
553
554 pub fn net_deferred_tax(&self) -> Decimal {
559 self.deferred_tax_asset - self.deferred_tax_liability
560 }
561}
562
563#[derive(Debug, Clone, Serialize, Deserialize)]
565pub struct UncertainTaxPosition {
566 pub id: String,
568 pub entity_id: String,
570 pub description: String,
572 #[serde(with = "rust_decimal::serde::str")]
574 pub tax_benefit: Decimal,
575 #[serde(with = "rust_decimal::serde::str")]
577 pub recognition_threshold: Decimal,
578 #[serde(with = "rust_decimal::serde::str")]
580 pub recognized_amount: Decimal,
581 pub measurement_method: TaxMeasurementMethod,
583}
584
585impl UncertainTaxPosition {
586 #[allow(clippy::too_many_arguments)]
588 pub fn new(
589 id: impl Into<String>,
590 entity_id: impl Into<String>,
591 description: impl Into<String>,
592 tax_benefit: Decimal,
593 recognition_threshold: Decimal,
594 recognized_amount: Decimal,
595 measurement_method: TaxMeasurementMethod,
596 ) -> Self {
597 Self {
598 id: id.into(),
599 entity_id: entity_id.into(),
600 description: description.into(),
601 tax_benefit,
602 recognition_threshold,
603 recognized_amount,
604 measurement_method,
605 }
606 }
607
608 pub fn unrecognized_amount(&self) -> Decimal {
610 self.tax_benefit - self.recognized_amount
611 }
612}
613
614#[derive(Debug, Clone, Serialize, Deserialize)]
616pub struct WithholdingTaxRecord {
617 pub id: String,
619 pub payment_id: String,
621 pub vendor_id: String,
623 pub withholding_type: WithholdingType,
625 #[serde(default, with = "rust_decimal::serde::str_option")]
627 pub treaty_rate: Option<Decimal>,
628 #[serde(with = "rust_decimal::serde::str")]
630 pub statutory_rate: Decimal,
631 #[serde(with = "rust_decimal::serde::str")]
633 pub applied_rate: Decimal,
634 #[serde(with = "rust_decimal::serde::str")]
636 pub base_amount: Decimal,
637 #[serde(with = "rust_decimal::serde::str")]
639 pub withheld_amount: Decimal,
640 pub certificate_number: Option<String>,
642}
643
644impl WithholdingTaxRecord {
645 #[allow(clippy::too_many_arguments)]
647 pub fn new(
648 id: impl Into<String>,
649 payment_id: impl Into<String>,
650 vendor_id: impl Into<String>,
651 withholding_type: WithholdingType,
652 statutory_rate: Decimal,
653 applied_rate: Decimal,
654 base_amount: Decimal,
655 ) -> Self {
656 let withheld_amount = (base_amount * applied_rate).round_dp(2);
657 Self {
658 id: id.into(),
659 payment_id: payment_id.into(),
660 vendor_id: vendor_id.into(),
661 withholding_type,
662 treaty_rate: None,
663 statutory_rate,
664 applied_rate,
665 base_amount,
666 withheld_amount,
667 certificate_number: None,
668 }
669 }
670
671 pub fn with_treaty_rate(mut self, rate: Decimal) -> Self {
673 self.treaty_rate = Some(rate);
674 self
675 }
676
677 pub fn with_certificate_number(mut self, number: impl Into<String>) -> Self {
679 self.certificate_number = Some(number.into());
680 self
681 }
682
683 pub fn has_treaty_benefit(&self) -> bool {
688 self.treaty_rate.is_some() && self.applied_rate < self.statutory_rate
689 }
690
691 pub fn treaty_savings(&self) -> Decimal {
695 ((self.statutory_rate - self.applied_rate) * self.base_amount).round_dp(2)
696 }
697}
698
699impl ToNodeProperties for TaxJurisdiction {
704 fn node_type_name(&self) -> &'static str {
705 "tax_jurisdiction"
706 }
707 fn node_type_code(&self) -> u16 {
708 410
709 }
710 fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
711 let mut p = HashMap::new();
712 p.insert("code".into(), GraphPropertyValue::String(self.id.clone()));
713 p.insert("name".into(), GraphPropertyValue::String(self.name.clone()));
714 p.insert(
715 "country".into(),
716 GraphPropertyValue::String(self.country_code.clone()),
717 );
718 if let Some(ref rc) = self.region_code {
719 p.insert("region".into(), GraphPropertyValue::String(rc.clone()));
720 }
721 p.insert(
722 "jurisdictionType".into(),
723 GraphPropertyValue::String(format!("{:?}", self.jurisdiction_type)),
724 );
725 p.insert(
726 "isActive".into(),
727 GraphPropertyValue::Bool(self.vat_registered),
728 );
729 p
730 }
731}
732
733impl ToNodeProperties for TaxCode {
734 fn node_type_name(&self) -> &'static str {
735 "tax_code"
736 }
737 fn node_type_code(&self) -> u16 {
738 411
739 }
740 fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
741 let mut p = HashMap::new();
742 p.insert("code".into(), GraphPropertyValue::String(self.code.clone()));
743 p.insert(
744 "description".into(),
745 GraphPropertyValue::String(self.description.clone()),
746 );
747 p.insert("rate".into(), GraphPropertyValue::Decimal(self.rate));
748 p.insert(
749 "taxType".into(),
750 GraphPropertyValue::String(format!("{:?}", self.tax_type)),
751 );
752 p.insert(
753 "jurisdiction".into(),
754 GraphPropertyValue::String(self.jurisdiction_id.clone()),
755 );
756 p.insert("isActive".into(), GraphPropertyValue::Bool(!self.is_exempt));
757 p.insert(
758 "isReverseCharge".into(),
759 GraphPropertyValue::Bool(self.is_reverse_charge),
760 );
761 p
762 }
763}
764
765impl ToNodeProperties for TaxLine {
766 fn node_type_name(&self) -> &'static str {
767 "tax_line"
768 }
769 fn node_type_code(&self) -> u16 {
770 412
771 }
772 fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
773 let mut p = HashMap::new();
774 p.insert(
775 "returnId".into(),
776 GraphPropertyValue::String(self.document_id.clone()),
777 );
778 p.insert(
779 "lineNumber".into(),
780 GraphPropertyValue::Int(self.line_number as i64),
781 );
782 p.insert(
783 "description".into(),
784 GraphPropertyValue::String(format!("{:?}", self.document_type)),
785 );
786 p.insert(
787 "amount".into(),
788 GraphPropertyValue::Decimal(self.tax_amount),
789 );
790 p.insert(
791 "taxableAmount".into(),
792 GraphPropertyValue::Decimal(self.taxable_amount),
793 );
794 p.insert(
795 "taxCode".into(),
796 GraphPropertyValue::String(self.tax_code_id.clone()),
797 );
798 p.insert(
799 "jurisdiction".into(),
800 GraphPropertyValue::String(self.jurisdiction_id.clone()),
801 );
802 p.insert(
803 "isDeductible".into(),
804 GraphPropertyValue::Bool(self.is_deductible),
805 );
806 p
807 }
808}
809
810impl ToNodeProperties for TaxReturn {
811 fn node_type_name(&self) -> &'static str {
812 "tax_return"
813 }
814 fn node_type_code(&self) -> u16 {
815 413
816 }
817 fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
818 let mut p = HashMap::new();
819 p.insert(
820 "entityCode".into(),
821 GraphPropertyValue::String(self.entity_id.clone()),
822 );
823 p.insert(
824 "period".into(),
825 GraphPropertyValue::String(format!("{}..{}", self.period_start, self.period_end)),
826 );
827 p.insert(
828 "jurisdiction".into(),
829 GraphPropertyValue::String(self.jurisdiction_id.clone()),
830 );
831 p.insert(
832 "filingType".into(),
833 GraphPropertyValue::String(format!("{:?}", self.return_type)),
834 );
835 p.insert(
836 "status".into(),
837 GraphPropertyValue::String(format!("{:?}", self.status)),
838 );
839 p.insert(
840 "totalTax".into(),
841 GraphPropertyValue::Decimal(self.total_output_tax),
842 );
843 p.insert(
844 "taxPaid".into(),
845 GraphPropertyValue::Decimal(self.total_input_tax),
846 );
847 p.insert(
848 "balanceDue".into(),
849 GraphPropertyValue::Decimal(self.net_payable),
850 );
851 p.insert(
852 "dueDate".into(),
853 GraphPropertyValue::Date(self.filing_deadline),
854 );
855 p.insert("isLate".into(), GraphPropertyValue::Bool(self.is_late));
856 p
857 }
858}
859
860impl ToNodeProperties for TaxProvision {
861 fn node_type_name(&self) -> &'static str {
862 "tax_provision"
863 }
864 fn node_type_code(&self) -> u16 {
865 414
866 }
867 fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
868 let mut p = HashMap::new();
869 p.insert(
870 "entityCode".into(),
871 GraphPropertyValue::String(self.entity_id.clone()),
872 );
873 p.insert("period".into(), GraphPropertyValue::Date(self.period));
874 p.insert(
875 "totalProvision".into(),
876 GraphPropertyValue::Decimal(self.current_tax_expense),
877 );
878 p.insert(
879 "deferredAsset".into(),
880 GraphPropertyValue::Decimal(self.deferred_tax_asset),
881 );
882 p.insert(
883 "deferredLiability".into(),
884 GraphPropertyValue::Decimal(self.deferred_tax_liability),
885 );
886 p.insert(
887 "statutoryRate".into(),
888 GraphPropertyValue::Decimal(self.statutory_rate),
889 );
890 p.insert(
891 "effectiveRate".into(),
892 GraphPropertyValue::Decimal(self.effective_rate),
893 );
894 p
895 }
896}
897
898impl ToNodeProperties for WithholdingTaxRecord {
899 fn node_type_name(&self) -> &'static str {
900 "withholding_tax_record"
901 }
902 fn node_type_code(&self) -> u16 {
903 415
904 }
905 fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
906 let mut p = HashMap::new();
907 p.insert(
908 "paymentId".into(),
909 GraphPropertyValue::String(self.payment_id.clone()),
910 );
911 p.insert(
912 "vendorId".into(),
913 GraphPropertyValue::String(self.vendor_id.clone()),
914 );
915 p.insert(
916 "taxCode".into(),
917 GraphPropertyValue::String(format!("{:?}", self.withholding_type)),
918 );
919 p.insert(
920 "grossAmount".into(),
921 GraphPropertyValue::Decimal(self.base_amount),
922 );
923 p.insert(
924 "withholdingRate".into(),
925 GraphPropertyValue::Decimal(self.applied_rate),
926 );
927 p.insert(
928 "withholdingAmount".into(),
929 GraphPropertyValue::Decimal(self.withheld_amount),
930 );
931 p.insert(
932 "treatyApplied".into(),
933 GraphPropertyValue::Bool(self.treaty_rate.is_some()),
934 );
935 if let Some(ref cn) = self.certificate_number {
936 p.insert(
937 "certificateNumber".into(),
938 GraphPropertyValue::String(cn.clone()),
939 );
940 }
941 p
942 }
943}
944
945impl ToNodeProperties for UncertainTaxPosition {
946 fn node_type_name(&self) -> &'static str {
947 "uncertain_tax_position"
948 }
949 fn node_type_code(&self) -> u16 {
950 416
951 }
952 fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
953 let mut p = HashMap::new();
954 p.insert(
955 "entityCode".into(),
956 GraphPropertyValue::String(self.entity_id.clone()),
957 );
958 p.insert(
959 "description".into(),
960 GraphPropertyValue::String(self.description.clone()),
961 );
962 p.insert(
963 "amount".into(),
964 GraphPropertyValue::Decimal(self.tax_benefit),
965 );
966 p.insert(
967 "probability".into(),
968 GraphPropertyValue::Decimal(self.recognition_threshold),
969 );
970 p.insert(
971 "reserveAmount".into(),
972 GraphPropertyValue::Decimal(self.recognized_amount),
973 );
974 p.insert(
975 "measurementMethod".into(),
976 GraphPropertyValue::String(format!("{:?}", self.measurement_method)),
977 );
978 p
979 }
980}
981
982#[cfg(test)]
987#[allow(clippy::unwrap_used)]
988mod tests {
989 use super::*;
990 use rust_decimal_macros::dec;
991
992 #[test]
993 fn test_tax_code_creation() {
994 let code = TaxCode::new(
995 "TC-001",
996 "VAT-STD-20",
997 "Standard VAT 20%",
998 TaxType::Vat,
999 dec!(0.20),
1000 "JUR-UK",
1001 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1002 )
1003 .with_expiry_date(NaiveDate::from_ymd_opt(2025, 12, 31).unwrap());
1004
1005 assert_eq!(code.tax_amount(dec!(1000.00)), dec!(200.00));
1007 assert_eq!(code.tax_amount(dec!(0)), dec!(0.00));
1008
1009 assert!(code.is_active(NaiveDate::from_ymd_opt(2024, 6, 15).unwrap()));
1011 assert!(!code.is_active(NaiveDate::from_ymd_opt(2023, 12, 31).unwrap()));
1013 assert!(!code.is_active(NaiveDate::from_ymd_opt(2025, 12, 31).unwrap()));
1015 assert!(!code.is_active(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap()));
1017 }
1018
1019 #[test]
1020 fn test_tax_code_exempt() {
1021 let code = TaxCode::new(
1022 "TC-002",
1023 "VAT-EX",
1024 "VAT Exempt",
1025 TaxType::Vat,
1026 dec!(0.20),
1027 "JUR-UK",
1028 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1029 )
1030 .with_exempt(true);
1031
1032 assert_eq!(code.tax_amount(dec!(5000.00)), dec!(0));
1033 }
1034
1035 #[test]
1036 fn test_tax_line_creation() {
1037 let line = TaxLine::new(
1038 "TL-001",
1039 TaxableDocumentType::VendorInvoice,
1040 "INV-001",
1041 1,
1042 "TC-001",
1043 "JUR-UK",
1044 dec!(1000.00),
1045 dec!(200.00),
1046 );
1047
1048 assert_eq!(line.effective_rate(), dec!(0.200000));
1049
1050 let zero_line = TaxLine::new(
1052 "TL-002",
1053 TaxableDocumentType::VendorInvoice,
1054 "INV-002",
1055 1,
1056 "TC-001",
1057 "JUR-UK",
1058 dec!(0),
1059 dec!(0),
1060 );
1061 assert_eq!(zero_line.effective_rate(), dec!(0));
1062 }
1063
1064 #[test]
1065 fn test_tax_return_net_payable() {
1066 let ret = TaxReturn::new(
1067 "TR-001",
1068 "ENT-001",
1069 "JUR-UK",
1070 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1071 NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
1072 TaxReturnType::VatReturn,
1073 dec!(50000),
1074 dec!(30000),
1075 NaiveDate::from_ymd_opt(2024, 4, 30).unwrap(),
1076 );
1077
1078 assert!(!ret.is_filed());
1080 assert_eq!(ret.net_payable, dec!(20000));
1081
1082 let filed = ret.with_filing(NaiveDate::from_ymd_opt(2024, 4, 15).unwrap());
1084 assert!(filed.is_filed());
1085 assert!(!filed.is_late);
1086
1087 let assessed = TaxReturn::new(
1089 "TR-002",
1090 "ENT-001",
1091 "JUR-UK",
1092 NaiveDate::from_ymd_opt(2024, 4, 1).unwrap(),
1093 NaiveDate::from_ymd_opt(2024, 6, 30).unwrap(),
1094 TaxReturnType::VatReturn,
1095 dec!(60000),
1096 dec!(40000),
1097 NaiveDate::from_ymd_opt(2024, 7, 31).unwrap(),
1098 )
1099 .with_status(TaxReturnStatus::Assessed);
1100 assert!(assessed.is_filed());
1101
1102 let paid = TaxReturn::new(
1104 "TR-003",
1105 "ENT-001",
1106 "JUR-UK",
1107 NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(),
1108 NaiveDate::from_ymd_opt(2024, 9, 30).unwrap(),
1109 TaxReturnType::IncomeTax,
1110 dec!(100000),
1111 dec!(0),
1112 NaiveDate::from_ymd_opt(2024, 10, 31).unwrap(),
1113 )
1114 .with_status(TaxReturnStatus::Paid);
1115 assert!(paid.is_filed());
1116
1117 let amended = TaxReturn::new(
1119 "TR-004",
1120 "ENT-001",
1121 "JUR-UK",
1122 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1123 NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
1124 TaxReturnType::VatReturn,
1125 dec!(50000),
1126 dec!(30000),
1127 NaiveDate::from_ymd_opt(2024, 4, 30).unwrap(),
1128 )
1129 .with_status(TaxReturnStatus::Amended);
1130 assert!(!amended.is_filed());
1131 }
1132
1133 #[test]
1134 fn test_tax_provision() {
1135 let provision = TaxProvision::new(
1136 "TP-001",
1137 "ENT-001",
1138 NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
1139 dec!(250000),
1140 dec!(80000),
1141 dec!(120000),
1142 dec!(0.21),
1143 dec!(0.245),
1144 )
1145 .with_reconciliation_item("State taxes", dec!(0.03))
1146 .with_reconciliation_item("R&D credits", dec!(-0.015));
1147
1148 assert_eq!(provision.net_deferred_tax(), dec!(-40000));
1150 assert_eq!(provision.rate_reconciliation.len(), 2);
1151 }
1152
1153 #[test]
1154 fn test_withholding_tax_record() {
1155 let wht = WithholdingTaxRecord::new(
1156 "WHT-001",
1157 "PAY-001",
1158 "V-100",
1159 WithholdingType::RoyaltyWithholding,
1160 dec!(0.30), dec!(0.10), dec!(100000), )
1164 .with_treaty_rate(dec!(0.10))
1165 .with_certificate_number("CERT-2024-001");
1166
1167 assert!(wht.has_treaty_benefit());
1168 assert_eq!(wht.treaty_savings(), dec!(20000.00));
1170 assert_eq!(wht.withheld_amount, dec!(10000.00));
1171 assert_eq!(wht.certificate_number, Some("CERT-2024-001".to_string()));
1172 }
1173
1174 #[test]
1175 fn test_withholding_no_treaty() {
1176 let wht = WithholdingTaxRecord::new(
1177 "WHT-002",
1178 "PAY-002",
1179 "V-200",
1180 WithholdingType::ServiceWithholding,
1181 dec!(0.25),
1182 dec!(0.25),
1183 dec!(50000),
1184 );
1185
1186 assert!(!wht.has_treaty_benefit());
1187 assert_eq!(wht.treaty_savings(), dec!(0.00));
1189 }
1190
1191 #[test]
1192 fn test_uncertain_tax_position() {
1193 let utp = UncertainTaxPosition::new(
1194 "UTP-001",
1195 "ENT-001",
1196 "R&D credit claim for software development",
1197 dec!(500000), dec!(0.50), dec!(350000), TaxMeasurementMethod::MostLikelyAmount,
1201 );
1202
1203 assert_eq!(utp.unrecognized_amount(), dec!(150000));
1205 }
1206
1207 #[test]
1208 fn test_jurisdiction_hierarchy() {
1209 let federal = TaxJurisdiction::new(
1210 "JUR-US",
1211 "United States - Federal",
1212 "US",
1213 JurisdictionType::Federal,
1214 );
1215 assert!(!federal.is_subnational());
1216
1217 let state = TaxJurisdiction::new("JUR-US-CA", "California", "US", JurisdictionType::State)
1218 .with_region_code("CA")
1219 .with_parent_jurisdiction_id("JUR-US");
1220 assert!(state.is_subnational());
1221 assert_eq!(state.region_code, Some("CA".to_string()));
1222 assert_eq!(state.parent_jurisdiction_id, Some("JUR-US".to_string()));
1223
1224 let local = TaxJurisdiction::new(
1225 "JUR-US-CA-SF",
1226 "San Francisco",
1227 "US",
1228 JurisdictionType::Local,
1229 )
1230 .with_parent_jurisdiction_id("JUR-US-CA");
1231 assert!(local.is_subnational());
1232
1233 let municipal = TaxJurisdiction::new(
1234 "JUR-US-NY-NYC",
1235 "New York City",
1236 "US",
1237 JurisdictionType::Municipal,
1238 )
1239 .with_parent_jurisdiction_id("JUR-US-NY");
1240 assert!(municipal.is_subnational());
1241
1242 let supra = TaxJurisdiction::new(
1243 "JUR-EU",
1244 "European Union",
1245 "EU",
1246 JurisdictionType::Supranational,
1247 );
1248 assert!(!supra.is_subnational());
1249 }
1250
1251 #[test]
1252 fn test_serde_roundtrip() {
1253 let code = TaxCode::new(
1254 "TC-SERDE",
1255 "VAT-STD-20",
1256 "Standard VAT 20%",
1257 TaxType::Vat,
1258 dec!(0.20),
1259 "JUR-UK",
1260 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1261 )
1262 .with_expiry_date(NaiveDate::from_ymd_opt(2025, 12, 31).unwrap())
1263 .with_reverse_charge(true);
1264
1265 let json = serde_json::to_string_pretty(&code).unwrap();
1266 let deserialized: TaxCode = serde_json::from_str(&json).unwrap();
1267
1268 assert_eq!(deserialized.id, code.id);
1269 assert_eq!(deserialized.code, code.code);
1270 assert_eq!(deserialized.rate, code.rate);
1271 assert_eq!(deserialized.tax_type, code.tax_type);
1272 assert_eq!(deserialized.is_reverse_charge, code.is_reverse_charge);
1273 assert_eq!(deserialized.effective_date, code.effective_date);
1274 assert_eq!(deserialized.expiry_date, code.expiry_date);
1275 }
1276
1277 #[test]
1278 fn test_withholding_serde_roundtrip() {
1279 let wht = WithholdingTaxRecord::new(
1281 "WHT-SERDE-1",
1282 "PAY-001",
1283 "V-001",
1284 WithholdingType::RoyaltyWithholding,
1285 dec!(0.30),
1286 dec!(0.15),
1287 dec!(50000),
1288 )
1289 .with_treaty_rate(dec!(0.10));
1290
1291 let json = serde_json::to_string_pretty(&wht).unwrap();
1292 let deserialized: WithholdingTaxRecord = serde_json::from_str(&json).unwrap();
1293 assert_eq!(deserialized.treaty_rate, Some(dec!(0.10)));
1294 assert_eq!(deserialized.statutory_rate, dec!(0.30));
1295 assert_eq!(deserialized.applied_rate, dec!(0.15));
1296 assert_eq!(deserialized.base_amount, dec!(50000));
1297 assert_eq!(deserialized.withheld_amount, wht.withheld_amount);
1298
1299 let wht_no_treaty = WithholdingTaxRecord::new(
1301 "WHT-SERDE-2",
1302 "PAY-002",
1303 "V-002",
1304 WithholdingType::ServiceWithholding,
1305 dec!(0.30),
1306 dec!(0.30),
1307 dec!(10000),
1308 );
1309
1310 let json2 = serde_json::to_string_pretty(&wht_no_treaty).unwrap();
1311 let deserialized2: WithholdingTaxRecord = serde_json::from_str(&json2).unwrap();
1312 assert_eq!(deserialized2.treaty_rate, None);
1313 assert_eq!(deserialized2.statutory_rate, dec!(0.30));
1314 }
1315}