using System.ComponentModel.DataAnnotations; using LudosData.Api.Domain; namespace LudosData.Api.Contracts; /// A page of results plus the totals the paginator needs. public record PagedResult(IReadOnlyList Items, int Page, int PageSize, int Total) { public int TotalPages => PageSize > 0 ? (int)Math.Ceiling(Total / (double)PageSize) : 0; } /// /// A game as returned to the client. Art is the stored filename; ArtUrl /// is the ready-to-use URL built server-side, so the client never has to /// string-concatenate upload paths the way the old grid did. /// public record GameResponse( int Id, string Title, string? System, string? Genre, string? Year, string? Developer, string? Publisher, string? Art, string? ArtUrl, string? Description, bool Own, bool Dumped, bool Played, bool Finished, int? Rating, string? Notes, GameCondition Condition, GameRegion Region, decimal? PurchasePrice, DateOnly? PurchaseDate, decimal? MarketValue, DateTimeOffset? MarketValueUpdatedAt, string? MarketValueSource, decimal? ValueLoose, decimal? ValueCib, decimal? ValueNew, DateTimeOffset CreatedAt, DateTimeOffset UpdatedAt); /// /// Create/update payload. Deliberately has no Id and no OwnerId — the route supplies /// the former and the JWT the latter, so neither can be spoofed by the client. /// public record GameRequest { [Required(AllowEmptyStrings = false), MaxLength(200)] public string Title { get; init; } = string.Empty; [MaxLength(50)] public string? System { get; init; } [MaxLength(50)] public string? Genre { get; init; } [MaxLength(50)] public string? Year { get; init; } [MaxLength(100)] public string? Developer { get; init; } [MaxLength(100)] public string? Publisher { get; init; } [MaxLength(200)] public string? Art { get; init; } [MaxLength(10_000)] public string? Description { get; init; } public bool Own { get; init; } public bool Dumped { get; init; } public bool Played { get; init; } public bool Finished { get; init; } [Range(1, 10)] public int? Rating { get; init; } [MaxLength(10_000)] public string? Notes { get; init; } public GameCondition Condition { get; init; } = GameCondition.Unspecified; public GameRegion Region { get; init; } = GameRegion.Unspecified; [Range(0, 1_000_000)] public decimal? PurchasePrice { get; init; } public DateOnly? PurchaseDate { get; init; } /// /// Current estimated resale value. Accepted here so a figure can be entered /// by hand; a price feed will later write the same field, stamping /// MarketValueUpdatedAt and MarketValueSource as it goes. /// [Range(0, 1_000_000)] public decimal? MarketValue { get; init; } [MaxLength(100)] public string? MarketValueSource { get; init; } [Range(0, 1_000_000)] public decimal? ValueLoose { get; init; } [Range(0, 1_000_000)] public decimal? ValueCib { get; init; } [Range(0, 1_000_000)] public decimal? ValueNew { get; init; } } /// Query string for the library list, bound from [FromQuery]. public record GameQuery { /// Free-text match against title, developer and publisher. public string? Search { get; init; } public string? System { get; init; } public string? Genre { get; init; } public bool? Own { get; init; } public bool? Dumped { get; init; } public bool? Played { get; init; } public bool? Finished { get; init; } public GameCondition? Condition { get; init; } public GameRegion? Region { get; init; } /// Lowest personal score to include. Unrated games are excluded when set. [Range(1, 10)] public int? MinRating { get; init; } /// Restrict to games that do, or do not, have a market value recorded. public bool? HasValue { get; init; } [Range(1, int.MaxValue)] public int Page { get; init; } = 1; /// Capped at 100 to keep a hostile or buggy client from asking for everything. [Range(1, 100)] public int PageSize { get; init; } = 20; /// /// One of: title, system, genre, year, developer, publisher, rating, /// value, price, purchased, created, updated. /// public string Sort { get; init; } = "title"; /// "asc" or "desc". public string Dir { get; init; } = "asc"; } /// Distinct values present in the user's library, for filter dropdowns. public record FacetsResponse(IReadOnlyList Systems, IReadOnlyList Genres); public record UploadResponse(string FileName, string Url);