feat(1099): wire W-9 document upload/view for recipients

Adds POST/GET payee-1099/{id}/w9, mirroring the expense-receipt upload:
IFileStorage saves to finance/w9/{id}{ext}, content-type derived from the
blob extension. Frontend dialog (edit mode) gains a W-9 file input and an
auth-correct blob "View W-9" link. Payee1099Service ctor now takes
IFileStorage; tests updated with an in-memory FakeStorage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Chris Chen
2026-06-25 18:11:11 -07:00
parent ad276c01f3
commit d29de83116
7 changed files with 182 additions and 4 deletions
@@ -10,12 +10,24 @@ using ROLAC.API.Entities;
using ROLAC.API.Services;
using ROLAC.API.Services.Logging;
using ROLAC.API.Services.Security;
using ROLAC.API.Services.Storage;
using Xunit;
namespace ROLAC.API.Tests.Services;
public class Payee1099ServiceTests
{
// Minimal in-memory IFileStorage (mirrors the ExpenseServiceTests fake).
private sealed class FakeStorage : IFileStorage
{
public Dictionary<string, byte[]> Files = new();
public Task<string> SaveAsync(Stream c, string p, CancellationToken ct = default)
{ using var ms = new MemoryStream(); c.CopyTo(ms); Files[p] = ms.ToArray(); return Task.FromResult(p); }
public Task<Stream?> OpenReadAsync(string p, CancellationToken ct = default)
=> Task.FromResult<Stream?>(Files.TryGetValue(p, out var b) ? new MemoryStream(b) : null);
public Task DeleteAsync(string p, CancellationToken ct = default) { Files.Remove(p); return Task.CompletedTask; }
}
private static (Payee1099Service svc, AppDbContext db) Build()
{
var httpContext = new DefaultHttpContext { User = new(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "test-user") })) };
@@ -25,7 +37,7 @@ public class Payee1099ServiceTests
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.AddInterceptors(new AuditSaveChangesInterceptor(new CurrentUserAccessor(accessorMock.Object))).Options);
var tin = new TinProtector(DataProtectionProvider.Create("ROLAC.Tests"));
return (new Payee1099Service(db, tin), db);
return (new Payee1099Service(db, tin, new FakeStorage()), db);
}
[Fact]
@@ -76,4 +88,25 @@ public class Payee1099ServiceTests
await svc.DeleteAsync(id);
Assert.Empty(await svc.GetAllAsync(includeInactive: true));
}
[Fact]
public async Task SaveW9_records_document_and_round_trips_bytes()
{
var (svc, _) = Build();
var id = await svc.CreateAsync(new SavePayee1099Request { LegalName = "W9 Payee" });
var bytes = new byte[] { 1, 2, 3, 4, 5 };
await svc.SaveW9Async(id, new MemoryStream(bytes), "w9.pdf");
var dto = await svc.GetByIdAsync(id);
Assert.NotNull(dto);
Assert.True(dto!.HasW9Document);
var opened = await svc.OpenW9Async(id);
Assert.NotNull(opened);
Assert.Equal("application/pdf", opened!.Value.contentType);
using var ms = new MemoryStream();
await opened.Value.stream.CopyToAsync(ms);
Assert.Equal(bytes, ms.ToArray());
}
}