groceries/SOFT_DELETE_IMPLEMENTATION.md
lasse 0b42a74fe9 Minor version bump (1.x.0) is appropriate because:
 New functionality added (soft delete system)
 Backward compatible (existing features unchanged)
 Significant enhancement (complete temporal tracking system)
 API additions (new endpoints, parameters)
 UI enhancements (new components, visual indicators)
2025-05-30 09:49:26 +02:00

271 lines
9.3 KiB
Markdown

# Soft Delete Implementation with Historical Tracking
## Overview
This implementation extends the existing temporal tables system to support soft deletes with proper historical tracking. When a product is "deleted", it's not actually removed from the database but marked as deleted with a timestamp, while maintaining full historical data.
## Key Features
### 1. Soft Delete Mechanism
- Products are marked as `deleted = true` instead of being physically removed
- Deletion creates a historical record of the product's state before deletion
- Deleted products get a new `valid_from` date set to the deletion date
- All historical versions remain intact for audit purposes
### 2. UI Enhancements
- **Product List**: "Show deleted" toggle next to "Add New Product" button
- **Visual Indicators**: Deleted products shown with:
- Red background tint and reduced opacity
- Strikethrough text
- 🗑️ emoji indicator
- Disabled edit/duplicate/delete actions
- **Shopping Events**: Products deleted before/on shopping date are automatically filtered out
### 3. API Behavior
- **Default**: Deleted products are hidden from all product listings
- **Optional**: `show_deleted=true` parameter shows all products including deleted ones
- **Shopping Events**: New endpoint `/products/available-for-shopping/{date}` filters products based on deletion status at specific date
## Database Schema Changes
### Products Table
```sql
ALTER TABLE products
ADD COLUMN deleted BOOLEAN DEFAULT FALSE NOT NULL;
```
### Products History Table
```sql
-- Already included in products_history table
deleted BOOLEAN DEFAULT FALSE NOT NULL
```
### Updated Trigger
The `products_versioning_trigger` now **consistently handles ALL historization**:
- **UPDATE operations**: Creates history records for both automatic and manual versioning
- **DELETE operations**: Creates history records when products are deleted
- **Smart versioning**: Automatically detects manual vs automatic versioning based on `valid_from` changes
- **Centralized logic**: All temporal logic is in the database trigger, not split between trigger and application
### Trigger Benefits
1. **Consistency**: All versioning operations follow the same pattern
2. **Reliability**: Database-level enforcement prevents inconsistencies
3. **Simplicity**: Application code just sets fields, trigger handles the rest
4. **Performance**: Single database operation handles both data update and history creation
## API Endpoints
### Modified Endpoints
#### `GET /products/`
- **New Parameter**: `show_deleted: bool = False`
- **Behavior**: Filters out deleted products by default
- **Usage**: `GET /products/?show_deleted=true` to include deleted products
#### `PUT /products/{id}` & `DELETE /products/{id}`
- **Enhanced**: Now properly handles soft delete with historical tracking
- **Validation**: Prevents operations on already-deleted products
### New Endpoints
#### `GET /products/available-for-shopping/{shopping_date}`
- **Purpose**: Get products that were available (not deleted) on a specific shopping date
- **Logic**: Returns products where:
- `deleted = false` (never deleted), OR
- `deleted = true` AND `valid_from > shopping_date` (deleted after shopping date)
- **Usage**: Used by shopping event modals to filter product lists
## Frontend Implementation
### ProductList Component
```typescript
// New state for toggle
const [showDeleted, setShowDeleted] = useState(false);
// Updated API call
const response = await productApi.getAll(showDeleted);
// Visual styling for deleted products
className={`hover:bg-gray-50 ${product.deleted ? 'bg-red-50 opacity-75' : ''}`}
```
### AddShoppingEventModal Component
```typescript
// Dynamic product fetching based on shopping date
const response = formData.date
? await productApi.getAvailableForShopping(formData.date)
: await productApi.getAll(false);
// Refetch products when date changes
useEffect(() => {
if (isOpen && formData.date) {
fetchProducts();
}
}, [formData.date, isOpen]);
```
## Deletion Process Flow
### 1. User Initiates Delete
- User clicks "Delete" button on a product
- Confirmation modal appears
### 2. Backend Processing
```python
# Simple application code - trigger handles the complexity
product.deleted = True
product.valid_from = date.today() # Manual versioning date
product.updated_at = func.now()
# Trigger automatically:
# 1. Detects the change (deleted field + valid_from change)
# 2. Creates history record with old data (deleted=False, valid_to=today)
# 3. Ensures new record has valid_to='9999-12-31'
```
**Trigger Logic (Automatic)**:
- Detects manual versioning because `valid_from` changed
- Uses the new `valid_from` as the cutoff date for history
- Creates history record: `{...old_data, valid_to: new_valid_from, operation: 'U'}`
- No additional application logic needed
### 3. Frontend Updates
- Product list refreshes
- Deleted product appears with visual indicators (if "Show deleted" is enabled)
- Product becomes unavailable for new shopping events
## Historical Data Integrity
### Shopping Events
- **Guarantee**: Shopping events always show products exactly as they existed when purchased
- **Implementation**: Uses `/products/{id}/at/{date}` endpoint to fetch historical product state
- **Benefit**: Even if a product is deleted later, historical shopping events remain accurate
### Audit Trail
- **Complete History**: All product versions are preserved in `products_history`
- **Deletion Tracking**: History records show when and why products were deleted
- **Temporal Queries**: Can reconstruct product state at any point in time
## Usage Examples
### 1. View All Products (Default)
```bash
GET /products/
# Returns only non-deleted products
```
### 2. View All Products Including Deleted
```bash
GET /products/?show_deleted=true
# Returns all products with deleted status
```
### 3. Get Products Available for Shopping on Specific Date
```bash
GET /products/available-for-shopping/2024-01-15
# Returns products that were not deleted on 2024-01-15
```
### 4. View Historical Shopping Event
```bash
GET /shopping-events/123/products-as-purchased
# Returns products exactly as they were when purchased, regardless of current deletion status
```
## Benefits
1. **Data Preservation**: No data is ever lost
2. **Audit Compliance**: Complete audit trail of all changes
3. **Historical Accuracy**: Shopping events remain accurate over time
4. **User Experience**: Clean interface with optional deleted product visibility
5. **Flexibility**: Easy to "undelete" products if needed (future enhancement)
## Future Enhancements
1. **Undelete Functionality**: Add ability to restore deleted products
2. **Bulk Operations**: Delete/restore multiple products at once
3. **Deletion Reasons**: Add optional reason field for deletions
4. **Advanced Filtering**: Filter by deletion date, reason, etc.
5. **Reporting**: Generate reports on deleted products and their impact
## Migration Instructions
1. **Run Migration**: Execute `temporal_migration.sql` to add `deleted` column
2. **Deploy Backend**: Update backend with new API endpoints and logic
3. **Deploy Frontend**: Update frontend with new UI components and API calls
4. **Test**: Verify soft delete functionality and historical data integrity
## Testing Scenarios
1. **Basic Deletion**: Delete a product and verify it's hidden from default view
2. **Show Deleted Toggle**: Enable "Show deleted" and verify deleted products appear with proper styling
3. **Shopping Event Filtering**: Create shopping event and verify deleted products don't appear in product list
4. **Historical Accuracy**: Delete a product that was in a past shopping event, verify the shopping event still shows correct historical data
5. **Date-based Filtering**: Test `/products/available-for-shopping/{date}` with various dates before/after product deletions
## Database Initialization
### ⚠️ Important: Trigger Creation
The temporal triggers are essential for the soft delete functionality. They must be created in **all environments**:
### **Method 1: Automatic (Recommended)**
The triggers are now automatically created when using SQLAlchemy's `create_all()`:
```python
# This now creates both tables AND triggers
models.Base.metadata.create_all(bind=engine)
```
**How it works**:
- SQLAlchemy event listener detects when `ProductHistory` table is created
- Automatically executes trigger creation SQL
- Works for fresh dev, test, and production databases
### **Method 2: Manual Database Script**
For explicit control, use the initialization script:
```bash
# Run this for fresh database setup
python backend/database_init.py
```
**Features**:
- Creates all tables
- Creates all triggers
- Checks for existing triggers (safe to run multiple times)
- Provides detailed feedback
### **Method 3: Migration File**
For existing databases, run the migration:
```sql
-- Execute temporal_migration.sql
\i temporal_migration.sql
```
## Environment Setup Guide
### **Development (Fresh DB)**
```bash
# Option A: Automatic (when starting app)
python backend/main.py
# ✅ Tables + triggers created automatically
# Option B: Explicit setup
python backend/database_init.py
python backend/main.py
```
### **Production (Fresh DB)**
```bash
# Recommended: Explicit initialization
python backend/database_init.py
# Then start the application
```
### **Existing Database (Migration)**
```bash
# Apply migration to add soft delete functionality
docker-compose exec db psql -U postgres -d groceries -f /tmp/temporal_migration.sql
```