diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index c290ba80..f4b38a27 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -635,6 +635,13 @@ dependencies = [ "subtle", ] +[[package]] +name = "hackathon-team-matching" +version = "0.1.0" +dependencies = [ + "soroban-sdk", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -896,6 +903,14 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "payment-gateway" +version = "0.1.0" +dependencies = [ + "rand", + "soroban-sdk", +] + [[package]] name = "payment-streaming" version = "0.1.0" diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 78809737..52ebf340 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -18,7 +18,8 @@ members = [ "course_proxy", "auth_checker", "cross_chain_client", - "freelance-platform" + "freelance-platform", + "hackathon-team-matching" ] [lib] diff --git a/contracts/hackathon-team-matching/Cargo.toml b/contracts/hackathon-team-matching/Cargo.toml new file mode 100644 index 00000000..2e7f38f2 --- /dev/null +++ b/contracts/hackathon-team-matching/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "hackathon-team-matching" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = "22.0.0" + +[dev-dependencies] +soroban-sdk = { version = "22.0.0", features = ["testutils"] } diff --git a/contracts/hackathon-team-matching/src/lib.rs b/contracts/hackathon-team-matching/src/lib.rs new file mode 100644 index 00000000..5ece67c5 --- /dev/null +++ b/contracts/hackathon-team-matching/src/lib.rs @@ -0,0 +1,658 @@ +#![no_std] +use soroban_sdk::{ + contract, contracterror, contractimpl, contracttype, panic_with_error, symbol_short, Address, + Env, Symbol, Vec, +}; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum MatchingError { + NotInitialized = 1, + AlreadyInitialized = 2, + Unauthorized = 3, + DeveloperNotFound = 4, + TeamNotFound = 5, + AlreadyInTeam = 6, + TeamFull = 7, + TeamClosed = 8, + NoActiveRequest = 9, + NoActiveInvitation = 10, + InvalidSkillVerificationContract = 11, + AlreadyRegistered = 12, +} + +#[contracttype] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum SkillLevel { + Beginner = 0, + Intermediate = 1, + Advanced = 2, + Expert = 3, +} + +#[soroban_sdk::contractclient(name = "SkillVerificationClient")] +pub trait SkillVerification { + fn verify_skill( + env: soroban_sdk::Env, + user: soroban_sdk::Address, + skill: soroban_sdk::Symbol, + min_level: SkillLevel, + ) -> bool; +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Developer { + pub address: Address, + pub skills: Vec, + pub preferred_role: Symbol, + pub team_id: u64, // 0 means no team +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Team { + pub id: u64, + pub creator: Address, + pub name: Symbol, + pub required_skills: Vec, + pub required_roles: Vec, + pub members: Vec
, + pub max_members: u32, + pub closed: bool, +} + +#[contracttype] +#[derive(Clone)] +pub enum DataKey { + Admin, + SkillVerifier, + NextTeamId, + Developer(Address), + Team(u64), + JoinRequests(u64), // Vec
+ Invitations(Address), // Vec + AllDevelopers, // Vec
+} + +#[contract] +pub struct HackathonTeamMatching; + +#[contractimpl] +impl HackathonTeamMatching { + /// Initialize the contract with admin and skill verification contract addresses + pub fn initialize(env: Env, admin: Address, skill_verifier: Address) { + if env.storage().instance().has(&DataKey::Admin) { + panic_with_error!(&env, MatchingError::AlreadyInitialized); + } + env.storage().instance().set(&DataKey::Admin, &admin); + env.storage().instance().set(&DataKey::SkillVerifier, &skill_verifier); + env.storage().instance().set(&DataKey::NextTeamId, &1u64); + env.storage().persistent().set(&DataKey::AllDevelopers, &Vec::
::new(&env)); + } + + /// Register a developer profile + pub fn register_developer( + env: Env, + developer: Address, + skills: Vec, + preferred_role: Symbol, + ) { + developer.require_auth(); + if env.storage().persistent().has(&DataKey::Developer(developer.clone())) { + panic_with_error!(&env, MatchingError::AlreadyRegistered); + } + + let dev = Developer { + address: developer.clone(), + skills, + preferred_role, + team_id: 0, + }; + env.storage().persistent().set(&DataKey::Developer(developer.clone()), &dev); + + let mut all_devs: Vec
= env + .storage() + .persistent() + .get(&DataKey::AllDevelopers) + .unwrap_or(Vec::new(&env)); + all_devs.push_back(developer.clone()); + env.storage().persistent().set(&DataKey::AllDevelopers, &all_devs); + + env.events().publish((symbol_short!("dev_reg"), developer), ()); + } + + /// Retrieve a developer profile + pub fn get_developer(env: Env, developer: Address) -> Option { + env.storage().persistent().get(&DataKey::Developer(developer)) + } + + /// Create a team + pub fn create_team( + env: Env, + creator: Address, + name: Symbol, + required_skills: Vec, + required_roles: Vec, + max_members: u32, + ) -> u64 { + creator.require_auth(); + + let mut creator_dev = env + .storage() + .persistent() + .get(&DataKey::Developer(creator.clone())) + .unwrap_or(Developer { + address: creator.clone(), + skills: Vec::new(&env), + preferred_role: Symbol::new(&env, "creator"), + team_id: 0, + }); + + if creator_dev.team_id != 0 { + panic_with_error!(&env, MatchingError::AlreadyInTeam); + } + + let team_id: u64 = env.storage().instance().get(&DataKey::NextTeamId).unwrap_or(1); + env.storage().instance().set(&DataKey::NextTeamId, &(team_id + 1)); + + let mut members = Vec::new(&env); + members.push_back(creator.clone()); + + let team = Team { + id: team_id, + creator: creator.clone(), + name: name.clone(), + required_skills, + required_roles, + members, + max_members, + closed: false, + }; + + env.storage().persistent().set(&DataKey::Team(team_id), &team); + + creator_dev.team_id = team_id; + env.storage().persistent().set(&DataKey::Developer(creator.clone()), &creator_dev); + + env.events().publish((symbol_short!("team_new"), creator), (team_id, name)); + team_id + } + + /// Retrieve a team + pub fn get_team(env: Env, team_id: u64) -> Option { + env.storage().persistent().get(&DataKey::Team(team_id)) + } + + /// A developer requests to join a team + pub fn request_to_join(env: Env, developer: Address, team_id: u64) { + developer.require_auth(); + + let dev = env + .storage() + .persistent() + .get::<_, Developer>(&DataKey::Developer(developer.clone())) + .ok_or_else(|| panic_with_error!(&env, MatchingError::DeveloperNotFound)) + .unwrap(); + + if dev.team_id != 0 { + panic_with_error!(&env, MatchingError::AlreadyInTeam); + } + + let team = env + .storage() + .persistent() + .get::<_, Team>(&DataKey::Team(team_id)) + .ok_or_else(|| panic_with_error!(&env, MatchingError::TeamNotFound)) + .unwrap(); + + if team.closed { + panic_with_error!(&env, MatchingError::TeamClosed); + } + if team.members.len() >= team.max_members { + panic_with_error!(&env, MatchingError::TeamFull); + } + + let mut requests: Vec
= env + .storage() + .persistent() + .get(&DataKey::JoinRequests(team_id)) + .unwrap_or(Vec::new(&env)); + + let mut exists = false; + for r in requests.iter() { + if r == developer { + exists = true; + break; + } + } + if !exists { + requests.push_back(developer.clone()); + env.storage().persistent().set(&DataKey::JoinRequests(team_id), &requests); + } + + env.events().publish((symbol_short!("join_req"), developer), team_id); + } + + /// Accept a developer's join request (team creator only) + pub fn accept_join_request(env: Env, creator: Address, developer: Address, team_id: u64) { + creator.require_auth(); + + let mut team = env + .storage() + .persistent() + .get::<_, Team>(&DataKey::Team(team_id)) + .ok_or_else(|| panic_with_error!(&env, MatchingError::TeamNotFound)) + .unwrap(); + + if team.creator != creator { + panic_with_error!(&env, MatchingError::Unauthorized); + } + if team.closed { + panic_with_error!(&env, MatchingError::TeamClosed); + } + if team.members.len() >= team.max_members { + panic_with_error!(&env, MatchingError::TeamFull); + } + + let mut requests: Vec
= env + .storage() + .persistent() + .get(&DataKey::JoinRequests(team_id)) + .ok_or_else(|| panic_with_error!(&env, MatchingError::NoActiveRequest)) + .unwrap(); + + let mut found_index = None; + for i in 0..requests.len() { + if requests.get(i).unwrap() == developer { + found_index = Some(i); + break; + } + } + + let idx = found_index + .ok_or_else(|| panic_with_error!(&env, MatchingError::NoActiveRequest)) + .unwrap(); + requests.remove(idx); + env.storage().persistent().set(&DataKey::JoinRequests(team_id), &requests); + + let mut dev = env + .storage() + .persistent() + .get::<_, Developer>(&DataKey::Developer(developer.clone())) + .ok_or_else(|| panic_with_error!(&env, MatchingError::DeveloperNotFound)) + .unwrap(); + + if dev.team_id != 0 { + panic_with_error!(&env, MatchingError::AlreadyInTeam); + } + + dev.team_id = team_id; + team.members.push_back(developer.clone()); + + env.storage().persistent().set(&DataKey::Developer(developer.clone()), &dev); + env.storage().persistent().set(&DataKey::Team(team_id), &team); + + env.events().publish((symbol_short!("join_acc"), creator), (developer, team_id)); + } + + /// Invite a developer to join the team (team creator only) + pub fn invite_developer(env: Env, creator: Address, developer: Address, team_id: u64) { + creator.require_auth(); + + let team = env + .storage() + .persistent() + .get::<_, Team>(&DataKey::Team(team_id)) + .ok_or_else(|| panic_with_error!(&env, MatchingError::TeamNotFound)) + .unwrap(); + + if team.creator != creator { + panic_with_error!(&env, MatchingError::Unauthorized); + } + if team.closed { + panic_with_error!(&env, MatchingError::TeamClosed); + } + if team.members.len() >= team.max_members { + panic_with_error!(&env, MatchingError::TeamFull); + } + + let dev = env + .storage() + .persistent() + .get::<_, Developer>(&DataKey::Developer(developer.clone())) + .ok_or_else(|| panic_with_error!(&env, MatchingError::DeveloperNotFound)) + .unwrap(); + + if dev.team_id != 0 { + panic_with_error!(&env, MatchingError::AlreadyInTeam); + } + + let mut invites: Vec = env + .storage() + .persistent() + .get(&DataKey::Invitations(developer.clone())) + .unwrap_or(Vec::new(&env)); + + let mut exists = false; + for id in invites.iter() { + if id == team_id { + exists = true; + break; + } + } + if !exists { + invites.push_back(team_id); + env.storage().persistent().set(&DataKey::Invitations(developer.clone()), &invites); + } + + env.events().publish((symbol_short!("invite_d"), creator), (developer, team_id)); + } + + /// Accept a team's invitation (developer only) + pub fn accept_invitation(env: Env, developer: Address, team_id: u64) { + developer.require_auth(); + + let mut dev = env + .storage() + .persistent() + .get::<_, Developer>(&DataKey::Developer(developer.clone())) + .ok_or_else(|| panic_with_error!(&env, MatchingError::DeveloperNotFound)) + .unwrap(); + + if dev.team_id != 0 { + panic_with_error!(&env, MatchingError::AlreadyInTeam); + } + + let mut team = env + .storage() + .persistent() + .get::<_, Team>(&DataKey::Team(team_id)) + .ok_or_else(|| panic_with_error!(&env, MatchingError::TeamNotFound)) + .unwrap(); + + if team.closed { + panic_with_error!(&env, MatchingError::TeamClosed); + } + if team.members.len() >= team.max_members { + panic_with_error!(&env, MatchingError::TeamFull); + } + + let mut invites: Vec = env + .storage() + .persistent() + .get(&DataKey::Invitations(developer.clone())) + .ok_or_else(|| panic_with_error!(&env, MatchingError::NoActiveInvitation)) + .unwrap(); + + let mut found_index = None; + for i in 0..invites.len() { + if invites.get(i).unwrap() == team_id { + found_index = Some(i); + break; + } + } + + let idx = found_index + .ok_or_else(|| panic_with_error!(&env, MatchingError::NoActiveInvitation)) + .unwrap(); + invites.remove(idx); + env.storage().persistent().set(&DataKey::Invitations(developer.clone()), &invites); + + dev.team_id = team_id; + team.members.push_back(developer.clone()); + + env.storage().persistent().set(&DataKey::Developer(developer.clone()), &dev); + env.storage().persistent().set(&DataKey::Team(team_id), &team); + + env.events().publish((symbol_short!("invite_a"), developer), team_id); + } + + /// Leave the current team (non-creator member only) + pub fn leave_team(env: Env, developer: Address, team_id: u64) { + developer.require_auth(); + + let mut dev = env + .storage() + .persistent() + .get::<_, Developer>(&DataKey::Developer(developer.clone())) + .ok_or_else(|| panic_with_error!(&env, MatchingError::DeveloperNotFound)) + .unwrap(); + + if dev.team_id != team_id { + panic_with_error!(&env, MatchingError::Unauthorized); + } + + let mut team = env + .storage() + .persistent() + .get::<_, Team>(&DataKey::Team(team_id)) + .ok_or_else(|| panic_with_error!(&env, MatchingError::TeamNotFound)) + .unwrap(); + + if team.closed { + panic_with_error!(&env, MatchingError::TeamClosed); + } + if team.creator == developer { + panic_with_error!(&env, MatchingError::Unauthorized); + } + + let mut found_index = None; + for i in 0..team.members.len() { + if team.members.get(i).unwrap() == developer { + found_index = Some(i); + break; + } + } + + let idx = found_index + .ok_or_else(|| panic_with_error!(&env, MatchingError::DeveloperNotFound)) + .unwrap(); + team.members.remove(idx); + dev.team_id = 0; + + env.storage().persistent().set(&DataKey::Developer(developer.clone()), &dev); + env.storage().persistent().set(&DataKey::Team(team_id), &team); + + env.events().publish((symbol_short!("team_lv"), developer), team_id); + } + + /// Remove a member from the team (team creator only) + pub fn remove_member(env: Env, creator: Address, developer: Address, team_id: u64) { + creator.require_auth(); + + let mut team = env + .storage() + .persistent() + .get::<_, Team>(&DataKey::Team(team_id)) + .ok_or_else(|| panic_with_error!(&env, MatchingError::TeamNotFound)) + .unwrap(); + + if team.creator != creator { + panic_with_error!(&env, MatchingError::Unauthorized); + } + if team.closed { + panic_with_error!(&env, MatchingError::TeamClosed); + } + if developer == creator { + panic_with_error!(&env, MatchingError::Unauthorized); + } + + let mut found_index = None; + for i in 0..team.members.len() { + if team.members.get(i).unwrap() == developer { + found_index = Some(i); + break; + } + } + + let idx = found_index + .ok_or_else(|| panic_with_error!(&env, MatchingError::DeveloperNotFound)) + .unwrap(); + team.members.remove(idx); + + let mut dev = env + .storage() + .persistent() + .get::<_, Developer>(&DataKey::Developer(developer.clone())) + .ok_or_else(|| panic_with_error!(&env, MatchingError::DeveloperNotFound)) + .unwrap(); + + if dev.team_id == team_id { + dev.team_id = 0; + env.storage().persistent().set(&DataKey::Developer(developer.clone()), &dev); + } + + env.storage().persistent().set(&DataKey::Team(team_id), &team); + + env.events().publish((symbol_short!("team_rm"), creator), (developer, team_id)); + } + + /// Close the team, finalizing members (team creator only) + pub fn close_team(env: Env, creator: Address, team_id: u64) { + creator.require_auth(); + + let mut team = env + .storage() + .persistent() + .get::<_, Team>(&DataKey::Team(team_id)) + .ok_or_else(|| panic_with_error!(&env, MatchingError::TeamNotFound)) + .unwrap(); + + if team.creator != creator { + panic_with_error!(&env, MatchingError::Unauthorized); + } + + team.closed = true; + env.storage().persistent().set(&DataKey::Team(team_id), &team); + + env.events().publish((symbol_short!("team_cls"), creator), team_id); + } + + /// Find matching teams for a developer + pub fn find_matching_teams(env: Env, developer: Address) -> Vec { + let dev = match env + .storage() + .persistent() + .get::<_, Developer>(&DataKey::Developer(developer)) + { + Some(d) => d, + None => return Vec::new(&env), + }; + + let next_id: u64 = env.storage().instance().get(&DataKey::NextTeamId).unwrap_or(1); + let mut matched_teams = Vec::new(&env); + + for id in 1..next_id { + if let Some(team) = env.storage().persistent().get::<_, Team>(&DataKey::Team(id)) { + if team.closed || team.members.len() >= team.max_members { + continue; + } + + // Check role match + let mut role_match = false; + for r in team.required_roles.iter() { + if r == dev.preferred_role { + role_match = true; + break; + } + } + + // Check skill match + let mut skill_match = false; + for s in team.required_skills.iter() { + for ds in dev.skills.iter() { + if s == ds { + skill_match = true; + break; + } + } + if skill_match { + break; + } + } + + if role_match || skill_match { + matched_teams.push_back(id); + } + } + } + + matched_teams + } + + /// Find matching developers for a team + pub fn find_matching_developers(env: Env, team_id: u64) -> Vec
{ + let team = match env + .storage() + .persistent() + .get::<_, Team>(&DataKey::Team(team_id)) + { + Some(t) => t, + None => return Vec::new(&env), + }; + + let all_devs: Vec
= env + .storage() + .persistent() + .get(&DataKey::AllDevelopers) + .unwrap_or(Vec::new(&env)); + + let mut matched_devs = Vec::new(&env); + + for dev_addr in all_devs.iter() { + if let Some(dev) = env + .storage() + .persistent() + .get::<_, Developer>(&DataKey::Developer(dev_addr.clone())) + { + if dev.team_id != 0 { + continue; + } + + // Check role match + let mut role_match = false; + for r in team.required_roles.iter() { + if r == dev.preferred_role { + role_match = true; + break; + } + } + + // Check skill match + let mut skill_match = false; + for s in team.required_skills.iter() { + for ds in dev.skills.iter() { + if s == ds { + skill_match = true; + break; + } + } + if skill_match { + break; + } + } + + if role_match || skill_match { + matched_devs.push_back(dev_addr); + } + } + } + + matched_devs + } + + /// Check if a developer's skill is verified via the SkillVerificationContract + pub fn check_skill_verified(env: Env, developer: Address, skill: Symbol) -> bool { + let verifier_opt: Option
= env.storage().instance().get(&DataKey::SkillVerifier); + if let Some(verifier) = verifier_opt { + let client = SkillVerificationClient::new(&env, &verifier); + client.verify_skill(&developer, &skill, &SkillLevel::Beginner) + } else { + false + } + } +} + +#[cfg(test)] +mod test; diff --git a/contracts/hackathon-team-matching/src/test.rs b/contracts/hackathon-team-matching/src/test.rs new file mode 100644 index 00000000..a777ae20 --- /dev/null +++ b/contracts/hackathon-team-matching/src/test.rs @@ -0,0 +1,296 @@ +#![cfg(test)] + +use super::*; +use soroban_sdk::{ + testutils::Address as _, + vec, Address, Env, +}; + +// --- Mock Skill Verifier Contract --- +#[contract] +pub struct MockSkillVerifier; + +#[contractimpl] +impl MockSkillVerifier { + pub fn verify_skill(env: Env, _user: Address, skill: Symbol, _min_level: SkillLevel) -> bool { + // In our mock, "rust" and "stellar" are verified, others are not + skill == Symbol::new(&env, "rust") || skill == Symbol::new(&env, "stellar") + } +} + +// --- Helper function to setup environment --- +fn setup() -> ( + Env, + Address, + Address, + HackathonTeamMatchingClient<'static>, + Address, +) { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + + // Register Mock Skill Verifier + let verifier_id = env.register(MockSkillVerifier, ()); + + // Register Hackathon Team Matching Contract + let contract_id = env.register(HackathonTeamMatching, ()); + let client = HackathonTeamMatchingClient::new(&env, &contract_id); + + client.initialize(&admin, &verifier_id); + + (env, admin, verifier_id, client, contract_id) +} + +#[test] +fn test_initialize_and_registration() { + let (env, _, _, client, _) = setup(); + + let developer = Address::generate(&env); + let skills = vec![&env, Symbol::new(&env, "rust"), Symbol::new(&env, "react")]; + let preferred_role = Symbol::new(&env, "backend"); + + client.register_developer(&developer, &skills, &preferred_role); + + let dev = client.get_developer(&developer).unwrap(); + assert_eq!(dev.address, developer); + assert_eq!(dev.skills, skills); + assert_eq!(dev.preferred_role, preferred_role); + assert_eq!(dev.team_id, 0); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #12)")] +fn test_double_registration_fails() { + let (env, _, _, client, _) = setup(); + + let developer = Address::generate(&env); + let skills = vec![&env, Symbol::new(&env, "rust")]; + let preferred_role = Symbol::new(&env, "backend"); + + client.register_developer(&developer, &skills, &preferred_role); + client.register_developer(&developer, &skills, &preferred_role); +} + +#[test] +fn test_create_team() { + let (env, _, _, client, _) = setup(); + + let creator = Address::generate(&env); + let skills = vec![&env, Symbol::new(&env, "rust")]; + client.register_developer(&creator, &skills, &Symbol::new(&env, "backend")); + + let team_name = Symbol::new(&env, "StellarBuilders"); + let req_skills = vec![&env, Symbol::new(&env, "react"), Symbol::new(&env, "stellar")]; + let req_roles = vec![&env, Symbol::new(&env, "frontend"), Symbol::new(&env, "designer")]; + + let team_id = client.create_team(&creator, &team_name, &req_skills, &req_roles, &5); + assert_eq!(team_id, 1); + + let team = client.get_team(&team_id).unwrap(); + assert_eq!(team.id, 1); + assert_eq!(team.creator, creator); + assert_eq!(team.name, team_name); + assert_eq!(team.required_skills, req_skills); + assert_eq!(team.required_roles, req_roles); + assert_eq!(team.members.len(), 1); + assert_eq!(team.members.get(0).unwrap(), creator); + assert_eq!(team.max_members, 5); + assert!(!team.closed); + + let dev = client.get_developer(&creator).unwrap(); + assert_eq!(dev.team_id, team_id); +} + +#[test] +fn test_join_request_flow() { + let (env, _, _, client, _) = setup(); + + // Create team + let creator = Address::generate(&env); + client.register_developer(&creator, &vec![&env], &Symbol::new(&env, "creator")); + let team_id = client.create_team(&creator, &Symbol::new(&env, "T1"), &vec![&env], &vec![&env], &2); + + // Register applicant + let applicant = Address::generate(&env); + client.register_developer(&applicant, &vec![&env, Symbol::new(&env, "rust")], &Symbol::new(&env, "backend")); + + // Request to join + client.request_to_join(&applicant, &team_id); + + // Accept join request + client.accept_join_request(&creator, &applicant, &team_id); + + let team = client.get_team(&team_id).unwrap(); + assert_eq!(team.members.len(), 2); + assert_eq!(team.members.get(1).unwrap(), applicant); + + let dev = client.get_developer(&applicant).unwrap(); + assert_eq!(dev.team_id, team_id); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #7)")] +fn test_join_request_team_full() { + let (env, _, _, client, _) = setup(); + + // Create team with max_members = 1 (only creator) + let creator = Address::generate(&env); + client.register_developer(&creator, &vec![&env], &Symbol::new(&env, "creator")); + let team_id = client.create_team(&creator, &Symbol::new(&env, "T1"), &vec![&env], &vec![&env], &1); + + let applicant = Address::generate(&env); + client.register_developer(&applicant, &vec![&env], &Symbol::new(&env, "backend")); + + client.request_to_join(&applicant, &team_id); +} + +#[test] +fn test_invitation_flow() { + let (env, _, _, client, _) = setup(); + + // Create team + let creator = Address::generate(&env); + client.register_developer(&creator, &vec![&env], &Symbol::new(&env, "creator")); + let team_id = client.create_team(&creator, &Symbol::new(&env, "T1"), &vec![&env], &vec![&env], &3); + + // Register developer + let developer = Address::generate(&env); + client.register_developer(&developer, &vec![&env], &Symbol::new(&env, "frontend")); + + // Invite developer + client.invite_developer(&creator, &developer, &team_id); + + // Accept invitation + client.accept_invitation(&developer, &team_id); + + let team = client.get_team(&team_id).unwrap(); + assert_eq!(team.members.len(), 2); + assert_eq!(team.members.get(1).unwrap(), developer); + + let dev = client.get_developer(&developer).unwrap(); + assert_eq!(dev.team_id, team_id); +} + +#[test] +fn test_leave_and_remove_member() { + let (env, _, _, client, _) = setup(); + + // Create team + let creator = Address::generate(&env); + client.register_developer(&creator, &vec![&env], &Symbol::new(&env, "creator")); + let team_id = client.create_team(&creator, &Symbol::new(&env, "T1"), &vec![&env], &vec![&env], &4); + + // Developers + let dev1 = Address::generate(&env); + let dev2 = Address::generate(&env); + client.register_developer(&dev1, &vec![&env], &Symbol::new(&env, "role1")); + client.register_developer(&dev2, &vec![&env], &Symbol::new(&env, "role2")); + + // Invite and join dev1, dev2 + client.invite_developer(&creator, &dev1, &team_id); + client.accept_invitation(&dev1, &team_id); + + client.invite_developer(&creator, &dev2, &team_id); + client.accept_invitation(&dev2, &team_id); + + let team = client.get_team(&team_id).unwrap(); + assert_eq!(team.members.len(), 3); + + // Dev1 leaves team + client.leave_team(&dev1, &team_id); + let team = client.get_team(&team_id).unwrap(); + assert_eq!(team.members.len(), 2); + assert_eq!(client.get_developer(&dev1).unwrap().team_id, 0); + + // Creator removes dev2 + client.remove_member(&creator, &dev2, &team_id); + let team = client.get_team(&team_id).unwrap(); + assert_eq!(team.members.len(), 1); + assert_eq!(client.get_developer(&dev2).unwrap().team_id, 0); +} + +#[test] +fn test_matching_engine() { + let (env, _, _, client, _) = setup(); + + // Setup Teams + let creator1 = Address::generate(&env); + client.register_developer(&creator1, &vec![&env], &Symbol::new(&env, "creator")); + let team_id1 = client.create_team( + &creator1, + &Symbol::new(&env, "StellarBuilders"), + &vec![&env, Symbol::new(&env, "rust"), Symbol::new(&env, "stellar")], + &vec![&env, Symbol::new(&env, "frontend")], + &5, + ); + + let creator2 = Address::generate(&env); + client.register_developer(&creator2, &vec![&env], &Symbol::new(&env, "creator")); + let team_id2 = client.create_team( + &creator2, + &Symbol::new(&env, "DeFiYield"), + &vec![&env, Symbol::new(&env, "solidity")], + &vec![&env, Symbol::new(&env, "backend")], + &5, + ); + + // Developer 1: Skills "rust", "react", Role "frontend" + let dev1 = Address::generate(&env); + client.register_developer( + &dev1, + &vec![&env, Symbol::new(&env, "rust"), Symbol::new(&env, "react")], + &Symbol::new(&env, "frontend"), + ); + + // Developer 2: Skills "solidity", Role "backend" + let dev2 = Address::generate(&env); + client.register_developer( + &dev2, + &vec![&env, Symbol::new(&env, "solidity")], + &Symbol::new(&env, "backend"), + ); + + // Developer 3: Skills "go", Role "designer" (No match for any team) + let dev3 = Address::generate(&env); + client.register_developer( + &dev3, + &vec![&env, Symbol::new(&env, "go")], + &Symbol::new(&env, "designer"), + ); + + // Test find_matching_teams for Dev 1 (Should match Team 1 on rust & frontend) + let matching_teams_dev1 = client.find_matching_teams(&dev1); + assert_eq!(matching_teams_dev1.len(), 1); + assert_eq!(matching_teams_dev1.get(0).unwrap(), team_id1); + + // Test find_matching_teams for Dev 2 (Should match Team 2 on solidity & backend) + let matching_teams_dev2 = client.find_matching_teams(&dev2); + assert_eq!(matching_teams_dev2.len(), 1); + assert_eq!(matching_teams_dev2.get(0).unwrap(), team_id2); + + // Test find_matching_teams for Dev 3 (Should match none) + let matching_teams_dev3 = client.find_matching_teams(&dev3); + assert_eq!(matching_teams_dev3.len(), 0); + + // Test find_matching_developers for Team 1 (Should match dev1) + let matching_devs_team1 = client.find_matching_developers(&team_id1); + assert_eq!(matching_devs_team1.len(), 1); + assert_eq!(matching_devs_team1.get(0).unwrap(), dev1); +} + +#[test] +fn test_check_skill_verified() { + let (env, _, _, client, _) = setup(); + + let dev = Address::generate(&env); + client.register_developer(&dev, &vec![&env], &Symbol::new(&env, "frontend")); + + // In MockSkillVerifier, "rust" and "stellar" are verified + assert!(client.check_skill_verified(&dev, &Symbol::new(&env, "rust"))); + assert!(client.check_skill_verified(&dev, &Symbol::new(&env, "stellar"))); + + // "solidity" is not verified in MockSkillVerifier + assert!(!client.check_skill_verified(&dev, &Symbol::new(&env, "solidity"))); +}