Skip to content

Commit c4cfa70

Browse files
committed
Add tax document storage on IPFS/Add tax advisor integration
1 parent 78f3423 commit c4cfa70

6 files changed

Lines changed: 766 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "tax-management"
3+
version = "0.1.0"
4+
edition = "2021"
5+
description = "Tax document storage on IPFS and tax advisor integration for StrellerMinds"
6+
license = "Apache-2.0"
7+
publish = false
8+
9+
[lib]
10+
crate-type = ["cdylib", "rlib"]
11+
12+
[dependencies]
13+
soroban-sdk = { workspace = true }
14+
15+
[dev-dependencies]
16+
soroban-sdk = { workspace = true, features = ["testutils"] }
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use soroban_sdk::contracterror;
2+
3+
#[contracterror]
4+
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
5+
#[repr(u32)]
6+
pub enum TaxError {
7+
AlreadyInitialized = 1,
8+
NotInitialized = 2,
9+
Unauthorized = 3,
10+
11+
InvalidIpfsHash = 10,
12+
InvalidTaxYear = 11,
13+
InvalidPropertyId = 12,
14+
DocumentNotFound = 13,
15+
DocumentAlreadyVerified = 14,
16+
17+
AdvisorAlreadyRegistered = 20,
18+
AdvisorNotFound = 21,
19+
AdvisorInactive = 22,
20+
InvalidLicense = 23,
21+
NoJurisdictions = 24,
22+
AdvisorNotAssigned = 25,
23+
}
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
#![no_std]
2+
3+
pub mod errors;
4+
pub mod types;
5+
6+
#[cfg(test)]
7+
mod test;
8+
9+
use crate::errors::TaxError;
10+
use crate::types::{DataKey, DocumentType, TaxAdvisor, TaxDocument};
11+
use soroban_sdk::{contract, contractimpl, Address, Env, String, Vec};
12+
13+
const MIN_TAX_YEAR: u32 = 1900;
14+
const MAX_TAX_YEAR: u32 = 2200;
15+
const MIN_IPFS_HASH_LEN: u32 = 16;
16+
17+
#[contract]
18+
pub struct TaxManagement;
19+
20+
#[contractimpl]
21+
impl TaxManagement {
22+
/// Initialize the contract with an admin address. Idempotent guard.
23+
pub fn initialize(env: Env, admin: Address) -> Result<(), TaxError> {
24+
if env.storage().instance().has(&DataKey::Admin) {
25+
return Err(TaxError::AlreadyInitialized);
26+
}
27+
admin.require_auth();
28+
env.storage().instance().set(&DataKey::Admin, &admin);
29+
env.storage().instance().set(&DataKey::DocumentCounter, &0u64);
30+
Ok(())
31+
}
32+
33+
// ─────────────────────────────────────────────────────────────
34+
// Tax document storage on IPFS
35+
// ─────────────────────────────────────────────────────────────
36+
37+
/// Upload a tax document referenced by its IPFS hash. Returns the new document id.
38+
pub fn upload_document(
39+
env: Env,
40+
owner: Address,
41+
property_id: String,
42+
doc_type: DocumentType,
43+
ipfs_hash: String,
44+
tax_year: u32,
45+
) -> Result<u64, TaxError> {
46+
Self::require_initialized(&env)?;
47+
owner.require_auth();
48+
49+
if property_id.len() == 0 {
50+
return Err(TaxError::InvalidPropertyId);
51+
}
52+
if ipfs_hash.len() < MIN_IPFS_HASH_LEN {
53+
return Err(TaxError::InvalidIpfsHash);
54+
}
55+
if tax_year < MIN_TAX_YEAR || tax_year > MAX_TAX_YEAR {
56+
return Err(TaxError::InvalidTaxYear);
57+
}
58+
59+
let id: u64 = env
60+
.storage()
61+
.instance()
62+
.get(&DataKey::DocumentCounter)
63+
.unwrap_or(0);
64+
let next_id = id + 1;
65+
66+
let document = TaxDocument {
67+
id: next_id,
68+
owner: owner.clone(),
69+
property_id: property_id.clone(),
70+
doc_type,
71+
ipfs_hash,
72+
tax_year,
73+
uploaded_at: env.ledger().timestamp(),
74+
verified: false,
75+
verifier: None,
76+
verified_at: 0,
77+
};
78+
79+
env.storage().persistent().set(&DataKey::Document(next_id), &document);
80+
env.storage().instance().set(&DataKey::DocumentCounter, &next_id);
81+
82+
let mut owner_docs: Vec<u64> = env
83+
.storage()
84+
.persistent()
85+
.get(&DataKey::OwnerDocuments(owner.clone()))
86+
.unwrap_or(Vec::new(&env));
87+
owner_docs.push_back(next_id);
88+
env.storage()
89+
.persistent()
90+
.set(&DataKey::OwnerDocuments(owner), &owner_docs);
91+
92+
let mut property_docs: Vec<u64> = env
93+
.storage()
94+
.persistent()
95+
.get(&DataKey::PropertyDocuments(property_id.clone()))
96+
.unwrap_or(Vec::new(&env));
97+
property_docs.push_back(next_id);
98+
env.storage()
99+
.persistent()
100+
.set(&DataKey::PropertyDocuments(property_id), &property_docs);
101+
102+
Ok(next_id)
103+
}
104+
105+
/// Verify a previously uploaded tax document. Only an active registered advisor
106+
/// may verify; verification is one-shot.
107+
pub fn verify_document(env: Env, advisor: Address, document_id: u64) -> Result<(), TaxError> {
108+
Self::require_initialized(&env)?;
109+
advisor.require_auth();
110+
111+
let advisor_record: TaxAdvisor = env
112+
.storage()
113+
.persistent()
114+
.get(&DataKey::Advisor(advisor.clone()))
115+
.ok_or(TaxError::AdvisorNotFound)?;
116+
if !advisor_record.active {
117+
return Err(TaxError::AdvisorInactive);
118+
}
119+
120+
let mut document: TaxDocument = env
121+
.storage()
122+
.persistent()
123+
.get(&DataKey::Document(document_id))
124+
.ok_or(TaxError::DocumentNotFound)?;
125+
if document.verified {
126+
return Err(TaxError::DocumentAlreadyVerified);
127+
}
128+
129+
document.verified = true;
130+
document.verifier = Some(advisor);
131+
document.verified_at = env.ledger().timestamp();
132+
env.storage()
133+
.persistent()
134+
.set(&DataKey::Document(document_id), &document);
135+
Ok(())
136+
}
137+
138+
/// Retrieve a tax document by id.
139+
pub fn get_document(env: Env, document_id: u64) -> Option<TaxDocument> {
140+
env.storage().persistent().get(&DataKey::Document(document_id))
141+
}
142+
143+
/// Confirm that the on-chain IPFS hash for `document_id` matches the supplied one.
144+
pub fn verify_ipfs_hash(env: Env, document_id: u64, ipfs_hash: String) -> bool {
145+
match env
146+
.storage()
147+
.persistent()
148+
.get::<DataKey, TaxDocument>(&DataKey::Document(document_id))
149+
{
150+
Some(doc) => doc.ipfs_hash == ipfs_hash,
151+
None => false,
152+
}
153+
}
154+
155+
pub fn get_documents_by_owner(env: Env, owner: Address) -> Vec<u64> {
156+
env.storage()
157+
.persistent()
158+
.get(&DataKey::OwnerDocuments(owner))
159+
.unwrap_or(Vec::new(&env))
160+
}
161+
162+
pub fn get_documents_by_property(env: Env, property_id: String) -> Vec<u64> {
163+
env.storage()
164+
.persistent()
165+
.get(&DataKey::PropertyDocuments(property_id))
166+
.unwrap_or(Vec::new(&env))
167+
}
168+
169+
// ─────────────────────────────────────────────────────────────
170+
// Tax advisor integration
171+
// ─────────────────────────────────────────────────────────────
172+
173+
/// Register a new tax advisor. Admin-only.
174+
pub fn register_advisor(
175+
env: Env,
176+
admin: Address,
177+
advisor: Address,
178+
name: String,
179+
license_id: String,
180+
jurisdictions: Vec<String>,
181+
) -> Result<(), TaxError> {
182+
Self::require_admin(&env, &admin)?;
183+
184+
if license_id.len() == 0 {
185+
return Err(TaxError::InvalidLicense);
186+
}
187+
if jurisdictions.is_empty() {
188+
return Err(TaxError::NoJurisdictions);
189+
}
190+
if env.storage().persistent().has(&DataKey::Advisor(advisor.clone())) {
191+
return Err(TaxError::AdvisorAlreadyRegistered);
192+
}
193+
194+
let record = TaxAdvisor {
195+
address: advisor.clone(),
196+
name,
197+
license_id,
198+
jurisdictions,
199+
active: true,
200+
registered_at: env.ledger().timestamp(),
201+
};
202+
env.storage().persistent().set(&DataKey::Advisor(advisor), &record);
203+
Ok(())
204+
}
205+
206+
/// Update the jurisdictions an advisor is licensed for. Admin-only.
207+
pub fn update_advisor_jurisdictions(
208+
env: Env,
209+
admin: Address,
210+
advisor: Address,
211+
jurisdictions: Vec<String>,
212+
) -> Result<(), TaxError> {
213+
Self::require_admin(&env, &admin)?;
214+
if jurisdictions.is_empty() {
215+
return Err(TaxError::NoJurisdictions);
216+
}
217+
let mut record: TaxAdvisor = env
218+
.storage()
219+
.persistent()
220+
.get(&DataKey::Advisor(advisor.clone()))
221+
.ok_or(TaxError::AdvisorNotFound)?;
222+
record.jurisdictions = jurisdictions;
223+
env.storage().persistent().set(&DataKey::Advisor(advisor), &record);
224+
Ok(())
225+
}
226+
227+
/// Deactivate a registered advisor. Admin-only.
228+
pub fn deactivate_advisor(
229+
env: Env,
230+
admin: Address,
231+
advisor: Address,
232+
) -> Result<(), TaxError> {
233+
Self::require_admin(&env, &admin)?;
234+
let mut record: TaxAdvisor = env
235+
.storage()
236+
.persistent()
237+
.get(&DataKey::Advisor(advisor.clone()))
238+
.ok_or(TaxError::AdvisorNotFound)?;
239+
record.active = false;
240+
env.storage().persistent().set(&DataKey::Advisor(advisor), &record);
241+
Ok(())
242+
}
243+
244+
pub fn get_advisor(env: Env, advisor: Address) -> Option<TaxAdvisor> {
245+
env.storage().persistent().get(&DataKey::Advisor(advisor))
246+
}
247+
248+
/// Owner-authorized assignment of a registered advisor to a property.
249+
pub fn assign_advisor_to_property(
250+
env: Env,
251+
owner: Address,
252+
property_id: String,
253+
advisor: Address,
254+
) -> Result<(), TaxError> {
255+
Self::require_initialized(&env)?;
256+
owner.require_auth();
257+
258+
if property_id.len() == 0 {
259+
return Err(TaxError::InvalidPropertyId);
260+
}
261+
let record: TaxAdvisor = env
262+
.storage()
263+
.persistent()
264+
.get(&DataKey::Advisor(advisor.clone()))
265+
.ok_or(TaxError::AdvisorNotFound)?;
266+
if !record.active {
267+
return Err(TaxError::AdvisorInactive);
268+
}
269+
env.storage()
270+
.persistent()
271+
.set(&DataKey::PropertyAdvisor(property_id), &advisor);
272+
Ok(())
273+
}
274+
275+
pub fn unassign_property_advisor(
276+
env: Env,
277+
owner: Address,
278+
property_id: String,
279+
) -> Result<(), TaxError> {
280+
Self::require_initialized(&env)?;
281+
owner.require_auth();
282+
283+
let key = DataKey::PropertyAdvisor(property_id);
284+
if !env.storage().persistent().has(&key) {
285+
return Err(TaxError::AdvisorNotAssigned);
286+
}
287+
env.storage().persistent().remove(&key);
288+
Ok(())
289+
}
290+
291+
pub fn get_property_advisor(env: Env, property_id: String) -> Option<Address> {
292+
env.storage().persistent().get(&DataKey::PropertyAdvisor(property_id))
293+
}
294+
295+
// ─────────────────────────────────────────────────────────────
296+
// Internal helpers
297+
// ─────────────────────────────────────────────────────────────
298+
299+
fn require_initialized(env: &Env) -> Result<(), TaxError> {
300+
if !env.storage().instance().has(&DataKey::Admin) {
301+
return Err(TaxError::NotInitialized);
302+
}
303+
Ok(())
304+
}
305+
306+
fn require_admin(env: &Env, caller: &Address) -> Result<(), TaxError> {
307+
let admin: Address = env
308+
.storage()
309+
.instance()
310+
.get(&DataKey::Admin)
311+
.ok_or(TaxError::NotInitialized)?;
312+
if &admin != caller {
313+
return Err(TaxError::Unauthorized);
314+
}
315+
caller.require_auth();
316+
Ok(())
317+
}
318+
}

0 commit comments

Comments
 (0)