Sweep kiosk intake submission for FK/null bugs

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-05-13 21:53:10 -04:00
parent 4a8778504f
commit 1df7c13abd
@@ -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,