dwctl 8.52.1

The Doubleword Control Layer - A self-hostable observability and analytics platform for LLM applications
//! API request/response models for organizations.

use crate::api::models::pagination::Pagination;
use crate::api::models::users::UserResponse;

use crate::types::UserId;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_with::rust::double_option;
use utoipa::{IntoParams, ToSchema};

/// Request body for creating a new organization
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct OrganizationCreate {
    /// Organization domain or unique identifier (becomes username, used for domain-based auto-join)
    #[schema(example = "acme.com")]
    pub name: String,
    /// Organization contact email (for billing, notifications)
    #[schema(example = "admin@acme.com")]
    pub email: String,
    /// Human-readable display name
    #[schema(example = "Acme Corporation")]
    pub display_name: Option<String>,
    /// User ID to assign as owner (platform managers only; defaults to current user)
    #[schema(value_type = Option<String>, format = "uuid")]
    pub owner_id: Option<UserId>,
}

/// Request body for updating an organization
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct OrganizationUpdate {
    /// New display name
    pub display_name: Option<String>,
    /// New contact email
    pub email: Option<String>,
    /// Whether batch completion/failure email notifications are enabled
    pub batch_notifications_enabled: Option<bool>,
    /// Low balance notification threshold in dollars
    /// (e.g. 2.0 means notify when balance drops below $2), set to null to disable.
    /// Omit entirely to leave unchanged.
    #[serde(default, skip_serializing_if = "Option::is_none", with = "double_option")]
    pub low_balance_threshold: Option<Option<f32>>,
}

/// Full organization details returned by the API.
/// Organizations are users with user_type = 'organization', so this wraps UserResponse
/// with additional org-specific fields.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct OrganizationResponse {
    /// The organization's user record
    #[serde(flatten)]
    pub user: UserResponse,
    /// Number of members in the organization
    #[serde(skip_serializing_if = "Option::is_none")]
    pub member_count: Option<i64>,
    /// A pending email change awaiting verification. Present when the
    /// caller has requested a new contact email but it has not yet been
    /// confirmed via the link sent to the new address.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pending_email_change: Option<PendingEmailChangeResponse>,
}

impl OrganizationResponse {
    pub fn from_user(user: UserResponse) -> Self {
        Self {
            user,
            member_count: None,
            pending_email_change: None,
        }
    }

    pub fn with_member_count(mut self, count: i64) -> Self {
        self.member_count = Some(count);
        self
    }

    pub fn with_pending_email_change(mut self, info: PendingEmailChangeResponse) -> Self {
        self.pending_email_change = Some(info);
        self
    }
}

/// Public projection of a pending organization email change.
///
/// Deliberately omits the row id, requested-by user, and token-related
/// fields stored in [`PendingOrgEmailChangeDBResponse`] so that no
/// token-adjacent metadata leaks out of the API layer.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct PendingEmailChangeResponse {
    /// The address the contact email will become if the verification link is clicked.
    pub new_email: String,
    /// When the verification token expires.
    pub expires_at: DateTime<Utc>,
}

impl From<crate::db::models::organizations::PendingOrgEmailChangeDBResponse> for PendingEmailChangeResponse {
    fn from(p: crate::db::models::organizations::PendingOrgEmailChangeDBResponse) -> Self {
        Self {
            new_email: p.new_email,
            expires_at: p.expires_at,
        }
    }
}

/// Organization member details
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct OrganizationMemberResponse {
    /// Membership row ID
    #[schema(value_type = String, format = "uuid")]
    pub id: uuid::Uuid,
    /// The member's user details (None for pending invites where user hasn't signed up)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub user: Option<UserResponse>,
    /// Role in the organization: 'owner', 'admin', or 'member'
    pub role: String,
    /// Membership status: 'active' or 'pending'
    pub status: String,
    /// When the membership was created
    pub created_at: DateTime<Utc>,
    /// Email address for pending invites
    #[serde(skip_serializing_if = "Option::is_none")]
    pub invite_email: Option<String>,
}

/// Request body for inviting a member by email
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct InviteMemberRequest {
    /// Email address to invite
    #[schema(example = "newuser@example.com")]
    pub email: String,
    /// Role to assign: 'owner', 'admin', or 'member' (defaults to 'member')
    pub role: Option<String>,
}

/// Response after creating an invite
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct InviteMemberResponse {
    /// Membership row ID
    #[schema(value_type = String, format = "uuid")]
    pub id: uuid::Uuid,
    /// Invited email address
    pub email: String,
    /// Assigned role
    pub role: String,
    /// Invite status (always 'pending')
    pub status: String,
    /// When the invite was created
    pub created_at: DateTime<Utc>,
    /// When the invite expires
    pub expires_at: DateTime<Utc>,
}

/// Details about a pending invite (returned when looking up by token)
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct InviteDetailsResponse {
    /// Name of the organization
    pub org_name: String,
    /// Role being offered
    pub role: String,
    /// Display name of the person who sent the invite
    pub inviter_name: Option<String>,
    /// When the invite expires
    pub expires_at: DateTime<Utc>,
}

/// Request body for adding a member to an organization
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct AddMemberRequest {
    /// User ID to add as a member
    #[schema(value_type = String, format = "uuid")]
    pub user_id: UserId,
    /// Role to assign: 'owner', 'admin', or 'member' (defaults to 'member')
    pub role: Option<String>,
}

/// Request body for updating a member's role
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct UpdateMemberRoleRequest {
    /// New role: 'owner', 'admin', or 'member'
    pub role: String,
}

/// Query parameters for listing organizations
#[derive(Debug, Deserialize, IntoParams, ToSchema)]
pub struct ListOrganizationsQuery {
    /// Pagination parameters
    #[serde(flatten)]
    #[param(inline)]
    pub pagination: Pagination,

    /// Search query to filter by name, display_name, or email
    pub search: Option<String>,

    /// Include related data (comma-separated: "member_count")
    pub include: Option<String>,
}

/// Request body for setting/clearing active organization context
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct SetActiveOrganizationRequest {
    /// Organization ID to activate, or null to clear
    #[schema(value_type = Option<String>, format = "uuid")]
    pub organization_id: Option<UserId>,
}

/// Response for the set active organization endpoint
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct SetActiveOrganizationResponse {
    /// The active organization ID, or null if cleared
    #[schema(value_type = Option<String>, format = "uuid")]
    pub active_organization_id: Option<UserId>,
}

/// Summary of an organization for inclusion in user responses
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct OrganizationSummary {
    #[schema(value_type = String, format = "uuid")]
    pub id: UserId,
    pub name: String,
    pub role: String,
}