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, utoipa::ToSchema)]
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, utoipa::ToSchema)]
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 moderate_cancel(&mut self, reason: String) -> Result<(), String> {
254 if self.status == ExchangeStatus::Completed {
255 return Err("Cannot cancel a completed exchange".to_string());
256 }
257
258 if self.status == ExchangeStatus::Cancelled {
259 return Err("Exchange is already cancelled".to_string());
260 }
261
262 self.status = ExchangeStatus::Cancelled;
263 self.cancelled_at = Some(Utc::now());
264 self.cancellation_reason = Some(reason);
265 self.updated_at = Utc::now();
266
267 Ok(())
268 }
269
270 pub fn rate_provider(&mut self, requester_id: Uuid, rating: i32) -> Result<(), String> {
272 if self.status != ExchangeStatus::Completed {
273 return Err("Can only rate completed exchanges".to_string());
274 }
275
276 if self.requester_id != Some(requester_id) {
277 return Err("Only the requester can rate the provider".to_string());
278 }
279
280 if !(1..=5).contains(&rating) {
281 return Err("Rating must be between 1 and 5".to_string());
282 }
283
284 self.provider_rating = Some(rating);
285 self.updated_at = Utc::now();
286
287 Ok(())
288 }
289
290 pub fn rate_requester(&mut self, provider_id: Uuid, rating: i32) -> Result<(), String> {
292 if self.status != ExchangeStatus::Completed {
293 return Err("Can only rate completed exchanges".to_string());
294 }
295
296 if self.provider_id != provider_id {
297 return Err("Only the provider can rate the requester".to_string());
298 }
299
300 if !(1..=5).contains(&rating) {
301 return Err("Rating must be between 1 and 5".to_string());
302 }
303
304 self.requester_rating = Some(rating);
305 self.updated_at = Utc::now();
306
307 Ok(())
308 }
309
310 pub fn is_active(&self) -> bool {
312 matches!(
313 self.status,
314 ExchangeStatus::Offered | ExchangeStatus::Requested | ExchangeStatus::InProgress
315 )
316 }
317
318 pub fn has_mutual_ratings(&self) -> bool {
320 self.provider_rating.is_some() && self.requester_rating.is_some()
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[test]
329 fn test_create_exchange_success() {
330 let building_id = Uuid::new_v4();
331 let provider_id = Uuid::new_v4();
332
333 let exchange = LocalExchange::new(
334 building_id,
335 provider_id,
336 ExchangeType::Service,
337 "Gardening help".to_string(),
338 "I can help with planting and weeding".to_string(),
339 2,
340 );
341
342 assert!(exchange.is_ok());
343 let exchange = exchange.unwrap();
344 assert_eq!(exchange.building_id, building_id);
345 assert_eq!(exchange.provider_id, provider_id);
346 assert_eq!(exchange.status, ExchangeStatus::Offered);
347 assert_eq!(exchange.credits, 2);
348 assert!(exchange.requester_id.is_none());
349 }
350
351 #[test]
352 fn test_create_exchange_validation() {
353 let building_id = Uuid::new_v4();
354 let provider_id = Uuid::new_v4();
355
356 let result = LocalExchange::new(
358 building_id,
359 provider_id,
360 ExchangeType::Service,
361 "".to_string(),
362 "Description".to_string(),
363 2,
364 );
365 assert!(result.is_err());
366
367 let result = LocalExchange::new(
369 building_id,
370 provider_id,
371 ExchangeType::Service,
372 "Title".to_string(),
373 "Description".to_string(),
374 -1,
375 );
376 assert!(result.is_err());
377
378 let result = LocalExchange::new(
380 building_id,
381 provider_id,
382 ExchangeType::Service,
383 "Title".to_string(),
384 "Description".to_string(),
385 101,
386 );
387 assert!(result.is_err());
388 }
389
390 #[test]
391 fn test_exchange_workflow() {
392 let building_id = Uuid::new_v4();
393 let provider_id = Uuid::new_v4();
394 let requester_id = Uuid::new_v4();
395
396 let mut exchange = LocalExchange::new(
397 building_id,
398 provider_id,
399 ExchangeType::Service,
400 "Babysitting".to_string(),
401 "Can watch kids for 3 hours".to_string(),
402 3,
403 )
404 .unwrap();
405
406 assert!(exchange.request(requester_id).is_ok());
408 assert_eq!(exchange.status, ExchangeStatus::Requested);
409 assert_eq!(exchange.requester_id, Some(requester_id));
410
411 assert!(exchange.start(provider_id).is_ok());
413 assert_eq!(exchange.status, ExchangeStatus::InProgress);
414
415 assert!(exchange.complete(provider_id).is_ok());
417 assert_eq!(exchange.status, ExchangeStatus::Completed);
418 assert!(exchange.completed_at.is_some());
419 }
420
421 #[test]
422 fn test_cannot_request_own_exchange() {
423 let building_id = Uuid::new_v4();
424 let provider_id = Uuid::new_v4();
425
426 let mut exchange = LocalExchange::new(
427 building_id,
428 provider_id,
429 ExchangeType::Service,
430 "Service".to_string(),
431 "Description".to_string(),
432 2,
433 )
434 .unwrap();
435
436 let result = exchange.request(provider_id);
437 assert!(result.is_err());
438 }
439
440 #[test]
441 fn test_cancel_exchange() {
442 let building_id = Uuid::new_v4();
443 let provider_id = Uuid::new_v4();
444 let requester_id = Uuid::new_v4();
445
446 let mut exchange = LocalExchange::new(
447 building_id,
448 provider_id,
449 ExchangeType::Service,
450 "Service".to_string(),
451 "Description".to_string(),
452 2,
453 )
454 .unwrap();
455
456 exchange.request(requester_id).unwrap();
457
458 assert!(exchange
460 .cancel(requester_id, Some("Changed my mind".to_string()))
461 .is_ok());
462 assert_eq!(exchange.status, ExchangeStatus::Cancelled);
463 assert!(exchange.cancelled_at.is_some());
464 assert_eq!(
465 exchange.cancellation_reason,
466 Some("Changed my mind".to_string())
467 );
468 }
469
470 #[test]
475 fn happy_moderate_cancel_par_un_tiers_a_l_echange() {
476 let building_id = Uuid::new_v4();
477 let provider_id = Uuid::new_v4();
478 let requester_id = Uuid::new_v4();
479
480 let mut exchange = LocalExchange::new(
481 building_id,
482 provider_id,
483 ExchangeType::Service,
484 "Service".to_string(),
485 "Description".to_string(),
486 2,
487 )
488 .unwrap();
489 exchange.request(requester_id).unwrap();
490
491 let result = exchange.moderate_cancel("Litige signalé par un voisin".to_string());
495 assert!(result.is_ok(), "moderate_cancel failed: {:?}", result.err());
496 assert_eq!(exchange.status, ExchangeStatus::Cancelled);
497 assert_eq!(
498 exchange.cancellation_reason,
499 Some("Litige signalé par un voisin".to_string())
500 );
501 }
502
503 #[test]
504 fn negative_moderate_cancel_refuse_un_echange_deja_complete() {
505 let building_id = Uuid::new_v4();
506 let provider_id = Uuid::new_v4();
507 let requester_id = Uuid::new_v4();
508
509 let mut exchange = LocalExchange::new(
510 building_id,
511 provider_id,
512 ExchangeType::Service,
513 "Service".to_string(),
514 "Description".to_string(),
515 2,
516 )
517 .unwrap();
518 exchange.request(requester_id).unwrap();
519 exchange.start(provider_id).unwrap();
520 exchange.complete(provider_id).unwrap();
521
522 let result = exchange.moderate_cancel("Motif".to_string());
523 assert!(result.is_err());
524 assert!(result.unwrap_err().contains("Cannot cancel a completed"));
525 }
526
527 #[test]
528 fn edge_moderate_cancel_refuse_un_echange_deja_annule() {
529 let building_id = Uuid::new_v4();
530 let provider_id = Uuid::new_v4();
531
532 let mut exchange = LocalExchange::new(
533 building_id,
534 provider_id,
535 ExchangeType::Service,
536 "Service".to_string(),
537 "Description".to_string(),
538 2,
539 )
540 .unwrap();
541 exchange
542 .moderate_cancel("Premier motif".to_string())
543 .unwrap();
544
545 let result = exchange.moderate_cancel("Second motif".to_string());
546 assert!(result.is_err());
547 assert!(result.unwrap_err().contains("already cancelled"));
548 }
549
550 #[test]
551 fn test_cannot_cancel_completed_exchange() {
552 let building_id = Uuid::new_v4();
553 let provider_id = Uuid::new_v4();
554 let requester_id = Uuid::new_v4();
555
556 let mut exchange = LocalExchange::new(
557 building_id,
558 provider_id,
559 ExchangeType::Service,
560 "Service".to_string(),
561 "Description".to_string(),
562 2,
563 )
564 .unwrap();
565
566 exchange.request(requester_id).unwrap();
567 exchange.start(provider_id).unwrap();
568 exchange.complete(provider_id).unwrap();
569
570 let result = exchange.cancel(provider_id, None);
571 assert!(result.is_err());
572 }
573
574 #[test]
575 fn test_ratings() {
576 let building_id = Uuid::new_v4();
577 let provider_id = Uuid::new_v4();
578 let requester_id = Uuid::new_v4();
579
580 let mut exchange = LocalExchange::new(
581 building_id,
582 provider_id,
583 ExchangeType::Service,
584 "Service".to_string(),
585 "Description".to_string(),
586 2,
587 )
588 .unwrap();
589
590 exchange.request(requester_id).unwrap();
591 exchange.start(provider_id).unwrap();
592 exchange.complete(provider_id).unwrap();
593
594 assert!(exchange.rate_provider(requester_id, 5).is_ok());
596 assert_eq!(exchange.provider_rating, Some(5));
597
598 assert!(exchange.rate_requester(provider_id, 4).is_ok());
600 assert_eq!(exchange.requester_rating, Some(4));
601
602 assert!(exchange.has_mutual_ratings());
603 }
604
605 #[test]
606 fn test_rating_validation() {
607 let building_id = Uuid::new_v4();
608 let provider_id = Uuid::new_v4();
609 let requester_id = Uuid::new_v4();
610
611 let mut exchange = LocalExchange::new(
612 building_id,
613 provider_id,
614 ExchangeType::Service,
615 "Service".to_string(),
616 "Description".to_string(),
617 2,
618 )
619 .unwrap();
620
621 exchange.request(requester_id).unwrap();
622 exchange.start(provider_id).unwrap();
623 exchange.complete(provider_id).unwrap();
624
625 assert!(exchange.rate_provider(requester_id, 0).is_err());
627
628 assert!(exchange.rate_provider(requester_id, 6).is_err());
630
631 assert!(exchange.rate_provider(provider_id, 5).is_err());
633 }
634
635 #[test]
636 fn test_exchange_type_sql_conversion() {
637 assert_eq!(ExchangeType::Service.to_sql(), "Service");
638 assert_eq!(ExchangeType::ObjectLoan.to_sql(), "ObjectLoan");
639 assert_eq!(ExchangeType::SharedPurchase.to_sql(), "SharedPurchase");
640
641 assert_eq!(
642 ExchangeType::from_sql("Service").unwrap(),
643 ExchangeType::Service
644 );
645 assert_eq!(
646 ExchangeType::from_sql("ObjectLoan").unwrap(),
647 ExchangeType::ObjectLoan
648 );
649 assert_eq!(
650 ExchangeType::from_sql("SharedPurchase").unwrap(),
651 ExchangeType::SharedPurchase
652 );
653 }
654
655 #[test]
656 fn test_exchange_status_sql_conversion() {
657 assert_eq!(ExchangeStatus::Offered.to_sql(), "Offered");
658 assert_eq!(ExchangeStatus::Requested.to_sql(), "Requested");
659 assert_eq!(ExchangeStatus::InProgress.to_sql(), "InProgress");
660 assert_eq!(ExchangeStatus::Completed.to_sql(), "Completed");
661 assert_eq!(ExchangeStatus::Cancelled.to_sql(), "Cancelled");
662
663 assert_eq!(
664 ExchangeStatus::from_sql("Offered").unwrap(),
665 ExchangeStatus::Offered
666 );
667 assert_eq!(
668 ExchangeStatus::from_sql("Requested").unwrap(),
669 ExchangeStatus::Requested
670 );
671 assert_eq!(
672 ExchangeStatus::from_sql("InProgress").unwrap(),
673 ExchangeStatus::InProgress
674 );
675 assert_eq!(
676 ExchangeStatus::from_sql("Completed").unwrap(),
677 ExchangeStatus::Completed
678 );
679 assert_eq!(
680 ExchangeStatus::from_sql("Cancelled").unwrap(),
681 ExchangeStatus::Cancelled
682 );
683 }
684}