1use chrono::{DateTime, Utc};
30use rust_decimal::Decimal;
31use serde::{Deserialize, Serialize};
32use sqlx::PgPool;
33use uuid::Uuid;
34
35use crate::error::ReputationError;
36use crate::score::{ReputationScore, ScoreComponents};
37use crate::tier::ReputationTier;
38
39type CommitmentRow = (
41 Uuid,
42 Uuid,
43 String,
44 String,
45 DateTime<Utc>,
46 String,
47 DateTime<Utc>,
48 Option<DateTime<Utc>>,
49);
50type RatingRow = (Uuid, Uuid, Uuid, Uuid, i32, Option<String>, DateTime<Utc>);
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct UserReputationExport {
55 pub user_id: Uuid,
56 pub exported_at: DateTime<Utc>,
57 pub score: ReputationScore,
58 pub tier_history: Vec<TierHistoryExport>,
59 pub score_history: Vec<ScoreHistoryExport>,
60 pub events: Vec<EventExport>,
61 pub commitments: Vec<CommitmentExport>,
62 pub ratings_given: Vec<RatingExport>,
63 pub ratings_received: Vec<RatingExport>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct TierHistoryExport {
69 pub tier: ReputationTier,
70 pub score: Decimal,
71 pub changed_at: DateTime<Utc>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct ScoreHistoryExport {
77 pub score: Decimal,
78 pub components: ScoreComponents,
79 pub snapshot_date: DateTime<Utc>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct EventExport {
85 pub event_id: Uuid,
86 pub event_type: String,
87 pub delta: Decimal,
88 pub reason: String,
89 pub created_at: DateTime<Utc>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct CommitmentExport {
95 pub commitment_id: Uuid,
96 pub token_id: Uuid,
97 pub title: String,
98 pub description: String,
99 pub deadline: DateTime<Utc>,
100 pub status: String,
101 pub created_at: DateTime<Utc>,
102 pub completed_at: Option<DateTime<Utc>>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct RatingExport {
108 pub rating_id: Uuid,
109 pub rater_user_id: Uuid,
110 pub issuer_user_id: Uuid,
111 pub token_id: Uuid,
112 pub rating: i32,
113 pub feedback: Option<String>,
114 pub created_at: DateTime<Utc>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct BulkReputationExport {
120 pub exported_at: DateTime<Utc>,
121 pub version: String,
122 pub user_count: usize,
123 pub users: Vec<UserReputationExport>,
124}
125
126pub struct ExportImportService {
128 pool: PgPool,
129}
130
131impl ExportImportService {
132 pub fn new(pool: PgPool) -> Self {
134 Self { pool }
135 }
136
137 pub async fn export_user(
139 &self,
140 user_id: Uuid,
141 ) -> Result<UserReputationExport, ReputationError> {
142 let score_row: (Uuid, Decimal) =
144 sqlx::query_as("SELECT user_id, reputation_score FROM users WHERE user_id = $1")
145 .bind(user_id)
146 .fetch_one(&self.pool)
147 .await?;
148
149 let score = ReputationScore {
150 user_id,
151 overall_score: score_row.1,
152 tier: ReputationTier::from_score(score_row.1),
153 components: ScoreComponents::default(),
154 };
155
156 let tier_history = self.export_tier_history(user_id).await?;
158
159 let score_history = self.export_score_history(user_id).await?;
161
162 let events = self.export_events(user_id).await?;
164
165 let commitments = self.export_commitments(user_id).await?;
167
168 let ratings_given = self.export_ratings_given(user_id).await?;
170 let ratings_received = self.export_ratings_received(user_id).await?;
171
172 Ok(UserReputationExport {
173 user_id,
174 exported_at: Utc::now(),
175 score,
176 tier_history,
177 score_history,
178 events,
179 commitments,
180 ratings_given,
181 ratings_received,
182 })
183 }
184
185 pub async fn export_bulk(
187 &self,
188 user_ids: Vec<Uuid>,
189 ) -> Result<BulkReputationExport, ReputationError> {
190 let mut users = Vec::new();
191
192 for user_id in &user_ids {
193 let user_export = self.export_user(*user_id).await?;
194 users.push(user_export);
195 }
196
197 Ok(BulkReputationExport {
198 exported_at: Utc::now(),
199 version: "1.0.0".to_string(),
200 user_count: users.len(),
201 users,
202 })
203 }
204
205 pub async fn export_all(&self) -> Result<BulkReputationExport, ReputationError> {
207 let user_rows: Vec<(Uuid,)> = sqlx::query_as("SELECT user_id FROM users ORDER BY user_id")
208 .fetch_all(&self.pool)
209 .await?;
210
211 let user_ids: Vec<Uuid> = user_rows.iter().map(|row| row.0).collect();
212 self.export_bulk(user_ids).await
213 }
214
215 pub fn export_to_json(export: &BulkReputationExport) -> Result<String, ReputationError> {
217 serde_json::to_string_pretty(export)
218 .map_err(|e| ReputationError::Validation(format!("JSON serialization failed: {}", e)))
219 }
220
221 pub fn import_from_json(json: &str) -> Result<BulkReputationExport, ReputationError> {
223 serde_json::from_str(json)
224 .map_err(|e| ReputationError::Validation(format!("JSON deserialization failed: {}", e)))
225 }
226
227 async fn export_tier_history(
230 &self,
231 user_id: Uuid,
232 ) -> Result<Vec<TierHistoryExport>, ReputationError> {
233 let rows: Vec<(String, Decimal, DateTime<Utc>)> = sqlx::query_as(
234 "SELECT tier, score, changed_at FROM tier_history WHERE user_id = $1 ORDER BY changed_at DESC"
235 )
236 .bind(user_id)
237 .fetch_all(&self.pool)
238 .await?;
239
240 Ok(rows
241 .into_iter()
242 .map(|row| TierHistoryExport {
243 tier: ReputationTier::from_score(row.1),
244 score: row.1,
245 changed_at: row.2,
246 })
247 .collect())
248 }
249
250 async fn export_score_history(
251 &self,
252 user_id: Uuid,
253 ) -> Result<Vec<ScoreHistoryExport>, ReputationError> {
254 let rows: Vec<(Decimal, DateTime<Utc>)> = sqlx::query_as(
255 "SELECT score, snapshot_date FROM score_snapshots WHERE user_id = $1 ORDER BY snapshot_date DESC"
256 )
257 .bind(user_id)
258 .fetch_all(&self.pool)
259 .await?;
260
261 Ok(rows
262 .into_iter()
263 .map(|row| ScoreHistoryExport {
264 score: row.0,
265 components: ScoreComponents::default(),
266 snapshot_date: row.1,
267 })
268 .collect())
269 }
270
271 async fn export_events(&self, user_id: Uuid) -> Result<Vec<EventExport>, ReputationError> {
272 let rows: Vec<(Uuid, String, Decimal, String, DateTime<Utc>)> = sqlx::query_as(
273 "SELECT event_id, event_type, delta, reason, created_at FROM reputation_events WHERE user_id = $1 ORDER BY created_at DESC"
274 )
275 .bind(user_id)
276 .fetch_all(&self.pool)
277 .await?;
278
279 Ok(rows
280 .into_iter()
281 .map(|row| EventExport {
282 event_id: row.0,
283 event_type: row.1,
284 delta: row.2,
285 reason: row.3,
286 created_at: row.4,
287 })
288 .collect())
289 }
290
291 async fn export_commitments(
292 &self,
293 user_id: Uuid,
294 ) -> Result<Vec<CommitmentExport>, ReputationError> {
295 let rows: Vec<CommitmentRow> = sqlx::query_as(
296 "SELECT commitment_id, token_id, title, description, deadline, status, created_at, completed_at FROM output_commitments WHERE user_id = $1 ORDER BY created_at DESC"
297 )
298 .bind(user_id)
299 .fetch_all(&self.pool)
300 .await?;
301
302 Ok(rows
303 .into_iter()
304 .map(|row| CommitmentExport {
305 commitment_id: row.0,
306 token_id: row.1,
307 title: row.2,
308 description: row.3,
309 deadline: row.4,
310 status: row.5,
311 created_at: row.6,
312 completed_at: row.7,
313 })
314 .collect())
315 }
316
317 async fn export_ratings_given(
318 &self,
319 user_id: Uuid,
320 ) -> Result<Vec<RatingExport>, ReputationError> {
321 let rows: Vec<RatingRow> = sqlx::query_as(
322 "SELECT rating_id, rater_user_id, issuer_user_id, token_id, rating, feedback, created_at FROM holder_ratings WHERE rater_user_id = $1 ORDER BY created_at DESC"
323 )
324 .bind(user_id)
325 .fetch_all(&self.pool)
326 .await?;
327
328 Ok(rows
329 .into_iter()
330 .map(|row| RatingExport {
331 rating_id: row.0,
332 rater_user_id: row.1,
333 issuer_user_id: row.2,
334 token_id: row.3,
335 rating: row.4,
336 feedback: row.5,
337 created_at: row.6,
338 })
339 .collect())
340 }
341
342 async fn export_ratings_received(
343 &self,
344 user_id: Uuid,
345 ) -> Result<Vec<RatingExport>, ReputationError> {
346 let rows: Vec<RatingRow> = sqlx::query_as(
347 "SELECT rating_id, rater_user_id, issuer_user_id, token_id, rating, feedback, created_at FROM holder_ratings WHERE issuer_user_id = $1 ORDER BY created_at DESC"
348 )
349 .bind(user_id)
350 .fetch_all(&self.pool)
351 .await?;
352
353 Ok(rows
354 .into_iter()
355 .map(|row| RatingExport {
356 rating_id: row.0,
357 rater_user_id: row.1,
358 issuer_user_id: row.2,
359 token_id: row.3,
360 rating: row.4,
361 feedback: row.5,
362 created_at: row.6,
363 })
364 .collect())
365 }
366
367 pub fn export_scores_to_csv(
376 &self,
377 scores: &[ReputationScore],
378 ) -> Result<String, ReputationError> {
379 let mut csv = String::from("user_id,overall_score,tier,timestamp\n");
380
381 let timestamp = Utc::now().to_rfc3339();
382 for score in scores {
383 csv.push_str(&format!(
384 "{},{},{},{}\n",
385 score.user_id,
386 score.overall_score,
387 score.tier.as_str(),
388 timestamp
389 ));
390 }
391
392 Ok(csv)
393 }
394
395 pub fn export_scores_detailed_to_csv(
400 &self,
401 scores: &[ReputationScore],
402 ) -> Result<String, ReputationError> {
403 let mut csv = String::from(
404 "user_id,overall_score,tier,commitment_fulfillment,response_time,quality_rating,community_trust,longevity,timestamp\n",
405 );
406
407 let timestamp = Utc::now().to_rfc3339();
408 for score in scores {
409 csv.push_str(&format!(
410 "{},{},{},{},{},{},{},{},{}\n",
411 score.user_id,
412 score.overall_score,
413 score.tier.as_str(),
414 score.components.commitment_fulfillment,
415 score.components.response_time,
416 score.components.quality_rating,
417 score.components.community_trust,
418 score.components.longevity,
419 timestamp
420 ));
421 }
422
423 Ok(csv)
424 }
425
426 pub fn export_events_to_csv(&self, events: &[EventExport]) -> Result<String, ReputationError> {
430 let mut csv = String::from("event_id,user_id,event_type,delta,reason,created_at\n");
431
432 for event in events {
433 let reason = event.reason.replace(',', ";").replace('\n', " ");
434 csv.push_str(&format!(
435 "{},{},{},{},\"{}\",{}\n",
436 event.event_id,
437 "", event.event_type,
439 event.delta,
440 reason,
441 event.created_at.to_rfc3339()
442 ));
443 }
444
445 Ok(csv)
446 }
447
448 pub fn export_commitments_to_csv(
452 &self,
453 commitments: &[CommitmentExport],
454 ) -> Result<String, ReputationError> {
455 let mut csv = String::from(
456 "commitment_id,token_id,title,description,deadline,status,created_at,completed_at\n",
457 );
458
459 for commitment in commitments {
460 let title = commitment.title.replace(',', ";").replace('\n', " ");
461 let description = commitment.description.replace(',', ";").replace('\n', " ");
462 let completed_at = commitment
463 .completed_at
464 .map(|dt| dt.to_rfc3339())
465 .unwrap_or_else(|| "".to_string());
466
467 csv.push_str(&format!(
468 "{},{},\"{}\",\"{}\",{},{},{},{}\n",
469 commitment.commitment_id,
470 commitment.token_id,
471 title,
472 description,
473 commitment.deadline.to_rfc3339(),
474 commitment.status,
475 commitment.created_at.to_rfc3339(),
476 completed_at
477 ));
478 }
479
480 Ok(csv)
481 }
482
483 pub fn export_ratings_to_csv(
487 &self,
488 ratings: &[RatingExport],
489 ) -> Result<String, ReputationError> {
490 let mut csv = String::from(
491 "rating_id,rater_user_id,issuer_user_id,token_id,rating,feedback,created_at\n",
492 );
493
494 for rating in ratings {
495 let feedback = rating
496 .feedback
497 .as_ref()
498 .map(|f| f.replace(',', ";").replace('\n', " ").replace('"', "'"))
499 .unwrap_or_else(|| "".to_string());
500
501 csv.push_str(&format!(
502 "{},{},{},{},{},\"{}\",{}\n",
503 rating.rating_id,
504 rating.rater_user_id,
505 rating.issuer_user_id,
506 rating.token_id,
507 rating.rating,
508 feedback,
509 rating.created_at.to_rfc3339()
510 ));
511 }
512
513 Ok(csv)
514 }
515
516 pub fn export_tier_history_to_csv(
520 &self,
521 history: &[TierHistoryExport],
522 ) -> Result<String, ReputationError> {
523 let mut csv = String::from("tier,score,changed_at\n");
524
525 for entry in history {
526 csv.push_str(&format!(
527 "{},{},{}\n",
528 entry.tier.as_str(),
529 entry.score,
530 entry.changed_at.to_rfc3339()
531 ));
532 }
533
534 Ok(csv)
535 }
536
537 pub fn export_score_history_to_csv(
542 &self,
543 history: &[ScoreHistoryExport],
544 ) -> Result<String, ReputationError> {
545 let mut csv = String::from(
546 "score,commitment_fulfillment,response_time,quality_rating,community_trust,longevity,snapshot_date\n",
547 );
548
549 for entry in history {
550 csv.push_str(&format!(
551 "{},{},{},{},{},{},{}\n",
552 entry.score,
553 entry.components.commitment_fulfillment,
554 entry.components.response_time,
555 entry.components.quality_rating,
556 entry.components.community_trust,
557 entry.components.longevity,
558 entry.snapshot_date.to_rfc3339()
559 ));
560 }
561
562 Ok(csv)
563 }
564}
565
566#[derive(Debug, Clone, Serialize, Deserialize)]
568pub struct ExportStats {
569 pub total_users: usize,
570 pub total_events: usize,
571 pub total_commitments: usize,
572 pub total_ratings: usize,
573 pub export_size_bytes: usize,
574}
575
576impl BulkReputationExport {
577 pub fn get_stats(&self) -> ExportStats {
579 let total_events: usize = self.users.iter().map(|u| u.events.len()).sum();
580 let total_commitments: usize = self.users.iter().map(|u| u.commitments.len()).sum();
581 let total_ratings: usize = self
582 .users
583 .iter()
584 .map(|u| u.ratings_given.len() + u.ratings_received.len())
585 .sum();
586
587 ExportStats {
588 total_users: self.user_count,
589 total_events,
590 total_commitments,
591 total_ratings,
592 export_size_bytes: 0, }
594 }
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600
601 #[test]
602 fn test_export_stats() {
603 let export = BulkReputationExport {
604 exported_at: Utc::now(),
605 version: "1.0.0".to_string(),
606 user_count: 2,
607 users: vec![UserReputationExport {
608 user_id: Uuid::new_v4(),
609 exported_at: Utc::now(),
610 score: ReputationScore {
611 user_id: Uuid::new_v4(),
612 overall_score: rust_decimal_macros::dec!(500),
613 tier: ReputationTier::Silver,
614 components: Default::default(),
615 },
616 tier_history: vec![],
617 score_history: vec![],
618 events: vec![],
619 commitments: vec![],
620 ratings_given: vec![],
621 ratings_received: vec![],
622 }],
623 };
624
625 let stats = export.get_stats();
626 assert_eq!(stats.total_users, 2);
627 }
628
629 #[test]
630 fn test_json_export_import() {
631 let export = BulkReputationExport {
632 exported_at: Utc::now(),
633 version: "1.0.0".to_string(),
634 user_count: 1,
635 users: vec![UserReputationExport {
636 user_id: Uuid::new_v4(),
637 exported_at: Utc::now(),
638 score: ReputationScore {
639 user_id: Uuid::new_v4(),
640 overall_score: rust_decimal_macros::dec!(500),
641 tier: ReputationTier::Silver,
642 components: Default::default(),
643 },
644 tier_history: vec![],
645 score_history: vec![],
646 events: vec![],
647 commitments: vec![],
648 ratings_given: vec![],
649 ratings_received: vec![],
650 }],
651 };
652
653 let json = ExportImportService::export_to_json(&export).unwrap();
654 let imported = ExportImportService::import_from_json(&json).unwrap();
655
656 assert_eq!(imported.user_count, 1);
657 assert_eq!(imported.version, "1.0.0");
658 }
659
660 fn mock_export_service() -> ExportImportService {
664 let options = sqlx::postgres::PgConnectOptions::new();
667 let pool = PgPool::connect_lazy_with(options);
668 ExportImportService::new(pool)
669 }
670
671 #[tokio::test]
672 async fn test_csv_export_scores() {
673 use rust_decimal_macros::dec;
674
675 let service = mock_export_service();
676
677 let user_id = Uuid::new_v4();
678 let scores = vec![
679 ReputationScore {
680 user_id,
681 overall_score: dec!(750),
682 tier: ReputationTier::Gold,
683 components: Default::default(),
684 },
685 ReputationScore {
686 user_id: Uuid::new_v4(),
687 overall_score: dec!(500),
688 tier: ReputationTier::Silver,
689 components: Default::default(),
690 },
691 ];
692
693 let csv = service.export_scores_to_csv(&scores).unwrap();
694
695 assert!(csv.starts_with("user_id,overall_score,tier,timestamp\n"));
697
698 assert!(csv.contains(&user_id.to_string()));
700 assert!(csv.contains("750"));
701 assert!(csv.contains("gold"));
702 assert!(csv.contains("500"));
703 assert!(csv.contains("silver"));
704
705 assert_eq!(csv.lines().count(), 3);
707 }
708
709 #[tokio::test]
710 async fn test_csv_export_scores_detailed() {
711 use rust_decimal_macros::dec;
712
713 let service = mock_export_service();
714
715 let user_id = Uuid::new_v4();
716 let scores = vec![ReputationScore {
717 user_id,
718 overall_score: dec!(750),
719 tier: ReputationTier::Gold,
720 components: ScoreComponents {
721 commitment_fulfillment: dec!(85),
722 response_time: dec!(75),
723 quality_rating: dec!(90),
724 community_trust: dec!(80),
725 longevity: dec!(70),
726 },
727 }];
728
729 let csv = service.export_scores_detailed_to_csv(&scores).unwrap();
730
731 assert!(csv.contains("commitment_fulfillment"));
733 assert!(csv.contains("response_time"));
734 assert!(csv.contains("quality_rating"));
735 assert!(csv.contains("community_trust"));
736 assert!(csv.contains("longevity"));
737
738 assert!(csv.contains("85"));
740 assert!(csv.contains("75"));
741 assert!(csv.contains("90"));
742 assert!(csv.contains("80"));
743 assert!(csv.contains("70"));
744 }
745
746 #[tokio::test]
747 async fn test_csv_export_events() {
748 let service = mock_export_service();
749
750 let events = vec![
751 EventExport {
752 event_id: Uuid::new_v4(),
753 event_type: "commitment_fulfilled".to_string(),
754 delta: rust_decimal_macros::dec!(10),
755 reason: "Delivered on time".to_string(),
756 created_at: Utc::now(),
757 },
758 EventExport {
759 event_id: Uuid::new_v4(),
760 event_type: "quality_score".to_string(),
761 delta: rust_decimal_macros::dec!(5),
762 reason: "High quality work, includes commas".to_string(),
763 created_at: Utc::now(),
764 },
765 ];
766
767 let csv = service.export_events_to_csv(&events).unwrap();
768
769 assert!(csv.starts_with("event_id,user_id,event_type,delta,reason,created_at\n"));
771
772 assert!(csv.contains("commitment_fulfilled"));
774 assert!(csv.contains("quality_score"));
775 assert!(csv.contains("10"));
776 assert!(csv.contains("5"));
777
778 assert!(csv.contains("includes commas"));
780 assert!(csv.contains(';')); }
782
783 #[tokio::test]
784 async fn test_csv_export_commitments() {
785 let pool = PgPool::connect_lazy("postgresql://localhost/test").unwrap();
786 let service = ExportImportService::new(pool);
787
788 let commitment_id = Uuid::new_v4();
789 let token_id = Uuid::new_v4();
790 let commitments = vec![CommitmentExport {
791 commitment_id,
792 token_id,
793 title: "Complete documentation".to_string(),
794 description: "Write comprehensive API docs, includes commas and newlines\nNext line"
795 .to_string(),
796 deadline: Utc::now(),
797 status: "verified".to_string(),
798 created_at: Utc::now(),
799 completed_at: Some(Utc::now()),
800 }];
801
802 let csv = service.export_commitments_to_csv(&commitments).unwrap();
803
804 assert!(csv.contains("commitment_id,token_id,title,description"));
806
807 assert!(csv.contains(&commitment_id.to_string()));
809 assert!(csv.contains(&token_id.to_string()));
810 assert!(csv.contains("Complete documentation"));
811 assert!(csv.contains("verified"));
812
813 assert!(!csv.contains("\n\nNext")); }
816
817 #[tokio::test]
818 async fn test_csv_export_ratings() {
819 let pool = PgPool::connect_lazy("postgresql://localhost/test").unwrap();
820 let service = ExportImportService::new(pool);
821
822 let ratings = vec![
823 RatingExport {
824 rating_id: Uuid::new_v4(),
825 rater_user_id: Uuid::new_v4(),
826 issuer_user_id: Uuid::new_v4(),
827 token_id: Uuid::new_v4(),
828 rating: 5,
829 feedback: Some("Excellent work!".to_string()),
830 created_at: Utc::now(),
831 },
832 RatingExport {
833 rating_id: Uuid::new_v4(),
834 rater_user_id: Uuid::new_v4(),
835 issuer_user_id: Uuid::new_v4(),
836 token_id: Uuid::new_v4(),
837 rating: 3,
838 feedback: None,
839 created_at: Utc::now(),
840 },
841 ];
842
843 let csv = service.export_ratings_to_csv(&ratings).unwrap();
844
845 assert!(csv.starts_with(
847 "rating_id,rater_user_id,issuer_user_id,token_id,rating,feedback,created_at\n"
848 ));
849
850 assert!(csv.contains("5"));
852 assert!(csv.contains("3"));
853 assert!(csv.contains("Excellent work!"));
854
855 assert_eq!(csv.lines().count(), 3); }
858
859 #[tokio::test]
860 async fn test_csv_export_tier_history() {
861 use rust_decimal_macros::dec;
862
863 let pool = PgPool::connect_lazy("postgresql://localhost/test").unwrap();
864 let service = ExportImportService::new(pool);
865
866 let history = vec![
867 TierHistoryExport {
868 tier: ReputationTier::Bronze,
869 score: dec!(250),
870 changed_at: Utc::now(),
871 },
872 TierHistoryExport {
873 tier: ReputationTier::Silver,
874 score: dec!(450),
875 changed_at: Utc::now(),
876 },
877 TierHistoryExport {
878 tier: ReputationTier::Gold,
879 score: dec!(750),
880 changed_at: Utc::now(),
881 },
882 ];
883
884 let csv = service.export_tier_history_to_csv(&history).unwrap();
885
886 assert!(csv.starts_with("tier,score,changed_at\n"));
888
889 assert!(csv.contains("bronze"));
891 assert!(csv.contains("silver"));
892 assert!(csv.contains("gold"));
893 assert!(csv.contains("250"));
894 assert!(csv.contains("450"));
895 assert!(csv.contains("750"));
896
897 assert_eq!(csv.lines().count(), 4); }
900
901 #[tokio::test]
902 async fn test_csv_export_score_history() {
903 use rust_decimal_macros::dec;
904
905 let pool = PgPool::connect_lazy("postgresql://localhost/test").unwrap();
906 let service = ExportImportService::new(pool);
907
908 let history = vec![ScoreHistoryExport {
909 score: dec!(750),
910 components: ScoreComponents {
911 commitment_fulfillment: dec!(85),
912 response_time: dec!(75),
913 quality_rating: dec!(90),
914 community_trust: dec!(80),
915 longevity: dec!(70),
916 },
917 snapshot_date: Utc::now(),
918 }];
919
920 let csv = service.export_score_history_to_csv(&history).unwrap();
921
922 assert!(csv.contains("score,commitment_fulfillment,response_time,quality_rating,community_trust,longevity,snapshot_date"));
924
925 assert!(csv.contains("750"));
927 assert!(csv.contains("85"));
928 assert!(csv.contains("75"));
929 assert!(csv.contains("90"));
930 assert!(csv.contains("80"));
931 assert!(csv.contains("70"));
932
933 assert_eq!(csv.lines().count(), 2); }
936
937 #[tokio::test]
938 async fn test_csv_empty_arrays() {
939 let pool = PgPool::connect_lazy("postgresql://localhost/test").unwrap();
940 let service = ExportImportService::new(pool);
941
942 let scores: Vec<ReputationScore> = vec![];
944 let events: Vec<EventExport> = vec![];
945 let commitments: Vec<CommitmentExport> = vec![];
946
947 let csv_scores = service.export_scores_to_csv(&scores).unwrap();
948 let csv_events = service.export_events_to_csv(&events).unwrap();
949 let csv_commitments = service.export_commitments_to_csv(&commitments).unwrap();
950
951 assert_eq!(csv_scores.lines().count(), 1);
953 assert_eq!(csv_events.lines().count(), 1);
954 assert_eq!(csv_commitments.lines().count(), 1);
955 }
956}