From b5081683388635cbf797de262dbbd33b2e1f58a8 Mon Sep 17 00:00:00 2001 From: pvsaint Date: Sun, 22 Feb 2026 08:45:38 +0100 Subject: [PATCH] Feat: Implement Rewards Integration Tests --- contracts/src/goal.rs | 22 ++- contracts/src/group.rs | 3 + contracts/src/lock.rs | 13 +- ...eak_does_not_award_completion_bonus.1.json | 51 +++++- ...awarded_on_create_if_target_reached.1.json | 51 +++++- ..._awarded_once_on_deposit_transition.1.json | 51 +++++- ...s_not_awarded_below_target_boundary.1.json | 51 +++++- ..._bonus_applies_only_above_threshold.1.json | 51 +++++- ...s_not_applied_at_threshold_boundary.1.json | 51 +++++- ...k_bonus_not_applied_below_threshold.1.json | 51 +++++- ...bonus_not_duplicated_after_withdraw.1.json | 51 +++++- contracts/tests/rewards_integration_test.rs | 151 ++++++++++++++++++ 12 files changed, 556 insertions(+), 41 deletions(-) create mode 100644 contracts/tests/rewards_integration_test.rs diff --git a/contracts/src/goal.rs b/contracts/src/goal.rs index 4a95e452..c95afa8c 100644 --- a/contracts/src/goal.rs +++ b/contracts/src/goal.rs @@ -92,6 +92,9 @@ pub fn create_goal_save( add_goal_to_user(env, &user, goal_id); increment_next_goal_id(env); + // Award deposit points + storage::award_deposit_points(env, user.clone(), initial_deposit)?; + // Extend TTL for new goal save and user data ttl::extend_goal_ttl(env, goal_id); ttl::extend_user_plan_list_ttl(env, &DataKey::UserGoalSaves(user.clone())); @@ -180,6 +183,9 @@ pub fn deposit_to_goal_save( } } + // Award deposit points + storage::award_deposit_points(env, user.clone(), amount)?; + Ok(()) } @@ -982,11 +988,13 @@ mod tests { client.deposit_to_goal_save(&user, &goal_id, &1_000); let rewards_after_completion = client.get_user_rewards(&user); - assert_eq!(rewards_after_completion.total_points, 250); + // Base points: (4000 + 1000) * 10 = 50000 + // Completion bonus: 250 + assert_eq!(rewards_after_completion.total_points, 50250); let _ = client.withdraw_completed_goal_save(&user, &goal_id); let rewards_after_withdraw = client.get_user_rewards(&user); - assert_eq!(rewards_after_withdraw.total_points, 250); + assert_eq!(rewards_after_withdraw.total_points, 50250); } #[test] @@ -1004,7 +1012,8 @@ mod tests { assert!(!goal_save.is_completed); let rewards = client.get_user_rewards(&user); - assert_eq!(rewards.total_points, 0); + // Base points: 4999 * 10 = 49990 + assert_eq!(rewards.total_points, 49990); } #[test] @@ -1022,7 +1031,9 @@ mod tests { assert!(goal.is_completed); let rewards = client.get_user_rewards(&user); - assert_eq!(rewards.total_points, 250); + // Base points: 5000 * 10 = 50000 + // Completion bonus: 250 + assert_eq!(rewards.total_points, 50250); } #[test] @@ -1055,6 +1066,7 @@ mod tests { let _ = client.break_goal_save(&user, &goal_id); let rewards = client.get_user_rewards(&user); - assert_eq!(rewards.total_points, 0); + // Base points: 2000 * 10 = 20000 + assert_eq!(rewards.total_points, 20000); } } diff --git a/contracts/src/group.rs b/contracts/src/group.rs index c8a03d5b..c21766fe 100644 --- a/contracts/src/group.rs +++ b/contracts/src/group.rs @@ -452,6 +452,9 @@ pub fn contribute_to_group_save( env.storage().persistent().set(&plan_key, &plan); } + // Award deposit points + crate::rewards::storage::award_deposit_points(env, user.clone(), amount)?; + // Extend TTL on contribution ttl::extend_group_ttl(env, group_id); ttl::extend_user_ttl(env, &user); diff --git a/contracts/src/lock.rs b/contracts/src/lock.rs index a977a6f7..9a2671b8 100644 --- a/contracts/src/lock.rs +++ b/contracts/src/lock.rs @@ -64,6 +64,7 @@ pub fn create_lock_save( user_data.savings_count += 1; env.storage().persistent().set(&user_key, &user_data); + storage::award_deposit_points(env, user.clone(), amount)?; storage::award_long_lock_bonus(env, user.clone(), amount, duration)?; // Extend TTL for new lock save and user data @@ -288,7 +289,8 @@ mod tests { let rewards = client.get_user_rewards(&user); // base points = 1000 * 10 = 10000, bonus = 20% = 2000 - assert_eq!(rewards.total_points, 2_000); + // base points = 1000 * 10 = 10000, bonus = 20% = 2000 + assert_eq!(rewards.total_points, 12_000); } #[test] @@ -303,7 +305,8 @@ mod tests { client.create_lock_save(&user, &amount, &LONG_LOCK_BONUS_THRESHOLD_SECS); let rewards = client.get_user_rewards(&user); - assert_eq!(rewards.total_points, 0); + // base points = 1000 * 10 = 10000 + assert_eq!(rewards.total_points, 10_000); } #[test] @@ -319,7 +322,8 @@ mod tests { client.create_lock_save(&user, &amount, &below_threshold); let rewards = client.get_user_rewards(&user); - assert_eq!(rewards.total_points, 0); + // base points = 1000 * 10 = 10000 + assert_eq!(rewards.total_points, 10_000); } #[test] @@ -357,6 +361,7 @@ mod tests { let _ = client.withdraw_lock_save(&user, &lock_id); let rewards = client.get_user_rewards(&user); - assert_eq!(rewards.total_points, 2_000); + // base points = 1000 * 10 = 10000, bonus = 2000 + assert_eq!(rewards.total_points, 12_000); } } diff --git a/contracts/test_snapshots/goal/tests/test_goal_break_does_not_award_completion_bonus.1.json b/contracts/test_snapshots/goal/tests/test_goal_break_does_not_award_completion_bonus.1.json index 22332977..195fa478 100644 --- a/contracts/test_snapshots/goal/tests/test_goal_break_does_not_award_completion_bonus.1.json +++ b/contracts/test_snapshots/goal/tests/test_goal_break_does_not_award_completion_bonus.1.json @@ -201,6 +201,49 @@ "min_temp_entry_ttl": 16, "max_entry_ttl": 6312000, "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "AllUsers" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "AllUsers" + } + ] + }, + "durability": "persistent", + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], [ { "contract_data": { @@ -546,7 +589,7 @@ "symbol": "current_streak" }, "val": { - "u32": 0 + "u32": 1 } }, { @@ -554,7 +597,7 @@ "symbol": "daily_points_earned" }, "val": { - "u128": "0" + "u128": "20000" } }, { @@ -578,7 +621,7 @@ "symbol": "lifetime_deposited" }, "val": { - "i128": "0" + "i128": "2000" } }, { @@ -586,7 +629,7 @@ "symbol": "total_points" }, "val": { - "u128": "0" + "u128": "20000" } } ] diff --git a/contracts/test_snapshots/goal/tests/test_goal_completion_bonus_awarded_on_create_if_target_reached.1.json b/contracts/test_snapshots/goal/tests/test_goal_completion_bonus_awarded_on_create_if_target_reached.1.json index be099c3c..8b55bd76 100644 --- a/contracts/test_snapshots/goal/tests/test_goal_completion_bonus_awarded_on_create_if_target_reached.1.json +++ b/contracts/test_snapshots/goal/tests/test_goal_completion_bonus_awarded_on_create_if_target_reached.1.json @@ -180,6 +180,49 @@ "min_temp_entry_ttl": 16, "max_entry_ttl": 6312000, "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "AllUsers" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "AllUsers" + } + ] + }, + "durability": "persistent", + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], [ { "contract_data": { @@ -529,7 +572,7 @@ "symbol": "current_streak" }, "val": { - "u32": 0 + "u32": 1 } }, { @@ -537,7 +580,7 @@ "symbol": "daily_points_earned" }, "val": { - "u128": "0" + "u128": "50000" } }, { @@ -561,7 +604,7 @@ "symbol": "lifetime_deposited" }, "val": { - "i128": "0" + "i128": "5000" } }, { @@ -569,7 +612,7 @@ "symbol": "total_points" }, "val": { - "u128": "250" + "u128": "50250" } } ] diff --git a/contracts/test_snapshots/goal/tests/test_goal_completion_bonus_awarded_once_on_deposit_transition.1.json b/contracts/test_snapshots/goal/tests/test_goal_completion_bonus_awarded_once_on_deposit_transition.1.json index d3d86c4f..cfa6279d 100644 --- a/contracts/test_snapshots/goal/tests/test_goal_completion_bonus_awarded_once_on_deposit_transition.1.json +++ b/contracts/test_snapshots/goal/tests/test_goal_completion_bonus_awarded_once_on_deposit_transition.1.json @@ -227,6 +227,49 @@ "min_temp_entry_ttl": 16, "max_entry_ttl": 6312000, "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "AllUsers" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "AllUsers" + } + ] + }, + "durability": "persistent", + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], [ { "contract_data": { @@ -576,7 +619,7 @@ "symbol": "current_streak" }, "val": { - "u32": 0 + "u32": 2 } }, { @@ -584,7 +627,7 @@ "symbol": "daily_points_earned" }, "val": { - "u128": "0" + "u128": "50000" } }, { @@ -608,7 +651,7 @@ "symbol": "lifetime_deposited" }, "val": { - "i128": "0" + "i128": "5000" } }, { @@ -616,7 +659,7 @@ "symbol": "total_points" }, "val": { - "u128": "250" + "u128": "50250" } } ] diff --git a/contracts/test_snapshots/goal/tests/test_goal_completion_bonus_not_awarded_below_target_boundary.1.json b/contracts/test_snapshots/goal/tests/test_goal_completion_bonus_not_awarded_below_target_boundary.1.json index 75cb3296..6095e596 100644 --- a/contracts/test_snapshots/goal/tests/test_goal_completion_bonus_not_awarded_below_target_boundary.1.json +++ b/contracts/test_snapshots/goal/tests/test_goal_completion_bonus_not_awarded_below_target_boundary.1.json @@ -180,6 +180,49 @@ "min_temp_entry_ttl": 16, "max_entry_ttl": 6312000, "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "AllUsers" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "AllUsers" + } + ] + }, + "durability": "persistent", + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], [ { "contract_data": { @@ -529,7 +572,7 @@ "symbol": "current_streak" }, "val": { - "u32": 0 + "u32": 1 } }, { @@ -537,7 +580,7 @@ "symbol": "daily_points_earned" }, "val": { - "u128": "0" + "u128": "49990" } }, { @@ -561,7 +604,7 @@ "symbol": "lifetime_deposited" }, "val": { - "i128": "0" + "i128": "4999" } }, { @@ -569,7 +612,7 @@ "symbol": "total_points" }, "val": { - "u128": "0" + "u128": "49990" } } ] diff --git a/contracts/test_snapshots/lock/tests/test_long_lock_bonus_applies_only_above_threshold.1.json b/contracts/test_snapshots/lock/tests/test_long_lock_bonus_applies_only_above_threshold.1.json index 6c444fbb..c1453abd 100644 --- a/contracts/test_snapshots/lock/tests/test_long_lock_bonus_applies_only_above_threshold.1.json +++ b/contracts/test_snapshots/lock/tests/test_long_lock_bonus_applies_only_above_threshold.1.json @@ -176,6 +176,49 @@ "min_temp_entry_ttl": 16, "max_entry_ttl": 6312000, "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "AllUsers" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "AllUsers" + } + ] + }, + "durability": "persistent", + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], [ { "contract_data": { @@ -460,7 +503,7 @@ "symbol": "current_streak" }, "val": { - "u32": 0 + "u32": 1 } }, { @@ -468,7 +511,7 @@ "symbol": "daily_points_earned" }, "val": { - "u128": "0" + "u128": "10000" } }, { @@ -492,7 +535,7 @@ "symbol": "lifetime_deposited" }, "val": { - "i128": "0" + "i128": "1000" } }, { @@ -500,7 +543,7 @@ "symbol": "total_points" }, "val": { - "u128": "2000" + "u128": "12000" } } ] diff --git a/contracts/test_snapshots/lock/tests/test_long_lock_bonus_not_applied_at_threshold_boundary.1.json b/contracts/test_snapshots/lock/tests/test_long_lock_bonus_not_applied_at_threshold_boundary.1.json index b1afe16e..f6962389 100644 --- a/contracts/test_snapshots/lock/tests/test_long_lock_bonus_not_applied_at_threshold_boundary.1.json +++ b/contracts/test_snapshots/lock/tests/test_long_lock_bonus_not_applied_at_threshold_boundary.1.json @@ -176,6 +176,49 @@ "min_temp_entry_ttl": 16, "max_entry_ttl": 6312000, "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "AllUsers" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "AllUsers" + } + ] + }, + "durability": "persistent", + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], [ { "contract_data": { @@ -460,7 +503,7 @@ "symbol": "current_streak" }, "val": { - "u32": 0 + "u32": 1 } }, { @@ -468,7 +511,7 @@ "symbol": "daily_points_earned" }, "val": { - "u128": "0" + "u128": "10000" } }, { @@ -492,7 +535,7 @@ "symbol": "lifetime_deposited" }, "val": { - "i128": "0" + "i128": "1000" } }, { @@ -500,7 +543,7 @@ "symbol": "total_points" }, "val": { - "u128": "0" + "u128": "10000" } } ] diff --git a/contracts/test_snapshots/lock/tests/test_long_lock_bonus_not_applied_below_threshold.1.json b/contracts/test_snapshots/lock/tests/test_long_lock_bonus_not_applied_below_threshold.1.json index 73c9a17e..3ca211c6 100644 --- a/contracts/test_snapshots/lock/tests/test_long_lock_bonus_not_applied_below_threshold.1.json +++ b/contracts/test_snapshots/lock/tests/test_long_lock_bonus_not_applied_below_threshold.1.json @@ -176,6 +176,49 @@ "min_temp_entry_ttl": 16, "max_entry_ttl": 6312000, "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "AllUsers" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "AllUsers" + } + ] + }, + "durability": "persistent", + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], [ { "contract_data": { @@ -460,7 +503,7 @@ "symbol": "current_streak" }, "val": { - "u32": 0 + "u32": 1 } }, { @@ -468,7 +511,7 @@ "symbol": "daily_points_earned" }, "val": { - "u128": "0" + "u128": "10000" } }, { @@ -492,7 +535,7 @@ "symbol": "lifetime_deposited" }, "val": { - "i128": "0" + "i128": "1000" } }, { @@ -500,7 +543,7 @@ "symbol": "total_points" }, "val": { - "u128": "0" + "u128": "10000" } } ] diff --git a/contracts/test_snapshots/lock/tests/test_long_lock_bonus_not_duplicated_after_withdraw.1.json b/contracts/test_snapshots/lock/tests/test_long_lock_bonus_not_duplicated_after_withdraw.1.json index de4e6086..49615a4c 100644 --- a/contracts/test_snapshots/lock/tests/test_long_lock_bonus_not_duplicated_after_withdraw.1.json +++ b/contracts/test_snapshots/lock/tests/test_long_lock_bonus_not_duplicated_after_withdraw.1.json @@ -198,6 +198,49 @@ "min_temp_entry_ttl": 16, "max_entry_ttl": 6312000, "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "AllUsers" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "AllUsers" + } + ] + }, + "durability": "persistent", + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], [ { "contract_data": { @@ -482,7 +525,7 @@ "symbol": "current_streak" }, "val": { - "u32": 0 + "u32": 1 } }, { @@ -490,7 +533,7 @@ "symbol": "daily_points_earned" }, "val": { - "u128": "0" + "u128": "10000" } }, { @@ -514,7 +557,7 @@ "symbol": "lifetime_deposited" }, "val": { - "i128": "0" + "i128": "1000" } }, { @@ -522,7 +565,7 @@ "symbol": "total_points" }, "val": { - "u128": "2000" + "u128": "12000" } } ] diff --git a/contracts/tests/rewards_integration_test.rs b/contracts/tests/rewards_integration_test.rs new file mode 100644 index 00000000..ad2547c8 --- /dev/null +++ b/contracts/tests/rewards_integration_test.rs @@ -0,0 +1,151 @@ +#![cfg(test)] + +use soroban_sdk::{ + testutils::{Address as _, Ledger}, + Address, BytesN, Env, Symbol, +}; +use Nestera::{NesteraContract, NesteraContractClient}; + +#[test] +fn test_rewards_full_lifecycle() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(NesteraContract, ()); + let client = NesteraContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let admin_pk = BytesN::from_array(&env, &[0u8; 32]); + client.initialize(&admin, &admin_pk); + + let user = Address::generate(&env); + client.init_user(&user); + + // 1. Initialize config + client.init_rewards_config( + &admin, &10, // points_per_token + &2000, // streak_bonus_bps (20%) + &1000, // long_lock_bonus_bps (10%) + &1000, // goal_completion_bonus (1000 points) + &true, // enabled + &10, // min_deposit_for_rewards (10 tokens) + &60, // action_cooldown_seconds (1 minute) + &100_000, // max_daily_points + &5000, // max_streak_multiplier (50%) + ); + + // 2. Deposit multiple times to build streak + // Streak starts at 1 + client.deposit_flexi(&user, &100); + let rewards = client.get_user_rewards(&user); + assert_eq!(rewards.current_streak, 1); + assert_eq!(rewards.total_points, 1000); // 100 * 10 + + // Advance time beyond cooldown but within streak window (7 days) + env.ledger().with_mut(|li| li.timestamp += 70); + + // Streak 2 + client.deposit_flexi(&user, &100); + let rewards = client.get_user_rewards(&user); + assert_eq!(rewards.current_streak, 2); + assert_eq!(rewards.total_points, 2000); // 1000 + 1000 + + // Advance time (1 day) + env.ledger().with_mut(|li| li.timestamp += 86400); + + // 3. Validate streak bonus (threshold is 3) + client.deposit_flexi(&user, &100); + let rewards = client.get_user_rewards(&user); + assert_eq!(rewards.current_streak, 3); + // Base: 1000, Bonus: 20% of 1000 = 200. Total added: 1200. + // Cumulative: 2000 + 1200 = 3200 + assert_eq!(rewards.total_points, 3200); + + // 4. Complete goal + let goal_name = Symbol::new(&env, "TestGoal"); + let target_amount = 500i128; + let initial_deposit = 100i128; + + env.ledger().with_mut(|li| li.timestamp += 70); + let goal_id = client.create_goal_save(&user, &goal_name, &target_amount, &initial_deposit); + + // Points for creation deposit (100 * 10 = 1000 + 20% streak bonus = 1200) + // Cumulative: 3200 + 1200 = 4400 + let rewards = client.get_user_rewards(&user); + assert_eq!(rewards.total_points, 4400); + + // Deposit to complete goal + env.ledger().with_mut(|li| li.timestamp += 70); + client.deposit_to_goal_save(&user, &goal_id, &400); + + // Points for 400 deposit (400 * 10 = 4000 + 20% streak bonus = 800) = 4800 + // Plus Goal Completion Bonus = 1000 + // Cumulative: 4400 + 4800 + 1000 = 10200 + let rewards = client.get_user_rewards(&user); + assert_eq!(rewards.total_points, 10200); + + // 5. Redeem points + client.redeem_points(&user, &2000); + let rewards = client.get_user_rewards(&user); + assert_eq!(rewards.total_points, 8200); + + // 7. Attempt abuse scenarios + + // - Zero deposit (should not award points) + client.deposit_flexi(&user, &10); // 100 base + 20 bonus = 120 + let rewards_before = client.get_user_rewards(&user).total_points; + let _ = client.try_deposit_flexi(&user, &0); + let rewards_after = client.get_user_rewards(&user).total_points; + assert_eq!( + rewards_before, rewards_after, + "Zero deposit should not award points" + ); + + // - Rapid-fire deposits (should skip points due to cooldown) + env.ledger().with_mut(|li| li.timestamp += 70); + client.deposit_flexi(&user, &100); // 1000 + 200 = 1200 + let rewards_before = client.get_user_rewards(&user).total_points; + client.deposit_flexi(&user, &100); // Should be COOLING DOWN + let rewards_after = client.get_user_rewards(&user).total_points; + assert_eq!( + rewards_before, rewards_after, + "Points should not be awarded during cooldown" + ); + + // 8. Pause logic + client.pause(&admin); + let result = client.try_deposit_flexi(&user, &100); + assert!(result.is_err(), "Deposit should fail when paused"); +} + +#[test] +fn test_rewards_invariants_and_abuse() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(NesteraContract, ()); + let client = NesteraContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let admin_pk = BytesN::from_array(&env, &[1u8; 32]); + client.initialize(&admin, &admin_pk); + + let user = Address::generate(&env); + client.init_user(&user); + + client.init_rewards_config(&admin, &10, &0, &0, &0, &true, &0, &0, &1000, &10000); + + // Over-redemption + client.deposit_flexi(&user, &50); // 500 points + let result = client.try_redeem_points(&user, &1000); + assert!(result.is_err(), "Redeem more than balance should fail"); + + // Non-admin config update + let prank_user = Address::generate(&env); + let bad_config = client.get_rewards_config(); + let result = client.try_update_rewards_config(&prank_user, &bad_config); + assert!( + result.is_err(), + "Non-admin should not be able to update config" + ); +}