Skip to main content

ferro_whatsapp/
message.rs

1use serde::{Deserialize, Serialize};
2
3/// Outbound WhatsApp message variant.
4///
5/// Build a message and pass it to [`WhatsApp::send`](crate::WhatsApp::send).
6#[derive(Debug, Clone)]
7pub enum Message {
8    /// Plain text message.
9    Text {
10        /// Message body text.
11        body: String,
12    },
13    /// Template message.
14    ///
15    /// Templates must be pre-approved by Meta before use.
16    /// Each element in `parameters` must be a typed parameter object per Meta spec,
17    /// e.g. `{"type": "text", "text": "value"}` or `{"type": "currency", ...}`.
18    Template {
19        /// Template name as registered in Meta Business Manager.
20        name: String,
21        /// Language code, e.g. `"en_US"` or `"it"`.
22        language: String,
23        /// Typed parameter objects for template variable substitution.
24        parameters: Vec<serde_json::Value>,
25    },
26}
27
28impl Message {
29    /// Serializes the message into the Meta Cloud API JSON payload format.
30    ///
31    /// The `to` field is the recipient phone number in E.164 format without `+`.
32    pub fn to_api_payload(&self, to: &str) -> serde_json::Value {
33        match self {
34            Message::Text { body } => serde_json::json!({
35                "messaging_product": "whatsapp",
36                "recipient_type": "individual",
37                "to": to,
38                "type": "text",
39                "text": { "body": body }
40            }),
41            Message::Template {
42                name,
43                language,
44                parameters,
45            } => serde_json::json!({
46                "messaging_product": "whatsapp",
47                "recipient_type": "individual",
48                "to": to,
49                "type": "template",
50                "template": {
51                    "name": name,
52                    "language": { "code": language },
53                    "components": parameters
54                }
55            }),
56        }
57    }
58}
59
60/// Result returned by a successful [`WhatsApp::send`](crate::WhatsApp::send) call.
61#[derive(Debug, Clone)]
62pub struct SendResult {
63    /// WhatsApp message ID returned by the Meta API.
64    ///
65    /// Use this ID to correlate outbound messages with delivery status webhooks.
66    pub wamid: String,
67}
68
69/// Identifies whether a WhatsApp message was sent by the business owner or a customer.
70///
71/// The phone number is in E.164 format without `+` prefix (as delivered by Meta).
72#[derive(Debug, Clone, Serialize, Deserialize)]
73#[serde(tag = "type", content = "phone")]
74#[serde(rename_all = "snake_case")]
75pub enum SenderIdentity {
76    /// Message sent by the business owner.
77    Owner(String),
78    /// Message sent by a customer.
79    Customer(String),
80}
81
82impl SenderIdentity {
83    /// Returns the phone number regardless of identity type.
84    pub fn phone(&self) -> &str {
85        match self {
86            SenderIdentity::Owner(p) | SenderIdentity::Customer(p) => p,
87        }
88    }
89}
90
91/// WhatsApp message delivery status.
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
93#[serde(rename_all = "snake_case")]
94pub enum DeliveryStatus {
95    /// Message sent to Meta's servers.
96    Sent,
97    /// Message delivered to the recipient's device.
98    Delivered,
99    /// Message read by the recipient.
100    Read,
101    /// Message delivery failed.
102    Failed,
103    /// Unknown status string from Meta.
104    #[serde(other)]
105    Unknown,
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn text_message_produces_correct_meta_api_json() {
114        let msg = Message::Text {
115            body: "Hello World".into(),
116        };
117        let payload = msg.to_api_payload("393401234567");
118
119        assert_eq!(payload["messaging_product"], "whatsapp");
120        assert_eq!(payload["recipient_type"], "individual");
121        assert_eq!(payload["to"], "393401234567");
122        assert_eq!(payload["type"], "text");
123        assert_eq!(payload["text"]["body"], "Hello World");
124    }
125
126    #[test]
127    fn template_message_produces_correct_meta_api_json() {
128        let params = vec![
129            serde_json::json!({"type": "text", "text": "Alberto"}),
130            serde_json::json!({"type": "text", "text": "123456"}),
131        ];
132        let msg = Message::Template {
133            name: "order_confirmation".into(),
134            language: "en_US".into(),
135            parameters: params.clone(),
136        };
137        let payload = msg.to_api_payload("393401234567");
138
139        assert_eq!(payload["messaging_product"], "whatsapp");
140        assert_eq!(payload["recipient_type"], "individual");
141        assert_eq!(payload["to"], "393401234567");
142        assert_eq!(payload["type"], "template");
143        assert_eq!(payload["template"]["name"], "order_confirmation");
144        assert_eq!(payload["template"]["language"]["code"], "en_US");
145        assert_eq!(payload["template"]["components"], serde_json::json!(params));
146    }
147
148    #[test]
149    fn sender_identity_owner_carries_phone_number() {
150        let identity = SenderIdentity::Owner("393401234567".into());
151        assert_eq!(identity.phone(), "393401234567");
152        assert!(matches!(identity, SenderIdentity::Owner(_)));
153    }
154
155    #[test]
156    fn sender_identity_customer_carries_phone_number() {
157        let identity = SenderIdentity::Customer("393409999999".into());
158        assert_eq!(identity.phone(), "393409999999");
159        assert!(matches!(identity, SenderIdentity::Customer(_)));
160    }
161
162    #[test]
163    fn delivery_status_deserializes_known_variants() {
164        assert_eq!(
165            serde_json::from_str::<DeliveryStatus>("\"sent\"").unwrap(),
166            DeliveryStatus::Sent
167        );
168        assert_eq!(
169            serde_json::from_str::<DeliveryStatus>("\"delivered\"").unwrap(),
170            DeliveryStatus::Delivered
171        );
172        assert_eq!(
173            serde_json::from_str::<DeliveryStatus>("\"read\"").unwrap(),
174            DeliveryStatus::Read
175        );
176        assert_eq!(
177            serde_json::from_str::<DeliveryStatus>("\"failed\"").unwrap(),
178            DeliveryStatus::Failed
179        );
180    }
181
182    #[test]
183    fn delivery_status_deserializes_unknown_variant() {
184        let status: DeliveryStatus = serde_json::from_str("\"expired\"").unwrap();
185        assert_eq!(status, DeliveryStatus::Unknown);
186    }
187}