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.
This commit is contained in:
225
lib/repositories/trip_invitation_repository.dart
Normal file
225
lib/repositories/trip_invitation_repository.dart
Normal file
@@ -0,0 +1,225 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user