koprogo_api/infrastructure/database/repositories/
mandate_repository_impl.rs1use crate::application::error::AppError;
7use crate::application::ports::MandateRepository;
8use crate::domain::entities::{Mandate, MandateKind, MandateScope};
9use crate::infrastructure::database::pool::DbPool;
10use async_trait::async_trait;
11use chrono::{DateTime, Utc};
12use sqlx::Row;
13use std::str::FromStr;
14use uuid::Uuid;
15
16pub struct PostgresMandateRepository {
17 pool: DbPool,
18}
19
20impl PostgresMandateRepository {
21 pub fn new(pool: DbPool) -> Self {
22 Self { pool }
23 }
24
25 fn row_to_mandate(row: &sqlx::postgres::PgRow) -> Result<Mandate, AppError> {
26 let kind_str: String = row.get("kind");
27 let kind = MandateKind::from_str(&kind_str)?;
28 let scope_kind_str: String = row.get("scope_kind");
29 let scope_id: Uuid = row.get("scope_id");
30 let scope = MandateScope::from_parts(&scope_kind_str, scope_id)?;
31 Ok(Mandate {
32 id: row.get("id"),
33 subject_user_id: row.get("subject_user_id"),
34 kind,
35 scope,
36 issued_by: row.get("issued_by"),
37 reason: row.get("reason"),
38 valid_from: row.get("valid_from"),
39 valid_until: row.get("valid_until"),
40 revoked_at: row.get("revoked_at"),
41 created_at: row.get("created_at"),
42 updated_at: row.get("updated_at"),
43 })
44 }
45}
46
47#[async_trait]
48impl MandateRepository for PostgresMandateRepository {
49 async fn save(&self, mandate: &Mandate) -> Result<(), AppError> {
50 sqlx::query(
51 r#"
52 INSERT INTO mandates (
53 id, subject_user_id, kind, scope_kind, scope_id,
54 issued_by, reason, valid_from, valid_until, revoked_at,
55 created_at, updated_at
56 )
57 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
58 "#,
59 )
60 .bind(mandate.id)
61 .bind(mandate.subject_user_id)
62 .bind(mandate.kind.to_string())
63 .bind(mandate.scope.kind_str())
64 .bind(mandate.scope.id())
65 .bind(mandate.issued_by)
66 .bind(&mandate.reason)
67 .bind(mandate.valid_from)
68 .bind(mandate.valid_until)
69 .bind(mandate.revoked_at)
70 .bind(mandate.created_at)
71 .bind(mandate.updated_at)
72 .execute(&self.pool)
73 .await
74 .map_err(|e| AppError::Database(e.to_string()))?;
75 Ok(())
76 }
77
78 async fn find_by_id(&self, id: Uuid) -> Result<Option<Mandate>, AppError> {
79 let row = sqlx::query(
80 r#"
81 SELECT id, subject_user_id, kind, scope_kind, scope_id,
82 issued_by, reason, valid_from, valid_until, revoked_at,
83 created_at, updated_at
84 FROM mandates
85 WHERE id = $1
86 "#,
87 )
88 .bind(id)
89 .fetch_optional(&self.pool)
90 .await
91 .map_err(|e| AppError::Database(e.to_string()))?;
92
93 match row {
94 None => Ok(None),
95 Some(row) => Ok(Some(Self::row_to_mandate(&row)?)),
96 }
97 }
98
99 async fn list_active_for_subject(
100 &self,
101 subject_user_id: Uuid,
102 ) -> Result<Vec<Mandate>, AppError> {
103 let rows = sqlx::query(
104 r#"
105 SELECT id, subject_user_id, kind, scope_kind, scope_id,
106 issued_by, reason, valid_from, valid_until, revoked_at,
107 created_at, updated_at
108 FROM mandates
109 WHERE subject_user_id = $1
110 AND revoked_at IS NULL
111 AND valid_from <= NOW()
112 AND valid_until > NOW()
113 ORDER BY valid_until ASC
114 "#,
115 )
116 .bind(subject_user_id)
117 .fetch_all(&self.pool)
118 .await
119 .map_err(|e| AppError::Database(e.to_string()))?;
120
121 rows.iter().map(Self::row_to_mandate).collect()
122 }
123
124 async fn list_for_scope(&self, scope: &MandateScope) -> Result<Vec<Mandate>, AppError> {
125 let rows = sqlx::query(
126 r#"
127 SELECT id, subject_user_id, kind, scope_kind, scope_id,
128 issued_by, reason, valid_from, valid_until, revoked_at,
129 created_at, updated_at
130 FROM mandates
131 WHERE scope_kind = $1 AND scope_id = $2
132 ORDER BY created_at DESC
133 "#,
134 )
135 .bind(scope.kind_str())
136 .bind(scope.id())
137 .fetch_all(&self.pool)
138 .await
139 .map_err(|e| AppError::Database(e.to_string()))?;
140
141 rows.iter().map(Self::row_to_mandate).collect()
142 }
143
144 async fn revoke(&self, id: Uuid, revoked_at: DateTime<Utc>) -> Result<(), AppError> {
145 sqlx::query(
147 r#"
148 UPDATE mandates
149 SET revoked_at = $2, updated_at = $2
150 WHERE id = $1 AND revoked_at IS NULL
151 "#,
152 )
153 .bind(id)
154 .bind(revoked_at)
155 .execute(&self.pool)
156 .await
157 .map_err(|e| AppError::Database(e.to_string()))?;
158 Ok(())
159 }
160}