nsg/brief_request/
mod.rs

1//! Brief request parser
2//!
3//! ## Example usage
4//! You can find example HTMLs in `src/tests/assets/brief_request/valid`
5//!
6//! ```
7//! use nsg::brief_request::BriefRequest;
8//!
9//! let html = include_str!("../tests/assets/brief_request/valid/1.html");
10//! let brief_request = BriefRequest::from(&html);
11//!
12//! println!("Order (brief request): {:#?}", brief_request);
13//! ```
14
15pub mod guaranteed;
16
17use std::fmt::Debug;
18use std::str::FromStr;
19
20use chrono::{DateTime, FixedOffset, NaiveDateTime};
21use chrono_tz::Europe::Kyiv;
22use itertools::Itertools;
23use scraper::{ElementRef, Html};
24use serde::{Deserialize, Serialize};
25
26use crate::data::address::Address;
27use crate::data::comment::{Comment, CommentError};
28use crate::data::internal_status::{InternalStatus, InternalStatusError};
29use crate::data::order_type::{OrderType, OrderTypeError};
30use crate::data::status::{Status, StatusError};
31use crate::data::time_constrains::{TimeConstrains, TimeConstrainsError};
32use crate::macros::match_and_set;
33use crate::serializable_int_error_kind::SerializableIntErrorKind;
34use crate::serializable_parse_error_kind::SerializableParseErrorKind;
35use crate::traits::is_it::IsIt;
36use crate::traits::prev_element_ref::PrevElementRef;
37
38/// Parsed brief request containing some (more fields available via
39/// [`ViewRequest`](crate::view_request::ViewRequest)) of the fields of order.
40/// Note that all fields will not fail hard allowing to work with partially
41/// valid order
42#[derive(Clone, PartialEq, PartialOrd, Eq, Ord, Debug, Hash, Serialize, Deserialize, Default)]
43pub struct BriefRequest {
44    pub order_id:          Option<Result<u32, SerializableIntErrorKind>>,
45    pub internal_order_id: Option<Result<u32, SerializableIntErrorKind>>,
46    pub order_type:        Option<Result<OrderType, OrderTypeError>>,
47    pub creation_date:     Option<Result<DateTime<FixedOffset>, SerializableParseErrorKind>>,
48    pub internal_status:   Option<Result<InternalStatus, InternalStatusError>>,
49    pub address:           Option<Address>,
50    /// Client's full name (Kyivstar's version)
51    pub client:            Option<String>,
52    /// Only orders for subscription (connection) to Kyivstar's network contain
53    /// service package name
54    pub service:           Option<String>,
55    /// Client's personal account number
56    pub pa:                Option<String>,
57    pub time_constrains:   Option<Result<TimeConstrains, TimeConstrainsError>>,
58    /// One order can have up to two installers
59    pub installers:        Vec<String>,
60    pub last_comment:      Option<Result<Comment, CommentError>>,
61    pub first_comment:     Option<Result<Comment, CommentError>>,
62    pub status:            Option<Result<Status, StatusError>>,
63    /// List of client's contact phone numbers
64    pub phones:            Vec<String>,
65}
66
67impl PrevElementRef for BriefRequest {}
68
69impl BriefRequest {
70    /// Parse brief request from HTML
71    pub fn from(html: &str) -> BriefRequest {
72        log::debug!(target: "nsg", "Processing HTML: {:?}", html);
73        let mut brief_request = BriefRequest::default();
74
75        let html_fragment = Html::parse_fragment(html);
76        brief_request.collect_fragments(&html_fragment);
77
78        brief_request
79    }
80
81    fn collect_fragments(&mut self, html_fragment: &Html) {
82        log::info!(target: "nsg", "Parsing brief request HTML...");
83
84        let elements = html_fragment
85            .root_element()
86            .descendent_elements()
87            .filter_map(|element| {
88                if element.child_elements().count() == 0 {
89                    return Some(element);
90                }
91
92                if element.child_elements().count() == 1 && element.child_elements().next()?.value().name() == "a" {
93                    return Some(element);
94                }
95
96                None
97            });
98        log::debug!(target: "nsg", "Defined iterator over elements without child elements");
99
100        let text_getter = |element: ElementRef<'_>| -> String { element.text().collect_vec().join(" ") };
101        log::debug!(target: "nsg", "Defined text getter");
102
103        let mut city = None;
104        let mut address_string = None;
105        let mut apartment = None;
106
107        for element in elements {
108            match_and_set!(
109                "заявка:",
110                self.internal_order_id,
111                (|| Some(u32::from_str(&cur_text).map_err(|err| err.kind().clone().into()))),
112                self,
113                element,
114                text_getter,
115                is_it,
116                cur_text
117            );
118
119            match_and_set!(
120                "наряд:",
121                self.order_id,
122                (|| Some(u32::from_str(&cur_text).map_err(|err| err.kind().clone().into()))),
123                self,
124                element,
125                text_getter,
126                is_it,
127                cur_text
128            );
129
130            match_and_set!(
131                "подтип:",
132                self.order_type,
133                (|| Some(OrderType::from_str(&cur_text))),
134                self,
135                element,
136                text_getter,
137                is_it,
138                cur_text
139            );
140
141            match_and_set!(
142                "дата создания:",
143                self.creation_date,
144                (|| Some(self.as_datetime(&cur_text))),
145                self,
146                element,
147                text_getter,
148                is_it,
149                cur_text
150            );
151
152            match_and_set!(
153                "статус:",
154                self.internal_status,
155                (|| Some(InternalStatus::from_str(&cur_text))),
156                self,
157                element,
158                text_getter,
159                is_it,
160                cur_text
161            );
162
163            match_and_set!(
164                "город:",
165                city,
166                (|| Some(cur_text)),
167                self,
168                element,
169                text_getter,
170                is_it,
171                cur_text
172            );
173
174            match_and_set!(
175                "адрес:",
176                address_string,
177                (|| Some(cur_text)),
178                self,
179                element,
180                text_getter,
181                is_it,
182                cur_text
183            );
184
185            match_and_set!(
186                "квартира:",
187                apartment,
188                (|| Some(cur_text)),
189                self,
190                element,
191                text_getter,
192                is_it,
193                cur_text
194            );
195
196            match_and_set!(
197                "клиент:",
198                self.client,
199                (|| Some(cur_text.trim().to_string())),
200                self,
201                element,
202                text_getter,
203                is_it,
204                cur_text
205            );
206            match_and_set!(
207                "пакет:",
208                self.service,
209                (|| {
210                    if cur_text.is_empty() {
211                        log::warn!(
212                            target: "nsg",
213                            "[{:?}] NOTE: intentionally ignoring self.service because it's empty",
214                            element.id()
215                        );
216                        return None;
217                    }
218
219                    Some(cur_text)
220                }),
221                self,
222                element,
223                text_getter,
224                is_it,
225                cur_text
226            );
227            match_and_set!(
228                "телефон:",
229                (|| self.phones.push(cur_text.trim().to_string())),
230                self,
231                element,
232                text_getter,
233                is_it,
234                cur_text
235            );
236            match_and_set!(
237                "телефон 2:",
238                (|| self.phones.push(cur_text.trim().to_string())),
239                self,
240                element,
241                text_getter,
242                is_it,
243                cur_text
244            );
245            match_and_set!(
246                "лицевой счет:",
247                self.pa,
248                (|| Some(cur_text)),
249                self,
250                element,
251                text_getter,
252                is_it,
253                cur_text
254            );
255            match_and_set!(
256                "время подключения:",
257                self.time_constrains,
258                (|| Some(TimeConstrains::from(&cur_text))),
259                self,
260                element,
261                text_getter,
262                is_it,
263                cur_text
264            );
265            match_and_set!(
266                "монтажник:",
267                (|| self.installers.push(cur_text)),
268                self,
269                element,
270                text_getter,
271                is_it,
272                cur_text
273            );
274            match_and_set!(
275                "монтажник 2:",
276                (|| self.installers.push(cur_text)),
277                self,
278                element,
279                text_getter,
280                is_it,
281                cur_text
282            );
283            match_and_set!(
284                "статус заказчика:",
285                self.status,
286                (|| Some(Status::from_str(&cur_text))),
287                self,
288                element,
289                text_getter,
290                is_it,
291                cur_text
292            );
293
294            match_and_set!(
295                "начальный комментарий:",
296                self.first_comment,
297                (|| Some(Comment::from(&cur_text))),
298                self,
299                element,
300                text_getter,
301                is_it,
302                cur_text
303            );
304
305            match_and_set!(
306                "последний комментарий:",
307                self.last_comment,
308                (|| Some(Comment::from(&cur_text))),
309                self,
310                element,
311                text_getter,
312                is_it,
313                cur_text
314            );
315        }
316
317        if let (Some(city), Some(address_string), Some(apartment)) = (city, address_string, apartment) {
318            let mut address_iter = address_string.split(',');
319
320            // TODO: strings can be empty
321            self.address = Some(Address::from_parts(
322                city,
323                address_iter.next().unwrap().to_string(),
324                address_iter.next().unwrap().to_string(),
325                apartment,
326            ));
327        }
328    }
329
330    fn as_datetime(&self, input: &str) -> Result<DateTime<FixedOffset>, SerializableParseErrorKind> {
331        let naive = NaiveDateTime::parse_from_str(input, "%Y-%m-%d %H:%M:%S")?;
332        let datetime = naive
333            .and_local_timezone(Kyiv)
334            .earliest()
335            .expect("Never should have gotten a time that doesn't exist in the Kyiv time zone")
336            .fixed_offset();
337
338        Ok(datetime)
339    }
340}