Skip to content

Commit 0a527f3

Browse files
author
fortinbra
committed
2 parents b34961c + 8ff3b65 commit 0a527f3

File tree

1 file changed

+377
-0
lines changed

1 file changed

+377
-0
lines changed
Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
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

Comments
 (0)