Files
ersatztv/ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplateHandler.cs
T
timothy cda5c12e34 feat(api): REST endpoints for deco templates and deco template groups (#144 S4 (#162))
Adds DecoTemplateController mirroring TemplateController: CRUD for deco
template groups, deco templates (flat list + by-id), item listing, and a
full-replace PUT for a deco template's item list.

Hardening (deviation from a literal port of the existing handlers, matching
the #144 S2 fix for ReplaceTemplateItemsHandler):
- CreateDecoTemplateHandler now validates DecoTemplateGroupId exists before
  insert (previously a bad id hit the FK constraint at SaveChanges and
  surfaced as a 500; now a 422).
- ReplaceDecoTemplateItemsHandler now rejects invalid items (unknown DecoId,
  StartTime >= EndTime unless EndTime is the end-of-day sentinel 00:00:00,
  or overlapping ranges) with a 422 instead of silently dropping/persisting
  them - the same silent-drop/silent-overlap bug class already fixed for
  templates.

Response DTOs serialize the raw item TimeSpans (via .TimeOfDay), so an
end-of-day item still round-trips as StartTime=22:00:00/EndTime=00:00:00
regardless of the ViewModel's day-wrapping DateTime representation.
2026-07-07 19:51:00 +02:00

72 lines
2.9 KiB
C#

using ErsatzTV.Core;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class CreateDecoTemplateHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<CreateDecoTemplate, Either<BaseError, DecoTemplateViewModel>>
{
public async Task<Either<BaseError, DecoTemplateViewModel>> Handle(
CreateDecoTemplate request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, DecoTemplate> validation = await Validate(dbContext, request);
return await validation.Apply(profile => PersistDecoTemplate(dbContext, profile));
}
private static async Task<DecoTemplateViewModel> PersistDecoTemplate(TvContext dbContext, DecoTemplate decoTemplate)
{
await dbContext.DecoTemplates.AddAsync(decoTemplate);
await dbContext.SaveChangesAsync();
await dbContext.Entry(decoTemplate).Reference(dt => dt.DecoTemplateGroup).LoadAsync();
return Mapper.ProjectToViewModel(decoTemplate);
}
private static async Task<Validation<BaseError, DecoTemplate>> Validate(
TvContext dbContext,
CreateDecoTemplate request)
{
Validation<BaseError, Unit> decoTemplateGroupValidation =
await ValidateDecoTemplateGroupExists(dbContext, request);
Validation<BaseError, string> nameValidation = await ValidateDecoTemplateName(dbContext, request);
return (decoTemplateGroupValidation, nameValidation).Apply((_, name) => new DecoTemplate
{
DecoTemplateGroupId = request.DecoTemplateGroupId,
Name = name
});
}
private static async Task<Validation<BaseError, Unit>> ValidateDecoTemplateGroupExists(
TvContext dbContext,
CreateDecoTemplate request)
{
bool decoTemplateGroupExists =
await dbContext.DecoTemplateGroups.AnyAsync(dtg => dtg.Id == request.DecoTemplateGroupId);
return decoTemplateGroupExists
? Success<BaseError, Unit>(Unit.Default)
: BaseError.New("Deco template group does not exist");
}
private static async Task<Validation<BaseError, string>> ValidateDecoTemplateName(
TvContext dbContext,
CreateDecoTemplate request)
{
if (request.Name.Length > 50)
{
return BaseError.New($"Deco template name \"{request.Name}\" is invalid");
}
bool duplicateName = await dbContext.DecoTemplates
.AnyAsync(r => r.DecoTemplateGroupId == request.DecoTemplateGroupId && r.Name == request.Name);
return duplicateName
? BaseError.New($"A deco template named \"{request.Name}\" already exists in that deco template group")
: Success<BaseError, string>(request.Name);
}
}