Skip to content
Merged
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
7 changes: 4 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ with the architecture below.

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

## Storage and services

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

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

## Coding and testing
Expand Down
38 changes: 1 addition & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ sends optional local reminders before renewals hit.
## Features

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

## Architecture at a Glance

```
lib/
├─ presentation/ # UI, view models, localization
├─ application/ # Use cases and dependency wiring
├─ domain/ # Pure business logic + repository interfaces
└─ infrastructure/ # Drift database, currency clients, platform services
```

- `lib/main.dart` boots the tabbed UI and wires dependencies.
- `lib/application/app_dependencies.dart` registers repositories, use cases, and
disposes the `YahooFinanceCurrencyClient`.
- Data lives in `lib/infrastructure/persistence/database.dart` (schema version 1
stored as `subctrl.db` in the app documents directory).

## Getting Started

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

## GitHub Pages Policies

Static policy/support pages for App Store review live in `docs/`. The folder now
contains a minimal Jekyll setup (`_config.yml`, `_layouts`, and `assets`) so
Markdown pages gain HTML wrappers when GitHub Pages builds the `gh-pages`
branch. The `Publish Docs` workflow pushes the folder to `gh-pages` only when a
commit touching `docs/` lands on `master`, so publishing simply means editing
Markdown with the required front matter (`layout`, `title`, `permalink`) and
pushing your change. Preview the pages locally with any static server (they
render as plain Markdown locally) or let GitHub Pages handle the Jekyll build.

## Testing

All unit and widget tests run through `flutter test --coverage`, and coverage
must stay above 70%. Run locally with:

```bash
flutter test --coverage
```

## License

MIT © Denis Dementev
1 change: 1 addition & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ docs_dir: pages
site_dir: site
nav:
- Home: index.md
- Currency Rates Backend: rates-backend.md
- Policy:
- Privacy Policy (EN): policy-en.md
- Политика конфиденциальности (RU): policy-ru.md
Expand Down
88 changes: 88 additions & 0 deletions docs/pages/rates-backend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Currency Rates Backend

Subctrl runs its own backend service for currency rates. The Flutter app talks
to it through `ProxyCurrencyRatesClient`.

## API Contract

`GET /v1/rates`

Query parameters:

- `base`: 3-letter ISO code (example: `USD`).
- `quotes`: comma-separated 3-letter codes (example: `EUR,GBP`).

Response (`200`):

```json
{
"base": "USD",
"rates": [
{
"quote": "EUR",
"rate": 1.08,
"fetched_at": "2026-01-23T12:00:00Z"
}
],
"provider": "yahoo_finance",
"as_of": "2026-01-23T12:00:05Z"
}
```

Errors:

- `400` validation or unsupported currencies.
- `429` rate limit exceeded.
- `502` upstream fetch failure.

Error body:

```json
{
"error": {
"code": "validation_error",
"message": "...",
"details": {}
}
}
```

Caching:

- Responses include `Cache-Control` and `ETag` headers.
- Send `If-None-Match` to receive `304 Not Modified`.

Refresh cadence defaults to 5 minutes (`CACHE_MAX_AGE_SECONDS=300`).

Supported currencies:

- By default the backend accepts any 3-letter ISO code.
- Set `SUPPORTED_CURRENCY_CODES` (comma-separated) to restrict supported codes.

## Configuration

Environment variables:

- `CACHE_MAX_AGE_SECONDS` (default `300`)
- `RATE_LIMIT_MAX` (default `60`)
- `RATE_LIMIT_WINDOW_SECONDS` (default `60`)
- `UPSTREAM_TIMEOUT_SECONDS` (default `6`)

Flutter client override:

- `SUBCTRL_RATES_URL` (compile-time Dart define) to point the app at the backend base URL.

## Cloudflare Workers Deployment

From `backend/`:

```bash
wrangler deploy
```

Required secrets/vars:

