Skip to content

Commit 39c471a

Browse files
committed
refactor(api): unify user preference limit checking
This refactoring consolidates the UserPreferenceLimitService to use a single, holistic method (checkUserContentPreferencesLimits) for all UserContentPreferences validations. The service interface and implementation have been updated to use a single method that compares the updated preferences with the current state to determine what has changed and apply the correct limits for interests, followed items, and saved headlines. The custom updater for user_content_preferences in DataOperationRegistry has been significantly simplified. It no longer contains complex logic to detect changes and now makes a single call to the unified service method. This change improves architectural purity by centralizing all limit-checking logic within the service, making the system more robust and easier to maintain.
1 parent 14b8c5e commit 39c471a

File tree

3 files changed

+113
-192
lines changed

3 files changed

+113
-192
lines changed

lib/src/registry/data_operation_registry.dart

Lines changed: 6 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -398,89 +398,17 @@ class DataOperationRegistry {
398398
id: id,
399399
);
400400

401-
// 2. Detect changes in the interests list.
402-
final currentIds = currentPreferences.interests
403-
.map((i) => i.id)
404-
.toSet();
405-
final updatedIds = preferencesToUpdate.interests
406-
.map((i) => i.id)
407-
.toSet();
408-
409-
final addedIds = updatedIds.difference(currentIds);
410-
final removedIds = currentIds.difference(updatedIds);
411-
412-
// For simplicity and clear validation, enforce one change at a time.
413-
if (addedIds.length + removedIds.length > 1) {
414-
throw const BadRequestException(
415-
'Only one interest can be added or removed per request.',
416-
);
417-
}
418-
419-
// 3. Perform permission and limit checks based on the detected action.
420-
if (addedIds.isNotEmpty) {
421-
// --- Interest Added ---
422-
final addedInterestId = addedIds.first;
423-
_log.info(
424-
'Detected interest addition for user ${authenticatedUser.id}.',
425-
);
426-
427-
final addedInterest = preferencesToUpdate.interests.firstWhere(
428-
(i) => i.id == addedInterestId,
429-
);
430-
431-
// Check business logic limits.
432-
await userPreferenceLimitService.checkInterestLimits(
433-
user: authenticatedUser,
434-
interest: addedInterest,
435-
existingInterests: currentPreferences.interests,
436-
);
437-
} else if (removedIds.isNotEmpty) {
438-
// --- Interest Removed ---
439-
_log.info(
440-
'Detected interest removal for user ${authenticatedUser.id}.',
441-
);
442-
} else {
443-
// --- Interest Potentially Updated ---
444-
// Check if any existing interest was modified.
445-
Interest? updatedInterest;
446-
for (final newInterest in preferencesToUpdate.interests) {
447-
// Find the corresponding interest in the old list.
448-
final oldInterest = currentPreferences.interests.firstWhere(
449-
(i) => i.id == newInterest.id,
450-
// This should not be hit if add/remove is handled, but as a
451-
// safeguard, we use the newInterest to avoid null issues.
452-
orElse: () => newInterest,
453-
);
454-
if (newInterest != oldInterest) {
455-
updatedInterest = newInterest;
456-
break; // Found the updated one, no need to continue loop.
457-
}
458-
}
459-
460-
if (updatedInterest != null) {
461-
_log.info(
462-
'Detected interest update for user ${authenticatedUser.id}.',
463-
);
464-
465-
// Check business logic limits.
466-
final otherInterests = currentPreferences.interests
467-
.where((i) => i.id != updatedInterest!.id)
468-
.toList();
469-
await userPreferenceLimitService.checkInterestLimits(
470-
user: authenticatedUser,
471-
interest: updatedInterest,
472-
existingInterests: otherInterests,
473-
);
474-
}
475-
}
476-
477-
// 4. Always validate general preference limits (followed items, etc.).
401+
// 2. Validate all limits using the consolidated service method.
402+
// The service now contains all logic to compare the updated and
403+
// current preferences and check all relevant limits (interests,
404+
// followed items, etc.) in one go.
478405
await userPreferenceLimitService.checkUserContentPreferencesLimits(
479406
user: authenticatedUser,
480407
updatedPreferences: preferencesToUpdate,
408+
currentPreferences: currentPreferences,
481409
);
482410

