Skip to content

Commit 77b9048

Browse files
authored
New rates client (#16)
1 parent 2101d3d commit 77b9048

14 files changed

Lines changed: 361 additions & 444 deletions

AGENTS.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ with the architecture below.
2020

2121
- `lib/main.dart` bootstraps the app and tabs.
2222
- `lib/application/app_dependencies.dart` wires repositories, use cases, and
23-
external clients; dispose `YahooFinanceCurrencyClient` when done.
23+
external clients; dispose `ProxyCurrencyRatesClient` when done.
2424

2525
## Storage and services
2626

2727
- Drift database in `lib/infrastructure/persistence/database.dart`, stored as
2828
`subctrl.db` in the app documents directory; `schemaVersion` is 1.
2929
- Currency seeds: `lib/infrastructure/persistence/seeds/currency_seed_data.dart`.
30-
- External rates: `YahooFinanceCurrencyClient` and
31-
`SubscriptionCurrencyRatesClient`.
30+
- Currency rates backend lives in `backend/` (Cloudflare Workers); the Flutter
31+
app uses `ProxyCurrencyRatesClient` and `SubscriptionCurrencyRatesClient`.
3232
- Notifications: `LocalNotificationsService` (timezone aware).
3333

3434
## Repo map
@@ -37,6 +37,7 @@ with the architecture below.
3737
- `lib/application/` use cases and DI
3838
- `lib/domain/` entities/repositories/services
3939
- `lib/infrastructure/` persistence/currency/platform/repositories
40+
- `backend/` Cloudflare Worker for currency rates
4041
- `test/` mirrors layers
4142

4243
## Coding and testing

README.md

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ sends optional local reminders before renewals hit.
2323
## Features
2424

2525
- Track subscriptions with local storage backed by Drift (`subctrl.db`).
26-
- Automatic currency conversion via Yahoo Finance rates with historical seeds to keep analytics stable offline.
26+
- Automatic currency conversion via a proxy rates service with historical seeds to keep analytics stable offline.
2727
- Analytics tabs for monthly burn, category totals, and long-term spend trends.
2828
- Local, timezone-aware notifications driven by `LocalNotificationsService`.
2929
- Clean Architecture split into presentation, application, domain, and
@@ -38,22 +38,6 @@ sends optional local reminders before renewals hit.
3838
<img src="assets/analytics-overall.png" width="260" alt="Analytics overview" />
3939
</p>
4040

41-
## Architecture at a Glance
42-
43-
```
44-
lib/
45-
├─ presentation/ # UI, view models, localization
46-
├─ application/ # Use cases and dependency wiring
47-
├─ domain/ # Pure business logic + repository interfaces
48-
└─ infrastructure/ # Drift database, currency clients, platform services
49-
```
50-
51-
- `lib/main.dart` boots the tabbed UI and wires dependencies.
52-
- `lib/application/app_dependencies.dart` registers repositories, use cases, and
53-
disposes the `YahooFinanceCurrencyClient`.
54-
- Data lives in `lib/infrastructure/persistence/database.dart` (schema version 1
55-
stored as `subctrl.db` in the app documents directory).
56-
5741
## Getting Started
5842

5943
1. **Prereqs**: Flutter 3.38.5 (matches CI), Dart SDK ^3.10.3, Xcode for iOS
@@ -69,26 +53,6 @@ lib/
6953
4. **Configure notifications** (optional): ensure the iOS simulator/device has
7054
notification permissions enabled so local reminders can fire.
7155

72-
## GitHub Pages Policies
73-
74-
Static policy/support pages for App Store review live in `docs/`. The folder now
75-
contains a minimal Jekyll setup (`_config.yml`, `_layouts`, and `assets`) so
76-
Markdown pages gain HTML wrappers when GitHub Pages builds the `gh-pages`
77-
branch. The `Publish Docs` workflow pushes the folder to `gh-pages` only when a
78-
commit touching `docs/` lands on `master`, so publishing simply means editing
79-
Markdown with the required front matter (`layout`, `title`, `permalink`) and
80-
pushing your change. Preview the pages locally with any static server (they
81-
render as plain Markdown locally) or let GitHub Pages handle the Jekyll build.
82-
83-
## Testing
84-
85-
All unit and widget tests run through `flutter test --coverage`, and coverage
86-
must stay above 70%. Run locally with:
87-
88-
```bash
89-
flutter test --coverage
90-
```
91-
9256
## License
9357

9458
MIT © Denis Dementev

docs/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ docs_dir: pages
77
site_dir: site
88
nav:
99
- Home: index.md
10+
- Currency Rates Backend: rates-backend.md
1011
- Policy:
1112
- Privacy Policy (EN): policy-en.md
1213
- Политика конфиденциальности (RU): policy-ru.md

docs/pages/rates-backend.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Currency Rates Backend
2+
3+
Subctrl runs its own backend service for currency rates. The Flutter app talks
4+
to it through `ProxyCurrencyRatesClient`.
5+
6+
## API Contract
7+
8+
`GET /v1/rates`
9+
10+
Query parameters:
11+
12+
- `base`: 3-letter ISO code (example: `USD`).
13+
- `quotes`: comma-separated 3-letter codes (example: `EUR,GBP`).
14+
15+
Response (`200`):
16+
17+
```json
18+
{
19+
"base": "USD",
20+
"rates": [
21+
{
22+
"quote": "EUR",
23+
"rate": 1.08,
24+
"fetched_at": "2026-01-23T12:00:00Z"
25+
}
26+
],
27+
"provider": "yahoo_finance",
28+
"as_of": "2026-01-23T12:00:05Z"
29+
}
30+
```
31+
32+
Errors:
33+
34+
- `400` validation or unsupported currencies.
35+
- `429` rate limit exceeded.
36+
- `502` upstream fetch failure.
37+
38+
Error body:
39+
40+
```json
41+
{
42+
"error": {
43+
"code": "validation_error",
44+
"message": "...",
45+
"details": {}
46+
}
47+
}
48+
```
49+
50+
Caching:
51+
52+
- Responses include `Cache-Control` and `ETag` headers.
53+
- Send `If-None-Match` to receive `304 Not Modified`.
54+
55+
Refresh cadence defaults to 5 minutes (`CACHE_MAX_AGE_SECONDS=300`).
56+
57+
Supported currencies:
58+
59+
- By default the backend accepts any 3-letter ISO code.
60+
- Set `SUPPORTED_CURRENCY_CODES` (comma-separated) to restrict supported codes.
61+
62+
## Configuration
63+
64+
Environment variables:
65+
66+
- `CACHE_MAX_AGE_SECONDS` (default `300`)
67+
- `RATE_LIMIT_MAX` (default `60`)
68+
- `RATE_LIMIT_WINDOW_SECONDS` (default `60`)
69+
- `UPSTREAM_TIMEOUT_SECONDS` (default `6`)
70+
71+
Flutter client override:
72+
73+
- `SUBCTRL_RATES_URL` (compile-time Dart define) to point the app at the backend base URL.
74+
75+
## Cloudflare Workers Deployment
76+
77+
From `backend/`:
78+
79+
```bash
80+
wrangler deploy
81+
```
82+
83+
Required secrets/vars:
84+
85+
- `CF_API_TOKEN` and `CF_ACCOUNT_ID` configured in CI.
86+
- Configure the environment variables above via `wrangler.toml` or the Cloudflare dashboard.
87+
88+
CI deploys the worker on pushes to `master` when `backend/` changes.

docs/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
2-
name = "safex-docs"
2+
name = "subctrl-docs"
33
version = "0.1.0"
4-
description = "Safex docs"
4+
description = "Subctrl docs"
55
readme = "README.md"
66
requires-python = ">=3.12"
77
dependencies = [

docs/uv.lock

Lines changed: 28 additions & 28 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/application/app_dependencies.dart

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ import 'package:subctrl/application/tags/create_tag_use_case.dart';
3434
import 'package:subctrl/application/tags/delete_tag_use_case.dart';
3535
import 'package:subctrl/application/tags/update_tag_use_case.dart';
3636
import 'package:subctrl/application/tags/watch_tags_use_case.dart';
37+
import 'package:subctrl/infrastructure/currency/proxy_currency_client.dart';
3738
import 'package:subctrl/infrastructure/currency/subscription_currency_rates_client.dart';
38-
import 'package:subctrl/infrastructure/currency/yahoo_finance_client.dart';
3939
import 'package:subctrl/infrastructure/persistence/daos/currencies_dao.dart';
4040
import 'package:subctrl/infrastructure/persistence/daos/currency_rates_dao.dart';
4141
import 'package:subctrl/infrastructure/persistence/daos/settings_dao.dart';
@@ -88,8 +88,8 @@ class AppDependencies {
8888
required this.scheduleNotificationsUseCase,
8989
required this.requestNotificationPermissionUseCase,
9090
required this.openNotificationSettingsUseCase,
91-
required YahooFinanceCurrencyClient yahooFinanceCurrencyClient,
92-
}) : _yahooFinanceCurrencyClient = yahooFinanceCurrencyClient;
91+
required ProxyCurrencyRatesClient proxyCurrencyRatesClient,
92+
}) : _proxyCurrencyRatesClient = proxyCurrencyRatesClient;
9393

