import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:travel_mate/models/trip_invitation.dart'; import 'package:travel_mate/services/error_service.dart'; /// Contrat de données pour la gestion des invitations de voyage. /// /// Cette interface permet d'injecter une implémentation réelle ou fake /// (tests) dans les écrans qui consomment les invitations. abstract class TripInvitationDataSource { /// Crée une invitation de voyage. Future createInvitation({ required String tripId, required String tripTitle, required String inviterId, required String inviterName, required String inviteeId, required String inviteeEmail, }); /// Retourne une invitation en attente pour le couple voyage/invité. Future getPendingInvitation({ required String tripId, required String inviteeId, }); /// Retourne une invitation par son identifiant. Future getInvitationById(String invitationId); /// Met à jour une invitation selon la réponse utilisateur. Future respondToInvitation({ required String invitationId, required bool isAccepted, }); /// Retourne les invitations en attente d'un utilisateur. Stream> watchPendingInvitationsForUser(String userId); /// Retourne toutes les invitations d'un utilisateur. Stream> watchInvitationsForUser(String userId); } /// Repository dédié à la gestion des invitations de voyage. /// /// Ce repository centralise la création d'invitation, la lecture des invitations /// en attente et la réponse (acceptation/refus) depuis l'application. class TripInvitationRepository implements TripInvitationDataSource { /// Instance Firestore injectée ou par défaut. final FirebaseFirestore _firestore; final ErrorService _errorService = ErrorService(); /// Crée une instance du repository. TripInvitationRepository({FirebaseFirestore? firestore}) : _firestore = firestore ?? FirebaseFirestore.instance; CollectionReference> get _collection => _firestore.collection('tripInvitations'); /// Cherche une invitation en attente pour un voyage et un utilisateur donné. /// /// Retourne `null` si aucune invitation `pending` n'existe. @override Future getPendingInvitation({ required String tripId, required String inviteeId, }) async { try { final query = await _collection .where('tripId', isEqualTo: tripId) .where('inviteeId', isEqualTo: inviteeId) .where('status', isEqualTo: 'pending') .limit(1) .get(); if (query.docs.isEmpty) { return null; } return TripInvitation.fromFirestore(query.docs.first); } catch (e, stackTrace) { _errorService.logError( 'TripInvitationRepository', 'Erreur getPendingInvitation: $e', stackTrace, ); throw Exception('Impossible de vérifier les invitations en attente.'); } } /// Crée une invitation de voyage en statut `pending`. /// /// La méthode bloque les doublons d'invitation active pour éviter les spams. /// Lance une exception si une invitation en attente existe déjà. @override Future createInvitation({ required String tripId, required String tripTitle, required String inviterId, required String inviterName, required String inviteeId, required String inviteeEmail, }) async { try { final pending = await getPendingInvitation( tripId: tripId, inviteeId: inviteeId, ); if (pending != null) { throw Exception( 'Une invitation est déjà en attente pour cet utilisateur.', ); } await _collection.add({ 'tripId': tripId, 'tripTitle': tripTitle, 'inviterId': inviterId, 'inviterName': inviterName, 'inviteeId': inviteeId, 'inviteeEmail': inviteeEmail, 'status': 'pending', 'createdAt': FieldValue.serverTimestamp(), }); } catch (e, stackTrace) { _errorService.logError( 'TripInvitationRepository', 'Erreur createInvitation: $e', stackTrace, ); rethrow; } } /// Récupère une invitation précise par son identifiant. /// /// Retourne `null` si le document n'existe pas. @override Future getInvitationById(String invitationId) async { try { final doc = await _collection.doc(invitationId).get(); if (!doc.exists) { return null; } return TripInvitation.fromFirestore(doc); } catch (e, stackTrace) { _errorService.logError( 'TripInvitationRepository', 'Erreur getInvitationById: $e', stackTrace, ); throw Exception('Impossible de charger l\'invitation.'); } } /// Accepte ou refuse une invitation en attente. /// /// [isAccepted] à `true` passe le statut à `accepted`, sinon `rejected`. /// Si l'invitation n'est plus en attente, aucune modification n'est appliquée. @override Future respondToInvitation({ required String invitationId, required bool isAccepted, }) async { try { await _firestore.runTransaction((transaction) async { final docRef = _collection.doc(invitationId); final snapshot = await transaction.get(docRef); if (!snapshot.exists) { throw Exception('Invitation introuvable.'); } final data = snapshot.data() ?? {}; if ((data['status'] as String? ?? 'pending') != 'pending') { return; } transaction.update(docRef, { 'status': isAccepted ? 'accepted' : 'rejected', 'respondedAt': FieldValue.serverTimestamp(), }); }); } catch (e, stackTrace) { _errorService.logError( 'TripInvitationRepository', 'Erreur respondToInvitation: $e', stackTrace, ); rethrow; } } /// Écoute les invitations en attente d'un utilisateur. /// /// Le flux est trié de la plus récente à la plus ancienne. @override Stream> watchPendingInvitationsForUser(String userId) { return _collection .where('inviteeId', isEqualTo: userId) .where('status', isEqualTo: 'pending') .orderBy('createdAt', descending: true) .snapshots() .map((snapshot) { return snapshot.docs .map(TripInvitation.fromFirestore) .toList(growable: false); }); } /// Écoute toutes les invitations d'un utilisateur (tous statuts confondus). /// /// Le flux est trié de la plus récente à la plus ancienne. @override Stream> watchInvitationsForUser(String userId) { return _collection .where('inviteeId', isEqualTo: userId) .orderBy('createdAt', descending: true) .snapshots() .map((snapshot) { return snapshot.docs .map(TripInvitation.fromFirestore) .toList(growable: false); }); } }