[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
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:
@ -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);
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SolutionErp.Application.Contracts.Services;
|
||||
using SolutionErp.Domain.Contracts;
|
||||
using SolutionErp.Domain.Forms;
|
||||
using SolutionErp.Domain.Identity;
|
||||
@ -34,9 +35,59 @@ public static class DbInitializer
|
||||
await SeedDemoMasterDataAsync(db, logger);
|
||||
await SeedContractTemplatesAsync(db, logger);
|
||||
await SeedWorkflowDefinitionsAsync(db, logger);
|
||||
|
||||
// Backfill mã HĐ cho HĐ legacy chưa có (sau khi đổi policy gen-tại-create).
|
||||
// Idempotent: chỉ HĐ MaHopDong IS NULL được gen.
|
||||
var codeGen = sp.GetRequiredService<IContractCodeGenerator>();
|
||||
await BackfillContractCodesAsync(db, codeGen, logger);
|
||||
|
||||
await WarnDefaultAdminPasswordAsync(userManager, logger);
|
||||
}
|
||||
|
||||
private static async Task BackfillContractCodesAsync(
|
||||
ApplicationDbContext db, IContractCodeGenerator codeGen, ILogger logger)
|
||||
{
|
||||
var orphanCount = await db.Contracts.CountAsync(c => c.MaHopDong == null);
|
||||
if (orphanCount == 0)
|
||||
{
|
||||
logger.LogInformation("Backfill mã HĐ: không có HĐ nào thiếu mã.");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation("Backfill mã HĐ: {Count} HĐ thiếu mã, đang gen...", orphanCount);
|
||||
var orphans = await db.Contracts.Where(c => c.MaHopDong == null).ToListAsync();
|
||||
int success = 0, failed = 0;
|
||||
|
||||
foreach (var contract in orphans)
|
||||
{
|
||||
var supplier = await db.Suppliers.FirstOrDefaultAsync(s => s.Id == contract.SupplierId);
|
||||
var project = await db.Projects.FirstOrDefaultAsync(p => p.Id == contract.ProjectId);
|
||||
if (supplier is null || project is null)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Backfill HĐ {Id}: skip vì supplier/project missing.", contract.Id);
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
contract.MaHopDong = await codeGen.GenerateAsync(contract, project.Code, supplier.Code);
|
||||
await db.SaveChangesAsync();
|
||||
logger.LogInformation("Backfill HĐ {Id} → {Code}", contract.Id, contract.MaHopDong);
|
||||
success++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Backfill HĐ {Id} thất bại.", contract.Id);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogInformation(
|
||||
"Backfill mã HĐ xong: {Success} thành công, {Failed} thất bại.", success, failed);
|
||||
}
|
||||
|
||||
// Seed v01 per ContractType from hardcoded WorkflowPolicies. Idempotent:
|
||||
// skip if any active definition already exists for that type (admin may
|
||||
// have already created custom versions — don't clobber).
|
||||
|
||||
@ -75,7 +75,10 @@ public class ContractWorkflowService(
|
||||
|
||||
var fromPhase = contract.Phase;
|
||||
|
||||
// Gen mã HĐ khi chuyển sang DangDongDau (BOD ký xong)
|
||||
// Defensive — gen mã HĐ nếu chưa có khi chuyển sang DangDongDau.
|
||||
// Nominal flow (sau user feedback): mã đã gen sẵn từ CreateContract → skip.
|
||||
// Fallback chỉ trigger cho HĐ legacy chưa qua backfill, hoặc HĐ tạo bằng
|
||||
// path khác (vd seed/import) chưa set MaHopDong.
|
||||
if (targetPhase == ContractPhase.DangDongDau && string.IsNullOrEmpty(contract.MaHopDong))
|
||||
{
|
||||
var supplier = await db.Suppliers.FirstOrDefaultAsync(s => s.Id == contract.SupplierId, ct)
|
||||
|
||||
Reference in New Issue
Block a user