Skip to main content

koprogo_api/infrastructure/web/handlers/
payment_method_handlers.rs

1use crate::application::dto::{
2    CreatePaymentMethodRequest, PaymentMethodResponse, UpdatePaymentMethodRequest,
3};
4use crate::domain::entities::payment_method::PaymentMethodType;
5use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
6use crate::infrastructure::web::{AppState, AuthenticatedUser};
7use actix_web::{delete, get, post, put, web, HttpResponse, Responder, ResponseError};
8use uuid::Uuid;
9
10// ==================== Payment Method CRUD Endpoints ====================
11
12#[utoipa::path(
13    post,
14    path = "/payment-methods",
15    tag = "PaymentMethods",
16    summary = "Enregistrer un moyen de paiement pour un copropriétaire",
17    request_body = CreatePaymentMethodRequest,
18    responses(
19        (status = 201, description = "Moyen de paiement créé", body = PaymentMethodResponse),
20        (status = 400, description = "Requête invalide — champs manquants ou moyen déjà enregistré"),
21        (status = 401, description = "L'utilisateur n'appartient à aucune organisation"),
22    ),
23    security(("bearer_auth" = []))
24)]
25#[post("/payment-methods")]
26pub async fn create_payment_method(
27    state: web::Data<AppState>,
28    user: AuthenticatedUser,
29    request: web::Json<CreatePaymentMethodRequest>,
30) -> impl Responder {
31    let organization_id = match user.require_organization() {
32        Ok(org_id) => org_id,
33        Err(e) => {
34            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
35        }
36    };
37
38    match state
39        .payment_method_use_cases
40        .create_payment_method(organization_id, request.into_inner())
41        .await
42    {
43        Ok(payment_method) => {
44            AuditLogEntry::new(
45                AuditEventType::PaymentMethodCreated,
46                Some(user.user_id),
47                Some(organization_id),
48            )
49            .with_resource("PaymentMethod", payment_method.id)
50            .log();
51
52            HttpResponse::Created().json(payment_method)
53        }
54        Err(err) => {
55            AuditLogEntry::new(
56                AuditEventType::PaymentMethodCreated,
57                Some(user.user_id),
58                Some(organization_id),
59            )
60            .with_error(err.clone())
61            .log();
62
63            HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
64        }
65    }
66}
67
68#[utoipa::path(
69    get,
70    path = "/payment-methods/{id}",
71    tag = "PaymentMethods",
72    summary = "Récupérer un moyen de paiement",
73    params(("id" = Uuid, Path, description = "Identifiant du moyen de paiement")),
74    responses(
75        (status = 200, description = "Moyen de paiement", body = PaymentMethodResponse),
76        (status = 404, description = "Introuvable"),
77    ),
78    security(("bearer_auth" = []))
79)]
80#[get("/payment-methods/{id}")]
81pub async fn get_payment_method(
82    state: web::Data<AppState>,
83    user: AuthenticatedUser,
84    id: web::Path<Uuid>,
85) -> impl Responder {
86    match state.payment_method_use_cases.get_payment_method(*id).await {
87        Ok(Some(method)) => {
88            // Verify organization access
89            if let Err(err) = user.verify_org_access(method.organization_id) {
90                return HttpResponse::Forbidden().json(serde_json::json!({"error": err}));
91            }
92            HttpResponse::Ok().json(method)
93        }
94        Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
95            "error": "Payment method not found"
96        })),
97        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
98    }
99}
100
101#[utoipa::path(
102    get,
103    path = "/payment-methods/stripe/{stripe_payment_method_id}",
104    tag = "PaymentMethods",
105    summary = "Récupérer un moyen de paiement par son identifiant Stripe",
106    params(("stripe_payment_method_id" = String, Path, description = "Identifiant Stripe")),
107    responses(
108        (status = 200, description = "Moyen de paiement", body = PaymentMethodResponse),
109        (status = 404, description = "Introuvable"),
110    ),
111    security(("bearer_auth" = []))
112)]
113#[get("/payment-methods/stripe/{stripe_payment_method_id}")]
114pub async fn get_payment_method_by_stripe_id(
115    state: web::Data<AppState>,
116    user: AuthenticatedUser,
117    stripe_payment_method_id: web::Path<String>,
118) -> impl Responder {
119    match state
120        .payment_method_use_cases
121        .get_payment_method_by_stripe_id(&stripe_payment_method_id)
122        .await
123    {
124        Ok(Some(method)) => {
125            // Cloisonnement multi-organisations : cette route ne prenait AUCUNE
126            // identité. N'importe qui pouvait lire ces données de paiement sur
127            // simple connaissance de l'identifiant Stripe. Le cliquet d'identité
128            // de #772 ne la voyait pas : il ne compte que les routes PRENANT une
129            // identité sans s'en servir. Cf. #845.
130            if let Err(e) = user.verify_org_access(method.organization_id) {
131                return HttpResponse::Forbidden().json(serde_json::json!({ "error": e }));
132            }
133            HttpResponse::Ok().json(method)
134        }
135        Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
136            "error": "Payment method not found"
137        })),
138        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
139    }
140}
141
142#[utoipa::path(
143    get,
144    path = "/owners/{owner_id}/payment-methods",
145    tag = "PaymentMethods",
146    summary = "Lister les moyens de paiement d'un copropriétaire",
147    params(("owner_id" = Uuid, Path, description = "Identifiant du copropriétaire")),
148    responses(
149        (status = 200, description = "Liste des moyens de paiement", body = Vec<PaymentMethodResponse>),
150    ),
151    security(("bearer_auth" = []))
152)]
153#[get("/owners/{owner_id}/payment-methods")]
154pub async fn list_owner_payment_methods(
155    state: web::Data<AppState>,
156    owner_id: web::Path<Uuid>,
157    user: AuthenticatedUser,
158) -> impl Responder {
159    // Route imbriquee non gardee au releve du 2026-09-06 (issue #772) : elle
160    // servait la situation financiere NOMINATIVE d'une personne a quiconque
161    // connaissait son identifiant.
162    if let Err(err) = crate::infrastructure::web::middleware::scope_guard::verify_owner_org_access(
163        &user,
164        *owner_id,
165        &state.owner_use_cases,
166    )
167    .await
168    {
169        return err.error_response();
170    }
171
172    match state
173        .payment_method_use_cases
174        .list_owner_payment_methods(*owner_id)
175        .await
176    {
177        Ok(methods) => HttpResponse::Ok().json(methods),
178        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
179    }
180}
181
182#[utoipa::path(
183    get,
184    path = "/owners/{owner_id}/payment-methods/active",
185    tag = "PaymentMethods",
186    summary = "Lister les moyens de paiement actifs d'un copropriétaire",
187    params(("owner_id" = Uuid, Path, description = "Identifiant du copropriétaire")),
188    responses(
189        (status = 200, description = "Liste des moyens actifs", body = Vec<PaymentMethodResponse>),
190    ),
191    security(("bearer_auth" = []))
192)]
193#[get("/owners/{owner_id}/payment-methods/active")]
194pub async fn list_active_owner_payment_methods(
195    state: web::Data<AppState>,
196    owner_id: web::Path<Uuid>,
197    user: AuthenticatedUser,
198) -> impl Responder {
199    // Route imbriquee non gardee au releve du 2026-09-06 (issue #772) : elle
200    // servait la situation financiere NOMINATIVE d'une personne a quiconque
201    // connaissait son identifiant.
202    if let Err(err) = crate::infrastructure::web::middleware::scope_guard::verify_owner_org_access(
203        &user,
204        *owner_id,
205        &state.owner_use_cases,
206    )
207    .await
208    {
209        return err.error_response();
210    }
211
212    match state
213        .payment_method_use_cases
214        .list_active_owner_payment_methods(*owner_id)
215        .await
216    {
217        Ok(methods) => HttpResponse::Ok().json(methods),
218        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
219    }
220}
221
222#[utoipa::path(
223    get,
224    path = "/owners/{owner_id}/payment-methods/default",
225    tag = "PaymentMethods",
226    summary = "Récupérer le moyen de paiement par défaut d'un copropriétaire",
227    params(("owner_id" = Uuid, Path, description = "Identifiant du copropriétaire")),
228    responses(
229        (status = 200, description = "Moyen par défaut", body = PaymentMethodResponse),
230        (status = 404, description = "Aucun moyen par défaut"),
231    ),
232    security(("bearer_auth" = []))
233)]
234#[get("/owners/{owner_id}/payment-methods/default")]
235pub async fn get_default_payment_method(
236    state: web::Data<AppState>,
237    owner_id: web::Path<Uuid>,
238    user: AuthenticatedUser,
239) -> impl Responder {
240    // Route imbriquee non gardee au releve du 2026-09-06 (issue #772) : elle
241    // servait la situation financiere NOMINATIVE d'une personne a quiconque
242    // connaissait son identifiant.
243    if let Err(err) = crate::infrastructure::web::middleware::scope_guard::verify_owner_org_access(
244        &user,
245        *owner_id,
246        &state.owner_use_cases,
247    )
248    .await
249    {
250        return err.error_response();
251    }
252
253    match state
254        .payment_method_use_cases
255        .get_default_payment_method(*owner_id)
256        .await
257    {
258        Ok(Some(method)) => HttpResponse::Ok().json(method),
259        Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
260            "error": "No default payment method found for owner"
261        })),
262        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
263    }
264}
265
266#[utoipa::path(
267    get,
268    path = "/organizations/{organization_id}/payment-methods",
269    tag = "PaymentMethods",
270    summary = "Lister les moyens de paiement d'une organisation",
271    params(("organization_id" = Uuid, Path, description = "Identifiant de l'organisation")),
272    responses(
273        (status = 200, description = "Liste des moyens de paiement", body = Vec<PaymentMethodResponse>),
274    ),
275    security(("bearer_auth" = []))
276)]
277#[get("/organizations/{organization_id}/payment-methods")]
278pub async fn list_organization_payment_methods(
279    state: web::Data<AppState>,
280    user: AuthenticatedUser,
281    organization_id: web::Path<Uuid>,
282) -> impl Responder {
283    if let Err(e) = user.verify_org_access(*organization_id) {
284        return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
285    }
286    match state
287        .payment_method_use_cases
288        .list_organization_payment_methods(*organization_id)
289        .await
290    {
291        Ok(methods) => HttpResponse::Ok().json(methods),
292        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
293    }
294}
295
296#[utoipa::path(
297    get,
298    path = "/owners/{owner_id}/payment-methods/type/{method_type}",
299    tag = "PaymentMethods",
300    summary = "Lister les moyens de paiement d'un copropriétaire par type",
301    params(
302        ("owner_id" = Uuid, Path, description = "Identifiant du copropriétaire"),
303        ("method_type" = String, Path, description = "Type de moyen : card, sepa_debit, bancontact"),
304    ),
305    responses(
306        (status = 200, description = "Liste filtrée", body = Vec<PaymentMethodResponse>),
307        (status = 400, description = "Type de moyen inconnu"),
308    ),
309    security(("bearer_auth" = []))
310)]
311#[get("/owners/{owner_id}/payment-methods/type/{method_type}")]
312pub async fn list_payment_methods_by_type(
313    state: web::Data<AppState>,
314    path: web::Path<(Uuid, String)>,
315    user: AuthenticatedUser,
316) -> impl Responder {
317    let (owner_id, method_type_str) = path.into_inner();
318    // Route imbriquee non gardee au releve du 2026-09-06 (issue #772) : elle
319    // servait la situation financiere NOMINATIVE d'une personne a quiconque
320    // connaissait son identifiant.
321    if let Err(err) = crate::infrastructure::web::middleware::scope_guard::verify_owner_org_access(
322        &user,
323        owner_id,
324        &state.owner_use_cases,
325    )
326    .await
327    {
328        return err.error_response();
329    }
330
331    // Parse method type string to enum
332    let method_type = match method_type_str.as_str() {
333        "card" => PaymentMethodType::Card,
334        "sepa_debit" => PaymentMethodType::SepaDebit,
335        _ => {
336            return HttpResponse::BadRequest().json(serde_json::json!({
337                "error": "Invalid payment method type. Must be one of: card, sepa_debit"
338            }))
339        }
340    };
341
342    match state
343        .payment_method_use_cases
344        .list_payment_methods_by_type(owner_id, method_type)
345        .await
346    {
347        Ok(methods) => HttpResponse::Ok().json(methods),
348        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
349    }
350}
351
352#[utoipa::path(
353    put,
354    path = "/payment-methods/{id}",
355    tag = "PaymentMethods",
356    summary = "Mettre à jour un moyen de paiement",
357    params(("id" = Uuid, Path, description = "Identifiant du moyen de paiement")),
358    request_body = UpdatePaymentMethodRequest,
359    responses(
360        (status = 200, description = "Moyen mis à jour", body = PaymentMethodResponse),
361        (status = 400, description = "Requête invalide"),
362        (status = 404, description = "Introuvable"),
363    ),
364    security(("bearer_auth" = []))
365)]
366#[put("/payment-methods/{id}")]
367pub async fn update_payment_method(
368    state: web::Data<AppState>,
369    user: AuthenticatedUser,
370    id: web::Path<Uuid>,
371    request: web::Json<UpdatePaymentMethodRequest>,
372) -> impl Responder {
373    let organization_id = match user.require_organization() {
374        Ok(org_id) => org_id,
375        Err(e) => {
376            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
377        }
378    };
379
380    match state
381        .payment_method_use_cases
382        .update_payment_method(*id, request.into_inner())
383        .await
384    {
385        Ok(method) => {
386            AuditLogEntry::new(
387                AuditEventType::PaymentMethodUpdated,
388                Some(user.user_id),
389                Some(organization_id),
390            )
391            .with_resource("PaymentMethod", method.id)
392            .log();
393
394            HttpResponse::Ok().json(method)
395        }
396        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
397    }
398}
399
400#[utoipa::path(
401    put,
402    path = "/payment-methods/{id}/set-default",
403    tag = "PaymentMethods",
404    summary = "Désigner un moyen de paiement comme moyen par défaut",
405    params(("id" = Uuid, Path, description = "Identifiant du moyen de paiement")),
406    responses(
407        (status = 200, description = "Moyen désigné par défaut", body = PaymentMethodResponse),
408        (status = 404, description = "Introuvable"),
409    ),
410    security(("bearer_auth" = []))
411)]
412#[put("/payment-methods/{id}/set-default")]
413pub async fn set_payment_method_as_default(
414    state: web::Data<AppState>,
415    user: AuthenticatedUser,
416    id: web::Path<Uuid>,
417    owner_id_json: web::Json<serde_json::Value>,
418) -> impl Responder {
419    let organization_id = match user.require_organization() {
420        Ok(org_id) => org_id,
421        Err(e) => {
422            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
423        }
424    };
425
426    let owner_id = match owner_id_json.get("owner_id").and_then(|v| v.as_str()) {
427        Some(id_str) => match Uuid::parse_str(id_str) {
428            Ok(id) => id,
429            Err(_) => {
430                return HttpResponse::BadRequest().json(serde_json::json!({
431                    "error": "Invalid owner_id format"
432                }))
433            }
434        },
435        None => {
436            return HttpResponse::BadRequest().json(serde_json::json!({
437                "error": "owner_id is required"
438            }))
439        }
440    };
441
442    match state
443        .payment_method_use_cases
444        .set_as_default(*id, owner_id)
445        .await
446    {
447        Ok(method) => {
448            AuditLogEntry::new(
449                AuditEventType::PaymentMethodSetDefault,
450                Some(user.user_id),
451                Some(organization_id),
452            )
453            .with_resource("PaymentMethod", method.id)
454            .log();
455
456            HttpResponse::Ok().json(method)
457        }
458        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
459    }
460}
461
462#[utoipa::path(
463    put,
464    path = "/payment-methods/{id}/deactivate",
465    tag = "PaymentMethods",
466    summary = "Désactiver un moyen de paiement",
467    params(("id" = Uuid, Path, description = "Identifiant du moyen de paiement")),
468    responses(
469        (status = 200, description = "Moyen désactivé", body = PaymentMethodResponse),
470        (status = 404, description = "Introuvable"),
471    ),
472    security(("bearer_auth" = []))
473)]
474#[put("/payment-methods/{id}/deactivate")]
475pub async fn deactivate_payment_method(
476    state: web::Data<AppState>,
477    user: AuthenticatedUser,
478    id: web::Path<Uuid>,
479) -> impl Responder {
480    let organization_id = match user.require_organization() {
481        Ok(org_id) => org_id,
482        Err(e) => {
483            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
484        }
485    };
486
487    match state
488        .payment_method_use_cases
489        .deactivate_payment_method(*id)
490        .await
491    {
492        Ok(method) => {
493            AuditLogEntry::new(
494                AuditEventType::PaymentMethodDeactivated,
495                Some(user.user_id),
496                Some(organization_id),
497            )
498            .with_resource("PaymentMethod", method.id)
499            .log();
500
501            HttpResponse::Ok().json(method)
502        }
503        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
504    }
505}
506
507#[utoipa::path(
508    put,
509    path = "/payment-methods/{id}/reactivate",
510    tag = "PaymentMethods",
511    summary = "Réactiver un moyen de paiement",
512    params(("id" = Uuid, Path, description = "Identifiant du moyen de paiement")),
513    responses(
514        (status = 200, description = "Moyen réactivé", body = PaymentMethodResponse),
515        (status = 404, description = "Introuvable"),
516    ),
517    security(("bearer_auth" = []))
518)]
519#[put("/payment-methods/{id}/reactivate")]
520pub async fn reactivate_payment_method(
521    state: web::Data<AppState>,
522    user: AuthenticatedUser,
523    id: web::Path<Uuid>,
524) -> impl Responder {
525    let organization_id = match user.require_organization() {
526        Ok(org_id) => org_id,
527        Err(e) => {
528            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
529        }
530    };
531
532    match state
533        .payment_method_use_cases
534        .reactivate_payment_method(*id)
535        .await
536    {
537        Ok(method) => {
538            AuditLogEntry::new(
539                AuditEventType::PaymentMethodReactivated,
540                Some(user.user_id),
541                Some(organization_id),
542            )
543            .with_resource("PaymentMethod", method.id)
544            .log();
545
546            HttpResponse::Ok().json(method)
547        }
548        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
549    }
550}
551
552#[utoipa::path(
553    delete,
554    path = "/payment-methods/{id}",
555    tag = "PaymentMethods",
556    summary = "Supprimer un moyen de paiement",
557    params(("id" = Uuid, Path, description = "Identifiant du moyen de paiement")),
558    responses(
559        (status = 204, description = "Moyen supprimé"),
560        (status = 404, description = "Introuvable"),
561    ),
562    security(("bearer_auth" = []))
563)]
564#[delete("/payment-methods/{id}")]
565pub async fn delete_payment_method(
566    state: web::Data<AppState>,
567    user: AuthenticatedUser,
568    id: web::Path<Uuid>,
569) -> impl Responder {
570    let organization_id = match user.require_organization() {
571        Ok(org_id) => org_id,
572        Err(e) => {
573            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
574        }
575    };
576
577    match state
578        .payment_method_use_cases
579        .delete_payment_method(*id)
580        .await
581    {
582        Ok(true) => {
583            AuditLogEntry::new(
584                AuditEventType::PaymentMethodDeleted,
585                Some(user.user_id),
586                Some(organization_id),
587            )
588            .with_resource("PaymentMethod", *id)
589            .log();
590
591            HttpResponse::NoContent().finish()
592        }
593        Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
594            "error": "Payment method not found"
595        })),
596        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
597    }
598}
599
600// ==================== Payment Method Statistics Endpoints ====================
601
602#[utoipa::path(
603    get,
604    path = "/owners/{owner_id}/payment-methods/count",
605    tag = "PaymentMethods",
606    summary = "Compter les moyens de paiement actifs d'un copropriétaire",
607    params(("owner_id" = Uuid, Path, description = "Identifiant du copropriétaire")),
608    responses(
609        (status = 200, description = "Nombre de moyens actifs"),
610    ),
611    security(("bearer_auth" = []))
612)]
613#[get("/owners/{owner_id}/payment-methods/count")]
614pub async fn count_active_payment_methods(
615    state: web::Data<AppState>,
616    owner_id: web::Path<Uuid>,
617    user: AuthenticatedUser,
618) -> impl Responder {
619    // Route imbriquee non gardee au releve du 2026-09-06 (issue #772) : elle
620    // servait la situation financiere NOMINATIVE d'une personne a quiconque
621    // connaissait son identifiant.
622    if let Err(err) = crate::infrastructure::web::middleware::scope_guard::verify_owner_org_access(
623        &user,
624        *owner_id,
625        &state.owner_use_cases,
626    )
627    .await
628    {
629        return err.error_response();
630    }
631
632    match state
633        .payment_method_use_cases
634        .count_active_payment_methods(*owner_id)
635        .await
636    {
637        Ok(count) => HttpResponse::Ok().json(serde_json::json!({
638            "owner_id": *owner_id,
639            "active_payment_methods_count": count
640        })),
641        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
642    }
643}
644
645#[utoipa::path(
646    get,
647    path = "/owners/{owner_id}/payment-methods/has-active",
648    tag = "PaymentMethods",
649    summary = "Indiquer si un copropriétaire a au moins un moyen de paiement actif",
650    params(("owner_id" = Uuid, Path, description = "Identifiant du copropriétaire")),
651    responses(
652        (status = 200, description = "Présence d'un moyen actif"),
653    ),
654    security(("bearer_auth" = []))
655)]
656#[get("/owners/{owner_id}/payment-methods/has-active")]
657pub async fn has_active_payment_methods(
658    state: web::Data<AppState>,
659    owner_id: web::Path<Uuid>,
660    user: AuthenticatedUser,
661) -> impl Responder {
662    // Route imbriquee non gardee au releve du 2026-09-06 (issue #772) : elle
663    // servait la situation financiere NOMINATIVE d'une personne a quiconque
664    // connaissait son identifiant.
665    if let Err(err) = crate::infrastructure::web::middleware::scope_guard::verify_owner_org_access(
666        &user,
667        *owner_id,
668        &state.owner_use_cases,
669    )
670    .await
671    {
672        return err.error_response();
673    }
674
675    match state
676        .payment_method_use_cases
677        .has_active_payment_methods(*owner_id)
678        .await
679    {
680        Ok(has_active) => HttpResponse::Ok().json(serde_json::json!({
681            "owner_id": *owner_id,
682            "has_active_payment_methods": has_active
683        })),
684        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
685    }
686}