yavsc/src/Yavsc.Server/Models/Billing/Signature.cs

77 lines
2.8 KiB
C#
Raw Normal View History

feat(api): POST /api/bill/estimate/{id}/sign — JSON signature capture Adds a new JSON-bodied signature endpoint as a sibling of the legacy PNG-based prosign/clisign routes. The legacy flow stays intact: the TeX invoice templates (Bill_tex.cshtml, Estimate_tex.cshtml) still consume the sign-{billingCode}-{id}.png files the old endpoints write, and the new endpoint writes to a distinct /signatures/ tree under UserFilesDirName. A future migration commit will regenerate PNGs from the JSON payload and decommission the PNG flow. Scope - New Signature entity (Yavsc.Server/Models/Billing/Signature.cs) with FK to Estimate, FK to ApplicationUser (Signer), Type (Pro/Client) enum, CoordinateMax (default 10_000), int[] Strokes (native Npgsql mapping), CapturedAtUtc, FilePath. Multiple versions per (EstimateId, Type) are allowed; the controller reads the most recent. - New Estimate.Signatures nav collection (InverseProperty) so the composite index covers both sides of the relation. - New DbSet<Signature> Signatures + composite index (EstimateId, Type, CapturedAtUtc DESC) in ApplicationDbContext OnModelCreating. DeleteBehavior.Cascade on Estimate deletion cleans up signatures automatically. - New EstimateSignatureFileHelper (Server/Helpers) with ReceiveEstimateSignatureAsync(user, estimateId, type, payload). Writes a yavsc.signature/v1 JSON envelope to UserFilesDirName/{user}/signatures/sign-{type}-{estimateId}-{ticks}.json. Quota update lives in the controller, not the helper, because the helper has no DbContext access. - New endpoint POST /api/bill/estimate/{id:long}/sign on BillingController. Authz is body-driven (the bearer token is the PostIt OAuth client, not the end user, so signerUserId is in the JSON body, validated against Estimate.OwnerId/ClientId). Returns 201 Created with the new Signature's metadata. Plumbing - SignatureSubmission (body type) lives next to BillingController in the same file — small enough to keep colocated. - The legacy prosign/clisign routes are untouched. They keep the IFormFile PNG contract; the new endpoint is the JSON counterpart. Tests - New EstimateSignatureFileHelperTests in Yavsc.Org.Tests (8 tests, all green): filename format incl. lowercase type and ticks, envelope v1 round-trip (parsed via JsonDocument, not text matching), null payload rejected, non-positive estimateId rejected. Disk side effects are isolated to a per-test temp root via AbstractFileSystemHelpers.UserFilesDirName. - Yavsc.Org.Tests full suite: 29/29 green. - PostIt.Tests: 57/57 green (untouched by this commit). - Builds: Yavsc.Server, Yavsc.Api, Yavsc.Org, Yavsc.Org.Tests all compile clean. Out of scope - EF migration: the Signatures table doesn't exist in the database yet. The migration is intentionally a separate commit so the generated SQL can be reviewed against the composite index and the int[] column type before it touches any prod database. Until the migration lands, the new endpoint will 500 on SaveChanges; the [DEV] button in PostIt is the only call site, so this is acceptable. - SignalR handler that opens the signature page on a 'devis received' push — commit 4.
2026-07-04 15:47:55 +01:00
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Newtonsoft.Json;
namespace Yavsc.Models.Billing;
/// <summary>
/// One captured signature, attached to a single
/// <see cref="Estimate"/>. Multiple versions are allowed per
/// (EstimateId, Type, SignerId) tuple — the controller reads the
/// most recent when asked. The wire-format payload is the
feat(api): POST /api/bill/estimate/{id}/sign — JSON signature capture Adds a new JSON-bodied signature endpoint as a sibling of the legacy PNG-based prosign/clisign routes. The legacy flow stays intact: the TeX invoice templates (Bill_tex.cshtml, Estimate_tex.cshtml) still consume the sign-{billingCode}-{id}.png files the old endpoints write, and the new endpoint writes to a distinct /signatures/ tree under UserFilesDirName. A future migration commit will regenerate PNGs from the JSON payload and decommission the PNG flow. Scope - New Signature entity (Yavsc.Server/Models/Billing/Signature.cs) with FK to Estimate, FK to ApplicationUser (Signer), Type (Pro/Client) enum, CoordinateMax (default 10_000), int[] Strokes (native Npgsql mapping), CapturedAtUtc, FilePath. Multiple versions per (EstimateId, Type) are allowed; the controller reads the most recent. - New Estimate.Signatures nav collection (InverseProperty) so the composite index covers both sides of the relation. - New DbSet<Signature> Signatures + composite index (EstimateId, Type, CapturedAtUtc DESC) in ApplicationDbContext OnModelCreating. DeleteBehavior.Cascade on Estimate deletion cleans up signatures automatically. - New EstimateSignatureFileHelper (Server/Helpers) with ReceiveEstimateSignatureAsync(user, estimateId, type, payload). Writes a yavsc.signature/v1 JSON envelope to UserFilesDirName/{user}/signatures/sign-{type}-{estimateId}-{ticks}.json. Quota update lives in the controller, not the helper, because the helper has no DbContext access. - New endpoint POST /api/bill/estimate/{id:long}/sign on BillingController. Authz is body-driven (the bearer token is the PostIt OAuth client, not the end user, so signerUserId is in the JSON body, validated against Estimate.OwnerId/ClientId). Returns 201 Created with the new Signature's metadata. Plumbing - SignatureSubmission (body type) lives next to BillingController in the same file — small enough to keep colocated. - The legacy prosign/clisign routes are untouched. They keep the IFormFile PNG contract; the new endpoint is the JSON counterpart. Tests - New EstimateSignatureFileHelperTests in Yavsc.Org.Tests (8 tests, all green): filename format incl. lowercase type and ticks, envelope v1 round-trip (parsed via JsonDocument, not text matching), null payload rejected, non-positive estimateId rejected. Disk side effects are isolated to a per-test temp root via AbstractFileSystemHelpers.UserFilesDirName. - Yavsc.Org.Tests full suite: 29/29 green. - PostIt.Tests: 57/57 green (untouched by this commit). - Builds: Yavsc.Server, Yavsc.Api, Yavsc.Org, Yavsc.Org.Tests all compile clean. Out of scope - EF migration: the Signatures table doesn't exist in the database yet. The migration is intentionally a separate commit so the generated SQL can be reviewed against the composite index and the int[] column type before it touches any prod database. Until the migration lands, the new endpoint will 500 on SaveChanges; the [DEV] button in PostIt is the only call site, so this is acceptable. - SignalR handler that opens the signature page on a 'devis received' push — commit 4.
2026-07-04 15:47:55 +01:00
/// same <c>int[]</c> shape PostIt produces (see
/// <c>PostIt.Models.SignaturePadData</c>): a length-prefixed
/// sequence of strokes, each stroke being
/// <c>[k, x0, y0, x1, y1, ...]</c> with <c>x, y ∈ [0,
/// CoordinateMax]</c>.
///
/// <para>
/// The <see cref="Strokes"/> column is the source of truth: the
/// signature is captured and stored entirely in-app (PostIt) and
/// round-tripped as JSON, with no on-disk copy. A dedicated table
/// (instead of a JSON column on <see cref="Estimate"/>) keeps the
/// <see cref="Estimate"/> row narrow for list views and preserves
/// superseded versions for audit.
feat(api): POST /api/bill/estimate/{id}/sign — JSON signature capture Adds a new JSON-bodied signature endpoint as a sibling of the legacy PNG-based prosign/clisign routes. The legacy flow stays intact: the TeX invoice templates (Bill_tex.cshtml, Estimate_tex.cshtml) still consume the sign-{billingCode}-{id}.png files the old endpoints write, and the new endpoint writes to a distinct /signatures/ tree under UserFilesDirName. A future migration commit will regenerate PNGs from the JSON payload and decommission the PNG flow. Scope - New Signature entity (Yavsc.Server/Models/Billing/Signature.cs) with FK to Estimate, FK to ApplicationUser (Signer), Type (Pro/Client) enum, CoordinateMax (default 10_000), int[] Strokes (native Npgsql mapping), CapturedAtUtc, FilePath. Multiple versions per (EstimateId, Type) are allowed; the controller reads the most recent. - New Estimate.Signatures nav collection (InverseProperty) so the composite index covers both sides of the relation. - New DbSet<Signature> Signatures + composite index (EstimateId, Type, CapturedAtUtc DESC) in ApplicationDbContext OnModelCreating. DeleteBehavior.Cascade on Estimate deletion cleans up signatures automatically. - New EstimateSignatureFileHelper (Server/Helpers) with ReceiveEstimateSignatureAsync(user, estimateId, type, payload). Writes a yavsc.signature/v1 JSON envelope to UserFilesDirName/{user}/signatures/sign-{type}-{estimateId}-{ticks}.json. Quota update lives in the controller, not the helper, because the helper has no DbContext access. - New endpoint POST /api/bill/estimate/{id:long}/sign on BillingController. Authz is body-driven (the bearer token is the PostIt OAuth client, not the end user, so signerUserId is in the JSON body, validated against Estimate.OwnerId/ClientId). Returns 201 Created with the new Signature's metadata. Plumbing - SignatureSubmission (body type) lives next to BillingController in the same file — small enough to keep colocated. - The legacy prosign/clisign routes are untouched. They keep the IFormFile PNG contract; the new endpoint is the JSON counterpart. Tests - New EstimateSignatureFileHelperTests in Yavsc.Org.Tests (8 tests, all green): filename format incl. lowercase type and ticks, envelope v1 round-trip (parsed via JsonDocument, not text matching), null payload rejected, non-positive estimateId rejected. Disk side effects are isolated to a per-test temp root via AbstractFileSystemHelpers.UserFilesDirName. - Yavsc.Org.Tests full suite: 29/29 green. - PostIt.Tests: 57/57 green (untouched by this commit). - Builds: Yavsc.Server, Yavsc.Api, Yavsc.Org, Yavsc.Org.Tests all compile clean. Out of scope - EF migration: the Signatures table doesn't exist in the database yet. The migration is intentionally a separate commit so the generated SQL can be reviewed against the composite index and the int[] column type before it touches any prod database. Until the migration lands, the new endpoint will 500 on SaveChanges; the [DEV] button in PostIt is the only call site, so this is acceptable. - SignalR handler that opens the signature page on a 'devis received' push — commit 4.
2026-07-04 15:47:55 +01:00
/// </para>
/// </summary>
public class Signature
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
public long EstimateId { get; set; }
[ForeignKey(nameof(EstimateId)), JsonIgnore]
public virtual Estimate Estimate { get; set; }
/// <summary>
/// The <c>ApplicationUser.Id</c> of the signer. Always
/// matches <c>Estimate.OwnerId</c> when
/// <see cref="Type"/> is <see cref="SignatureType.Pro"/>, and
/// <c>Estimate.ClientId</c> when
/// <see cref="Type"/> is <see cref="SignatureType.Client"/>.
/// The authz layer enforces this invariant; we don't
/// duplicate the constraint in the schema to keep the model
/// honest if a future business rule relaxes it (e.g. proxy
/// signing).
/// </summary>
[Required]
public string SignerId { get; set; }
[ForeignKey(nameof(SignerId)), JsonIgnore]
public virtual ApplicationUser Signer { get; set; }
public SignatureType Type { get; set; }
/// <summary>
/// The normalised coordinate upper bound used at capture
/// time. Today always
/// <c>PostIt.Models.SignaturePadData.CoordinateMax</c>
/// (10_000). Stored so a future change to the wire format
/// can be replayed against old signatures without data
/// loss.
/// </summary>
public int CoordinateMax { get; set; }
/// <summary>
/// Wire-format payload. PostgreSQL stores an <c>int[]</c>
/// natively via Npgsql; the column is round-tripped through
/// <c>JsonConvert</c> only if the migration binds it as
/// <c>text</c> for backwards compatibility (see the EF
/// configuration in <c>ApplicationDbContext</c>).
/// </summary>
[Required]
public int[] Strokes { get; set; } = Array.Empty<int>();
public DateTime CapturedAtUtc { get; set; }
}