Files
TravelMate/lib/repositories/trip_invitation_repository.dart
Van Leemput Dayron 3215a990d1 feat: Implement trip invitation functionality and notification handling
- Added TripInvitationRepository for managing trip invitations.
- Created TripInvitation model with serialization methods.
- Implemented notification payload parser for handling FCM notifications.
- Enhanced NotificationService to manage trip invitations and related actions.
- Updated UserRepository to include user search functionality.
- Modified AuthRepository to store multiple FCM tokens.
- Added tests for trip invitation logic and notification payload parsing.
- Updated pubspec.yaml and pubspec.lock for dependency management.
2026-03-13 13:54:47 +01:00

226 lines
7.0 KiB
Dart

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<void> 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<TripInvitation?> getPendingInvitation({
required String tripId,
required String inviteeId,
});
/// Retourne une invitation par son identifiant.
Future<TripInvitation?> getInvitationById(String invitationId);
/// Met à jour une invitation selon la réponse utilisateur.
Future<void> respondToInvitation({
required String invitationId,
required bool isAccepted,
});
/// Retourne les invitations en attente d'un utilisateur.
Stream<List<TripInvitation>> watchPendingInvitationsForUser(String userId);
/// Retourne toutes les invitations d'un utilisateur.
Stream<List<TripInvitation>> 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<Map<String, dynamic>> 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<TripInvitation?> 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<void> 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<TripInvitation?> 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<void> 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() ?? <String, dynamic>{};
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<List<TripInvitation>> 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<List<TripInvitation>> 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);
});
}
}