Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/core/constants/app_constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ class AppConstants {
static const int maxDescriptionLength = 500;
static const int maxImageSizeMB = 5;

// Raphcon Expiry
static const int raphconExpiryDays = 365; // Raphcons expire after 1 year

// UI
static const double defaultPadding = 16.0;
static const double smallPadding = 8.0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:injectable/injectable.dart';

import '../../../../core/errors/exceptions.dart';
import '../../../../core/enums/raphcon_type.dart';
import '../../../../core/constants/app_constants.dart';
import '../models/raphcon_model.dart';

abstract class RaphconsRemoteDataSource {
Expand All @@ -13,6 +14,7 @@ abstract class RaphconsRemoteDataSource {
Future<void> addRaphcon(
String userId, String createdBy, String? comment, RaphconType type);
Future<void> deleteRaphcon(String raphconId);
Future<int> expireOldRaphcons();

// Stream-based methods for real-time updates
Stream<List<RaphconModel>> getUserRaphconsStream(String userId);
Expand Down Expand Up @@ -199,4 +201,55 @@ class RaphconsRemoteDataSourceImpl implements RaphconsRemoteDataSource {
'Failed to stream all raphcons: ${error.toString()}');
});
}

@override
Future<int> expireOldRaphcons() async {
try {
// Calculate date one year ago from now
final expiryDate = DateTime.now().subtract(
Duration(days: AppConstants.raphconExpiryDays),
);

// Query for active raphcons older than one year
final querySnapshot = await firestore
.collection('raphcons')
.where('isActive', isEqualTo: true)
.where('createdAt', isLessThan: expiryDate)
.get();

if (querySnapshot.docs.isEmpty) {
return 0;
}

// Use batch to update all expired raphcons and user counts
final batch = firestore.batch();
final userRaphconCounts = <String, int>{};

// Mark raphcons as inactive and count per user
for (final doc in querySnapshot.docs) {
batch.update(doc.reference, {'isActive': false});

final data = doc.data();
final userId = data['userId'];

// Validate userId exists and is a string before updating counts
if (userId != null && userId is String) {
userRaphconCounts[userId] = (userRaphconCounts[userId] ?? 0) + 1;
}
}

// Update user raphcon counts
for (final entry in userRaphconCounts.entries) {
final userRef = firestore.collection('users').doc(entry.key);
batch.update(userRef, {
'raphconCount': FieldValue.increment(-entry.value),
});
}

await batch.commit();
return querySnapshot.docs.length;
} catch (e) {
throw ServerException('Failed to expire old raphcons: ${e.toString()}');
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,22 @@ class RaphconsRepositoryImpl implements RaphconsRepository {
}
}

@override
Future<Either<Failure, int>> expireOldRaphcons() async {
if (await networkInfo.isConnected) {
try {
final expiredCount = await remoteDataSource.expireOldRaphcons();
return Right(expiredCount);
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} catch (e) {
return Left(ServerFailure(e.toString()));
}
} else {
return const Left(NetworkFailure());
}
}

@override
Stream<Either<Failure, List<RaphconEntity>>> getUserRaphconsStream(
String userId) async* {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ abstract class RaphconsRepository {
Future<Either<Failure, List<RaphconEntity>>> getAllRaphcons();
Future<Either<Failure, void>> addRaphcon(AddRaphconParams params);
Future<Either<Failure, void>> deleteRaphcon(String raphconId);
Future<Either<Failure, int>> expireOldRaphcons();

// Stream-based methods for real-time updates
Stream<Either<Failure, List<RaphconEntity>>> getUserRaphconsStream(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';

import '../../../../core/errors/failures.dart';
import '../repositories/raphcons_repository.dart';

@injectable
class ExpireOldRaphcons {
final RaphconsRepository repository;

ExpireOldRaphcons(this.repository);

Future<Either<Failure, int>> call() async {
return await repository.expireOldRaphcons();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ class DeleteRaphconEvent extends RaphconEvent {
List<Object> get props => [raphconId];
}

class ExpireOldRaphconsEvent extends RaphconEvent {}

// Stream Events
class StartUserRaphconsStreamEvent extends RaphconEvent {
final String userId;
Expand Down Expand Up @@ -160,6 +162,15 @@ class RaphconError extends RaphconState {
List<Object> get props => [message];
}

class RaphconsExpired extends RaphconState {
final int expiredCount;

RaphconsExpired(this.expiredCount);

@override
List<Object> get props => [expiredCount];
}

// Stream States
class RaphconsStreamLoaded extends RaphconState {
final List<RaphconEntity> raphcons;
Expand Down Expand Up @@ -188,6 +199,7 @@ class RaphconBloc extends Bloc<RaphconEvent, RaphconState> {
final DeleteRaphcon _deleteRaphcon;
final GetUserRaphconsStream _getUserRaphconsStream;
final GetUserRaphconsByTypeStream _getUserRaphconsByTypeStream;
final RaphconsRepository _repository;

StreamSubscription? _raphconsStreamSubscription;

Expand All @@ -197,12 +209,14 @@ class RaphconBloc extends Bloc<RaphconEvent, RaphconState> {
this._getUserRaphconsByType,
this._deleteRaphcon,
this._getUserRaphconsStream,
this._getUserRaphconsByTypeStream)
this._getUserRaphconsByTypeStream,
this._repository)
: super(RaphconInitial()) {
on<AddRaphconEvent>(_onAddRaphcon);
on<LoadUserRaphconStatisticsEvent>(_onLoadUserRaphconStatistics);
on<LoadUserRaphconsByTypeEvent>(_onLoadUserRaphconsByType);
on<DeleteRaphconEvent>(_onDeleteRaphcon);
on<ExpireOldRaphconsEvent>(_onExpireOldRaphcons);
on<StartUserRaphconsStreamEvent>(_onStartUserRaphconsStream);
on<StartUserRaphconsByTypeStreamEvent>(_onStartUserRaphconsByTypeStream);
on<StopRaphconsStreamEvent>(_onStopRaphconsStream);
Expand Down Expand Up @@ -275,6 +289,26 @@ class RaphconBloc extends Bloc<RaphconEvent, RaphconState> {
);
}

Future<void> _onExpireOldRaphcons(
ExpireOldRaphconsEvent event,
Emitter<RaphconState> emit,
) async {
// Don't emit loading state - this runs silently in the background
final result = await _repository.expireOldRaphcons();
result.fold(
(failure) {
// Silent fail - don't emit error state
// This operation should not interrupt user experience
},
(expiredCount) {
if (expiredCount > 0) {
emit(RaphconsExpired(expiredCount));
}
// If no raphcons expired, don't emit any state
},
);
}

Future<void> _onStartUserRaphconsStream(
StartUserRaphconsStreamEvent event,
Emitter<RaphconState> emit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ class _PublicUserListPageState extends State<PublicUserListPage> {
displayName: currentUser.displayName ?? displayName,
));
}

// Admin-only: Check and expire old raphcons (older than 1 year)
if (mounted) {
context.read<RaphconBloc>().add(ExpireOldRaphconsEvent());
}
} else {
// For other users, just check admin status
context.read<AdminBloc>().add(CheckAdminStatusEvent(currentUser.uid));
Expand Down
1 change: 1 addition & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ class AngryRaphiApp extends StatelessWidget {
deleteRaphcon,
getUserRaphconsStream,
getUserRaphconsByTypeStream,
raphconRepository,
);
},
),
Expand Down
28 changes: 28 additions & 0 deletions lib/services/raphcon_expiry_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:injectable/injectable.dart';

import '../features/raphcon_management/domain/repositories/raphcons_repository.dart';

@injectable
class RaphconExpiryService {
final RaphconsRepository raphconRepository;

RaphconExpiryService({required this.raphconRepository});

/// Check and expire Raphcons older than one year
/// Returns the number of expired Raphcons
Future<int> checkAndExpireOldRaphcons() async {
try {
final result = await raphconRepository.expireOldRaphcons();
return result.fold(
(failure) {
// Log error but don't throw - silent fail
return 0;
},
(count) => count,
);
} catch (e) {
// Silent fail - don't interrupt user experience
return 0;
}
}
}
21 changes: 21 additions & 0 deletions test/raphcon_expiry_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:angry_raphi/core/constants/app_constants.dart';

void main() {
group('Raphcon Expiry Constants', () {
test('Raphcon expiry is set to 365 days (1 year)', () {
expect(AppConstants.raphconExpiryDays, 365);
});

test('Expiry calculation produces correct date', () {
final now = DateTime(2024, 12, 12);
final expiryDate = now.subtract(
Duration(days: AppConstants.raphconExpiryDays),
);

expect(expiryDate.year, 2023);
expect(expiryDate.month, 12);
expect(expiryDate.day, 12);
});
});
}