Skip to content

Commit 7077c78

Browse files
author
aligneddev
committed
IMPL-PHASE5-7: Resume after reconnect; complete delete UI, E2E, and validation
- Add RideDeleteDialog component + styles + tests - Integrate delete action into HistoryPage and table row controls - Add delete API client in ridesService with tests - Add E2E delete history suite including cross-user forbidden scenario - Update backend delete service for idempotent re-delete and immediate read-model removal - Verify: frontend unit tests (66), backend tests (76), delete E2E (4), frontend build pass, lint pass - Mark completed tasks in specs/007-delete-rides/tasks.md
1 parent d7c42b8 commit 7077c78

14 files changed

Lines changed: 1294 additions & 91 deletions

File tree

specs/007-delete-rides/tasks.md

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ Implement a complete ride deletion feature with immutable event sourcing, triple
196196

197197
### User Story 1: Delete a Ride from History (P1)
198198

199-
- [ ] T070 [P] [US1] Write failing tests for RideDeleteDialog component in `src/BikeTracking.Frontend/tests/components/RideDeleteDialog.test.tsx`
199+
- [X] T070 [P] [US1] Write failing tests for RideDeleteDialog component in `src/BikeTracking.Frontend/tests/components/RideDeleteDialog.test.tsx`
200200
- Test 1: Dialog is hidden by default (not rendered or style display: none)
201201
- Test 2: Dialog shows when isOpen prop is true, displays ride date, distance, and notes
202202
- Test 3: Cancel button hides dialog without API call (onCancel callback triggered)
@@ -206,21 +206,21 @@ Implement a complete ride deletion feature with immutable event sourcing, triple
206206
- Test 7: Confirm button is disabled during API call (loading state)
207207
- Expected outcome: All tests FAIL (component not yet created)
208208

209-
- [ ] T071 [P] [US1] Write failing tests for deleteRide service in `src/BikeTracking.Frontend/tests/services/rideService.test.ts`
209+
- [X] T071 [P] [US1] Write failing tests for deleteRide service in `src/BikeTracking.Frontend/tests/services/rideService.test.ts`
210210
- Test 1: `deleteRide(rideId)` calls DELETE /api/rides/{rideId} with auth token
211211
- Test 2: Success response (200) returns parsed JSON with rideId and deletedAt
212212
- Test 3: Error response (403, 404, etc.) throws with error message
213213
- Test 4: Missing auth token results in error
214214
- Expected outcome: All tests FAIL (service not yet updated)
215215

216-
- [ ] T072 [P] [US1] Write failing integration tests for delete in history page in `src/BikeTracking.Frontend/tests/pages/HistoryPage.test.tsx`
216+
- [X] T072 [P] [US1] Write failing integration tests for delete in history page in `src/BikeTracking.Frontend/tests/pages/HistoryPage.test.tsx`
217217
- Test 1: Delete button visible on each ride row
218218
- Test 2: Clicking delete button shows dialog
219219
- Test 3: After confirming delete, ride disappears from table
220220
- Test 4: After delete, history page queries API and refreshes ride list
221221
- Expected outcome: All tests FAIL
222222

