feat(locations): persist accountId on create and update

Allow location CRUD to stamp Locations.AccountId after account existence checks so board create can resolve tenant scope.
This commit is contained in:
Arthur Bassi 2026-08-26 14:03:12 -03:00
parent 5a70d5004e
commit 2be36d4eac
5 changed files with 97 additions and 7 deletions

View file

@ -29,6 +29,7 @@ public class LocationControllerSitesTests
var dataService = new LocationDataService(ctx);
var service = new LocationService(
dataService,
new AccountDataService(ctx),
Mock.Of<ICreateLocationValidation>(),
Mock.Of<IUpdateLocationValidation>());

View file

@ -90,6 +90,25 @@ public class LocationControllerTests
service.Verify(s => s.CreateLocationFromRequestAsync(It.IsAny<LocationCreateRequestDTO>(), It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task AddLocation_MapsAccountIdFromBodyString()
{
var service = new Mock<ILocationService>();
LocationCreateRequestDTO? captured = null;
service
.Setup(s => s.CreateLocationFromRequestAsync(It.IsAny<LocationCreateRequestDTO>(), It.IsAny<CancellationToken>()))
.Callback<LocationCreateRequestDTO, CancellationToken>((dto, _) => captured = dto)
.Returns(Task.CompletedTask);
var result = await NewController(service).AddLocation(
new Location_DTO { Name = "X", AccountId = "1" },
CancellationToken.None);
result.Should().BeOfType<OkObjectResult>();
captured.Should().NotBeNull();
captured!.AccountId.Should().Be(1);
}
[Fact]
public async Task EditLocation_WhenMissing_ReturnsNotFound()
{

View file

@ -20,7 +20,17 @@ public class LocationServiceTests
}
private static LocationService NewService(ApplicationDbContext ctx) =>
new(new LocationDataService(ctx), new CreateLocationValidation(), new UpdateLocationValidation());
new(
new LocationDataService(ctx),
new AccountDataService(ctx),
new CreateLocationValidation(),
new UpdateLocationValidation());
private static void SeedAccount(ApplicationDbContext ctx, int id, string name = "Customer")
{
ctx.Accounts.Add(new Accounts { Id = id, Name = name, IsDeleted = false });
ctx.SaveChanges();
}
private static Locations SeedLocation(ApplicationDbContext ctx, string name, string? city = null, string? status = "Active")
{
@ -34,6 +44,7 @@ public class LocationServiceTests
public async Task CreateLocationFromRequestAsync_PersistsMappedFields()
{
using var ctx = NewContext();
SeedAccount(ctx, 9);
var service = NewService(ctx);
await service.CreateLocationFromRequestAsync(new LocationCreateRequestDTO
@ -52,7 +63,7 @@ public class LocationServiceTests
var entity = ctx.Locations.Single();
entity.Name.Should().Be("Warehouse");
entity.AccountId.Should().BeNull();
entity.AccountId.Should().Be(9);
entity.Title.Should().Be("Main WH");
entity.Address1.Should().Be("1 Depot Rd");
entity.City.Should().Be("Austin");
@ -137,9 +148,11 @@ public class LocationServiceTests
}
[Fact]
public async Task UpdateLocationFromRequestAsync_IgnoresClientAccountIdRelabel()
public async Task UpdateLocationFromRequestAsync_AppliesClientAccountIdWhenProvided()
{
using var ctx = NewContext();
SeedAccount(ctx, 4);
SeedAccount(ctx, 99);
var existing = SeedLocation(ctx, "Owned", "Austin");
existing.AccountId = 4;
await ctx.SaveChangesAsync();
@ -150,7 +163,35 @@ public class LocationServiceTests
AccountId = 99
}, CancellationToken.None);
ctx.Locations.Single().AccountId.Should().Be(4);
ctx.Locations.Single().AccountId.Should().Be(99);
}
[Fact]
public async Task UpdateLocationFromRequestAsync_UnknownAccount_ThrowsValidationException()
{
using var ctx = NewContext();
var existing = SeedLocation(ctx, "Owned", "Austin");
var act = () => NewService(ctx).UpdateLocationFromRequestAsync(
existing.Id,
new LocationUpdateRequestDTO { Name = "Owned", AccountId = 404 },
CancellationToken.None);
await act.Should().ThrowAsync<FluentValidation.ValidationException>();
ctx.Locations.Single().AccountId.Should().BeNull();
}
[Fact]
public async Task CreateLocationFromRequestAsync_UnknownAccount_ThrowsValidationException()
{
using var ctx = NewContext();
var act = () => NewService(ctx).CreateLocationFromRequestAsync(
new LocationCreateRequestDTO { Name = "Warehouse", AccountId = 404 },
CancellationToken.None);
await act.Should().ThrowAsync<FluentValidation.ValidationException>();
ctx.Locations.Should().BeEmpty();
}
[Fact]

View file

@ -177,7 +177,8 @@ namespace Api.SeaHavenIndustries.Controllers
ZipCode = model.ZipCode,
Phone = model.Phone,
ContactEmail = model.ContactEmail,
Status = model.Status
Status = model.Status,
AccountId = model.GetAccountId()
};
}
@ -193,7 +194,8 @@ namespace Api.SeaHavenIndustries.Controllers
ZipCode = model.ZipCode,
Phone = model.Phone,
ContactEmail = model.ContactEmail,
Status = model.Status
Status = model.Status,
AccountId = model.GetAccountId()
};
}
}

