revelation_user/entity/user.rs
1// SPDX-FileCopyrightText: 2025 Revelation Team
2// SPDX-License-Identifier: MIT
3
4//! User entity - the core aggregate of the user domain.
5//!
6//! This module defines [`RUser`], the primary user representation in the
7//! Revelation ecosystem. Uses `entity-derive` for DTO and repository
8//! generation.
9//!
10//! # Generated Types
11//!
12//! The `Entity` derive macro generates:
13//! - [`CreateRUserRequest`] - DTO for user creation
14//! - [`UpdateRUserRequest`] - DTO for profile updates
15//! - [`RUserResponse`] - DTO for API responses
16//! - [`RUserRow`] - Database row mapping
17//! - [`InsertableRUser`] - For INSERT operations
18//! - [`RUserRepository`] - Async CRUD trait
19//!
20//! # Examples
21//!
22//! ```rust
23//! use revelation_user::RUser;
24//!
25//! // From Telegram authentication
26//! let user = RUser::from_telegram(123456789);
27//!
28//! // From email authentication
29//! let user = RUser::from_email("user@example.com");
30//!
31//! // Empty user (for OAuth flows)
32//! let user = RUser::empty();
33//! ```
34
35use chrono::{DateTime, NaiveDate, Utc};
36use entity_derive::Entity;
37use serde::{Deserialize, Serialize};
38use uuid::Uuid;
39
40use crate::Gender;
41
42/// Core user entity for the Revelation ecosystem.
43///
44/// `RUser` represents a registered user with support for multiple
45/// authentication methods and optional profile data.
46///
47/// # Fields
48///
49/// | Field | Type | Create | Update | Response |
50/// |-------|------|--------|--------|----------|
51/// | `id` | `Uuid` | — | — | Yes |
52/// | `name` | `Option<String>` | — | Yes | Yes |
53/// | `gender` | `Option<Gender>` | — | Yes | Yes |
54/// | `birth_date` | `Option<NaiveDate>` | — | Yes | Yes |
55/// | `confession_id` | `Option<Uuid>` | — | Yes | Yes |
56/// | `email` | `Option<String>` | Yes | — | Yes |
57/// | `phone` | `Option<String>` | Yes | — | Yes |
58/// | `telegram_id` | `Option<i64>` | Yes | — | Yes |
59/// | `created_at` | `DateTime<Utc>` | — | — | Yes |
60/// | `updated_at` | `DateTime<Utc>` | — | — | Yes |
61#[derive(Debug, Clone, Serialize, Deserialize, Entity)]
62#[entity(table = "users", schema = "public", sql = "none")]
63pub struct RUser {
64 /// Unique user identifier (UUIDv7).
65 #[id]
66 pub id: Uuid,
67
68 /// Display name (2-100 chars).
69 #[field(update, response)]
70 pub name: Option<String>,
71
72 /// User's gender.
73 #[field(update, response)]
74 pub gender: Option<Gender>,
75
76 /// Date of birth.
77 #[field(update, response)]
78 pub birth_date: Option<NaiveDate>,
79
80 /// Reference to confession/denomination.
81 #[field(update, response)]
82 pub confession_id: Option<Uuid>,
83
84 /// Verified email address.
85 #[field(create, response)]
86 pub email: Option<String>,
87
88 /// Phone number in E.164 format.
89 #[field(create, response)]
90 pub phone: Option<String>,
91
92 /// Telegram user ID.
93 #[field(create, response)]
94 pub telegram_id: Option<i64>,
95
96 /// Creation timestamp.
97 #[field(response)]
98 #[auto]
99 pub created_at: DateTime<Utc>,
100
101 /// Last update timestamp.
102 #[field(response)]
103 #[auto]
104 pub updated_at: DateTime<Utc>
105}
106
107impl RUser {
108 /// Create user from Telegram authentication.
109 ///
110 /// # Examples
111 ///
112 /// ```rust
113 /// use revelation_user::RUser;
114 ///
115 /// let user = RUser::from_telegram(123456789);
116 /// assert_eq!(user.telegram_id, Some(123456789));
117 /// ```
118 #[must_use]
119 pub fn from_telegram(telegram_id: i64) -> Self {
120 Self {
121 id: Uuid::now_v7(),
122 name: None,
123 gender: None,
124 birth_date: None,
125 confession_id: None,
126 email: None,
127 phone: None,
128 telegram_id: Some(telegram_id),
129 created_at: Utc::now(),
130 updated_at: Utc::now()
131 }
132 }
133
134 /// Create user from email authentication.
135 ///
136 /// # Examples
137 ///
138 /// ```rust
139 /// use revelation_user::RUser;
140 ///
141 /// let user = RUser::from_email("user@example.com");
142 /// assert_eq!(user.email.as_deref(), Some("user@example.com"));
143 /// ```
144 #[must_use]
145 pub fn from_email(email: impl Into<String>) -> Self {
146 Self {
147 id: Uuid::now_v7(),
148 name: None,
149 gender: None,
150 birth_date: None,
151 confession_id: None,
152 email: Some(email.into()),
153 phone: None,
154 telegram_id: None,
155 created_at: Utc::now(),
156 updated_at: Utc::now()
157 }
158 }
159
160 /// Create user from phone authentication.
161 ///
162 /// # Examples
163 ///
164 /// ```rust
165 /// use revelation_user::RUser;
166 ///
167 /// let user = RUser::from_phone("+79991234567");
168 /// assert!(user.phone.is_some());
169 /// ```
170 #[must_use]
171 pub fn from_phone(phone: impl Into<String>) -> Self {
172 Self {
173 id: Uuid::now_v7(),
174 name: None,
175 gender: None,
176 birth_date: None,
177 confession_id: None,
178 email: None,
179 phone: Some(phone.into()),
180 telegram_id: None,
181 created_at: Utc::now(),
182 updated_at: Utc::now()
183 }
184 }
185
186 /// Create empty user with only ID.
187 ///
188 /// # Examples
189 ///
190 /// ```rust
191 /// use revelation_user::RUser;
192 ///
193 /// let user = RUser::empty();
194 /// assert!(user.email.is_none());
195 /// ```
196 #[must_use]
197 pub fn empty() -> Self {
198 Self {
199 id: Uuid::now_v7(),
200 name: None,
201 gender: None,
202 birth_date: None,
203 confession_id: None,
204 email: None,
205 phone: None,
206 telegram_id: None,
207 created_at: Utc::now(),
208 updated_at: Utc::now()
209 }
210 }
211
212 /// Create user with a specific ID.
213 ///
214 /// # Examples
215 ///
216 /// ```rust
217 /// use revelation_user::RUser;
218 /// use uuid::Uuid;
219 ///
220 /// let id = Uuid::now_v7();
221 /// let user = RUser::with_id(id);
222 /// assert_eq!(user.id, id);
223 /// ```
224 #[must_use]
225 pub fn with_id(id: Uuid) -> Self {
226 Self {
227 id,
228 name: None,
229 gender: None,
230 birth_date: None,
231 confession_id: None,
232 email: None,
233 phone: None,
234 telegram_id: None,
235 created_at: Utc::now(),
236 updated_at: Utc::now()
237 }
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn from_telegram_sets_telegram_id() {
247 let user = RUser::from_telegram(123);
248 assert_eq!(user.telegram_id, Some(123));
249 assert!(user.email.is_none());
250 }
251
252 #[test]
253 fn from_email_sets_email() {
254 let user = RUser::from_email("test@example.com");
255 assert_eq!(user.email.as_deref(), Some("test@example.com"));
256 assert!(user.telegram_id.is_none());
257 }
258
259 #[test]
260 fn from_phone_sets_phone() {
261 let user = RUser::from_phone("+14155551234");
262 assert_eq!(user.phone.as_deref(), Some("+14155551234"));
263 }
264
265 #[test]
266 fn empty_has_only_id() {
267 let user = RUser::empty();
268 assert!(user.name.is_none());
269 assert!(user.email.is_none());
270 assert!(user.telegram_id.is_none());
271 }
272
273 #[test]
274 fn with_id_sets_specific_id() {
275 let id = Uuid::nil();
276 let user = RUser::with_id(id);
277 assert_eq!(user.id, id);
278 }
279
280 #[test]
281 fn serializes_to_json() {
282 let user = RUser::from_telegram(123);
283 let json = serde_json::to_string(&user).unwrap();
284 assert!(json.contains("\"telegram_id\":123"));
285 }
286}