Skip to main content

sofie_tenancy/
invitation.rs

1use std::time::Duration;
2
3use sofie::core::{Instant, RuleText};
4use sofie::db::{Query, Source};
5use sofie::prelude::*;
6
7use sofie_identity::AccountId;
8
9use crate::organization::{OrganizationId, OrganizationRole};
10
11/// Long enough for a colleague back from a week away.
12pub(crate) const VALID_FOR: Duration = Duration::from_hours(7 * 24);
13pub(crate) const NOT_OPEN: RuleText = RuleText::new(
14    "tenancy.rule.not_open",
15    "This invitation was accepted already or has expired.",
16);
17
18/// A role offered to whoever holds the mailed link and signs in with its address.
19#[derive(Aggregate)]
20pub struct Invitation {
21    meta: Meta,
22    #[owner]
23    organization: OrganizationId,
24    email: Email,
25    role: OrganizationRole,
26    invited_by: AccountId,
27    expires_at: Instant,
28    accepted_at: Option<Instant>,
29}
30
31impl Invitation {
32    pub(crate) fn new(
33        organization: OrganizationId,
34        email: Email,
35        role: OrganizationRole,
36        invited_by: AccountId,
37        now: Instant,
38    ) -> Result<Self> {
39        Ok(Self {
40            meta: Meta::new()?,
41            organization,
42            email,
43            role,
44            invited_by,
45            expires_at: now.checked_add(VALID_FOR)?,
46            accepted_at: None,
47        })
48    }
49
50    #[must_use]
51    pub fn organization(&self) -> OrganizationId {
52        self.organization
53    }
54
55    #[must_use]
56    pub fn email(&self) -> &Email {
57        &self.email
58    }
59
60    #[must_use]
61    pub fn role(&self) -> OrganizationRole {
62        self.role
63    }
64
65    pub(crate) fn invited_by(&self) -> AccountId {
66        self.invited_by
67    }
68
69    #[must_use]
70    pub fn expires_at(&self) -> Instant {
71        self.expires_at
72    }
73
74    #[must_use]
75    pub fn is_open(&self, now: Instant) -> bool {
76        self.accepted_at.is_none() && self.expires_at > now
77    }
78
79    /// Expired at once, so its link opens nothing and its seat is free.
80    pub(crate) fn withdraw(&mut self, now: Instant) -> Result<()> {
81        if !self.is_open(now) {
82            return Err(NOT_OPEN.into());
83        }
84        self.expires_at = now;
85        Ok(())
86    }
87
88    pub(crate) fn accept(&mut self, now: Instant) -> Result<()> {
89        if !self.is_open(now) {
90            return Err(NOT_OPEN.into());
91        }
92        self.accepted_at = Some(now);
93        Ok(())
94    }
95}
96
97/// Those still waiting, which hold a seat as a member does.
98pub(crate) fn pending<T: Source<Base = Invitation>>(
99    query: Query<T>,
100    organization: OrganizationId,
101    now: Instant,
102) -> Query<T> {
103    query
104        .where_(Invitation::ORGANIZATION.eq(organization))
105        .where_(Invitation::ACCEPTED_AT.is_null())
106        .where_(Invitation::EXPIRES_AT.gt(now))
107}