fixes and tweaks

This commit is contained in:
moarten
2026-04-29 20:36:08 +02:00
parent b71f45e76c
commit 385119bb27
58 changed files with 1512 additions and 350 deletions
@@ -5,12 +5,8 @@ using Microsoft.AspNetCore.Mvc;
namespace Lutra.API.Controllers;
/// <summary>
/// Provides a dedicated endpoint group for Supermarkt-related operations.
/// Provides endpoints for Supermarkt-related operations.
/// </summary>
/// <remarks>
/// This controller is intentionally empty for now. Endpoints will be added when the Supermarkt
/// use cases are implemented.
/// </remarks>
[ApiController]
[Route("api/supermarkten")]
[Produces("application/json")]
@@ -21,7 +17,7 @@ public class SupermarktenController(IMediator mediator) : ControllerBase
/// </summary>
/// <param name="skip">The number of items to skip.</param>
/// <param name="take">The maximum number of items to return.</param>
/// <returns>The requested verspakket page.</returns>
/// <returns>The requested supermarkt page.</returns>
[HttpGet]
[ProducesResponseType(typeof(GetSupermarkten.Response), StatusCodes.Status200OK)]
public async Task<GetSupermarkten.Response> Get(int skip = 0, int take = 50)
@@ -3,134 +3,158 @@ using Lutra.API.Requests;
using Lutra.Application.Verspakketten;
using Microsoft.AspNetCore.Mvc;
namespace Lutra.API.Controllers
namespace Lutra.API.Controllers;
/// <summary>
/// Provides access to verspakket resources.
/// </summary>
[ApiController]
[Route("api/verspakketten")]
[Produces("application/json")]
public class VerspakkettenController(IMediator mediator) : ControllerBase
{
/// <summary>
/// Provides access to verspakket resources.
/// Gets a page of verspakketten.
/// </summary>
[ApiController]
[Route("api/verspakketten")]
[Produces("application/json")]
public class VerspakkettenController(IMediator mediator) : ControllerBase
/// <param name="skip">The number of items to skip. Default: 0.</param>
/// <param name="take">The maximum number of items to return. Default: 50.</param>
/// <param name="sortField">The field to sort by: Naam, PrijsInCenten, AverageCijferSmaak, or AverageCijferBereiden. Default: Naam.</param>
/// <param name="sortDirection">The sort direction: Ascending or Descending. Default: Ascending.</param>
/// <returns>The requested verspakket page.</returns>
[HttpGet]
[ProducesResponseType(typeof(GetVerspakketten.Response), StatusCodes.Status200OK)]
public async Task<GetVerspakketten.Response> Get(
int skip = 0,
int take = 50,
VerspakketSortField sortField = VerspakketSortField.Naam,
SortDirection sortDirection = SortDirection.Ascending)
{
/// <summary>
/// Gets a page of verspakketten.
/// </summary>
/// <param name="skip">The number of items to skip. Default: 0.</param>
/// <param name="take">The maximum number of items to return. Default: 50.</param>
/// <param name="sortField">The field to sort by: Naam, PrijsInCenten, AverageCijferSmaak, or AverageCijferBereiden. Default: Naam.</param>
/// <param name="sortDirection">The sort direction: Ascending or Descending. Default: Ascending.</param>
/// <returns>The requested verspakket page.</returns>
[HttpGet]
[ProducesResponseType(typeof(GetVerspakketten.Response), StatusCodes.Status200OK)]
public async Task<GetVerspakketten.Response> Get(
int skip = 0,
int take = 50,
VerspakketSortField sortField = VerspakketSortField.Naam,
SortDirection sortDirection = SortDirection.Ascending)
return await mediator.SendQueryAsync<GetVerspakketten.Query, GetVerspakketten.Response>(
new GetVerspakketten.Query(skip, take, sortField, sortDirection));
}
/// <summary>
/// Gets a specific verspakket by ID.
/// </summary>
/// <param name="id">The verspakket ID.</param>
/// <returns>Returns 200 OK with the verspakket when found, or 404 Not Found when not found.</returns>
[HttpGet("{id:guid}")]
[ProducesResponseType(typeof(GetVerspakket.Response), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<GetVerspakket.Response?>> GetById(Guid id)
{
var result = await mediator.SendQueryAsync<GetVerspakket.Query, GetVerspakket.Response?>(new GetVerspakket.Query(id));
if (result?.Verspakket == null)
{
return await mediator.SendQueryAsync<GetVerspakketten.Query, GetVerspakketten.Response>(
new GetVerspakketten.Query(skip, take, sortField, sortDirection));
return NotFound();
}
/// <summary>
/// Gets a specific verspakket by ID.
/// </summary>
/// <param name="id">The verspakket ID.</param>
/// <returns>Returns 200 OK with the verspakket when found, or 404 Not Found when not found.</returns>
[HttpGet("{id:guid}")]
[ProducesResponseType(typeof(GetVerspakket.Response), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<GetVerspakket.Response?>> GetById(Guid id)
{
var result = await mediator.SendQueryAsync<GetVerspakket.Query, GetVerspakket.Response?>(new GetVerspakket.Query(id));
if (result?.Verspakket == null)
{
return NotFound();
}
return Ok(result);
}
return Ok(result);
/// <summary>
/// Creates a new verspakket.
/// </summary>
/// <param name="request">The verspakket values to create.</param>
/// <returns>Returns 201 Created with the created verspakket identifier.</returns>
[HttpPost]
[ProducesResponseType(typeof(CreateVerspakket.Response), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<CreateVerspakket.Response>> Post([FromBody] CreateVerspakketRequest request)
{
try
{
var beoordeling = request.Beoordeling is null
? null
: new Application.Models.Verspakketten.Beoordeling
{
CijferSmaak = request.Beoordeling.CijferSmaak,
CijferBereiden = request.Beoordeling.CijferBereiden,
Aanbevolen = request.Beoordeling.Aanbevolen,
Tekst = request.Beoordeling.Tekst
};
var fotos = request.Fotos?
.Select(f => new Application.Models.Verspakketten.VerspakketFoto(f.Base64Data, f.IsMainImage))
.ToList();
var command = new CreateVerspakket.Command(
request.Naam,
request.PrijsInCenten,
request.AantalPersonen,
request.SupermarktId,
beoordeling,
fotos);
var result = await mediator.SendCommandAsync<CreateVerspakket.Command, CreateVerspakket.Response>(command);
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
}
/// <summary>
/// Creates a new verspakket.
/// </summary>
/// <param name="command">The verspakket values to create.</param>
/// <returns>Returns 201 Created with the created verspakket identifier.</returns>
[HttpPost]
[ProducesResponseType(typeof(CreateVerspakket.Response), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<CreateVerspakket.Response>> Post([FromBody] CreateVerspakket.Command command)
catch (InvalidOperationException ex)
{
try
{
var result = await mediator.SendCommandAsync<CreateVerspakket.Command, CreateVerspakket.Response>(command);
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
}
catch (InvalidOperationException ex)
{
return BadRequest(ex.Message);
}
return BadRequest(ex.Message);
}
}
/// <summary>
/// Updates an existing verspakket with the provided values.
/// </summary>
/// <param name="id">The verspakket identifier.</param>
/// <param name="request">The updated verspakket values.</param>
/// <returns>
/// Returns 204 No Content when the update succeeds.
/// Returns 404 Not Found when the specified verspakket does not exist.
/// </returns>
[HttpPut("{id:guid}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateVerspakketRequest request)
/// <summary>
/// Updates an existing verspakket with the provided values.
/// </summary>
/// <param name="id">The verspakket identifier.</param>
/// <param name="request">The updated verspakket values.</param>
/// <returns>
/// Returns 204 No Content when the update succeeds.
/// Returns 404 Not Found when the specified verspakket does not exist.
/// </returns>
[HttpPut("{id:guid}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateVerspakketRequest request)
{
try
{
try
{
var command = new UpdateVerspakket.Command(id, request.Naam, request.PrijsInCenten, request.AantalPersonen, request.SupermarktId);
await mediator.SendCommandAsync<UpdateVerspakket.Command, UpdateVerspakket.Response>(command);
return NoContent();
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
catch (InvalidOperationException ex) when (ex.Message.StartsWith($"Verspakket with id '{id}'"))
{
return NotFound();
}
catch (InvalidOperationException ex)
{
return BadRequest(ex.Message);
}
var fotos = request.Fotos?
.Select(f => new Application.Models.Verspakketten.VerspakketFoto(f.Base64Data, f.IsMainImage))
.ToList();
var command = new UpdateVerspakket.Command(id, request.Naam, request.PrijsInCenten, request.AantalPersonen, request.SupermarktId, fotos);
await mediator.SendCommandAsync<UpdateVerspakket.Command, UpdateVerspakket.Response>(command);
return NoContent();
}
/// <summary>
/// Adds a beoordeling to an existing verspakket.
/// </summary>
/// <param name="id">The verspakket ID.</param>
/// <param name="request">The beoordeling values to add.</param>
/// <returns>Returns 201 Created with the created beoordeling identifier.</returns>
[HttpPost("{id:guid}/beoordelingen")]
[ProducesResponseType(typeof(AddBeoordeling.Response), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<AddBeoordeling.Response>> AddBeoordeling(Guid id, [FromBody] AddBeoordelingRequest request)
catch (ArgumentException ex)
{
try
{
var command = new AddBeoordeling.Command(id, request.CijferSmaak, request.CijferBereiden, request.Aanbevolen, request.Tekst);
var result = await mediator.SendCommandAsync<AddBeoordeling.Command, AddBeoordeling.Response>(command);
return BadRequest(ex.Message);
}
catch (InvalidOperationException ex) when (ex.Message.StartsWith($"Verspakket with id '{id}'"))
{
return NotFound();
}
catch (InvalidOperationException ex)
{
return BadRequest(ex.Message);
}
}
return CreatedAtAction(nameof(GetById), new { id }, result);
}
catch (InvalidOperationException ex)
{
return BadRequest(ex.Message);
}
/// <summary>
/// Adds a beoordeling to an existing verspakket.
/// </summary>
/// <param name="id">The verspakket ID.</param>
/// <param name="request">The beoordeling values to add.</param>
/// <returns>Returns 201 Created with the created beoordeling identifier.</returns>
[HttpPost("{id:guid}/beoordelingen")]
[ProducesResponseType(typeof(AddBeoordeling.Response), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<AddBeoordeling.Response>> AddBeoordeling(Guid id, [FromBody] AddBeoordelingRequest request)
{
try
{
var command = new AddBeoordeling.Command(id, request.CijferSmaak, request.CijferBereiden, request.Aanbevolen, request.Tekst);
var result = await mediator.SendCommandAsync<AddBeoordeling.Command, AddBeoordeling.Response>(command);
return CreatedAtAction(nameof(GetById), new { id }, result);
}
catch (InvalidOperationException ex)
{
return BadRequest(ex.Message);
}
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
# This stage is used when running from VS in fast mode (Default for Debug configuration)
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080
@@ -9,7 +9,7 @@ EXPOSE 8081
# This stage is used to build the service project
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["Lutra.API/Lutra.API.csproj", "Lutra.API/"]
+3 -3
View File
@@ -12,13 +12,13 @@
<ItemGroup>
<PackageReference Include="Cortex.Mediator" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.6">
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
<PackageReference Include="Scalar.AspNetCore" Version="2.14.0" />
<PackageReference Include="Scalar.AspNetCore" Version="2.14.4" />
<ProjectReference Include="..\Lutra.Infrastructure\Lutra.Infrastructure.Sql.csproj" />
</ItemGroup>
@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace Lutra.API.Requests;
/// <summary>
/// Represents the data required to create a verspakket.
/// </summary>
public sealed record CreateVerspakketRequest(
[Required, MaxLength(50)] string Naam,
[Range(0, int.MaxValue)] int? PrijsInCenten,
[Range(1, 10)] int AantalPersonen,
[Required] Guid SupermarktId,
AddBeoordelingRequest? Beoordeling = null,
IReadOnlyList<VerspakketFotoRequest>? Fotos = null);
@@ -9,4 +9,7 @@ public sealed record UpdateVerspakketRequest(
[Required, MaxLength(50)] string Naam,
[Range(0, int.MaxValue)] int PrijsInCenten,
[Range(1, 10)] int AantalPersonen,
[Required] Guid SupermarktId);
[Required] Guid SupermarktId,
IReadOnlyList<VerspakketFotoRequest>? Fotos = null);
@@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;
namespace Lutra.API.Requests;
/// <summary>
/// Represents a foto in a request, encoded as base64.
/// </summary>
public sealed record VerspakketFotoRequest(
[Required] string Base64Data,
bool IsMainImage);
@@ -1,4 +1,7 @@
{
"ConnectionStrings": {
"LutraDb": "Host=localhost;Database=lutra;Username=postgres;Password=<set-locally>"
},
"Logging": {
"LogLevel": {
"Default": "Information",
+1 -1
View File
@@ -6,7 +6,7 @@
}
},
"ConnectionStrings": {
"LutraDb": "Host=db.m91.nl;Username=user;Password=password;Database=Lutra"
"LutraDb": ""
},
"AllowedHosts": "*"
}