# Plan: Soft Delete and Concurrency Tokens ## Goal Add soft delete (`IsDeleted` / `DeletedAt` + global EF query filter) and a `xmin`-based concurrency token (`RowVersion`) to the entities that benefit from them, matching the stwaddle stack convention. ## Current state - Hard deletes on all entities — `db.X.Remove(entity); await db.SaveChangesAsync()`. - No concurrency tokens — concurrent updates overwrite each other silently. - No `IsDeleted` / `DeletedAt` columns. ## Scope decisions **Soft delete:** Apply to `Budget`, `Income`, `Outgo`, and `BudgetShare`. `KnownUser` is a provisioning-only table; hard deletes are fine there. **Concurrency token:** Apply to `Budget` only for now — it is the entity most likely to be edited by multiple users simultaneously (owner + shared editor). `Income` and `Outgo` updates are user-local; `BudgetShare` and `KnownUser` are low-contention. Extend later if needed. ## Steps ### Phase 1 — Update entity models > If plan #10 (project split) has been implemented, these files live in `Budget.Core/Models/`. > Otherwise they are in `Budget.Api/Models/`. 1. Add an `ISoftDeletable` interface (or base class) with `bool IsDeleted`, `DateTimeOffset? DeletedAt`. 2. Implement on `Budget`, `Income`, `Outgo`, `BudgetShare`. 3. Add `byte[] RowVersion` (mapped to `xmin`) to `Budget` only. ### Phase 2 — Update AppDbContext / EF configuration > If plan #10 has been implemented, `AppDbContext` is in `Budget.Infrastructure/Data/`. 4. In `AppDbContext.OnModelCreating`, for each soft-deletable entity add: ```csharp builder.HasQueryFilter(x => !x.IsDeleted); ``` 5. For `Budget`, add the concurrency token config: ```csharp builder.Property(e => e.RowVersion) .HasColumnName("xmin") .HasColumnType("xid") .ValueGeneratedOnAddOrUpdate() .IsConcurrencyToken(); ``` ### Phase 3 — Update delete endpoints to soft delete 6. In each controller, replace `db.X.Remove(entity)` with: ```csharp entity.IsDeleted = true; entity.DeletedAt = DateTimeOffset.UtcNow; ``` Affected actions: `BudgetsController.Delete`, `IncomesController.Delete`, `OutgosController.Delete`, `SharesController.Revoke`. ### Phase 4 — Handle concurrency conflicts 7. In `BudgetsController.Update` (and any other Budget write endpoint), wrap `SaveChangesAsync` in a try/catch for `DbUpdateConcurrencyException` and return `Conflict(new { error = "The budget was modified by another user. Please refresh and retry." })`. ### Phase 5 — Migration 8. `dotnet ef migrations add AddSoftDeleteAndConcurrency --project Budget.Infrastructure --startup-project Budget.Api` (adjust project flags if plan #10 has not been implemented yet). 9. Review the generated migration — verify `xmin` column is not added as a normal column (it should appear only in `modelSnapshot`, not as an `AddColumn` since `xmin` is a Postgres system column). 10. Apply: `dotnet ef database update` or let startup migrations handle it. ### Phase 6 — Build and verify 11. `dotnet build` — zero errors. 12. Manually verify that a soft-deleted budget no longer appears in `GET /api/budgets` without any controller changes (query filter handles it). ## Key decisions - No admin endpoint to view/restore deleted records is in scope here — add `IgnoreQueryFilters()` to a future admin endpoint if needed. - `RowVersion` is not exposed in DTOs or returned to the client in this plan. If optimistic concurrency is needed client-side (e.g., ETag), that is a separate concern. - Cascade behavior: when a `Budget` is soft-deleted its `Incomes`, `Outgos`, and `BudgetShares` are not automatically soft-deleted. They are filtered out indirectly because `BudgetsController` checks budget access before returning child resources. Explicit cascading can be added later. ## Files affected - `Budget.Core/Models/Budget.cs` (or `Budget.Api/Models/Budget.cs`) - `Budget.Core/Models/Income.cs` - `Budget.Core/Models/Outgo.cs` - `Budget.Core/Models/BudgetShare.cs` - New: `Budget.Core/Models/ISoftDeletable.cs` (optional interface) - `Budget.Infrastructure/Data/AppDbContext.cs` (or `Budget.Api/Data/AppDbContext.cs`) - `Budget.Api/Controllers/BudgetsController.cs` - `Budget.Api/Controllers/IncomesController.cs` - `Budget.Api/Controllers/OutgosController.cs` - `Budget.Api/Controllers/SharesController.cs` - New migration file