wacloudapi/
flows.rs

1//! Flows API for WhatsApp Flows
2
3use crate::client::Client;
4use crate::error::Result;
5use crate::types::MessageResponse;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9/// Flows API client
10pub struct FlowsApi {
11    client: Client,
12}
13
14impl FlowsApi {
15    pub(crate) fn new(client: Client) -> Self {
16        Self { client }
17    }
18
19    /// Send a flow message
20    ///
21    /// # Arguments
22    ///
23    /// * `to` - Recipient's phone number
24    /// * `flow_token` - Flow token for the session
25    /// * `flow_id` - The flow ID
26    /// * `flow_cta` - Call to action button text
27    /// * `flow_action` - Flow action (navigate or data_exchange)
28    /// * `screen` - Initial screen name
29    /// * `data` - Optional flow data
30    /// * `header` - Optional header text
31    /// * `body_text` - Body text
32    /// * `footer` - Optional footer text
33    pub async fn send_flow(
34        &self,
35        to: &str,
36        flow_token: &str,
37        flow_id: &str,
38        flow_cta: &str,
39        flow_action: FlowAction,
40        screen: &str,
41        data: Option<Value>,
42        header: Option<&str>,
43        body_text: &str,
44        footer: Option<&str>,
45    ) -> Result<MessageResponse> {
46        let body = SendFlowRequest {
47            messaging_product: "whatsapp".to_string(),
48            recipient_type: "individual".to_string(),
49            to: to.to_string(),
50            message_type: "interactive".to_string(),
51            interactive: FlowInteractive {
52                interactive_type: "flow".to_string(),
53                header: header.map(|h| FlowHeader {
54                    header_type: "text".to_string(),
55                    text: h.to_string(),
56                }),
57                body: FlowBody {
58                    text: body_text.to_string(),
59                },
60                footer: footer.map(|f| FlowFooter {
61                    text: f.to_string(),
62                }),
63                action: FlowActionPayload {
64                    name: "flow".to_string(),
65                    parameters: FlowParameters {
66                        flow_message_version: "3".to_string(),
67                        flow_token: flow_token.to_string(),
68                        flow_id: flow_id.to_string(),
69                        flow_cta: flow_cta.to_string(),
70                        flow_action: flow_action.as_str().to_string(),
71                        flow_action_payload: FlowActionPayloadData {
72                            screen: screen.to_string(),
73                            data,
74                        },
75                    },
76                },
77            },
78        };
79
80        let url = format!("{}/messages", self.client.base_url());
81        self.client.post(&url, &body).await
82    }
83
84    /// List flows for the WABA
85    ///
86    /// # Arguments
87    ///
88    /// * `waba_id` - WhatsApp Business Account ID
89    pub async fn list_flows(&self, waba_id: &str) -> Result<FlowsListResponse> {
90        let url = self.client.endpoint_url(&format!("{}/flows", waba_id));
91        self.client.get(&url).await
92    }
93
94    /// Get flow details
95    ///
96    /// # Arguments
97    ///
98    /// * `flow_id` - The flow ID
99    pub async fn get_flow(&self, flow_id: &str) -> Result<Flow> {
100        let url = self.client.endpoint_url(flow_id);
101        self.client.get(&url).await
102    }
103
104    /// Create a new flow
105    ///
106    /// # Arguments
107    ///
108    /// * `waba_id` - WhatsApp Business Account ID
109    /// * `name` - Flow name
110    /// * `categories` - Flow categories
111    pub async fn create_flow(
112        &self,
113        waba_id: &str,
114        name: &str,
115        categories: Vec<FlowCategory>,
116    ) -> Result<CreateFlowResponse> {
117        let body = CreateFlowRequest {
118            name: name.to_string(),
119            categories: categories.iter().map(|c| c.as_str().to_string()).collect(),
120        };
121
122        let url = self.client.endpoint_url(&format!("{}/flows", waba_id));
123        self.client.post(&url, &body).await
124    }
125
126    /// Update flow JSON
127    ///
128    /// # Arguments
129    ///
130    /// * `flow_id` - The flow ID
131    /// * `flow_json` - The flow JSON content
132    pub async fn update_flow_json(
133        &self,
134        flow_id: &str,
135        flow_json: &str,
136    ) -> Result<UpdateFlowResponse> {
137        let form = reqwest::multipart::Form::new()
138            .text("name", "flow.json")
139            .text("file", flow_json.to_string());
140
141        let url = self.client.endpoint_url(&format!("{}/assets", flow_id));
142        self.client.post_form(&url, form).await
143    }
144
145    /// Publish a flow
146    ///
147    /// # Arguments
148    ///
149    /// * `flow_id` - The flow ID
150    pub async fn publish_flow(&self, flow_id: &str) -> Result<crate::types::SuccessResponse> {
151        let body = PublishFlowRequest {
152            status: "PUBLISHED".to_string(),
153        };
154
155        let url = self.client.endpoint_url(flow_id);
156        self.client.post(&url, &body).await
157    }
158
159    /// Delete a flow
160    ///
161    /// # Arguments
162    ///
163    /// * `flow_id` - The flow ID
164    pub async fn delete_flow(&self, flow_id: &str) -> Result<crate::types::SuccessResponse> {
165        let url = self.client.endpoint_url(flow_id);
166        self.client.delete(&url).await
167    }
168
169    /// Deprecate a flow
170    ///
171    /// # Arguments
172    ///
173    /// * `flow_id` - The flow ID
174    pub async fn deprecate_flow(&self, flow_id: &str) -> Result<crate::types::SuccessResponse> {
175        let body = DeprecateFlowRequest {
176            status: "DEPRECATED".to_string(),
177        };
178
179        let url = self.client.endpoint_url(flow_id);
180        self.client.post(&url, &body).await
181    }
182
183    /// Get flow preview URL
184    ///
185    /// # Arguments
186    ///
187    /// * `flow_id` - The flow ID
188    pub async fn get_preview(&self, flow_id: &str) -> Result<FlowPreviewResponse> {
189        let url = self.client.endpoint_url(&format!("{}/preview", flow_id));
190        self.client.get(&url).await
191    }
192}
193
194/// Flow action type
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196pub enum FlowAction {
197    /// Navigate to a screen
198    Navigate,
199    /// Exchange data
200    DataExchange,
201}
202
203impl FlowAction {
204    fn as_str(&self) -> &'static str {
205        match self {
206            FlowAction::Navigate => "navigate",
207            FlowAction::DataExchange => "data_exchange",
208        }
209    }
210}
211
212/// Flow category
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub enum FlowCategory {
215    SignUp,
216    SignIn,
217    Appointment,
218    LeadGeneration,
219    ContactUs,
220    CustomerSupport,
221    Survey,
222    Other,
223}
224
225impl FlowCategory {
226    fn as_str(&self) -> &'static str {
227        match self {
228            FlowCategory::SignUp => "SIGN_UP",
229            FlowCategory::SignIn => "SIGN_IN",
230            FlowCategory::Appointment => "APPOINTMENT_BOOKING",
231            FlowCategory::LeadGeneration => "LEAD_GENERATION",
232            FlowCategory::ContactUs => "CONTACT_US",
233            FlowCategory::CustomerSupport => "CUSTOMER_SUPPORT",
234            FlowCategory::Survey => "SURVEY",
235            FlowCategory::Other => "OTHER",
236        }
237    }
238}
239
240// Request types
241
242#[derive(Debug, Serialize)]
243struct SendFlowRequest {
244    messaging_product: String,
245    recipient_type: String,
246    to: String,
247    #[serde(rename = "type")]
248    message_type: String,
249    interactive: FlowInteractive,
250}
251
252#[derive(Debug, Serialize)]
253struct FlowInteractive {
254    #[serde(rename = "type")]
255    interactive_type: String,
256    #[serde(skip_serializing_if = "Option::is_none")]
257    header: Option<FlowHeader>,
258    body: FlowBody,
259    #[serde(skip_serializing_if = "Option::is_none")]
260    footer: Option<FlowFooter>,
261    action: FlowActionPayload,
262}
263
264#[derive(Debug, Serialize)]
265struct FlowHeader {
266    #[serde(rename = "type")]
267    header_type: String,
268    text: String,
269}
270
271#[derive(Debug, Serialize)]
272struct FlowBody {
273    text: String,
274}
275
276#[derive(Debug, Serialize)]
277struct FlowFooter {
278    text: String,
279}
280
281#[derive(Debug, Serialize)]
282struct FlowActionPayload {
283    name: String,
284    parameters: FlowParameters,
285}
286
287#[derive(Debug, Serialize)]
288struct FlowParameters {
289    flow_message_version: String,
290    flow_token: String,
291    flow_id: String,
292    flow_cta: String,
293    flow_action: String,
294    flow_action_payload: FlowActionPayloadData,
295}
296
297#[derive(Debug, Serialize)]
298struct FlowActionPayloadData {
299    screen: String,
300    #[serde(skip_serializing_if = "Option::is_none")]
301    data: Option<Value>,
302}
303
304#[derive(Debug, Serialize)]
305struct CreateFlowRequest {
306    name: String,
307    categories: Vec<String>,
308}
309
310#[derive(Debug, Serialize)]
311struct PublishFlowRequest {
312    status: String,
313}
314
315#[derive(Debug, Serialize)]
316struct DeprecateFlowRequest {
317    status: String,
318}
319
320// Response types
321
322/// Flow list response
323#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct FlowsListResponse {
325    /// List of flows
326    pub data: Vec<Flow>,
327    /// Paging info
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub paging: Option<Paging>,
330}
331
332/// Flow details
333#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct Flow {
335    /// Flow ID
336    pub id: String,
337    /// Flow name
338    pub name: String,
339    /// Flow status
340    pub status: String,
341    /// Flow categories
342    #[serde(default)]
343    pub categories: Vec<String>,
344    /// Validation errors
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub validation_errors: Option<Vec<FlowValidationError>>,
347    /// JSON version
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub json_version: Option<String>,
350    /// Data API version
351    #[serde(skip_serializing_if = "Option::is_none")]
352    pub data_api_version: Option<String>,
353    /// Endpoint URI
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub endpoint_uri: Option<String>,
356    /// Preview info
357    #[serde(skip_serializing_if = "Option::is_none")]
358    pub preview: Option<FlowPreview>,
359}
360
361/// Flow validation error
362#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct FlowValidationError {
364    /// Error message
365    pub error: String,
366    /// Error type
367    pub error_type: String,
368    /// Line start
369    #[serde(skip_serializing_if = "Option::is_none")]
370    pub line_start: Option<i32>,
371    /// Line end
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub line_end: Option<i32>,
374    /// Column start
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub column_start: Option<i32>,
377    /// Column end
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub column_end: Option<i32>,
380}
381
382/// Flow preview
383#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct FlowPreview {
385    /// Preview URL
386    pub preview_url: String,
387    /// Expires at
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub expires_at: Option<String>,
390}
391
392/// Create flow response
393#[derive(Debug, Clone, Serialize, Deserialize)]
394pub struct CreateFlowResponse {
395    /// Created flow ID
396    pub id: String,
397}
398
399/// Update flow response
400#[derive(Debug, Clone, Serialize, Deserialize)]
401pub struct UpdateFlowResponse {
402    /// Success status
403    pub success: bool,
404    /// Validation errors
405    #[serde(skip_serializing_if = "Option::is_none")]
406    pub validation_errors: Option<Vec<FlowValidationError>>,
407}
408
409/// Flow preview response
410#[derive(Debug, Clone, Serialize, Deserialize)]
411pub struct FlowPreviewResponse {
412    /// Preview URL
413    pub preview_url: String,
414    /// Expires at
415    #[serde(skip_serializing_if = "Option::is_none")]
416    pub expires_at: Option<String>,
417}
418
419/// Paging info
420#[derive(Debug, Clone, Serialize, Deserialize)]
421pub struct Paging {
422    /// Cursors
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub cursors: Option<PagingCursors>,
425}
426
427/// Paging cursors
428#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct PagingCursors {
430    /// Before cursor
431    #[serde(skip_serializing_if = "Option::is_none")]
432    pub before: Option<String>,
433    /// After cursor
434    #[serde(skip_serializing_if = "Option::is_none")]
435    pub after: Option<String>,
436}