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, 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, 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    pub async fn send(self) -> Result<(Vec<Payee>, i64), Error> {
85        let result: PayeesDataEnvelope = self
86            .client
87            .get(&format!("plans/{}/payees", self.plan_id), NO_PARAMS)
88            .await?;
89        Ok((result.data.payees, result.data.server_knowledge))
90    }
91}
92
93impl Client {
94    /// Returns all payees. The second return value is server knowledge for delta requests.
95    pub fn get_payees(&self, plan_id: PlanId) -> GetPayeesBuilder<'_> {
96        GetPayeesBuilder {
97            client: self,
98            plan_id,
99            last_knowledge_of_server: None,
100        }
101    }
102    /// Returns a single payee.
103    pub async fn get_payee(&self, plan_id: PlanId, payee_id: Uuid) -> Result<Payee, Error> {
104        let result: PayeeDataEnvelope = self
105            .get(&format!("plans/{}/payees/{}", plan_id, payee_id), NO_PARAMS)
106            .await?;
107        Ok(result.data.payee)
108    }
109
110    /// Returns all payee locations.
111    pub async fn get_payee_locations(&self, plan_id: PlanId) -> Result<Vec<PayeeLocation>, Error> {
112        let result: PayeeLocationsDataEnvelope = self
113            .get(&format!("plans/{}/payee_locations", plan_id), NO_PARAMS)
114            .await?;
115        Ok(result.data.payee_locations)
116    }
117
118    /// Returns all payee locations for a specified payee.
119    pub async fn get_payee_locations_by_payee(
120        &self,
121        plan_id: PlanId,
122        payee_id: Uuid,
123    ) -> Result<Vec<PayeeLocation>, Error> {
124        let result: PayeeLocationsDataEnvelope = self
125            .get(
126                &format!("plans/{}/payees/{}/payee_locations", plan_id, payee_id),
127                NO_PARAMS,
128            )
129            .await?;
130        Ok(result.data.payee_locations)
131    }
132
133    /// Returns a single payee location.
134    pub async fn get_payee_location(
135        &self,
136        plan_id: PlanId,
137        location_id: Uuid,
138    ) -> Result<PayeeLocation, Error> {
139        let result: PayeeLocationDataEnvelope = self
140            .get(
141                &format!("plans/{}/payee_locations/{}", plan_id, location_id),
142                NO_PARAMS,
143            )
144            .await?;
145        Ok(result.data.payee_location)
146    }
147}
148
149/// Request body for creating a new payee. Name is required and must not exceed 500
150/// characters.
151#[derive(Debug, Serialize)]
152pub struct PostPayee {
153    pub name: String,
154}
155#[derive(Debug, Serialize)]
156struct PostPayeeWrapper {
157    payee: PostPayee,
158}
159
160/// Request body for updating an existing payee. All fields are optional; omitted fields are
161/// not changed.
162#[derive(Debug, Serialize)]
163pub struct SavePayee {
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub name: Option<String>,
166}
167#[derive(Debug, Serialize)]
168struct PatchPayeeWrapper {
169    payee: SavePayee,
170}
171
172impl Client {
173    /// Creates a new payee. Returns the created payee and server knowledge for delta requests.
174    pub async fn create_payee(
175        &self,
176        plan_id: PlanId,
177        payee: PostPayee,
178    ) -> Result<(Payee, i64), Error> {
179        let result: PayeeDataEnvelope = self
180            .post(
181                &format!("plans/{}/payees", plan_id),
182                PostPayeeWrapper { payee },
183            )
184            .await?;
185        Ok((result.data.payee, result.data.server_knowledge))
186    }
187
188    /// Updates an existing payee. Returns the updated payee and server knowledge for delta
189    /// requests.
190    pub async fn update_payee(
191        &self,
192        plan_id: PlanId,
193        payee_id: Uuid,
194        payee: SavePayee,
195    ) -> Result<(Payee, i64), Error> {
196        let result: PayeeDataEnvelope = self
197            .patch(
198                &format!("plans/{}/payees/{}", plan_id, payee_id),
199                PatchPayeeWrapper { payee },
200            )
201            .await?;
202        Ok((result.data.payee, result.data.server_knowledge))
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use crate::ynab::testutil::{
210        TEST_ID_1, TEST_ID_3, TEST_ID_4, error_body, new_test_client, payee_fixture,
211        payee_location_fixture,
212    };
213    use serde_json::json;
214    use uuid::uuid;
215    use wiremock::matchers::{method, path};
216    use wiremock::{Mock, ResponseTemplate};
217
218    fn payees_list_fixture() -> serde_json::Value {
219        json!({ "data": { "payees": [payee_fixture()], "server_knowledge": 3 } })
220    }
221
222    fn payee_single_fixture() -> serde_json::Value {
223        json!({ "data": { "payee": payee_fixture(), "server_knowledge": 3 } })
224    }
225
226    fn payee_locations_list_fixture() -> serde_json::Value {
227        json!({ "data": { "payee_locations": [payee_location_fixture()] } })
228    }
229
230    fn payee_location_single_fixture() -> serde_json::Value {
231        json!({ "data": { "payee_location": payee_location_fixture() } })
232    }
233
234    #[tokio::test]
235    async fn get_payees_returns_payees() {
236        let (client, server) = new_test_client().await;
237        Mock::given(method("GET"))
238            .and(path(format!("/plans/{}/payees", TEST_ID_1)))
239            .respond_with(ResponseTemplate::new(200).set_body_json(payees_list_fixture()))
240            .expect(1)
241            .mount(&server)
242            .await;
243        let (payees, sk) = client
244            .get_payees(PlanId::Id(uuid!(TEST_ID_1)))
245            .send()
246            .await
247            .unwrap();
248        assert_eq!(payees.len(), 1);
249        assert_eq!(payees[0].id.to_string(), TEST_ID_3);
250        assert_eq!(sk, 3);
251    }
252
253    #[tokio::test]
254    async fn get_payee_returns_payee() {
255        let (client, server) = new_test_client().await;
256        Mock::given(method("GET"))
257            .and(path(format!("/plans/{}/payees/{}", TEST_ID_1, TEST_ID_3)))
258            .respond_with(ResponseTemplate::new(200).set_body_json(payee_single_fixture()))
259            .expect(1)
260            .mount(&server)
261            .await;
262        let payee = client
263            .get_payee(PlanId::Id(uuid!(TEST_ID_1)), uuid!(TEST_ID_3))
264            .await
265            .unwrap();
266        assert_eq!(payee.id.to_string(), TEST_ID_3);
267        assert_eq!(payee.name, "Amazon");
268    }
269
270    #[tokio::test]
271    async fn get_payee_locations_returns_locations() {
272        let (client, server) = new_test_client().await;
273        Mock::given(method("GET"))
274            .and(path(format!("/plans/{}/payee_locations", TEST_ID_1)))
275            .respond_with(ResponseTemplate::new(200).set_body_json(payee_locations_list_fixture()))
276            .expect(1)
277            .mount(&server)
278            .await;
279        let locations = client
280            .get_payee_locations(PlanId::Id(uuid!(TEST_ID_1)))
281            .await
282            .unwrap();
283        assert_eq!(locations.len(), 1);
284        assert_eq!(locations[0].id.to_string(), TEST_ID_4);
285    }
286
287    #[tokio::test]
288    async fn get_payee_locations_by_payee_returns_locations() {
289        let (client, server) = new_test_client().await;
290        Mock::given(method("GET"))
291            .and(path(format!(
292                "/plans/{}/payees/{}/payee_locations",
293                TEST_ID_1, TEST_ID_3
294            )))
295            .respond_with(ResponseTemplate::new(200).set_body_json(payee_locations_list_fixture()))
296            .expect(1)
297            .mount(&server)
298            .await;
299        let locations = client
300            .get_payee_locations_by_payee(PlanId::Id(uuid!(TEST_ID_1)), uuid!(TEST_ID_3))
301            .await
302            .unwrap();
303        assert_eq!(locations.len(), 1);
304        assert_eq!(locations[0].payee_id.to_string(), TEST_ID_3);
305    }
306
307    #[tokio::test]
308    async fn get_payee_location_returns_location() {
309        let (client, server) = new_test_client().await;
310        Mock::given(method("GET"))
311            .and(path(format!(
312                "/plans/{}/payee_locations/{}",
313                TEST_ID_1, TEST_ID_4
314            )))
315            .respond_with(ResponseTemplate::new(200).set_body_json(payee_location_single_fixture()))
316            .expect(1)
317            .mount(&server)
318            .await;
319        let location = client
320            .get_payee_location(PlanId::Id(uuid!(TEST_ID_1)), uuid!(TEST_ID_4))
321            .await
322            .unwrap();
323        assert_eq!(location.id.to_string(), TEST_ID_4);
324        assert_eq!(location.latitude, "37.7749");
325    }
326
327    #[tokio::test]
328    async fn create_payee_succeeds() {
329        let (client, server) = new_test_client().await;
330        Mock::given(method("POST"))
331            .and(path(format!("/plans/{}/payees", TEST_ID_1)))
332            .respond_with(ResponseTemplate::new(201).set_body_json(payee_single_fixture()))
333            .expect(1)
334            .mount(&server)
335            .await;
336        let (payee, sk) = client
337            .create_payee(
338                PlanId::Id(uuid!(TEST_ID_1)),
339                PostPayee {
340                    name: "Amazon".to_string(),
341                },
342            )
343            .await
344            .unwrap();
345        assert_eq!(payee.id.to_string(), TEST_ID_3);
346        assert_eq!(sk, 3);
347    }
348
349    #[tokio::test]
350    async fn update_payee_succeeds() {
351        let (client, server) = new_test_client().await;
352        Mock::given(method("PATCH"))
353            .and(path(format!("/plans/{}/payees/{}", TEST_ID_1, TEST_ID_3)))
354            .respond_with(ResponseTemplate::new(200).set_body_json(payee_single_fixture()))
355            .expect(1)
356            .mount(&server)
357            .await;
358        let (payee, _) = client
359            .update_payee(
360                PlanId::Id(uuid!(TEST_ID_1)),
361                uuid!(TEST_ID_3),
362                SavePayee {
363                    name: Some("Amazon Updated".to_string()),
364                },
365            )
366            .await
367            .unwrap();
368        assert_eq!(payee.id.to_string(), TEST_ID_3);
369    }
370
371    #[tokio::test]
372    async fn get_payee_returns_not_found() {
373        let (client, server) = new_test_client().await;
374        Mock::given(method("GET"))
375            .and(path(format!("/plans/{}/payees/{}", TEST_ID_1, TEST_ID_3)))
376            .respond_with(ResponseTemplate::new(404).set_body_json(error_body(
377                "404",
378                "not_found",
379                "Payee not found",
380            )))
381            .mount(&server)
382            .await;
383        let err = client
384            .get_payee(PlanId::Id(uuid!(TEST_ID_1)), uuid!(TEST_ID_3))
385            .await
386            .unwrap_err();
387        assert!(matches!(err, Error::NotFound(_)));
388    }
389}