mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 04:53:11 +00:00
fix: return vendor duplicate name conflict (#92)
This commit is contained in:
parent
e90e51d271
commit
b605d5be02
4 changed files with 82 additions and 2 deletions
|
|
@ -133,6 +133,28 @@ public class VendorCompanyRosterControllerTests
|
|||
result.Should().BeOfType<BadRequestObjectResult>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_DuplicateName_ReturnsStable409()
|
||||
{
|
||||
var service = new Mock<IVendorCompanyRosterService>();
|
||||
service.Setup(x => x.CreateRosterAsync(It.IsAny<CreateVendorRosterDTO>(), "42", It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new VendorRosterDuplicateNameException(
|
||||
"database detail",
|
||||
new InvalidOperationException("IX_VendorCompanies_NormalizedName")));
|
||||
|
||||
var result = await NewController(service).Create(
|
||||
new CreateVendorRosterDTO { Name = "Acme", Email = "acme@example.com" },
|
||||
CancellationToken.None);
|
||||
|
||||
var conflict = result.Should().BeOfType<ConflictObjectResult>().Subject;
|
||||
conflict.StatusCode.Should().Be(StatusCodes.Status409Conflict);
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(conflict.Value);
|
||||
json.Should().Contain("duplicate_vendor_company_name");
|
||||
json.Should().Contain("Another vendor company already uses that name.");
|
||||
json.Should().NotContain("IX_VendorCompanies_NormalizedName");
|
||||
json.Should().NotContain("database detail");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reconcile_Unauthenticated_Returns401()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -74,6 +74,26 @@ public class VendorCompanyRosterDataServiceTests
|
|||
context.Vendors.Where(v => v.CompanyId == company.Id).Should().OnlyContain(v => v.CompanyName == "Atomic Co");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateRoster_DuplicateNormalizedName_ThrowsWithoutPersisting()
|
||||
{
|
||||
var dbName = Guid.NewGuid().ToString();
|
||||
using var context = NewContext(dbName);
|
||||
await SeedCompanyWithTechnicianAsync(context, "Eletrizante", "Existing");
|
||||
var service = new VendorCompanyRosterDataService(context);
|
||||
|
||||
var call = () => service.CreateRosterAsync(new VendorCompanyRosterWriteModel
|
||||
{
|
||||
Name = " ELETRIZANTE ",
|
||||
Email = "duplicate@example.com",
|
||||
ActorUserId = "42"
|
||||
}, CancellationToken.None);
|
||||
|
||||
await call.Should().ThrowAsync<VendorRosterDuplicateNameException>();
|
||||
context.VendorCompanies.Should().ContainSingle();
|
||||
context.Vendors.Should().ContainSingle();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetRoster_ByVendorId_ResolvesCompanyAndAllTechnicians()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -82,6 +82,15 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
{
|
||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
||||
}
|
||||
catch (VendorRosterDuplicateNameException dup)
|
||||
{
|
||||
return Conflict(new
|
||||
{
|
||||
Status = "Conflict",
|
||||
Message = _logger.Sanitize(dup, "Another vendor company already uses that name."),
|
||||
Code = "duplicate_vendor_company_name"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new Response { Status = "Error", Message = _logger.Sanitize(ex) });
|
||||
|
|
|
|||
|
|
@ -219,11 +219,24 @@ namespace SeaHaven.DataServices.Implementation
|
|||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var trimmedName = roster.Name.Trim();
|
||||
var normalizedName = trimmedName.ToLowerInvariant();
|
||||
|
||||
// NormalizedName has an unfiltered unique index, so deleted companies still
|
||||
// reserve their names. Detect the ordinary collision before tracking a new
|
||||
// aggregate; the save guard below covers the concurrent-create race.
|
||||
var nameTaken = await _context.VendorCompanies
|
||||
.AnyAsync(company => company.NormalizedName == normalizedName, cancellationToken);
|
||||
|
||||
if (nameTaken)
|
||||
throw new VendorRosterDuplicateNameException(
|
||||
"Another vendor company already uses that name.",
|
||||
new InvalidOperationException(
|
||||
$"NormalizedName '{normalizedName}' is already in use."));
|
||||
|
||||
var company = new VendorCompany
|
||||
{
|
||||
Name = trimmedName,
|
||||
NormalizedName = trimmedName.ToLowerInvariant(),
|
||||
NormalizedName = normalizedName,
|
||||
CompanyPhone = roster.CompanyPhone,
|
||||
Email = roster.Email,
|
||||
Address = roster.Address,
|
||||
|
|
@ -263,12 +276,28 @@ namespace SeaHaven.DataServices.Implementation
|
|||
await _context.Vendors.AddAsync(vendor, cancellationToken);
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
catch (DbUpdateException ex) when (IsUniqueConstraintViolation(ex))
|
||||
{
|
||||
throw new VendorRosterDuplicateNameException(
|
||||
"Another vendor company already uses that name.",
|
||||
ex);
|
||||
}
|
||||
|
||||
return await GetRosterAsync(null, company.Id, cancellationToken)
|
||||
?? throw new InvalidOperationException("Roster could not be reloaded after create.");
|
||||
}
|
||||
|
||||
private static bool IsUniqueConstraintViolation(DbUpdateException ex)
|
||||
{
|
||||
var message = ex.InnerException?.Message ?? ex.Message;
|
||||
return message.Contains("duplicate", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("UNIQUE", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public async Task<VendorCompanyRosterReadModel> AddTechniciansAsync(
|
||||
VendorCompanyRosterAddWriteModel roster,
|
||||
CancellationToken cancellationToken)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue