1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
16pub struct LocalExchange {
17 pub id: Uuid,
18 pub building_id: Uuid,
19 pub provider_id: Uuid, pub requester_id: Option<Uuid>, pub exchange_type: ExchangeType,
22 pub title: String,
23 pub description: String,
24 pub credits: i32, pub status: ExchangeStatus,
26 pub offered_at: DateTime<Utc>,
27 pub requested_at: Option<DateTime<Utc>>,
28 pub started_at: Option<DateTime<Utc>>,
29 pub completed_at: Option<DateTime<Utc>>,
30 pub cancelled_at: Option<DateTime<Utc>>,
31 pub cancellation_reason: Option<String>,
32 pub provider_rating: Option<i32>, pub requester_rating: Option<i32>, pub created_at: DateTime<Utc>,
35 pub updated_at: DateTime<Utc>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
39#[serde(rename_all = "PascalCase")]
40pub enum ExchangeType {
41 Service, ObjectLoan, SharedPurchase, }
45
46impl ExchangeType {
47 pub fn to_sql(&self) -> &'static str {
48 match self {
49 ExchangeType::Service => "Service",
50 ExchangeType::ObjectLoan => "ObjectLoan",
51 ExchangeType::SharedPurchase => "SharedPurchase",
52 }
53 }
54
55 pub fn from_sql(s: &str) -> Result<Self, String> {
56 match s {
57 "Service" => Ok(ExchangeType::Service),
58 "ObjectLoan" => Ok(ExchangeType::ObjectLoan),
59 "SharedPurchase" => Ok(ExchangeType::SharedPurchase),
60 _ => Err(format!("Invalid exchange type: {}", s)),
61 }
62 }
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
66#[serde(rename_all = "PascalCase")]
67pub enum ExchangeStatus {
68 Offered, Requested, InProgress, Completed, Cancelled, }
74
75impl ExchangeStatus {
76 pub fn to_sql(&self) -> &'static str {
77 match self {
78 ExchangeStatus::Offered => "Offered",
79 ExchangeStatus::Requested => "Requested",
80 ExchangeStatus::InProgress => "InProgress",
81 ExchangeStatus::Completed => "Completed",
82 ExchangeStatus::Cancelled => "Cancelled",
83 }
84 }
85
86 pub fn from_sql(s: &str) -> Result<Self, String> {
87 match s {
88 "Offered" => Ok(ExchangeStatus::Offered),
89 "Requested" => Ok(ExchangeStatus::Requested),
90 "InProgress" => Ok(ExchangeStatus::InProgress),
91 "Completed" => Ok(ExchangeStatus::Completed),
92 "Cancelled" => Ok(ExchangeStatus::Cancelled),
93 _ => Err(format!("Invalid exchange status: {}", s)),
94 }
95 }
96}
97
98impl LocalExchange {
99 pub fn new(
101 building_id: Uuid,
102 provider_id: Uuid,
103 exchange_type: ExchangeType,
104 title: String,
105 description: String,
106 credits: i32,
107 ) -> Result<Self, String> {
108 if title.trim().is_empty() {
110 return Err("Title cannot be empty".to_string());
111 }
112
113 if title.len() > 255 {
114 return Err("Title cannot exceed 255 characters".to_string());
115 }
116
117 if description.trim().is_empty() {
118 return Err("Description cannot be empty".to_string());
119 }
120
121 if description.len() > 2000 {
122 return Err("Description cannot exceed 2000 characters".to_string());
123 }
124
125 if credits <= 0 {
126 return Err("Credits must be positive".to_string());
127 }
128
129 if credits > 100 {
130 return Err("Credits cannot exceed 100 (maximum 100 hours)".to_string());
131 }
132
133 let now = Utc::now();
134
135 Ok(LocalExchange {
136 id: Uuid::new_v4(),
137 building_id,
138 provider_id,
139 requester_id: None,
140 exchange_type,
141 title: title.trim().to_string(),
142 description: description.trim().to_string(),
143 credits,
144 status: ExchangeStatus::Offered,
145 offered_at: now,
146 requested_at: None,
147 started_at: None,
148 completed_at: None,
149 cancelled_at: None,
150 cancellation_reason: None,
151 provider_rating: None,
152 requester_rating: None,
153 created_at: now,
154 updated_at: now,
155 })
156 }
157
158 pub fn request(&mut self, requester_id: Uuid) -> Result<(), String> {
160 if self.status != ExchangeStatus::Offered {
161 return Err(format!(
162 "Cannot request exchange in status {:?}",
163 self.status
164 ));
165 }
166
167 if self.provider_id == requester_id {
168 return Err("Provider cannot request their own exchange".to_string());
169 }
170
171 self.requester_id = Some(requester_id);
172 self.status = ExchangeStatus::Requested;
173 self.requested_at = Some(Utc::now());
174 self.updated_at = Utc::now();
175
176 Ok(())
177 }
178
179 pub fn start(&mut self, actor_id: Uuid) -> Result<(), String> {
181 if self.status != ExchangeStatus::Requested {
182 return Err(format!("Cannot start exchange in status {:?}", self.status));
183 }
184
185 if self.provider_id != actor_id {
187 return Err("Only the provider can start the exchange".to_string());
188 }
189
190 self.status = ExchangeStatus::InProgress;
191 self.started_at = Some(Utc::now());
192 self.updated_at = Utc::now();
193
194 Ok(())
195 }
196
197 pub fn complete(&mut self, actor_id: Uuid) -> Result<(), String> {
200 if self.status != ExchangeStatus::InProgress {
201 return Err(format!(
202 "Cannot complete exchange in status {:?}",
203 self.status
204 ));
205 }
206
207 if self.provider_id != actor_id && self.requester_id != Some(actor_id) {
209 return Err("Only provider or requester can complete the exchange".to_string());
210 }
211
212 self.status = ExchangeStatus::Completed;
213 self.completed_at = Some(Utc::now());
214 self.updated_at = Utc::now();
215
216 Ok(())
217 }
218
219 pub fn cancel(&mut self, actor_id: Uuid, reason: Option<String>) -> Result<(), String> {
221 if self.status == ExchangeStatus::Completed {
223 return Err("Cannot cancel a completed exchange".to_string());
224 }
225
226 if self.status == ExchangeStatus::Cancelled {
227 return Err("Exchange is already cancelled".to_string());
228 }
229
230 if self.provider_id != actor_id && self.requester_id != Some(actor_id) {
232 return Err("Only provider or requester can cancel the exchange".to_string());
233 }
234
235 self.status = ExchangeStatus::Cancelled;
236 self.cancelled_at = Some(Utc::now());
237 self.cancellation_reason = reason;
238 self.updated_at = Utc::now();
239
240 Ok(())
241 }
242
243 pub fn rate_provider(&mut self, requester_id: Uuid, rating: i32) -> Result<(), String> {
245 if self.status != ExchangeStatus::Completed {
246 return Err("Can only rate completed exchanges".to_string());
247 }
248
249 if self.requester_id != Some(requester_id) {
250 return Err("Only the requester can rate the provider".to_string());
251 }
252
253 if !(1..=5).contains(&rating) {
254 return Err("Rating must be between 1 and 5".to_string());
255 }
256
257 self.provider_rating = Some(rating);
258 self.updated_at = Utc::now();
259
260 Ok(())
261 }
262
263 pub fn rate_requester(&mut self, provider_id: Uuid, rating: i32) -> Result<(), String> {
265 if self.status != ExchangeStatus::Completed {
266 return Err("Can only rate completed exchanges".to_string());
267 }
268
269 if self.provider_id != provider_id {
270 return Err("Only the provider can rate the requester".to_string());
271 }
272
273 if !(1..=5).contains(&rating) {
274 return Err("Rating must be between 1 and 5".to_string());
275 }
276
277 self.requester_rating = Some(rating);
278 self.updated_at = Utc::now();
279
280 Ok(())
281 }
282
283 pub fn is_active(&self) -> bool {
285 matches!(
286 self.status,
287 ExchangeStatus::Offered | ExchangeStatus::Requested | ExchangeStatus::InProgress
288 )
289 }
290
291 pub fn has_mutual_ratings(&self) -> bool {
293 self.provider_rating.is_some() && self.requester_rating.is_some()
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 #[test]
302 fn test_create_exchange_success() {
303 let building_id = Uuid::new_v4();
304 let provider_id = Uuid::new_v4();
305
306 let exchange = LocalExchange::new(
307 building_id,
308 provider_id,
309 ExchangeType::Service,
310 "Gardening help".to_string(),
311 "I can help with planting and weeding".to_string(),
312 2,
313 );
314
315 assert!(exchange.is_ok());
316 let exchange = exchange.unwrap();
317 assert_eq!(exchange.building_id, building_id);
318 assert_eq!(exchange.provider_id, provider_id);
319 assert_eq!(exchange.status, ExchangeStatus::Offered);
320 assert_eq!(exchange.credits, 2);
321 assert!(exchange.requester_id.is_none());
322 }
323
324 #[test]
325 fn test_create_exchange_validation() {
326 let building_id = Uuid::new_v4();
327 let provider_id = Uuid::new_v4();
328
329 let result = LocalExchange::new(
331 building_id,
332 provider_id,
333 ExchangeType::Service,
334 "".to_string(),
335 "Description".to_string(),
336 2,
337 );
338 assert!(result.is_err());
339
340 let result = LocalExchange::new(
342 building_id,
343 provider_id,
344 ExchangeType::Service,
345 "Title".to_string(),
346 "Description".to_string(),
347 -1,
348 );
349 assert!(result.is_err());
350
351 let result = LocalExchange::new(
353 building_id,
354 provider_id,
355 ExchangeType::Service,
356 "Title".to_string(),
357 "Description".to_string(),
358 101,
359 );
360 assert!(result.is_err());
361 }
362
363 #[test]
364 fn test_exchange_workflow() {
365 let building_id = Uuid::new_v4();
366 let provider_id = Uuid::new_v4();
367 let requester_id = Uuid::new_v4();
368
369 let mut exchange = LocalExchange::new(
370 building_id,
371 provider_id,
372 ExchangeType::Service,
373 "Babysitting".to_string(),
374 "Can watch kids for 3 hours".to_string(),
375 3,
376 )
377 .unwrap();
378
379 assert!(exchange.request(requester_id).is_ok());
381 assert_eq!(exchange.status, ExchangeStatus::Requested);
382 assert_eq!(exchange.requester_id, Some(requester_id));
383
384 assert!(exchange.start(provider_id).is_ok());
386 assert_eq!(exchange.status, ExchangeStatus::InProgress);
387
388 assert!(exchange.complete(provider_id).is_ok());
390 assert_eq!(exchange.status, ExchangeStatus::Completed);
391 assert!(exchange.completed_at.is_some());
392 }
393
394 #[test]
395 fn test_cannot_request_own_exchange() {
396 let building_id = Uuid::new_v4();
397 let provider_id = Uuid::new_v4();
398
399 let mut exchange = LocalExchange::new(
400 building_id,
401 provider_id,
402 ExchangeType::Service,
403 "Service".to_string(),
404 "Description".to_string(),
405 2,
406 )
407 .unwrap();
408
409 let result = exchange.request(provider_id);
410 assert!(result.is_err());
411 }
412
413 #[test]
414 fn test_cancel_exchange() {
415 let building_id = Uuid::new_v4();
416 let provider_id = Uuid::new_v4();
417 let requester_id = Uuid::new_v4();
418
419 let mut exchange = LocalExchange::new(
420 building_id,
421 provider_id,
422 ExchangeType::Service,
423 "Service".to_string(),
424 "Description".to_string(),
425 2,
426 )
427 .unwrap();
428
429 exchange.request(requester_id).unwrap();
430
431 assert!(exchange
433 .cancel(requester_id, Some("Changed my mind".to_string()))
434 .is_ok());
435 assert_eq!(exchange.status, ExchangeStatus::Cancelled);
436 assert!(exchange.cancelled_at.is_some());
437 assert_eq!(
438 exchange.cancellation_reason,
439 Some("Changed my mind".to_string())
440 );
441 }
442
443 #[test]
444 fn test_cannot_cancel_completed_exchange() {
445 let building_id = Uuid::new_v4();
446 let provider_id = Uuid::new_v4();
447 let requester_id = Uuid::new_v4();
448
449 let mut exchange = LocalExchange::new(
450 building_id,
451 provider_id,
452 ExchangeType::Service,
453 "Service".to_string(),
454 "Description".to_string(),
455 2,
456 )
457 .unwrap();
458
459 exchange.request(requester_id).unwrap();
460 exchange.start(provider_id).unwrap();
461 exchange.complete(provider_id).unwrap();
462
463 let result = exchange.cancel(provider_id, None);
464 assert!(result.is_err());
465 }
466
467 #[test]
468 fn test_ratings() {
469 let building_id = Uuid::new_v4();
470 let provider_id = Uuid::new_v4();
471 let requester_id = Uuid::new_v4();
472
473 let mut exchange = LocalExchange::new(
474 building_id,
475 provider_id,
476 ExchangeType::Service,
477 "Service".to_string(),
478 "Description".to_string(),
479 2,
480 )
481 .unwrap();
482
483 exchange.request(requester_id).unwrap();
484 exchange.start(provider_id).unwrap();
485 exchange.complete(provider_id).unwrap();
486
487 assert!(exchange.rate_provider(requester_id, 5).is_ok());
489 assert_eq!(exchange.provider_rating, Some(5));
490
491 assert!(exchange.rate_requester(provider_id, 4).is_ok());
493 assert_eq!(exchange.requester_rating, Some(4));
494
495 assert!(exchange.has_mutual_ratings());
496 }
497
498 #[test]
499 fn test_rating_validation() {
500 let building_id = Uuid::new_v4();
501 let provider_id = Uuid::new_v4();
502 let requester_id = Uuid::new_v4();
503
504 let mut exchange = LocalExchange::new(
505 building_id,
506 provider_id,
507 ExchangeType::Service,
508 "Service".to_string(),
509 "Description".to_string(),
510 2,
511 )
512 .unwrap();
513
514 exchange.request(requester_id).unwrap();
515 exchange.start(provider_id).unwrap();
516 exchange.complete(provider_id).unwrap();
517
518 assert!(exchange.rate_provider(requester_id, 0).is_err());
520
521 assert!(exchange.rate_provider(requester_id, 6).is_err());
523
524 assert!(exchange.rate_provider(provider_id, 5).is_err());
526 }
527
528 #[test]
529 fn test_exchange_type_sql_conversion() {
530 assert_eq!(ExchangeType::Service.to_sql(), "Service");
531 assert_eq!(ExchangeType::ObjectLoan.to_sql(), "ObjectLoan");
532 assert_eq!(ExchangeType::SharedPurchase.to_sql(), "SharedPurchase");
533
534 assert_eq!(
535 ExchangeType::from_sql("Service").unwrap(),
536 ExchangeType::Service
537 );
538 assert_eq!(
539 ExchangeType::from_sql("ObjectLoan").unwrap(),
540 ExchangeType::ObjectLoan
541 );
542 assert_eq!(
543 ExchangeType::from_sql("SharedPurchase").unwrap(),
544 ExchangeType::SharedPurchase
545 );
546 }
547
548 #[test]
549 fn test_exchange_status_sql_conversion() {
550 assert_eq!(ExchangeStatus::Offered.to_sql(), "Offered");
551 assert_eq!(ExchangeStatus::Requested.to_sql(), "Requested");
552 assert_eq!(ExchangeStatus::InProgress.to_sql(), "InProgress");
553 assert_eq!(ExchangeStatus::Completed.to_sql(), "Completed");
554 assert_eq!(ExchangeStatus::Cancelled.to_sql(), "Cancelled");
555
556 assert_eq!(
557 ExchangeStatus::from_sql("Offered").unwrap(),
558 ExchangeStatus::Offered
559 );
560 assert_eq!(
561 ExchangeStatus::from_sql("Requested").unwrap(),
562 ExchangeStatus::Requested
563 );
564 assert_eq!(
565 ExchangeStatus::from_sql("InProgress").unwrap(),
566 ExchangeStatus::InProgress
567 );
568 assert_eq!(
569 ExchangeStatus::from_sql("Completed").unwrap(),
570 ExchangeStatus::Completed
571 );
572 assert_eq!(
573 ExchangeStatus::from_sql("Cancelled").unwrap(),
574 ExchangeStatus::Cancelled
575 );
576 }
577}