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