use std::sync::Arc;
use crate::platform::adapters::data::DataAdapter;
use crate::platform::error::PlatformError;
use crate::platform::model::{
CreateProjectInviteRequest, ProjectInvite, ProjectInviteStatus, now_ts, slug_segment,
};
pub struct ProjectInviteService {
data: Arc<dyn DataAdapter>,
}
impl ProjectInviteService {
pub fn new(data: Arc<dyn DataAdapter>) -> Self {
Self { data }
}
pub fn list_invites(
&self,
owner: &str,
project: &str,
) -> Result<Vec<ProjectInvite>, PlatformError> {
self.data
.list_project_invites(&slug_segment(owner), &slug_segment(project))
}
pub fn create_invite(
&self,
actor_user: &str,
owner: &str,
project: &str,
req: &CreateProjectInviteRequest,
) -> Result<ProjectInvite, PlatformError> {
let actor_user = slug_segment(actor_user);
let owner = slug_segment(owner);
let project = slug_segment(project);
let target_user = slug_segment(&req.target_user);
if target_user.is_empty() {
return Err(PlatformError::new(
"PLATFORM_INVITE_INVALID",
"target_user must not be empty",
));
}
if self.data.get_project(&owner, &project)?.is_none() {
return Err(PlatformError::new(
"PLATFORM_PROJECT_MISSING",
format!("project '{owner}/{project}' not found"),
));
}
if self.data.get_user_auth(&target_user)?.is_none() {
return Err(PlatformError::new(
"PLATFORM_INVITE_USER_MISSING",
format!("user '{target_user}' not found"),
));
}
let now = now_ts();
let invite_id = format!(
"invite-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis()
);
let invite = ProjectInvite {
owner,
project,
invite_id,
target_user,
role_preset: req.role_preset,
custom_policy_ids: req.custom_policy_ids.clone(),
mcp_capability_ceiling: req.mcp_capability_ceiling.clone(),
note: req.note.trim().to_string(),
invited_by: actor_user,
status: ProjectInviteStatus::Pending,
expires_at: req.expires_at,
created_at: now,
updated_at: now,
};
self.data.put_project_invite(&invite)?;
Ok(invite)
}
pub fn revoke_invite(
&self,
owner: &str,
project: &str,
invite_id: &str,
) -> Result<(), PlatformError> {
let owner = slug_segment(owner);
let project = slug_segment(project);
let invite_id = invite_id.trim();
let Some(mut invite) = self.data.get_project_invite(&owner, &project, invite_id)? else {
return Err(PlatformError::new(
"PLATFORM_INVITE_NOT_FOUND",
format!("invite '{invite_id}' not found"),
));
};
invite.status = ProjectInviteStatus::Revoked;
invite.updated_at = now_ts();
self.data.put_project_invite(&invite)?;
Ok(())
}
pub fn list_invites_for_user(
&self,
target_user: &str,
) -> Result<Vec<ProjectInvite>, PlatformError> {
let target_user = slug_segment(target_user);
let now = now_ts();
Ok(self
.data
.list_project_invites_for_user(&target_user)?
.into_iter()
.filter(|invite| invite.status == ProjectInviteStatus::Pending)
.filter(|invite| invite.expires_at.is_none_or(|at| at > now))
.collect())
}
pub fn accept_invite(
&self,
actor_user: &str,
owner: &str,
project: &str,
invite_id: &str,
) -> Result<ProjectInvite, PlatformError> {
let actor_user = slug_segment(actor_user);
let mut invite = self.answerable_invite(&actor_user, owner, project, invite_id)?;
invite.status = ProjectInviteStatus::Accepted;
invite.updated_at = now_ts();
self.data.put_project_invite(&invite)?;
Ok(invite)
}
pub fn decline_invite(
&self,
actor_user: &str,
owner: &str,
project: &str,
invite_id: &str,
) -> Result<ProjectInvite, PlatformError> {
let actor_user = slug_segment(actor_user);
let mut invite = self.answerable_invite(&actor_user, owner, project, invite_id)?;
invite.status = ProjectInviteStatus::Revoked;
invite.updated_at = now_ts();
self.data.put_project_invite(&invite)?;
Ok(invite)
}
pub fn peek_answerable(
&self,
actor_user: &str,
owner: &str,
project: &str,
invite_id: &str,
) -> Result<ProjectInvite, PlatformError> {
self.answerable_invite(&slug_segment(actor_user), owner, project, invite_id)
}
fn answerable_invite(
&self,
actor_user: &str,
owner: &str,
project: &str,
invite_id: &str,
) -> Result<ProjectInvite, PlatformError> {
let owner = slug_segment(owner);
let project = slug_segment(project);
let Some(invite) = self
.data
.get_project_invite(&owner, &project, invite_id.trim())?
else {
return Err(PlatformError::new(
"PLATFORM_INVITE_NOT_FOUND",
format!("invite '{}' not found", invite_id.trim()),
));
};
if invite.target_user != actor_user {
return Err(PlatformError::new(
"PLATFORM_INVITE_NOT_FOUND",
format!("invite '{}' not found", invite_id.trim()),
));
}
if invite.status != ProjectInviteStatus::Pending {
return Err(PlatformError::new(
"PLATFORM_INVITE_NOT_PENDING",
format!(
"invite '{}' was already {}",
invite.invite_id,
invite.status.key()
),
));
}
if invite.expires_at.is_some_and(|at| at <= now_ts()) {
return Err(PlatformError::new(
"PLATFORM_INVITE_EXPIRED",
format!("invite '{}' has expired", invite.invite_id),
));
}
Ok(invite)
}
}