# SLA Expiry Auto-Approve Flow

> **Status:** 📝 Planned (Phase 3)
> **Business rule nguồn:** [`../workflow-contract.md §4`](../workflow-contract.md) — *"Mỗi bộ phận chỉ có 01 ngày để xử lý. Nếu kéo dài hơn 01 ngày mà vẫn chưa xử lý xong, thì xem như đã thông qua."*

## 1. Mục đích

Tránh bottleneck khi 1 role giữ HĐ quá SLA. Hệ thống auto-approve để HĐ tiếp tục chạy. Mọi auto-approve được log rõ ràng (`Decision=AutoApprove`) để audit.

## 2. Data model liên quan

```csharp
public class Contract : AuditableEntity
{
    public ContractPhase Phase { get; set; }
    public DateTime? SlaDeadline { get; set; }  // khi nào phase hiện tại hết hạn
    // ...
}
```

`SlaDeadline = UtcNow + PhaseSla` được set mỗi khi transition.

### SLA mỗi phase

| Phase | SLA | Từ workflow spec |
|---|---|---|
| DangSoanThao | 7 ngày | Drafter có 7d soạn thảo |
| DangGopY | 7 ngày | Các phòng góp ý |
| DangDamPhan | 7 ngày | Đàm phán |
| DangInKy | 1 ngày | In + ký nháy |
| DangKiemTraCCM | 3 ngày | CCM review |
| DangTrinhKy | 1 ngày | BOD ký |
| DangDongDau | (không SLA) | Phụ thuộc HRA |
| DaPhatHanh | (final) | — |

## 3. Hosted service `SlaExpiryJob`

```mermaid
flowchart TD
    Start([BackgroundService<br/>ExecuteAsync]) --> Loop{Loop}
    Loop --> Wait[Wait 15 minutes<br/>Task.Delay with CancellationToken]
    Wait --> Query[Query Contracts<br/>WHERE SlaDeadline IS NOT NULL<br/>AND SlaDeadline < UtcNow<br/>AND Phase NOT IN finished_phases]
    Query -->|count > 0| Process[Process each contract]
    Query -->|count = 0| Loop

    Process --> Decide{Có role<br/>next để<br/>auto approve?}
    Decide -->|yes| AutoApprove[Transition → nextPhase<br/>với system actor<br/>Decision=AutoApprove]
    Decide -->|no<br/>(HĐ đang ở final phase)| Skip[Skip]

    AutoApprove --> Log[Log audit + send notify]
    Log --> Loop
    Skip --> Loop
```

## 4. Implementation skeleton

```csharp
// Infrastructure/HostedServices/SlaExpiryJob.cs
public class SlaExpiryJob : BackgroundService
{
    private readonly IServiceProvider _sp;
    private readonly ILogger<SlaExpiryJob> _logger;
    private static readonly TimeSpan Interval = TimeSpan.FromMinutes(15);

    public SlaExpiryJob(IServiceProvider sp, ILogger<SlaExpiryJob> logger)
    {
        _sp = sp;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await using var scope = _sp.CreateAsyncScope();
                var db = scope.ServiceProvider.GetRequiredService<IApplicationDbContext>();
                var workflow = scope.ServiceProvider.GetRequiredService<IContractWorkflowService>();
                var now = DateTime.UtcNow;

                var expired = await db.Contracts
                    .Where(c => c.SlaDeadline != null && c.SlaDeadline < now)
                    .Where(c => c.Phase != ContractPhase.DaPhatHanh && c.Phase != ContractPhase.TuChoi)
                    .ToListAsync(stoppingToken);

                _logger.LogInformation("SlaExpiryJob: {Count} contracts expired", expired.Count);

                foreach (var contract in expired)
                {
                    try
                    {
                        await workflow.AutoApproveExpiredAsync(contract, stoppingToken);
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, "SlaExpiryJob: failed to auto-approve {ContractId}", contract.Id);
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "SlaExpiryJob: iteration failed");
            }

            await Task.Delay(Interval, stoppingToken);
        }
    }
}
```

## 5. Auto-approve logic

