Skip to main content

vortex_sdk/
types.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4// ============================================================================
5// Enums for type-safe API values
6// ============================================================================
7
8/// Target type for invitation responses (who was invited)
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum InvitationTargetType {
12    Email,
13    Phone,
14    Share,
15    Internal,
16}
17
18/// Target type for creating invitations
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum CreateInvitationTargetType {
22    Email,
23    Phone,
24    Internal,
25}
26
27/// Type of invitation
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum InvitationType {
31    SingleUse,
32    MultiUse,
33    Autojoin,
34}
35
36/// Current status of an invitation
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum InvitationStatus {
40    Queued,
41    Sending,
42    Sent,
43    Delivered,
44    Accepted,
45    Shared,
46    Unfurled,
47    AcceptedElsewhere,
48}
49
50/// Delivery type for invitations
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum DeliveryType {
54    Email,
55    Phone,
56    Share,
57    Internal,
58}
59
60// ============================================================================
61// Core types
62// ============================================================================
63
64/// User type for JWT generation
65/// Optional fields: user_name (max 200 chars), user_avatar_url (HTTPS URL, max 2000 chars), admin_scopes
66#[derive(Debug, Clone, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct User {
69    pub id: String,
70    pub email: String,
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub user_name: Option<String>,
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub user_avatar_url: Option<String>,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub admin_scopes: Option<Vec<String>>,
77}
78
79impl User {
80    pub fn new(id: &str, email: &str) -> Self {
81        Self {
82            id: id.to_string(),
83            email: email.to_string(),
84            user_name: None,
85            user_avatar_url: None,
86            admin_scopes: None,
87        }
88    }
89
90    pub fn with_user_name(mut self, name: &str) -> Self {
91        self.user_name = Some(name.to_string());
92        self
93    }
94
95    pub fn with_user_avatar_url(mut self, avatar_url: &str) -> Self {
96        self.user_avatar_url = Some(avatar_url.to_string());
97        self
98    }
99
100    pub fn with_admin_scopes(mut self, scopes: Vec<String>) -> Self {
101        self.admin_scopes = Some(scopes);
102        self
103    }
104}
105
106/// Identifier for a user (email, sms, etc.)
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct Identifier {
109    #[serde(rename = "type")]
110    pub identifier_type: String,
111    pub value: String,
112}
113
114impl Identifier {
115    pub fn new(identifier_type: &str, value: &str) -> Self {
116        Self {
117            identifier_type: identifier_type.to_string(),
118            value: value.to_string(),
119        }
120    }
121}
122
123/// Group information for JWT generation (input)
124/// Supports both 'id' (legacy) and 'groupId' (preferred) for backward compatibility
125#[derive(Debug, Clone, Serialize, Deserialize)]
126#[serde(rename_all = "camelCase")]
127pub struct Group {
128    #[serde(rename = "type")]
129    pub group_type: String,
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub id: Option<String>,
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub group_id: Option<String>,
134    pub name: String,
135}
136
137impl Group {
138    pub fn new(group_type: &str, name: &str) -> Self {
139        Self {
140            group_type: group_type.to_string(),
141            id: None,
142            group_id: None,
143            name: name.to_string(),
144        }
145    }
146
147    pub fn with_id(mut self, id: &str) -> Self {
148        self.id = Some(id.to_string());
149        self
150    }
151
152    pub fn with_group_id(mut self, group_id: &str) -> Self {
153        self.group_id = Some(group_id.to_string());
154        self
155    }
156}
157
158/// Invitation group from API responses
159/// This matches the MemberGroups table structure from the API
160#[derive(Debug, Clone, Serialize, Deserialize)]
161#[serde(rename_all = "camelCase")]
162pub struct InvitationGroup {
163    /// Vortex internal UUID
164    pub id: String,
165    /// Vortex account ID
166    pub account_id: String,
167    /// Customer's group ID (the ID they provided to Vortex)
168    pub group_id: String,
169    /// Group type (e.g., "workspace", "team")
170    #[serde(rename = "type")]
171    pub group_type: String,
172    /// Group name
173    pub name: String,
174    /// ISO 8601 timestamp when the group was created
175    pub created_at: String,
176}
177
178/// Invitation target from API responses
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct InvitationTarget {
181    #[serde(rename = "type")]
182    pub target_type: InvitationTargetType,
183    pub value: String,
184    /// Display name of the person being invited
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub name: Option<String>,
187    /// Avatar URL for the person being invited (for display in invitation lists)
188    #[serde(skip_serializing_if = "Option::is_none")]
189    #[serde(rename = "avatarUrl")]
190    pub avatar_url: Option<String>,
191}
192
193impl InvitationTarget {
194    pub fn new(target_type: InvitationTargetType, value: &str) -> Self {
195        Self {
196            target_type,
197            value: value.to_string(),
198            name: None,
199            avatar_url: None,
200        }
201    }
202
203    pub fn email(value: &str) -> Self {
204        Self::new(InvitationTargetType::Email, value)
205    }
206
207    pub fn phone(value: &str) -> Self {
208        Self::new(InvitationTargetType::Phone, value)
209    }
210
211    pub fn with_name(mut self, name: &str) -> Self {
212        self.name = Some(name.to_string());
213        self
214    }
215
216    pub fn with_avatar_url(mut self, avatar_url: &str) -> Self {
217        self.avatar_url = Some(avatar_url.to_string());
218        self
219    }
220}
221
222/// User data for accepting invitations (preferred format)
223///
224/// At least one of email or phone must be provided.
225///
226/// # Example
227///
228/// ```
229/// use vortex_sdk::AcceptUser;
230///
231/// // With email only
232/// let user = AcceptUser::new().with_email("user@example.com");
233///
234/// // With email and name
235/// let user = AcceptUser::new()
236///     .with_email("user@example.com")
237///     .with_name("John Doe");
238///
239/// // With all fields
240/// let user = AcceptUser::new()
241///     .with_email("user@example.com")
242///     .with_phone("+1234567890")
243///     .with_name("John Doe");
244/// ```
245#[derive(Debug, Clone, Serialize, Deserialize, Default)]
246pub struct AcceptUser {
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub email: Option<String>,
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub phone: Option<String>,
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub name: Option<String>,
253}
254
255impl AcceptUser {
256    pub fn new() -> Self {
257        Self::default()
258    }
259
260    pub fn with_email(mut self, email: &str) -> Self {
261        self.email = Some(email.to_string());
262        self
263    }
264
265    pub fn with_phone(mut self, phone: &str) -> Self {
266        self.phone = Some(phone.to_string());
267        self
268    }
269
270    pub fn with_name(mut self, name: &str) -> Self {
271        self.name = Some(name.to_string());
272        self
273    }
274}
275
276/// Invitation acceptance information
277#[derive(Debug, Clone, Serialize, Deserialize)]
278#[serde(rename_all = "camelCase")]
279pub struct InvitationAcceptance {
280    pub id: Option<String>,
281    pub account_id: Option<String>,
282    pub project_id: Option<String>,
283    pub accepted_at: Option<String>,
284    pub target: Option<InvitationTarget>,
285}
286
287/// Full invitation details
288#[derive(Debug, Clone, Serialize, Deserialize)]
289#[serde(rename_all = "camelCase")]
290pub struct Invitation {
291    #[serde(default)]
292    pub id: String,
293    #[serde(default)]
294    pub account_id: String,
295    #[serde(default)]
296    pub click_throughs: u32,
297    pub configuration_attributes: Option<HashMap<String, serde_json::Value>>,
298    pub attributes: Option<HashMap<String, serde_json::Value>>,
299    #[serde(default)]
300    pub created_at: String,
301    #[serde(default)]
302    pub deactivated: bool,
303    #[serde(default)]
304    pub delivery_count: u32,
305    #[serde(default)]
306    pub delivery_types: Vec<DeliveryType>,
307    #[serde(default)]
308    pub foreign_creator_id: String,
309    pub invitation_type: InvitationType,
310    pub modified_at: Option<String>,
311    pub status: InvitationStatus,
312    #[serde(default)]
313    pub target: Vec<InvitationTarget>,
314    #[serde(default)]
315    pub views: u32,
316    #[serde(default)]
317    pub widget_configuration_id: String,
318    #[serde(default)]
319    pub project_id: String,
320    #[serde(default)]
321    pub groups: Vec<InvitationGroup>,
322    #[serde(default)]
323    pub accepts: Vec<InvitationAcceptance>,
324    pub expired: bool,
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub expires: Option<String>,
327    #[serde(skip_serializing_if = "Option::is_none")]
328    pub source: Option<String>,
329    /// Customer-defined subtype for analytics segmentation (e.g., "pymk", "find-friends")
330    #[serde(skip_serializing_if = "Option::is_none")]
331    pub subtype: Option<String>,
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub creator_name: Option<String>,
334    #[serde(skip_serializing_if = "Option::is_none")]
335    pub creator_avatar_url: Option<String>,
336}
337
338/// Response containing multiple invitations
339#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct InvitationsResponse {
341    pub invitations: Option<Vec<Invitation>>,
342}
343
344/// Accept invitation parameter - supports both new User format and legacy Target format
345#[derive(Debug, Clone)]
346pub enum AcceptInvitationParam {
347    /// New User format (preferred)
348    User(AcceptUser),
349    /// Legacy target format (deprecated)
350    Target(InvitationTarget),
351    /// Legacy multiple targets format (deprecated)
352    Targets(Vec<InvitationTarget>),
353}
354
355impl From<AcceptUser> for AcceptInvitationParam {
356    fn from(user: AcceptUser) -> Self {
357        AcceptInvitationParam::User(user)
358    }
359}
360
361impl From<InvitationTarget> for AcceptInvitationParam {
362    fn from(target: InvitationTarget) -> Self {
363        AcceptInvitationParam::Target(target)
364    }
365}
366
367impl From<Vec<InvitationTarget>> for AcceptInvitationParam {
368    fn from(targets: Vec<InvitationTarget>) -> Self {
369        AcceptInvitationParam::Targets(targets)
370    }
371}
372
373// --- Types for creating invitations via backend API ---
374
375/// Target for creating an invitation
376#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct CreateInvitationTarget {
378    #[serde(rename = "type")]
379    pub target_type: CreateInvitationTargetType,
380    /// Target value: email address, phone number, or internal user ID
381    pub value: String,
382    /// Display name of the person being invited
383    #[serde(skip_serializing_if = "Option::is_none")]
384    pub name: Option<String>,
385    /// Avatar URL for the person being invited (for display in invitation lists)
386    #[serde(skip_serializing_if = "Option::is_none")]
387    #[serde(rename = "avatarUrl")]
388    pub avatar_url: Option<String>,
389}
390
391impl CreateInvitationTarget {
392    pub fn new(target_type: CreateInvitationTargetType, value: &str) -> Self {
393        Self {
394            target_type,
395            value: value.to_string(),
396            name: None,
397            avatar_url: None,
398        }
399    }
400
401    pub fn email(value: &str) -> Self {
402        Self::new(CreateInvitationTargetType::Email, value)
403    }
404
405    pub fn phone(value: &str) -> Self {
406        Self::new(CreateInvitationTargetType::Phone, value)
407    }
408
409    /// Alias for phone (backward compatibility)
410    pub fn sms(value: &str) -> Self {
411        Self::phone(value)
412    }
413
414    pub fn internal(value: &str) -> Self {
415        Self::new(CreateInvitationTargetType::Internal, value)
416    }
417
418    pub fn with_name(mut self, name: &str) -> Self {
419        self.name = Some(name.to_string());
420        self
421    }
422
423    pub fn with_avatar_url(mut self, avatar_url: &str) -> Self {
424        self.avatar_url = Some(avatar_url.to_string());
425        self
426    }
427}
428
429/// Information about the user creating the invitation (the inviter)
430#[derive(Debug, Clone, Serialize, Deserialize)]
431#[serde(rename_all = "camelCase")]
432pub struct Inviter {
433    /// Required: Your internal user ID for the inviter
434    pub user_id: String,
435    /// Optional: Email of the inviter
436    #[serde(skip_serializing_if = "Option::is_none")]
437    pub user_email: Option<String>,
438    /// Optional: Display name of the inviter
439    #[serde(skip_serializing_if = "Option::is_none")]
440    pub user_name: Option<String>,
441    /// Optional: Avatar URL of the inviter
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub user_avatar_url: Option<String>,
444}
445
446impl Inviter {
447    pub fn new(user_id: &str) -> Self {
448        Self {
449            user_id: user_id.to_string(),
450            user_email: None,
451            user_name: None,
452            user_avatar_url: None,
453        }
454    }
455
456    pub fn with_email(mut self, email: &str) -> Self {
457        self.user_email = Some(email.to_string());
458        self
459    }
460
461    pub fn with_user_name(mut self, name: &str) -> Self {
462        self.user_name = Some(name.to_string());
463        self
464    }
465
466    pub fn with_user_avatar_url(mut self, url: &str) -> Self {
467        self.user_avatar_url = Some(url.to_string());
468        self
469    }
470}
471
472/// Group information for creating invitations
473#[derive(Debug, Clone, Serialize, Deserialize)]
474#[serde(rename_all = "camelCase")]
475pub struct CreateInvitationGroup {
476    /// Group type (e.g., "team", "organization")
477    #[serde(rename = "type")]
478    pub group_type: String,
479    /// Your internal group ID
480    pub group_id: String,
481    /// Display name of the group
482    pub name: String,
483}
484
485impl CreateInvitationGroup {
486    pub fn new(group_type: &str, group_id: &str, name: &str) -> Self {
487        Self {
488            group_type: group_type.to_string(),
489            group_id: group_id.to_string(),
490            name: name.to_string(),
491        }
492    }
493}
494
495/// Valid Open Graph types for unfurl configuration
496#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
497#[serde(rename_all = "snake_case")]
498pub enum UnfurlOgType {
499    Website,
500    Article,
501    Video,
502    Music,
503    Book,
504    Profile,
505    Product,
506}
507
508/// Configuration for link unfurl (Open Graph) metadata
509/// Controls how the invitation link appears when shared on social platforms or messaging apps
510#[derive(Debug, Clone, Serialize, Deserialize, Default)]
511#[serde(rename_all = "camelCase")]
512pub struct UnfurlConfig {
513    /// The title shown in link previews (og:title)
514    #[serde(skip_serializing_if = "Option::is_none")]
515    pub title: Option<String>,
516    /// The description shown in link previews (og:description)
517    #[serde(skip_serializing_if = "Option::is_none")]
518    pub description: Option<String>,
519    /// The image URL shown in link previews (og:image) - must be HTTPS
520    #[serde(skip_serializing_if = "Option::is_none")]
521    pub image: Option<String>,
522    /// The Open Graph type (og:type)
523    #[serde(skip_serializing_if = "Option::is_none", rename = "type")]
524    pub og_type: Option<UnfurlOgType>,
525    /// The site name shown in link previews (og:site_name)
526    #[serde(skip_serializing_if = "Option::is_none")]
527    pub site_name: Option<String>,
528}
529
530impl UnfurlConfig {
531    pub fn new() -> Self {
532        Self::default()
533    }
534
535    pub fn with_title(mut self, title: &str) -> Self {
536        self.title = Some(title.to_string());
537        self
538    }
539
540    pub fn with_description(mut self, description: &str) -> Self {
541        self.description = Some(description.to_string());
542        self
543    }
544
545    pub fn with_image(mut self, image: &str) -> Self {
546        self.image = Some(image.to_string());
547        self
548    }
549
550    pub fn with_type(mut self, og_type: UnfurlOgType) -> Self {
551        self.og_type = Some(og_type);
552        self
553    }
554
555    pub fn with_site_name(mut self, site_name: &str) -> Self {
556        self.site_name = Some(site_name.to_string());
557        self
558    }
559}
560
561/// Request body for creating an invitation
562#[derive(Debug, Clone, Serialize, Deserialize)]
563#[serde(rename_all = "camelCase")]
564pub struct CreateInvitationRequest {
565    pub widget_configuration_id: String,
566    pub target: CreateInvitationTarget,
567    pub inviter: Inviter,
568    #[serde(skip_serializing_if = "Option::is_none")]
569    pub groups: Option<Vec<CreateInvitationGroup>>,
570    #[serde(skip_serializing_if = "Option::is_none")]
571    pub source: Option<String>,
572    /// Customer-defined subtype for analytics segmentation (e.g., "pymk", "find-friends")
573    #[serde(skip_serializing_if = "Option::is_none")]
574    pub subtype: Option<String>,
575    #[serde(skip_serializing_if = "Option::is_none")]
576    pub template_variables: Option<HashMap<String, String>>,
577    #[serde(skip_serializing_if = "Option::is_none")]
578    pub metadata: Option<HashMap<String, serde_json::Value>>,
579    #[serde(skip_serializing_if = "Option::is_none")]
580    pub unfurl_config: Option<UnfurlConfig>,
581}
582
583impl CreateInvitationRequest {
584    pub fn new(
585        widget_configuration_id: &str,
586        target: CreateInvitationTarget,
587        inviter: Inviter,
588    ) -> Self {
589        Self {
590            widget_configuration_id: widget_configuration_id.to_string(),
591            target,
592            inviter,
593            groups: None,
594            source: None,
595            subtype: None,
596            template_variables: None,
597            metadata: None,
598            unfurl_config: None,
599        }
600    }
601
602    pub fn with_groups(mut self, groups: Vec<CreateInvitationGroup>) -> Self {
603        self.groups = Some(groups);
604        self
605    }
606
607    pub fn with_source(mut self, source: &str) -> Self {
608        self.source = Some(source.to_string());
609        self
610    }
611
612    pub fn with_subtype(mut self, subtype: &str) -> Self {
613        self.subtype = Some(subtype.to_string());
614        self
615    }
616
617    pub fn with_template_variables(mut self, vars: HashMap<String, String>) -> Self {
618        self.template_variables = Some(vars);
619        self
620    }
621
622    pub fn with_metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
623        self.metadata = Some(metadata);
624        self
625    }
626
627    pub fn with_unfurl_config(mut self, unfurl_config: UnfurlConfig) -> Self {
628        self.unfurl_config = Some(unfurl_config);
629        self
630    }
631}
632
633/// Response from creating an invitation
634#[derive(Debug, Clone, Serialize, Deserialize)]
635#[serde(rename_all = "camelCase")]
636pub struct CreateInvitationResponse {
637    /// The ID of the created invitation
638    pub id: String,
639    /// The short link for the invitation
640    pub short_link: String,
641    /// The status of the invitation
642    pub status: String,
643    /// When the invitation was created
644    pub created_at: String,
645}