9494
factory AppDependencies() {
9595
final database = AppDatabase();
@@ -107,9 +107,9 @@ class AppDependencies {
107107
);
108108
final tagRepository = DriftTagRepository(tagsDao);
109109
final settingsRepository = DriftSettingsRepository(settingsDao);
110-
final yahooFinanceClient = YahooFinanceCurrencyClient();
110+
final proxyRatesClient = ProxyCurrencyRatesClient();
111111
final subscriptionRatesClient = SubscriptionCurrencyRatesClient(
112-
yahooFinanceCurrencyClient: yahooFinanceClient,
112+
proxyCurrencyClient: proxyRatesClient,
113113
currencyRepository: currencyRepository,
114114
);
115115
final localNotificationsService = LocalNotificationsService();
@@ -182,19 +182,18 @@ class AppDependencies {
182182
getPendingNotificationsUseCase: GetPendingNotificationsUseCase(
183183
localNotificationsService,
184184
),
185-
cancelNotificationsUseCase:
186-
CancelNotificationsUseCase(localNotificationsService),
185+
cancelNotificationsUseCase: CancelNotificationsUseCase(
186+
localNotificationsService,
187+
),
187188
scheduleNotificationsUseCase: ScheduleNotificationsUseCase(
188189
localNotificationsService,
189190
),
190191
requestNotificationPermissionUseCase:
191-
RequestNotificationPermissionUseCase(
192-
notificationPermissionService,
193-
),
192+
RequestNotificationPermissionUseCase(notificationPermissionService),
194193
openNotificationSettingsUseCase: OpenNotificationSettingsUseCase(
195194
notificationPermissionService,
196195
),
197-
yahooFinanceCurrencyClient: yahooFinanceClient,
196+
proxyCurrencyRatesClient: proxyRatesClient,
198197
);
199198
}
200199

@@ -242,9 +241,9 @@ class AppDependencies {
242241
requestNotificationPermissionUseCase;
243242
final OpenNotificationSettingsUseCase openNotificationSettingsUseCase;
244243

245-
final YahooFinanceCurrencyClient _yahooFinanceCurrencyClient;
244+
final ProxyCurrencyRatesClient _proxyCurrencyRatesClient;
246245

247246
void dispose() {
248-
_yahooFinanceCurrencyClient.close();
247+
_proxyCurrencyRatesClient.close();
249248
}
250249
}

0 commit comments

Comments
 (0)