using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Newtonsoft.Json; namespace Yavsc.Models.Billing; /// /// One captured signature, attached to a single /// . Multiple versions are allowed per /// (EstimateId, Type, SignerId) tuple — the controller reads /// the most recent when asked. The wire-format payload is the /// same int[] shape PostIt produces (see /// PostIt.Models.SignaturePadData): a length-prefixed /// sequence of strokes, each stroke being /// [k, x0, y0, x1, y1, ...] with x, y ∈ [0, /// CoordinateMax]. /// /// /// Why a separate table (instead of a JSON column on /// ): the jalon 1 spec calls for at least /// two distinct signatures per estimate cycle (provider /// validation + client agreement) and the audit value of /// preserving superseded versions. A dedicated table also keeps /// the row narrow, which matters for /// list views. /// /// 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; } /// /// The ApplicationUser.Id of the signer. Always /// matches Estimate.OwnerId when /// is , and /// Estimate.ClientId when /// is . /// 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). /// [Required] public string SignerId { get; set; } [ForeignKey(nameof(SignerId)), JsonIgnore] public virtual ApplicationUser Signer { get; set; } public SignatureType Type { get; set; } /// /// The normalised coordinate upper bound used at capture /// time. Today always /// PostIt.Models.SignaturePadData.CoordinateMax /// (10_000). Stored so a future change to the wire format /// can be replayed against old signatures without data /// loss. /// public int CoordinateMax { get; set; } /// /// Wire-format payload. PostgreSQL stores an int[] /// natively via Npgsql; the column is round-tripped through /// JsonConvert only if the migration binds it as /// text for backwards compatibility (see the EF /// configuration in ApplicationDbContext). /// [Required] public int[] Strokes { get; set; } = Array.Empty(); public DateTime CapturedAtUtc { get; set; } /// /// Path to the JSON-serialised wire payload on disk, /// relative to UserFilesDirName. The disk copy is the /// source of truth for the wire bytes; the /// column is a denormalised index for queries. They are /// written together in the same transaction by the /// controller; the migration should keep them in sync /// through ApplicationDbContext.SaveChanges. /// [Required] public string FilePath { get; set; } }