konnektoren_core/marketplace/
coupon.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use thiserror::Error;
4use uuid::Uuid;
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7pub struct Coupon {
8 pub id: Uuid, pub code: String, pub challenge_ids: Vec<String>, pub max_uses: u32, pub uses_remaining: u32, pub expiration_date: DateTime<Utc>, pub used_by: Vec<String>, }
16
17#[derive(Debug, Error)]
18pub enum CouponRedemptionError {
19 #[error("Coupon expired on {0}")]
20 Expired(DateTime<Utc>),
21 #[error("Coupon already used with this trace ID")]
22 AlreadyUsed,
23 #[error("Invalid challenge ID for this coupon")]
24 InvalidChallenge,
25 #[error("Coupon has no uses remaining")]
26 NoUsesRemaining,
27}
28
29impl Coupon {
30 pub fn new(
31 code: String,
32 challenge_ids: Vec<String>,
33 max_uses: u32,
34 expiration_date: DateTime<Utc>,
35 ) -> Self {
36 Coupon {
37 id: Uuid::new_v4(),
38 code,
39 challenge_ids,
40 max_uses,
41 uses_remaining: max_uses,
42 expiration_date,
43 used_by: Vec::new(),
44 }
45 }
46
47 pub fn is_valid(&self, challenge_id: &str, user_id: &str) -> bool {
48 if self.expiration_date < Utc::now() {
50 return false;
51 }
52
53 if self.uses_remaining == 0 {
55 return false;
56 }
57
58 if !self.challenge_ids.contains(&challenge_id.to_string()) {
60 return false;
61 }
62
63 if self.used_by.contains(&user_id.to_string()) {
65 return false;
66 }
67
68 true
69 }
70
71 pub fn redeem(&mut self, user_id: String) -> Result<(), CouponRedemptionError> {
72 if self.expiration_date < Utc::now() {
73 return Err(CouponRedemptionError::Expired(self.expiration_date));
74 }
75 if self.uses_remaining == 0 {
76 return Err(CouponRedemptionError::NoUsesRemaining);
77 }
78 if self.used_by.contains(&user_id) {
79 return Err(CouponRedemptionError::AlreadyUsed);
80 }
81 self.uses_remaining -= 1;
82 self.used_by.push(user_id);
83 Ok(())
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use chrono::Duration;
91
92 #[test]
93 fn test_coupon_creation() {
94 let expiration_date = Utc::now() + Duration::days(7);
95 let coupon = Coupon::new(
96 "TEST1234".to_string(),
97 vec!["challenge1".to_string(), "challenge2".to_string()],
98 2,
99 expiration_date,
100 );
101 assert_eq!(coupon.code, "TEST1234");
102 assert_eq!(coupon.max_uses, 2);
103 assert_eq!(coupon.uses_remaining, 2);
104 }
105
106 #[test]
107 fn test_coupon_redemption() {
108 let expiration_date = Utc::now() + Duration::days(7);
109 let mut coupon = Coupon::new(
110 "TEST1234".to_string(),
111 vec!["challenge1".to_string()],
112 1,
113 expiration_date,
114 );
115 let trace_id = "trace123".to_string();
116
117 assert!(coupon.redeem(trace_id.clone()).is_ok());
118 assert!(coupon.redeem(trace_id.clone()).is_err()); assert_eq!(coupon.uses_remaining, 0);
120
121 let expiration_date_past = Utc::now() - Duration::days(1);
122 let mut coupon_past = Coupon::new(
123 "TEST1235".to_string(),
124 vec!["challenge1".to_string()],
125 1,
126 expiration_date_past,
127 );
128 assert!(coupon_past.redeem(trace_id.clone()).is_err()); }
130
131 #[test]
132 fn test_coupon_redemption_expired() {
133 let expiration_date_past = Utc::now() - Duration::days(1);
134 let mut coupon = Coupon::new(
135 "TEST1235".to_string(),
136 vec!["challenge1".to_string()],
137 1,
138 expiration_date_past,
139 );
140 let trace_id = "trace123".to_string();
141 match coupon.redeem(trace_id) {
142 Err(CouponRedemptionError::Expired(date)) => {
143 assert_eq!(date, expiration_date_past);
144 }
145 _ => panic!("Expected Expired error"),
146 }
147 }
148}