Skip to main content

rust_ynab/ynab/
payee.rs

1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4use crate::PlanId;
5use crate::ynab::client::Client;
6use crate::ynab::common::NO_PARAMS;
7use crate::ynab::errors::Error;
8
9#[derive(Debug, Deserialize, Serialize)]
10struct PayeesDataEnvelope {
11    data: PayeesData,
12}
13
14#[derive(Debug, Deserialize, Serialize)]
15struct PayeesData {
16    payees: Vec<Payee>,
17    server_knowledge: i64,
18}
19
20#[derive(Debug, Deserialize, Serialize)]
21struct PayeeDataEnvelope {
22    data: PayeeData,
23}
24
25#[derive(Debug, Deserialize, Serialize)]
26struct PayeeData {
27    payee: Payee,
28    server_knowledge: i64,
29}
30
31/// A payee for a plan.
32#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
33pub struct Payee {
34    pub id: Uuid,
35    pub name: String,
36    pub transfer_account_id: Option<Uuid>,
37    pub deleted: bool,
38}
39
40#[derive(Debug, Deserialize, Serialize)]
41struct PayeeLocationDataEnvelope {
42    data: PayeeLocationData,
43}
44
45#[derive(Debug, Deserialize, Serialize)]
46struct PayeeLocationData {
47    payee_location: PayeeLocation,
48}
49
50#[derive(Debug, Deserialize, Serialize)]
51struct PayeeLocationsDataEnvelope {
52    data: PayeeLocationsData,
53}
54
55#[derive(Debug, Deserialize, Serialize)]
56struct PayeeLocationsData {
57    payee_locations: Vec<PayeeLocation>,
58}
59
60/// A GPS location stored when a transaction is entered on a mobile device. Locations will not be
61/// available for all payees.
62#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
63pub struct PayeeLocation {
64    pub id: Uuid,
65    pub payee_id: Uuid,
66    pub latitude: String,
67    pub longitude: String,
68    pub deleted: bool,
69}
70
71#[derive(Debug)]
72pub struct GetPayeesBuilder<'a> {
73    client: &'a Client,
74    plan_id: PlanId,
75    last_knowledge_of_server: Option<i64>,
76}
77
78impl<'a> GetPayeesBuilder<'a> {
79    pub fn with_server_knowledge(mut self, sk: i64) -> Self {
80        self.last_knowledge_of_server = Some(sk);
81        self
82    }
83
84    /// Sends the request. Returns payees and server knowledge for use in subsequent delta requests.
85    pub async fn send(self) -> Result<(Vec<Payee>, i64), Error> {
86        let params: Option<&[(&str, &str)]> = if let Some(sk) = self.last_knowledge_of_server {
87            Some(&[("last_knowledge_of_server", &sk.to_string())])
88        } else {
89            None
90        };
91        let result: PayeesDataEnvelope = self
92            .client
93            .get(&format!("plans/{}/payees", self.plan_id), params)
94            .await?;
95        Ok((result.data.payees, result.data.server_knowledge))
96    }
97}
98
99impl Client {
100    /// Returns a builder for fetching all payees. Chain `.with_server_knowledge()` for a delta request.
101    pub fn get_payees(&self, plan_id: PlanId) -> GetPayeesBuilder<'_> {
102        GetPayeesBuilder {
103            client: self,
104            plan_id,
105            last_knowledge_of_server: None,
106        }
107    }
108    /// Returns a single payee.
109    pub async fn get_payee(&self, plan_id: PlanId, payee_id: Uuid) -> Result<(Payee, i64), Error> {
110        let result: PayeeDataEnvelope = self
111            .get(&format!("plans/{}/payees/{}", plan_id, payee_id), NO_PARAMS)
112            .await?;
113        Ok((result.data.payee, result.data.server_knowledge))
114    }
115
116    /// Returns all payee locations.
117    pub async fn get_payee_locations(&self, plan_id: PlanId) -> Result<Vec<PayeeLocation>, Error> {
118        let result: PayeeLocationsDataEnvelope = self
119            .get(&format!("plans/{}/payee_locations", plan_id), NO_PARAMS)
120            .await?;
121        Ok(result.data.payee_locations)
122    }
123
124    /// Returns all payee locations for a specified payee.
125    pub async fn get_payee_locations_by_payee(
126        &self,
127        plan_id: PlanId,
128        payee_id: Uuid,
129    ) -> Result<Vec<PayeeLocation>, Error> {
130        let result: PayeeLocationsDataEnvelope = self
131            .get(
132                &format!("plans/{}/payees/{}/payee_locations", plan_id, payee_id),
133                NO_PARAMS,
134            )
135            .await?;
136        Ok(result.data.payee_locations)
137    }
138
139    /// Returns a single payee location.
140    pub async fn get_payee_location(
141        &self,
142        plan_id: PlanId,
143        location_id: Uuid,
144    ) -> Result<PayeeLocation, Error> {
145        let result: PayeeLocationDataEnvelope = self
146            .get(
147                &format!("plans/{}/payee_locations/{}", plan_id, location_id),
148                NO_PARAMS,
149            )
150            .await?;
151        Ok(result.data.payee_location)
152    }
153}
154
155/// Request body for creating a new payee. Name is required and must not exceed 500
156/// characters.
157#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
158pub struct PostPayee {
159    pub name: String,
160}
161#[derive(Debug, Serialize)]
162struct PostPayeeWrapper {
163    payee: PostPayee,
164}
165
166/// Request body for updating an existing payee. All fields are optional; omitted fields are
167/// not changed.
168#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
169pub struct SavePayee {
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub name: Option<String>,
172}
173#[derive(Debug, Serialize)]
174struct PatchPayeeWrapper {
175    payee: SavePayee,
176}
177
178impl Client {
179    /// Creates a new payee. Returns the created payee and server knowledge for delta requests.
180    pub async fn create_payee(
181        &self,
182        plan_id: PlanId,
183        payee: PostPayee,
184    ) -> Result<(Payee, i64), Error> {
185        let result: PayeeDataEnvelope = self
186            .post(
187                &format!("plans/{}/payees", plan_id),
188                PostPayeeWrapper { payee },
189            )
190            .await?;
191        Ok((result.data.payee, result.data.server_knowledge))
192    }
193
194    /// Updates an existing payee. Returns the updated payee and server knowledge for delta
195    /// requests.
196    pub async fn update_payee(
197        &self,
198        plan_id: PlanId,
199        payee_id: Uuid,
200        payee: SavePayee,
201    ) -> Result<(Payee, i64), Error> {
202        let result: PayeeDataEnvelope = self
203            .patch(
204                &format!("plans/{}/payees/{}", plan_id, payee_id),
205                PatchPayeeWrapper { payee },
206            )
207            .await?;
208        Ok((result.data.payee, result.data.server_knowledge))
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use crate::ynab::testutil::{
216        TEST_ID_1, TEST_ID_3, TEST_ID_4, error_body, new_test_client, payee_fixture,
217        payee_location_fixture,
218    };
219    use serde_json::json;
220    use uuid::uuid;
221    use wiremock::matchers::{method, path};
222    use wiremock::{Mock, ResponseTemplate};
223
224    fn payees_list_fixture() -> serde_json::Value {
225        json!({ "data": { "payees": [payee_fixture()], "server_knowledge": 3 } })
226    }
227
228    fn payee_single_fixture() -> serde_json::Value {
229        json!({ "data": { "payee": payee_fixture(), "server_knowledge": 3 } })
230    }
231
232    fn payee_locations_list_fixture() -> serde_json::Value {
233        json!({ "data": { "payee_locations": [payee_location_fixture()] } })
234    }
235
236    fn payee_location_single_fixture() -> serde_json::Value {
237        json!({ "data": { "payee_location": payee_location_fixture() } })
238    }
239
240    #[tokio::test]
241    async fn get_payees_returns_payees() {
242        let (client, server) = new_test_client().await;
243        Mock::given(method("GET"))
244            .and(path(format!("/plans/{}/payees", TEST_ID_1)))
245            .respond_with(ResponseTemplate::new(200).set_body_json(payees_list_fixture()))
246            .expect(1)
247            .mount(&server)
248            .await;
249        let (payees, sk) = client
250            .get_payees(PlanId::Id(uuid!(TEST_ID_1)))
251            .send()
252            .await
253            .unwrap();
254        assert_eq!(payees.len(), 1);
255        assert_eq!(payees[0].id.to_string(), TEST_ID_3);
256        assert_eq!(sk, 3);
257    }
258
259    #[tokio::test]
260    async fn get_payee_returns_payee() {
261        let (client, server) = new_test_client().await;
262        Mock::given(method("GET"))
263            .and(path(format!("/plans/{}/payees/{}", TEST_ID_1, TEST_ID_3)))
264            .respond_with(ResponseTemplate::new(200).set_body_json(payee_single_fixture()))
265            .expect(1)
266            .mount(&server)
267            .await;
268        let (payee, _) = client
269            .get_payee(PlanId::Id(uuid!(TEST_ID_1)), uuid!(TEST_ID_3))
270            .await
271            .unwrap();
272        assert_eq!(payee.id.to_string(), TEST_ID_3);
273        assert_eq!(payee.name, "Amazon");
274    }
275
276    #[tokio::test]
277    async fn get_payee_locations_returns_locations() {
278        let (client, server) = new_test_client().await;
279        Mock::given(method("GET"))
280            .and(path(format!("/plans/{}/payee_locations", TEST_ID_1)))
281            .respond_with(ResponseTemplate::new(200).set_body_json(payee_locations_list_fixture()))
282            .expect(1)
283            .mount(&server)
284            .await;
285        let locations = client
286            .get_payee_locations(PlanId::Id(uuid!(TEST_ID_1)))
287            .await
288            .unwrap();
289        assert_eq!(locations.len(), 1);
290        assert_eq!(locations[0].id.to_string(), TEST_ID_4);
291    }
292
293    #[tokio::test]
294    async fn get_payee_locations_by_payee_returns_locations() {
295        let (client, server) = new_test_client().await;
296        Mock::given(method("GET"))
297            .and(path(format!(
298                "/plans/{}/payees/{}/payee_locations",
299                TEST_ID_1, TEST_ID_3
300            )))
301            .respond_with(ResponseTemplate::new(200).set_body_json(payee_locations_list_fixture()))
302            .expect(1)
303            .mount(&server)
304            .await;
305        let locations = client
306            .get_payee_locations_by_payee(PlanId::Id(uuid!(TEST_ID_1)), uuid!(TEST_ID_3))
307            .await
308            .unwrap();
309        assert_eq!(locations.len(), 1);
310        assert_eq!(locations[0].payee_id.to_string(), TEST_ID_3);
311    }
312
313    #[tokio::test]
314    async fn get_payee_location_returns_location() {
315        let (client, server) = new_test_client().await;
316        Mock::given(method("GET"))
317            .and(path(format!(
318                "/plans/{}/payee_locations/{}",
319                TEST_ID_1, TEST_ID_4
320            )))
321            .respond_with(ResponseTemplate::new(200).set_body_json(payee_location_single_fixture()))
322            .expect(1)
323            .mount(&server)
324            .await;
325        let location = client
326            .get_payee_location(PlanId::Id(uuid!(TEST_ID_1)), uuid!(TEST_ID_4))
327            .await
328            .unwrap();
329        assert_eq!(location.id.to_string(), TEST_ID_4);
330        assert_eq!(location.latitude, "37.7749");
331    }
332
333    #[tokio::test]
334    async fn create_payee_succeeds() {
335        let (client, server) = new_test_client().await;
336        Mock::given(method("POST"))
337            .and(path(format!("/plans/{}/payees", TEST_ID_1)))
338            .respond_with(ResponseTemplate::new(201).set_body_json(payee_single_fixture()))
339            .expect(1)
340            .mount(&server)
341            .await;
342        let (payee, sk) = client
343            .create_payee(
344                PlanId::Id(uuid!(TEST_ID_1)),
345                PostPayee {
346                    name: "Amazon".to_string(),
347                },
348            )
349            .await
350            .unwrap();
351        assert_eq!(payee.id.to_string(), TEST_ID_3);
352        assert_eq!(sk, 3);
353    }
354
355    #[tokio::test]
356    async fn update_payee_succeeds() {
357        let (client, server) = new_test_client().await;
358        Mock::given(method("PATCH"))
359            .and(path(format!("/plans/{}/payees/{}", TEST_ID_1, TEST_ID_3)))
360            .respond_with(ResponseTemplate::new(200).set_body_json(payee_single_fixture()))
361            .expect(1)
362            .mount(&server)
363            .await;
364        let (payee, _) = client
365            .update_payee(
366                PlanId::Id(uuid!(TEST_ID_1)),
367                uuid!(TEST_ID_3),
368                SavePayee {
369                    name: Some("Amazon Updated".to_string()),
370                },
371            )
372            .await
373            .unwrap();
374        assert_eq!(payee.id.to_string(), TEST_ID_3);
375    }
376
377    #[tokio::test]
378    async fn get_payee_returns_not_found() {
379        let (client, server) = new_test_client().await;
380        Mock::given(method("GET"))
381            .and(path(format!("/plans/{}/payees/{}", TEST_ID_1, TEST_ID_3)))
382            .respond_with(ResponseTemplate::new(404).set_body_json(error_body(
383                "404",
384                "not_found",
385                "Payee not found",
386            )))
387            .mount(&server)
388            .await;
389        let err = client
390            .get_payee(PlanId::Id(uuid!(TEST_ID_1)), uuid!(TEST_ID_3))
391            .await
392            .unwrap_err();
393        assert!(matches!(err, Error::NotFound(_)));
394    }
395}