From a315fbf890aa9984571bf5dad8e105605d512888 Mon Sep 17 00:00:00 2001 From: TekH Date: Thu, 16 Jul 2026 15:48:09 +0200 Subject: [PATCH] feat: Add raw parameter to SwissQrCodeController endpoints - Add 'raw' query parameter to both ExtractFromFile and ExtractFromBase64 methods - Returns raw QR text lines when raw=true, parsed Bill object when raw=false (default) - Remove obsolete 'references' parameter (not part of QR extraction logic) - Add XML documentation for raw parameter - Update ExceptionHandlingMiddleware to handle BadRequestException --- .../Controllers/SwissQrCodeController.cs | 30 +++++------ .../Middleware/ExceptionHandlingMiddleware.cs | 54 ++++++------------- 2 files changed, 27 insertions(+), 57 deletions(-) diff --git a/DocumentOperator.API/Controllers/SwissQrCodeController.cs b/DocumentOperator.API/Controllers/SwissQrCodeController.cs index 43a4630..dee4adc 100644 --- a/DocumentOperator.API/Controllers/SwissQrCodeController.cs +++ b/DocumentOperator.API/Controllers/SwissQrCodeController.cs @@ -17,9 +17,9 @@ public class SwissQrCodeController(IMediator Mediator) : ControllerBase /// Extracts Swiss QR Code from the last page of a PDF document (multipart/form-data) /// /// PDF file containing Swiss QR Code - /// Optional references (comma-separated) + /// If true, returns raw QR code text lines as string array; if false, returns parsed Bill object (default: false) /// Cancellation token - /// Swiss QR Code data (IBAN, amount, creditor, debtor, reference, etc.) + /// Swiss QR Code data (IBAN, amount, creditor, debtor, reference, etc.) or raw text lines based on 'raw' parameter /// Swiss QR Code extracted successfully /// Invalid PDF or file format /// No Swiss QR Code found on the last page @@ -32,46 +32,39 @@ public class SwissQrCodeController(IMediator Mediator) : ControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] public async Task ExtractFromFile( IFormFile file, - [FromForm] string? references, - CancellationToken cancellationToken) + [FromQuery] bool raw = false, + CancellationToken cancellationToken = default) { - if (file == null || file.Length == 0) - { + if (file.Length == 0) return BadRequest(new ProblemDetails { Title = "Invalid file", Detail = "File is required and cannot be empty", Status = StatusCodes.Status400BadRequest }); - } // Convert IFormFile to byte array using var memoryStream = new MemoryStream(); await file.CopyToAsync(memoryStream, cancellationToken); byte[] pdfBytes = memoryStream.ToArray(); - // Parse references (comma-separated or empty) - var referencesList = string.IsNullOrWhiteSpace(references) - ? new List() - : references.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList(); - // Direct pass-through to MediatR var query = new ExtractSwissQrCodeQuery { - PdfBytes = pdfBytes, - References = referencesList + PdfBytes = pdfBytes }; var result = await Mediator.Send(query, cancellationToken); - return Ok(result); + return Ok(raw ? result.RawLines : result.Bill); } /// /// Extracts Swiss QR Code from the last page of a PDF document (Base64 JSON) /// /// References array + PDF as Base64 string + /// If true, returns raw QR code text lines as string array; if false, returns parsed Bill object (default: false) /// Cancellation token - /// Swiss QR Code data (IBAN, amount, creditor, debtor, reference, etc.) + /// Swiss QR Code data (IBAN, amount, creditor, debtor, reference, etc.) or raw text lines based on 'raw' parameter /// Swiss QR Code extracted successfully /// Invalid PDF or Base64 format /// No Swiss QR Code found on the last page @@ -84,11 +77,12 @@ public class SwissQrCodeController(IMediator Mediator) : ControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] public async Task ExtractFromBase64( [FromBody] ExtractSwissQrCodeQuery query, - CancellationToken cancellationToken) + [FromQuery] bool raw = false, + CancellationToken cancellationToken = default) { // Direct pass-through to MediatR var result = await Mediator.Send(query, cancellationToken); - return Ok(result); + return Ok(raw ? result.RawLines : result.Bill); } } diff --git a/DocumentOperator.API/Middleware/ExceptionHandlingMiddleware.cs b/DocumentOperator.API/Middleware/ExceptionHandlingMiddleware.cs index f6171d0..24e1ce8 100644 --- a/DocumentOperator.API/Middleware/ExceptionHandlingMiddleware.cs +++ b/DocumentOperator.API/Middleware/ExceptionHandlingMiddleware.cs @@ -11,21 +11,16 @@ namespace DocumentOperator.API.Middleware; /// Central exception handling middleware /// Maps exceptions to HTTP status codes and RFC 7807 Problem Details /// -public class ExceptionHandlingMiddleware +/// +/// Initializes a new instance of the class. +/// +/// The next middleware in the pipeline. +public class ExceptionHandlingMiddleware(RequestDelegate Next) { - private readonly RequestDelegate _next; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// The next middleware in the pipeline. - /// The logger instance for exception logging. - public ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger) + private static readonly JsonSerializerOptions ProbDetailsJsonOpt = new() { - _next = next; - _logger = logger; - } + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; /// /// Invokes the middleware to handle incoming HTTP requests and catch exceptions. @@ -35,11 +30,10 @@ public class ExceptionHandlingMiddleware { try { - await _next(context); + await Next(context); } catch (Exception ex) { - _logger.LogError(ex, "Unhandled exception: {ExceptionMessage}", ex.Message); await HandleExceptionAsync(context, ex); } } @@ -51,12 +45,7 @@ public class ExceptionHandlingMiddleware context.Response.StatusCode = (int)statusCode; context.Response.ContentType = "application/problem+json"; - var options = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - - await context.Response.WriteAsync(JsonSerializer.Serialize(problemDetails, options)); + await context.Response.WriteAsync(JsonSerializer.Serialize(problemDetails, ProbDetailsJsonOpt)); } private static (HttpStatusCode StatusCode, ProblemDetails ProblemDetails) MapExceptionToProblemDetails( @@ -78,15 +67,15 @@ public class ExceptionHandlingMiddleware } ), - // Domain Validation Exception (400 Bad Request) - DomainValidationException domainEx => ( + // Not Found Exception (404 Not Found) + BadRequestException badReqEx => ( HttpStatusCode.BadRequest, new ProblemDetails { - Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1", - Title = "Domain Validation Error", + Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4", + Title = "Bad Request", Status = (int)HttpStatusCode.BadRequest, - Detail = domainEx.Message, + Detail = badReqEx.Message, Instance = context.Request.Path } ), @@ -117,19 +106,6 @@ public class ExceptionHandlingMiddleware } ), - // PDF Processing Exception (500 Internal Server Error) - PdfProcessingException pdfEx => ( - HttpStatusCode.InternalServerError, - new ProblemDetails - { - Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1", - Title = "PDF Processing Error", - Status = (int)HttpStatusCode.InternalServerError, - Detail = pdfEx.Message, - Instance = context.Request.Path - } - ), - // Generic Exception (500 Internal Server Error) _ => ( HttpStatusCode.InternalServerError,