[CLAUDE] Supplier: Excel-import Phase B (upload NCC preview/confirm) + Mig 63
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m25s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m25s
Import Excel 'Database NCC': upload→preview classify (New/Update/Skip/Error)→confirm all-or-nothing upsert-by-Code (OrdinalIgnoreCase dedup, fill-nulls-safe). Mig 63 +SourceUpdatedAt/By (2 nullable). Parser layout-locked header-fingerprint NFC-normalized + absolute-cell-index + #REF!-null + NAS-backslash-raw. Type-lạ→NhaCungCap. FE dialog fe-admin+fe-user byte-identical. 10 test SupplierExcelImportServiceTests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -0,0 +1,28 @@
|
||||
using MediatR;
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
using SolutionErp.Application.Master.Suppliers.Dtos;
|
||||
using SolutionErp.Application.Master.Suppliers.Import;
|
||||
|
||||
namespace SolutionErp.Application.Master.Suppliers.Commands.ImportSuppliers;
|
||||
|
||||
/// <summary>
|
||||
/// Commit các hàng đã preview (ALL-OR-NOTHING). Body JSON = { "rows": [ ... ] }.
|
||||
/// Actor resolve từ ICurrentUser (chỉ để log — audit CreatedBy/UpdatedBy do interceptor set).
|
||||
/// </summary>
|
||||
public sealed record SupplierImportConfirmCommand(IReadOnlyList<SupplierImportRowDto> Rows)
|
||||
: IRequest<SupplierImportResultDto>;
|
||||
|
||||
public sealed class SupplierImportConfirmCommandHandler(
|
||||
ISupplierExcelImportService importService,
|
||||
ICurrentUser currentUser)
|
||||
: IRequestHandler<SupplierImportConfirmCommand, SupplierImportResultDto>
|
||||
{
|
||||
public Task<SupplierImportResultDto> Handle(SupplierImportConfirmCommand request, CancellationToken ct)
|
||||
{
|
||||
var actor = currentUser.FullName
|
||||
?? currentUser.Email
|
||||
?? currentUser.UserId?.ToString()
|
||||
?? "unknown";
|
||||
return importService.ConfirmAsync(request.Rows ?? Array.Empty<SupplierImportRowDto>(), actor, ct);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
using MediatR;
|
||||
using SolutionErp.Application.Master.Suppliers.Dtos;
|
||||
using SolutionErp.Application.Master.Suppliers.Import;
|
||||
|
||||
namespace SolutionErp.Application.Master.Suppliers.Commands.ImportSuppliers;
|
||||
|
||||
/// <summary>
|
||||
/// Preview import NCC từ file Excel (KHÔNG ghi DB). Stream do controller mở từ IFormFile
|
||||
/// (mirror UploadPurchaseEvaluationAttachmentCommand — command mang Stream).
|
||||
/// </summary>
|
||||
public sealed record SupplierImportPreviewCommand(Stream Xlsx) : IRequest<SupplierImportPreviewDto>;
|
||||
|
||||
public sealed class SupplierImportPreviewCommandHandler(ISupplierExcelImportService importService)
|
||||
: IRequestHandler<SupplierImportPreviewCommand, SupplierImportPreviewDto>
|
||||
{
|
||||
public Task<SupplierImportPreviewDto> Handle(SupplierImportPreviewCommand request, CancellationToken ct)
|
||||
=> importService.PreviewAsync(request.Xlsx, ct);
|
||||
}
|
||||
@ -31,5 +31,8 @@ public record SupplierDto(
|
||||
string? ReferralSource,
|
||||
string? OwnerPmh,
|
||||
SupplierStatus? Status,
|
||||
// Import provenance (Supplier Phase B — cột 29-30 file Excel nguồn), KHÁC audit CreatedAt/UpdatedAt.
|
||||
DateTime? SourceUpdatedAt,
|
||||
string? SourceUpdatedBy,
|
||||
DateTime CreatedAt,
|
||||
DateTime? UpdatedAt);
|
||||
|
||||
@ -0,0 +1,87 @@
|
||||
using SolutionErp.Domain.Master;
|
||||
|
||||
namespace SolutionErp.Application.Master.Suppliers.Dtos;
|
||||
|
||||
// ============================================================================
|
||||
// Supplier Phase B — Upload Excel "Database NCC" (Approach A, layout-locked).
|
||||
// DTOs shared giữa preview + confirm. Records có property SETTABLE vì hàng import
|
||||
// được parse → classify → round-trip FE → confirm (mutable trong toàn pipeline).
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>Phân loại mỗi hàng sau khi preview classify.</summary>
|
||||
public enum RowImportStatus
|
||||
{
|
||||
New = 0, // Code chưa có trong DB (case-insensitive) → sẽ INSERT
|
||||
Update = 1, // Code đã có (case-insensitive) → sẽ FILL-NULLS (không đè non-null)
|
||||
Skip = 2, // hàng rỗng hoàn toàn → bỏ qua
|
||||
Error = 3, // thiếu Code hoặc Name → không import được
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 1 hàng Excel đã parse + map sang field Supplier (27) + provenance (2) + chẩn đoán.
|
||||
/// <c>Status</c> = phân loại hàng (RowImportStatus). <c>SupplierStatus</c> = tình trạng NCC (cột 27).
|
||||
/// </summary>
|
||||
public sealed record SupplierImportRowDto
|
||||
{
|
||||
public int RowIndex { get; set; } // số dòng thật trên Excel (1-based)
|
||||
|
||||
// ---- 27 field map sang Supplier (thứ tự theo cột Excel) ----
|
||||
public string? PackageCategory { get; set; } // col 2
|
||||
public SupplierType Type { get; set; } = SupplierType.NhaCungCap; // col 3 (unknown → NhaCungCap, decision 5)
|
||||
public string? Code { get; set; } // col 4 "TÊN VIẾT TẮT" (upsert key, Trim)
|
||||
public string? Name { get; set; } // col 5
|
||||
public string? Address { get; set; } // col 6
|
||||
public string? OfficeAddress { get; set; } // col 7
|
||||
public string? Phone { get; set; } // col 8
|
||||
public string? Fax { get; set; } // col 9
|
||||
public string? BankAccount { get; set; } // col 10 (raw composite)
|
||||
public string? SecondaryBankAccount { get; set; } // col 11
|
||||
public string? TaxCode { get; set; } // col 12 (raw, nullable)
|
||||
public string? LegalRepresentative { get; set; } // col 13
|
||||
public string? LegalRepTitle { get; set; } // col 14
|
||||
public string? AuthorizationNote { get; set; } // col 15
|
||||
public string? LinkGuq { get; set; } // col 16 (raw NAS backslash)
|
||||
public string? LinkGpkd { get; set; } // col 17
|
||||
public string? LinkHsnl { get; set; } // col 18
|
||||
public string? ContactPerson { get; set; } // col 19
|
||||
public string? ContactTitle { get; set; } // col 20
|
||||
public string? ContactPhone { get; set; } // col 21
|
||||
public string? Email { get; set; } // col 22
|
||||
public string? MailingAddress { get; set; } // col 23
|
||||
public string? MailRecipient { get; set; } // col 24 (raw composite)
|
||||
public string? ReferralSource { get; set; } // col 25
|
||||
public string? OwnerPmh { get; set; } // col 26
|
||||
public SupplierStatus? SupplierStatus { get; set; } // col 27 (unknown → null)
|
||||
public string? Note { get; set; } // col 28
|
||||
|
||||
// ---- 2 import-provenance ----
|
||||
public DateTime? SourceUpdatedAt { get; set; } // col 29 (null nếu không parse được ngày)
|
||||
public string? SourceUpdatedBy { get; set; } // col 30
|
||||
|
||||
// ---- classify + diagnostics ----
|
||||
public RowImportStatus Status { get; set; }
|
||||
public List<string> Messages { get; set; } = new(); // cảnh báo truncate / default type / lý do lỗi
|
||||
public Guid? ExistingSupplierId { get; set; } // set khi Status=Update (hỗ trợ FE hiển thị "sẽ cập nhật")
|
||||
}
|
||||
|
||||
/// <summary>Kết quả preview (KHÔNG ghi DB). LayoutValid=false khi header-fingerprint không khớp.</summary>
|
||||
public sealed record SupplierImportPreviewDto
|
||||
{
|
||||
public List<SupplierImportRowDto> Rows { get; set; } = new();
|
||||
public int NewCount { get; set; }
|
||||
public int UpdateCount { get; set; }
|
||||
public int SkipCount { get; set; }
|
||||
public int ErrorCount { get; set; }
|
||||
public List<string> Warnings { get; set; } = new();
|
||||
public bool LayoutValid { get; set; } = true; // false → file bị từ chối (sai layout), Rows rỗng
|
||||
}
|
||||
|
||||
/// <summary>Kết quả confirm. Committed=false + Errors khi có hard-error (all-or-nothing, không ghi gì).</summary>
|
||||
public sealed record SupplierImportResultDto
|
||||
{
|
||||
public int Inserted { get; set; }
|
||||
public int Updated { get; set; }
|
||||
public int Skipped { get; set; }
|
||||
public List<string> Errors { get; set; } = new();
|
||||
public bool Committed { get; set; } // true nếu đã SaveChanges; false nếu abort do hard-error
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
using SolutionErp.Application.Master.Suppliers.Dtos;
|
||||
|
||||
namespace SolutionErp.Application.Master.Suppliers.Import;
|
||||
|
||||
/// <summary>
|
||||
/// Import NCC từ file Excel "Database NCC" (Supplier Phase B, Approach A — layout-locked).
|
||||
/// Impl ở Infrastructure (ClosedXML). Inject IApplicationDbContext để testable.
|
||||
/// </summary>
|
||||
public interface ISupplierExcelImportService
|
||||
{
|
||||
/// <summary>
|
||||
/// Parse + validate + classify từng hàng (KHÔNG ghi DB). Match Code case-insensitive
|
||||
/// (SQL Server unique-index CI) → existing = Update, mới = New, thiếu Code/Name = Error,
|
||||
/// hàng rỗng = Skip. Header sai layout → PreviewDto.LayoutValid=false.
|
||||
/// </summary>
|
||||
Task<SupplierImportPreviewDto> PreviewAsync(Stream xlsx, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// ALL-OR-NOTHING: re-validate; nếu có hard-error (thiếu Code/Name) → trả Errors, KHÔNG ghi gì.
|
||||
/// Ngược lại upsert (New → insert; existing case-insensitive-Code → fill-nulls-only) rồi
|
||||
/// SaveChanges 1 lần. <paramref name="actor"/> chỉ dùng để log (audit thật do interceptor set).
|
||||
/// </summary>
|
||||
Task<SupplierImportResultDto> ConfirmAsync(IReadOnlyList<SupplierImportRowDto> rows, string actor, CancellationToken ct = default);
|
||||
}
|
||||
@ -22,6 +22,7 @@ public class GetSupplierQueryHandler : IRequestHandler<GetSupplierQuery, Supplie
|
||||
x.PackageCategory, x.OfficeAddress, x.MailingAddress, x.Fax, x.BankAccount, x.SecondaryBankAccount,
|
||||
x.LegalRepresentative, x.LegalRepTitle, x.AuthorizationNote, x.LinkGuq, x.LinkGpkd, x.LinkHsnl,
|
||||
x.ContactTitle, x.ContactPhone, x.MailRecipient, x.ReferralSource, x.OwnerPmh, x.Status,
|
||||
x.SourceUpdatedAt, x.SourceUpdatedBy,
|
||||
x.CreatedAt, x.UpdatedAt);
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,6 +50,7 @@ public class ListSuppliersQueryHandler : IRequestHandler<ListSuppliersQuery, Pag
|
||||
x.PackageCategory, x.OfficeAddress, x.MailingAddress, x.Fax, x.BankAccount, x.SecondaryBankAccount,
|
||||
x.LegalRepresentative, x.LegalRepTitle, x.AuthorizationNote, x.LinkGuq, x.LinkGpkd, x.LinkHsnl,
|
||||
x.ContactTitle, x.ContactPhone, x.MailRecipient, x.ReferralSource, x.OwnerPmh, x.Status,
|
||||
x.SourceUpdatedAt, x.SourceUpdatedBy,
|
||||
x.CreatedAt, x.UpdatedAt))
|
||||
.ToListAsync(ct);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user