Skip to content

Commit d29c508

Browse files
author
fortinbra
committed
feat(application): refactor batch realize logic into PastDueService and simplify RecurringController
1 parent a4b9a35 commit d29c508

File tree

4 files changed

+301
-73
lines changed

4 files changed

+301
-73
lines changed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Feature 074: Batch Realize — NotImplementedException in Application Service
2+
> **Status:** Planning
3+
> **Priority:** Medium (code hygiene / architecture)
4+
> **Estimated Effort:** Small (< 1 day)
5+
> **Dependencies:** None
6+
7+
## Overview
8+
9+
The `PastDueService.RealizeBatchAsync` method throws `NotImplementedException`, but the batch realize feature already works end-to-end because the `RecurringController` implements the logic directly. This violates clean architecture by placing business logic in the controller layer instead of the application service layer. The fix is to move the existing controller logic into `PastDueService.RealizeBatchAsync` and have the controller delegate to it.
10+
11+
## Problem Statement
12+
13+
### Current State
14+
15+
- **Controller** (`RecurringController.RealizeBatchAsync`): Contains full batch realize logic — iterates `BatchRealizeRequest.Items`, calls `ITransactionRealizationService` or `ITransferRealizationService` per item type, builds `BatchRealizeResultDto` with success/failure tracking.
16+
- **Application service** (`PastDueService.RealizeBatchAsync`): Throws `NotImplementedException` with comment `// This will be implemented when we wire up the batch endpoint`.
17+
- **Client** (`BudgetApiService.RealizeBatchAsync`): Correctly calls `POST api/v1/recurring/realize-batch`.
18+
19+
The feature works, but the orchestration logic lives in the wrong layer.
20+
21+
### Target State
22+
23+
- `PastDueService.RealizeBatchAsync` contains the batch realize orchestration logic.
24+
- `RecurringController.RealizeBatchAsync` delegates to `IPastDueService.RealizeBatchAsync`.
25+
- No `NotImplementedException` remains in the codebase.
26+
27+
---
28+
29+
## User Stories
30+
31+
### Refactor Batch Realize to Application Layer
32+
33+
#### US-074-001: Move Batch Realize Logic to Application Service
34+
**As a** developer
35+
**I want to** have batch realize business logic in the application service layer
36+
**So that** the architecture follows the clean architecture pattern and the controller remains thin.
37+
38+
**Acceptance Criteria:**
39+
- [ ] `PastDueService.RealizeBatchAsync` implements batch realize logic (iterate items, call realization services, collect failures)
40+
- [ ] `RecurringController.RealizeBatchAsync` delegates to `IPastDueService.RealizeBatchAsync`
41+
- [ ] `NotImplementedException` is removed
42+
- [ ] Existing API behavior is unchanged (same request/response shape, same error handling)
43+
- [ ] Unit tests cover `PastDueService.RealizeBatchAsync` (success, partial failure, unknown type)
44+
- [ ] Existing E2E/integration tests still pass
45+
46+
---
47+
48+
## Technical Design
49+
50+
### Architecture Changes
51+
52+
Move the batch realize orchestration from `RecurringController` into `PastDueService`. The controller should only validate the request and return the result.
53+
54+
### Files to Modify
55+
56+
| File | Change |
57+
|------|--------|
58+
| `src/BudgetExperiment.Application/Calendar/PastDueService.cs` | Implement `RealizeBatchAsync` with logic currently in controller |
59+
| `src/BudgetExperiment.Api/Controllers/RecurringController.cs` | Simplify to delegate to `IPastDueService.RealizeBatchAsync` |
60+
61+
### Current Controller Logic to Relocate
62+
63+
```csharp
64+
// Currently in RecurringController — should move to PastDueService
65+
foreach (var item in request.Items)
66+
{
67+
try
68+
{
69+
if (item.Type == "recurring-transaction")
70+
{
71+
await _transactionRealizationService.RealizeInstanceAsync(item.Id, ...);
72+
successCount++;
73+
}
74+
else if (item.Type == "recurring-transfer")
75+
{
76+
await _transferRealizationService.RealizeInstanceAsync(item.Id, ...);
77+
successCount++;
78+
}
79+
else
80+
{
81+
failures.Add(new BatchRealizeFailure { ... });
82+
}
83+
}
84+
catch (Exception ex)
85+
{
86+
failures.Add(new BatchRealizeFailure { ... });
87+
}
88+
}
89+
```
90+
91+
---
92+
93+
## Testing Strategy
94+
95+
### Unit Tests (TDD)
96+
97+
1. **RED**: Write test for `PastDueService.RealizeBatchAsync` — happy path with mixed transaction/transfer items
98+
2. **RED**: Write test for unknown item type → failure entry
99+
3. **RED**: Write test for partial failure (one item throws) → success count + failure list
100+
4. **GREEN**: Implement `RealizeBatchAsync` in `PastDueService`
101+
5. **REFACTOR**: Simplify controller, verify existing integration/E2E tests pass
102+
103+
---
104+
105+
## Definition of Done
106+
107+
- [ ] No `NotImplementedException` in `PastDueService`
108+
- [ ] Controller delegates to application service
109+
- [ ] Unit tests cover all batch realize paths
110+
- [ ] All existing tests pass
111+
- [ ] No new StyleCop warnings

