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