View file

@ -1,5 +1,6 @@
using Data.SeaHavenIndustries;
using FluentValidation;
using FluentValidation.Results;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Interfaces;
@ -14,15 +15,18 @@ namespace SeaHaven.Services.Implementation
public class LocationService : ILocationService
{
private readonly ILocationDataService _locationDataService;
private readonly IAccountDataService _accountDataService;
private readonly ICreateLocationValidation _createValidator;
private readonly IUpdateLocationValidation _updateValidator;
public LocationService(
ILocationDataService locationDataService,
IAccountDataService accountDataService,
ICreateLocationValidation createValidator,
IUpdateLocationValidation updateValidator)
{
_locationDataService = locationDataService;
_accountDataService = accountDataService;
_createValidator = createValidator;
_updateValidator = updateValidator;
}
@ -177,6 +181,8 @@ namespace SeaHaven.Services.Implementation
public async Task CreateLocationFromRequestAsync(LocationCreateRequestDTO request, CancellationToken cancellationToken)
{
await EnsureAccountExistsAsync(request.AccountId);
var location = new Locations
{
Name = request.Name,
@ -187,7 +193,8 @@ namespace SeaHaven.Services.Implementation
Zip = request.ZipCode,
PhoneNumber = request.Phone,
Email = request.ContactEmail,
Status = request.Status
Status = request.Status,
AccountId = request.AccountId
};
await _locationDataService.AddAsync(location, cancellationToken);
@ -209,6 +216,12 @@ namespace SeaHaven.Services.Implementation
location.Email = request.ContactEmail;
location.Status = request.Status;
if (request.AccountId is int accountId)
{
await EnsureAccountExistsAsync(accountId);
location.AccountId = accountId;
}
await _locationDataService.UpdateAsync(location, cancellationToken);
}
@ -217,6 +230,20 @@ namespace SeaHaven.Services.Implementation
return _locationDataService.DeleteByIdAsync(id, cancellationToken);
}
private async Task EnsureAccountExistsAsync(int? accountId)
{
if (accountId is not int id)
return;
if (!await _accountDataService.ExistsAsync(id))
{
throw new ValidationException(new[]
{
new ValidationFailure(nameof(LocationCreateRequestDTO.AccountId), "Account was not found.")
});
}
}
// Private helper methods
private LocationDTO MapToDTO(Locations location)
{