[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:
@ -0,0 +1,44 @@
|
|||||||
|
using SolutionErp.Domain.Contracts;
|
||||||
|
|
||||||
|
namespace SolutionErp.Application.Common.Interfaces;
|
||||||
|
|
||||||
|
// Service ghi ContractChangelog. KHÔNG SaveChanges — caller (CQRS handler)
|
||||||
|
// chịu trách nhiệm save atomic cùng với business changes. Pattern giống
|
||||||
|
// INotificationService.
|
||||||
|
public interface IChangelogService
|
||||||
|
{
|
||||||
|
Task LogContractChangeAsync(
|
||||||
|
Guid contractId,
|
||||||
|
ChangelogAction action,
|
||||||
|
string? summary = null,
|
||||||
|
string? fieldChangesJson = null,
|
||||||
|
string? contextNote = null,
|
||||||
|
ContractPhase? phaseAtChange = null,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
Task LogDetailChangeAsync(
|
||||||
|
Guid contractId,
|
||||||
|
Guid detailId,
|
||||||
|
ChangelogAction action,
|
||||||
|
string? summary = null,
|
||||||
|
string? fieldChangesJson = null,
|
||||||
|
ContractPhase? phaseAtChange = null,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
Task LogWorkflowTransitionAsync(
|
||||||
|
Guid contractId,
|
||||||
|
ContractPhase fromPhase,
|
||||||
|
ContractPhase toPhase,
|
||||||
|
string? comment,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
Task LogCommentAddedAsync(Guid contractId, string content, ContractPhase phase, CancellationToken ct = default);
|
||||||
|
|
||||||
|
Task LogAttachmentAsync(
|
||||||
|
Guid contractId,
|
||||||
|
Guid attachmentId,
|
||||||
|
ChangelogAction action,
|
||||||
|
string fileName,
|
||||||
|
ContractPhase phase,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
}
|
||||||
@ -55,7 +55,8 @@ public class UploadContractAttachmentCommandValidator : AbstractValidator<Upload
|
|||||||
|
|
||||||
public class UploadContractAttachmentCommandHandler(
|
public class UploadContractAttachmentCommandHandler(
|
||||||
IApplicationDbContext db,
|
IApplicationDbContext db,
|
||||||
IFileStorage storage) : IRequestHandler<UploadContractAttachmentCommand, ContractAttachmentDto>
|
IFileStorage storage,
|
||||||
|
IChangelogService changelog) : IRequestHandler<UploadContractAttachmentCommand, ContractAttachmentDto>
|
||||||
{
|
{
|
||||||
public async Task<ContractAttachmentDto> Handle(UploadContractAttachmentCommand request, CancellationToken ct)
|
public async Task<ContractAttachmentDto> Handle(UploadContractAttachmentCommand request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@ -64,7 +65,6 @@ public class UploadContractAttachmentCommandHandler(
|
|||||||
|
|
||||||
var attId = Guid.NewGuid();
|
var attId = Guid.NewGuid();
|
||||||
var safeName = SanitizeFileName(request.FileName);
|
var safeName = SanitizeFileName(request.FileName);
|
||||||
// Store under contracts/{contractId}/{attId}_{safeName} to keep originals separate
|
|
||||||
var relativePath = $"contracts/{contract.Id}/{attId}_{safeName}";
|
var relativePath = $"contracts/{contract.Id}/{attId}_{safeName}";
|
||||||
|
|
||||||
await storage.SaveAsync(relativePath, request.Content, ct);
|
await storage.SaveAsync(relativePath, request.Content, ct);
|
||||||
@ -73,7 +73,7 @@ public class UploadContractAttachmentCommandHandler(
|
|||||||
{
|
{
|
||||||
Id = attId,
|
Id = attId,
|
||||||
ContractId = contract.Id,
|
ContractId = contract.Id,
|
||||||
FileName = request.FileName, // keep original for display
|
FileName = request.FileName,
|
||||||
StoragePath = relativePath,
|
StoragePath = relativePath,
|
||||||
FileSize = request.FileSize,
|
FileSize = request.FileSize,
|
||||||
ContentType = request.ContentType,
|
ContentType = request.ContentType,
|
||||||
@ -81,6 +81,7 @@ public class UploadContractAttachmentCommandHandler(
|
|||||||
Note = request.Note,
|
Note = request.Note,
|
||||||
};
|
};
|
||||||
db.ContractAttachments.Add(entity);
|
db.ContractAttachments.Add(entity);
|
||||||
|
await changelog.LogAttachmentAsync(contract.Id, entity.Id, ChangelogAction.Insert, entity.FileName, contract.Phase, ct);
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
|
|
||||||
return new ContractAttachmentDto(
|
return new ContractAttachmentDto(
|
||||||
@ -128,15 +129,19 @@ public record DeleteContractAttachmentCommand(Guid ContractId, Guid AttachmentId
|
|||||||
|
|
||||||
public class DeleteContractAttachmentCommandHandler(
|
public class DeleteContractAttachmentCommandHandler(
|
||||||
IApplicationDbContext db,
|
IApplicationDbContext db,
|
||||||
IFileStorage storage) : IRequestHandler<DeleteContractAttachmentCommand>
|
IFileStorage storage,
|
||||||
|
IChangelogService changelog) : IRequestHandler<DeleteContractAttachmentCommand>
|
||||||
{
|
{
|
||||||
public async Task Handle(DeleteContractAttachmentCommand request, CancellationToken ct)
|
public async Task Handle(DeleteContractAttachmentCommand request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var att = await db.ContractAttachments
|
var att = await db.ContractAttachments
|
||||||
.FirstOrDefaultAsync(a => a.Id == request.AttachmentId && a.ContractId == request.ContractId, ct)
|
.FirstOrDefaultAsync(a => a.Id == request.AttachmentId && a.ContractId == request.ContractId, ct)
|
||||||
?? throw new NotFoundException("Attachment", request.AttachmentId);
|
?? throw new NotFoundException("Attachment", request.AttachmentId);
|
||||||
|
var contract = await db.Contracts.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(c => c.Id == request.ContractId, ct);
|
||||||
|
|
||||||
db.ContractAttachments.Remove(att);
|
db.ContractAttachments.Remove(att);
|
||||||
|
await changelog.LogAttachmentAsync(request.ContractId, att.Id, ChangelogAction.Delete, att.FileName, contract?.Phase ?? ContractPhase.DangSoanThao, ct);
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
|
|
||||||
// Best-effort file delete — if it fails, DB row is already gone (orphan
|
// Best-effort file delete — if it fails, DB row is already gone (orphan
|
||||||
|
|||||||
@ -42,7 +42,8 @@ public class CreateContractCommandValidator : AbstractValidator<CreateContractCo
|
|||||||
public class CreateContractCommandHandler(
|
public class CreateContractCommandHandler(
|
||||||
IApplicationDbContext db,
|
IApplicationDbContext db,
|
||||||
ICurrentUser currentUser,
|
ICurrentUser currentUser,
|
||||||
IContractWorkflowService workflow) : IRequestHandler<CreateContractCommand, Guid>
|
IContractWorkflowService workflow,
|
||||||
|
IChangelogService changelog) : IRequestHandler<CreateContractCommand, Guid>
|
||||||
{
|
{
|
||||||
public async Task<Guid> Handle(CreateContractCommand request, CancellationToken ct)
|
public async Task<Guid> Handle(CreateContractCommand request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@ -51,8 +52,6 @@ public class CreateContractCommandHandler(
|
|||||||
if (!await db.Projects.AnyAsync(p => p.Id == request.ProjectId, ct))
|
if (!await db.Projects.AnyAsync(p => p.Id == request.ProjectId, ct))
|
||||||
throw new NotFoundException("Project", request.ProjectId);
|
throw new NotFoundException("Project", request.ProjectId);
|
||||||
|
|
||||||
// Pin to currently active WorkflowDefinition for this type. New versions
|
|
||||||
// created later do not retroactively affect this contract.
|
|
||||||
var activeWfId = await db.WorkflowDefinitions.AsNoTracking()
|
var activeWfId = await db.WorkflowDefinitions.AsNoTracking()
|
||||||
.Where(w => w.ContractType == request.Type && w.IsActive)
|
.Where(w => w.ContractType == request.Type && w.IsActive)
|
||||||
.Select(w => (Guid?)w.Id)
|
.Select(w => (Guid?)w.Id)
|
||||||
@ -76,6 +75,14 @@ public class CreateContractCommandHandler(
|
|||||||
SlaDeadline = DateTime.UtcNow.Add(workflow.GetPhaseSla(ContractPhase.DangSoanThao) ?? TimeSpan.FromDays(7)),
|
SlaDeadline = DateTime.UtcNow.Add(workflow.GetPhaseSla(ContractPhase.DangSoanThao) ?? TimeSpan.FromDays(7)),
|
||||||
};
|
};
|
||||||
db.Contracts.Add(entity);
|
db.Contracts.Add(entity);
|
||||||
|
|
||||||
|
await changelog.LogContractChangeAsync(
|
||||||
|
entity.Id,
|
||||||
|
ChangelogAction.Insert,
|
||||||
|
summary: $"Tạo HĐ {entity.TenHopDong ?? "(chưa tên)"}",
|
||||||
|
phaseAtChange: entity.Phase,
|
||||||
|
ct: ct);
|
||||||
|
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
return entity.Id;
|
return entity.Id;
|
||||||
}
|
}
|
||||||
@ -91,7 +98,9 @@ public record UpdateContractDraftCommand(
|
|||||||
Guid? TemplateId,
|
Guid? TemplateId,
|
||||||
string? DraftData) : IRequest;
|
string? DraftData) : IRequest;
|
||||||
|
|
||||||
public class UpdateContractDraftCommandHandler(IApplicationDbContext db) : IRequestHandler<UpdateContractDraftCommand>
|
public class UpdateContractDraftCommandHandler(
|
||||||
|
IApplicationDbContext db,
|
||||||
|
IChangelogService changelog) : IRequestHandler<UpdateContractDraftCommand>
|
||||||
{
|
{
|
||||||
public async Task Handle(UpdateContractDraftCommand request, CancellationToken ct)
|
public async Task Handle(UpdateContractDraftCommand request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@ -101,11 +110,34 @@ public class UpdateContractDraftCommandHandler(IApplicationDbContext db) : IRequ
|
|||||||
if (entity.Phase != ContractPhase.DangSoanThao)
|
if (entity.Phase != ContractPhase.DangSoanThao)
|
||||||
throw new ConflictException("Chỉ được sửa HĐ khi ở phase Đang soạn thảo.");
|
throw new ConflictException("Chỉ được sửa HĐ khi ở phase Đang soạn thảo.");
|
||||||
|
|
||||||
|
// Capture diff trước update để log
|
||||||
|
var changes = new List<object>();
|
||||||
|
if (entity.GiaTri != request.GiaTri)
|
||||||
|
changes.Add(new { Field = "GiaTri", Old = entity.GiaTri, New = request.GiaTri });
|
||||||
|
if (entity.TenHopDong != request.TenHopDong)
|
||||||
|
changes.Add(new { Field = "TenHopDong", Old = entity.TenHopDong, New = request.TenHopDong });
|
||||||
|
if (entity.NoiDung != request.NoiDung)
|
||||||
|
changes.Add(new { Field = "NoiDung", Old = entity.NoiDung, New = request.NoiDung });
|
||||||
|
if (entity.TemplateId != request.TemplateId)
|
||||||
|
changes.Add(new { Field = "TemplateId", Old = entity.TemplateId, New = request.TemplateId });
|
||||||
|
|
||||||
entity.GiaTri = request.GiaTri;
|
entity.GiaTri = request.GiaTri;
|
||||||
entity.TenHopDong = request.TenHopDong;
|
entity.TenHopDong = request.TenHopDong;
|
||||||
entity.NoiDung = request.NoiDung;
|
entity.NoiDung = request.NoiDung;
|
||||||
entity.TemplateId = request.TemplateId;
|
entity.TemplateId = request.TemplateId;
|
||||||
entity.DraftData = request.DraftData;
|
entity.DraftData = request.DraftData;
|
||||||
|
|
||||||
|
if (changes.Count > 0)
|
||||||
|
{
|
||||||
|
await changelog.LogContractChangeAsync(
|
||||||
|
entity.Id,
|
||||||
|
ChangelogAction.Update,
|
||||||
|
summary: $"Cập nhật HĐ ({changes.Count} field)",
|
||||||
|
fieldChangesJson: System.Text.Json.JsonSerializer.Serialize(changes),
|
||||||
|
phaseAtChange: entity.Phase,
|
||||||
|
ct: ct);
|
||||||
|
}
|
||||||
|
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -166,7 +198,10 @@ public class AddCommentCommandValidator : AbstractValidator<AddCommentCommand>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AddCommentCommandHandler(IApplicationDbContext db, ICurrentUser currentUser) : IRequestHandler<AddCommentCommand, Guid>
|
public class AddCommentCommandHandler(
|
||||||
|
IApplicationDbContext db,
|
||||||
|
ICurrentUser currentUser,
|
||||||
|
IChangelogService changelog) : IRequestHandler<AddCommentCommand, Guid>
|
||||||
{
|
{
|
||||||
public async Task<Guid> Handle(AddCommentCommand request, CancellationToken ct)
|
public async Task<Guid> Handle(AddCommentCommand request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@ -184,6 +219,7 @@ public class AddCommentCommandHandler(IApplicationDbContext db, ICurrentUser cur
|
|||||||
Content = request.Content,
|
Content = request.Content,
|
||||||
};
|
};
|
||||||
db.ContractComments.Add(comment);
|
db.ContractComments.Add(comment);
|
||||||
|
await changelog.LogCommentAddedAsync(request.ContractId, request.Content, contract.Phase, ct);
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
return comment.Id;
|
return comment.Id;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<IContractWorkflowService, ContractWorkflowService>();
|
services.AddScoped<IContractWorkflowService, ContractWorkflowService>();
|
||||||
services.AddScoped<IContractExcelExporter, ContractExcelExporter>();
|
services.AddScoped<IContractExcelExporter, ContractExcelExporter>();
|
||||||
services.AddScoped<INotificationService, NotificationService>();
|
services.AddScoped<INotificationService, NotificationService>();
|
||||||
|
services.AddScoped<IChangelogService, ChangelogService>();
|
||||||
services.AddSingleton<IFileStorage, LocalFileStorage>();
|
services.AddSingleton<IFileStorage, LocalFileStorage>();
|
||||||
|
|
||||||
// Phase 3 iteration 2 — SLA auto-approve background service
|
// 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,
|
IApplicationDbContext db,
|
||||||
IContractCodeGenerator codeGenerator,
|
IContractCodeGenerator codeGenerator,
|
||||||
IDateTime dateTime,
|
IDateTime dateTime,
|
||||||
INotificationService notifications) : IContractWorkflowService
|
INotificationService notifications,
|
||||||
|
IChangelogService changelog) : IContractWorkflowService
|
||||||
{
|
{
|
||||||
// Expose per-policy SLA via the contract — accepts optional contract so the
|
// Expose per-policy SLA via the contract — accepts optional contract so the
|
||||||
// caller (CreateContractCommand) can ask for a specific type's SLA even
|
// caller (CreateContractCommand) can ask for a specific type's SLA even
|
||||||
@ -101,6 +102,9 @@ public class ContractWorkflowService(
|
|||||||
ApprovedAt = dateTime.UtcNow,
|
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)
|
if (contract.DrafterUserId is Guid drafterId && drafterId != actorUserId)
|
||||||
{
|
{
|
||||||
var title = targetPhase switch
|
var title = targetPhase switch
|
||||||
|
|||||||
Reference in New Issue
Block a user