---
task_id: sprint_2_story_2_task_1
story: sprint_2_story_2
status: not_started
estimated_hours: 6
created_date: 2025-11-05
assignee: Backend Team
---

# Task 1: Implement Changed Fields Detection (JSON Diff)

**Story**: Story 2 - Audit Log Core Features (Phase 2)
**Estimated**: 6 hours

## Description

Enhance the audit logging to detect and store only the changed fields (JSON diff) instead of full entity snapshots. This optimizes storage and makes audit logs more readable.

## Acceptance Criteria

- [ ] JSON diff algorithm implemented
- [ ] Only changed fields stored in OldValues/NewValues
- [ ] Nested object changes detected
- [ ] Unit tests for diff algorithm
- [ ] Storage size reduced by 50-70%

## Implementation Details

**Files to Update**:

1. **Diff Service**: `colaflow-api/src/ColaFlow.Infrastructure/Services/JsonDiffService.cs`
```csharp
public class JsonDiffService : IJsonDiffService
{
    public ChangedFields GetChangedFields(EntityEntry entry)
    {
        var changedFields = new Dictionary<string, FieldChange>();

        foreach (var property in entry.Properties.Where(p => p.IsModified && !p.Metadata.IsPrimaryKey()))
        {
            changedFields[property.Metadata.Name] = new FieldChange
            {
                OldValue = property.OriginalValue,
                NewValue = property.CurrentValue
            };
        }

        return new ChangedFields { Fields = changedFields };
    }

    public string SerializeChangedFields(ChangedFields changes)
    {
        return JsonSerializer.Serialize(changes, new JsonSerializerOptions
        {
            WriteIndented = false,
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
        });
    }
}

public class ChangedFields
{
    public Dictionary<string, FieldChange> Fields { get; set; } = new();
}

public class FieldChange
{
    public object? OldValue { get; set; }
    public object? NewValue { get; set; }
}
```

2. **Update Interceptor**: `colaflow-api/src/ColaFlow.Infrastructure/Interceptors/AuditLogInterceptor.cs`
```csharp
public class AuditLogInterceptor : SaveChangesInterceptor
{
    private readonly IJsonDiffService _diffService;

    public AuditLogInterceptor(
        ITenantContext tenantContext,
        IHttpContextAccessor httpContextAccessor,
        IJsonDiffService diffService)
    {
        _tenantContext = tenantContext;
        _httpContextAccessor = httpContextAccessor;
        _diffService = diffService;
    }

    private List<AuditLog> CreateAuditEntries(DbContext context)
    {
        // ... existing code ...

        foreach (var entry in entries)
        {
            string? oldValues = null;
            string? newValues = null;

            if (entry.State == EntityState.Modified)
            {
                // Use diff service to get only changed fields
                var changes = _diffService.GetChangedFields(entry);
                var diffJson = _diffService.SerializeChangedFields(changes);

                // Store diff in both OldValues and NewValues
                // OldValues: { "Title": { "OldValue": "Old", "NewValue": "New" } }
                oldValues = diffJson;
                newValues = diffJson;
            }
            else if (entry.State == EntityState.Added)
            {
                // For Create, store all current values
                newValues = SerializeAllFields(entry);
            }
            else if (entry.State == EntityState.Deleted)
            {
                // For Delete, store all original values
                oldValues = SerializeAllFields(entry);
            }

            var auditLog = new AuditLog
            {
                // ... existing fields ...
                OldValues = oldValues,
                NewValues = newValues
            };

            auditLogs.Add(auditLog);
        }

        return auditLogs;
    }

    private string SerializeAllFields(EntityEntry entry)
    {
        var allFields = entry.Properties
            .Where(p => !p.Metadata.IsPrimaryKey())
            .ToDictionary(p => p.Metadata.Name, p => p.CurrentValue);
        return JsonSerializer.Serialize(allFields);
    }
}
```

**Example Output**:
```json
// Before (Full Snapshot - 500 bytes):
{
  "OldValues": {"Id":"abc","Title":"Old Title","Description":"Long description...","Status":"InProgress","Priority":1},
  "NewValues": {"Id":"abc","Title":"New Title","Description":"Long description...","Status":"InProgress","Priority":1}
}

// After (Diff Only - 80 bytes, 84% reduction):
{
  "OldValues": {"Title":{"OldValue":"Old Title","NewValue":"New Title"}},
  "NewValues": {"Title":{"OldValue":"Old Title","NewValue":"New Title"}}
}
```

## Technical Notes

- Use System.Text.Json for performance
- Store diff in both OldValues and NewValues for query flexibility
- Consider nested object changes (e.g., Address.City)
- Ignore computed properties and navigation properties

## Testing

**Unit Tests**:
```csharp
[Fact]
public void GetChangedFields_ShouldReturnOnlyModifiedFields()
{
    // Arrange
    var entry = CreateMockEntry(
        original: new { Title = "Old", Status = "InProgress" },
        current: new { Title = "New", Status = "InProgress" }
    );

    // Act
    var changes = _diffService.GetChangedFields(entry);

    // Assert
    Assert.Single(changes.Fields);
    Assert.True(changes.Fields.ContainsKey("Title"));
    Assert.Equal("Old", changes.Fields["Title"].OldValue);
    Assert.Equal("New", changes.Fields["Title"].NewValue);
}
```

---

**Created**: 2025-11-05 by Backend Agent