src/BudgetExperiment.Api/Controllers/RecurringController.cs

Lines changed: 3 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,14 @@ namespace BudgetExperiment.Api.Controllers;
2020
public sealed class RecurringController : ControllerBase
2121
{
2222
private readonly IPastDueService _pastDueService;
23-
private readonly IRecurringTransactionRealizationService _transactionRealizationService;
24-
private readonly IRecurringTransferRealizationService _transferRealizationService;
2523

2624
/// <summary>
2725
/// Initializes a new instance of the <see cref="RecurringController"/> class.
2826
/// </summary>
2927
/// <param name="pastDueService">The past-due service.</param>
30-
/// <param name="transactionRealizationService">The recurring transaction realization service.</param>
31-
/// <param name="transferRealizationService">The recurring transfer realization service.</param>
32-
public RecurringController(
33-
IPastDueService pastDueService,
34-
IRecurringTransactionRealizationService transactionRealizationService,
35-
IRecurringTransferRealizationService transferRealizationService)
28+
public RecurringController(IPastDueService pastDueService)
3629
{
3730
this._pastDueService = pastDueService;
38-
this._transactionRealizationService = transactionRealizationService;
39-
this._transferRealizationService = transferRealizationService;
4031
}
4132

4233
/// <summary>
@@ -73,65 +64,7 @@ public async Task<IActionResult> RealizeBatchAsync(
7364
return this.BadRequest("At least one item is required.");
7465
}
7566

76-
var successCount = 0;
77-
var failures = new List<BatchRealizeFailure>();
78-
79-
foreach (var item in request.Items)
80-
{
81-
try
82-
{
83-
if (item.Type == "recurring-transaction")
84-
{
85-
var realizeRequest = new RealizeRecurringTransactionRequest
86-
{
87-
InstanceDate = item.InstanceDate,
88-
};
89-
await this._transactionRealizationService.RealizeInstanceAsync(
90-
item.Id,
91-
realizeRequest,
92-
cancellationToken);
93-
successCount++;
94-
}
95-
else if (item.Type == "recurring-transfer")
96-
{
97-
var realizeRequest = new RealizeRecurringTransferRequest
98-
{
99-
InstanceDate = item.InstanceDate,
100-
};
101-
await this._transferRealizationService.RealizeInstanceAsync(
102-
item.Id,
103-
realizeRequest,
104-
cancellationToken);
105-
successCount++;
106-
}
107-
else
108-
{
109-
failures.Add(new BatchRealizeFailure
110-
{
111-
Id = item.Id,
112-
Type = item.Type,
113-
InstanceDate = item.InstanceDate,
114-
Error = $"Unknown item type: {item.Type}",
115-
});
116-
}
117-
}
118-
catch (Exception ex)
119-
{
120-
failures.Add(new BatchRealizeFailure
121-
{
122-
Id = item.Id,
123-
Type = item.Type,
124-
InstanceDate = item.InstanceDate,
125-
Error = ex.Message,
126-
});
127-
}
128-
}
129-
130-
return this.Ok(new BatchRealizeResultDto
131-
{
132-
SuccessCount = successCount,
133-
FailureCount = failures.Count,
134-
Failures = failures,
135-
});
67+
var result = await this._pastDueService.RealizeBatchAsync(request, cancellationToken);
68+
return this.Ok(result);
13669
}
13770
}

src/BudgetExperiment.Application/Calendar/PastDueService.cs

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Copyright (c) BecauseImClever. All rights reserved.
33
// </copyright>
44

5+
using BudgetExperiment.Application.Recurring;
56
using BudgetExperiment.Contracts.Dtos;
67
using BudgetExperiment.Domain;
78