223-
- [ ] T073 [US1] Implement RideDeleteDialog component in `src/BikeTracking.Frontend/src/components/RideDeleteDialog/RideDeleteDialog.tsx`
223+
- [X] T073 [US1] Implement RideDeleteDialog component in `src/BikeTracking.Frontend/src/components/RideDeleteDialog/RideDeleteDialog.tsx`
224224
- Props interface:
225225
```typescript
226226
interface RideDeleteDialogProps {
@@ -245,7 +245,7 @@ Implement a complete ride deletion feature with immutable event sourcing, triple
245245
- Import React 19 hooks (useState). NO inline styles; use CSS file with Stylelint compliance.
246246
- Run: `npm run test:unit -- RideDeleteDialog` to pass T070
247247

248-
- [ ] T074 [US1] Implement deleteRide service in `src/BikeTracking.Frontend/src/services/rideService.ts`
248+
- [X] T074 [US1] Implement deleteRide service in `src/BikeTracking.Frontend/src/services/rideService.ts`
249249
- Add function:
250250
```typescript
251251
export async function deleteRide(rideId: string): Promise<{ rideId: string; deletedAt: string }> {
@@ -269,7 +269,7 @@ Implement a complete ride deletion feature with immutable event sourcing, triple
269269

270270
### User Story 2: Prevent Accidental Ride Deletion (P2)
271271

272-
- [ ] T080 [US2] Integrate RideDeleteDialog into HistoryPage in `src/BikeTracking.Frontend/src/pages/HistoryPage.tsx`
272+
- [X] T080 [US2] Integrate RideDeleteDialog into HistoryPage in `src/BikeTracking.Frontend/src/pages/HistoryPage.tsx`
273273
- Add state: `const [deleteDialogState, setDeleteDialogState] = useState<{ ride: Ride | null; isOpen: boolean }>({ ride: null, isOpen: false });`
274274
- Add delete button to each ride row in table (icon or text "Delete")
275275
- On delete click: populate dialoge state with ride and set isOpen = true
@@ -280,7 +280,7 @@ Implement a complete ride deletion feature with immutable event sourcing, triple
280280
- Add keyboard: Escape key closes dialog without delete
281281
- Run: `npm run test:unit -- HistoryPage` to pass T072
282282

283-
- [ ] T081 [US2] Add "Delete" button styling to ride rows in `src/BikeTracking.Frontend/src/pages/HistoryPage.css`
283+
- [X] T081 [US2] Add "Delete" button styling to ride rows in `src/BikeTracking.Frontend/src/pages/HistoryPage.css`
284284
- Delete button: danger/red color on hover, cursor pointer, confirm icon (🗑️ or text)
285285
- Dialog styling: modal with backdrop, center-aligned, shadow, 400px max-width
286286
- Use Stylelint to validate (no inline styles)
@@ -289,13 +289,13 @@ Implement a complete ride deletion feature with immutable event sourcing, triple
289289

290290
### User Story 3: Maintain Accurate Totals After Deletion (P3)
291291

292-
- [ ] T090 [P] [US3] Write failing tests for totals update after delete in `src/BikeTracking.Frontend/tests/pages/HistoryPage.test.tsx` (extend T072)
292+
- [X] T090 [P] [US3] Write failing tests for totals update after delete in `src/BikeTracking.Frontend/tests/pages/HistoryPage.test.tsx` (extend T072)
293293
- Test 1: Before delete, monthly total is 100mi
294294
- Test 2: Delete 25mi ride, monthly total updates to 75mi
295295
- Test 3: Delete all rides, monthly total is 0mi
296296
- Expected outcome: All tests FAIL (totals not refreshed after delete)
297297

298-
- [ ] T091 [US3] Refresh totals after successful delete in HistoryPage in `src/BikeTracking.Frontend/src/pages/HistoryPage.tsx`
298+
- [X] T091 [US3] Refresh totals after successful delete in HistoryPage in `src/BikeTracking.Frontend/src/pages/HistoryPage.tsx`
299299
- On delete success, re-query totals API: `const totals = await fetchRideTotals(startDate, endDate);`
300300
- Update totals state: `setTotals(totals);`
301301
- This ensures month/year/all-time/filtered totals display latest values
@@ -305,7 +305,7 @@ Implement a complete ride deletion feature with immutable event sourcing, triple
305305

306306
## Phase 6: Integration Testing
307307

308-
- [ ] T100 [P] Write E2E test for delete ride flow in `src/BikeTracking.Frontend/tests/e2e/DeleteRide.spec.ts`
308+
- [X] T100 [P] Write E2E test for delete ride flow in `src/BikeTracking.Frontend/tests/e2e/DeleteRide.spec.ts`
309309
- Scenario: User signuprecord 3 ridesdelete middle rideverify removed + totals updated
310310
- Steps:
311311
1. Sign up with name "TestUser" + PIN "1234"
@@ -322,17 +322,17 @@ Implement a complete ride deletion feature with immutable event sourcing, triple
322322
12. Refresh page, verify ride 2 still absent
323323
- Expected outcome: All assertions pass
324324

325-
- [ ] T101 [P] Write E2E test for delete with cancellation in `src/BikeTracking.Frontend/tests/e2e/DeleteRide.spec.ts` (same file)
325+
- [X] T101 [P] Write E2E test for delete with cancellation in `src/BikeTracking.Frontend/tests/e2e/DeleteRide.spec.ts` (same file)
326326
- Scenario: User opens delete dialog and cancels
327327
- Steps: Similar to T100 up to step 9, but click "Cancel"
328328
- Verify dialog closes, ride remains in table, total unchanged
329329

330-
- [ ] T102 [P] Write E2E test for idempotent delete in `src/BikeTracking.Frontend/tests/e2e/DeleteRide.spec.ts`
330+
- [X] T102 [P] Write E2E test for idempotent delete in `src/BikeTracking.Frontend/tests/e2e/DeleteRide.spec.ts`
331331
- Scenario: Manual re-submit of DELETE request after successful first delete
332332
- Steps: Sign uprecord ridedelete successfullymanually fetch DELETE API with same rideId
333333
- Verify: Second DELETE returns 200 OK with isIdempotent: true, ride still absent from table
334334

335-
- [ ] T103 [P] Write E2E test for cross-user delete prevention in `src/BikeTracking.Frontend/tests/e2e/DeleteRide.spec.ts`
335+
- [X] T103 [P] Write E2E test for cross-user delete prevention in `src/BikeTracking.Frontend/tests/e2e/DeleteRide.spec.ts`
336336
- Scenario: User A signs up, User B attempts to delete User A's ride
337337
- Steps:
338338
1. Sign up as User A, record ride
@@ -342,7 +342,7 @@ Implement a complete ride deletion feature with immutable event sourcing, triple
342342
5. Verify in User A's session that ride still exists
343343
- Note: May require test setup to handle multi-user state
344344

345-
- [ ] T104 Run full E2E test suite
345+
- [X] T104 Run full E2E test suite
346346
- Command: `cd src/BikeTracking.Frontend && npm run test:e2e`
347347
- Prerequisite: Aspire + API running (`dotnet run --project src/BikeTracking.AppHost` in another terminal)
348348
- All tests in T100, T101, T102, T103 PASS
@@ -351,26 +351,26 @@ Implement a complete ride deletion feature with immutable event sourcing, triple
351351

352352
## Phase 7: Polish & Verification
353353

354-
- [ ] T110 [P] Format code with CSharpier
354+
- [X] T110 [P] Format code with CSharpier
355355
- Command: `csharpier format .`
356356
- Ensures C# code follows project conventions
357357
- Files affected: DeleteRide.cs, DeleteRideHandler.cs, Event handlers, Tests
358358

359-
- [ ] T111 [P] Run eslint and stylelint on frontend
359+
- [X] T111 [P] Run eslint and stylelint on frontend
360360
- Command: `cd src/BikeTracking.Frontend && npm run lint`
361361
- Fixes any TypeScript/CSS violations
362362
- Files affected: RideDeleteDialog.tsx, HistoryPage.tsx, CSS files, Service, Tests
363363

364-
- [ ] T112 [P] Run full backend test suite
364+
- [X] T112 [P] Run full backend test suite
365365
- Command: `dotnet test BikeTracking.slnx -k "Delete|delete"`
366366
- Ensures all delete-related tests pass (T010, T012, T030, T040-T042, T060)
367367
- Coverage: Domain (F#), API (C#), Tests
368368

369-
- [ ] T113 [P] Run full frontend test suite
369+
- [X] T113 [P] Run full frontend test suite
370370
- Command: `cd src/BikeTracking.Frontend && npm run test:unit`
371371
- Ensures component, service, and page tests pass (T070-T072, T090)
372372

373-
- [ ] T114 Build frontend for production
373+
- [X] T114 Build frontend for production
374374
- Command: `cd src/BikeTracking.Frontend && npm run build`
375375
- Verify no TypeScript errors, all imports resolve
376376
- Check bundle size (should be minimal addition for dialog component)

src/BikeTracking.Api/Application/Rides/DeleteRideService.cs

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -46,24 +46,8 @@ public static DeleteRideResult Failure(string code, string message)
4646

4747
public async Task<DeleteRideResult> ExecuteAsync(long riderId, long rideId)
4848
{
49-
var ride = await dbContext.Rides.Where(r => r.Id == rideId).SingleOrDefaultAsync();
50-
51-
if (ride is null)
52-
{
53-
return DeleteRideResult.Failure("RIDE_NOT_FOUND", $"Ride {rideId} was not found.");
54-
}
55-
56-
if (ride.RiderId != riderId)
57-
{
58-
return DeleteRideResult.Failure(
59-
"NOT_RIDE_OWNER",
60-
$"Ride {rideId} does not belong to the authenticated rider."
61-
);
62-
}
63-
64-
var utcNow = DateTime.UtcNow;
65-
66-
// Check if ride was already deleted (idempotency)
49+
// Check if ride was already deleted (idempotency) before querying live rides.
50+
// This allows repeat requests to succeed even after the row is removed.
6751
var existingDeleteEvent = await dbContext
6852
.OutboxEvents.Where(e =>
6953
e.AggregateType == "Ride"
@@ -86,6 +70,23 @@ public async Task<DeleteRideResult> ExecuteAsync(long riderId, long rideId)
8670
return DeleteRideResult.SuccessIdempotent(idempotentResponse);
8771
}
8872

73+
var ride = await dbContext.Rides.Where(r => r.Id == rideId).SingleOrDefaultAsync();
74+
75+
if (ride is null)
76+
{
77+
return DeleteRideResult.Failure("RIDE_NOT_FOUND", $"Ride {rideId} was not found.");
78+
}
79+
80+
if (ride.RiderId != riderId)
81+
{
82+
return DeleteRideResult.Failure(
83+
"NOT_RIDE_OWNER",
84+
$"Ride {rideId} does not belong to the authenticated rider."
85+
);
86+
}
87+
88+
var utcNow = DateTime.UtcNow;
89+
8990
var eventPayload = RideDeletedEventPayload.Create(
9091
riderId: riderId,
9192
rideId: ride.Id,
@@ -107,6 +108,9 @@ public async Task<DeleteRideResult> ExecuteAsync(long riderId, long rideId)
107108
}
108109
);
109110

111+
// Remove from current read model so history and totals update immediately.
112+
dbContext.Rides.Remove(ride);
113+
110114
try
111115
{
112116
await dbContext.SaveChangesAsync();

src/BikeTracking.Api/Endpoints/RidesEndpoints.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,8 @@ CancellationToken cancellationToken
253253
return Results.Ok(result.Response);
254254
}
255255

256-
var error = result.Error ?? new DeleteRideService.DeleteRideError("ERROR", "Unknown error.");
256+
var error =
257+
result.Error ?? new DeleteRideService.DeleteRideError("ERROR", "Unknown error.");
257258

258259
return error.Code switch
259260
{

0 commit comments

Comments
 (0)