483-
// 5. If all checks pass, proceed with the update.
411+
// 3. If all checks pass, proceed with the update.
484412
_log.info(
485413
'All preference validations passed for user ${authenticatedUser.id}. '
486414
'Proceeding with update.',

lib/src/services/default_user_preference_limit_service.dart

Lines changed: 103 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:core/core.dart';
22
import 'package:data_repository/data_repository.dart';
3+
import 'package:flutter_news_app_api_server_full_source_code/src/helpers/set_equality_helper.dart';
34
import 'package:flutter_news_app_api_server_full_source_code/src/services/user_preference_limit_service.dart';
45
import 'package:logging/logging.dart';
56

@@ -22,95 +23,15 @@ class DefaultUserPreferenceLimitService implements UserPreferenceLimitService {
2223
// Assuming a fixed ID for the RemoteConfig document
2324
static const String _remoteConfigId = kRemoteConfigId;
2425

25-
@override
26-
Future<void> checkInterestLimits({
27-
required User user,
28-
required Interest interest,
29-
required List<Interest> existingInterests,
30-
}) async {
31-
_log.info('Checking interest limits for user ${user.id}.');
32-
final remoteConfig = await _remoteConfigRepository.read(
33-
id: _remoteConfigId,
34-
);
35-
final limits = remoteConfig.interestConfig.limits[user.appRole];
36-
37-
if (limits == null) {
38-
_log.severe(
39-
'Interest limits not found for role ${user.appRole}. '
40-
'Denying request by default.',
41-
);
42-
throw const ForbiddenException('Interest limits are not configured.');
43-
}
44-
45-
// 1. Check total number of interests.
46-
final newTotal = existingInterests.length + 1;
47-
if (newTotal > limits.total) {
48-
_log.warning(
49-
'User ${user.id} exceeded total interest limit: '
50-
'${limits.total} (attempted $newTotal).',
51-
);
52-
throw ForbiddenException(
53-
'You have reached your limit of ${limits.total} saved interests.',
54-
);
55-
}
56-
57-
// 2. Check total number of pinned feed filters.
58-
if (interest.isPinnedFeedFilter) {
59-
final pinnedCount =
60-
existingInterests.where((i) => i.isPinnedFeedFilter).length + 1;
61-
if (pinnedCount > limits.pinnedFeedFilters) {
62-
_log.warning(
63-
'User ${user.id} exceeded pinned feed filter limit: '
64-
'${limits.pinnedFeedFilters} (attempted $pinnedCount).',
65-
);
66-
throw ForbiddenException(
67-
'You have reached your limit of ${limits.pinnedFeedFilters} '
68-
'pinned feed filters.',
69-
);
70-
}
71-
}
72-
73-
// 3. Check notification subscription limits for each type.
74-
for (final deliveryType in interest.deliveryTypes) {
75-
final notificationLimit = limits.notifications[deliveryType];
76-
if (notificationLimit == null) {
77-
_log.severe(
78-
'Notification limit for type ${deliveryType.name} not found for '
79-
'role ${user.appRole}. Denying request by default.',
80-
);
81-
throw ForbiddenException(
82-
'Notification limits for ${deliveryType.name} are not configured.',
83-
);
84-
}
85-
86-
final subscriptionCount =
87-
existingInterests
88-
.where((i) => i.deliveryTypes.contains(deliveryType))
89-
.length +
90-
1;
91-
92-
if (subscriptionCount > notificationLimit) {
93-
_log.warning(
94-
'User ${user.id} exceeded notification limit for '
95-
'${deliveryType.name}: $notificationLimit '
96-
'(attempted $subscriptionCount).',
97-
);
98-
throw ForbiddenException(
99-
'You have reached your limit of $notificationLimit '
100-
'${deliveryType.name} notification subscriptions.',
101-
);
102-
}
103-
}
104-
105-
_log.info('Interest limits check passed for user ${user.id}.');
106-
}
107-
10826
@override
10927
Future<void> checkUserContentPreferencesLimits({
11028
required User user,
11129
required UserContentPreferences updatedPreferences,
30+
required UserContentPreferences currentPreferences,
11231
}) async {
113-
_log.info('Checking user content preferences limits for user ${user.id}.');
32+
_log.info(
33+
'Checking all user content preferences limits for user ${user.id}.',
34+
);
11435
final remoteConfig = await _remoteConfigRepository.read(
11536
id: _remoteConfigId,
11637
);
@@ -121,7 +42,7 @@ class DefaultUserPreferenceLimitService implements UserPreferenceLimitService {
12142
limits,
12243
);
12344

124-
// Check followed countries
45+
// --- 1. Check general preference limits ---
12546
if (updatedPreferences.followedCountries.length > followedItemsLimit) {
12647
_log.warning(
12748
'User ${user.id} exceeded followed countries limit: '
@@ -133,7 +54,6 @@ class DefaultUserPreferenceLimitService implements UserPreferenceLimitService {
13354
);
13455
}
13556

136-
// Check followed sources
13757
if (updatedPreferences.followedSources.length > followedItemsLimit) {
13858
_log.warning(
13959
'User ${user.id} exceeded followed sources limit: '
@@ -145,7 +65,6 @@ class DefaultUserPreferenceLimitService implements UserPreferenceLimitService {
14565
);
14666
}
14767

148-
// Check followed topics
14968
if (updatedPreferences.followedTopics.length > followedItemsLimit) {
15069
_log.warning(
15170
'User ${user.id} exceeded followed topics limit: '
@@ -157,7 +76,6 @@ class DefaultUserPreferenceLimitService implements UserPreferenceLimitService {
15776
);
15877
}
15978

160-
// Check saved headlines
16179
if (updatedPreferences.savedHeadlines.length > savedHeadlinesLimit) {
16280
_log.warning(
16381
'User ${user.id} exceeded saved headlines limit: '
@@ -169,8 +87,104 @@ class DefaultUserPreferenceLimitService implements UserPreferenceLimitService {
16987
);
17088
}
17189

90+
// --- 2. Check interest-specific limits ---
91+
final interestLimits = remoteConfig.interestConfig.limits[user.appRole];
92+
if (interestLimits == null) {
93+
_log.severe(
94+
'Interest limits not found for role ${user.appRole}. '
95+
'Denying request by default.',
96+
);
97+
throw const ForbiddenException('Interest limits are not configured.');
98+
}
99+
100+
// Check total number of interests.
101+
if (updatedPreferences.interests.length > interestLimits.total) {
102+
_log.warning(
103+
'User ${user.id} exceeded total interest limit: '
104+
'${interestLimits.total} (attempted '
105+
'${updatedPreferences.interests.length}).',
106+
);
107+
throw ForbiddenException(
108+
'You have reached your limit of ${interestLimits.total} saved interests.',
109+
);
110+
}
111+
112+
// Find the interest that was added or updated to check its specific limits.
113+
// This logic assumes only one interest is added or updated per request.
114+
final currentInterestIds = currentPreferences.interests
115+
.map((i) => i.id)
116+
.toSet();
117+
Interest? changedInterest;
118+
119+
for (final updatedInterest in updatedPreferences.interests) {
120+
if (!currentInterestIds.contains(updatedInterest.id)) {
121+
// This is a newly added interest.
122+
changedInterest = updatedInterest;
123+
break;
124+
} else {
125+
// This is a potentially updated interest. Find the original.
126+
final originalInterest = currentPreferences.interests.firstWhere(
127+
(i) => i.id == updatedInterest.id,
128+
);
129+
if (updatedInterest != originalInterest) {
130+
changedInterest = updatedInterest;
131+
break;
132+
}
133+
}
134+
}
135+
136+
// If an interest was added or updated, check its specific limits.
137+
if (changedInterest != null) {
138+
_log.info('Checking limits for changed interest: ${changedInterest.id}');
139+
140+
// Check total number of pinned feed filters.
141+
final pinnedCount = updatedPreferences.interests
142+
.where((i) => i.isPinnedFeedFilter)
143+
.length;
144+
if (pinnedCount > interestLimits.pinnedFeedFilters) {
145+
_log.warning(
146+
'User ${user.id} exceeded pinned feed filter limit: '
147+
'${interestLimits.pinnedFeedFilters} (attempted $pinnedCount).',
148+
);
149+
throw ForbiddenException(
150+
'You have reached your limit of ${interestLimits.pinnedFeedFilters} '
151+
'pinned feed filters.',
152+
);
153+
}
154+
155+
// Check notification subscription limits for each type.
156+
for (final deliveryType in changedInterest.deliveryTypes) {
157+
final notificationLimit = interestLimits.notifications[deliveryType];
158+
if (notificationLimit == null) {
159+
_log.severe(
160+
'Notification limit for type ${deliveryType.name} not found for '
161+
'role ${user.appRole}. Denying request by default.',
162+
);
163+
throw ForbiddenException(
164+
'Notification limits for ${deliveryType.name} are not configured.',
165+
);
166+
}
167+
168+
final subscriptionCount = updatedPreferences.interests
169+
.where((i) => i.deliveryTypes.contains(deliveryType))
170+
.length;
171+
172+
if (subscriptionCount > notificationLimit) {
173+
_log.warning(
174+
'User ${user.id} exceeded notification limit for '
175+
'${deliveryType.name}: $notificationLimit '
176+
'(attempted $subscriptionCount).',
177+
);
178+
throw ForbiddenException(
179+
'You have reached your limit of $notificationLimit '
180+
'${deliveryType.name} notification subscriptions.',
181+
);
182+
}
183+
}
184+
}
185+
172186
_log.info(
173-
'User content preferences limits check passed for user ${user.id}.',
187+
'All user content preferences limits check passed for user ${user.id}.',
174188
);
175189
}
176190

lib/src/services/user_preference_limit_service.dart

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,14 @@ abstract class UserPreferenceLimitService {
1111
/// {@macro user_preference_limit_service}
1212
const UserPreferenceLimitService();
1313

14-
/// Validates a new or updated [Interest] against the user's role-based
15-
/// limits defined in `InterestConfig`.
14+
/// Validates an updated [UserContentPreferences] object against all limits
15+
/// defined in `RemoteConfig`, including interests, followed items, and
16+
/// saved headlines.
1617
///
17-
/// This method checks multiple limits:
18-
/// - The total number of interests.
19-
/// - The number of interests marked as pinned feed filters.
20-
/// - The number of subscriptions for each notification delivery type across
21-
/// all of the user's interests.
22-
///
23-
/// - [user]: The authenticated user.
24-
/// - [interest]: The `Interest` object being created or updated.
25-
/// - [existingInterests]: A list of the user's other existing interests,
26-
/// used to calculate total counts.
27-
///
28-
/// Throws a [ForbiddenException] if any limit is exceeded.
29-
Future<void> checkInterestLimits({
30-
required User user,
31-
required Interest interest,
32-
required List<Interest> existingInterests,
33-
});
34-
35-
/// Validates an updated [UserContentPreferences] object against the limits
36-
/// defined in `UserPreferenceConfig`.
37-
///
38-
/// This method checks the total counts for followed items (countries,
39-
/// sources, topics) and saved headlines.
4018
/// Throws a [ForbiddenException] if any limit is exceeded.
4119
Future<void> checkUserContentPreferencesLimits({
4220
required User user,
4321
required UserContentPreferences updatedPreferences,
22+
required UserContentPreferences currentPreferences,
4423
});
4524
}

0 commit comments

Comments
 (0)