- `CF_API_TOKEN` and `CF_ACCOUNT_ID` configured in CI.
- Configure the environment variables above via `wrangler.toml` or the Cloudflare dashboard.

CI deploys the worker on pushes to `master` when `backend/` changes.
4 changes: 2 additions & 2 deletions docs/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "safex-docs"
name = "subctrl-docs"
version = "0.1.0"
description = "Safex docs"
description = "Subctrl docs"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
Expand Down
56 changes: 28 additions & 28 deletions docs/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 12 additions & 13 deletions lib/application/app_dependencies.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ import 'package:subctrl/application/tags/create_tag_use_case.dart';
import 'package:subctrl/application/tags/delete_tag_use_case.dart';
import 'package:subctrl/application/tags/update_tag_use_case.dart';
import 'package:subctrl/application/tags/watch_tags_use_case.dart';
import 'package:subctrl/infrastructure/currency/proxy_currency_client.dart';
import 'package:subctrl/infrastructure/currency/subscription_currency_rates_client.dart';
import 'package:subctrl/infrastructure/currency/yahoo_finance_client.dart';
import 'package:subctrl/infrastructure/persistence/daos/currencies_dao.dart';
import 'package:subctrl/infrastructure/persistence/daos/currency_rates_dao.dart';
import 'package:subctrl/infrastructure/persistence/daos/settings_dao.dart';
Expand Down Expand Up @@ -88,8 +88,8 @@ class AppDependencies {
required this.scheduleNotificationsUseCase,
required this.requestNotificationPermissionUseCase,
required this.openNotificationSettingsUseCase,
required YahooFinanceCurrencyClient yahooFinanceCurrencyClient,
}) : _yahooFinanceCurrencyClient = yahooFinanceCurrencyClient;
required ProxyCurrencyRatesClient proxyCurrencyRatesClient,
}) : _proxyCurrencyRatesClient = proxyCurrencyRatesClient;

factory AppDependencies() {
final database = AppDatabase();
Expand All @@ -107,9 +107,9 @@ class AppDependencies {
);
final tagRepository = DriftTagRepository(tagsDao);
final settingsRepository = DriftSettingsRepository(settingsDao);
final yahooFinanceClient = YahooFinanceCurrencyClient();
final proxyRatesClient = ProxyCurrencyRatesClient();
final subscriptionRatesClient = SubscriptionCurrencyRatesClient(
yahooFinanceCurrencyClient: yahooFinanceClient,
proxyCurrencyClient: proxyRatesClient,
currencyRepository: currencyRepository,
);
final localNotificationsService = LocalNotificationsService();
Expand Down Expand Up @@ -182,19 +182,18 @@ class AppDependencies {
getPendingNotificationsUseCase: GetPendingNotificationsUseCase(
localNotificationsService,
),
cancelNotificationsUseCase:
CancelNotificationsUseCase(localNotificationsService),
cancelNotificationsUseCase: CancelNotificationsUseCase(
localNotificationsService,
),
scheduleNotificationsUseCase: ScheduleNotificationsUseCase(
localNotificationsService,
),
requestNotificationPermissionUseCase:
RequestNotificationPermissionUseCase(
notificationPermissionService,
),
RequestNotificationPermissionUseCase(notificationPermissionService),
openNotificationSettingsUseCase: OpenNotificationSettingsUseCase(
notificationPermissionService,
),
yahooFinanceCurrencyClient: yahooFinanceClient,
proxyCurrencyRatesClient: proxyRatesClient,
);
}

Expand Down Expand Up @@ -242,9 +241,9 @@ class AppDependencies {
requestNotificationPermissionUseCase;
final OpenNotificationSettingsUseCase openNotificationSettingsUseCase;

final YahooFinanceCurrencyClient _yahooFinanceCurrencyClient;
final ProxyCurrencyRatesClient _proxyCurrencyRatesClient;

void dispose() {
_yahooFinanceCurrencyClient.close();
_proxyCurrencyRatesClient.close();
}
}
Loading
Loading