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, 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, 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 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 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 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 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 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#[derive(Debug, Serialize)]
152pub struct PostPayee {
153 pub name: String,
154}
155#[derive(Debug, Serialize)]
156struct PostPayeeWrapper {
157 payee: PostPayee,
158}
159
160#[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 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 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}