[CLAUDE] App+Infra: IChangelogService + log Workflow/Contract/Comment/Attachment
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m26s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m26s
User decision B (log cả 3): mọi thay đổi liên quan HĐ ghi vào unified ContractChangelogs để render tab Lịch sử FE. ## IChangelogService (Application/Common/Interfaces/) 5 methods: - LogContractChangeAsync — Header insert/update - LogDetailChangeAsync — line item insert/update/delete - LogWorkflowTransitionAsync — phase change (parallel với ContractApprovals) - LogCommentAddedAsync — góp ý mới - LogAttachmentAsync — upload/delete file KHÔNG SaveChanges trong service — caller chịu trách nhiệm save atomic cùng business changes (pattern giống INotificationService). ## ChangelogService impl - Resolve actor qua ICurrentUser → UserManager.FindByIdAsync - Denormalize UserName (FullName ?? Email) cho log readable - null UserId = system action (vd SLA auto-approve) - DI: AddScoped trong DependencyInjection.cs ## Wiring vào handlers hiện tại - ContractWorkflowService.TransitionAsync — LogWorkflowTransitionAsync sau khi insert ContractApproval - CreateContractCommandHandler — LogContractChangeAsync(Insert) - UpdateContractDraftCommandHandler — diff GiaTri/TenHopDong/NoiDung/ TemplateId trước update, log update với fieldChangesJson nếu có thay đổi - AddCommentCommandHandler — LogCommentAddedAsync - UploadContractAttachmentCommandHandler — LogAttachmentAsync(Insert) - DeleteContractAttachmentCommandHandler — LogAttachmentAsync(Delete) Build: dotnet build BE pass (0 error, 2 pre-existing warning trong DocxRenderer.cs) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -34,6 +34,7 @@ public static class DependencyInjection
|
||||
services.AddScoped<IContractWorkflowService, ContractWorkflowService>();
|
||||
services.AddScoped<IContractExcelExporter, ContractExcelExporter>();
|
||||
services.AddScoped<INotificationService, NotificationService>();
|
||||
services.AddScoped<IChangelogService, ChangelogService>();
|
||||
services.AddSingleton<IFileStorage, LocalFileStorage>();
|
||||
|
||||
// Phase 3 iteration 2 — SLA auto-approve background service
|
||||
|
||||
@ -0,0 +1,132 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
using SolutionErp.Domain.Contracts;
|
||||
using SolutionErp.Domain.Identity;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Services;
|
||||
|
||||
// Impl IChangelogService. Resolve current user qua ICurrentUser (null = system,
|
||||
// vd SLA auto-approve). Denormalize UserName để log readable mà không cần JOIN
|
||||
// Users table khi render.
|
||||
public class ChangelogService : IChangelogService
|
||||
{
|
||||
private readonly IApplicationDbContext _db;
|
||||
private readonly ICurrentUser _currentUser;
|
||||
private readonly UserManager<User> _userManager;
|
||||
|
||||
public ChangelogService(IApplicationDbContext db, ICurrentUser currentUser, UserManager<User> userManager)
|
||||
{
|
||||
_db = db;
|
||||
_currentUser = currentUser;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
private async Task<(Guid? Id, string? Name)> ResolveActorAsync()
|
||||
{
|
||||
if (_currentUser.UserId is null) return (null, null);
|
||||
var user = await _userManager.FindByIdAsync(_currentUser.UserId.Value.ToString());
|
||||
return (user?.Id, user?.FullName ?? user?.Email);
|
||||
}
|
||||
|
||||
public async Task LogContractChangeAsync(
|
||||
Guid contractId, ChangelogAction action, string? summary = null,
|
||||
string? fieldChangesJson = null, string? contextNote = null,
|
||||
ContractPhase? phaseAtChange = null, CancellationToken ct = default)
|
||||
{
|
||||
var (uid, uname) = await ResolveActorAsync();
|
||||
_db.ContractChangelogs.Add(new ContractChangelog
|
||||
{
|
||||
ContractId = contractId,
|
||||
EntityType = ChangelogEntityType.Contract,
|
||||
EntityId = null,
|
||||
Action = action,
|
||||
PhaseAtChange = phaseAtChange,
|
||||
UserId = uid,
|
||||
UserName = uname,
|
||||
Summary = summary,
|
||||
FieldChangesJson = fieldChangesJson,
|
||||
ContextNote = contextNote,
|
||||
});
|
||||
}
|
||||
|
||||
public async Task LogDetailChangeAsync(
|
||||
Guid contractId, Guid detailId, ChangelogAction action,
|
||||
string? summary = null, string? fieldChangesJson = null,
|
||||
ContractPhase? phaseAtChange = null, CancellationToken ct = default)
|
||||
{
|
||||
var (uid, uname) = await ResolveActorAsync();
|
||||
_db.ContractChangelogs.Add(new ContractChangelog
|
||||
{
|
||||
ContractId = contractId,
|
||||
EntityType = ChangelogEntityType.Detail,
|
||||
EntityId = detailId,
|
||||
Action = action,
|
||||
PhaseAtChange = phaseAtChange,
|
||||
UserId = uid,
|
||||
UserName = uname,
|
||||
Summary = summary,
|
||||
FieldChangesJson = fieldChangesJson,
|
||||
});
|
||||
}
|
||||
|
||||
public async Task LogWorkflowTransitionAsync(
|
||||
Guid contractId, ContractPhase fromPhase, ContractPhase toPhase,
|
||||
string? comment, CancellationToken ct = default)
|
||||
{
|
||||
var (uid, uname) = await ResolveActorAsync();
|
||||
_db.ContractChangelogs.Add(new ContractChangelog
|
||||
{
|
||||
ContractId = contractId,
|
||||
EntityType = ChangelogEntityType.Workflow,
|
||||
EntityId = null,
|
||||
Action = ChangelogAction.Transition,
|
||||
PhaseAtChange = toPhase,
|
||||
UserId = uid,
|
||||
UserName = uname ?? "Hệ thống",
|
||||
Summary = $"Chuyển phase {fromPhase} → {toPhase}",
|
||||
ContextNote = comment,
|
||||
});
|
||||
}
|
||||
|
||||
public async Task LogCommentAddedAsync(
|
||||
Guid contractId, string content, ContractPhase phase, CancellationToken ct = default)
|
||||
{
|
||||
var (uid, uname) = await ResolveActorAsync();
|
||||
_db.ContractChangelogs.Add(new ContractChangelog
|
||||
{
|
||||
ContractId = contractId,
|
||||
EntityType = ChangelogEntityType.Comment,
|
||||
EntityId = null,
|
||||
Action = ChangelogAction.Insert,
|
||||
PhaseAtChange = phase,
|
||||
UserId = uid,
|
||||
UserName = uname,
|
||||
Summary = "Thêm góp ý",
|
||||
ContextNote = content.Length > 200 ? content[..200] + "..." : content,
|
||||
});
|
||||
}
|
||||
|
||||
public async Task LogAttachmentAsync(
|
||||
Guid contractId, Guid attachmentId, ChangelogAction action,
|
||||
string fileName, ContractPhase phase, CancellationToken ct = default)
|
||||
{
|
||||
var (uid, uname) = await ResolveActorAsync();
|
||||
var verb = action switch
|
||||
{
|
||||
ChangelogAction.Insert => "Tải lên",
|
||||
ChangelogAction.Delete => "Xóa",
|
||||
_ => "Cập nhật",
|
||||
};
|
||||
_db.ContractChangelogs.Add(new ContractChangelog
|
||||
{
|
||||
ContractId = contractId,
|
||||
EntityType = ChangelogEntityType.Attachment,
|
||||
EntityId = attachmentId,
|
||||
Action = action,
|
||||
PhaseAtChange = phase,
|
||||
UserId = uid,
|
||||
UserName = uname,
|
||||
Summary = $"{verb} file: {fileName}",
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -16,7 +16,8 @@ public class ContractWorkflowService(
|
||||
IApplicationDbContext db,
|
||||
IContractCodeGenerator codeGenerator,
|
||||
IDateTime dateTime,
|
||||
INotificationService notifications) : IContractWorkflowService
|
||||
INotificationService notifications,
|
||||
IChangelogService changelog) : IContractWorkflowService
|
||||
{
|
||||
// Expose per-policy SLA via the contract — accepts optional contract so the
|
||||
// caller (CreateContractCommand) can ask for a specific type's SLA even
|
||||
@ -101,6 +102,9 @@ public class ContractWorkflowService(
|
||||
ApprovedAt = dateTime.UtcNow,
|
||||
});
|
||||
|
||||
// Log workflow transition vào unified Changelog (cho user xem trên tab Lịch sử)
|
||||
await changelog.LogWorkflowTransitionAsync(contract.Id, fromPhase, targetPhase, comment);
|
||||
|
||||
if (contract.DrafterUserId is Guid drafterId && drafterId != actorUserId)
|
||||
{
|
||||
var title = targetPhase switch
|
||||
|
||||
Reference in New Issue
Block a user