mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-01 06:23:16 +00:00
Hide deleted sites from every location read; require the Add Site fields on create
- Legacy reads (by id, all, by client, paged, address book, exists, count) and the vendor preference site check now skip tombstoned sites - Create requires a client, street address, city, state and at least one complete contact
This commit is contained in:
parent
60b1afd8e0
commit
92dabbfbe3
6 changed files with 149 additions and 36 deletions
|
|
@ -15,6 +15,27 @@ namespace Api.SeaHavenIndustries.Tests;
|
|||
|
||||
public class LocationServiceTests
|
||||
{
|
||||
/// <summary>Fills the fields a new site must carry (client, address, one contact) unless the test set them.</summary>
|
||||
private static LocationCreateRequestDTO WithSiteFields(ApplicationDbContext ctx, LocationCreateRequestDTO request)
|
||||
{
|
||||
if (request.AccountId == null)
|
||||
{
|
||||
if (!ctx.Accounts.Any(a => a.Id == 7))
|
||||
{
|
||||
ctx.Accounts.Add(new Accounts { Id = 7, Name = "Customer", IsDeleted = false });
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
request.AccountId = 7;
|
||||
}
|
||||
|
||||
request.Address ??= "1 Depot Rd";
|
||||
request.City ??= "Dallas";
|
||||
request.State ??= "TX";
|
||||
request.Contacts ??= new List<SiteContactRequestDTO> { new() { Name = "Main", Phone = "555-0100" } };
|
||||
return request;
|
||||
}
|
||||
|
||||
private static ApplicationDbContext NewContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||
|
|
@ -85,7 +106,7 @@ public class LocationServiceTests
|
|||
SeedAccount(ctx, 9);
|
||||
var service = NewService(ctx);
|
||||
|
||||
await service.CreateLocationFromRequestAsync(new LocationCreateRequestDTO
|
||||
await service.CreateLocationFromRequestAsync(WithSiteFields(ctx, new LocationCreateRequestDTO
|
||||
{
|
||||
Name = "Warehouse",
|
||||
Title = "Main WH",
|
||||
|
|
@ -97,7 +118,7 @@ public class LocationServiceTests
|
|||
ContactEmail = "wh@example.com",
|
||||
Status = "Active",
|
||||
AccountId = 9
|
||||
}, OrgWideAdmin(), CancellationToken.None);
|
||||
}), OrgWideAdmin(), CancellationToken.None);
|
||||
|
||||
var entity = ctx.Locations.Single();
|
||||
entity.Name.Should().Be("Warehouse");
|
||||
|
|
@ -390,7 +411,7 @@ public class LocationServiceTests
|
|||
using var ctx = NewContext();
|
||||
|
||||
var act = () => NewService(ctx).CreateLocationFromRequestAsync(
|
||||
new LocationCreateRequestDTO { Name = "Warehouse", AccountId = 404 },
|
||||
WithSiteFields(ctx, new LocationCreateRequestDTO { Name = "Warehouse", AccountId = 404 }),
|
||||
OrgWideAdmin(),
|
||||
CancellationToken.None);
|
||||
|
||||
|
|
@ -406,7 +427,7 @@ public class LocationServiceTests
|
|||
ctx.SaveChanges();
|
||||
|
||||
var act = () => NewService(ctx).CreateLocationFromRequestAsync(
|
||||
new LocationCreateRequestDTO { Name = "Warehouse", AccountId = 9 },
|
||||
WithSiteFields(ctx, new LocationCreateRequestDTO { Name = "Warehouse", AccountId = 9 }),
|
||||
OrgWideAdmin(),
|
||||
CancellationToken.None);
|
||||
|
||||
|
|
@ -422,7 +443,7 @@ public class LocationServiceTests
|
|||
SeedAccount(ctx, 99);
|
||||
|
||||
var act = () => NewService(ctx).CreateLocationFromRequestAsync(
|
||||
new LocationCreateRequestDTO { Name = "Site", AccountId = 99 },
|
||||
WithSiteFields(ctx, new LocationCreateRequestDTO { Name = "Site", AccountId = 99 }),
|
||||
AccountUser(4),
|
||||
CancellationToken.None);
|
||||
|
||||
|
|
@ -457,7 +478,7 @@ public class LocationServiceTests
|
|||
SeedAccount(ctx, 9);
|
||||
|
||||
var act = () => NewService(ctx).CreateLocationFromRequestAsync(
|
||||
new LocationCreateRequestDTO { Name = "Site", AccountId = 9 },
|
||||
WithSiteFields(ctx, new LocationCreateRequestDTO { Name = "Site", AccountId = 9 }),
|
||||
MissingScope(),
|
||||
CancellationToken.None);
|
||||
|
||||
|
|
@ -487,7 +508,7 @@ public class LocationServiceTests
|
|||
using var cts = new CancellationTokenSource();
|
||||
|
||||
await service.CreateLocationFromRequestAsync(
|
||||
new LocationCreateRequestDTO { Name = "Site", AccountId = 9 },
|
||||
WithSiteFields(ctx, new LocationCreateRequestDTO { Name = "Site", AccountId = 9 }),
|
||||
OrgWideAdmin(),
|
||||
cts.Token);
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,27 @@ namespace Api.SeaHavenIndustries.Tests;
|
|||
|
||||
public class LocationSiteContactsTests
|
||||
{
|
||||
/// <summary>Fills the fields a new site must carry (client, address, one contact) unless the test set them.</summary>
|
||||
private static LocationCreateRequestDTO WithSiteFields(ApplicationDbContext ctx, LocationCreateRequestDTO request)
|
||||
{
|
||||
if (request.AccountId == null)
|
||||
{
|
||||
if (!ctx.Accounts.Any(a => a.Id == 7))
|
||||
{
|
||||
ctx.Accounts.Add(new Accounts { Id = 7, Name = "Customer", IsDeleted = false });
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
request.AccountId = 7;
|
||||
}
|
||||
|
||||
request.Address ??= "1 Depot Rd";
|
||||
request.City ??= "Dallas";
|
||||
request.State ??= "TX";
|
||||
request.Contacts ??= new List<SiteContactRequestDTO> { new() { Name = "Main", Phone = "555-0100" } };
|
||||
return request;
|
||||
}
|
||||
|
||||
private static ApplicationDbContext NewContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||
|
|
@ -53,7 +74,7 @@ public class LocationSiteContactsTests
|
|||
private static async Task<Locations> SeedLocationWithContactsAsync(ApplicationDbContext ctx)
|
||||
{
|
||||
var service = NewService(ctx);
|
||||
await service.CreateLocationFromRequestAsync(new LocationCreateRequestDTO
|
||||
await service.CreateLocationFromRequestAsync(WithSiteFields(ctx, new LocationCreateRequestDTO
|
||||
{
|
||||
Name = "Depot",
|
||||
Phone = "555-9000",
|
||||
|
|
@ -63,7 +84,7 @@ public class LocationSiteContactsTests
|
|||
new() { Name = " Alice Cooper ", Phone = " 555-0100 " },
|
||||
new() { Name = "Bob Dillon", Phone = "555-0200"}
|
||||
}
|
||||
}, OrgWideAdmin(), CancellationToken.None);
|
||||
}), OrgWideAdmin(), CancellationToken.None);
|
||||
return ctx.Locations.Include(l => l.Contacts).Single();
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +95,7 @@ public class LocationSiteContactsTests
|
|||
ctx.Accounts.Add(new Accounts { Id = 7, Name = "Customer", IsDeleted = false });
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
await NewService(ctx).CreateLocationFromRequestAsync(new LocationCreateRequestDTO
|
||||
await NewService(ctx).CreateLocationFromRequestAsync(WithSiteFields(ctx, new LocationCreateRequestDTO
|
||||
{
|
||||
Name = "Warehouse",
|
||||
AccountId = 7,
|
||||
|
|
@ -83,7 +104,7 @@ public class LocationSiteContactsTests
|
|||
new() { Name = " Alice Cooper ", Phone = " 555-0100 " },
|
||||
new() { Name = "Bob Dillon", Phone = "555-0200" }
|
||||
}
|
||||
}, OrgWideAdmin(), CancellationToken.None);
|
||||
}), OrgWideAdmin(), CancellationToken.None);
|
||||
|
||||
var location = ctx.Locations.Include(l => l.Contacts).Single();
|
||||
location.PhoneNumber.Should().BeNull("Site Phone is independent of the contacts");
|
||||
|
|
@ -107,11 +128,11 @@ public class LocationSiteContactsTests
|
|||
{
|
||||
using var ctx = NewContext();
|
||||
|
||||
var act = () => NewService(ctx).CreateLocationFromRequestAsync(new LocationCreateRequestDTO
|
||||
var act = () => NewService(ctx).CreateLocationFromRequestAsync(WithSiteFields(ctx, new LocationCreateRequestDTO
|
||||
{
|
||||
Name = "Warehouse",
|
||||
Contacts = new List<SiteContactRequestDTO>()
|
||||
}, OrgWideAdmin(), CancellationToken.None);
|
||||
}), OrgWideAdmin(), CancellationToken.None);
|
||||
|
||||
(await act.Should().ThrowAsync<FluentValidation.ValidationException>())
|
||||
.Which.Errors.Should().ContainSingle(e => e.PropertyName == "Contacts");
|
||||
|
|
@ -130,11 +151,11 @@ public class LocationSiteContactsTests
|
|||
using var ctx = NewContext();
|
||||
var contacts = new List<SiteContactRequestDTO> { new() { Name = name, Phone = phone } };
|
||||
|
||||
var act = () => NewService(ctx).CreateLocationFromRequestAsync(new LocationCreateRequestDTO
|
||||
var act = () => NewService(ctx).CreateLocationFromRequestAsync(WithSiteFields(ctx, new LocationCreateRequestDTO
|
||||
{
|
||||
Name = "Warehouse",
|
||||
Contacts = contacts
|
||||
}, OrgWideAdmin(), CancellationToken.None);
|
||||
}), OrgWideAdmin(), CancellationToken.None);
|
||||
|
||||
if (expectedError == null)
|
||||
{
|
||||
|
|
@ -151,14 +172,14 @@ public class LocationSiteContactsTests
|
|||
{
|
||||
using var ctx = NewContext();
|
||||
|
||||
var act = () => NewService(ctx).CreateLocationFromRequestAsync(new LocationCreateRequestDTO
|
||||
var act = () => NewService(ctx).CreateLocationFromRequestAsync(WithSiteFields(ctx, new LocationCreateRequestDTO
|
||||
{
|
||||
Name = "Warehouse",
|
||||
Contacts = new List<SiteContactRequestDTO>
|
||||
{
|
||||
new() { Name = new string('x', 101), Phone = new string('5', 21) }
|
||||
}
|
||||
}, OrgWideAdmin(), CancellationToken.None);
|
||||
}), OrgWideAdmin(), CancellationToken.None);
|
||||
|
||||
var thrown = (await act.Should().ThrowAsync<FluentValidation.ValidationException>()).Which;
|
||||
thrown.Errors.Should().Contain(e => e.ErrorMessage.Contains("cannot exceed 100"));
|
||||
|
|
@ -173,11 +194,11 @@ public class LocationSiteContactsTests
|
|||
.Select(i => new SiteContactRequestDTO { Name = $"C{i}", Phone = "555-0100" })
|
||||
.ToList();
|
||||
|
||||
var act = () => NewService(ctx).CreateLocationFromRequestAsync(new LocationCreateRequestDTO
|
||||
var act = () => NewService(ctx).CreateLocationFromRequestAsync(WithSiteFields(ctx, new LocationCreateRequestDTO
|
||||
{
|
||||
Name = "Warehouse",
|
||||
Contacts = contacts
|
||||
}, OrgWideAdmin(), CancellationToken.None);
|
||||
}), OrgWideAdmin(), CancellationToken.None);
|
||||
|
||||
(await act.Should().ThrowAsync<FluentValidation.ValidationException>())
|
||||
.Which.Errors.Should().ContainSingle(e => e.ErrorMessage.Contains("cannot exceed 20"));
|
||||
|
|
|
|||
|
|
@ -423,13 +423,15 @@ public class SiteRegistryServiceTests
|
|||
using var cts = new CancellationTokenSource();
|
||||
var token = cts.Token;
|
||||
var data = new Mock<ILocationDataService>();
|
||||
data.Setup(d => d.SiteCodeExistsAsync("BK5", null, null, token)).ReturnsAsync(true);
|
||||
var service = NewService(data.Object, Mock.Of<IAccountDataService>());
|
||||
data.Setup(d => d.SiteCodeExistsAsync("BK5", 1, null, token)).ReturnsAsync(true);
|
||||
var accounts = new Mock<IAccountDataService>();
|
||||
accounts.Setup(a => a.ExistsActiveAsync(1, token)).ReturnsAsync(true);
|
||||
var service = NewService(data.Object, accounts.Object);
|
||||
|
||||
var act = () => service.CreateLocationFromRequestAsync(CreateRequest("BK5", null), OrgWide(), token);
|
||||
var act = () => service.CreateLocationFromRequestAsync(CreateRequest("BK5", 1), OrgWide(), token);
|
||||
|
||||
await act.Should().ThrowAsync<SiteCodeConflictException>();
|
||||
data.Verify(d => d.SiteCodeExistsAsync("BK5", null, null, token), Times.Once);
|
||||
data.Verify(d => d.SiteCodeExistsAsync("BK5", 1, null, token), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -446,4 +448,56 @@ public class SiteRegistryServiceTests
|
|||
using var frozen = JsonDocument.Parse(workOrder.FrozenPoc!);
|
||||
frozen.RootElement.GetProperty("notes").GetString().Should().Be("Gate code 4411");
|
||||
}
|
||||
|
||||
public static TheoryData<string, Action<LocationCreateRequestDTO>, string> MissingSiteFields => new()
|
||||
{
|
||||
{ "no client", r => r.AccountId = null, "Client is required." },
|
||||
{ "no street address", r => r.Address = " ", "Street Address is required." },
|
||||
{ "no city", r => r.City = null, "City is required." },
|
||||
{ "no state", r => r.State = "", "State is required." },
|
||||
{ "no contacts list", r => r.Contacts = null, "At least one contact is required." },
|
||||
{ "empty contacts list", r => r.Contacts = new List<SiteContactRequestDTO>(), "At least one contact is required." },
|
||||
{ "contact without phone", r => r.Contacts = new List<SiteContactRequestDTO> { new() { Name = "Main", Phone = " " } }, "Contact phone is required." }
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(MissingSiteFields))]
|
||||
public async Task Create_WithoutARequiredSiteField_IsAValidationErrorAndStoresNothing(
|
||||
string _,
|
||||
Action<LocationCreateRequestDTO> strip,
|
||||
string expectedMessage)
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
SeedAccounts(ctx, 1);
|
||||
var request = CreateRequest("BK9", 1);
|
||||
strip(request);
|
||||
|
||||
var act = () => NewService(ctx).CreateLocationFromRequestAsync(request, OrgWide(), CancellationToken.None);
|
||||
|
||||
(await act.Should().ThrowAsync<FluentValidation.ValidationException>())
|
||||
.Which.Errors.Should().Contain(e => e.ErrorMessage == expectedMessage);
|
||||
ctx.Locations.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_RemovesTheSiteFromEveryLegacyRead()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
SeedAccounts(ctx, 1);
|
||||
var deleted = SeedSite(ctx, "BK5", 1);
|
||||
var kept = SeedSite(ctx, "BK6", 1);
|
||||
var service = NewService(ctx);
|
||||
|
||||
(await service.DeleteLocationByIdAsync(deleted.Id, OrgWide(), CancellationToken.None)).Should().BeTrue();
|
||||
|
||||
(await service.GetLocationByIdAsync(deleted.Id)).Should().BeNull();
|
||||
(await service.GetLocationByIdWithDetailsAsync(deleted.Id)).Should().BeNull();
|
||||
(await service.LocationExistsAsync(deleted.Id)).Should().BeFalse();
|
||||
(await service.GetTotalLocationCountAsync()).Should().Be(1);
|
||||
(await service.GetAllLocationsAsync()).Select(l => l.Id).Should().Equal(kept.Id);
|
||||
(await service.GetLocationsByAccountIdAsync(1)).Select(l => l.Id).Should().Equal(kept.Id);
|
||||
(await service.GetLocationsPagedAsync(1, 10)).Items.Select(l => l.Id).Should().Equal(kept.Id);
|
||||
(await new LocationDataService(ctx).GetAddressbookPagedAsync(1, 10)).TotalCount.Should().Be(1);
|
||||
(await new VendorOperationsDataService(ctx, Mock.Of<IDispatchDataService>()).LocationExistsAsync(deleted.Id, CancellationToken.None)).Should().BeFalse();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,25 +17,25 @@ namespace SeaHaven.DataServices.Implementation
|
|||
|
||||
public async Task<Locations?> GetByIdAsync(int id)
|
||||
{
|
||||
return await _context.Locations.FindAsync(id);
|
||||
return await _context.Locations.FirstOrDefaultAsync(l => l.Id == id && l.IsDeleted != true);
|
||||
}
|
||||
|
||||
public async Task<Locations?> GetByIdWithDetailsAsync(int id)
|
||||
{
|
||||
return await _context.Locations
|
||||
.FirstOrDefaultAsync(l => l.Id == id);
|
||||
.FirstOrDefaultAsync(l => l.Id == id && l.IsDeleted != true);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Locations>> GetAllAsync()
|
||||
{
|
||||
return await _context.Locations.ToListAsync();
|
||||
return await _context.Locations.Where(l => l.IsDeleted != true).ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Locations>> GetByAccountIdAsync(int accountId)
|
||||
{
|
||||
return await _context.Locations
|
||||
.AsNoTracking()
|
||||
.Where(l => l.AccountId == accountId)
|
||||
.Where(l => l.AccountId == accountId && l.IsDeleted != true)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ namespace SeaHaven.DataServices.Implementation
|
|||
int pageSize,
|
||||
string? search = null)
|
||||
{
|
||||
var query = _context.Locations.AsQueryable();
|
||||
var query = _context.Locations.Where(l => l.IsDeleted != true);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
|
|
@ -125,12 +125,12 @@ namespace SeaHaven.DataServices.Implementation
|
|||
|
||||
public async Task<bool> ExistsAsync(int id)
|
||||
{
|
||||
return await _context.Locations.AnyAsync(l => l.Id == id);
|
||||
return await _context.Locations.AnyAsync(l => l.Id == id && l.IsDeleted != true);
|
||||
}
|
||||
|
||||
public async Task<int> CountAsync()
|
||||
{
|
||||
return await _context.Locations.CountAsync();
|
||||
return await _context.Locations.CountAsync(l => l.IsDeleted != true);
|
||||
}
|
||||
|
||||
public async Task<(List<Locations> Items, int TotalCount)> GetListPagedAsync(
|
||||
|
|
@ -313,7 +313,7 @@ namespace SeaHaven.DataServices.Implementation
|
|||
{
|
||||
search ??= "";
|
||||
|
||||
var query = _context.Locations.AsQueryable();
|
||||
var query = _context.Locations.Where(l => l.IsDeleted != true);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ namespace SeaHaven.DataServices.Implementation
|
|||
}
|
||||
|
||||
public Task<bool> LocationExistsAsync(int locationId, CancellationToken cancellationToken)
|
||||
=> _context.Locations.AnyAsync(location => location.Id == locationId, cancellationToken);
|
||||
=> _context.Locations.AnyAsync(location => location.Id == locationId && location.IsDeleted != true, cancellationToken);
|
||||
|
||||
public async Task<Dictionary<int, Vendor>> GetVendorsByIdsAsync(IReadOnlyCollection<int> vendorIds, CancellationToken cancellationToken)
|
||||
=> await _context.Vendors.Where(vendor => vendorIds.Contains(vendor.Id))
|
||||
|
|
|
|||
|
|
@ -273,6 +273,7 @@ namespace SeaHaven.Services.Implementation
|
|||
CancellationToken cancellationToken)
|
||||
{
|
||||
await EnsureAccountAssignableAsync(user, request.AccountId, existingLocationAccountId: null, cancellationToken);
|
||||
ThrowOnMissingSiteFields(request);
|
||||
|
||||
var siteCode = request.Name?.Trim();
|
||||
if (string.IsNullOrEmpty(siteCode))
|
||||
|
|
@ -300,10 +301,7 @@ namespace SeaHaven.Services.Implementation
|
|||
Notes = NormalizeNotes(request.Notes)
|
||||
};
|
||||
|
||||
if (request.Contacts is List<SiteContactRequestDTO> contacts)
|
||||
{
|
||||
ApplySiteContactsForCreate(location, contacts, GetActorId(user));
|
||||
}
|
||||
ApplySiteContactsForCreate(location, request.Contacts!, GetActorId(user));
|
||||
|
||||
await _locationDataService.AddAsync(location, cancellationToken);
|
||||
}
|
||||
|
|
@ -548,6 +546,25 @@ namespace SeaHaven.Services.Implementation
|
|||
private static string? GetActorId(ClaimsPrincipal user) =>
|
||||
user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
/// <summary>A new site needs a client, a full address and at least one point of contact.</summary>
|
||||
private static void ThrowOnMissingSiteFields(LocationCreateRequestDTO request)
|
||||
{
|
||||
var failures = new List<ValidationFailure>();
|
||||
if (request.AccountId == null)
|
||||
failures.Add(new ValidationFailure(nameof(LocationCreateRequestDTO.AccountId), "Client is required."));
|
||||
if (string.IsNullOrWhiteSpace(request.Address))
|
||||
failures.Add(new ValidationFailure(nameof(LocationCreateRequestDTO.Address), "Street Address is required."));
|
||||
if (string.IsNullOrWhiteSpace(request.City))
|
||||
failures.Add(new ValidationFailure(nameof(LocationCreateRequestDTO.City), "City is required."));
|
||||
if (string.IsNullOrWhiteSpace(request.State))
|
||||
failures.Add(new ValidationFailure(nameof(LocationCreateRequestDTO.State), "State is required."));
|
||||
if (request.Contacts == null || request.Contacts.Count == 0)
|
||||
failures.Add(new ValidationFailure(nameof(LocationCreateRequestDTO.Contacts), "At least one contact is required."));
|
||||
|
||||
if (failures.Count > 0)
|
||||
throw new ValidationException(failures);
|
||||
}
|
||||
|
||||
private static void ThrowOnInvalidContacts(List<SiteContactRequestDTO> contacts)
|
||||
{
|
||||
var failures = SiteContactsValidation.Validate(contacts);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue