fix(calendar): resolve PlanningCaseSite by SDK case id, not by date - #1158
Conversation
Update and UpdateFromCalendar located the occurrence's PlanningCaseSite
with a heuristic that carried no occurrence key:
x.CreatedAt.Date == compliance.StartDate.Date
&& x.PlanningId == compliance.PlanningId
That only holds while a planning deploys at most one occurrence per day.
The past-series backfill breaks it on both sides of the comparison:
PnBase.Create stamps CreatedAt = UtcNow, so every back-filled
PlanningCaseSite shares the day the backfill ran, and
CalendarPastSeriesBackfillService pins planning.LastExecutedTime to today,
which EventDeployService then stamps into every Compliance.StartDate.
(Compliance.Deadline is per-occurrence; StartDate is not.)
Both sides collapse to one value, so the predicate matches every sibling
and FirstOrDefaultAsync returns the same row for every completion. The
following `if (planningCase.Status != 100)` guard then skips the promotion
for completion #2 onward, so only the first completed occurrence ever
reached Status 100 -- and the Logbook report, which filters PlanningCases
on Status == 100, showed a single row. The calendar still showed every
occurrence green because it reads sdkCase.Status, which is why the symptom
was "done in the calendar, missing from the report".
Match on MicrotingSdkCaseId instead, as the mobile path
(EventsGrpcService) and the scheduler (eFormCompletedHandler) already do;
the web path was the only outlier.
No fallback to the old heuristic: it is the bug, and if it ever fired it
would silently pick a wrong row and re-corrupt the linkage. A non-match
now logs and returns a failure instead of being skipped in silence, which
is what the previous `if (planningCaseSite != null)` with no else did.
The Status != 100 guard is deliberately kept -- with the correct row it
suppresses nothing, and it still preserves the original DoneAt/DoneBy on
re-completion, matching the gRPC path.
No data repair for rows already corrupted by this: out of scope, see the
issue. The pre-existing partial-write hazard on the failure path (the
compliance row is already deleted by the time this returns) is tracked
separately in #1157.
Refs #1156
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sXLtgzZU8QL9m84GqMkoJ
There was a problem hiding this comment.
🟢 Approval recommended
The change is narrowly scoped, aligns the web completion path with existing SDK-case-id matching used elsewhere, and directly addresses the reported report-missing-occurrence defect.
Pull request overview
This PR fixes a correctness bug in the backend calendar completion flow by resolving the per-occurrence PlanningCaseSite using the SDK case id (MicrotingSdkCaseId) instead of a date-based heuristic that can collapse multiple occurrences onto the same row (notably for back-filled past series), ensuring each completed occurrence promotes its own PlanningCase to Status = 100 so it appears in the Logbøger report.
Changes:
- Update
UpdateandUpdateFromCalendarto locatePlanningCaseSitebyMicrotingSdkCaseId == foundCase.Id(withWorkflowStatefiltering), rather thanCreatedAt.Date == compliance.StartDate.Date. - Add an explicit failure path when no matching
PlanningCaseSiteis found (instead of silently skipping promotion). - Update integration test doc comments to reflect the new lookup key.
File summaries
| File | Description |
|---|---|
| eFormAPI/Plugins/BackendConfiguration.Pn/BackendConfiguration.Pn/Services/BackendConfigurationCompliancesService/BackendConfigurationCompliancesService.cs | Fixes occurrence-to-PlanningCaseSite resolution by matching on SDK case id in both calendar completion paths and makes non-matches explicit failures. |
| eFormAPI/Plugins/BackendConfiguration.Pn/BackendConfiguration.Pn.Integration.Test/ComplianceCompletionLegacyPathsTests.cs | Updates test documentation comments to match the new lookup strategy (no behavioral test changes). |
Review details
Suppressed comments (3)
eFormAPI/Plugins/BackendConfiguration.Pn/BackendConfiguration.Pn/Services/BackendConfigurationCompliancesService/BackendConfigurationCompliancesService.cs:519
- The inline comment references other code paths using hard-coded line ranges (e.g.,
EventsGrpcService.cs:1703-1707). These line numbers will drift as files change, making the reference misleading. Prefer referencing the file (and ideally the method/class) without specific line numbers.
// Matches the mobile path (EventsGrpcService.cs:1703-1707) and the
// scheduler path (eFormCompletedHandler.cs:62-63).
eFormAPI/Plugins/BackendConfiguration.Pn/BackendConfiguration.Pn/Services/BackendConfigurationCompliancesService/BackendConfigurationCompliancesService.cs:522
- The
WorkflowState == nullcheck is redundant here:x.WorkflowState != Removedalready includes null values (null != "Removed" evaluates true). You can simplify the query and combine predicates into a singleFirstOrDefaultAsyncfor readability and to avoid an extraWherecall.
var planningCaseSite = await _itemsPlanningPnDbContext.PlanningCaseSites
.Where(x => x.WorkflowState != Constants.WorkflowStates.Removed || x.WorkflowState == null)
.FirstOrDefaultAsync(x => x.MicrotingSdkCaseId == foundCase.Id).ConfigureAwait(false);
eFormAPI/Plugins/BackendConfiguration.Pn/BackendConfiguration.Pn/Services/BackendConfigurationCompliancesService/BackendConfigurationCompliancesService.cs:556
- This branch returns the localized "CaseNotFound" message, but at this point the SDK case was found; what is missing is the
PlanningCaseSitemapping. Returning an accurate message would make failures diagnosable for users/support (and distinguish it from the earlierfoundCase == nullcase).
Log.LogException(
$"[ERROR] BackendConfigurationCompliancesService.UpdateFromCalendar: no PlanningCaseSite found for MicrotingSdkCaseId {foundCase.Id} (complianceId: {compliance.Id}, planningId: {compliance.PlanningId})");
return new OperationResult(false, _localizationService.GetString("CaseNotFound"));
}
- Files reviewed: 2/2 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| Log.LogException( | ||
| $"[ERROR] BackendConfigurationCompliancesService.Update: no PlanningCaseSite found for MicrotingSdkCaseId {foundCase.Id} (complianceId: {compliance.Id}, planningId: {compliance.PlanningId})"); | ||
| return new OperationResult(false, _localizationService.GetString("CaseNotFound")); | ||
| } |
| // Matches the mobile path (EventsGrpcService.cs:1703-1707) and the | ||
| // scheduler path (eFormCompletedHandler.cs:62-63). |
| var planningCaseSite = await _itemsPlanningPnDbContext.PlanningCaseSites | ||
| .FirstOrDefaultAsync(x => x.CreatedAt.Date == compliance.StartDate.Date && x.PlanningId == compliance.PlanningId).ConfigureAwait(false); | ||
| .Where(x => x.WorkflowState != Constants.WorkflowStates.Removed || x.WorkflowState == null) | ||
| .FirstOrDefaultAsync(x => x.MicrotingSdkCaseId == foundCase.Id).ConfigureAwait(false); |
Fixes #1156
Problem
Complete two or more overdue occurrences of a back-filled recurring task from the calendar, then run the Logbøger report over that period: only the first completion appears. The calendar shows them all green.
UpdateandUpdateFromCalendarlocated the occurrence'sPlanningCaseSitewith a heuristic carrying no occurrence key:That only holds while a planning deploys at most one occurrence per day. The past-series backfill breaks it on both sides:
PnBase.CreatestampsCreatedAt = UtcNow, so every back-filledPlanningCaseSiteshares the day the backfill ran.CalendarPastSeriesBackfillServicepinsplanning.LastExecutedTimeto today, whichEventDeployServicestamps into everyCompliance.StartDate. (Deadlineis per-occurrence;StartDateis not.)Both sides collapse to one value → the predicate matches every sibling →
FirstOrDefaultAsyncreturns the same row every time → theif (planningCase.Status != 100)guard skips the promotion for completion #2 onward.The report filters
PlanningCasesonStatus == 100, so it sees one row. The calendar readssdkCase.Status, hence "done in the calendar, missing from the report".Change
Match on
MicrotingSdkCaseIdinstead — asEventsGrpcService(mobile) andeFormCompletedHandler(scheduler) already do. The web path was the only outlier; this is parity, not a new pattern.No fallback to the old heuristic. It is the bug, and if it ever fired it would silently pick a wrong row and re-corrupt the linkage. A non-match now logs and returns a failure rather than being skipped in silence — which is what the previous
if (planningCaseSite != null)with noelsedid.The
Status != 100guard is deliberately kept: with the correct row it suppresses nothing, and it still preserves the originalDoneAt/DoneByon re-completion, matching the gRPC path.Out of scope
-base.Verification
Plugin-only. No
-basechange, no migration.dotnet build BackendConfiguration.Pn.csproj -c Debug: 0 errors, 95 warnings — all pre-existingCS8632/CS8618in untouched files, none in the edited file.WorkflowStatefilter (byte-identical to the gRPC reference),MicrotingSdkCaseIduniqueness at both creation sites, and noChangeTrackerpoisoning all came back clean.ComplianceCompletionLegacyPathsTests.csdescribed the now-deleted heuristic; updated to match. No test code, assertion, or seed value changed —SeedPlanningCaseSiteAsyncalready seedsMicrotingSdkCaseId, so existing tests resolve correctly under the new predicate.Independent of the picture-upload work
This does not depend on microting/eform-angular-frontend#8033 and can merge on its own.
🤖 Generated with Claude Code
https://claude.ai/code/session_015sXLtgzZU8QL9m84GqMkoJ