[CLAUDE] App+Infra: Mã HĐ gen ngay tại CreateContract + backfill HĐ legacy
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m45s

User feedback: HĐ phải có mã ngay khi tạo (không đợi đến DangDongDau như
cũ). HĐ đã tạo trước đây nhưng chưa có mã → backfill tự động.

## Thay đổi

### CreateContractCommandHandler (App)

- Inject IContractCodeGenerator
- Load supplier + project FULL (cần Code, không chỉ check tồn tại như trước)
- Call codeGenerator.GenerateAsync TRƯỚC khi db.Contracts.Add — entity
  chưa tracked nên GenerateAsync internal SaveChangesAsync chỉ save SEQ
  (không kèm contract chưa tracked)
- Set entity.MaHopDong = result trước khi Add → INSERT contract đã có mã
- Changelog summary include mã: "Tạo HĐ {mã} — {tên}"

### Trade-off documented

- Mã gen sớm → HĐ TuChoi sẽ "wasted" 1 mã (gap trong sequence)
- Acceptable vì user cần mã reference vào tài liệu/giấy tờ ngay từ đầu

### ContractWorkflowService.TransitionAsync (Infra)

- Giữ logic cũ `if MaHopDong is null → gen` ở DangDongDau
- Update comment: nominal flow skip vì mã đã có; defensive cho HĐ legacy
  hoặc HĐ tạo bằng path khác (seed/import)

### DbInitializer.BackfillContractCodesAsync (Infra)

- Chạy 1 lần trước WarnDefaultAdminPasswordAsync
- Idempotent: count Contracts WHERE MaHopDong IS NULL → skip nếu 0
- Loop từng HĐ: load supplier+project → GenerateAsync → SaveChangesAsync
- Skip + log warning nếu missing supplier/project (legacy data corruption)
- Try-catch per HĐ, log success/failed count cuối cùng

## Build

dotnet build BE pass (0 error, 2 pre-existing DocxRenderer warning)

## Note

Khi deploy lên prod, DbInitializer chạy startup → backfill HĐ cũ tự động.
Log line "Backfill mã HĐ: X HĐ thiếu mã, đang gen..." sẽ xuất hiện ở
Logs/log-{date}.txt để verify.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-04-23 11:36:59 +07:00
parent 7f26ff9d66
commit 51449d6b9d
3 changed files with 70 additions and 6 deletions

View File

@ -43,14 +43,16 @@ public class CreateContractCommandHandler(
IApplicationDbContext db,
ICurrentUser currentUser,
IContractWorkflowService workflow,
IContractCodeGenerator codeGenerator,
IChangelogService changelog) : IRequestHandler<CreateContractCommand, Guid>
{
public async Task<Guid> Handle(CreateContractCommand request, CancellationToken ct)
{
if (!await db.Suppliers.AnyAsync(s => s.Id == request.SupplierId, ct))
throw new NotFoundException("Supplier", request.SupplierId);
if (!await db.Projects.AnyAsync(p => p.Id == request.ProjectId, ct))
throw new NotFoundException("Project", request.ProjectId);
// Load supplier + project full (cần Code cho gen mã RG-001)
var supplier = await db.Suppliers.FirstOrDefaultAsync(s => s.Id == request.SupplierId, ct)
?? throw new NotFoundException("Supplier", request.SupplierId);
var project = await db.Projects.FirstOrDefaultAsync(p => p.Id == request.ProjectId, ct)
?? throw new NotFoundException("Project", request.ProjectId);
var activeWfId = await db.WorkflowDefinitions.AsNoTracking()
.Where(w => w.ContractType == request.Type && w.IsActive)
@ -74,12 +76,20 @@ public class CreateContractCommandHandler(
WorkflowDefinitionId = activeWfId,
SlaDeadline = DateTime.UtcNow.Add(workflow.GetPhaseSla(ContractPhase.DangSoanThao) ?? TimeSpan.FromDays(7)),
};
// Gen mã HĐ NGAY tại create (user request) — không đợi DangDongDau như cũ.
// Trade-off: nếu HĐ bị TuChoi → mã bị "wasted" (gap trong sequence). Acceptable
// vì user muốn có mã ngay từ đầu để reference vào tài liệu/giấy tờ.
// GenerateAsync chạy SERIALIZABLE transaction riêng — entity chưa tracked nên
// không bị save kèm.
entity.MaHopDong = await codeGenerator.GenerateAsync(entity, project.Code, supplier.Code, ct);
db.Contracts.Add(entity);
await changelog.LogContractChangeAsync(
entity.Id,
ChangelogAction.Insert,
summary: $"Tạo HĐ {entity.TenHopDong ?? "(chưa tên)"}",
summary: $"Tạo HĐ {entity.MaHopDong} — {entity.TenHopDong ?? "(chưa tên)"}",
phaseAtChange: entity.Phase,
ct: ct);