Skip to content

fix(calendar): resolve PlanningCaseSite by SDK case id, not by date - #1158

Merged
renemadsen merged 1 commit into
stablefrom
fix/1156-planningcasesite-lookup-by-sdk-case-id
Sep 3, 2026
Merged

fix(calendar): resolve PlanningCaseSite by SDK case id, not by date#1158
renemadsen merged 1 commit into
stablefrom
fix/1156-planningcasesite-lookup-by-sdk-case-id

Conversation

@renemadsen

Copy link
Copy Markdown
Member

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.

Update and UpdateFromCalendar located the occurrence's PlanningCaseSite with a heuristic carrying 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:

  • PnBase.Create stamps CreatedAt = UtcNow, so every back-filled PlanningCaseSite shares the day the backfill ran.
  • CalendarPastSeriesBackfillService pins planning.LastExecutedTime to today, which EventDeployService stamps into every Compliance.StartDate. (Deadline is per-occurrence; StartDate is not.)

Both sides collapse to one value → the predicate matches every sibling → FirstOrDefaultAsync returns the same row every time → the if (planningCase.Status != 100) guard skips the promotion for completion #2 onward.

1st completion 2nd completion
SDK case → Status 100 case A, DoneAt 07.01 case B, DoneAt 14.01
PlanningCaseSite resolved PCS-A PCS-A again
PlanningCase A: 66 → 100 guard false → untouched; B stays 66

The report filters PlanningCases on Status == 100, so it sees one row. The calendar reads sdkCase.Status, hence "done in the calendar, missing from the report".

Change

Match on MicrotingSdkCaseId instead — as EventsGrpcService (mobile) and eFormCompletedHandler (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 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.

Out of scope

Verification

Plugin-only. No -base change, no migration.

  • dotnet build BackendConfiguration.Pn.csproj -c Debug: 0 errors, 95 warnings — all pre-existing CS8632/CS8618 in untouched files, none in the edited file.
  • Reviewed by subagent: predicate correctness in both methods, the WorkflowState filter (byte-identical to the gRPC reference), MicrotingSdkCaseId uniqueness at both creation sites, and no ChangeTracker poisoning all came back clean.
  • The two doc comments in ComplianceCompletionLegacyPathsTests.cs described the now-deleted heuristic; updated to match. No test code, assertion, or seed value changed — SeedPlanningCaseSiteAsync already seeds MicrotingSdkCaseId, 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

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
Copilot AI lite review requested due to automatic review settings September 3, 2026 07:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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 Update and UpdateFromCalendar to locate PlanningCaseSite by MicrotingSdkCaseId == foundCase.Id (with WorkflowState filtering), rather than CreatedAt.Date == compliance.StartDate.Date.
  • Add an explicit failure path when no matching PlanningCaseSite is 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 == null check is redundant here: x.WorkflowState != Removed already includes null values (null != "Removed" evaluates true). You can simplify the query and combine predicates into a single FirstOrDefaultAsync for readability and to avoid an extra Where call.
                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 PlanningCaseSite mapping. Returning an accurate message would make failures diagnosable for users/support (and distinguish it from the earlier foundCase == null case).
                    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.

Comment on lines +355 to +358
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"));
}
Comment on lines +320 to +321
// Matches the mobile path (EventsGrpcService.cs:1703-1707) and the
// scheduler path (eFormCompletedHandler.cs:62-63).
Comment on lines 322 to +324
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);
@renemadsen
renemadsen merged commit a84858a into stable Sep 3, 2026
32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants