|
| 1 | +# Feature 119: Recurring Charge Detection & Suggestions |
| 2 | +> **Status:** Planning |
| 3 | +
|
| 4 | +## Overview |
| 5 | + |
| 6 | +Automatically detect recurring charges from transaction history and suggest creating `RecurringTransaction` entries. Today, users must manually create recurring transactions. This feature analyzes imported transaction data to identify patterns — such as "Netflix" appearing monthly at $15.99 — and surfaces actionable suggestions the user can accept, dismiss, or tune. |
| 7 | + |
| 8 | +## Problem Statement |
| 9 | + |
| 10 | +### Current State |
| 11 | + |
| 12 | +- Users manually create recurring transactions with a description, amount, account, recurrence pattern, and optional category. |
| 13 | +- `ImportPatternValue` on `RecurringTransaction` lets the system auto-link future imports, but only **after** a recurring transaction already exists. |
| 14 | +- There is no automated way to discover recurring charges buried in months of transaction history. Users must notice patterns themselves. |
| 15 | + |
| 16 | +### Target State |
| 17 | + |
| 18 | +- The system scans transaction history, groups by normalized merchant/description, and detects regular intervals and consistent amounts. |
| 19 | +- Detected patterns are presented as **Recurring Charge Suggestions** with confidence scores. |
| 20 | +- Users can accept a suggestion (creating a `RecurringTransaction` with pre-filled fields and import patterns), dismiss it, or adjust parameters before accepting. |
| 21 | +- Already-linked transactions (those with a `RecurringTransactionId`) are excluded from detection to avoid duplicates. |
| 22 | +- Detection can run on-demand or automatically after a CSV import. |
| 23 | + |
| 24 | +--- |
| 25 | + |
| 26 | +## User Stories |
| 27 | + |
| 28 | +### Detection |
| 29 | + |
| 30 | +#### US-119-001: Detect Recurring Charges |
| 31 | +**As a** budget user |
| 32 | +**I want** the system to analyze my transaction history and identify charges that recur on a regular schedule |
| 33 | +**So that** I don't have to manually spot and create recurring transactions |
| 34 | + |
| 35 | +**Acceptance Criteria:** |
| 36 | +- [ ] Transactions are grouped by normalized description (stripping noise like POS/PURCHASE prefixes, trailing reference numbers) |
| 37 | +- [ ] Groups with ≥ 3 occurrences within the analysis window are evaluated for periodicity |
| 38 | +- [ ] Supported frequencies detected: Weekly, BiWeekly, Monthly, Quarterly, Yearly |
| 39 | +- [ ] Amount variance tolerance is configurable (default ±5 %) to handle slight fluctuations |
| 40 | +- [ ] Each detected pattern receives a confidence score (0.0–1.0) based on interval regularity, amount consistency, and sample size |
| 41 | +- [ ] Transactions already linked to a `RecurringTransaction` are excluded |
| 42 | + |
| 43 | +#### US-119-002: View Recurring Charge Suggestions |
| 44 | +**As a** budget user |
| 45 | +**I want** to see a list of detected recurring charge suggestions sorted by confidence |
| 46 | +**So that** I can quickly review and act on them |
| 47 | + |
| 48 | +**Acceptance Criteria:** |
| 49 | +- [ ] Suggestions display: merchant/description, detected frequency, average amount, number of matching transactions, confidence score, and last occurrence date |
| 50 | +- [ ] Suggestions are sorted by confidence descending by default |
| 51 | +- [ ] User can filter by status (Pending, Accepted, Dismissed) |
| 52 | + |
| 53 | +#### US-119-003: Accept a Recurring Charge Suggestion |
| 54 | +**As a** budget user |
| 55 | +**I want** to accept a suggestion and have a `RecurringTransaction` created automatically |
| 56 | +**So that** future imports are tracked and budgeted correctly |
| 57 | + |
| 58 | +**Acceptance Criteria:** |
| 59 | +- [ ] Accepting creates a `RecurringTransaction` with description, amount, detected recurrence pattern, account, and category (if transactions were categorized) |
| 60 | +- [ ] An `ImportPatternValue` is auto-generated from the normalized description so future imports link automatically |
| 61 | +- [ ] Existing matching transactions are retroactively linked to the new `RecurringTransaction` via `RecurringTransactionId` |
| 62 | +- [ ] Suggestion status changes to Accepted |
| 63 | +- [ ] User can edit fields (description, amount, frequency, category) before confirming |
| 64 | + |
| 65 | +#### US-119-004: Dismiss a Recurring Charge Suggestion |
| 66 | +**As a** budget user |
| 67 | +**I want** to dismiss a suggestion I don't care about |
| 68 | +**So that** it doesn't clutter my pending list |
| 69 | + |
| 70 | +**Acceptance Criteria:** |
| 71 | +- [ ] Dismissed suggestions are hidden from the default view |
| 72 | +- [ ] Dismissed suggestions can be viewed in a separate "Dismissed" list |
| 73 | +- [ ] Dismissing a suggestion does not delete it; it can be restored |
| 74 | + |
| 75 | +### Post-Import Trigger |
| 76 | + |
| 77 | +#### US-119-005: Auto-Detect After Import |
| 78 | +**As a** budget user |
| 79 | +**I want** the system to automatically run recurring charge detection after I import transactions |
| 80 | +**So that** new patterns are surfaced without manual effort |
| 81 | + |
| 82 | +**Acceptance Criteria:** |
| 83 | +- [ ] After a successful CSV import, detection runs for the affected account(s) |
| 84 | +- [ ] Only new/changed suggestions are surfaced (no duplicate suggestions for already-pending patterns) |
| 85 | +- [ ] User is notified of new suggestions (UI indicator or toast) |
| 86 | + |
| 87 | +--- |
| 88 | + |
| 89 | +## Technical Design |
| 90 | + |
| 91 | +### Architecture Changes |
| 92 | + |
| 93 | +New components slot into the existing layered architecture: |
| 94 | + |
| 95 | +| Layer | New Component | Responsibility | |
| 96 | +|-------|---------------|----------------| |
| 97 | +| Domain | `RecurringChargeSuggestion` entity | Persists detected patterns and user decisions | |
| 98 | +| Domain | `RecurrenceDetector` (pure logic) | Groups transactions, detects intervals, scores confidence | |
| 99 | +| Application | `IRecurringChargeDetectionService` | Orchestrates detection, stores suggestions | |
| 100 | +| Application | `RecurringChargeSuggestionAcceptanceHandler` | Creates `RecurringTransaction` + links transactions on accept | |
| 101 | +| Contracts | `RecurringChargeSuggestionResponse` | API DTO | |
| 102 | +| Infrastructure | `RecurringChargeSuggestionRepository` | EF Core persistence | |
| 103 | +| API | `RecurringChargeSuggestionsController` | REST endpoints | |
| 104 | +| Client | `RecurringChargeSuggestions` component | UI for reviewing and acting on suggestions | |
| 105 | + |
| 106 | +### Domain Model |
| 107 | + |
| 108 | +```csharp |
| 109 | +// New entity: src/BudgetExperiment.Domain/Recurring/RecurringChargeSuggestion.cs |
| 110 | +public class RecurringChargeSuggestion |
| 111 | +{ |
| 112 | + public Guid Id { get; private set; } |
| 113 | + public Guid AccountId { get; private set; } |
| 114 | + public string NormalizedDescription { get; private set; } |
| 115 | + public string SampleDescription { get; private set; } // Original description for display |
| 116 | + public MoneyValue AverageAmount { get; private set; } |
| 117 | + public RecurrenceFrequency DetectedFrequency { get; private set; } |
| 118 | + public int DetectedInterval { get; private set; } // e.g. 1 for monthly, 2 for every-other-month |
| 119 | + public decimal Confidence { get; private set; } // 0.0 – 1.0 |
| 120 | + public int MatchingTransactionCount { get; private set; } |
| 121 | + public DateOnly FirstOccurrence { get; private set; } |
| 122 | + public DateOnly LastOccurrence { get; private set; } |
| 123 | + public Guid? CategoryId { get; private set; } // Most-used category from matched transactions |
| 124 | + public SuggestionStatus Status { get; private set; } // Pending, Accepted, Dismissed |
| 125 | + public Guid? AcceptedRecurringTransactionId { get; private set; } // Set on accept |
| 126 | + public BudgetScope Scope { get; private set; } |
| 127 | + public Guid? OwnerUserId { get; private set; } |
| 128 | + public Guid CreatedByUserId { get; private set; } |
| 129 | + public DateTime CreatedAtUtc { get; private set; } |
| 130 | + public DateTime UpdatedAtUtc { get; private set; } |
| 131 | +} |
| 132 | +``` |
| 133 | + |
| 134 | +```csharp |
| 135 | +// Pure domain logic: src/BudgetExperiment.Domain/Recurring/RecurrenceDetector.cs |
| 136 | +public static class RecurrenceDetector |
| 137 | +{ |
| 138 | + public static IReadOnlyList<DetectedPattern> Detect( |
| 139 | + IReadOnlyList<Transaction> transactions, |
| 140 | + RecurrenceDetectionOptions options); |
| 141 | +} |
| 142 | + |
| 143 | +public record DetectedPattern( |
| 144 | + string NormalizedDescription, |
| 145 | + string SampleDescription, |
| 146 | + MoneyValue AverageAmount, |
| 147 | + RecurrenceFrequency Frequency, |
| 148 | + int Interval, |
| 149 | + decimal Confidence, |
| 150 | + IReadOnlyList<Transaction> MatchingTransactions, |
| 151 | + DateOnly FirstOccurrence, |
| 152 | + DateOnly LastOccurrence, |
| 153 | + Guid? MostUsedCategoryId); |
| 154 | + |
| 155 | +public record RecurrenceDetectionOptions( |
| 156 | + int MinimumOccurrences = 3, |
| 157 | + decimal AmountVarianceTolerance = 0.05m, // ±5% |
| 158 | + int AnalysisWindowMonths = 12); |
| 159 | +``` |
| 160 | + |
| 161 | +### API Endpoints |
| 162 | + |
| 163 | +| Method | Endpoint | Description | |
| 164 | +|--------|----------|-------------| |
| 165 | +| POST | `/api/v1/recurring-charge-suggestions/detect` | Trigger detection for an account or all accounts | |
| 166 | +| GET | `/api/v1/recurring-charge-suggestions` | List suggestions (filterable by status, account) | |
| 167 | +| GET | `/api/v1/recurring-charge-suggestions/{id}` | Get suggestion detail with matched transactions | |
| 168 | +| POST | `/api/v1/recurring-charge-suggestions/{id}/accept` | Accept suggestion → create RecurringTransaction | |
| 169 | +| POST | `/api/v1/recurring-charge-suggestions/{id}/dismiss` | Dismiss suggestion | |
| 170 | + |
| 171 | +### Database Changes |
| 172 | + |
| 173 | +- New table `RecurringChargeSuggestions` with columns matching the entity above. |
| 174 | +- Index on `(AccountId, Status)` for filtered queries. |
| 175 | +- Index on `(NormalizedDescription, AccountId)` for duplicate detection. |
| 176 | + |
| 177 | +### Description Normalization |
| 178 | + |
| 179 | +Reuse the merchant-normalization logic from the existing AI prompt system and `MerchantKnowledgeBase`. The normalizer should: |
| 180 | + |
| 181 | +1. Strip common bank prefixes: `POS`, `PURCHASE`, `DEBIT`, `ACH`, `CHECKCARD`, etc. |
| 182 | +2. Strip trailing reference/confirmation numbers (numeric sequences > 4 digits at end). |
| 183 | +3. Strip trailing dates in common formats. |
| 184 | +4. Trim and collapse whitespace. |
| 185 | +5. Case-fold to uppercase for grouping, preserve original for display. |
| 186 | + |
| 187 | +### Confidence Scoring |
| 188 | + |
| 189 | +Confidence is a weighted composite: |
| 190 | + |
| 191 | +| Factor | Weight | Description | |
| 192 | +|--------|--------|-------------| |
| 193 | +| Interval regularity | 40% | Standard deviation of days between occurrences vs. expected interval | |
| 194 | +| Amount consistency | 30% | Coefficient of variation of amounts (lower = higher score) | |
| 195 | +| Sample size | 20% | More occurrences = higher confidence (capped at 12) | |
| 196 | +| Recency | 10% | Bonus if last occurrence is within one expected interval of today | |
| 197 | + |
| 198 | +Minimum confidence threshold for surfacing: **0.5** (configurable). |
| 199 | + |
| 200 | +### UI Components |
| 201 | + |
| 202 | +- **RecurringChargeSuggestions page** — table/card list of pending suggestions with accept/dismiss actions. |
| 203 | +- **Suggestion detail modal** — shows matched transactions, lets user edit fields before accepting. |
| 204 | +- **Post-import banner** — "We found N new recurring charge patterns. [Review]" after CSV import. |
| 205 | + |
| 206 | +--- |
| 207 | + |
| 208 | +## Implementation Plan |
| 209 | + |
| 210 | +### Phase 1: Domain – Description Normalizer & Recurrence Detector |
| 211 | + |
| 212 | +**Objective:** Build the pure domain logic for normalizing transaction descriptions and detecting recurrence patterns. Fully TDD. |
| 213 | + |
| 214 | +**Tasks:** |
| 215 | +- [ ] Create `DescriptionNormalizer` static class with bank-prefix stripping, reference-number removal, whitespace normalization |
| 216 | +- [ ] Write unit tests for normalizer edge cases (various bank formats, international characters) |
| 217 | +- [ ] Create `RecurrenceDetector` static class with grouping, interval detection, confidence scoring |
| 218 | +- [ ] Write unit tests for detection: monthly charges, weekly charges, varying amounts within tolerance, noise rejection |
| 219 | +- [ ] Create `RecurringChargeSuggestion` entity with factory method and status transitions |
| 220 | +- [ ] Write unit tests for entity invariants |
| 221 | + |
| 222 | +**Commit:** |
| 223 | +```bash |
| 224 | +git commit -m "feat(domain): add recurrence detection and description normalizer |
| 225 | +
|
| 226 | +- DescriptionNormalizer strips bank prefixes and trailing references |
| 227 | +- RecurrenceDetector groups transactions and detects periodic patterns |
| 228 | +- RecurringChargeSuggestion entity with confidence scoring |
| 229 | +- Comprehensive unit tests for all detection scenarios |
| 230 | +
|
| 231 | +Refs: #119" |
| 232 | +``` |
| 233 | + |
| 234 | +--- |
| 235 | + |
| 236 | +### Phase 2: Infrastructure – Persistence & Repository |
| 237 | + |
| 238 | +**Objective:** Add EF Core configuration and repository for `RecurringChargeSuggestion`. |
| 239 | + |
| 240 | +**Tasks:** |
| 241 | +- [ ] Add `RecurringChargeSuggestion` to `BudgetDbContext` |
| 242 | +- [ ] Create EF Core entity configuration (table, indexes, value conversions) |
| 243 | +- [ ] Add migration |
| 244 | +- [ ] Create `IRecurringChargeSuggestionRepository` interface in Domain |
| 245 | +- [ ] Implement repository in Infrastructure |
| 246 | +- [ ] Write integration tests with test database |
| 247 | + |
| 248 | +**Commit:** |
| 249 | +```bash |
| 250 | +git commit -m "feat(infra): add RecurringChargeSuggestion persistence |
| 251 | +
|
| 252 | +- EF Core configuration with indexes on AccountId+Status |
| 253 | +- Migration for RecurringChargeSuggestions table |
| 254 | +- Repository implementation with filtered queries |
| 255 | +
|
| 256 | +Refs: #119" |
| 257 | +``` |
| 258 | + |
| 259 | +--- |
| 260 | + |
| 261 | +### Phase 3: Application – Detection Service & Acceptance Handler |
| 262 | + |
| 263 | +**Objective:** Orchestration layer that ties detection to persistence and handles accept/dismiss workflows. |
| 264 | + |
| 265 | +**Tasks:** |
| 266 | +- [ ] Create `IRecurringChargeDetectionService` with `DetectAsync(Guid? accountId)` and suggestion CRUD |
| 267 | +- [ ] Implement service: load transactions, run detector, upsert suggestions (avoid duplicates) |
| 268 | +- [ ] Create `RecurringChargeSuggestionAcceptanceHandler`: on accept, create `RecurringTransaction`, generate `ImportPatternValue`, link existing transactions |
| 269 | +- [ ] Write unit tests with faked repositories |
| 270 | +- [ ] Add post-import hook: call detection after `ImportService` completes |
| 271 | + |
| 272 | +**Commit:** |
| 273 | +```bash |
| 274 | +git commit -m "feat(app): add recurring charge detection service |
| 275 | +
|
| 276 | +- DetectAsync analyzes transactions and persists suggestions |
| 277 | +- AcceptanceHandler creates RecurringTransaction from suggestion |
| 278 | +- Post-import trigger for automatic detection |
| 279 | +- Unit tests with faked repositories |
| 280 | +
|
| 281 | +Refs: #119" |
| 282 | +``` |
| 283 | + |
| 284 | +--- |
| 285 | + |
| 286 | +### Phase 4: Contracts & API Endpoints |
| 287 | + |
| 288 | +**Objective:** Expose recurring charge suggestions via REST API. |
| 289 | + |
| 290 | +**Tasks:** |
| 291 | +- [ ] Add `RecurringChargeSuggestionResponse`, `DetectRecurringChargesRequest`, `AcceptRecurringChargeSuggestionRequest` DTOs to Contracts |
| 292 | +- [ ] Create `RecurringChargeSuggestionsController` with endpoints per design table |
| 293 | +- [ ] Add mapping between domain and contracts |
| 294 | +- [ ] Write API integration tests (happy path + validation + 404) |
| 295 | +- [ ] Verify OpenAPI spec generation |
| 296 | + |
| 297 | +**Commit:** |
| 298 | +```bash |
| 299 | +git commit -m "feat(api): add recurring charge suggestion endpoints |
| 300 | +
|
| 301 | +- POST detect, GET list/detail, POST accept/dismiss |
| 302 | +- Request validation and Problem Details error responses |
| 303 | +- Integration tests with WebApplicationFactory |
| 304 | +
|
| 305 | +Refs: #119" |
| 306 | +``` |
| 307 | + |
| 308 | +--- |
| 309 | + |
| 310 | +### Phase 5: Client UI |
| 311 | + |
| 312 | +**Objective:** Blazor WebAssembly UI for reviewing and acting on recurring charge suggestions. |
| 313 | + |
| 314 | +**Tasks:** |
| 315 | +- [ ] Create `RecurringChargeSuggestions.razor` page with sortable/filterable table |
| 316 | +- [ ] Create `RecurringChargeSuggestionDetail.razor` modal with editable fields and matched transaction list |
| 317 | +- [ ] Add post-import notification banner to import page |
| 318 | +- [ ] Add navigation link in sidebar |
| 319 | +- [ ] Write bUnit tests for component logic |
| 320 | + |
| 321 | +**Commit:** |
| 322 | +```bash |
| 323 | +git commit -m "feat(client): add recurring charge suggestions UI |
| 324 | +
|
| 325 | +- Suggestions page with confidence-sorted table |
| 326 | +- Detail modal with editable accept flow |
| 327 | +- Post-import notification banner |
| 328 | +- Navigation integration |
| 329 | +
|
| 330 | +Refs: #119" |
| 331 | +``` |
| 332 | + |
| 333 | +--- |
| 334 | + |
| 335 | +### Phase 6: Documentation & Cleanup |
| 336 | + |
| 337 | +**Objective:** Final polish, documentation updates, and cleanup. |
| 338 | + |
| 339 | +**Tasks:** |
| 340 | +- [ ] Update API documentation / OpenAPI specs |
| 341 | +- [ ] Add XML comments for public APIs |
| 342 | +- [ ] Remove any TODO comments |
| 343 | +- [ ] Final code review |
| 344 | + |
| 345 | +**Commit:** |
| 346 | +```bash |
| 347 | +git commit -m "docs(recurring): add documentation for feature 119 |
| 348 | +
|
| 349 | +- XML comments for public API surface |
| 350 | +- OpenAPI spec updates |
| 351 | +
|
| 352 | +Refs: #119" |
| 353 | +``` |
| 354 | + |
| 355 | +--- |
| 356 | + |
| 357 | +## Design Decisions & Notes |
| 358 | + |
| 359 | +1. **Pure domain detection** — `RecurrenceDetector` is a static, pure function with no dependencies. This keeps it fast, testable, and free of infrastructure concerns. The application service handles I/O. |
| 360 | + |
| 361 | +2. **Separate entity from `CategorySuggestion`** — Although both are "suggestion" concepts, recurring charge suggestions have different lifecycle (they create `RecurringTransaction` + link transactions, not categories/rules). A shared `SuggestionStatus` enum is reused. |
| 362 | + |
| 363 | +3. **No AI required** — Detection is algorithmic (interval math + statistical scoring), not AI-driven. This keeps it fast, deterministic, and works without an AI provider configured. AI could enhance normalization in a future iteration. |
| 364 | + |
| 365 | +4. **Duplicate avoidance** — When detection re-runs, existing pending suggestions for the same `(NormalizedDescription, AccountId)` are updated rather than duplicated. |
| 366 | + |
| 367 | +5. **Amount tolerance** — Fixed-amount subscriptions get high confidence. Variable charges (e.g., utility bills) still match if within the configured tolerance, but with lower confidence. |
| 368 | + |
| 369 | +## Conventional Commit Reference |
| 370 | + |
| 371 | +| Type | When to Use | SemVer Impact | |
| 372 | +|------|-------------|---------------| |
| 373 | +| `feat` | New feature or capability | Minor | |
| 374 | +| `fix` | Bug fix | Patch | |
| 375 | +| `test` | Adding or fixing tests | None | |
| 376 | +| `docs` | Documentation only | None | |
| 377 | +| `refactor` | Code restructure, no feature/fix | None | |
0 commit comments