namespace SeaHaven.Services.Helpers { /// /// SH-116 client-confirmed media contract (2026-09-22): photos up to 10 MB (JPEG/PNG/HEIC), /// videos up to 100 MB and 90 seconds (MP4/MOV), at most 10 photos and 3 videos per work /// order, across the dispatcher media modal, the completion-doc media tab and the vendor /// portal upload. Documents (PDF/DOC/DOCX) keep the 50 MB document cap. Duration is only /// checkable client-side (the server cannot probe it cheaply), so it is enforced in the /// browser and documented here as part of the same contract. /// public static class WorkOrderMediaContract { public const long MaxPhotoBytes = 10_000_000; public const long MaxVideoBytes = 100_000_000; public const long MaxDocumentBytes = 50_000_000; public const int MaxPhotosPerWorkOrder = 10; public const int MaxVideosPerWorkOrder = 3; public const int MaxVideoDurationSeconds = 90; /// /// Request-level ceiling for media endpoints (RequestSizeLimit / multipart limit). It sits /// above plus multipart overhead so an oversize file reaches the /// per-kind check and gets its generic message instead of a framework 413. The EB nginx /// proxy (.platform/nginx/conf.d/01_upload_body_size.conf) must stay above this value. /// public const long MaxUploadRequestBytes = 110_000_000; public enum UploadKind { Photo, Video, Document, Unknown } public static UploadKind ResolveKind(string? contentType, string? fileName) { var type = (contentType ?? string.Empty).Trim(); var extension = Path.GetExtension(fileName ?? string.Empty); if (type.StartsWith("video/", StringComparison.OrdinalIgnoreCase) || extension.Equals(".mp4", StringComparison.OrdinalIgnoreCase) || extension.Equals(".mov", StringComparison.OrdinalIgnoreCase)) return UploadKind.Video; if (type.StartsWith("image/", StringComparison.OrdinalIgnoreCase) || extension.Equals(".jpg", StringComparison.OrdinalIgnoreCase) || extension.Equals(".jpeg", StringComparison.OrdinalIgnoreCase) || extension.Equals(".png", StringComparison.OrdinalIgnoreCase) || extension.Equals(".heic", StringComparison.OrdinalIgnoreCase)) return UploadKind.Photo; if (type.Equals("application/pdf", StringComparison.OrdinalIgnoreCase) || type.Equals("application/msword", StringComparison.OrdinalIgnoreCase) || type.Equals( "application/vnd.openxmlformats-officedocument.wordprocessingml.document", StringComparison.OrdinalIgnoreCase) || extension.Equals(".pdf", StringComparison.OrdinalIgnoreCase) || extension.Equals(".doc", StringComparison.OrdinalIgnoreCase) || extension.Equals(".docx", StringComparison.OrdinalIgnoreCase)) return UploadKind.Document; return UploadKind.Unknown; } /// Stable, generic per-kind size message; never echoes file metadata. public static string? ValidateSize(long length, UploadKind kind) { return kind switch { UploadKind.Photo when length > MaxPhotoBytes => "Photos must be 10 MB or smaller.", UploadKind.Video when length > MaxVideoBytes => "Videos must be 100 MB or smaller.", UploadKind.Document when length > MaxDocumentBytes => "Documents must be 50 MB or smaller.", UploadKind.Unknown when length > MaxVideoBytes => "Videos must be 100 MB or smaller.", _ => null }; } public static string? ValidateSize(string? contentType, string? fileName, long length) => ValidateSize(length, ResolveKind(contentType, fileName)); } }