1use std::collections::{BTreeMap, HashSet};
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17pub struct SendRequestId(pub String);
18
19impl SendRequestId {
20 #[must_use]
21 pub fn new(id: impl Into<String>) -> Self {
22 Self(id.into())
23 }
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Hash)]
33pub struct TemplateId(pub String);
34
35impl TemplateId {
36 #[must_use]
37 pub fn new(id: impl Into<String>) -> Self {
38 Self(id.into())
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Hash)]
48pub struct Ppnum(String);
49
50#[derive(Debug, Clone, thiserror::Error)]
52pub enum PpnumError {
53 #[error("ppnum is empty")]
55 Empty,
56 #[error("ppnum too short ({len} digits, minimum 11)")]
58 TooShort { len: usize },
59 #[error("ppnum contains non-digit character at position {pos}")]
61 NonDigit { pos: usize },
62}
63
64impl Ppnum {
65 pub fn try_new(s: impl Into<String>) -> Result<Self, PpnumError> {
72 let s = s.into();
73 if s.is_empty() {
74 return Err(PpnumError::Empty);
75 }
76 for (i, b) in s.bytes().enumerate() {
77 if !b.is_ascii_digit() {
78 return Err(PpnumError::NonDigit { pos: i });
79 }
80 }
81 if s.len() < 11 {
82 return Err(PpnumError::TooShort { len: s.len() });
83 }
84 Ok(Self(s))
85 }
86
87 #[must_use]
89 pub fn as_str(&self) -> &str {
90 &self.0
91 }
92
93 #[must_use]
95 pub fn into_inner(self) -> String {
96 self.0
97 }
98}
99
100impl AsRef<str> for Ppnum {
101 fn as_ref(&self) -> &str {
102 &self.0
103 }
104}
105
106#[derive(Debug, Clone)]
114pub struct Recipient {
115 pub ppnum: Ppnum,
116 pub vars: BTreeMap<String, String>,
117}
118
119impl Recipient {
120 #[must_use]
122 pub fn bare(ppnum: Ppnum) -> Self {
123 Self { ppnum, vars: BTreeMap::new() }
124 }
125}
126
127#[derive(Debug, Clone)]
138pub struct RecipientList(Vec<Recipient>);
139
140#[derive(Debug, Clone, thiserror::Error)]
142pub enum RecipientListError {
143 #[error("recipient list is empty")]
145 Empty,
146 #[error("recipient list exceeds 1000 (got {count})")]
148 TooLarge { count: usize },
149}
150
151const RECIPIENT_LIST_MAX: usize = 1000;
152
153impl RecipientList {
154 pub fn try_new(recipients: Vec<Recipient>) -> Result<Self, RecipientListError> {
165 if recipients.is_empty() {
166 return Err(RecipientListError::Empty);
167 }
168 let mut seen: HashSet<String> = HashSet::with_capacity(recipients.len());
169 let mut deduped = Vec::with_capacity(recipients.len());
170 for r in recipients {
171 if seen.insert(r.ppnum.as_str().to_string()) {
172 deduped.push(r);
173 }
174 }
175 if deduped.len() > RECIPIENT_LIST_MAX {
176 return Err(RecipientListError::TooLarge { count: deduped.len() });
177 }
178 Ok(Self(deduped))
179 }
180
181 pub fn from_ppnums(ppnums: Vec<Ppnum>) -> Result<Self, RecipientListError> {
188 Self::try_new(ppnums.into_iter().map(Recipient::bare).collect())
189 }
190
191 #[must_use]
192 pub fn len(&self) -> usize {
193 self.0.len()
194 }
195
196 #[must_use]
197 pub fn is_empty(&self) -> bool {
198 self.0.is_empty()
199 }
200
201 pub fn iter(&self) -> impl Iterator<Item = &Recipient> {
204 self.0.iter()
205 }
206}
207
208impl<'a> IntoIterator for &'a RecipientList {
209 type Item = &'a Recipient;
210 type IntoIter = std::slice::Iter<'a, Recipient>;
211 fn into_iter(self) -> Self::IntoIter {
212 self.0.iter()
213 }
214}
215
216#[derive(Debug, Clone, Default)]
220pub struct PollConfig {
221 pub expires_in_hours: Option<i32>,
223 pub allow_multiple: bool,
225}
226
227#[derive(Debug, Clone)]
229pub struct SendOutcome {
230 pub id: SendRequestId,
231 pub state: SendRequestState,
232 pub total_recipients: u32,
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
238pub enum SendRequestState {
239 Queued,
240 Processing,
241 Completed,
242 Failed,
243 Unknown,
245}
246
247#[derive(Debug, Clone)]
252pub struct SendStatus {
253 pub id: SendRequestId,
254 pub state: SendRequestState,
255 pub totals: SendStatusTotals,
256}
257
258#[derive(Debug, Clone, Copy, Default)]
260pub struct SendStatusTotals {
261 pub total: u32,
262 pub delivered: u32,
263 pub pending_consent: u32,
264 pub failed: u32,
265}
266
267pub struct DeliveryStream(
277 Box<dyn futures_core::Stream<Item = Result<DeliveryEvent, crate::Error>> + Send + Unpin>,
278);
279
280impl DeliveryStream {
281 pub(crate) fn new(
282 inner: impl futures_core::Stream<Item = Result<DeliveryEvent, crate::Error>>
283 + Send
284 + Unpin
285 + 'static,
286 ) -> Self {
287 Self(Box::new(inner))
288 }
289
290 pub async fn message(&mut self) -> Result<Option<DeliveryEvent>, crate::Error> {
295 use futures_util::StreamExt as _;
296 match self.0.next().await {
297 None => Ok(None),
298 Some(Ok(evt)) => Ok(Some(evt)),
299 Some(Err(e)) => Err(e),
300 }
301 }
302}
303
304impl std::fmt::Debug for DeliveryStream {
305 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306 f.debug_struct("DeliveryStream").finish_non_exhaustive()
307 }
308}
309
310#[derive(Debug, Clone)]
313pub struct DeliveryEvent {
314 pub event_id: String,
317 pub send_request_id: SendRequestId,
318 pub kind: DeliveryEventKind,
319 pub occurred_at: Option<prost_types::Timestamp>,
321}
322
323#[derive(Debug, Clone)]
328pub enum DeliveryEventKind {
329 RecipientDelivered { ppnum: String, message_id: Option<String> },
330 RecipientFailed { ppnum: String, error_code: Option<String> },
331 RecipientPendingConsent { ppnum: String },
332 ConsentGranted,
333 ConsentDenied,
334 RequestCompleted,
335 PollResponseReceived,
336 Unknown,
338}
339
340#[cfg(test)]
341#[allow(clippy::unwrap_used, clippy::expect_used)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn ppnum_accepts_11_digits() {
347 let p = Ppnum::try_new("12345678901").unwrap();
348 assert_eq!(p.as_str(), "12345678901");
349 }
350
351 #[test]
352 fn ppnum_accepts_longer_than_11() {
353 let p = Ppnum::try_new("123456789012345").unwrap();
354 assert_eq!(p.as_str().len(), 15);
355 }
356
357 #[test]
358 fn ppnum_rejects_short() {
359 let err = Ppnum::try_new("1234567890").unwrap_err();
360 assert!(matches!(err, PpnumError::TooShort { len: 10 }));
361 }
362
363 #[test]
364 fn ppnum_rejects_empty() {
365 let err = Ppnum::try_new("").unwrap_err();
366 assert!(matches!(err, PpnumError::Empty));
367 }
368
369 #[test]
370 fn ppnum_rejects_non_digit() {
371 let err = Ppnum::try_new("1234567890a").unwrap_err();
372 assert!(matches!(err, PpnumError::NonDigit { pos: 10 }));
373 }
374
375 #[test]
376 fn ppnum_rejects_hyphens() {
377 let err = Ppnum::try_new("123-1234-5678").unwrap_err();
379 assert!(matches!(err, PpnumError::NonDigit { .. }));
380 }
381
382 fn p(s: &str) -> Ppnum {
383 Ppnum::try_new(s).unwrap()
384 }
385
386 #[test]
387 fn recipient_list_rejects_empty() {
388 let err = RecipientList::try_new(vec![]).unwrap_err();
389 assert!(matches!(err, RecipientListError::Empty));
390 }
391
392 #[test]
393 fn recipient_list_dedupes_by_ppnum() {
394 let list = RecipientList::from_ppnums(vec![
395 p("12345678901"),
396 p("12345678901"),
397 p("12345678902"),
398 ])
399 .unwrap();
400 assert_eq!(list.len(), 2);
401 }
402
403 #[test]
404 fn recipient_list_dedup_keeps_first_occurrence() {
405 let mut a = Recipient::bare(p("12345678901"));
406 a.vars.insert("name".into(), "first".into());
407 let mut b = Recipient::bare(p("12345678901"));
408 b.vars.insert("name".into(), "second".into());
409 let c = Recipient::bare(p("12345678902"));
410 let list = RecipientList::try_new(vec![a, b, c]).unwrap();
411 assert_eq!(list.len(), 2);
412 let first = list.iter().next().unwrap();
413 assert_eq!(first.vars.get("name").map(String::as_str), Some("first"));
414 }
415
416 #[test]
417 fn recipient_list_accepts_max_1000() {
418 let ppnums: Vec<Ppnum> =
419 (0..1000).map(|i| p(&format!("100000{:05}", i))).collect();
420 let list = RecipientList::from_ppnums(ppnums).unwrap();
421 assert_eq!(list.len(), 1000);
422 }
423
424 #[test]
425 fn recipient_list_rejects_over_1000_after_dedup() {
426 let ppnums: Vec<Ppnum> =
427 (0..1001).map(|i| p(&format!("100000{:05}", i))).collect();
428 let err = RecipientList::from_ppnums(ppnums).unwrap_err();
429 assert!(matches!(err, RecipientListError::TooLarge { count: 1001 }));
430 }
431
432 #[test]
433 fn recipient_list_dedup_can_rescue_oversize_input() {
434 let mut ppnums: Vec<Ppnum> =
436 (0..750).map(|i| p(&format!("100000{:05}", i))).collect();
437 ppnums.extend(ppnums.clone());
438 let list = RecipientList::from_ppnums(ppnums).unwrap();
439 assert_eq!(list.len(), 750);
440 }
441}