concierge_worker/
types.rs

1use serde::{Deserialize, Serialize};
2
3// ============================================================================
4// Tenant Types
5// ============================================================================
6
7#[derive(Serialize, Deserialize, Clone, Debug)]
8pub struct Tenant {
9    pub id: String,
10    pub email: String,
11    pub name: Option<String>,
12    #[serde(default)]
13    pub facebook_id: Option<String>,
14    pub plan: String,
15    pub created_at: String,
16    pub updated_at: String,
17}
18
19#[derive(Serialize, Deserialize, Clone, Debug, Default)]
20pub struct TenantCredentials {}
21
22// ============================================================================
23// WhatsApp Account Resource
24// ============================================================================
25
26#[derive(Serialize, Deserialize, Clone, Debug)]
27pub struct WhatsAppAccount {
28    pub id: String,
29    pub tenant_id: String,
30    pub name: String,
31    pub phone_number: String,
32    pub phone_number_id: String,
33    pub auto_reply: AutoReplyConfig,
34    pub created_at: String,
35    pub updated_at: String,
36}
37
38#[derive(Serialize, Deserialize, Clone, Debug, Default)]
39pub struct AutoReplyConfig {
40    pub enabled: bool,
41    pub mode: AutoReplyMode,
42    pub prompt: String,
43}
44
45#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
46#[serde(rename_all = "snake_case")]
47pub enum AutoReplyMode {
48    #[default]
49    Static,
50    Ai,
51}
52
53// ============================================================================
54// Instagram Account Resource
55// ============================================================================
56
57#[derive(Serialize, Deserialize, Clone, Debug)]
58pub struct InstagramAccount {
59    pub id: String,
60    pub tenant_id: String,
61    pub instagram_user_id: String,
62    pub instagram_username: String,
63    pub page_id: String,
64    pub auto_reply: AutoReplyConfig,
65    pub enabled: bool,
66    pub created_at: String,
67    #[serde(default)]
68    pub updated_at: String,
69}
70
71// ============================================================================
72// Lead Capture Form
73// ============================================================================
74
75#[derive(Serialize, Deserialize, Clone, Debug)]
76pub struct LeadCaptureForm {
77    pub id: String,
78    pub tenant_id: String,
79    pub name: String,
80    pub slug: String,
81    pub whatsapp_account_id: String,
82    pub reply_mode: AutoReplyMode,
83    pub reply_prompt: String,
84    pub style: LeadFormStyle,
85    pub allowed_origins: Vec<String>,
86    pub enabled: bool,
87    pub created_at: String,
88    pub updated_at: String,
89}
90
91#[derive(Serialize, Deserialize, Clone, Debug)]
92pub struct LeadFormStyle {
93    pub primary_color: String,
94    pub text_color: String,
95    pub background_color: String,
96    pub border_radius: String,
97    pub button_text: String,
98    pub placeholder_text: String,
99    pub success_message: String,
100    #[serde(default)]
101    pub custom_css: String,
102}
103
104impl Default for LeadFormStyle {
105    fn default() -> Self {
106        Self {
107            primary_color: String::from("#F38020"),
108            text_color: String::from("#333333"),
109            background_color: String::from("#ffffff"),
110            border_radius: String::from("8px"),
111            button_text: String::from("Get in touch"),
112            placeholder_text: String::from("Your phone number"),
113            success_message: String::from("Thanks! We'll message you on WhatsApp shortly."),
114            custom_css: String::new(),
115        }
116    }
117}
118
119// ============================================================================
120// Instagram Token
121// ============================================================================
122
123#[derive(Serialize, Deserialize, Clone, Debug)]
124pub struct InstagramToken {
125    pub access_token: String,
126    pub expires_at: String,
127    pub user_id: String,
128}
129
130// ============================================================================
131// WhatsApp Webhook Types
132// ============================================================================
133
134#[derive(Debug, Deserialize)]
135pub struct WhatsAppWebhook {
136    pub object: String,
137    #[serde(default)]
138    pub entry: Vec<WebhookEntry>,
139}
140
141#[derive(Debug, Deserialize)]
142pub struct WebhookEntry {
143    pub id: String,
144    #[serde(default)]
145    pub changes: Vec<WebhookChange>,
146}
147
148#[derive(Debug, Deserialize)]
149pub struct WebhookChange {
150    pub field: String,
151    pub value: WebhookValue,
152}
153
154#[derive(Debug, Deserialize)]
155pub struct WebhookValue {
156    pub messaging_product: String,
157    pub metadata: WebhookMetadata,
158    #[serde(default)]
159    pub contacts: Vec<WebhookContact>,
160    #[serde(default)]
161    pub messages: Vec<WhatsAppMessage>,
162}
163
164#[derive(Debug, Deserialize)]
165pub struct WebhookMetadata {
166    pub display_phone_number: String,
167    pub phone_number_id: String,
168}
169
170#[derive(Debug, Deserialize)]
171pub struct WebhookContact {
172    pub wa_id: String,
173    pub profile: ContactProfile,
174}
175
176#[derive(Debug, Deserialize)]
177pub struct ContactProfile {
178    pub name: String,
179}
180
181#[derive(Debug, Deserialize)]
182pub struct WhatsAppMessage {
183    pub from: String,
184    pub id: String,
185    pub timestamp: String,
186    #[serde(rename = "type")]
187    pub message_type: String,
188    #[serde(default)]
189    pub text: Option<TextMessage>,
190}
191
192#[derive(Debug, Deserialize)]
193pub struct TextMessage {
194    pub body: String,
195}
196
197#[derive(Debug, Clone)]
198pub struct IncomingMessage {
199    pub from: String,
200    pub sender_name: String,
201    pub text: String,
202    pub message_id: String,
203    pub timestamp: String,
204}
205
206// ============================================================================
207// Instagram DM Webhook Types
208// ============================================================================
209
210#[derive(Debug, Deserialize)]
211pub struct InstagramWebhookPayload {
212    pub object: String,
213    #[serde(default)]
214    pub entry: Vec<InstagramWebhookEntry>,
215}
216
217#[derive(Debug, Deserialize)]
218pub struct InstagramWebhookEntry {
219    pub id: String,
220    #[serde(default)]
221    pub time: i64,
222    #[serde(default)]
223    pub messaging: Vec<InstagramMessaging>,
224}
225
226#[derive(Debug, Deserialize)]
227pub struct InstagramMessaging {
228    pub sender: IdField,
229    pub recipient: IdField,
230    #[serde(default)]
231    pub timestamp: i64,
232    #[serde(default)]
233    pub message: Option<InstagramDm>,
234}
235
236#[derive(Debug, Deserialize)]
237pub struct IdField {
238    pub id: String,
239}
240
241#[derive(Debug, Deserialize)]
242pub struct InstagramDm {
243    pub mid: String,
244    #[serde(default)]
245    pub text: Option<String>,
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_auto_reply_mode_serialization() {
254        assert_eq!(
255            serde_json::to_string(&AutoReplyMode::Static).unwrap(),
256            "\"static\""
257        );
258        assert_eq!(serde_json::to_string(&AutoReplyMode::Ai).unwrap(), "\"ai\"");
259        let mode: AutoReplyMode = serde_json::from_str("\"ai\"").unwrap();
260        assert_eq!(mode, AutoReplyMode::Ai);
261    }
262
263    #[test]
264    fn test_lead_form_style_default() {
265        let style = LeadFormStyle::default();
266        assert_eq!(style.primary_color, "#F38020");
267        assert_eq!(style.button_text, "Get in touch");
268        assert!(style.custom_css.is_empty());
269    }
270
271    #[test]
272    fn test_whatsapp_webhook_deserialization() {
273        let json = r#"{
274            "object": "whatsapp_business_account",
275            "entry": [{
276                "id": "123456789",
277                "changes": [{
278                    "field": "messages",
279                    "value": {
280                        "messaging_product": "whatsapp",
281                        "metadata": {
282                            "display_phone_number": "+1234567890",
283                            "phone_number_id": "phone-123"
284                        },
285                        "contacts": [{
286                            "wa_id": "user123",
287                            "profile": {"name": "Test User"}
288                        }],
289                        "messages": [{
290                            "from": "user123",
291                            "id": "msg-123",
292                            "timestamp": "1234567890",
293                            "type": "text",
294                            "text": {"body": "Hello!"}
295                        }]
296                    }
297                }]
298            }]
299        }"#;
300
301        let webhook: WhatsAppWebhook = serde_json::from_str(json).unwrap();
302        assert_eq!(webhook.object, "whatsapp_business_account");
303        assert_eq!(
304            webhook.entry[0].changes[0].value.messages[0].from,
305            "user123"
306        );
307    }
308
309    #[test]
310    fn test_instagram_webhook_deserialization() {
311        let json = r#"{
312            "object": "instagram",
313            "entry": [{
314                "id": "page-123",
315                "time": 1700000000,
316                "messaging": [{
317                    "sender": {"id": "sender-456"},
318                    "recipient": {"id": "page-123"},
319                    "timestamp": 1700000000,
320                    "message": {
321                        "mid": "mid-789",
322                        "text": "Hello!"
323                    }
324                }]
325            }]
326        }"#;
327
328        let payload: InstagramWebhookPayload = serde_json::from_str(json).unwrap();
329        assert_eq!(payload.object, "instagram");
330        assert_eq!(payload.entry[0].messaging[0].sender.id, "sender-456");
331        assert_eq!(
332            payload.entry[0].messaging[0]
333                .message
334                .as_ref()
335                .unwrap()
336                .text
337                .as_deref(),
338            Some("Hello!")
339        );
340    }
341}