diff --git a/contracts/identity-registry-contract/src/contract.rs b/contracts/identity-registry-contract/src/contract.rs index c241478..1814245 100644 --- a/contracts/identity-registry-contract/src/contract.rs +++ b/contracts/identity-registry-contract/src/contract.rs @@ -31,7 +31,7 @@ pub fn batch_add_experts(env: Env, experts: Vec
) -> Result<(), Registry } // Default empty URI for batch adds let empty_uri = String::from_str(&env, ""); - storage::set_expert_record(&env, &expert, ExpertStatus::Verified, empty_uri); + storage::set_expert_record(&env, &expert, ExpertStatus::Verified, empty_uri, 0); storage::add_expert_to_index(&env, &expert); events::emit_status_change(&env, expert, status, ExpertStatus::Verified, admin.clone()); } @@ -54,7 +54,7 @@ pub fn batch_ban_experts(env: Env, experts: Vec) -> Result<(), Registry return Err(RegistryError::AlreadyBanned); } let existing = storage::get_expert_record(&env, &expert); - storage::set_expert_record(&env, &expert, ExpertStatus::Banned, existing.data_uri); + storage::set_expert_record(&env, &expert, ExpertStatus::Banned, existing.data_uri, existing.category_id); events::emit_status_change(&env, expert, status, ExpertStatus::Banned, admin.clone()); } @@ -82,6 +82,7 @@ pub fn verify_expert( caller: &Address, expert: &Address, data_uri: String, + category_id: u32, ) -> Result<(), RegistryError> { let admin = storage::get_admin(env).ok_or(RegistryError::NotInitialized)?; @@ -104,7 +105,7 @@ pub fn verify_expert( return Err(RegistryError::UriTooLong); } - storage::set_expert_record(env, expert, ExpertStatus::Verified, data_uri); + storage::set_expert_record(env, expert, ExpertStatus::Verified, data_uri, category_id); storage::add_expert_to_index(env, expert); events::emit_status_change( @@ -136,9 +137,9 @@ pub fn ban_expert(env: &Env, caller: &Address, expert: &Address) -> Result<(), R return Err(RegistryError::AlreadyBanned); } - // Preserve existing data_uri when banning + // Preserve existing data_uri and category_id when banning let existing = storage::get_expert_record(env, expert); - storage::set_expert_record(env, expert, ExpertStatus::Banned, existing.data_uri); + storage::set_expert_record(env, expert, ExpertStatus::Banned, existing.data_uri, existing.category_id); events::emit_status_change( env, @@ -162,9 +163,9 @@ pub fn unban_expert(env: &Env, expert: &Address) -> Result<(), RegistryError> { return Err(RegistryError::NotBanned); } - // Preserve existing data_uri when unbanning + // Preserve existing data_uri and category_id when unbanning let existing = storage::get_expert_record(env, expert); - storage::set_expert_record(env, expert, ExpertStatus::Verified, existing.data_uri); + storage::set_expert_record(env, expert, ExpertStatus::Verified, existing.data_uri, existing.category_id); events::emit_status_change( env, @@ -199,7 +200,7 @@ pub fn is_verified(env: &Env, expert: &Address) -> bool { } /// Allow a verified expert to update their own profile URI -pub fn update_profile(env: &Env, expert: &Address, new_uri: String) -> Result<(), RegistryError> { +pub fn update_profile(env: &Env, expert: &Address, new_uri: String, category_id: u32) -> Result<(), RegistryError> { expert.require_auth(); // Validate URI length @@ -213,7 +214,7 @@ pub fn update_profile(env: &Env, expert: &Address, new_uri: String) -> Result<() } // Update record preserving status - storage::set_expert_record(env, expert, status, new_uri.clone()); + storage::set_expert_record(env, expert, status, new_uri.clone(), category_id); events::emit_profile_updated(env, expert.clone(), new_uri); Ok(()) } @@ -222,7 +223,7 @@ pub fn update_profile(env: &Env, expert: &Address, new_uri: String) -> Result<() /// Allows admins to update multiple expert metadata URIs in a single transaction pub fn batch_update_profiles( env: &Env, - updates: Vec<(Address, String, u32)>, + updates: Vec<(Address, String, u32, u32)>, ) -> Result<(), RegistryError> { // Limit batch size to prevent DoS if updates.len() > 20 { @@ -233,7 +234,7 @@ pub fn batch_update_profiles( admin.require_auth(); for update in updates { - let (expert, new_uri, status_u32) = update; + let (expert, new_uri, status_u32, category_id) = update; // Validate URI length if new_uri.len() > 64 { @@ -249,7 +250,7 @@ pub fn batch_update_profiles( }; // Update the expert record - storage::set_expert_record(env, &expert, status, new_uri); + storage::set_expert_record(env, &expert, status, new_uri, category_id); } Ok(()) diff --git a/contracts/identity-registry-contract/src/lib.rs b/contracts/identity-registry-contract/src/lib.rs index 4963b6c..dec5ed4 100644 --- a/contracts/identity-registry-contract/src/lib.rs +++ b/contracts/identity-registry-contract/src/lib.rs @@ -49,8 +49,9 @@ impl IdentityRegistryContract { caller: Address, expert: Address, data_uri: String, + category_id: u32, ) -> Result<(), RegistryError> { - contract::verify_expert(&env, &caller, &expert, data_uri) + contract::verify_expert(&env, &caller, &expert, data_uri, category_id) } /// Ban an expert and revoke their verification status (Admin or Moderator) @@ -85,15 +86,15 @@ impl IdentityRegistryContract { } /// Allow a verified expert to update their own profile URI - pub fn update_profile(env: Env, expert: Address, new_uri: String) -> Result<(), RegistryError> { - contract::update_profile(&env, &expert, new_uri) + pub fn update_profile(env: Env, expert: Address, new_uri: String, category_id: u32) -> Result<(), RegistryError> { + contract::update_profile(&env, &expert, new_uri, category_id) } /// Batch update expert profiles (Admin only) /// Allows admins to update multiple expert metadata URIs in a single transaction pub fn batch_update_profiles( env: Env, - updates: Vec<(Address, String, u32)>, + updates: Vec<(Address, String, u32, u32)>, ) -> Result<(), RegistryError> { contract::batch_update_profiles(&env, updates) } diff --git a/contracts/identity-registry-contract/src/storage.rs b/contracts/identity-registry-contract/src/storage.rs index c3cc222..d8fb0a8 100644 --- a/contracts/identity-registry-contract/src/storage.rs +++ b/contracts/identity-registry-contract/src/storage.rs @@ -68,13 +68,14 @@ pub fn remove_moderator(env: &Env, address: &Address) { // ... [Expert Helpers] ... /// Set the expert record with status, data_uri and timestamp -pub fn set_expert_record(env: &Env, expert: &Address, status: ExpertStatus, data_uri: String) { +pub fn set_expert_record(env: &Env, expert: &Address, status: ExpertStatus, data_uri: String, category_id: u32) { let key = DataKey::Expert(expert.clone()); let record = ExpertRecord { status, updated_at: env.ledger().timestamp(), data_uri, + category_id, }; // 1. Save the data @@ -107,6 +108,7 @@ pub fn get_expert_record(env: &Env, expert: &Address) -> ExpertRecord { status: ExpertStatus::Unverified, updated_at: 0, data_uri: String::from_str(env, ""), + category_id: 0, }) } diff --git a/contracts/identity-registry-contract/src/test.rs b/contracts/identity-registry-contract/src/test.rs index 7f491d1..5fc4277 100644 --- a/contracts/identity-registry-contract/src/test.rs +++ b/contracts/identity-registry-contract/src/test.rs @@ -41,7 +41,7 @@ fn test_data_uri_persisted_on_verify() { let uri = String::from_str(&env, "ipfs://persisted"); client.init(&admin); - client.add_expert(&admin, &expert, &uri); + client.add_expert(&admin, &expert, &uri, &0u32); // Read storage as contract and assert data_uri persisted env.as_contract(&contract_id, || { @@ -64,10 +64,10 @@ fn test_update_profile_updates_uri_and_emits_event() { let uri2 = String::from_str(&env, "ipfs://updated"); client.init(&admin); - client.add_expert(&admin, &expert, &uri1); + client.add_expert(&admin, &expert, &uri1, &0u32); // Update profile URI - client.update_profile(&expert, &uri2); + client.update_profile(&expert, &uri2, &1u32); // Assert record updated env.as_contract(&contract_id, || { @@ -92,18 +92,18 @@ fn test_update_profile_rejections() { // NotVerified when updating without being verified let new_uri = String::from_str(&env, "ipfs://new"); - let res = client.try_update_profile(&unverified, &new_uri); + let res = client.try_update_profile(&unverified, &new_uri, &0u32); assert_eq!(res, Err(Ok(RegistryError::NotVerified))); // Verify then try overlong uri let expert = Address::generate(&env); let ok_uri = String::from_str(&env, "ipfs://ok"); - client.add_expert(&admin, &expert, &ok_uri); + client.add_expert(&admin, &expert, &ok_uri, &0u32); // Build >64 length string let long_str = "a".repeat(65); let long_uri = String::from_str(&env, long_str.as_str()); - let res2 = client.try_update_profile(&expert, &long_uri); + let res2 = client.try_update_profile(&expert, &long_uri, &0u32); assert_eq!(res2, Err(Ok(RegistryError::UriTooLong))); } @@ -239,7 +239,7 @@ fn test_add_expert() { client.init(&admin); let data_uri = String::from_str(&env, "ipfs://profile1"); - let res = client.try_add_expert(&admin, &expert, &data_uri); + let res = client.try_add_expert(&admin, &expert, &data_uri, &0u32); assert!(res.is_ok()); assert_eq!( @@ -250,7 +250,7 @@ fn test_add_expert() { function: AuthorizedFunction::Contract(( contract_id.clone(), Symbol::new(&env, "add_expert"), - (admin.clone(), expert.clone(), data_uri.clone()).into_val(&env) + (admin.clone(), expert.clone(), data_uri.clone(), 0u32).into_val(&env) )), sub_invocations: std::vec![] } @@ -272,7 +272,7 @@ fn test_add_expert_unauthorized() { client.init(&admin); let data_uri = String::from_str(&env, "ipfs://unauth"); // admin is the caller but no auth is mocked — should panic - client.add_expert(&admin, &expert, &data_uri); + client.add_expert(&admin, &expert, &data_uri, &0u32); } #[test] @@ -288,7 +288,7 @@ fn test_expert_status_changed_event() { client.init(&admin); let data_uri = String::from_str(&env, "ipfs://event"); - client.add_expert(&admin, &expert, &data_uri); + client.add_expert(&admin, &expert, &data_uri, &0u32); let events = env.events().all(); let event = events.last().unwrap(); @@ -314,7 +314,7 @@ fn test_ban_expert() { // Verify the expert first env.mock_all_auths(); let data_uri = String::from_str(&env, "ipfs://ban"); - client.add_expert(&admin, &expert, &data_uri); + client.add_expert(&admin, &expert, &data_uri, &0u32); // Verify status is Verified let status = client.get_status(&expert); @@ -346,7 +346,7 @@ fn test_ban_expert_unauthorized() { env.mock_all_auths(); let data_uri = String::from_str(&env, "ipfs://ban-unauth"); - client.add_expert(&admin, &expert, &data_uri); + client.add_expert(&admin, &expert, &data_uri, &0u32); env.mock_all_auths_allowing_non_root_auth(); @@ -401,9 +401,9 @@ fn test_ban_expert_workflow() { let uri1 = String::from_str(&env, "ipfs://u1"); let uri2 = String::from_str(&env, "ipfs://u2"); let uri3 = String::from_str(&env, "ipfs://u3"); - client.add_expert(&admin, &expert1, &uri1); - client.add_expert(&admin, &expert2, &uri2); - client.add_expert(&admin, &expert3, &uri3); + client.add_expert(&admin, &expert1, &uri1, &0u32); + client.add_expert(&admin, &expert2, &uri2, &0u32); + client.add_expert(&admin, &expert3, &uri3, &0u32); // Check all are verified assert_eq!(client.get_status(&expert1), ExpertStatus::Verified); @@ -462,7 +462,7 @@ fn test_complete_expert_lifecycle() { // 2. Verify the expert let data_uri = String::from_str(&env, "ipfs://life"); - client.add_expert(&admin, &expert, &data_uri); + client.add_expert(&admin, &expert, &data_uri, &0u32); assert_eq!(client.get_status(&expert), ExpertStatus::Verified); // 3. Ban the expert @@ -489,7 +489,7 @@ fn test_getters() { // Test 2: Verify an expert and check is_verified (should be true) let expert = Address::generate(&env); let data_uri = String::from_str(&env, "ipfs://getters"); - client.add_expert(&admin, &expert, &data_uri); + client.add_expert(&admin, &expert, &data_uri, &0u32); assert_eq!(client.is_verified(&expert), true); assert_eq!(client.get_status(&expert), ExpertStatus::Verified); @@ -518,9 +518,9 @@ fn test_expert_directory_enumeration() { let uri1 = String::from_str(&env, "ipfs://e1"); let uri2 = String::from_str(&env, "ipfs://e2"); let uri3 = String::from_str(&env, "ipfs://e3"); - client.add_expert(&admin, &expert1, &uri1); - client.add_expert(&admin, &expert2, &uri2); - client.add_expert(&admin, &expert3, &uri3); + client.add_expert(&admin, &expert1, &uri1, &0u32); + client.add_expert(&admin, &expert2, &uri2, &0u32); + client.add_expert(&admin, &expert3, &uri3, &0u32); // Total should be 3 assert_eq!(client.get_total_experts(), 3u64); @@ -545,13 +545,13 @@ fn test_expert_directory_no_duplicates_on_reverify() { client.init(&admin); let uri = String::from_str(&env, "ipfs://expert"); - client.add_expert(&admin, &expert, &uri); + client.add_expert(&admin, &expert, &uri, &0u32); // Total is 1 assert_eq!(client.get_total_experts(), 1u64); // Re-verifying an already verified expert returns AlreadyVerified - let result = client.try_add_expert(&admin, &expert, &uri); + let result = client.try_add_expert(&admin, &expert, &uri, &0u32); assert_eq!(result, Err(Ok(RegistryError::AlreadyVerified))); // Total remains 1 — no duplicate in the index @@ -609,11 +609,11 @@ fn test_batch_update_profiles() { let uri4 = String::from_str(&env, "ipfs://original4"); let uri5 = String::from_str(&env, "ipfs://original5"); - client.add_expert(&admin, &expert1, &uri1); - client.add_expert(&admin, &expert2, &uri2); - client.add_expert(&admin, &expert3, &uri3); - client.add_expert(&admin, &expert4, &uri4); - client.add_expert(&admin, &expert5, &uri5); + client.add_expert(&admin, &expert1, &uri1, &0u32); + client.add_expert(&admin, &expert2, &uri2, &0u32); + client.add_expert(&admin, &expert3, &uri3, &0u32); + client.add_expert(&admin, &expert4, &uri4, &0u32); + client.add_expert(&admin, &expert5, &uri5, &0u32); // Prepare batch updates with new URIs let new_uri1 = String::from_str(&env, "ipfs://updated1"); @@ -624,11 +624,11 @@ fn test_batch_update_profiles() { let updates = vec![ &env, - (expert1.clone(), new_uri1.clone(), 1u32), // Verified - (expert2.clone(), new_uri2.clone(), 1u32), // Verified - (expert3.clone(), new_uri3.clone(), 1u32), // Verified - (expert4.clone(), new_uri4.clone(), 1u32), // Verified - (expert5.clone(), new_uri5.clone(), 1u32), // Verified + (expert1.clone(), new_uri1.clone(), 1u32, 0u32), + (expert2.clone(), new_uri2.clone(), 1u32, 0u32), + (expert3.clone(), new_uri3.clone(), 1u32, 0u32), + (expert4.clone(), new_uri4.clone(), 1u32, 0u32), + (expert5.clone(), new_uri5.clone(), 1u32, 0u32), ]; // Execute batch update @@ -674,7 +674,7 @@ fn test_batch_update_profiles_max_vec() { for _ in 0..21 { let expert = Address::generate(&env); let uri = String::from_str(&env, "ipfs://test"); - updates.push_back((expert, uri, 1u32)); + updates.push_back((expert, uri, 1u32, 0u32)); } // This should fail with ExpertVecMax error @@ -694,13 +694,13 @@ fn test_batch_update_profiles_uri_too_long() { let expert = Address::generate(&env); let uri = String::from_str(&env, "ipfs://initial"); - client.add_expert(&admin, &expert, &uri); + client.add_expert(&admin, &expert, &uri, &0u32); // Create update with URI that's too long (>64 chars) let long_str = "a".repeat(65); let long_uri = String::from_str(&env, long_str.as_str()); - let updates = vec![&env, (expert.clone(), long_uri, 1u32)]; + let updates = vec![&env, (expert.clone(), long_uri, 1u32, 0u32)]; // This should fail with UriTooLong error let result = client.try_batch_update_profiles(&updates); @@ -723,7 +723,7 @@ fn test_expert_pagination() { for _ in 0..15 { let expert = Address::generate(&env); let uri = String::from_str(&env, "ipfs://expert"); - client.add_expert(&admin, &expert, &uri); + client.add_expert(&admin, &expert, &uri, &0u32); experts.push_back(expert); } @@ -762,7 +762,7 @@ fn test_unban_expert() { env.mock_all_auths(); let data_uri = String::from_str(&env, "ipfs://unban"); - client.add_expert(&admin, &expert, &data_uri); + client.add_expert(&admin, &expert, &data_uri, &0u32); // Initial status: Verified assert_eq!(client.get_status(&expert), ExpertStatus::Verified); @@ -809,7 +809,7 @@ fn test_moderator_can_verify_expert() { // Moderator verifies an expert (should succeed) let uri = String::from_str(&env, "ipfs://mod-verify"); - let res = client.try_add_expert(&moderator, &expert, &uri); + let res = client.try_add_expert(&moderator, &expert, &uri, &0u32); assert!(res.is_ok()); assert_eq!(client.get_status(&expert), ExpertStatus::Verified); @@ -832,7 +832,7 @@ fn test_moderator_can_ban_expert() { // Verify expert first (by admin) let uri = String::from_str(&env, "ipfs://mod-ban"); - client.add_expert(&admin, &expert, &uri); + client.add_expert(&admin, &expert, &uri, &0u32); // Moderator bans the expert (should succeed) let res = client.try_ban_expert(&moderator, &expert); @@ -857,7 +857,7 @@ fn test_non_moderator_cannot_verify_expert() { // Random address (not admin, not moderator) tries to verify — should fail with Unauthorized let uri = String::from_str(&env, "ipfs://unauth"); - let res = client.try_add_expert(&random, &expert, &uri); + let res = client.try_add_expert(&random, &expert, &uri, &0u32); assert_eq!(res, Err(Ok(RegistryError::Unauthorized))); } @@ -886,6 +886,57 @@ fn test_remove_moderator() { // After removal, moderator can no longer verify experts let uri = String::from_str(&env, "ipfs://removed-mod"); - let res = client.try_add_expert(&moderator, &expert, &uri); + let res = client.try_add_expert(&moderator, &expert, &uri, &0u32); assert_eq!(res, Err(Ok(RegistryError::Unauthorized))); } + +#[test] +fn test_category_id_persisted_and_updated() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(IdentityRegistryContract, ()); + let client = IdentityRegistryContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let expert = Address::generate(&env); + + client.init(&admin); + + // Add expert with category_id = 5 + let uri = String::from_str(&env, "ipfs://cat"); + client.add_expert(&admin, &expert, &uri, &5u32); + + env.as_contract(&contract_id, || { + let rec = storage::get_expert_record(&env, &expert); + assert_eq!(rec.category_id, 5); + }); + + // Update profile with new category_id = 10 + let uri2 = String::from_str(&env, "ipfs://cat2"); + client.update_profile(&expert, &uri2, &10u32); + + env.as_contract(&contract_id, || { + let rec = storage::get_expert_record(&env, &expert); + assert_eq!(rec.category_id, 10); + assert_eq!(rec.data_uri, uri2); + }); + + // Ban and verify category_id is preserved + client.ban_expert(&admin, &expert); + + env.as_contract(&contract_id, || { + let rec = storage::get_expert_record(&env, &expert); + assert_eq!(rec.category_id, 10); + assert_eq!(rec.status, ExpertStatus::Banned); + }); + + // Unban and verify category_id is preserved + client.unban_expert(&expert); + + env.as_contract(&contract_id, || { + let rec = storage::get_expert_record(&env, &expert); + assert_eq!(rec.category_id, 10); + assert_eq!(rec.status, ExpertStatus::Verified); + }); +} diff --git a/contracts/identity-registry-contract/src/types.rs b/contracts/identity-registry-contract/src/types.rs index 4199f80..37cd212 100644 --- a/contracts/identity-registry-contract/src/types.rs +++ b/contracts/identity-registry-contract/src/types.rs @@ -18,4 +18,5 @@ pub struct ExpertRecord { pub status: ExpertStatus, pub updated_at: u64, // Ledger timestamp of the last change pub data_uri: String, + pub category_id: u32, } diff --git a/contracts/reputation-scoring-contract/src/contract.rs b/contracts/reputation-scoring-contract/src/contract.rs index f6f579a..bafefd3 100644 --- a/contracts/reputation-scoring-contract/src/contract.rs +++ b/contracts/reputation-scoring-contract/src/contract.rs @@ -1,7 +1,8 @@ use crate::error::ReputationError; use crate::events; use crate::storage; -use soroban_sdk::{Address, BytesN, Env}; +use crate::types::{BookingRecord, BookingStatus, ExpertStats, ReviewRecord}; +use soroban_sdk::{Address, BytesN, Env, IntoVal, Symbol}; pub fn initialize( env: &Env, @@ -49,3 +50,76 @@ pub fn upgrade_contract(env: &Env, new_wasm_hash: BytesN<32>) -> Result<(), Repu env.deployer().update_current_contract_wasm(new_wasm_hash); Ok(()) } + +pub fn submit_review( + env: &Env, + reviewer: &Address, + booking_id: u64, + score: u32, +) -> Result<(), ReputationError> { + storage::get_admin(env).ok_or(ReputationError::NotInitialized)?; + + if storage::is_paused(env) { + return Err(ReputationError::ContractPaused); + } + + reviewer.require_auth(); + + // Validate score 1–5 + if score < 1 || score > 5 { + return Err(ReputationError::InvalidScore); + } + + // Prevent duplicate reviews + if storage::has_review(env, booking_id) { + return Err(ReputationError::AlreadyReviewed); + } + + // Cross-contract call to vault: get_booking + let vault_address = storage::get_vault_address(env) + .ok_or(ReputationError::NotInitialized)?; + + let booking: BookingRecord = env.invoke_contract( + &vault_address, + &Symbol::new(env, "get_booking"), + soroban_sdk::vec![env, booking_id.into_val(env)], + ); + + // Check booking is Complete + if booking.status != BookingStatus::Complete { + return Err(ReputationError::BookingNotComplete); + } + + // Verify reviewer is the booking user + if *reviewer != booking.user { + return Err(ReputationError::NotBookingUser); + } + + // Store review + let review = ReviewRecord { + booking_id, + reviewer: reviewer.clone(), + expert: booking.expert.clone(), + score, + timestamp: env.ledger().timestamp(), + }; + storage::set_review(env, booking_id, &review); + + // Update expert stats + let mut stats = storage::get_expert_stats(env, &booking.expert); + stats.total_score += score as u64; + stats.review_count += 1; + storage::set_expert_stats(env, &booking.expert, &stats); + + events::review_submitted(env, booking_id, reviewer, &booking.expert, score); + + Ok(()) +} + +pub fn get_review(env: &Env, booking_id: u64) -> Option