using Microsoft.EntityFrameworkCore; using ROLAC.API.Data; using ROLAC.API.DTOs.Payee; using ROLAC.API.Entities; using ROLAC.API.Services.Security; using ROLAC.API.Services.Storage; namespace ROLAC.API.Services; public class Payee1099Service : IPayee1099Service { private readonly AppDbContext _db; private readonly ITinProtector _tin; private readonly IFileStorage _storage; public Payee1099Service(AppDbContext db, ITinProtector tin, IFileStorage storage) { _db = db; _tin = tin; _storage = storage; } public async Task> GetAllAsync(bool includeInactive) { var q = _db.Payee1099s.AsNoTracking().Include(p => p.Member).AsQueryable(); if (!includeInactive) q = q.Where(p => p.IsActive); return await q.OrderBy(p => p.LegalName).Select(p => new Payee1099ListItemDto { Id = p.Id, LegalName = p.LegalName, DisplayName = p.DisplayName, MemberId = p.MemberId, MemberName = p.Member != null ? p.Member.FirstName_en + " " + p.Member.LastName_en : null, TaxClassification = p.TaxClassification, Is1099Tracked = p.Is1099Tracked, TinType = p.TinType, TinLast4 = p.TinLast4, W9Status = p.W9Status, IsActive = p.IsActive, }).ToListAsync(); } public async Task GetByIdAsync(int id) { var p = await _db.Payee1099s.AsNoTracking().Include(x => x.Member).FirstOrDefaultAsync(x => x.Id == id); if (p is null) return null; return new Payee1099Dto { Id = p.Id, LegalName = p.LegalName, DisplayName = p.DisplayName, MemberId = p.MemberId, MemberName = p.Member != null ? $"{p.Member.FirstName_en} {p.Member.LastName_en}" : null, TaxClassification = p.TaxClassification, Is1099Tracked = p.Is1099Tracked, TinType = p.TinType, TinLast4 = p.TinLast4, W9Status = p.W9Status, IsActive = p.IsActive, AddressLine1 = p.AddressLine1, AddressLine2 = p.AddressLine2, City = p.City, State = p.State, Zip = p.Zip, Email = p.Email, Phone = p.Phone, W9ReceivedDate = p.W9ReceivedDate?.ToString("yyyy-MM-dd"), HasW9Document = p.W9BlobPath != null, Notes = p.Notes, }; } public async Task CreateAsync(SavePayee1099Request r) { var p = new Payee1099(); Apply(p, r); _db.Payee1099s.Add(p); await _db.SaveChangesAsync(); return p.Id; } public async Task UpdateAsync(int id, SavePayee1099Request r) { var p = await _db.Payee1099s.FirstOrDefaultAsync(x => x.Id == id) ?? throw new KeyNotFoundException($"Payee1099 {id} not found."); Apply(p, r); await _db.SaveChangesAsync(); } public async Task DeleteAsync(int id) { var p = await _db.Payee1099s.FirstOrDefaultAsync(x => x.Id == id) ?? throw new KeyNotFoundException($"Payee1099 {id} not found."); p.IsDeleted = true; p.DeletedAt = DateTimeOffset.UtcNow; await _db.SaveChangesAsync(); } public async Task RevealTinAsync(int id) { var p = await _db.Payee1099s.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id); return p?.TinEncrypted is null ? null : _tin.Unprotect(p.TinEncrypted); } public async Task SaveW9Async(int id, Stream content, string fileName) { var p = await _db.Payee1099s.FirstOrDefaultAsync(x => x.Id == id) ?? throw new KeyNotFoundException($"Payee1099 {id} not found."); // Mirror the expense-receipt blob convention: a stable per-record path under a feature folder, // preserving the original extension. Re-uploads overwrite the prior blob. var ext = Path.GetExtension(fileName); var path = $"finance/w9/{p.Id}{ext}"; if (p.W9BlobPath != null && p.W9BlobPath != path) await _storage.DeleteAsync(p.W9BlobPath); var saved = await _storage.SaveAsync(content, path); p.W9BlobPath = saved; await _db.SaveChangesAsync(); } public async Task<(Stream stream, string contentType)?> OpenW9Async(int id) { var p = await _db.Payee1099s.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id); if (p?.W9BlobPath is null) return null; var stream = await _storage.OpenReadAsync(p.W9BlobPath); if (stream is null) return null; var ext = Path.GetExtension(p.W9BlobPath).ToLowerInvariant(); var contentType = ext switch { ".png" => "image/png", ".webp" => "image/webp", ".pdf" => "application/pdf", _ => "image/jpeg", }; return (stream, contentType); } // Maps request fields onto the entity. A null/blank Tin leaves the existing ciphertext untouched (update case). private void Apply(Payee1099 p, SavePayee1099Request r) { p.LegalName = r.LegalName; p.DisplayName = r.DisplayName; p.MemberId = r.MemberId; p.TaxClassification = r.TaxClassification; p.Is1099Tracked = r.Is1099Tracked; p.TinType = r.TinType; p.AddressLine1 = r.AddressLine1; p.AddressLine2 = r.AddressLine2; p.City = r.City; p.State = r.State; p.Zip = r.Zip; p.Email = r.Email; p.Phone = r.Phone; p.W9Status = r.W9Status; p.W9ReceivedDate = r.W9ReceivedDate; p.IsActive = r.IsActive; p.Notes = r.Notes; if (!string.IsNullOrWhiteSpace(r.Tin)) { p.TinEncrypted = _tin.Protect(r.Tin); p.TinLast4 = TinProtector.Last4(r.Tin); } } }