SHA256
GuestDocument Versions - Part 1:
- Changed GuestDocument PK from `DocumentType`+`DocumentNumber` to to `ID`+`Version` for future versioning. - Added DB field "IsLastVersion" to simplify queries where only last/current version is allowed. - Updated GuestDocument DB fields type for `CreateTime` & `UpdateTime` to add precision. - Updated usages of GuestDocument PK to new PK. - Limit delete of a GuestDocument when it's currently used in a StayEntry. Note: DB changes require recreating db tables, so all data is lost and migration "rollback" is not allowed.
This commit is contained in:
@@ -12,7 +12,8 @@ namespace Somnia.GuestLog.Services.Entity;
|
||||
public class GuestDocumentEntityService(
|
||||
ILogger<GuestDocumentEntityService> logger,
|
||||
IDbContextFactory<GuestLogContext> contextFactory,
|
||||
IValidator<GuestDocumentDTO> dtoValidator
|
||||
IValidator<GuestDocumentDTO> dtoValidator,
|
||||
IStayEntryEntityService stayEntityService
|
||||
) : IGuestDocumentEntityService
|
||||
{
|
||||
public async Task<Result> CreateDocumentAsync(GuestDocumentDTO dto)
|
||||
@@ -40,6 +41,7 @@ public class GuestDocumentEntityService(
|
||||
DateTime createTime = DateTime.UtcNow;
|
||||
GuestDocument dbItem = new()
|
||||
{
|
||||
Version = GuestConsts.GuestDocument_FirstVersion,
|
||||
DocumentType = dto.DocumentType!,
|
||||
DocumentNum = dto.DocumentNum!,
|
||||
IssueDate = DateOnly.FromDateTime(dto.IssueDate!.Value),
|
||||
@@ -55,6 +57,7 @@ public class GuestDocumentEntityService(
|
||||
AddressDetails = addressItem,
|
||||
CreateTime = createTime,
|
||||
UpdateTime = createTime,
|
||||
IsLastVersion = true, //On creation, created entity is last version.
|
||||
};
|
||||
|
||||
try
|
||||
@@ -80,54 +83,93 @@ public class GuestDocumentEntityService(
|
||||
|
||||
public Task<Result> DeleteDocumentAsync(GuestDocumentDTO dto)
|
||||
{
|
||||
if (dto.DocumentType is null || dto.DocumentNum is null)
|
||||
if (dto.Id is null)
|
||||
{
|
||||
throw new InvalidOperationException("GuestDocumentService - DeleteDocument: A DTO without key can't be deleted!");
|
||||
throw new InvalidOperationException("GuestDocumentService - DeleteDocument: A DTO without ID can't be deleted!");
|
||||
}
|
||||
|
||||
return DeleteDocumentAsync(dto.DocumentType, dto.DocumentNum);
|
||||
return DeleteDocumentAsync(dto.Id.Value);
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteDocumentAsync(string docType, string docNum)
|
||||
public async Task<Result> DeleteDocumentAsync(long id)
|
||||
{
|
||||
using var dbContext = contextFactory.CreateDbContext();
|
||||
GuestDocument? dbItem = await dbContext.GuestDocuments.FindAsync(docType, docNum);
|
||||
if (dbItem is null)
|
||||
{
|
||||
logger.LogWarning("GuestDocumentService - DeleteDocument: Error deleting GuestDocument. Entity not found with key: '{key}-{key2}'", docType, docNum);
|
||||
return Result.Fail(ResultCode.NotFound);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
dbContext.GuestDocuments.Remove(dbItem);
|
||||
await dbContext.SaveChangesAsync();
|
||||
using var dbContext = contextFactory.CreateDbContext();
|
||||
|
||||
logger.LogInformation("GuestDocumentService - DeleteDocument: GuestDocument deleted: '{id}'", dbItem.GetFullKey());
|
||||
//Check if GuestDocument is used in a Stay.
|
||||
bool isUsed = await stayEntityService.IsGuestInStay(id);
|
||||
if (isUsed)
|
||||
{
|
||||
logger.LogWarning("GuestDocumentService - DeleteDocument: Error deleting GuestDocument. Guest (ID: '{ID}') is in a Stay", id);
|
||||
return Result.FailWithContext(ResultCode.GuestInStay, id);
|
||||
}
|
||||
|
||||
int deletedCount = await dbContext.GuestDocuments
|
||||
.Where(e => e.Id == id)
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
if (deletedCount == 0)
|
||||
{
|
||||
logger.LogWarning("GuestDocumentService - DeleteDocument: Error deleting GuestDocument. Entity not found with ID: '{ID}'", id);
|
||||
return Result.Fail(ResultCode.NotFound);
|
||||
}
|
||||
|
||||
logger.LogInformation(
|
||||
"GuestDocumentService - DeleteDocument: GuestDocument deleted with Id: '{id}'. Versions deleted: '{Count}'",
|
||||
id, deletedCount);
|
||||
return Result.Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "GuestDocumentService - CreateDocument: Error deleting GuestDocument: '{id}'", dbItem.GetFullKey());
|
||||
logger.LogError(ex, "GuestDocumentService - CreateDocument: Error deleting GuestDocument: '{id}'", id);
|
||||
return Result.FailUnknownError();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<GuestDocumentDTO?> GetDtoByIdAsync(string docType, string docNum)
|
||||
public async Task<GuestDocumentDTO?> GetLastVersionDtoByIdAsync(long id)
|
||||
{
|
||||
using var dbContext = contextFactory.CreateDbContext();
|
||||
GuestDocument? dbItem = await dbContext.GuestDocuments.FindAsync(docType, docNum);
|
||||
GuestDocument? dbItem = await dbContext.GuestDocuments
|
||||
.Where(e => e.Id == id)
|
||||
.OrderByDescending(e => e.Version)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
return dbItem is not null ? GetDtoFromDb(dbItem) : null;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<GuestDocumentDTO>> GetDtoListAsync()
|
||||
public async Task<GuestDocumentDTO?> GetLastVersionDtoByDocumentAsync(string docType, string docNum)
|
||||
{
|
||||
using var dbContext = contextFactory.CreateDbContext();
|
||||
var dbList = await dbContext.GuestDocuments
|
||||
.OrderByDescending(e => e.UpdateTime)
|
||||
GuestDocument? dbItem = await dbContext.GuestDocuments
|
||||
.Where(e => e.DocumentType == docType && e.DocumentNum == docNum)
|
||||
.OrderByDescending(e => e.Version)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
return dbItem is not null ? GetDtoFromDb(dbItem) : null;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<GuestDocumentDTO>> GetDtoListAsync(bool onlyLastVersion)
|
||||
{
|
||||
using var dbContext = contextFactory.CreateDbContext();
|
||||
|
||||
// TEMP solution
|
||||
var query = dbContext.GuestDocuments.AsQueryable();
|
||||
if (onlyLastVersion)
|
||||
{
|
||||
query = query.Where(e => e.IsLastVersion == true);
|
||||
}
|
||||
var dbList = await query.OrderByDescending(e => e.UpdateTime)
|
||||
.ToListAsync();
|
||||
|
||||
// TODO: Waiting for EF Core Window function improvements:
|
||||
// See expected translation: https://github.com/dotnet/efcore/issues/36380#issuecomment-3082788964
|
||||
// See window function status: https://github.com/dotnet/efcore/issues/12747
|
||||
// var dbList = await dbContext.GuestDocuments
|
||||
// .GroupBy(e => e.Id)
|
||||
// .Select(g => g.OrderByDescending(e => e.Version).First())
|
||||
// .ToListAsync();
|
||||
|
||||
return dbList.Select(GetDtoFromDb);
|
||||
}
|
||||
|
||||
@@ -154,8 +196,21 @@ public class GuestDocumentEntityService(
|
||||
query = query.WhereIf(!string.IsNullOrEmpty(filter.Surname),
|
||||
e => EF.Functions.Like(e.Surname, filter.Surname));
|
||||
|
||||
// TEMP while waiting for EF Core Window function improvements
|
||||
query = query.WhereIf(filter.OnlyLastVersion,
|
||||
e => e.IsLastVersion == true);
|
||||
query = query.OrderByDescending(e => e.UpdateTime);
|
||||
|
||||
|
||||
// TODO: Waiting for EF Core Window function improvements:
|
||||
// See: https://github.com/dotnet/efcore/issues/27470
|
||||
// See window function status: https://github.com/dotnet/efcore/issues/12747
|
||||
// For Ordering (or operations post-grouping), see: https://github.com/dotnet/efcore/pull/38687
|
||||
// query = query
|
||||
// .OrderByDescending(e => e.UpdateTime)
|
||||
// .GroupBy(e => e.Id)
|
||||
// .Select(g => g.OrderByDescending(e => e.Version).First());
|
||||
|
||||
if (limit > 0)
|
||||
{
|
||||
query = query.Take(limit);
|
||||
|
||||
@@ -6,11 +6,23 @@ public interface IGuestDocumentEntityService
|
||||
{
|
||||
public Task<Result> CreateDocumentAsync(GuestDocumentDTO dto);
|
||||
public Task<Result> UpdateDocumentAsync(GuestDocumentDTO dto);
|
||||
public Task<Result> DeleteDocumentAsync(GuestDocumentDTO dto);
|
||||
public Task<Result> DeleteDocumentAsync(string docType, string docNum);
|
||||
|
||||
public Task<GuestDocumentDTO?> GetDtoByIdAsync(string docType, string docNum);
|
||||
/// <summary>
|
||||
/// Same behavior as <see cref="DeleteDocumentAsync(long)"/>
|
||||
/// </summary>
|
||||
public Task<Result> DeleteDocumentAsync(GuestDocumentDTO dto);
|
||||
|
||||
/// <summary>
|
||||
/// Delete documents by ID.
|
||||
/// This operation will delete all versions of a document.
|
||||
/// </summary>
|
||||
/// <param name="id">ID of the document to Delete</param>
|
||||
public Task<Result> DeleteDocumentAsync(long id);
|
||||
|
||||
public Task<GuestDocumentDTO?> GetLastVersionDtoByIdAsync(long id);
|
||||
public Task<GuestDocumentDTO?> GetLastVersionDtoByDocumentAsync(string docType, string docNum);
|
||||
|
||||
public Task<IEnumerable<GuestDocumentDTO>> GetDtoListAsync(bool onlyLastVersion);
|
||||
public Task<IEnumerable<GuestDocumentDTO>> GetDtoListByFilterAsync(GuestDocFilterDTO filter, int limit = 0);
|
||||
public Task<IEnumerable<GuestDocumentDTO>> GetDtoListAsync();
|
||||
|
||||
}
|
||||
@@ -15,7 +15,8 @@ public interface IStayEntryEntityService
|
||||
public Task<IEnumerable<StayEntryDTO>> GetDtoListByFilterAsync(StayEntryFilterDTO filter, int limit = 0, StayEntryQueryConfig? queryConfig = null);
|
||||
|
||||
|
||||
public Task<bool> IsGuestInStayInPeriod(string docType, string docNum, DateTime startTime, DateTime endTime);
|
||||
public Task<IEnumerable<StayEntryDTO>> GetStaysWithGuest(string docType, string docNum, StayEntryQueryConfig? queryConfig = null);
|
||||
public Task<bool> IsGuestInStay(long guestDocId);
|
||||
public Task<bool> IsGuestInStayInPeriod(long guestDocId, DateTime startTime, DateTime endTime);
|
||||
public Task<IEnumerable<StayEntryDTO>> GetStaysWithGuest(long guestDocId, StayEntryQueryConfig? queryConfig = null);
|
||||
|
||||
}
|
||||
@@ -46,12 +46,13 @@ public class StayEntryEntityService(
|
||||
//Check and add GuestDocuments
|
||||
foreach (GuestDocumentDTO guestDto in dto.GuestDocuments!)
|
||||
{
|
||||
//Get it from db.
|
||||
GuestDocument? dbGuest = await dbContext.GuestDocuments.FindAsync(guestDto.DocumentType, guestDto.DocumentNum)
|
||||
?? throw new InvalidOperationException($"GuestDocument not found! Id: '{guestDto.FullKey}'");
|
||||
//Get guest from db.
|
||||
GuestDocument dbGuest = await dbContext.GuestDocuments
|
||||
.Where(gd => gd.Id == guestDto.Id && gd.Version == guestDto.Version)
|
||||
.SingleAsync();
|
||||
|
||||
//Check if Guest is already in other stay during the same period.
|
||||
bool isInOtherStay = await this.IsGuestInStayInPeriod(dbGuest.DocumentType, dbGuest.DocumentNum, dbStay.DateIn, dbStay.DateOut);
|
||||
bool isInOtherStay = await this.IsGuestInStayInPeriod(dbGuest.Id, dbStay.DateIn, dbStay.DateOut);
|
||||
if (isInOtherStay)
|
||||
{
|
||||
string period = $"{dbStay.DateIn}-{dbStay.DateOut}";
|
||||
@@ -168,24 +169,34 @@ public class StayEntryEntityService(
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> IsGuestInStayInPeriod(string docType, string docNum, DateTime startTime, DateTime endTime)
|
||||
public async Task<bool> IsGuestInStay(long guestDocId)
|
||||
{
|
||||
using var dbContext = contextFactory.CreateDbContext();
|
||||
|
||||
bool rs = await dbContext.StayEntries
|
||||
.AnyAsync(s => s.GuestDocuments.Any(g => g.Id == guestDocId));
|
||||
|
||||
return rs;
|
||||
}
|
||||
|
||||
public async Task<bool> IsGuestInStayInPeriod(long guestDocId, DateTime startTime, DateTime endTime)
|
||||
{
|
||||
using var dbContext = contextFactory.CreateDbContext();
|
||||
|
||||
bool rs = await dbContext.StayEntries
|
||||
.AnyAsync(s => s.DateIn < endTime && s.DateOut > startTime
|
||||
&& s.GuestDocuments.Any(g => g.DocumentType == docType && g.DocumentNum == docNum)
|
||||
&& s.GuestDocuments.Any(g => g.Id == guestDocId)
|
||||
);
|
||||
|
||||
return rs;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<StayEntryDTO>> GetStaysWithGuest(string docType, string docNum, StayEntryQueryConfig? queryConfig = null)
|
||||
public async Task<IEnumerable<StayEntryDTO>> GetStaysWithGuest(long guestDocId, StayEntryQueryConfig? queryConfig = null)
|
||||
{
|
||||
using var dbContext = contextFactory.CreateDbContext();
|
||||
|
||||
var rs = await dbContext.StayEntries
|
||||
.Where(s => s.GuestDocuments.Any(g => g.DocumentType == docType && g.DocumentNum == docNum))
|
||||
.Where(s => s.GuestDocuments.Any(g => g.Id == guestDocId))
|
||||
.AddQueryConfig(queryConfig)
|
||||
.OrderByDescending(e => e.DateIn)
|
||||
.ToListAsync();
|
||||
|
||||
Reference in New Issue
Block a user