6080946e74
Implement IPayee1099Service and Payee1099Service: list/get/create/update/ soft-delete and RevealTin. TIN is encrypted via ITinProtector on write; TinLast4 is the only clear-text fragment stored. Null Tin on update preserves the existing ciphertext. Four xUnit tests cover encrypt-on-create, null-tin-keeps-ciphertext, list-masks-to-last4, and soft-delete hides from list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
129 lines
4.6 KiB
C#
129 lines
4.6 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using ROLAC.API.Data;
|
|
using ROLAC.API.DTOs.Payee;
|
|
using ROLAC.API.Entities;
|
|
using ROLAC.API.Services.Security;
|
|
|
|
namespace ROLAC.API.Services;
|
|
|
|
public class Payee1099Service : IPayee1099Service
|
|
{
|
|
private readonly AppDbContext _db;
|
|
private readonly ITinProtector _tin;
|
|
|
|
public Payee1099Service(AppDbContext db, ITinProtector tin)
|
|
{
|
|
_db = db;
|
|
_tin = tin;
|
|
}
|
|
|
|
public async Task<List<Payee1099ListItemDto>> 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<Payee1099Dto?> 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<int> 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<string?> RevealTinAsync(int id)
|
|
{
|
|
var p = await _db.Payee1099s.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id);
|
|
return p?.TinEncrypted is null ? null : _tin.Unprotect(p.TinEncrypted);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
}
|