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