[CLAUDE] Docs: database-guide + 6 flow diagrams
docs/database/database-guide.md: - Conventions (naming, data types, audit fields, soft delete) - Schema hien tai (Identity tables sau migration Init) + seed 12 role + admin - Schema planned: Phase 1 dot 2 (Supplier/Project/Department + Permission Matrix) - Schema planned: Phase 3 (Contract + Approval + Comment + Attachment + Template + Clause + CodeSequence) - Mermaid ERD cho tung phase - Migration workflow (create/apply/revert) - Index strategy + unique indexes - Backup/restore SQL - Common pitfalls + SQL cheatsheet docs/flows/ — 6 flow documentation: - README.md: index - auth-flow.md: login/refresh/me/logout (IMPLEMENTED, sequence + edge cases + security checklist) - permission-flow.md: Phase 1 dot 2 - Role x MenuKey x CRUD resolution + FE guard + BE policy - contract-creation-flow.md: Phase 2 - Drafter flow chon template -> fill -> preview -> save draft - contract-approval-flow.md: Phase 3 - state machine 9 phase chi tiet + reject flow + timeline UI - form-render-flow.md: Phase 2 - OpenXml + ClosedXML + LibreOffice PDF convert - sla-expiry-flow.md: Phase 3 - BackgroundService auto-approve qua SLA + warning notify Update references: - CLAUDE.md (root): them 2 row Tai lieu quan trong - docs/CLAUDE.md: update project layout voi flows/ + database/ - docs/STATUS.md: log docs addition - docs/changelog/migration-todos.md: tick Phase 0 docs items Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
240
docs/flows/sla-expiry-flow.md
Normal file
240
docs/flows/sla-expiry-flow.md
Normal file
@ -0,0 +1,240 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user