From f7a70415155d9e5eb97b4ccb474830e832af51b7 Mon Sep 17 00:00:00 2001 From: Userhorlie <99200015+Userhorlie@users.noreply.github.com> Date: Fri, 29 May 2026 09:00:24 +0000 Subject: [PATCH] feat: add cntr contract helper modules (closes #1018, #1019, #1020, #1021) - subscription_expiry.rs: is_subscription_expired + days_until_expiry - member_tier.rs: MemberTier enum + calculate_tier - escrow_release.rs: validate_escrow_release with dispute window check - escrow_refund.rs: calculate_refund_amount with cancellation policy - 26 unit tests, all passing --- contracts/Cargo.toml | 1 + contracts/cntr/Cargo.toml | 4 ++ contracts/cntr/src/escrow_refund.rs | 53 +++++++++++++++++++ contracts/cntr/src/escrow_release.rs | 54 +++++++++++++++++++ contracts/cntr/src/lib.rs | 4 ++ contracts/cntr/src/member_tier.rs | 64 +++++++++++++++++++++++ contracts/cntr/src/subscription_expiry.rs | 45 ++++++++++++++++ 7 files changed, 225 insertions(+) create mode 100644 contracts/cntr/Cargo.toml create mode 100644 contracts/cntr/src/escrow_refund.rs create mode 100644 contracts/cntr/src/escrow_release.rs create mode 100644 contracts/cntr/src/lib.rs create mode 100644 contracts/cntr/src/member_tier.rs create mode 100644 contracts/cntr/src/subscription_expiry.rs diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index a6c082c..5cf4daa 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -8,6 +8,7 @@ members = [ "workspace_booking", "payment_escrow", "resource_credits", + "cntr", ] [workspace.dependencies] diff --git a/contracts/cntr/Cargo.toml b/contracts/cntr/Cargo.toml new file mode 100644 index 0000000..40fdfbc --- /dev/null +++ b/contracts/cntr/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "cntr" +version = "0.1.0" +edition = "2021" diff --git a/contracts/cntr/src/escrow_refund.rs b/contracts/cntr/src/escrow_refund.rs new file mode 100644 index 0000000..a95daf3 --- /dev/null +++ b/contracts/cntr/src/escrow_refund.rs @@ -0,0 +1,53 @@ +pub fn calculate_refund_amount( + booking_amount_stroops: i128, + cancellation_hours_before_start: u64, +) -> i128 { + if cancellation_hours_before_start >= 48 { + booking_amount_stroops + } else if cancellation_hours_before_start >= 24 { + booking_amount_stroops / 2 + } else { + 0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn full_refund_at_48h() { + assert_eq!(calculate_refund_amount(1_000_000, 48), 1_000_000); + } + + #[test] + fn full_refund_beyond_48h() { + assert_eq!(calculate_refund_amount(1_000_000, 72), 1_000_000); + } + + #[test] + fn half_refund_at_24h() { + assert_eq!(calculate_refund_amount(1_000_000, 24), 500_000); + } + + #[test] + fn half_refund_at_47h() { + assert_eq!(calculate_refund_amount(1_000_000, 47), 500_000); + } + + #[test] + fn no_refund_at_23h() { + assert_eq!(calculate_refund_amount(1_000_000, 23), 0); + } + + #[test] + fn no_refund_at_0h() { + assert_eq!(calculate_refund_amount(1_000_000, 0), 0); + } + + #[test] + fn integer_division_no_float() { + // odd amount: 1_000_001 / 2 = 500_000 (integer division) + assert_eq!(calculate_refund_amount(1_000_001, 24), 500_000); + } +} diff --git a/contracts/cntr/src/escrow_release.rs b/contracts/cntr/src/escrow_release.rs new file mode 100644 index 0000000..1e4fefc --- /dev/null +++ b/contracts/cntr/src/escrow_release.rs @@ -0,0 +1,54 @@ +pub fn validate_escrow_release( + booking_status: &str, + dispute_window_expired: bool, +) -> Result<(), &'static str> { + if booking_status != "COMPLETED" { + return Err("Booking not completed"); + } + if !dispute_window_expired { + return Err("Dispute window still active"); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ok_when_completed_and_window_expired() { + assert_eq!(validate_escrow_release("COMPLETED", true), Ok(())); + } + + #[test] + fn err_when_not_completed() { + assert_eq!( + validate_escrow_release("PENDING", true), + Err("Booking not completed") + ); + } + + #[test] + fn err_when_dispute_window_active() { + assert_eq!( + validate_escrow_release("COMPLETED", false), + Err("Dispute window still active") + ); + } + + #[test] + fn err_when_both_conditions_fail_returns_not_completed() { + assert_eq!( + validate_escrow_release("PENDING", false), + Err("Booking not completed") + ); + } + + #[test] + fn err_for_cancelled_status() { + assert_eq!( + validate_escrow_release("CANCELLED", true), + Err("Booking not completed") + ); + } +} diff --git a/contracts/cntr/src/lib.rs b/contracts/cntr/src/lib.rs new file mode 100644 index 0000000..a98fbd2 --- /dev/null +++ b/contracts/cntr/src/lib.rs @@ -0,0 +1,4 @@ +pub mod subscription_expiry; +pub mod member_tier; +pub mod escrow_release; +pub mod escrow_refund; diff --git a/contracts/cntr/src/member_tier.rs b/contracts/cntr/src/member_tier.rs new file mode 100644 index 0000000..d8d0366 --- /dev/null +++ b/contracts/cntr/src/member_tier.rs @@ -0,0 +1,64 @@ +#[derive(Debug, PartialEq, Clone)] +pub enum MemberTier { + Bronze, + Silver, + Gold, + Platinum, +} + +pub fn calculate_tier(total_bookings: u32, total_spend_stroops: i128) -> MemberTier { + if total_bookings >= 100 || total_spend_stroops >= 100_000_000 { + MemberTier::Platinum + } else if total_bookings >= 30 || total_spend_stroops >= 20_000_000 { + MemberTier::Gold + } else if total_bookings >= 10 || total_spend_stroops >= 5_000_000 { + MemberTier::Silver + } else { + MemberTier::Bronze + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bronze_by_default() { + assert_eq!(calculate_tier(0, 0), MemberTier::Bronze); + } + + #[test] + fn silver_at_10_bookings() { + assert_eq!(calculate_tier(10, 0), MemberTier::Silver); + } + + #[test] + fn silver_by_spend() { + assert_eq!(calculate_tier(0, 5_000_000), MemberTier::Silver); + } + + #[test] + fn gold_at_30_bookings() { + assert_eq!(calculate_tier(30, 0), MemberTier::Gold); + } + + #[test] + fn gold_by_spend() { + assert_eq!(calculate_tier(0, 20_000_000), MemberTier::Gold); + } + + #[test] + fn platinum_at_100_bookings() { + assert_eq!(calculate_tier(100, 0), MemberTier::Platinum); + } + + #[test] + fn platinum_by_spend() { + assert_eq!(calculate_tier(0, 100_000_000), MemberTier::Platinum); + } + + #[test] + fn below_silver_threshold() { + assert_eq!(calculate_tier(9, 4_999_999), MemberTier::Bronze); + } +} diff --git a/contracts/cntr/src/subscription_expiry.rs b/contracts/cntr/src/subscription_expiry.rs new file mode 100644 index 0000000..2510362 --- /dev/null +++ b/contracts/cntr/src/subscription_expiry.rs @@ -0,0 +1,45 @@ +pub fn is_subscription_expired(expiry_timestamp: u64, current_timestamp: u64) -> bool { + current_timestamp >= expiry_timestamp +} + +pub fn days_until_expiry(expiry_timestamp: u64, current_timestamp: u64) -> i64 { + let diff = expiry_timestamp as i64 - current_timestamp as i64; + diff / 86400 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn expired_when_current_past_expiry() { + assert!(is_subscription_expired(100, 200)); + } + + #[test] + fn not_expired_when_current_before_expiry() { + assert!(!is_subscription_expired(200, 100)); + } + + #[test] + fn expired_at_boundary() { + assert!(is_subscription_expired(100, 100)); + } + + #[test] + fn days_until_positive_when_not_expired() { + // 10 days in the future + assert_eq!(days_until_expiry(864000, 0), 10); + } + + #[test] + fn days_until_negative_when_expired() { + // expired 1 day ago + assert_eq!(days_until_expiry(0, 86400), -1); + } + + #[test] + fn days_until_zero_at_boundary() { + assert_eq!(days_until_expiry(100, 100), 0); + } +}