Fix lualatex stdin PDF gen + unit test for signed estimate PDF

GenerateEstimatePdf passed /dev/stdin as the input filename so lualatex
treats stdin as a regular input file instead of the terminal — without
it, lualatex emergency-stops after the first line of a multi-line
document (the real root cause of the production "Emergency stop", not
bera/luaotfload which were separate host-package gaps already fixed).

EstimatePdf endpoint calls GenerateEstimatePdf directly and streams the
returned FileInfo, guarding on fo.Exists. QueryAcceptanceRequest is
extracted into its own file under ViewModels/FrontOffice.

Add EstimatePdfGenerationTests: unit tests for SignatureToTikz (vector
draws of both signatures) and GenerateEstimatePdf on a devis signed by
both parties — asserts the PDF exists at the expected DestDir path, is
non-empty, and that only the PDF persists (aux/log cleaned). Skips
cleanly when lualatex is absent.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Paul Schneider 2026-09-22 18:33:47 +01:00
commit 35d784fdf1
Signed by: notazof
GPG key ID: AD4B9B545FBD7D12
4 changed files with 242 additions and 41 deletions

View file

@ -140,7 +140,7 @@ namespace Yavsc.ApiControllers
// we stream the generated PDF bytes — but driving the
// generation through the view keeps the template as the
// output producer, per the original design.
if (model.GenerateEstimatePdf() is FileInfo fo)
if (model.GenerateEstimatePdf() is FileInfo fo && fo.Exists)
{
return File(fo.OpenRead(), "application/pdf");
}
@ -370,42 +370,3 @@ namespace Yavsc.ApiControllers
internal enum ActorRole { None, Provider, Client, Admin }
}
namespace Yavsc.ViewModels.FrontOffice
{
/// <summary>
/// Body of <c>POST /api/v1/front/query/accept</c> and
/// <c>.../reject</c>. The <see cref="Strokes"/> field is optional:
/// when present (non-empty) on accept, it is the PostIt wire-format
/// signature captured with the acceptance. All fields are optional
/// so a caller may post <c>{}</c> to accept/reject without a
/// signature.
/// </summary>
public class QueryAcceptanceRequest
{
/// <summary>
/// Wire-format strokes. See
/// <c>PostIt.Models.SignaturePadData</c>. Null or empty means
/// no signature.
/// </summary>
public int[]? Strokes { get; set; }
public int CoordinateMax { get; set; } = 10_000;
public DateTime? CapturedAtUtc { get; set; }
/// <summary>
/// Which signature is being submitted:
/// <see cref="SignatureType.Pro"/> (the provider) or
/// <see cref="SignatureType.Client"/>. When <see cref="Strokes"/>
/// is present this declares the side the caller is signing as;
/// the server honors it only if the caller is that party
/// (<c>query.PerformerId</c> for Pro, <c>query.ClientId</c>
/// for Client). This disambiguates the case where one user is
/// both parties, and makes "write only for its author"
/// explicit. When null, the server falls back to the role
/// inferred from the caller's identity.
/// </summary>
public SignatureType? SignatureType { get; set; }
}
}