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:
@@ -394,6 +394,9 @@ public class KioskController : Controller
|
|||||||
var session = await LoadSessionAsync(token);
|
var session = await LoadSessionAsync(token);
|
||||||
if (session == null) return View("KioskError", "Session not found.");
|
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
|
// Require signature for in-person sessions
|
||||||
if (session.SessionType == KioskSessionType.InPerson &&
|
if (session.SessionType == KioskSessionType.InPerson &&
|
||||||
string.IsNullOrEmpty(dto.SignatureDataBase64))
|
string.IsNullOrEmpty(dto.SignatureDataBase64))
|
||||||
@@ -423,8 +426,9 @@ public class KioskController : Controller
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error processing kiosk submission for session {SessionToken}", token);
|
_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
|
// Customer-facing page always succeeds — staff can convert the session manually.
|
||||||
await _unitOfWork.CompleteAsync();
|
// 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 });
|
return RedirectToAction(nameof(Confirmation), new { token });
|
||||||
@@ -589,9 +593,14 @@ public class KioskController : Controller
|
|||||||
// 3. Create Job in Pending status with Normal priority
|
// 3. Create Job in Pending status with Normal priority
|
||||||
var statuses = await _lookupCache.GetJobStatusLookupsAsync(companyId);
|
var statuses = await _lookupCache.GetJobStatusLookupsAsync(companyId);
|
||||||
var pendingStatus = statuses.FirstOrDefault(s => s.StatusCode == AppConstants.StatusCodes.Job.Pending);
|
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 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 jobNumber = await GenerateJobNumberAsync(companyId);
|
||||||
var job = new Job
|
var job = new Job
|
||||||
@@ -599,29 +608,27 @@ public class KioskController : Controller
|
|||||||
CompanyId = companyId,
|
CompanyId = companyId,
|
||||||
CustomerId = customer!.Id,
|
CustomerId = customer!.Id,
|
||||||
JobNumber = jobNumber,
|
JobNumber = jobNumber,
|
||||||
JobStatusId = pendingStatus?.Id ?? 1,
|
JobStatusId = pendingStatus.Id,
|
||||||
JobPriorityId = normalPriority?.Id ?? 1,
|
JobPriorityId = normalPriority.Id,
|
||||||
SpecialInstructions = session.JobDescription,
|
Description = session.JobDescription,
|
||||||
Description = $"Walk-in intake — {session.CustomerFirstName} {session.CustomerLastName}".Trim()
|
SpecialInstructions = $"Source: {session.SessionType} kiosk intake"
|
||||||
};
|
};
|
||||||
|
|
||||||
await _unitOfWork.Jobs.AddAsync(job);
|
await _unitOfWork.Jobs.AddAsync(job);
|
||||||
|
|
||||||
// 4. Update session links
|
// 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.LinkedCustomerId = customer.Id;
|
||||||
session.LinkedJobId = job.Id; // will be populated after SaveChanges below
|
|
||||||
|
|
||||||
await _unitOfWork.CompleteAsync();
|
|
||||||
|
|
||||||
// job.Id is now set — update session again if needed
|
|
||||||
if (session.LinkedJobId == 0)
|
|
||||||
{
|
|
||||||
session.LinkedJobId = job.Id;
|
session.LinkedJobId = job.Id;
|
||||||
await _unitOfWork.CompleteAsync();
|
await _unitOfWork.CompleteAsync();
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Fire staff notification
|
// 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();
|
var fullName = $"{session.CustomerFirstName} {session.CustomerLastName}".Trim();
|
||||||
await _inApp.CreateAsync(
|
await _inApp.CreateAsync(
|
||||||
companyId,
|
companyId,
|
||||||
|
|||||||
Reference in New Issue
Block a user