From 3a6fc74dfe3f8c9eb2fdf5fa0f675c648a7025ee Mon Sep 17 00:00:00 2001 From: dinahmaccodes Date: Thu, 29 Jan 2026 12:05:41 +0100 Subject: [PATCH] feat: implement the reject and refund features --- .../payment-vault-contract/src/contract.rs | 37 ++++ .../payment-vault-contract/src/events.rs | 6 + contracts/payment-vault-contract/src/lib.rs | 10 + contracts/payment-vault-contract/src/test.rs | 178 ++++++++++++++++++ contracts/payment-vault-contract/src/types.rs | 3 +- 5 files changed, 233 insertions(+), 1 deletion(-) diff --git a/contracts/payment-vault-contract/src/contract.rs b/contracts/payment-vault-contract/src/contract.rs index e43e6c1..0673145 100644 --- a/contracts/payment-vault-contract/src/contract.rs +++ b/contracts/payment-vault-contract/src/contract.rs @@ -174,5 +174,42 @@ pub fn reclaim_stale_session( // 8. Emit event events::session_reclaimed(env, booking_id, booking.total_deposit); + Ok(()) +} + +pub fn reject_session( + env: &Env, + expert: &Address, + booking_id: u64, +) -> Result<(), VaultError> { + // 1. Require expert authorization + expert.require_auth(); + + // 2. Get booking and verify it exists + let booking = storage::get_booking(env, booking_id) + .ok_or(VaultError::BookingNotFound)?; + + // 3. Verify the caller is the expert in the booking + if booking.expert != *expert { + return Err(VaultError::NotAuthorized); + } + + // 4. Verify booking is in Pending status + if booking.status != BookingStatus::Pending { + return Err(VaultError::BookingNotPending); + } + + // 5. Transfer total_deposit back to user + let token_address = storage::get_token(env); + let token_client = token::Client::new(env, &token_address); + let contract_address = env.current_contract_address(); + token_client.transfer(&contract_address, &booking.user, &booking.total_deposit); + + // 6. Update booking status to Rejected + storage::update_booking_status(env, booking_id, BookingStatus::Rejected); + + // 7. Emit event + events::session_rejected(env, booking_id, "Expert declined session"); + Ok(()) } \ No newline at end of file diff --git a/contracts/payment-vault-contract/src/events.rs b/contracts/payment-vault-contract/src/events.rs index db2a857..2ff8dfe 100644 --- a/contracts/payment-vault-contract/src/events.rs +++ b/contracts/payment-vault-contract/src/events.rs @@ -16,3 +16,9 @@ pub fn session_reclaimed(env: &Env, booking_id: u64, amount: i128) { let topics = (symbol_short!("reclaim"), booking_id); env.events().publish(topics, amount); } + +/// Emitted when an expert rejects a pending session +pub fn session_rejected(env: &Env, booking_id: u64, reason: &str) { + let topics = (symbol_short!("reject"), booking_id); + env.events().publish(topics, reason); +} diff --git a/contracts/payment-vault-contract/src/lib.rs b/contracts/payment-vault-contract/src/lib.rs index 67220ec..8fa71f6 100644 --- a/contracts/payment-vault-contract/src/lib.rs +++ b/contracts/payment-vault-contract/src/lib.rs @@ -59,6 +59,16 @@ impl PaymentVaultContract { contract::reclaim_stale_session(&env, &user, booking_id) } + /// Reject a pending session (Expert-only) + /// Experts can reject a pending booking, instantly refunding the user + pub fn reject_session( + env: Env, + expert: Address, + booking_id: u64, + ) -> Result<(), VaultError> { + contract::reject_session(&env, &expert, booking_id) + } + /// Get all booking IDs for a specific user pub fn get_user_bookings(env: Env, user: Address) -> Vec { storage::get_user_bookings(&env, &user) diff --git a/contracts/payment-vault-contract/src/test.rs b/contracts/payment-vault-contract/src/test.rs index 054366d..15a0c6f 100644 --- a/contracts/payment-vault-contract/src/test.rs +++ b/contracts/payment-vault-contract/src/test.rs @@ -461,3 +461,181 @@ fn test_reclaim_already_finalized() { let result = client.try_reclaim_stale_session(&user, &booking_id); assert!(result.is_err()); } + +#[test] +fn test_expert_rejects_pending_session() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let expert = Address::generate(&env); + let oracle = Address::generate(&env); + + let token_admin = Address::generate(&env); + let token = create_token_contract(&env, &token_admin); + token.mint(&user, &10_000); + + let client = create_client(&env); + client.init(&admin, &token.address, &oracle); + + // Create booking + let rate_per_second = 10_i128; + let max_duration = 100_u64; + let booking_id = client.book_session(&user, &expert, &rate_per_second, &max_duration); + + // Verify initial state + assert_eq!(token.balance(&user), 9_000); + assert_eq!(token.balance(&client.address), 1_000); + + // Expert rejects the session + let result = client.try_reject_session(&expert, &booking_id); + assert!(result.is_ok()); + + // Verify user balance increased (full refund) + assert_eq!(token.balance(&user), 10_000); + assert_eq!(token.balance(&client.address), 0); + assert_eq!(token.balance(&expert), 0); + + // Verify booking status is Rejected + let booking = client.get_booking(&booking_id).unwrap(); + use crate::types::BookingStatus; + assert_eq!(booking.status, BookingStatus::Rejected); +} + +#[test] +fn test_user_cannot_reject_session() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let expert = Address::generate(&env); + let oracle = Address::generate(&env); + + let token_admin = Address::generate(&env); + let token = create_token_contract(&env, &token_admin); + token.mint(&user, &10_000); + + let client = create_client(&env); + client.init(&admin, &token.address, &oracle); + + let rate_per_second = 10_i128; + let max_duration = 100_u64; + let booking_id = client.book_session(&user, &expert, &rate_per_second, &max_duration); + + // User tries to reject their own session (should fail - not authorized) + let result = client.try_reject_session(&user, &booking_id); + assert!(result.is_err()); + + // Verify funds still in contract + assert_eq!(token.balance(&client.address), 1_000); +} + +#[test] +fn test_reject_already_complete_session() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let expert = Address::generate(&env); + let oracle = Address::generate(&env); + + let token_admin = Address::generate(&env); + let token = create_token_contract(&env, &token_admin); + token.mint(&user, &10_000); + + let client = create_client(&env); + client.init(&admin, &token.address, &oracle); + + let rate_per_second = 10_i128; + let max_duration = 100_u64; + let booking_id = client.book_session(&user, &expert, &rate_per_second, &max_duration); + + // Oracle finalizes the session + client.finalize_session(&booking_id, &50); + + // Expert tries to reject after completion (should fail - not pending) + let result = client.try_reject_session(&expert, &booking_id); + assert!(result.is_err()); +} + +#[test] +fn test_reject_already_reclaimed_session() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let expert = Address::generate(&env); + let oracle = Address::generate(&env); + + let token_admin = Address::generate(&env); + let token = create_token_contract(&env, &token_admin); + token.mint(&user, &10_000); + + let client = create_client(&env); + client.init(&admin, &token.address, &oracle); + + let rate_per_second = 10_i128; + let max_duration = 100_u64; + let booking_id = client.book_session(&user, &expert, &rate_per_second, &max_duration); + + // Advance time and user reclaims + env.ledger().set_timestamp(env.ledger().timestamp() + 90_000); + client.reclaim_stale_session(&user, &booking_id); + + // Expert tries to reject after reclamation (should fail - not pending) + let result = client.try_reject_session(&expert, &booking_id); + assert!(result.is_err()); +} + +#[test] +fn test_wrong_expert_cannot_reject() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let expert = Address::generate(&env); + let wrong_expert = Address::generate(&env); + let oracle = Address::generate(&env); + + let token_admin = Address::generate(&env); + let token = create_token_contract(&env, &token_admin); + token.mint(&user, &10_000); + + let client = create_client(&env); + client.init(&admin, &token.address, &oracle); + + let rate_per_second = 10_i128; + let max_duration = 100_u64; + let booking_id = client.book_session(&user, &expert, &rate_per_second, &max_duration); + + // Different expert tries to reject (should fail - not authorized) + let result = client.try_reject_session(&wrong_expert, &booking_id); + assert!(result.is_err()); + + // Verify funds still in contract + assert_eq!(token.balance(&client.address), 1_000); +} + +#[test] +fn test_reject_nonexistent_booking() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let expert = Address::generate(&env); + let token = Address::generate(&env); + let oracle = Address::generate(&env); + + let client = create_client(&env); + client.init(&admin, &token, &oracle); + + // Expert tries to reject non-existent booking (should fail - not found) + let result = client.try_reject_session(&expert, &999); + assert!(result.is_err()); +} + diff --git a/contracts/payment-vault-contract/src/types.rs b/contracts/payment-vault-contract/src/types.rs index 6d985ad..03a3c2c 100644 --- a/contracts/payment-vault-contract/src/types.rs +++ b/contracts/payment-vault-contract/src/types.rs @@ -7,7 +7,8 @@ use soroban_sdk::{contracttype, Address}; pub enum BookingStatus { Pending = 0, Complete = 1, - Reclaimed = 2, + Rejected = 2, + Reclaimed = 3, } /// Record of a consultation booking with deposit locked