mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-03 13:53:30 +00:00
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:
parent
5a70d5004e
commit
2be36d4eac
5 changed files with 97 additions and 7 deletions
|
|
@ -29,6 +29,7 @@ public class LocationControllerSitesTests
|
||||||
var dataService = new LocationDataService(ctx);
|
var dataService = new LocationDataService(ctx);
|
||||||
var service = new LocationService(
|
var service = new LocationService(
|
||||||
dataService,
|
dataService,
|
||||||
|
new AccountDataService(ctx),
|
||||||
Mock.Of<ICreateLocationValidation>(),
|
Mock.Of<ICreateLocationValidation>(),
|
||||||
Mock.Of<IUpdateLocationValidation>());
|
Mock.Of<IUpdateLocationValidation>());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,25 @@ public class LocationControllerTests
|
||||||
service.Verify(s => s.CreateLocationFromRequestAsync(It.IsAny<LocationCreateRequestDTO>(), It.IsAny<CancellationToken>()), Times.Once);
|
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]
|
[Fact]
|
||||||
public async Task EditLocation_WhenMissing_ReturnsNotFound()
|
public async Task EditLocation_WhenMissing_ReturnsNotFound()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,17 @@ public class LocationServiceTests
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LocationService NewService(ApplicationDbContext ctx) =>
|
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")
|
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()
|
public async Task CreateLocationFromRequestAsync_PersistsMappedFields()
|
||||||
{
|
{
|
||||||
using var ctx = NewContext();
|
using var ctx = NewContext();
|
||||||
|
SeedAccount(ctx, 9);
|
||||||
var service = NewService(ctx);
|
var service = NewService(ctx);
|
||||||
|
|
||||||
await service.CreateLocationFromRequestAsync(new LocationCreateRequestDTO
|
await service.CreateLocationFromRequestAsync(new LocationCreateRequestDTO
|
||||||
|
|
@ -52,7 +63,7 @@ public class LocationServiceTests
|
||||||
|
|
||||||
var entity = ctx.Locations.Single();
|
var entity = ctx.Locations.Single();
|
||||||
entity.Name.Should().Be("Warehouse");
|
entity.Name.Should().Be("Warehouse");
|
||||||
entity.AccountId.Should().BeNull();
|
entity.AccountId.Should().Be(9);
|
||||||
entity.Title.Should().Be("Main WH");
|
entity.Title.Should().Be("Main WH");
|
||||||
entity.Address1.Should().Be("1 Depot Rd");
|
entity.Address1.Should().Be("1 Depot Rd");
|
||||||
entity.City.Should().Be("Austin");
|
entity.City.Should().Be("Austin");
|
||||||
|
|
@ -137,9 +148,11 @@ public class LocationServiceTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task UpdateLocationFromRequestAsync_IgnoresClientAccountIdRelabel()
|
public async Task UpdateLocationFromRequestAsync_AppliesClientAccountIdWhenProvided()
|
||||||
{
|
{
|
||||||
using var ctx = NewContext();
|
using var ctx = NewContext();
|
||||||
|
SeedAccount(ctx, 4);
|
||||||
|
SeedAccount(ctx, 99);
|
||||||
var existing = SeedLocation(ctx, "Owned", "Austin");
|
var existing = SeedLocation(ctx, "Owned", "Austin");
|
||||||
existing.AccountId = 4;
|
existing.AccountId = 4;
|
||||||
await ctx.SaveChangesAsync();
|
await ctx.SaveChangesAsync();
|
||||||
|
|
@ -150,7 +163,35 @@ public class LocationServiceTests
|
||||||
AccountId = 99
|
AccountId = 99
|
||||||
}, CancellationToken.None);
|
}, 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]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,8 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
ZipCode = model.ZipCode,
|
ZipCode = model.ZipCode,
|
||||||
Phone = model.Phone,
|
Phone = model.Phone,
|
||||||
ContactEmail = model.ContactEmail,
|
ContactEmail = model.ContactEmail,
|
||||||
Status = model.Status
|
Status = model.Status,
|
||||||
|
AccountId = model.GetAccountId()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -193,7 +194,8 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
ZipCode = model.ZipCode,
|
ZipCode = model.ZipCode,
|
||||||
Phone = model.Phone,
|
Phone = model.Phone,
|
||||||
ContactEmail = model.ContactEmail,
|
ContactEmail = model.ContactEmail,
|
||||||
Status = model.Status
|
Status = model.Status,
|
||||||
|
AccountId = model.GetAccountId()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
|
using FluentValidation.Results;
|
||||||
using SeaHaven.DataServices.Interfaces;
|
using SeaHaven.DataServices.Interfaces;
|
||||||
using SeaHaven.Services.DTOs;
|
using SeaHaven.Services.DTOs;
|
||||||
using SeaHaven.Services.Interfaces;
|
using SeaHaven.Services.Interfaces;
|
||||||
|
|
@ -14,15 +15,18 @@ namespace SeaHaven.Services.Implementation
|
||||||
public class LocationService : ILocationService
|
public class LocationService : ILocationService
|
||||||
{
|
{
|
||||||
private readonly ILocationDataService _locationDataService;
|
private readonly ILocationDataService _locationDataService;
|
||||||
|
private readonly IAccountDataService _accountDataService;
|
||||||
private readonly ICreateLocationValidation _createValidator;
|
private readonly ICreateLocationValidation _createValidator;
|
||||||
private readonly IUpdateLocationValidation _updateValidator;
|
private readonly IUpdateLocationValidation _updateValidator;
|
||||||
|
|
||||||
public LocationService(
|
public LocationService(
|
||||||
ILocationDataService locationDataService,
|
ILocationDataService locationDataService,
|
||||||
|
IAccountDataService accountDataService,
|
||||||
ICreateLocationValidation createValidator,
|
ICreateLocationValidation createValidator,
|
||||||
IUpdateLocationValidation updateValidator)
|
IUpdateLocationValidation updateValidator)
|
||||||
{
|
{
|
||||||
_locationDataService = locationDataService;
|
_locationDataService = locationDataService;
|
||||||
|
_accountDataService = accountDataService;
|
||||||
_createValidator = createValidator;
|
_createValidator = createValidator;
|
||||||
_updateValidator = updateValidator;
|
_updateValidator = updateValidator;
|
||||||
}
|
}
|
||||||
|
|
@ -177,6 +181,8 @@ namespace SeaHaven.Services.Implementation
|
||||||
|
|
||||||
public async Task CreateLocationFromRequestAsync(LocationCreateRequestDTO request, CancellationToken cancellationToken)
|
public async Task CreateLocationFromRequestAsync(LocationCreateRequestDTO request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
await EnsureAccountExistsAsync(request.AccountId);
|
||||||
|
|
||||||
var location = new Locations
|
var location = new Locations
|
||||||
{
|
{
|
||||||
Name = request.Name,
|
Name = request.Name,
|
||||||
|
|
@ -187,7 +193,8 @@ namespace SeaHaven.Services.Implementation
|
||||||
Zip = request.ZipCode,
|
Zip = request.ZipCode,
|
||||||
PhoneNumber = request.Phone,
|
PhoneNumber = request.Phone,
|
||||||
Email = request.ContactEmail,
|
Email = request.ContactEmail,
|
||||||
Status = request.Status
|
Status = request.Status,
|
||||||
|
AccountId = request.AccountId
|
||||||
};
|
};
|
||||||
|
|
||||||
await _locationDataService.AddAsync(location, cancellationToken);
|
await _locationDataService.AddAsync(location, cancellationToken);
|
||||||
|
|
@ -209,6 +216,12 @@ namespace SeaHaven.Services.Implementation
|
||||||
location.Email = request.ContactEmail;
|
location.Email = request.ContactEmail;
|
||||||
location.Status = request.Status;
|
location.Status = request.Status;
|
||||||
|
|
||||||
|
if (request.AccountId is int accountId)
|
||||||
|
{
|
||||||
|
await EnsureAccountExistsAsync(accountId);
|
||||||
|
location.AccountId = accountId;
|
||||||
|
}
|
||||||
|
|
||||||
await _locationDataService.UpdateAsync(location, cancellationToken);
|
await _locationDataService.UpdateAsync(location, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -217,6 +230,20 @@ namespace SeaHaven.Services.Implementation
|
||||||
return _locationDataService.DeleteByIdAsync(id, cancellationToken);
|
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 helper methods
|
||||||
private LocationDTO MapToDTO(Locations location)
|
private LocationDTO MapToDTO(Locations location)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue