Update
ci-cd-nas / build-push (push) Failing after 27s
ci-cd-nas / deploy (push) Has been skipped

This commit is contained in:
Chris Chen
2026-06-20 22:26:52 -07:00
parent 7ab8e9703b
commit ddced87dc6
12 changed files with 655 additions and 52 deletions
@@ -85,4 +85,28 @@ public class OfferingEntryController : ControllerBase
await _hub.Clients.Group(result.SessionDate).SendAsync("LineAdded", result);
return Ok(result);
}
// ── Paper-proof PDF for the date's session (merged client-side) ──────────
// Date-keyed so the anonymous page (which has no session id) can attach the
// count sheet / envelope photos. Mirrors OfferingSessionsController's proof
// validation; the desktop session page reviews/deletes the result.
[HttpGet("proof")]
public async Task<IActionResult> GetProof([FromQuery] DateOnly date)
{
var result = await _sessions.OpenProofForDateAsync(date);
if (result is null) return NoContent(); // no session/proof yet — client merges nothing
return File(result.Value.stream, result.Value.contentType);
}
[HttpPost("proof")]
[RequestSizeLimit(52_428_800)] // 50 MB — a merged multi-image PDF is larger than one receipt
public async Task<IActionResult> UploadProof([FromForm] DateOnly date, IFormFile file)
{
if (file is null || file.Length == 0) return BadRequest(new { message = "No file." });
if (file.ContentType != "application/pdf") return BadRequest(new { message = "Proof must be a PDF." });
await using var stream = file.OpenReadStream();
await _sessions.SaveProofForDateAsync(date, stream, file.FileName);
return NoContent();
}
}
@@ -10,5 +10,6 @@ public class OfferingEntrySummaryDto
public string? Status { get; set; } // null when no session yet
public decimal SystemTotal { get; set; }
public int LineCount { get; set; }
public bool HasProof { get; set; } // a merged paper-proof PDF is attached to this session
public List<OfferingGivingLineDto> Lines { get; set; } = [];
}
@@ -21,4 +21,8 @@ public interface IOfferingSessionService
Task SaveProofAsync(int id, Stream content, string fileName);
Task<(Stream stream, string contentType)?> OpenProofAsync(int id);
Task DeleteProofAsync(int id);
// Date-keyed variants for the anonymous mobile page (knows the date, not the id).
Task SaveProofForDateAsync(DateOnly date, Stream content, string fileName);
Task<(Stream stream, string contentType)?> OpenProofForDateAsync(DateOnly date);
}
@@ -183,6 +183,7 @@ public class OfferingSessionService : IOfferingSessionService
Status = session.Status,
SystemTotal = session.SystemTotal,
LineCount = lines.Count,
HasProof = session.ProofPdfPath != null,
Lines = await BuildLineDtosAsync(lines),
};
}
@@ -339,6 +340,39 @@ public class OfferingSessionService : IOfferingSessionService
await _db.SaveChangesAsync();
}
// ── Date-keyed proof (anonymous mobile page knows the date, not the id) ──
// Attach a proof PDF to the date's session, creating a Draft session if none
// exists yet (the volunteer may snap the count sheet before any line is keyed).
public async Task SaveProofForDateAsync(DateOnly date, Stream content, string fileName)
{
var session = await _db.OfferingSessions.FirstOrDefaultAsync(s => s.SessionDate == date);
if (session is null)
{
session = new OfferingSession { SessionDate = date, Status = "Draft" };
_db.OfferingSessions.Add(session);
try
{
await _db.SaveChangesAsync();
}
catch (DbUpdateException)
{
// Lost the create race — another phone inserted today's session first.
_db.ChangeTracker.Clear();
session = await _db.OfferingSessions.FirstAsync(s => s.SessionDate == date);
}
}
await SaveProofAsync(session.Id, content, fileName);
}
public async Task<(Stream stream, string contentType)?> OpenProofForDateAsync(DateOnly date)
{
var session = await _db.OfferingSessions.AsNoTracking()
.FirstOrDefaultAsync(s => s.SessionDate == date);
if (session is null) return null;
return await OpenProofAsync(session.Id);
}
private static Giving MapLine(OfferingGivingLineRequest line, DateOnly sessionDate) => new()
{
MemberId = line.IsAnonymous ? null : line.MemberId,