Skip to content

Commit b042f2f

Browse files
committed
fix! Call recount next payment on start
1 parent d8945ab commit b042f2f

6 files changed

Lines changed: 133 additions & 0 deletions

File tree

lib/application/app_dependencies.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import 'package:subctrl/application/settings/set_notifications_enabled_use_case.
2727
import 'package:subctrl/application/settings/set_theme_preference_use_case.dart';
2828
import 'package:subctrl/application/subscriptions/add_subscription_use_case.dart';
2929
import 'package:subctrl/application/subscriptions/delete_subscription_use_case.dart';
30+
import 'package:subctrl/application/subscriptions/refresh_overdue_next_payments_use_case.dart';
3031
import 'package:subctrl/application/subscriptions/update_subscription_use_case.dart';
3132
import 'package:subctrl/application/subscriptions/watch_subscriptions_use_case.dart';
3233
import 'package:subctrl/application/tags/create_tag_use_case.dart';
@@ -55,6 +56,7 @@ class AppDependencies {
5556
required this.addSubscriptionUseCase,
5657
required this.updateSubscriptionUseCase,
5758
required this.deleteSubscriptionUseCase,
59+
required this.refreshOverdueNextPaymentsUseCase,
5860
required this.watchCurrenciesUseCase,
5961
required this.getCurrenciesUseCase,
6062
required this.setCurrencyEnabledUseCase,
@@ -124,6 +126,9 @@ class AppDependencies {
124126
deleteSubscriptionUseCase: DeleteSubscriptionUseCase(
125127
subscriptionRepository,
126128
),
129+
refreshOverdueNextPaymentsUseCase: RefreshOverdueNextPaymentsUseCase(
130+
subscriptionRepository,
131+
),
127132
watchCurrenciesUseCase: WatchCurrenciesUseCase(currencyRepository),
128133
getCurrenciesUseCase: GetCurrenciesUseCase(currencyRepository),
129134
setCurrencyEnabledUseCase: SetCurrencyEnabledUseCase(currencyRepository),
@@ -197,6 +202,7 @@ class AppDependencies {
197202
final AddSubscriptionUseCase addSubscriptionUseCase;
198203
final UpdateSubscriptionUseCase updateSubscriptionUseCase;
199204
final DeleteSubscriptionUseCase deleteSubscriptionUseCase;
205+
final RefreshOverdueNextPaymentsUseCase refreshOverdueNextPaymentsUseCase;
200206

201207
final WatchCurrenciesUseCase watchCurrenciesUseCase;
202208
final GetCurrenciesUseCase getCurrenciesUseCase;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import 'package:subctrl/domain/entities/subscription.dart';
2+
import 'package:subctrl/domain/repositories/subscription_repository.dart';
3+
4+
class RefreshOverdueNextPaymentsUseCase {
5+
RefreshOverdueNextPaymentsUseCase(
6+
this._repository, {
7+
DateTime Function()? nowProvider,
8+
}) : _nowProvider = nowProvider ?? DateTime.now;
9+
10+
final SubscriptionRepository _repository;
11+
final DateTime Function() _nowProvider;
12+
13+
Future<void> call(List<Subscription> subscriptions) async {
14+
if (subscriptions.isEmpty) {
15+
return;
16+
}
17+
final now = _nowProvider();
18+
for (final subscription in subscriptions) {
19+
if (!isBeforeDay(subscription.nextPaymentDate, now)) {
20+
continue;
21+
}
22+
final nextPaymentDate = subscription.cycle.nextPaymentDate(
23+
subscription.purchaseDate,
24+
now,
25+
);
26+
if (nextPaymentDate == subscription.nextPaymentDate) {
27+
continue;
28+
}
29+
await _repository.updateSubscription(
30+
subscription.copyWith(nextPaymentDate: nextPaymentDate),
31+
);
32+
}
33+
}
34+
}

lib/presentation/screens/subscriptions_screen.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ class _SubscriptionsScreenState extends State<SubscriptionsScreen> {
6363
addSubscriptionUseCase: widget.dependencies.addSubscriptionUseCase,
6464
updateSubscriptionUseCase: widget.dependencies.updateSubscriptionUseCase,
6565
deleteSubscriptionUseCase: widget.dependencies.deleteSubscriptionUseCase,
66+
refreshOverdueNextPaymentsUseCase:
67+
widget.dependencies.refreshOverdueNextPaymentsUseCase,
6668
watchCurrenciesUseCase: widget.dependencies.watchCurrenciesUseCase,
6769
getCurrenciesUseCase: widget.dependencies.getCurrenciesUseCase,
6870
watchCurrencyRatesUseCase: widget.dependencies.watchCurrencyRatesUseCase,

lib/presentation/viewmodels/subscriptions_view_model.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import 'package:subctrl/application/notifications/get_pending_notifications_use_
1414
import 'package:subctrl/application/notifications/schedule_notifications_use_case.dart';
1515
import 'package:subctrl/application/subscriptions/add_subscription_use_case.dart';
1616
import 'package:subctrl/application/subscriptions/delete_subscription_use_case.dart';
17+
import 'package:subctrl/application/subscriptions/refresh_overdue_next_payments_use_case.dart';
1718
import 'package:subctrl/application/subscriptions/update_subscription_use_case.dart';
1819
import 'package:subctrl/application/subscriptions/watch_subscriptions_use_case.dart';
1920
import 'package:subctrl/application/tags/watch_tags_use_case.dart';
@@ -35,6 +36,7 @@ class SubscriptionsViewModel extends ChangeNotifier {
3536
required AddSubscriptionUseCase addSubscriptionUseCase,
3637
required UpdateSubscriptionUseCase updateSubscriptionUseCase,
3738
required DeleteSubscriptionUseCase deleteSubscriptionUseCase,
39+
required RefreshOverdueNextPaymentsUseCase refreshOverdueNextPaymentsUseCase,
3840
required WatchCurrenciesUseCase watchCurrenciesUseCase,
3941
required GetCurrenciesUseCase getCurrenciesUseCase,
4042
required WatchCurrencyRatesUseCase watchCurrencyRatesUseCase,
@@ -54,6 +56,7 @@ class SubscriptionsViewModel extends ChangeNotifier {
5456
_addSubscriptionUseCase = addSubscriptionUseCase,
5557
_updateSubscriptionUseCase = updateSubscriptionUseCase,
5658
_deleteSubscriptionUseCase = deleteSubscriptionUseCase,
59+
_refreshOverdueNextPaymentsUseCase = refreshOverdueNextPaymentsUseCase,
5760
_watchCurrenciesUseCase = watchCurrenciesUseCase,
5861
_getCurrenciesUseCase = getCurrenciesUseCase,
5962
_watchCurrencyRatesUseCase = watchCurrencyRatesUseCase,
@@ -79,6 +82,7 @@ class SubscriptionsViewModel extends ChangeNotifier {
7982
final AddSubscriptionUseCase _addSubscriptionUseCase;
8083
final UpdateSubscriptionUseCase _updateSubscriptionUseCase;
8184
final DeleteSubscriptionUseCase _deleteSubscriptionUseCase;
85+
final RefreshOverdueNextPaymentsUseCase _refreshOverdueNextPaymentsUseCase;
8286
final WatchCurrenciesUseCase _watchCurrenciesUseCase;
8387
final GetCurrenciesUseCase _getCurrenciesUseCase;
8488
final WatchCurrencyRatesUseCase _watchCurrencyRatesUseCase;
@@ -104,6 +108,7 @@ class SubscriptionsViewModel extends ChangeNotifier {
104108
bool _isFetchingRates = false;
105109
bool _autoDownloadEnabled = true;
106110
bool _isSyncingNotifications = false;
111+
bool _isUpdatingNextPayments = false;
107112

108113
List<Subscription> _subscriptions = const [];
109114
List<Tag> _tags = const [];
@@ -233,6 +238,7 @@ class SubscriptionsViewModel extends ChangeNotifier {
233238
_subscriptions = subscriptions;
234239
_isLoadingSubscriptions = false;
235240
notifyListeners();
241+
unawaited(_refreshOverdueNextPayments(subscriptions));
236242
if (_autoDownloadEnabled) {
237243
unawaited(_refreshCurrencyRatesForSubscriptions());
238244
}
@@ -331,6 +337,20 @@ class SubscriptionsViewModel extends ChangeNotifier {
331337
}
332338
}
333339

340+
Future<void> _refreshOverdueNextPayments(
341+
List<Subscription> subscriptions,
342+
) async {
343+
if (_isUpdatingNextPayments) {
344+
return;
345+
}
346+
_isUpdatingNextPayments = true;
347+
try {
348+
await _refreshOverdueNextPaymentsUseCase(subscriptions);
349+
} finally {
350+
_isUpdatingNextPayments = false;
351+
}
352+
}
353+
334354
Map<String, CurrencyRate> _latestRatesFrom(List<CurrencyRate> rates) {
335355
final Map<String, CurrencyRate> result = {};
336356
for (final rate in rates) {

test/application/subscriptions/subscription_use_cases_test.dart

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart';
44
import 'package:mocktail/mocktail.dart';
55
import 'package:subctrl/application/subscriptions/add_subscription_use_case.dart';
66
import 'package:subctrl/application/subscriptions/delete_subscription_use_case.dart';
7+
import 'package:subctrl/application/subscriptions/refresh_overdue_next_payments_use_case.dart';
78
import 'package:subctrl/application/subscriptions/update_subscription_use_case.dart';
89
import 'package:subctrl/application/subscriptions/watch_subscriptions_use_case.dart';
910
import 'package:subctrl/domain/entities/subscription.dart';
@@ -30,13 +31,18 @@ void main() {
3031
late AddSubscriptionUseCase addUseCase;
3132
late UpdateSubscriptionUseCase updateUseCase;
3233
late DeleteSubscriptionUseCase deleteUseCase;
34+
late RefreshOverdueNextPaymentsUseCase refreshOverdueNextPaymentsUseCase;
3335

3436
setUp(() {
3537
repository = _MockSubscriptionRepository();
3638
watchUseCase = WatchSubscriptionsUseCase(repository);
3739
addUseCase = AddSubscriptionUseCase(repository);
3840
updateUseCase = UpdateSubscriptionUseCase(repository);
3941
deleteUseCase = DeleteSubscriptionUseCase(repository);
42+
refreshOverdueNextPaymentsUseCase = RefreshOverdueNextPaymentsUseCase(
43+
repository,
44+
nowProvider: () => DateTime(2024, 2, 15),
45+
);
4046
});
4147

4248
test('watch use case delegates to repository stream', () async {
@@ -92,4 +98,35 @@ void main() {
9298
await deleteUseCase(42);
9399
verify(() => repository.deleteSubscription(42)).called(1);
94100
});
101+
102+
test('refresh overdue next payments updates stored subscriptions', () async {
103+
when(() => repository.updateSubscription(any())).thenAnswer((_) async {});
104+
final stale = Subscription(
105+
id: 1,
106+
name: 'Stale',
107+
amount: 5,
108+
currency: 'USD',
109+
cycle: BillingCycle.monthly,
110+
purchaseDate: DateTime(2024, 1, 1),
111+
nextPaymentDate: DateTime(2024, 2, 1),
112+
);
113+
final fresh = Subscription(
114+
id: 2,
115+
name: 'Fresh',
116+
amount: 5,
117+
currency: 'USD',
118+
cycle: BillingCycle.monthly,
119+
purchaseDate: DateTime(2024, 2, 1),
120+
nextPaymentDate: DateTime(2024, 3, 1),
121+
);
122+
123+
await refreshOverdueNextPaymentsUseCase([stale, fresh]);
124+
125+
verify(
126+
() => repository.updateSubscription(
127+
stale.copyWith(nextPaymentDate: DateTime(2024, 3, 1)),
128+
),
129+
).called(1);
130+
verifyNever(() => repository.updateSubscription(fresh));
131+
});
95132
}

test/presentation/viewmodels/subscriptions_view_model_test.dart

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import 'package:subctrl/application/notifications/get_pending_notifications_use_
1414
import 'package:subctrl/application/notifications/schedule_notifications_use_case.dart';
1515
import 'package:subctrl/application/subscriptions/add_subscription_use_case.dart';
1616
import 'package:subctrl/application/subscriptions/delete_subscription_use_case.dart';
17+
import 'package:subctrl/application/subscriptions/refresh_overdue_next_payments_use_case.dart';
1718
import 'package:subctrl/application/subscriptions/update_subscription_use_case.dart';
1819
import 'package:subctrl/application/subscriptions/watch_subscriptions_use_case.dart';
1920
import 'package:subctrl/application/tags/watch_tags_use_case.dart';
@@ -38,6 +39,9 @@ class _MockUpdateSubscriptionUseCase extends Mock
3839
class _MockDeleteSubscriptionUseCase extends Mock
3940
implements DeleteSubscriptionUseCase {}
4041

42+
class _MockRefreshOverdueNextPaymentsUseCase extends Mock
43+
implements RefreshOverdueNextPaymentsUseCase {}
44+
4145
class _MockWatchCurrenciesUseCase extends Mock
4246
implements WatchCurrenciesUseCase {}
4347

@@ -85,6 +89,7 @@ void main() {
8589
late _MockAddSubscriptionUseCase addSubscriptionUseCase;
8690
late _MockUpdateSubscriptionUseCase updateSubscriptionUseCase;
8791
late _MockDeleteSubscriptionUseCase deleteSubscriptionUseCase;
92+
late _MockRefreshOverdueNextPaymentsUseCase refreshOverdueNextPaymentsUseCase;
8893
late _MockWatchCurrenciesUseCase watchCurrenciesUseCase;
8994
late _MockGetCurrenciesUseCase getCurrenciesUseCase;
9095
late _MockWatchCurrencyRatesUseCase watchCurrencyRatesUseCase;
@@ -107,6 +112,8 @@ void main() {
107112
addSubscriptionUseCase = _MockAddSubscriptionUseCase();
108113
updateSubscriptionUseCase = _MockUpdateSubscriptionUseCase();
109114
deleteSubscriptionUseCase = _MockDeleteSubscriptionUseCase();
115+
refreshOverdueNextPaymentsUseCase =
116+
_MockRefreshOverdueNextPaymentsUseCase();
110117
watchCurrenciesUseCase = _MockWatchCurrenciesUseCase();
111118
getCurrenciesUseCase = _MockGetCurrenciesUseCase();
112119
watchCurrencyRatesUseCase = _MockWatchCurrencyRatesUseCase();
@@ -161,12 +168,16 @@ void main() {
161168
when(() => addSubscriptionUseCase(any())).thenAnswer((_) async {});
162169
when(() => updateSubscriptionUseCase(any())).thenAnswer((_) async {});
163170
when(() => deleteSubscriptionUseCase(any())).thenAnswer((_) async {});
171+
when(
172+
() => refreshOverdueNextPaymentsUseCase(any()),
173+
).thenAnswer((_) async {});
164174

165175
viewModel = SubscriptionsViewModel(
166176
watchSubscriptionsUseCase: watchSubscriptionsUseCase,
167177
addSubscriptionUseCase: addSubscriptionUseCase,
168178
updateSubscriptionUseCase: updateSubscriptionUseCase,
169179
deleteSubscriptionUseCase: deleteSubscriptionUseCase,
180+
refreshOverdueNextPaymentsUseCase: refreshOverdueNextPaymentsUseCase,
170181
watchCurrenciesUseCase: watchCurrenciesUseCase,
171182
getCurrenciesUseCase: getCurrenciesUseCase,
172183
watchCurrencyRatesUseCase: watchCurrencyRatesUseCase,
@@ -247,6 +258,28 @@ void main() {
247258
expect(viewModel.isLoadingCurrencies, isFalse);
248259
});
249260

261+
test('triggers overdue next payment refresh on updates', () async {
262+
final subscription = Subscription(
263+
id: 1,
264+
name: 'Overdue',
265+
amount: 5,
266+
currency: 'usd',
267+
cycle: BillingCycle.monthly,
268+
purchaseDate: DateTime(2024, 1, 1),
269+
);
270+
subscriptionsController.add([subscription]);
271+
tagsController.add(const []);
272+
currenciesController.add(const []);
273+
ratesController.add(const []);
274+
await Future<void>.delayed(Duration.zero);
275+
276+
verify(
277+
() => refreshOverdueNextPaymentsUseCase(
278+
any(that: predicate<List<Subscription>>((subs) => subs.length == 1)),
279+
),
280+
).called(1);
281+
});
282+
250283
test('updateBaseCurrencyCode re-listens to currency rates stream', () async {
251284
viewModel.updateBaseCurrencyCode('eur');
252285
await Future<void>.delayed(Duration.zero);
@@ -322,6 +355,7 @@ void main() {
322355
addSubscriptionUseCase: addSubscriptionUseCase,
323356
updateSubscriptionUseCase: updateSubscriptionUseCase,
324357
deleteSubscriptionUseCase: deleteSubscriptionUseCase,
358+
refreshOverdueNextPaymentsUseCase: refreshOverdueNextPaymentsUseCase,
325359
watchCurrenciesUseCase: watchCurrenciesUseCase,
326360
getCurrenciesUseCase: getCurrenciesUseCase,
327361
watchCurrencyRatesUseCase: watchCurrencyRatesUseCase,

0 commit comments

Comments
 (0)