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#[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#[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 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 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 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 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 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 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#[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#[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 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 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}