diff --git a/Api.SeaHavenIndustries.Tests/LocationServiceTests.cs b/Api.SeaHavenIndustries.Tests/LocationServiceTests.cs
index dbf05cb..2a3f17c 100644
--- a/Api.SeaHavenIndustries.Tests/LocationServiceTests.cs
+++ b/Api.SeaHavenIndustries.Tests/LocationServiceTests.cs
@@ -15,6 +15,27 @@ namespace Api.SeaHavenIndustries.Tests;
public class LocationServiceTests
{
+ /// Fills the fields a new site must carry (client, address, one contact) unless the test set them.
+ 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 { new() { Name = "Main", Phone = "555-0100" } };
+ return request;
+ }
+
private static ApplicationDbContext NewContext()
{
var options = new DbContextOptionsBuilder()
@@ -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);
diff --git a/Api.SeaHavenIndustries.Tests/LocationSiteContactsTests.cs b/Api.SeaHavenIndustries.Tests/LocationSiteContactsTests.cs
index 5e64489..b366331 100644
--- a/Api.SeaHavenIndustries.Tests/LocationSiteContactsTests.cs
+++ b/Api.SeaHavenIndustries.Tests/LocationSiteContactsTests.cs
@@ -22,6 +22,27 @@ namespace Api.SeaHavenIndustries.Tests;
public class LocationSiteContactsTests
{
+ /// Fills the fields a new site must carry (client, address, one contact) unless the test set them.
+ 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 { new() { Name = "Main", Phone = "555-0100" } };
+ return request;
+ }
+
private static ApplicationDbContext NewContext()
{
var options = new DbContextOptionsBuilder()
@@ -53,7 +74,7 @@ public class LocationSiteContactsTests
private static async Task 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()
- }, OrgWideAdmin(), CancellationToken.None);
+ }), OrgWideAdmin(), CancellationToken.None);
(await act.Should().ThrowAsync())
.Which.Errors.Should().ContainSingle(e => e.PropertyName == "Contacts");
@@ -130,11 +151,11 @@ public class LocationSiteContactsTests
using var ctx = NewContext();
var contacts = new List { 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
{
new() { Name = new string('x', 101), Phone = new string('5', 21) }
}
- }, OrgWideAdmin(), CancellationToken.None);
+ }), OrgWideAdmin(), CancellationToken.None);
var thrown = (await act.Should().ThrowAsync()).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())
.Which.Errors.Should().ContainSingle(e => e.ErrorMessage.Contains("cannot exceed 20"));
diff --git a/Api.SeaHavenIndustries.Tests/SiteRegistryServiceTests.cs b/Api.SeaHavenIndustries.Tests/SiteRegistryServiceTests.cs
index 5e8c18c..c5a718f 100644
--- a/Api.SeaHavenIndustries.Tests/SiteRegistryServiceTests.cs
+++ b/Api.SeaHavenIndustries.Tests/SiteRegistryServiceTests.cs
@@ -423,13 +423,15 @@ public class SiteRegistryServiceTests
using var cts = new CancellationTokenSource();
var token = cts.Token;
var data = new Mock();
- data.Setup(d => d.SiteCodeExistsAsync("BK5", null, null, token)).ReturnsAsync(true);
- var service = NewService(data.Object, Mock.Of());
+ data.Setup(d => d.SiteCodeExistsAsync("BK5", 1, null, token)).ReturnsAsync(true);
+ var accounts = new Mock();
+ 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();
- 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> 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(), "At least one contact is required." },
+ { "contact without phone", r => r.Contacts = new List { new() { Name = "Main", Phone = " " } }, "Contact phone is required." }
+ };
+
+ [Theory]
+ [MemberData(nameof(MissingSiteFields))]
+ public async Task Create_WithoutARequiredSiteField_IsAValidationErrorAndStoresNothing(
+ string _,
+ Action 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())
+ .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()).LocationExistsAsync(deleted.Id, CancellationToken.None)).Should().BeFalse();
+ }
}
diff --git a/SeaHaven.DataServices/Implementation/LocationDataService.cs b/SeaHaven.DataServices/Implementation/LocationDataService.cs
index d1553a2..5215e0e 100644
--- a/SeaHaven.DataServices/Implementation/LocationDataService.cs
+++ b/SeaHaven.DataServices/Implementation/LocationDataService.cs
@@ -17,25 +17,25 @@ namespace SeaHaven.DataServices.Implementation
public async Task GetByIdAsync(int id)
{
- return await _context.Locations.FindAsync(id);
+ return await _context.Locations.FirstOrDefaultAsync(l => l.Id == id && l.IsDeleted != true);
}
public async Task GetByIdWithDetailsAsync(int id)
{
return await _context.Locations
- .FirstOrDefaultAsync(l => l.Id == id);
+ .FirstOrDefaultAsync(l => l.Id == id && l.IsDeleted != true);
}
public async Task> GetAllAsync()
{
- return await _context.Locations.ToListAsync();
+ return await _context.Locations.Where(l => l.IsDeleted != true).ToListAsync();
}
public async Task> 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 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 CountAsync()
{
- return await _context.Locations.CountAsync();
+ return await _context.Locations.CountAsync(l => l.IsDeleted != true);
}
public async Task<(List 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))
{
diff --git a/SeaHaven.DataServices/Implementation/VendorOperationsDataService.cs b/SeaHaven.DataServices/Implementation/VendorOperationsDataService.cs
index 4fabfba..1ae72ac 100644
--- a/SeaHaven.DataServices/Implementation/VendorOperationsDataService.cs
+++ b/SeaHaven.DataServices/Implementation/VendorOperationsDataService.cs
@@ -150,7 +150,7 @@ namespace SeaHaven.DataServices.Implementation
}
public Task 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> GetVendorsByIdsAsync(IReadOnlyCollection vendorIds, CancellationToken cancellationToken)
=> await _context.Vendors.Where(vendor => vendorIds.Contains(vendor.Id))
diff --git a/SeaHaven.Services/Implementation/LocationService.cs b/SeaHaven.Services/Implementation/LocationService.cs
index bc1772a..fabeb97 100644
--- a/SeaHaven.Services/Implementation/LocationService.cs
+++ b/SeaHaven.Services/Implementation/LocationService.cs
@@ -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 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;
+ /// A new site needs a client, a full address and at least one point of contact.
+ private static void ThrowOnMissingSiteFields(LocationCreateRequestDTO request)
+ {
+ var failures = new List();
+ 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 contacts)
{
var failures = SiteContactsValidation.Validate(contacts);