Skip to main content

systemprompt_users/services/user/
merge.rs

1//! Account merge and anonymous promotion across every owning domain.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use systemprompt_identifiers::UserId;
7
8use super::UserService;
9use crate::error::{Result, UserError};
10use crate::models::UserRole;
11use crate::repository::MergeResult;
12
13impl UserService {
14    pub async fn merge_users(&self, source_id: &UserId, target_id: &UserId) -> Result<MergeResult> {
15        if self.owner_reassignments.is_empty() {
16            return Err(UserError::MergeUnavailable);
17        }
18        if source_id == target_id {
19            return Err(UserError::Validation(
20                "cannot merge a user into itself".to_owned(),
21            ));
22        }
23        for id in [source_id, target_id] {
24            if self.repository.find_by_id(id).await?.is_none() {
25                return Err(UserError::NotFound(id.clone()));
26            }
27        }
28
29        let mut tasks = 0;
30        let mut total_rows = 0;
31        for reassignment in self.owner_reassignments.iter() {
32            let moved = reassignment
33                .reassign_owner(source_id, target_id)
34                .await
35                .map_err(|source| UserError::OwnerReassignment {
36                    domain: reassignment.domain(),
37                    source,
38                })?;
39            tasks += moved
40                .tables
41                .iter()
42                .filter(|(table, _)| *table == "agent_tasks")
43                .map(|(_, rows)| rows)
44                .sum::<u64>();
45            total_rows += moved.total();
46        }
47
48        let sessions = self.repository.complete_merge(source_id, target_id).await?;
49        Ok(MergeResult {
50            sessions,
51            tasks,
52            total_rows: total_rows + sessions,
53        })
54    }
55
56    pub async fn promote_anonymous(
57        &self,
58        source_id: &UserId,
59        target_id: &UserId,
60    ) -> Result<MergeResult> {
61        if source_id == target_id {
62            return Err(UserError::Validation(
63                "cannot promote a user onto itself".to_owned(),
64            ));
65        }
66        let source = self
67            .repository
68            .find_by_id(source_id)
69            .await?
70            .ok_or_else(|| UserError::NotFound(source_id.clone()))?;
71        if !source.has_role(UserRole::Anonymous) {
72            return Err(UserError::Validation(format!(
73                "user {} is not anonymous; use an explicit admin merge instead",
74                source_id
75            )));
76        }
77        self.merge_users(source_id, target_id).await
78    }
79}