Skip to content

Commit 1f4e947

Browse files
authored
Merge pull request #423 from Gogo-Eng/issue-268-tax-loss-harvesting
Add tax loss harvesting recommendations
2 parents c7446c6 + a59cd4c commit 1f4e947

1 file changed

Lines changed: 149 additions & 0 deletions

File tree

  • contracts/tax-compliance/src

contracts/tax-compliance/src/lib.rs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,31 @@ mod tax_compliance {
306306
pub status: TaxStatus,
307307
}
308308

309+
#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)]
310+
#[cfg_attr(
311+
feature = "std",
312+
derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout)
313+
)]
314+
pub enum TaxLossHarvestingOpportunityKind {
315+
Reassessment,
316+
ExemptionReview,
317+
}
318+
319+
#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)]
320+
#[cfg_attr(
321+
feature = "std",
322+
derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout)
323+
)]
324+
pub struct TaxLossHarvestingOpportunity {
325+
pub property_id: u64,
326+
pub jurisdiction_code: u32,
327+
pub reporting_period: u64,
328+
pub kind: TaxLossHarvestingOpportunityKind,
329+
pub estimated_savings: Balance,
330+
pub current_tax_due: Balance,
331+
pub revised_tax_due: Balance,
332+
}
333+
309334
#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
310335
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
311336
pub enum Error {
@@ -1202,6 +1227,81 @@ pub struct TaxDeadlineNotification {
12021227
.get((property_id, jurisdiction_code, reporting_period))
12031228
}
12041229

1230+
#[ink(message)]
1231+
pub fn get_tax_loss_harvesting_opportunities(
1232+
&self,
1233+
property_id: u64,
1234+
jurisdiction: Jurisdiction,
1235+
) -> Result<Vec<TaxLossHarvestingOpportunity>> {
1236+
let now = self.env().block_timestamp();
1237+
let rule = self.get_active_rule(jurisdiction.code)?;
1238+
let assessment = self
1239+
.property_assessments
1240+
.get((property_id, jurisdiction.code))
1241+
.ok_or(Error::AssessmentNotFound)?;
1242+
let reporting_period = self
1243+
.latest_reporting_period
1244+
.get((property_id, jurisdiction.code))
1245+
.unwrap_or(self.reporting_period(now, rule.reporting_frequency));
1246+
let current_tax_due = self.estimate_tax_due(&rule, &assessment);
1247+
let current_taxable_value = self.taxable_value(&rule, &assessment);
1248+
let mut opportunities = Vec::new();
1249+
1250+
if let Some(record) = self
1251+
.tax_records
1252+
.get((property_id, jurisdiction.code, reporting_period))
1253+
{
1254+
let previous_base_due = self
1255+
.base_tax_due(record.taxable_value, rule.rate_basis_points)
1256+
.saturating_add(rule.fixed_charge);
1257+
let revised_base_due = current_tax_due;
1258+
1259+
if assessment.assessed_value < record.assessed_value
1260+
|| current_taxable_value < record.taxable_value
1261+
{
1262+
let estimated_savings = previous_base_due.saturating_sub(revised_base_due);
1263+
if estimated_savings > 0 {
1264+
opportunities.push(TaxLossHarvestingOpportunity {
1265+
property_id,
1266+
jurisdiction_code: jurisdiction.code,
1267+
reporting_period,
1268+
kind: TaxLossHarvestingOpportunityKind::Reassessment,
1269+
estimated_savings,
1270+
current_tax_due: previous_base_due,
1271+
revised_tax_due: revised_base_due,
1272+
});
1273+
}
1274+
}
1275+
}
1276+
1277+
let exemption_threshold = assessment.assessed_value / 20;
1278+
if assessment.exemption_override < exemption_threshold {
1279+
let revised_taxable_value = assessment.assessed_value.saturating_sub(
1280+
rule.exemption_amount
1281+
.saturating_add(exemption_threshold),
1282+
);
1283+
let revised_tax_due = self
1284+
.base_tax_due(revised_taxable_value, rule.rate_basis_points)
1285+
.saturating_add(rule.fixed_charge);
1286+
let estimated_savings = current_tax_due.saturating_sub(revised_tax_due);
1287+
1288+
if estimated_savings > 0 {
1289+
opportunities.push(TaxLossHarvestingOpportunity {
1290+
property_id,
1291+
jurisdiction_code: jurisdiction.code,
1292+
reporting_period,
1293+
kind: TaxLossHarvestingOpportunityKind::ExemptionReview,
1294+
estimated_savings,
1295+
current_tax_due,
1296+
revised_tax_due,
1297+
});
1298+
}
1299+
}
1300+
1301+
opportunities.sort_by(|left, right| right.estimated_savings.cmp(&left.estimated_savings));
1302+
Ok(opportunities)
1303+
}
1304+
12051305
#[ink(message)]
12061306
pub fn get_audit_trail(&self, property_id: u64, limit: u64) -> Vec<AuditEntry> {
12071307
let count = self.audit_log_count.get(property_id).unwrap_or(0);
@@ -1255,6 +1355,21 @@ pub struct TaxDeadlineNotification {
12551355
record.tax_due.saturating_sub(record.paid_amount)
12561356
}
12571357

1358+
fn taxable_value(&self, rule: &TaxRule, assessment: &PropertyAssessment) -> Balance {
1359+
assessment
1360+
.assessed_value
1361+
.saturating_sub(rule.exemption_amount.saturating_add(assessment.exemption_override))
1362+
}
1363+
1364+
fn base_tax_due(&self, taxable_value: Balance, rate_basis_points: u32) -> Balance {
1365+
taxable_value.saturating_mul(rate_basis_points as Balance) / BASIS_POINTS_DENOMINATOR
1366+
}
1367+
1368+
fn estimate_tax_due(&self, rule: &TaxRule, assessment: &PropertyAssessment) -> Balance {
1369+
self.base_tax_due(self.taxable_value(rule, assessment), rule.rate_basis_points)
1370+
.saturating_add(rule.fixed_charge)
1371+
}
1372+
12581373
fn registry_compliant(&self, owner: AccountId) -> bool {
12591374
match self.compliance_registry {
12601375
Some(registry) => {
@@ -1709,6 +1824,40 @@ pub struct TaxDeadlineNotification {
17091824
assert_eq!(record.status, TaxStatus::Assessed);
17101825
}
17111826

1827+
#[ink::test]
1828+
fn tax_loss_harvesting_recommends_reassessment_after_value_drop() {
1829+
let mut contract = TaxComplianceModule::new(None);
1830+
let owner = AccountId::from([0x07; 32]);
1831+
1832+
contract
1833+
.configure_tax_rule(jurisdiction(), rule())
1834+
.expect("rule");
1835+
contract
1836+
.set_property_assessment(11, jurisdiction(), owner, 240_000, 0)
1837+
.expect("assessment");
1838+
1839+
let initial_record = contract.calculate_tax(11, jurisdiction()).expect("tax");
1840+
1841+
contract
1842+
.set_property_assessment(11, jurisdiction(), owner, 180_000, 0)
1843+
.expect("updated assessment");
1844+
1845+
let opportunities = contract
1846+
.get_tax_loss_harvesting_opportunities(11, jurisdiction())
1847+
.expect("opportunities");
1848+
1849+
let reassessment = opportunities
1850+
.iter()
1851+
.find(|opportunity| {
1852+
opportunity.kind == TaxLossHarvestingOpportunityKind::Reassessment
1853+
})
1854+
.expect("reassessment opportunity");
1855+
1856+
assert!(reassessment.estimated_savings > 0);
1857+
assert!(reassessment.current_tax_due >= reassessment.revised_tax_due);
1858+
assert_eq!(reassessment.reporting_period, initial_record.reporting_period);
1859+
}
1860+
17121861
#[ink::test]
17131862
fn compliance_requires_payment_reporting_and_documents() {
17141863
let mut contract = TaxComplianceModule::new(None);

0 commit comments

Comments
 (0)