From 1df7c13abded2c356ced2597d1f9b319ba2ddc22 Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Wed, 13 May 2026 21:53:10 -0400 Subject: [PATCH] Sweep kiosk intake submission for FK/null bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix Jobs.Id FK violation: save job first with CompleteAsync() to get its DB-assigned Id, THEN set session.LinkedJobId and save again. Previously job.Id was still 0 when written to the nullable FK column. - Replace ?? 1 fallbacks on JobStatusId/JobPriorityId with explicit InvalidOperationException — hardcoded 1 may not exist in the company's lookup tables; now fails loud with a clear message instead of an FK error. - Add ValidateSessionState check to Terms POST so expired/already-submitted sessions don't re-run ProcessSubmissionAsync and create duplicate jobs. - Null-guard session.JobDescription before slicing for notification snippet. - Tighten catch block: wrap the fallback CompleteAsync in its own try/catch so a secondary failure doesn't mask the original error in logs. - Swap Job.Description / SpecialInstructions: Description now holds the actual job description text; SpecialInstructions records the intake source. Co-Authored-By: Claude Sonnet 4.6 --- .../Controllers/KioskController.cs | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/PowderCoating.Web/Controllers/KioskController.cs b/src/PowderCoating.Web/Controllers/KioskController.cs index 5e8938e..2e12dc6 100644 --- a/src/PowderCoating.Web/Controllers/KioskController.cs +++ b/src/PowderCoating.Web/Controllers/KioskController.cs @@ -394,6 +394,9 @@ public class KioskController : Controller var session = await LoadSessionAsync(token); if (session == null) return View("KioskError", "Session not found."); + // Expired/already-submitted sessions go straight to Confirmation + if (!await ValidateSessionState(session)) return RedirectToAction(nameof(Confirmation), new { token }); + // Require signature for in-person sessions if (session.SessionType == KioskSessionType.InPerson && string.IsNullOrEmpty(dto.SignatureDataBase64)) @@ -423,8 +426,9 @@ public class KioskController : Controller catch (Exception ex) { _logger.LogError(ex, "Error processing kiosk submission for session {SessionToken}", token); - // Don't fail the customer-facing page — save what we have and let staff convert manually - await _unitOfWork.CompleteAsync(); + // Customer-facing page always succeeds — staff can convert the session manually. + // Persist the session's agreed/submitted state even if job creation failed. + try { await _unitOfWork.CompleteAsync(); } catch { /* best-effort */ } } return RedirectToAction(nameof(Confirmation), new { token }); @@ -589,9 +593,14 @@ public class KioskController : Controller // 3. Create Job in Pending status with Normal priority var statuses = await _lookupCache.GetJobStatusLookupsAsync(companyId); var pendingStatus = statuses.FirstOrDefault(s => s.StatusCode == AppConstants.StatusCodes.Job.Pending); + if (pendingStatus == null) + throw new InvalidOperationException($"No Pending job status found for company {companyId}. Run Seed Data from Platform Management."); var priorities = await _lookupCache.GetJobPriorityLookupsAsync(companyId); - var normalPriority = priorities.FirstOrDefault(p => p.PriorityCode == "NORMAL") ?? priorities.FirstOrDefault(); + var normalPriority = priorities.FirstOrDefault(p => p.PriorityCode == "NORMAL") + ?? priorities.FirstOrDefault(); + if (normalPriority == null) + throw new InvalidOperationException($"No job priority rows found for company {companyId}. Run Seed Data from Platform Management."); var jobNumber = await GenerateJobNumberAsync(companyId); var job = new Job @@ -599,29 +608,27 @@ public class KioskController : Controller CompanyId = companyId, CustomerId = customer!.Id, JobNumber = jobNumber, - JobStatusId = pendingStatus?.Id ?? 1, - JobPriorityId = normalPriority?.Id ?? 1, - SpecialInstructions = session.JobDescription, - Description = $"Walk-in intake — {session.CustomerFirstName} {session.CustomerLastName}".Trim() + JobStatusId = pendingStatus.Id, + JobPriorityId = normalPriority.Id, + Description = session.JobDescription, + SpecialInstructions = $"Source: {session.SessionType} kiosk intake" }; await _unitOfWork.Jobs.AddAsync(job); - // 4. Update session links - session.LinkedCustomerId = customer.Id; - session.LinkedJobId = job.Id; // will be populated after SaveChanges below + // Save the job first so EF generates its Id, then link the session. + // Setting session.LinkedJobId = job.Id before CompleteAsync would write 0 + // to the FK column because the DB hasn't assigned the Id yet. + await _unitOfWork.CompleteAsync(); // job.Id is now valid + // 4. Update session links now that both Ids exist + session.LinkedCustomerId = customer.Id; + session.LinkedJobId = job.Id; await _unitOfWork.CompleteAsync(); - // job.Id is now set — update session again if needed - if (session.LinkedJobId == 0) - { - session.LinkedJobId = job.Id; - await _unitOfWork.CompleteAsync(); - } - // 5. Fire staff notification - var snippet = session.JobDescription.Length > 60 ? session.JobDescription[..60] + "…" : session.JobDescription; + var jobDesc = session.JobDescription ?? ""; + var snippet = jobDesc.Length > 60 ? jobDesc[..60] + "…" : jobDesc; var fullName = $"{session.CustomerFirstName} {session.CustomerLastName}".Trim(); await _inApp.CreateAsync( companyId,