koprogo_api/application/use_cases/
individual_member_use_cases.rs

1use crate::application::dto::individual_member_dto::IndividualMemberResponseDto;
2use crate::application::ports::individual_member_repository::IndividualMemberRepository;
3use crate::domain::entities::IndividualMember;
4use chrono::Utc;
5use std::sync::Arc;
6use uuid::Uuid;
7
8pub struct IndividualMemberUseCases {
9    pub repo: Arc<dyn IndividualMemberRepository>,
10}
11
12impl IndividualMemberUseCases {
13    pub fn new(repo: Arc<dyn IndividualMemberRepository>) -> Self {
14        Self { repo }
15    }
16
17    /// Join a campaign as an individual member
18    pub async fn join_campaign(
19        &self,
20        campaign_id: Uuid,
21        email: String,
22        postal_code: String,
23    ) -> Result<IndividualMemberResponseDto, String> {
24        // Check for duplicate email in campaign
25        if let Some(_existing) = self
26            .repo
27            .find_by_email_and_campaign(&email, campaign_id)
28            .await?
29        {
30            return Err("Email already registered for this campaign".to_string());
31        }
32
33        let member = IndividualMember::new(campaign_id, email, postal_code)?;
34        let created = self.repo.create(&member).await?;
35        Ok(IndividualMemberResponseDto::from(created))
36    }
37
38    /// Grant GDPR consent
39    pub async fn grant_consent(
40        &self,
41        campaign_id: Uuid,
42        member_id: Uuid,
43    ) -> Result<IndividualMemberResponseDto, String> {
44        let mut member = self
45            .repo
46            .find_by_id(member_id)
47            .await?
48            .ok_or_else(|| format!("Member {} not found", member_id))?;
49
50        if member.campaign_id != campaign_id {
51            return Err("Member does not belong to this campaign".to_string());
52        }
53
54        member.has_gdpr_consent = true;
55        member.consent_at = Some(Utc::now());
56
57        let updated = self.repo.update(&member).await?;
58        Ok(IndividualMemberResponseDto::from(updated))
59    }
60
61    /// Update consumption data
62    pub async fn update_consumption(
63        &self,
64        campaign_id: Uuid,
65        member_id: Uuid,
66        annual_kwh: Option<f64>,
67        current_provider: Option<String>,
68        ean_code: Option<String>,
69    ) -> Result<IndividualMemberResponseDto, String> {
70        let mut member = self
71            .repo
72            .find_by_id(member_id)
73            .await?
74            .ok_or_else(|| format!("Member {} not found", member_id))?;
75
76        if member.campaign_id != campaign_id {
77            return Err("Member does not belong to this campaign".to_string());
78        }
79
80        if let Some(kwh) = annual_kwh {
81            if kwh < 0.0 {
82                return Err("Consumption cannot be negative".to_string());
83            }
84            member.annual_consumption_kwh = Some(kwh);
85        }
86        if let Some(provider) = current_provider {
87            member.current_provider = Some(provider);
88        }
89        if let Some(ean) = ean_code {
90            member.ean_code = Some(ean);
91        }
92
93        let updated = self.repo.update(&member).await?;
94        Ok(IndividualMemberResponseDto::from(updated))
95    }
96
97    /// Withdraw from campaign (GDPR Art. 17)
98    pub async fn withdraw(&self, campaign_id: Uuid, member_id: Uuid) -> Result<String, String> {
99        let member = self
100            .repo
101            .find_by_id(member_id)
102            .await?
103            .ok_or_else(|| format!("Member {} not found", member_id))?;
104
105        if member.campaign_id != campaign_id {
106            return Err("Member does not belong to this campaign".to_string());
107        }
108
109        let email = member.email.clone();
110        self.repo.withdraw_consent(member_id).await?;
111        Ok(email)
112    }
113}