@@ -18,6 +19,8 @@ public sealed class PastDueService : IPastDueService
1819
private readonly IRecurringTransferRepository _recurringTransferRepo;
1920
private readonly ITransactionRepository _transactionRepo;
2021
private readonly IAccountRepository _accountRepo;
22+
private readonly IRecurringTransactionRealizationService _transactionRealizationService;
23+
private readonly IRecurringTransferRealizationService _transferRealizationService;
2124
private readonly Func<DateOnly> _todayProvider;
2225

2326
/// <summary>
@@ -27,18 +30,24 @@ public sealed class PastDueService : IPastDueService
2730
/// <param name="recurringTransferRepo">The recurring transfer repository.</param>
2831
/// <param name="transactionRepo">The transaction repository.</param>
2932
/// <param name="accountRepo">The account repository.</param>
33+
/// <param name="transactionRealizationService">The recurring transaction realization service.</param>
34+
/// <param name="transferRealizationService">The recurring transfer realization service.</param>
3035
/// <param name="todayProvider">Optional function to provide current date (for testing).</param>
3136
public PastDueService(
3237
IRecurringTransactionRepository recurringTransactionRepo,
3338
IRecurringTransferRepository recurringTransferRepo,
3439
ITransactionRepository transactionRepo,
3540
IAccountRepository accountRepo,
41+
IRecurringTransactionRealizationService transactionRealizationService,
42+
IRecurringTransferRealizationService transferRealizationService,
3643
Func<DateOnly>? todayProvider = null)
3744
{
3845
this._recurringTransactionRepo = recurringTransactionRepo;
3946
this._recurringTransferRepo = recurringTransferRepo;
4047
this._transactionRepo = transactionRepo;
4148
this._accountRepo = accountRepo;
49+
this._transactionRealizationService = transactionRealizationService;
50+
this._transferRealizationService = transferRealizationService;
4251
this._todayProvider = todayProvider ?? (() => DateOnly.FromDateTime(DateTime.UtcNow));
4352
}
4453

@@ -156,9 +165,67 @@ public async Task<PastDueSummaryDto> GetPastDueItemsAsync(Guid? accountId = null
156165
}
157166

158167
/// <inheritdoc/>
159-
public Task<BatchRealizeResultDto> RealizeBatchAsync(BatchRealizeRequest request, CancellationToken cancellationToken = default)
168+
public async Task<BatchRealizeResultDto> RealizeBatchAsync(BatchRealizeRequest request, CancellationToken cancellationToken = default)
160169
{
161-
// This will be implemented when we wire up the batch endpoint
162-
throw new NotImplementedException();
170+
var successCount = 0;
171+
var failures = new List<BatchRealizeFailure>();
172+
173+
foreach (var item in request.Items)
174+
{
175+
try
176+
{
177+
if (item.Type == "recurring-transaction")
178+
{
179+
var realizeRequest = new RealizeRecurringTransactionRequest
180+
{
181+
InstanceDate = item.InstanceDate,
182+
};
183+
await this._transactionRealizationService.RealizeInstanceAsync(
184+
item.Id,
185+
realizeRequest,
186+
cancellationToken);
187+
successCount++;
188+
}
189+
else if (item.Type == "recurring-transfer")
190+
{
191+
var realizeRequest = new RealizeRecurringTransferRequest
192+
{
193+
InstanceDate = item.InstanceDate,
194+
};
195+
await this._transferRealizationService.RealizeInstanceAsync(
196+
item.Id,
197+
realizeRequest,
198+
cancellationToken);
199+
successCount++;
200+
}
201+
else
202+
{
203+
failures.Add(new BatchRealizeFailure
204+
{
205+
Id = item.Id,
206+
Type = item.Type,
207+
InstanceDate = item.InstanceDate,
208+
Error = $"Unknown item type: {item.Type}",
209+
});
210+
}
211+
}
212+
catch (Exception ex)
213+
{
214+
failures.Add(new BatchRealizeFailure
215+
{
216+
Id = item.Id,
217+
Type = item.Type,
218+
InstanceDate = item.InstanceDate,
219+
Error = ex.Message,
220+
});
221+
}
222+
}
223+
224+
return new BatchRealizeResultDto
225+
{
226+
SuccessCount = successCount,
227+
FailureCount = failures.Count,
228+
Failures = failures,
229+
};
163230
}
164231
}

0 commit comments

Comments
 (0)