claude_api/admin/
invites.rs1use serde::{Deserialize, Serialize};
4
5use crate::client::Client;
6use crate::error::Result;
7use crate::pagination::Paginated;
8
9use super::{InviteStatus, ListParams, OrganizationRole, WriteOrganizationRole};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13#[non_exhaustive]
14pub struct Invite {
15 pub id: String,
17 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
19 pub ty: Option<String>,
20 pub email: String,
22 pub role: OrganizationRole,
24 pub status: InviteStatus,
26 pub invited_at: String,
28 pub expires_at: String,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34#[non_exhaustive]
35pub struct InviteDeleted {
36 pub id: String,
38 #[serde(rename = "type")]
40 pub ty: String,
41}
42
43#[derive(Debug, Clone, Serialize)]
45#[non_exhaustive]
46pub struct CreateInviteRequest {
47 pub email: String,
49 pub role: WriteOrganizationRole,
51}
52
53impl CreateInviteRequest {
54 #[must_use]
56 pub fn new(email: impl Into<String>, role: WriteOrganizationRole) -> Self {
57 Self {
58 email: email.into(),
59 role,
60 }
61 }
62}
63
64pub struct Invites<'a> {
66 client: &'a Client,
67}
68
69impl<'a> Invites<'a> {
70 pub(crate) fn new(client: &'a Client) -> Self {
71 Self { client }
72 }
73
74 pub async fn create(&self, request: CreateInviteRequest) -> Result<Invite> {
76 let body = &request;
77 self.client
78 .execute_with_retry(
79 || {
80 self.client
81 .request_builder(reqwest::Method::POST, "/v1/organizations/invites")
82 .json(body)
83 },
84 &[],
85 )
86 .await
87 }
88
89 pub async fn retrieve(&self, invite_id: &str) -> Result<Invite> {
91 let path = format!("/v1/organizations/invites/{invite_id}");
92 self.client
93 .execute_with_retry(
94 || self.client.request_builder(reqwest::Method::GET, &path),
95 &[],
96 )
97 .await
98 }
99
100 pub async fn list(&self, params: ListParams) -> Result<Paginated<Invite>> {
102 let query = params.to_query();
103 self.client
104 .execute_with_retry(
105 || {
106 let mut req = self
107 .client
108 .request_builder(reqwest::Method::GET, "/v1/organizations/invites");
109 for (k, v) in &query {
110 req = req.query(&[(k, v)]);
111 }
112 req
113 },
114 &[],
115 )
116 .await
117 }
118
119 pub async fn delete(&self, invite_id: &str) -> Result<InviteDeleted> {
121 let path = format!("/v1/organizations/invites/{invite_id}");
122 self.client
123 .execute_with_retry(
124 || self.client.request_builder(reqwest::Method::DELETE, &path),
125 &[],
126 )
127 .await
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use pretty_assertions::assert_eq;
135 use serde_json::json;
136 use wiremock::matchers::{body_partial_json, method, path};
137 use wiremock::{Mock, MockServer, ResponseTemplate};
138
139 fn client_for(mock: &MockServer) -> Client {
140 Client::builder()
141 .api_key("sk-ant-admin-test")
142 .base_url(mock.uri())
143 .build()
144 .unwrap()
145 }
146
147 fn fake_invite() -> serde_json::Value {
148 json!({
149 "id": "invite_01",
150 "type": "invite",
151 "email": "u@example.com",
152 "role": "user",
153 "status": "pending",
154 "invited_at": "2026-05-01T00:00:00Z",
155 "expires_at": "2026-05-08T00:00:00Z"
156 })
157 }
158
159 #[test]
160 fn organization_role_unknown_falls_through_to_other() {
161 let raw = json!("future_role");
162 let r: OrganizationRole = serde_json::from_value(raw).unwrap();
163 assert_eq!(r, OrganizationRole::Other("future_role".into()));
164 }
165
166 #[test]
167 fn organization_role_round_trips_known_variants() {
168 for v in ["user", "developer", "billing", "admin", "claude_code_user"] {
169 let r: OrganizationRole = serde_json::from_value(json!(v)).unwrap();
170 let s = serde_json::to_value(&r).unwrap();
171 assert_eq!(s, json!(v));
172 }
173 }
174
175 #[tokio::test]
176 async fn create_invite_posts_email_and_role() {
177 let mock = MockServer::start().await;
178 Mock::given(method("POST"))
179 .and(path("/v1/organizations/invites"))
180 .and(body_partial_json(json!({
181 "email": "u@example.com",
182 "role": "developer"
183 })))
184 .respond_with(ResponseTemplate::new(200).set_body_json(fake_invite()))
185 .mount(&mock)
186 .await;
187 let client = client_for(&mock);
188 let inv = client
189 .admin()
190 .invites()
191 .create(CreateInviteRequest::new(
192 "u@example.com",
193 WriteOrganizationRole::Developer,
194 ))
195 .await
196 .unwrap();
197 assert_eq!(inv.id, "invite_01");
198 }
199
200 #[tokio::test]
201 async fn retrieve_invite_returns_typed_record() {
202 let mock = MockServer::start().await;
203 Mock::given(method("GET"))
204 .and(path("/v1/organizations/invites/invite_01"))
205 .respond_with(ResponseTemplate::new(200).set_body_json(fake_invite()))
206 .mount(&mock)
207 .await;
208 let client = client_for(&mock);
209 let inv = client
210 .admin()
211 .invites()
212 .retrieve("invite_01")
213 .await
214 .unwrap();
215 assert_eq!(inv.email, "u@example.com");
216 }
217
218 #[tokio::test]
219 async fn list_invites_passes_pagination_query() {
220 let mock = MockServer::start().await;
221 Mock::given(method("GET"))
222 .and(path("/v1/organizations/invites"))
223 .and(wiremock::matchers::query_param("limit", "50"))
224 .and(wiremock::matchers::query_param("after_id", "invite_x"))
225 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
226 "data": [fake_invite()],
227 "has_more": false,
228 "first_id": "invite_01",
229 "last_id": "invite_01"
230 })))
231 .mount(&mock)
232 .await;
233 let client = client_for(&mock);
234 let page = client
235 .admin()
236 .invites()
237 .list(ListParams {
238 after_id: Some("invite_x".into()),
239 limit: Some(50),
240 ..Default::default()
241 })
242 .await
243 .unwrap();
244 assert_eq!(page.data.len(), 1);
245 }
246
247 #[tokio::test]
248 async fn delete_invite_returns_deleted_response() {
249 let mock = MockServer::start().await;
250 Mock::given(method("DELETE"))
251 .and(path("/v1/organizations/invites/invite_01"))
252 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
253 "id": "invite_01",
254 "type": "invite_deleted"
255 })))
256 .mount(&mock)
257 .await;
258 let client = client_for(&mock);
259 let r = client.admin().invites().delete("invite_01").await.unwrap();
260 assert_eq!(r.ty, "invite_deleted");
261 }
262}