koprogo_api/infrastructure/web/handlers/
call_for_funds_handlers.rs

1use crate::application::dto::{
2    CallForFundsResponse, CreateCallForFundsRequest, SendCallForFundsRequest,
3    SendCallForFundsResponse,
4};
5use crate::domain::entities::ContributionType;
6use crate::infrastructure::web::{AppState, AuthenticatedUser};
7use actix_web::{delete, get, post, put, web, HttpResponse};
8use uuid::Uuid;
9
10/// POST /api/v1/call-for-funds
11/// Create a new call for funds
12#[post("/call-for-funds")]
13pub async fn create_call_for_funds(
14    state: web::Data<AppState>,
15    user: AuthenticatedUser,
16    req: web::Json<CreateCallForFundsRequest>,
17) -> HttpResponse {
18    let organization_id = match user.organization_id {
19        Some(org_id) => org_id,
20        None => return HttpResponse::BadRequest().body("Organization ID required"),
21    };
22
23    // Parse contribution type
24    let contribution_type = match req.contribution_type.as_str() {
25        "regular" => ContributionType::Regular,
26        "extraordinary" => ContributionType::Extraordinary,
27        "advance" => ContributionType::Advance,
28        "adjustment" => ContributionType::Adjustment,
29        _ => return HttpResponse::BadRequest().body("Invalid contribution type"),
30    };
31
32    match state
33        .call_for_funds_use_cases
34        .create_call_for_funds(
35            organization_id,
36            req.building_id,
37            req.title.clone(),
38            req.description.clone(),
39            req.total_amount,
40            contribution_type,
41            req.call_date,
42            req.due_date,
43            req.account_code.clone(),
44            Some(user.user_id),
45        )
46        .await
47    {
48        Ok(call) => {
49            let response = CallForFundsResponse::from(call);
50            HttpResponse::Created().json(response)
51        }
52        Err(e) => HttpResponse::BadRequest().body(e),
53    }
54}
55
56/// GET /api/v1/call-for-funds/{id}
57/// Get a call for funds by ID
58#[get("/call-for-funds/{id}")]
59pub async fn get_call_for_funds(
60    state: web::Data<AppState>,
61    _user: AuthenticatedUser,
62    id: web::Path<Uuid>,
63) -> HttpResponse {
64    match state.call_for_funds_use_cases.get_call_for_funds(*id).await {
65        Ok(Some(call)) => {
66            let response = CallForFundsResponse::from(call);
67            HttpResponse::Ok().json(response)
68        }
69        Ok(None) => HttpResponse::NotFound().body("Call for funds not found"),
70        Err(e) => HttpResponse::InternalServerError().body(e),
71    }
72}
73
74/// GET /api/v1/call-for-funds?building_id={uuid}
75/// List all calls for funds for a building or organization
76#[get("/call-for-funds")]
77pub async fn list_call_for_funds(
78    state: web::Data<AppState>,
79    user: AuthenticatedUser,
80    query: web::Query<std::collections::HashMap<String, String>>,
81) -> HttpResponse {
82    // If building_id provided, filter by building
83    if let Some(id_str) = query.get("building_id") {
84        let building_id = match Uuid::parse_str(id_str) {
85            Ok(id) => id,
86            Err(_) => return HttpResponse::BadRequest().body("Invalid building_id format"),
87        };
88
89        match state
90            .call_for_funds_use_cases
91            .list_by_building(building_id)
92            .await
93        {
94            Ok(calls) => {
95                let responses: Vec<CallForFundsResponse> =
96                    calls.into_iter().map(Into::into).collect();
97                return HttpResponse::Ok().json(responses);
98            }
99            Err(e) => return HttpResponse::InternalServerError().body(e),
100        }
101    }
102
103    // Otherwise, return all calls for user's organization
104    let organization_id = match user.organization_id {
105        Some(org_id) => org_id,
106        None => return HttpResponse::BadRequest().body("Organization ID required"),
107    };
108
109    match state
110        .call_for_funds_use_cases
111        .list_by_organization(organization_id)
112        .await
113    {
114        Ok(calls) => {
115            let responses: Vec<CallForFundsResponse> = calls.into_iter().map(Into::into).collect();
116            HttpResponse::Ok().json(responses)
117        }
118        Err(e) => HttpResponse::InternalServerError().body(e),
119    }
120}
121
122/// GET /api/v1/call-for-funds/overdue
123/// Get all overdue calls for funds
124#[get("/call-for-funds/overdue")]
125pub async fn get_overdue_calls(
126    state: web::Data<AppState>,
127    _user: AuthenticatedUser,
128) -> HttpResponse {
129    match state.call_for_funds_use_cases.get_overdue_calls().await {
130        Ok(calls) => {
131            let responses: Vec<CallForFundsResponse> = calls.into_iter().map(Into::into).collect();
132            HttpResponse::Ok().json(responses)
133        }
134        Err(e) => HttpResponse::InternalServerError().body(e),
135    }
136}
137
138/// POST /api/v1/call-for-funds/{id}/send
139/// Send a call for funds (marks as sent and generates individual contributions)
140#[post("/call-for-funds/{id}/send")]
141pub async fn send_call_for_funds(
142    state: web::Data<AppState>,
143    _user: AuthenticatedUser,
144    id: web::Path<Uuid>,
145    _req: web::Json<SendCallForFundsRequest>,
146) -> HttpResponse {
147    match state
148        .call_for_funds_use_cases
149        .send_call_for_funds(*id)
150        .await
151    {
152        Ok(call) => {
153            // Get the number of contributions generated
154            // (In a real implementation, we'd return this from send_call_for_funds)
155            let contributions_generated = match state
156                .owner_contribution_use_cases
157                .get_contributions_by_organization(call.organization_id)
158                .await
159            {
160                Ok(contribs) => contribs
161                    .iter()
162                    .filter(|c| c.call_for_funds_id == Some(call.id))
163                    .count(),
164                Err(_) => 0,
165            };
166
167            let response = SendCallForFundsResponse {
168                call_for_funds: CallForFundsResponse::from(call),
169                contributions_generated,
170            };
171            HttpResponse::Ok().json(response)
172        }
173        Err(e) => HttpResponse::BadRequest().body(e),
174    }
175}
176
177/// PUT /api/v1/call-for-funds/{id}/cancel
178/// Cancel a call for funds
179#[put("/call-for-funds/{id}/cancel")]
180pub async fn cancel_call_for_funds(
181    state: web::Data<AppState>,
182    _user: AuthenticatedUser,
183    id: web::Path<Uuid>,
184) -> HttpResponse {
185    match state
186        .call_for_funds_use_cases
187        .cancel_call_for_funds(*id)
188        .await
189    {
190        Ok(call) => {
191            let response = CallForFundsResponse::from(call);
192            HttpResponse::Ok().json(response)
193        }
194        Err(e) => HttpResponse::BadRequest().body(e),
195    }
196}
197
198/// DELETE /api/v1/call-for-funds/{id}
199/// Delete a call for funds (only if in draft status)
200#[delete("/call-for-funds/{id}")]
201pub async fn delete_call_for_funds(
202    state: web::Data<AppState>,
203    _user: AuthenticatedUser,
204    id: web::Path<Uuid>,
205) -> HttpResponse {
206    match state
207        .call_for_funds_use_cases
208        .delete_call_for_funds(*id)
209        .await
210    {
211        Ok(true) => HttpResponse::NoContent().finish(),
212        Ok(false) => HttpResponse::NotFound().body("Call for funds not found"),
213        Err(e) => HttpResponse::BadRequest().body(e),
214    }
215}