```csharp
public async Task AutoApproveExpiredAsync(Contract contract, CancellationToken ct)
{
    var nextPhase = DetermineNextPhase(contract);
    if (nextPhase is null) return; // no next phase → stay (rare)

    // Record approval với system actor
    var approval = new ContractApproval
    {
        ContractId = contract.Id,
        Phase = contract.Phase,
        ApproverUserId = null,  // system (hoặc Guid Empty)
        Decision = ApprovalDecision.AutoApprove,
        Comment = $"Tự động duyệt do quá SLA {contract.Phase} (deadline {contract.SlaDeadline:yyyy-MM-dd HH:mm})",
        ApprovedAt = DateTime.UtcNow,
    };
    _db.ContractApprovals.Add(approval);

    // Update phase + reset deadline cho phase mới
    contract.Phase = nextPhase.Value;
    contract.SlaDeadline = DateTime.UtcNow.Add(GetSlaForPhase(nextPhase.Value));

    // Gen mã HĐ nếu transition tới DangDongDau
    if (nextPhase == ContractPhase.DangDongDau && string.IsNullOrEmpty(contract.MaHopDong))
    {
        contract.MaHopDong = await _codeGen.GenerateAsync(contract, ct);
    }

    await _db.SaveChangesAsync(ct);

    // Notify drafter + role next
    await _notifications.NotifyAutoApproveAsync(contract, approval);
}

private ContractPhase? DetermineNextPhase(Contract contract) => contract.Phase switch
{
    ContractPhase.DangSoanThao => ContractPhase.DangGopY,
    ContractPhase.DangGopY => ContractPhase.DangDamPhan,
    ContractPhase.DangDamPhan => ContractPhase.DangInKy,
    ContractPhase.DangInKy => ContractPhase.DangKiemTraCCM,
    ContractPhase.DangKiemTraCCM => ContractPhase.DangTrinhKy,
    ContractPhase.DangTrinhKy => ContractPhase.DangDongDau,
    ContractPhase.DangDongDau => ContractPhase.DaPhatHanh,
    _ => null,
};
```

## 6. Warning notification (còn 20% SLA)

Thêm 1 job warning tách biệt (hoặc merge vào cùng `SlaExpiryJob`):

```csharp
// Query contracts với SlaDeadline sắp hết (80% đã trôi qua)
var warning = await db.Contracts
    .Where(c => c.SlaDeadline != null)
    .Where(c => c.SlaDeadline > now && c.SlaDeadline - now < FractionRemaining(c.Phase, 0.2))
    .Where(c => !c.SlaWarningSent)  // boolean flag để không gửi 2 lần
    .ToListAsync();

foreach (var c in warning)
{
    await _notifications.NotifySlaWarningAsync(c);
    c.SlaWarningSent = true;
}
await db.SaveChangesAsync();
```

Reset `SlaWarningSent = false` khi chuyển phase.

## 7. Registration (Program.cs)

```csharp
builder.Services.AddHostedService<SlaExpiryJob>();
```

Hosted service chạy cùng vòng đời app. Trên IIS với multiple worker processes → mỗi worker chạy 1 instance → có thể duplicate auto-approve 1 HĐ. **Fix:** single worker process prod (IIS app pool `maxProcesses = 1`) hoặc distributed lock qua Redis (Phase 4).

## 8. Monitoring

| Metric | Alert threshold |
|---|---|
| Contracts auto-approved / day | > 20% tổng transition → review quy trình |
| Job iteration duration | > 5s → DB query slow, cần index `SlaDeadline` |
| Job errors in last hour | > 3 → page oncall |
| HĐ có SLA quá hạn > 24h (job không process) | > 0 → system down / deadlock |

Log structured với Serilog:
```
[SlaExpiryJob] Auto-approved contract {ContractId} from {OldPhase} → {NewPhase} (expired {Hours}h ago)
```

## 9. Edge cases

| Case | Xử lý |
|---|---|
| Job đang chạy → app crash giữa chừng | Không save changes → next iteration xử lý lại |
| Contract đã được user duyệt ngay lúc job chạy | Optimistic concurrency (RowVersion) — job fail → skip, next iteration sẽ thấy phase đã đổi |
| Contract đã transition tới final phase (`DaPhatHanh`) nhưng `SlaDeadline` không được clear | Query filter loại luôn → không xử lý |
| Timezone server khác UTC | Luôn compare bằng UTC (`DateTime.UtcNow`, `SlaDeadline` lưu UTC) |
| Admin pause auto-approve (ví dụ kỳ lễ không ai làm việc) | Thêm config `Sla:Enabled = false` → job skip loop |
| SLA config thay đổi (ví dụ từ 7 → 10 ngày) | Chỉ áp dụng cho transition mới, HĐ đang dở vẫn dùng deadline cũ |

## 10. Testing

### Unit test
```csharp
[Fact]
public async Task AutoApprove_transitions_to_next_phase()
{
    var contract = new Contract { Phase = ContractPhase.DangGopY, SlaDeadline = DateTime.UtcNow.AddHours(-1) };
    await _service.AutoApproveExpiredAsync(contract, default);
    Assert.Equal(ContractPhase.DangDamPhan, contract.Phase);
}
```

### Integration test
```csharp
// Seed HĐ với SlaDeadline = 5 phút trước
// Start SlaExpiryJob với interval 1s
// Wait 2s
// Query DB → phase phải đã advance + có ContractApproval với Decision=AutoApprove
```

## 11. Liên quan

- [`contract-approval-flow.md`](contract-approval-flow.md) — manual transition (ngược lại với auto)
- [`../workflow-contract.md`](../workflow-contract.md) — SLA rule gốc
