@@ -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