From dfdf59ec9bc9f963891549c370121e4072bc8cc0 Mon Sep 17 00:00:00 2001 From: RennyThompson Date: Sat, 30 May 2026 09:45:07 +0000 Subject: [PATCH] feat(cntr): add dispute window checker and cancellation policy enforcer - Add dispute_window.rs: is_within_dispute_window and dispute_window_expires_at with 5 unit tests (closes #1022) - Add cancellation_policy.rs: CancellationPolicy enum (Flexible/Moderate/Strict) with apply_policy and 9 unit tests (closes #1033) - Register both modules in lib.rs, remove duplicate entries --- contracts/cntr/src/cancellation_policy.rs | 92 +++++++++++++++++++++++ contracts/cntr/src/dispute_window.rs | 49 ++++++++++++ contracts/cntr/src/lib.rs | 5 +- 3 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 contracts/cntr/src/cancellation_policy.rs create mode 100644 contracts/cntr/src/dispute_window.rs diff --git a/contracts/cntr/src/cancellation_policy.rs b/contracts/cntr/src/cancellation_policy.rs new file mode 100644 index 0000000..7e21cce --- /dev/null +++ b/contracts/cntr/src/cancellation_policy.rs @@ -0,0 +1,92 @@ +#[derive(Debug, PartialEq, Clone)] +pub enum CancellationPolicy { + Flexible, + Moderate, + Strict, +} + +/// Computes the refund amount in stroops based on the cancellation policy. +/// +/// - Flexible: always 100% refund. +/// - Moderate: 100% if >= 48h before start, 50% if 24–47h, 0% if < 24h. +/// - Strict: 100% if >= 96h before start, 0% otherwise. +pub fn apply_policy( + policy: CancellationPolicy, + hours_before_start: u64, + amount_stroops: i128, +) -> i128 { + match policy { + CancellationPolicy::Flexible => amount_stroops, + CancellationPolicy::Moderate => { + if hours_before_start >= 48 { + amount_stroops + } else if hours_before_start >= 24 { + amount_stroops / 2 + } else { + 0 + } + } + CancellationPolicy::Strict => { + if hours_before_start >= 96 { + amount_stroops + } else { + 0 + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const AMOUNT: i128 = 1_000_000; + + // Flexible + #[test] + fn flexible_always_full_refund() { + assert_eq!(apply_policy(CancellationPolicy::Flexible, 0, AMOUNT), AMOUNT); + } + + #[test] + fn flexible_full_refund_at_48h() { + assert_eq!(apply_policy(CancellationPolicy::Flexible, 48, AMOUNT), AMOUNT); + } + + #[test] + fn flexible_full_refund_at_100h() { + assert_eq!(apply_policy(CancellationPolicy::Flexible, 100, AMOUNT), AMOUNT); + } + + // Moderate + #[test] + fn moderate_full_refund_at_48h_or_more() { + assert_eq!(apply_policy(CancellationPolicy::Moderate, 48, AMOUNT), AMOUNT); + } + + #[test] + fn moderate_half_refund_between_24_and_47h() { + assert_eq!(apply_policy(CancellationPolicy::Moderate, 24, AMOUNT), AMOUNT / 2); + } + + #[test] + fn moderate_no_refund_under_24h() { + assert_eq!(apply_policy(CancellationPolicy::Moderate, 10, AMOUNT), 0); + } + + // Strict + #[test] + fn strict_full_refund_at_96h_or_more() { + assert_eq!(apply_policy(CancellationPolicy::Strict, 96, AMOUNT), AMOUNT); + } + + #[test] + fn strict_no_refund_at_95h() { + assert_eq!(apply_policy(CancellationPolicy::Strict, 95, AMOUNT), 0); + } + + #[test] + fn strict_no_refund_under_24h() { + assert_eq!(apply_policy(CancellationPolicy::Strict, 0, AMOUNT), 0); + } +} diff --git a/contracts/cntr/src/dispute_window.rs b/contracts/cntr/src/dispute_window.rs new file mode 100644 index 0000000..433d6f5 --- /dev/null +++ b/contracts/cntr/src/dispute_window.rs @@ -0,0 +1,49 @@ +/// Default dispute window: 48 hours in seconds. +pub const DEFAULT_DISPUTE_WINDOW_SECONDS: u64 = 172_800; + +/// Returns `true` if `current_timestamp` is within the dispute window after checkout. +pub fn is_within_dispute_window( + checkout_timestamp: u64, + current_timestamp: u64, + window_seconds: u64, +) -> bool { + current_timestamp < checkout_timestamp.saturating_add(window_seconds) +} + +/// Returns the timestamp at which the dispute window expires. +pub fn dispute_window_expires_at(checkout_timestamp: u64, window_seconds: u64) -> u64 { + checkout_timestamp.saturating_add(window_seconds) +} + +#[cfg(test)] +mod tests { + use super::*; + + const CHECKOUT: u64 = 1_000_000; + const WINDOW: u64 = DEFAULT_DISPUTE_WINDOW_SECONDS; + + #[test] + fn inside_window() { + assert!(is_within_dispute_window(CHECKOUT, CHECKOUT + 1000, WINDOW)); + } + + #[test] + fn outside_window() { + assert!(!is_within_dispute_window(CHECKOUT, CHECKOUT + WINDOW + 1, WINDOW)); + } + + #[test] + fn exactly_at_boundary_is_outside() { + assert!(!is_within_dispute_window(CHECKOUT, CHECKOUT + WINDOW, WINDOW)); + } + + #[test] + fn at_checkout_moment() { + assert!(is_within_dispute_window(CHECKOUT, CHECKOUT, WINDOW)); + } + + #[test] + fn expires_at_returns_correct_timestamp() { + assert_eq!(dispute_window_expires_at(CHECKOUT, WINDOW), CHECKOUT + WINDOW); + } +} diff --git a/contracts/cntr/src/lib.rs b/contracts/cntr/src/lib.rs index cedee37..e3f3667 100644 --- a/contracts/cntr/src/lib.rs +++ b/contracts/cntr/src/lib.rs @@ -10,7 +10,6 @@ pub mod credit_topup; pub mod token_validator; pub mod grace_period; pub mod payment_validator; -pub mod credit_topup; pub mod multi_booking; -pub mod payment_validator; -pub mod role_checker; +pub mod dispute_window; +pub mod cancellation_policy;