use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TenantSummary {
pub id: String,
pub display_name: String,
pub active: bool,
pub agent_count: usize,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TenantDetail {
pub id: String,
pub display_name: String,
pub active: bool,
pub created_at: DateTime<Utc>,
#[serde(default)]
pub llm_provider_refs: Vec<String>,
#[serde(default)]
pub metadata: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default)]
pub struct TenantsListFilter {
pub active_only: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TenantsListResponse {
pub tenants: Vec<TenantSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TenantsGetParams {
pub tenant_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TenantsGetResponse {
pub tenant: Option<TenantDetail>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TenantsUpsertInput {
pub id: String,
pub display_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub llm_provider_refs: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TenantsUpsertResponse {
pub tenant: TenantDetail,
pub created: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TenantsDeleteParams {
pub tenant_id: String,
#[serde(default)]
pub purge: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TenantsDeleteResponse {
pub removed: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub orphaned_agents: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
use serde_json::{from_value, json, to_value};
fn fixed_ts() -> DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 5, 2, 12, 0, 0).unwrap()
}
#[test]
fn empresa_summary_round_trips() {
let s = TenantSummary {
id: "acme-corp".into(),
display_name: "Acme Corp.".into(),
active: true,
agent_count: 3,
created_at: fixed_ts(),
};
let v = to_value(&s).unwrap();
let back: TenantSummary = from_value(v).unwrap();
assert_eq!(s, back);
}
#[test]
fn empresa_detail_round_trips_with_metadata() {
let mut metadata = BTreeMap::new();
metadata.insert("contact".into(), json!("support@acme.example"));
metadata.insert("tier".into(), json!("pro"));
let d = TenantDetail {
id: "acme-corp".into(),
display_name: "Acme Corp.".into(),
active: true,
created_at: fixed_ts(),
llm_provider_refs: vec!["acme-claude".into(), "acme-minimax".into()],
metadata,
};
let v = to_value(&d).unwrap();
let back: TenantDetail = from_value(v).unwrap();
assert_eq!(d, back);
}
#[test]
fn empresas_upsert_input_omits_optional_fields_when_none() {
let p = TenantsUpsertInput {
id: "globex".into(),
display_name: "Globex".into(),
active: None,
llm_provider_refs: None,
metadata: None,
};
let v = to_value(&p).unwrap();
let obj = v.as_object().unwrap();
assert!(!obj.contains_key("active"));
assert!(!obj.contains_key("llm_provider_refs"));
assert!(!obj.contains_key("metadata"));
}
#[test]
fn empresas_delete_response_omits_orphans_when_empty() {
let r = TenantsDeleteResponse {
removed: true,
orphaned_agents: vec![],
};
let v = to_value(&r).unwrap();
let obj = v.as_object().unwrap();
assert!(!obj.contains_key("orphaned_agents"));
}
#[test]
fn empresas_delete_response_includes_orphans_when_present() {
let r = TenantsDeleteResponse {
removed: false,
orphaned_agents: vec!["a-1".into(), "a-2".into()],
};
let v = to_value(&r).unwrap();
assert_eq!(v["removed"], json!(false));
assert_eq!(v["orphaned_agents"], json!(["a-1", "a-2"]));
}
#[test]
fn empresas_list_filter_default_serializes_compact() {
let f = TenantsListFilter::default();
let v = to_value(&f).unwrap();
assert_eq!(v, json!({ "active_only": false }));
}
#[test]
fn empresas_get_response_serializes_none_explicitly() {
let r = TenantsGetResponse { tenant: None };
let v = to_value(&r).unwrap();
assert_eq!(v, json!({ "tenant": null }));
}
#[test]
fn empresas_upsert_response_round_trips_created_flag() {
let r = TenantsUpsertResponse {
tenant: TenantDetail {
id: "x".into(),
display_name: "X".into(),
active: true,
created_at: fixed_ts(),
llm_provider_refs: vec![],
metadata: BTreeMap::new(),
},
created: true,
};
let v = to_value(&r).unwrap();
assert_eq!(v["created"], json!(true));
let back: TenantsUpsertResponse = from_value(v).unwrap();
assert_eq!(r, back);
}
}