diff --git a/src/budgetchain/Budget.cairo b/src/budgetchain/Budget.cairo index 1f1cd67..13b53e9 100644 --- a/src/budgetchain/Budget.cairo +++ b/src/budgetchain/Budget.cairo @@ -83,6 +83,7 @@ pub mod Budget { #[flat] SRC5Event: SRC5Component::Event, FundsRequested: FundsRequested, + OrganizationRemoved: OrganizationRemoved, FundsReturned: FundsReturned, } @@ -156,6 +157,11 @@ pub mod Budget { pub milestone_id: u64, } + #[derive(Drop, starknet::Event)] + pub struct OrganizationRemoved { + pub org_id: u256, + } + #[constructor] fn constructor(ref self: ContractState, default_admin: ContractAddress) { assert(default_admin != contract_address_const::<0>(), ERROR_ZERO_ADDRESS); @@ -735,7 +741,16 @@ pub mod Budget { fn is_paused(self: @ContractState) -> bool { self.is_paused.read() } + fn remove_organization(ref self: ContractState, org_id: u256) { + let caller = get_caller_address(); + assert(caller == self.admin.read(), ERROR_ONLY_ADMIN); + + let mut org = self.organizations.read(org_id); + org.is_active = false; + self.organizations.write(org_id, org); + self.emit(OrganizationRemoved { org_id: org_id }); + } fn request_funds( ref self: ContractState, requester: ContractAddress, diff --git a/src/interfaces/IBudget.cairo b/src/interfaces/IBudget.cairo index 7e5314c..f4da138 100644 --- a/src/interfaces/IBudget.cairo +++ b/src/interfaces/IBudget.cairo @@ -42,6 +42,7 @@ pub trait IBudget { ) -> u256; fn get_organization(self: @TContractState, org_id: u256) -> Organization; fn is_authorized_organization(self: @TContractState, org: ContractAddress) -> bool; + fn remove_organization(ref self: TContractState, org_id: u256); // Fund Request Management fn get_fund_request(self: @TContractState, project_id: u64, request_id: u64) -> FundRequest; diff --git a/tests/test_budgetchain.cairo b/tests/test_budgetchain.cairo index aa1ece9..a60fbb0 100644 --- a/tests/test_budgetchain.cairo +++ b/tests/test_budgetchain.cairo @@ -1440,3 +1440,90 @@ fn test_project_transaction_count_and_storage() { assert(txs.get(1).unwrap().id == 1_u64, 'Second tx id should be 1'); assert(txs.get(2).unwrap().id == 2_u64, 'Third tx id should be 2'); } + +#[test] +fn test_remove_organization_success() { + let (contract_address, admin_address) = setup(); + let dispatcher = IBudgetDispatcher { contract_address }; + + // Create an organization first + let org_name = 'Test Org'; + let org_address = contract_address_const::<'Organization'>(); + let org_mission = 'Testing Budget Chain'; + + // Set admin as caller to create organization + cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); + let org_id = dispatcher.create_organization(org_name, org_address, org_mission); + stop_cheat_caller_address(admin_address); + + // Verify organization is active before removal + let org_before = dispatcher.get_organization(org_id); + assert(org_before.is_active == true, 'be active before removal'); + + // Remove organization as admin + cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); + dispatcher.remove_organization(org_id); + stop_cheat_caller_address(admin_address); + + // Verify organization is inactive after removal + let org_after = dispatcher.get_organization(org_id); + assert(org_after.is_active == false, 'be inactive after removal'); +} + +#[test] +#[should_panic(expected: 'ONLY ADMIN')] +fn test_remove_organization_not_admin() { + let (contract_address, admin_address) = setup(); + let dispatcher = IBudgetDispatcher { contract_address }; + + // Create an organization first + let org_name = 'Test Org'; + let org_address = contract_address_const::<'Organization'>(); + let org_mission = 'Testing Budget Chain'; + + // Set admin as caller to create organization + cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); + let org_id = dispatcher.create_organization(org_name, org_address, org_mission); + stop_cheat_caller_address(admin_address); + + // Try to remove organization as non-admin + let non_admin = contract_address_const::<'non_admin'>(); + cheat_caller_address(contract_address, non_admin, CheatSpan::Indefinite); + dispatcher.remove_organization(org_id); + stop_cheat_caller_address(non_admin); +} + +#[test] +fn test_remove_organization_event_emission() { + let (contract_address, admin_address) = setup(); + let dispatcher = IBudgetDispatcher { contract_address }; + let mut spy = spy_events(); + + // Create an organization first + let org_name = 'Test Org'; + let org_address = contract_address_const::<'Organization'>(); + let org_mission = 'Testing Budget Chain'; + + // Set admin as caller to create organization + cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); + let org_id = dispatcher.create_organization(org_name, org_address, org_mission); + stop_cheat_caller_address(admin_address); + + // Remove organization as admin + cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); + dispatcher.remove_organization(org_id); + stop_cheat_caller_address(admin_address); + + // Verify event emission + spy + .assert_emitted( + @array![ + ( + contract_address, + Budget::Budget::Event::OrganizationRemoved( + Budget::Budget::OrganizationRemoved { org_id: org_id }, + ), + ), + ], + ); +}