Skip to content

RG-T117 API CORS fix - #458

Merged
ucswift merged 5 commits into
masterfrom
develop
Aug 11, 2026
Merged

RG-T117 API CORS fix#458
ucswift merged 5 commits into
masterfrom
develop

Conversation

@ucswift

@ucswift ucswift commented Aug 11, 2026

Copy link
Copy Markdown
Member

PR Description

This PR addresses several fixes and enhancements, primarily focused on API CORS handling (RG-T117), along with Redis cache resilience improvements, department deletion cleanup, and minor API/task hardening.

CORS Enhancements (CorsHelper.cs, ApiConfig.cs)

  • Wildcard host support: Configured origins now accept wildcard patterns like *.resgrid.com, which match the apex domain and all subdomains on any scheme/port. This replaces the need to list individual subdomains.
  • Verbatim origin matching for non-standard schemes: Desktop-app origins such as Electron's app://. are now matched verbatim against config entries before URI parsing, since these custom-scheme origins cannot be reliably parsed as absolute URIs. This unblocks Electron-based desktop clients.

Redis Cache Resilience (AzureRedisCacheProvider.cs)

  • Added handling for RedisConnectionException across all cache operations (retrieve, set, remove, increment — both sync and async), so transient connection drops (idle resets, failovers) gracefully fall back to the source data instead of throwing.
  • Set a 30-second keep-alive interval to prevent intermediate idle timeouts from dropping the connection before the default 60s keep-alive fires.

Department Deletion Cleanup (DeleteRepository.cs)

  • Added DepartmentCallPruning to the cascade of tables deleted when a department is removed, preventing orphaned pruning configuration rows.

Call Prune Task Hardening (CallPruneTask.cs)

  • Added a null-department guard to skip orphaned pruning settings for deleted departments, preventing null-reference exceptions and repeated error logging on each run.

Chat API Enhancement (ChatController.cs)

  • Added an optional callId parameter to the GetChannels v4 endpoint, allowing callers (e.g., an incident view) to retrieve only channels attached to a specific call instead of the full department list.

Tests (CorsHelperTests.cs)

  • Added test coverage for wildcard host matching (including negative cases for lookalike domains) and Electron custom-scheme verbatim matching.

Summary by CodeRabbit

  • New Features

    • Added call-specific filtering when viewing chat channels.
    • Department access can now be revoked without deactivating an entire account.
    • Account and department cleanup now removes related tasks, memberships, security data, and associated records.
  • Bug Fixes

    • Improved CORS matching for custom origins and wildcard domains across schemes and ports.
    • Improved Redis resilience during connection failures.
    • Prevented errors when processing orphaned call-pruning settings.
    • Excluded deleted or disabled members from scheduled-task processing.
    • Added a clearer warning to account deletion confirmation.

@Resgrid-Bot

This comment has been minimized.

@request-info

request-info Bot commented Aug 11, 2026

Copy link
Copy Markdown

Thanks for opening this, but we'd appreciate a little more information. Could you update it with more details?

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR separates department access revocation from full account deactivation, expands deletion cleanup, updates CORS and Redis handling, adds call-scoped chat filtering, and skips orphaned call-pruning settings.

Changes

User deprovisioning

Layer / File(s) Summary
Deprovisioning contracts and cleanup services
Core/Resgrid.Model/Repositories/IScheduledTasksRepository.cs, Core/Resgrid.Model/Services/*, Core/Resgrid.Services/DistributionListsService.cs, Core/Resgrid.Services/ScheduledTasksService.cs, Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs
New methods remove distribution-list memberships and scheduled tasks globally or within a department.
Deletion flow and controller integration
Core/Resgrid.Services/DeleteService.cs, Web/Resgrid.Web/Areas/User/Controllers/PersonnelController.cs, Web/Resgrid.Web/Areas/User/Controllers/ProfileController.cs, Web/Resgrid.Web/Areas/User/Views/Account/DeleteAccount.cshtml
Deletion now revokes department access when other memberships remain. Otherwise, it deactivates the account and clears additional profile data. Controllers use the shared deletion service.
Department and identity data cleanup
Repositories/Resgrid.Repositories.DataRepository/DeleteRepository.cs, Repositories/Resgrid.Repositories.DataRepository/IdentityRepository.cs
Department deletion removes dependent records. Identity cleanup removes authentication, chatbot, push, recovery, and OIDC data for PostgreSQL and SQL Server.

CORS origin matching

Layer / File(s) Summary
CORS validation and documentation
Core/Resgrid.Config/ApiConfig.cs, Core/Resgrid.Config/CorsHelper.cs
CORS validation checks exact raw origins before URI parsing and supports wildcard hosts across schemes and ports. Documentation describes the matching rules.

Redis connection handling

Layer / File(s) Summary
Cache operation resilience
Providers/Resgrid.Providers.Cache/AzureRedisCacheProvider.cs
Cache operations log Redis connection failures, preserve fallback behavior, suppress selected timeouts, and use 30-second keep-alives.

Chat channel filtering

Layer / File(s) Summary
Call-scoped channel retrieval
Web/Resgrid.Web.Services/Controllers/v4/ChatController.cs, Web/Resgrid.Web.Services/Resgrid.Web.Services.xml
GetChannels accepts an optional callId parameter and filters channels by CallId when provided. The XML documentation reflects the updated signature.

Call-pruning orphan guard

Layer / File(s) Summary
Missing-department handling
Workers/Resgrid.Workers.Console/Tasks/CallPruneTask.cs, Core/Resgrid.Framework/Logging.cs
ProcessAsync warning-logs orphaned pruning settings and skips them when the department lookup returns null.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PersonnelController
  participant DeleteService
  participant DistributionListsService
  participant ScheduledTasksService
  participant IdentityRepository
  PersonnelController->>DeleteService: DeleteUserAsync
  DeleteService->>DistributionListsService: Remove department memberships
  DeleteService->>ScheduledTasksService: Delete department or global tasks
  DeleteService->>IdentityRepository: Clear account authentication data
Loading

Possibly related PRs

  • Resgrid/Core#457: Also modifies CORS origin matching in CorsHelper and ApiConfig.
  • Resgrid/Core#431: Also modifies AzureRedisCacheProvider for Redis connection reliability.
  • Resgrid/Core#450: Also modifies chat channel behavior and visibility.

Suggested reviewers: github-actions

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary API CORS change described in the pull request objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Providers/Resgrid.Providers.Cache/AzureRedisCacheProvider.cs`:
- Around line 52-56: Replace Logging.LogError(ex) with Logging.LogException(ex)
in every RedisConnectionException and TimeoutException handler in
AzureRedisCacheProvider, including the additional listed handlers. Preserve each
handler’s existing fallback return value.

In `@Workers/Resgrid.Workers.Console/Tasks/CallPruneTask.cs`:
- Line 56: Add a LogWarning method to the Resgrid.Framework.Logging facade, then
update CallPruneTask to call that facade method instead of _logger.LogWarning
while preserving the existing orphaned-department message and interpolation.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8f15c6f6-b78f-4f0a-ad29-aca36bf1212c

📥 Commits

Reviewing files that changed from the base of the PR and between 93a86a0 and 53264e5.

⛔ Files ignored due to path filters (1)
  • Tests/Resgrid.Tests/Config/CorsHelperTests.cs is excluded by !**/Tests/**
📒 Files selected for processing (7)
  • Core/Resgrid.Config/ApiConfig.cs
  • Core/Resgrid.Config/CorsHelper.cs
  • Providers/Resgrid.Providers.Cache/AzureRedisCacheProvider.cs
  • Repositories/Resgrid.Repositories.DataRepository/DeleteRepository.cs
  • Web/Resgrid.Web.Services/Controllers/v4/ChatController.cs
  • Web/Resgrid.Web.Services/Resgrid.Web.Services.xml
  • Workers/Resgrid.Workers.Console/Tasks/CallPruneTask.cs

Comment on lines +52 to +56
catch (RedisConnectionException ex)
{
// Transient connection drop (idle reset, failover); fallback below handles it.
Logging.LogError(ex);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use LogException in the new exception handlers.

The RedisConnectionException handlers call Logging.LogError(ex). The new TimeoutException handlers also discard the exception without logging. Use Logging.LogException(ex) in each handler. Preserve the existing fallback return values.

Proposed fix
-Logging.LogError(ex);
+Logging.LogException(ex);

-catch (TimeoutException)
-{
-}
+catch (TimeoutException ex)
+{
+	Logging.LogException(ex);
+}

As per coding guidelines, caught exceptions must use Resgrid.Framework.Logging.LogException(...) because it captures caller information.

Also applies to: 74-77, 98-103, 143-147, 165-168, 195-198, 226-231, 264-269, 306-311

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Providers/Resgrid.Providers.Cache/AzureRedisCacheProvider.cs` around lines 52
- 56, Replace Logging.LogError(ex) with Logging.LogException(ex) in every
RedisConnectionException and TimeoutException handler in
AzureRedisCacheProvider, including the additional listed handlers. Preserve each
handler’s existing fallback return value.

Source: Coding guidelines

{
// Orphaned pruning row for a deleted department; skip it so we don't
// null-ref in CallPruneLogic or spam error logs every run.
_logger.LogWarning($"CallPrune::Skipping orphaned pruning settings for deleted DepartmentId:{item.PruneSettings.DepartmentId}");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 2 '\bResgrid\.Framework\.Logging\b|\bLogWarning\s*\(' --glob '*.cs'

Repository: Resgrid/Core

Length of output: 150


🏁 Script executed:

#!/bin/bash
set -euo pipefail
file='Workers/Resgrid.Workers.Console/Tasks/CallPruneTask.cs'
printf '%s\n' '--- target file ---'
cat -n "$file" | sed -n '1,110p'
printf '%s\n' '--- logging facade references ---'
rg -n -C 2 'class Logging|static class Logging|LogException|LogError|LogInfo|LogDebug|LogWarning|ILogger' --glob '*.cs' . | sed -n '1,240p'
printf '%s\n' '--- logging-related files ---'
git ls-files '*.cs' | rg -i 'logging|logger|log'

Repository: Resgrid/Core

Length of output: 34818


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Resgrid.Framework.Logging implementation ---'
cat -n Core/Resgrid.Framework/Logging.cs | sed -n '1,240p'
printf '%s\n' '--- worker logging call patterns ---'
rg -n '(_logger|Logging|Framework\.Logging)\.(Log[A-Za-z]+)' Workers --glob '*.cs' | sed -n '1,240p'
printf '%s\n' '--- read-only API check ---'
python3 - <<'PY'
from pathlib import Path
import re

facade = Path("Core/Resgrid.Framework/Logging.cs").read_text(encoding="utf-8-sig")
target = Path("Workers/Resgrid.Workers.Console/Tasks/CallPruneTask.cs").read_text(encoding="utf-8-sig")
methods = sorted(set(re.findall(r'\b(?:public|private|internal|protected)\s+static\s+\w+(?:<[^>]+>)?\s+(Log\w+)\s*\(', facade)))
calls = sorted(set(re.findall(r'\b(?:_logger|Logging|Framework\.Logging)\.(Log\w+)\s*\(', target)))
print("facade static methods:", methods)
print("target logging methods:", calls)
print("target LogWarning facade method exists:", "LogWarning" in methods)
PY

Repository: Resgrid/Core

Length of output: 38951


Add a warning method to Resgrid.Framework.Logging.

The facade has no LogWarning method. Add one and replace _logger.LogWarning(...) at line 56 with it. Preserve the orphaned-department message.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Workers/Resgrid.Workers.Console/Tasks/CallPruneTask.cs` at line 56, Add a
LogWarning method to the Resgrid.Framework.Logging facade, then update
CallPruneTask to call that facade method instead of _logger.LogWarning while
preserving the existing orphaned-department message and interpolation.

Source: Coding guidelines

catch (RedisConnectionException ex)
{
// Transient connection drop (idle reset, failover); fallback below handles it.
Logging.LogError(ex);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Insufficient logging context in the Logging.LogError call, as the method passes only the exception object without the operation name or identifiers like cacheKey, violating Rule 3 for traceable errors. Include context using Logging.LogError("Retrieve cache failed", new { cacheKey, ex }).

Kody rule violation: Include error context in structured logs

Prompt for LLM

File Providers/Resgrid.Providers.Cache/AzureRedisCacheProvider.cs:

Line 55:

Insufficient logging context in the Logging.LogError call, as the method passes only the exception object without the operation name or identifiers like cacheKey, violating Rule 3 for traceable errors. Include context using Logging.LogError("Retrieve cache failed", new { cacheKey, ex }).

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment on lines +226 to +227
catch (TimeoutException)
{ }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Silent exception swallowing in the catch block, where the empty block catches TimeoutException without logging or handling it, violating Rule 28. Log the timeout with operation context using Logging.LogError("GetStringAsync timed out", new { cacheKey }).

Kody rule violation: Avoid empty catch blocks

Prompt for LLM

File Providers/Resgrid.Providers.Cache/AzureRedisCacheProvider.cs:

Line 226 to 227:

Silent exception swallowing in the catch block, where the empty block catches TimeoutException without logging or handling it, violating Rule 28. Log the timeout with operation context using Logging.LogError("GetStringAsync timed out", new { cacheKey }).

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

@Resgrid-Bot

This comment has been minimized.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Core/Resgrid.Services/DeleteService.cs (1)

50-75: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Resolve IScheduledTasksService in the constructor.

Line 50 adds constructor injection. Resolve IScheduledTasksService through Bootstrapper.GetKernel().Resolve<IScheduledTasksService>() in the constructor instead. Remove the added constructor parameter.

As per coding guidelines, use "Service Locator pattern via Bootstrapper.GetKernel().Resolve<T>() to resolve dependencies explicitly in constructors, rather than constructor injection."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Core/Resgrid.Services/DeleteService.cs` around lines 50 - 75, Update the
DeleteService constructor to remove the IScheduledTasksService parameter and
resolve it inside the constructor using
Bootstrapper.GetKernel().Resolve<IScheduledTasksService>(). Keep assigning the
resolved service to _scheduledTasksService and preserve the existing handling of
all other dependencies.

Source: Coding guidelines

🧹 Nitpick comments (3)
Web/Resgrid.Web/Areas/User/Controllers/ProfileController.cs (1)

53-60: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Resolve IDeleteService with the service locator instead of adding a constructor parameter.

The coding guidelines require dependencies to be resolved with Bootstrapper.GetKernel().Resolve<T>() inside the constructor, and they require the number of constructor-injected dependencies to stay small. This constructor already accepts 14 dependencies. The change adds a 15th.

♻️ Proposed refactor
 		public ProfileController(IDepartmentsService departmentsService, IUsersService usersService, Model.Services.IAuthorizationService authorizationService,
 			IUserProfileService userProfileService, IScheduledTasksService scheduledTasksService, ICertificationService certificationService,
 			ICustomStateService customStateService, IImageService imageService, IOptions<AppOptions> appOptionsAccessor,
 			IEmailService emailService, UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager,
 			IDepartmentSsoService departmentSsoService,
-			IStringLocalizer<Resgrid.Localization.Areas.User.Security.Security> secLocalizer, IDeleteService deleteService)
+			IStringLocalizer<Resgrid.Localization.Areas.User.Security.Security> secLocalizer)
 		{
-			_deleteService = deleteService;
+			_deleteService = Bootstrapper.GetKernel().Resolve<IDeleteService>();

As per coding guidelines: "Use Service Locator pattern via Bootstrapper.GetKernel().Resolve<T>() to resolve dependencies explicitly in constructors, rather than constructor injection" and "Minimize constructor injection; keep the number of injected dependencies small".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Web/Resgrid.Web/Areas/User/Controllers/ProfileController.cs` around lines 53
- 60, Remove the IDeleteService deleteService parameter from ProfileController’s
constructor and resolve the dependency inside the constructor with
Bootstrapper.GetKernel().Resolve<IDeleteService>(). Assign the resolved service
to the existing _deleteService field while preserving all other constructor
dependencies and behavior.

Source: Coding guidelines

Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs (1)

138-180: 🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy lift

Route the new deletes through _connectionProvider and _unitOfWork, and wrap them in a transaction.

Three points apply to both new methods:

  1. Both methods create raw NpgsqlConnection and SqlConnection instances. GetAllUpcomingOrRecurringReportDeliveryTasksAsync at Lines 97-127 uses _connectionProvider and _unitOfWork instead. The new methods therefore run outside any ambient transaction that a caller started.
  2. Each method runs two DELETE statements with no transaction. If the second statement fails, the log rows are already gone but the tasks remain.
  3. Both methods return a constant true. The bool result carries no information, so a caller cannot detect a failure.

DeleteAllTasksForUserAsync and DeleteAllTasksForUserInDepartmentAsync differ only by the department predicate. Extract one private helper that takes the optional department id.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs`
around lines 138 - 180, Refactor DeleteAllTasksForUserAsync and
DeleteAllTasksForUserInDepartmentAsync into one private helper accepting an
optional departmentId, and have both public methods delegate to it. Use
_connectionProvider and _unitOfWork like
GetAllUpcomingOrRecurringReportDeliveryTasksAsync, execute both deletes within a
transaction, and return the actual operation result rather than a constant true.
Repositories/Resgrid.Repositories.DataRepository/DeleteRepository.cs (1)

124-151: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Materialize the parent id sets once instead of repeating the subqueries.

(SELECT CallId FROM [dbo].[Calls] WHERE DepartmentId = @DepartmentId) appears 8 times. The Shifts chain nests the same lookup up to three levels deep on Lines 140-142. SQL Server re-evaluates each subquery per statement, and the whole batch holds one transaction open. This lengthens lock duration on a busy database.

Load the ids into temp tables once, then delete against them.

♻️ Proposed refactor (pattern)
+								SELECT CallId INTO `#DeptCalls` FROM [dbo].[Calls] WHERE DepartmentId = `@DepartmentId`
+								SELECT ShiftId INTO `#DeptShifts` FROM [dbo].[Shifts] WHERE DepartmentId = `@DepartmentId`
+								SELECT ShiftSignupId INTO `#DeptShiftSignups` FROM [dbo].[ShiftSignups] WHERE ShiftId IN (SELECT ShiftId FROM `#DeptShifts`)
+
 								-- Call child data (parents deleted further down)
-								DELETE FROM [dbo].[CallAttachments] WHERE CallId IN (SELECT CallId FROM [dbo].[Calls] WHERE DepartmentId = `@DepartmentId`)
-								DELETE FROM [dbo].[CallNotes] WHERE CallId IN (SELECT CallId FROM [dbo].[Calls] WHERE DepartmentId = `@DepartmentId`)
+								DELETE FROM [dbo].[CallAttachments] WHERE CallId IN (SELECT CallId FROM `#DeptCalls`)
+								DELETE FROM [dbo].[CallNotes] WHERE CallId IN (SELECT CallId FROM `#DeptCalls`)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Repositories/Resgrid.Repositories.DataRepository/DeleteRepository.cs` around
lines 124 - 151, In the department-delete SQL block, materialize the
department’s CallId, ShiftId, ShiftSignupId, and related parent ID sets into
temporary tables once, then reuse those tables for the CallAttachments through
CallProtocols deletes and the ShiftSignup/ShiftGroup deletes. Replace the
repeated nested subqueries while preserving deletion order and the existing
`@DepartmentId` scope, and clean up the temporary tables within the batch.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Core/Resgrid.Services/DeleteService.cs`:
- Around line 111-117: The DeactivateUserAccountCoreAsync cleanup flow must not
leave memberships deleted when a later cleanup operation fails. In
Core/Resgrid.Services/DeleteService.cs lines 111-117 and 171-180, ensure all
role, group, distribution-list, and scheduled-task cleanup completes
successfully before invoking the membership-deletion operation DeleteUserAsync;
preserve the same ordering and atomic behavior at both affected sites.

In `@Core/Resgrid.Services/ScheduledTasksService.cs`:
- Around line 235-248: Update DeleteAllTasksForUserAsync and
DeleteAllTasksForUserInDepartmentAsync to call
cancellationToken.ThrowIfCancellationRequested() before invoking their
repository deletion methods. Propagate the token through the scheduled-task
repository contract, implementations, and database commands so in-progress
deletions can observe cancellation where supported, while preserving cache
invalidation and return behavior.

In `@Repositories/Resgrid.Repositories.DataRepository/DeleteRepository.cs`:
- Around line 152-236: In the department deletion SQL within the repository’s
department-delete method, move the CommandDefinitions deletion ahead of the
CallTypes deletion so CommandDefinitions.CallTypeId dependencies are removed
first. Leave DepartmentCallPriorities ordering unchanged because it does not
reference Calls.

In `@Repositories/Resgrid.Repositories.DataRepository/IdentityRepository.cs`:
- Around line 483-497: The multi-statement de-provisioning in
ClearOutUserLoginAsync must execute atomically. In
Repositories/Resgrid.Repositories.DataRepository/IdentityRepository.cs lines
483-497, begin a transaction on the NpgsqlConnection, pass it to every
ExecuteAsync call, and commit after the final statement; apply the identical
transaction wrapper to the SqlConnection branch at lines 514-528, rolling back
on failure as supported by the existing flow.

In
`@Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs`:
- Around line 66-67: Update both member predicates in
GetAllActiveTasksForTypesAsync: at
Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs
lines 66-67, allow NULL IsDeleted values alongside false while preserving the
existing IsDisabled guard; apply the equivalent NULL-or-zero condition at lines
85-86 for the second database branch.

In `@Web/Resgrid.Web/Areas/User/Controllers/PersonnelController.cs`:
- Line 713: Update the user-facing error message in PersonnelController’s
deletion flow to replace the misspelled word “latter” with “later,” leaving the
rest of the message unchanged.

In `@Web/Resgrid.Web/Areas/User/Controllers/ProfileController.cs`:
- Line 1168: Update all three RevokeDepartmentAccessAsync call sites in
DeleteDepartmentLink—Web/Resgrid.Web/Areas/User/Controllers/ProfileController.cs#L1168-L1168,
`#L1196-L1196`, and `#L1220-L1220`—to capture the returned bool. When revocation
fails, log the failure, add an error, and return to YourDepartments; preserve
the existing successful re-sign-in/dashboard flow for the first two paths and
successful YourDepartments redirect for the third.

---

Outside diff comments:
In `@Core/Resgrid.Services/DeleteService.cs`:
- Around line 50-75: Update the DeleteService constructor to remove the
IScheduledTasksService parameter and resolve it inside the constructor using
Bootstrapper.GetKernel().Resolve<IScheduledTasksService>(). Keep assigning the
resolved service to _scheduledTasksService and preserve the existing handling of
all other dependencies.

---

Nitpick comments:
In `@Repositories/Resgrid.Repositories.DataRepository/DeleteRepository.cs`:
- Around line 124-151: In the department-delete SQL block, materialize the
department’s CallId, ShiftId, ShiftSignupId, and related parent ID sets into
temporary tables once, then reuse those tables for the CallAttachments through
CallProtocols deletes and the ShiftSignup/ShiftGroup deletes. Replace the
repeated nested subqueries while preserving deletion order and the existing
`@DepartmentId` scope, and clean up the temporary tables within the batch.

In
`@Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs`:
- Around line 138-180: Refactor DeleteAllTasksForUserAsync and
DeleteAllTasksForUserInDepartmentAsync into one private helper accepting an
optional departmentId, and have both public methods delegate to it. Use
_connectionProvider and _unitOfWork like
GetAllUpcomingOrRecurringReportDeliveryTasksAsync, execute both deletes within a
transaction, and return the actual operation result rather than a constant true.

In `@Web/Resgrid.Web/Areas/User/Controllers/ProfileController.cs`:
- Around line 53-60: Remove the IDeleteService deleteService parameter from
ProfileController’s constructor and resolve the dependency inside the
constructor with Bootstrapper.GetKernel().Resolve<IDeleteService>(). Assign the
resolved service to the existing _deleteService field while preserving all other
constructor dependencies and behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 45ed5f70-0607-4e36-afd7-62bb59d308b8

📥 Commits

Reviewing files that changed from the base of the PR and between 53264e5 and 4afb2d2.

⛔ Files ignored due to path filters (9)
  • Core/Resgrid.Localization/Areas/User/Account/DeleteAccount.ar.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Account/DeleteAccount.de.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Account/DeleteAccount.en.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Account/DeleteAccount.es.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Account/DeleteAccount.fr.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Account/DeleteAccount.it.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Account/DeleteAccount.pl.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Account/DeleteAccount.sv.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Account/DeleteAccount.uk.resx is excluded by !**/*.resx
📒 Files selected for processing (13)
  • Core/Resgrid.Model/Repositories/IScheduledTasksRepository.cs
  • Core/Resgrid.Model/Services/IDeleteService.cs
  • Core/Resgrid.Model/Services/IDistributionListsService.cs
  • Core/Resgrid.Model/Services/IScheduledTasksService.cs
  • Core/Resgrid.Services/DeleteService.cs
  • Core/Resgrid.Services/DistributionListsService.cs
  • Core/Resgrid.Services/ScheduledTasksService.cs
  • Repositories/Resgrid.Repositories.DataRepository/DeleteRepository.cs
  • Repositories/Resgrid.Repositories.DataRepository/IdentityRepository.cs
  • Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs
  • Web/Resgrid.Web/Areas/User/Controllers/PersonnelController.cs
  • Web/Resgrid.Web/Areas/User/Controllers/ProfileController.cs
  • Web/Resgrid.Web/Areas/User/Views/Account/DeleteAccount.cshtml

Comment thread Core/Resgrid.Services/DeleteService.cs Outdated
Comment on lines +111 to +117
await _personnelRolesService.RemoveUserFromAllRolesAsync(userId, departmentId, cancellationToken);
await _departmentGroupsService.DeleteUserFromGroupsAsync(userId, departmentId, cancellationToken);
await _distributionListsService.RemoveUserFromAllListsInDepartmentAsync(userId, departmentId, cancellationToken);
await _scheduledTasksService.DeleteAllTasksForUserInDepartmentAsync(userId, departmentId, cancellationToken);

// Soft-delete the membership last (this also writes the audit event and clears caches).
await _departmentsService.DeleteUserAsync(departmentId, userId, revokingUserId, cancellationToken);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs --items all

rg -n -C 12 'DeleteAllTasksForUser(Async|InDepartmentAsync)' \
  Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs \
  Core/Resgrid.Services/DeleteService.cs \
  Core/Resgrid.Services/ScheduledTasksService.cs

Repository: Resgrid/Core

Length of output: 14838


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- ScheduledTasksRepository deletion methods ---'
sed -n '138,190p' Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs

printf '%s\n' '--- DeleteService account-deletion result path ---'
sed -n '122,235p' Core/Resgrid.Services/DeleteService.cs

printf '%s\n' '--- Related cleanup signatures and implementations ---'
rg -n -C 8 \
  'RemoveUserFromAllRolesAsync|DeleteUserFromGroupsAsync|RemoveUserFromAllLists(InDepartment)?Async|DeleteUserAsync' \
  Core/Resgrid.Services Repositories Core/Resgrid.Model

Repository: Resgrid/Core

Length of output: 35759


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Membership soft-delete implementation ---'
sed -n '286,330p' Core/Resgrid.Services/DepartmentsService.cs

printf '%s\n' '--- Department-group cleanup implementation ---'
sed -n '337,365p' Core/Resgrid.Services/DepartmentGroupsService.cs

printf '%s\n' '--- Distribution-list cleanup implementation ---'
sed -n '111,145p' Core/Resgrid.Services/DistributionListsService.cs

printf '%s\n' '--- Cleanup return-value tests and implementations ---'
rg -n -C 5 \
  'DeleteAllTasksForUser(InDepartment)?Async|RemoveUserFromAllRolesAsync|DeleteUserFromGroupsAsync|RemoveUserFromAllLists(InDepartment)?Async|RevokeDepartmentAccessAsync' \
  --glob '*Tests*' --glob '*Test*' . || true

Repository: Resgrid/Core

Length of output: 3942


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

repo = Path("Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs").read_text()
service = Path("Core/Resgrid.Services/ScheduledTasksService.cs").read_text()

methods = [
    "DeleteAllTasksForUserAsync",
    "DeleteAllTasksForUserInDepartmentAsync",
]

for name in methods:
    match = re.search(
        rf"public async Task<bool> {name}\b.*?\n\t\t\}}",
        repo,
        re.S,
    )
    if not match:
        raise SystemExit(f"missing repository method: {name}")
    body = match.group(0)
    print(f"{name}: return_true={bool(re.search(r'\\breturn true;', body))}, "
          f"return_false={bool(re.search(r'\\breturn false;', body))}, "
          f"throws_explicitly={bool(re.search(r'\\bthrow\\b', body))}")

for name in methods:
    match = re.search(
        rf"public async Task<bool> {name}\b.*?\n\t\t\}}",
        service,
        re.S,
    )
    if not match:
        raise SystemExit(f"missing service method: {name}")
    body = match.group(0)
    print(f"service {name}: forwards_result={bool(re.search(r'return result;', body))}")
PY

Repository: Resgrid/Core

Length of output: 373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

repo = Path("Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs").read_text()
service = Path("Core/Resgrid.Services/ScheduledTasksService.cs").read_text()

methods = [
    "DeleteAllTasksForUserAsync",
    "DeleteAllTasksForUserInDepartmentAsync",
]

for name in methods:
    match = re.search(
        rf"public async Task<bool> {name}\b.*?\n\t\t\}}",
        repo,
        re.S,
    )
    if not match:
        raise SystemExit(f"missing repository method: {name}")
    body = match.group(0)
    print(
        f"{name}: "
        f"return_true={bool(re.search(r'\\breturn true;', body))}, "
        f"return_false={bool(re.search(r'\\breturn false;', body))}, "
        f"throws_explicitly={bool(re.search(r'\\bthrow\\b', body))}"
    )

for name in methods:
    match = re.search(
        rf"public async Task<bool> {name}\b.*?\n\t\t\}}",
        service,
        re.S,
    )
    if not match:
        raise SystemExit(f"missing service method: {name}")
    body = match.group(0)
    print(
        f"service {name}: "
        f"forwards_result={bool(re.search(r'return result;', body))}"
    )
PY

Repository: Resgrid/Core

Length of output: 253


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

repo = Path("Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs").read_text()
service = Path("Core/Resgrid.Services/ScheduledTasksService.cs").read_text()

methods = [
    "DeleteAllTasksForUserAsync",
    "DeleteAllTasksForUserInDepartmentAsync",
]

for source_name, source in [("repository", repo), ("service", service)]:
    for name in methods:
        match = re.search(
            rf"public async Task<bool> {name}\b.*?\n\t\t\}}",
            source,
            re.S,
        )
        if not match:
            raise SystemExit(f"missing {source_name} method: {name}")

        body = match.group(0)
        has_true = bool(re.search(r"\breturn true;", body))
        has_false = bool(re.search(r"\breturn false;", body))
        has_throw = bool(re.search(r"\bthrow\b", body))
        forwards_result = bool(re.search(r"\breturn result;", body))

        print(
            source_name,
            name,
            f"return_true={has_true}",
            f"return_false={has_false}",
            f"throws_explicitly={has_throw}",
            f"forwards_result={forwards_result}",
        )
PY

Repository: Resgrid/Core

Length of output: 648


Make account deactivation atomic across cleanup failures.

DeactivateUserAccountCoreAsync saves deleted memberships before removing roles, groups, distribution-list memberships, and scheduled tasks. If a later cleanup call throws, the login remains active while earlier memberships are deleted and later cleanup may be incomplete. Move membership writes after successful cleanup, or use transactional recovery.

📍 Affects 1 file
  • Core/Resgrid.Services/DeleteService.cs#L111-L117 (this comment)
  • Core/Resgrid.Services/DeleteService.cs#L171-L180
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Core/Resgrid.Services/DeleteService.cs` around lines 111 - 117, The
DeactivateUserAccountCoreAsync cleanup flow must not leave memberships deleted
when a later cleanup operation fails. In Core/Resgrid.Services/DeleteService.cs
lines 111-117 and 171-180, ensure all role, group, distribution-list, and
scheduled-task cleanup completes successfully before invoking the
membership-deletion operation DeleteUserAsync; preserve the same ordering and
atomic behavior at both affected sites.

Comment thread Core/Resgrid.Services/ScheduledTasksService.cs
Comment thread Repositories/Resgrid.Repositories.DataRepository/IdentityRepository.cs Outdated
Comment on lines +66 to +67
WHERE st.departmentid = 0 AND st.active = true AND st.tasktype = any (@types)
AND dm.isdeleted = false AND (dm.isdisabled IS NULL OR dm.isdisabled = false)", new { types = types });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The new member filters guard NULL on IsDisabled but not on IsDeleted. Both database branches of GetAllActiveTasksForTypesAsync use the same predicate shape. If DepartmentMembers.IsDeleted is nullable, rows with a NULL value are now excluded and their scheduled tasks stop running.

  • Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs#L66-L67: change dm.isdeleted = false to (dm.isdeleted IS NULL OR dm.isdeleted = false), or drop the NULL guard on isdisabled if the column is non-nullable.
  • Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs#L85-L86: apply the matching change to dm.IsDeleted = 0.
📍 Affects 1 file
  • Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs#L66-L67 (this comment)
  • Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs#L85-L86
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs`
around lines 66 - 67, Update both member predicates in
GetAllActiveTasksForTypesAsync: at
Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs
lines 66-67, allow NULL IsDeleted values alongside false while preserving the
existing IsDisabled guard; apply the equivalent NULL-or-zero condition at lines
85-86 for the second database branch.

Comment thread Web/Resgrid.Web/Areas/User/Controllers/PersonnelController.cs Outdated
Comment thread Web/Resgrid.Web/Areas/User/Controllers/ProfileController.cs Outdated
foreach (var dm in departments)
{
var dep = await _departmentsService.GetDepartmentByUserIdAsync(userIdToDelete);
var dep = await _departmentsService.GetDepartmentByIdAsync(dm.DepartmentId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

N+1 query generated by calling GetDepartmentByIdAsync inside a foreach loop, causing one database round-trip per membership. Fetch all departments in a single call using GetDepartmentsByIdsAsync or a LINQ Contains query before the loop to eliminate sequential queries.

Kody rule violation: Optimize database queries with JOINs

Prompt for LLM

File Core/Resgrid.Services/DeleteService.cs:

Line 141:

N+1 query generated by calling `GetDepartmentByIdAsync` inside a foreach loop, causing one database round-trip per membership. Fetch all departments in a single call using `GetDepartmentsByIdsAsync` or a LINQ `Contains` query before the loop to eliminate sequential queries.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment on lines +106 to +120
public async Task<bool> RevokeDepartmentAccessAsync(string userId, int departmentId, string revokingUserId, CancellationToken cancellationToken = default(CancellationToken))
{
//if (!await _authorizationService.CanUserDeleteUserAsync(departmentId, authorizingUserId, userIdToDelete))
// return DeleteUserResults.UnAuthroized;
// Strip everything that would keep the department reaching the user: roles,
// group memberships, distribution lists and their scheduled automations
// (status changes, staffing changes, report deliveries) for this department.
await _personnelRolesService.RemoveUserFromAllRolesAsync(userId, departmentId, cancellationToken);
await _departmentGroupsService.DeleteUserFromGroupsAsync(userId, departmentId, cancellationToken);
await _distributionListsService.RemoveUserFromAllListsInDepartmentAsync(userId, departmentId, cancellationToken);
await _scheduledTasksService.DeleteAllTasksForUserInDepartmentAsync(userId, departmentId, cancellationToken);

// Soft-delete the membership last (this also writes the audit event and clears caches).
await _departmentsService.DeleteUserAsync(departmentId, userId, revokingUserId, cancellationToken);

return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Bug medium

Partial-revocation risk identified in RevokeDepartmentAccessAsync, as 5 sequential async database operations execute without a transaction wrapper. A mid-sequence failure, such as a DB timeout, leaves the user in a partially-revoked state without automatic rollback. Wrap the operations in a database transaction to ensure atomic execution.

// Consider wrapping in a transaction or implementing compensating logic
// so that a failure mid-sequence does not leave the user partially revoked.
Prompt for LLM

File Core/Resgrid.Services/DeleteService.cs:

Line 106 to 120:

Partial-revocation risk identified in `RevokeDepartmentAccessAsync`, as 5 sequential async database operations execute without a transaction wrapper. A mid-sequence failure, such as a DB timeout, leaves the user in a partially-revoked state without automatic rollback. Wrap the operations in a database transaction to ensure atomic execution.

Suggested Code:

// Consider wrapping in a transaction or implementing compensating logic
// so that a failure mid-sequence does not leave the user partially revoked.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment on lines +121 to +124
foreach (var member in members.Where(x => departmentListIds.Contains(x.DistributionListId)))
{
await _distributionListMemberRepository.DeleteAsync(member, cancellationToken);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

N+1 delete pattern triggered by calling DeleteAsync individually for each member inside a loop, degrading performance as member count grows. Replace the loop with a batch delete method like DeleteManyAsync or a single DELETE WHERE … IN (…) query.

Kody rule violation: Detect N+1 style queries and suggest batching

Prompt for LLM

File Core/Resgrid.Services/DistributionListsService.cs:

Line 121 to 124:

N+1 delete pattern triggered by calling `DeleteAsync` individually for each member inside a loop, degrading performance as member count grows. Replace the loop with a batch delete method like `DeleteManyAsync` or a single `DELETE WHERE … IN (…)` query.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

WHERE DepartmentId = @DepartmentId

-- Child rows of data the cursors below delete piecemeal; remove while parents still exist
DELETE FROM [dbo].[ScheduledTaskLogs] WHERE ScheduledTaskId IN (SELECT ScheduledTaskId FROM [dbo].[ScheduledTasks] WHERE DepartmentId = @DepartmentId)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Data inconsistency risk caused by executing a large cascade of DELETE statements without a visible transaction boundary. Wrap the department-deletion batch in an explicit BEGIN TRANSACTION / COMMIT block or a C# TransactionScope with a TRY…CATCH rollback to prevent broken referential integrity upon failure.

Kody rule violation: Handle transaction rollbacks properly

Prompt for LLM

File Repositories/Resgrid.Repositories.DataRepository/DeleteRepository.cs:

Line 51:

Data inconsistency risk caused by executing a large cascade of DELETE statements without a visible transaction boundary. Wrap the department-deletion batch in an explicit `BEGIN TRANSACTION / COMMIT` block or a C# `TransactionScope` with a `TRY…CATCH` rollback to prevent broken referential integrity upon failure.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

DELETE FROM [dbo].[ChatbotDepartmentConfigs] WHERE DepartmentId = @DepartmentId

-- Remaining department-scoped tables
DELETE FROM [dbo].[AuditLogs] WHERE DepartmentId = @DepartmentId

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Data integrity violation caused by permanently deleting audit log entries during the department deletion cascade. Audit logs must remain immutable to preserve records required for compliance; remove this DELETE statement or anonymize the DepartmentId and user identifiers while recording the department-deletion action as a new audit entry.

Kody rule violation: Write immutable audit logs for all ePHI access

Prompt for LLM

File Repositories/Resgrid.Repositories.DataRepository/DeleteRepository.cs:

Line 211:

Data integrity violation caused by permanently deleting audit log entries during the department deletion cascade. Audit logs must remain immutable to preserve records required for compliance; remove this DELETE statement or anonymize the `DepartmentId` and user identifiers while recording the department-deletion action as a new audit entry.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

{
var member = await _departmentsService.DeleteUserAsync(DepartmentId, model.UserId, UserId, cancellationToken);
//var result = await _deleteService.DeleteUser(DepartmentId, UserId, model.UserId);
var result = await _deleteService.DeleteUserAsync(DepartmentId, UserId, model.UserId, cancellationToken);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unstructured 500 error risk caused by invoking _deleteService.DeleteUserAsync(...) without error handling. If the deletion service rejects, the exception propagates unhandled. Wrap the await call in a try/catch block, log the exception with context, and return a user-friendly ModelState error.

Kody rule violation: Handle async operations with proper error handling

Prompt for LLM

File Web/Resgrid.Web/Areas/User/Controllers/PersonnelController.cs:

Line 694:

Unstructured 500 error risk caused by invoking `_deleteService.DeleteUserAsync(...)` without error handling. If the deletion service rejects, the exception propagates unhandled. Wrap the await call in a try/catch block, log the exception with context, and return a user-friendly `ModelState` error.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

{
var member = await _departmentsService.DeleteUserAsync(DepartmentId, model.UserId, UserId, cancellationToken);
//var result = await _deleteService.DeleteUser(DepartmentId, UserId, model.UserId);
var result = await _deleteService.DeleteUserAsync(DepartmentId, UserId, model.UserId, cancellationToken);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unhandled exception risk identified where the external service call _deleteService.DeleteUserAsync(...) executes without a try/catch boundary. Wrap the call in a try/catch block, enrich the logs with DepartmentId and UserId, and return a meaningful error result.

Kody rule violation: Add try-catch blocks for external calls

Prompt for LLM

File Web/Resgrid.Web/Areas/User/Controllers/PersonnelController.cs:

Line 694:

Unhandled exception risk identified where the external service call `_deleteService.DeleteUserAsync(...)` executes without a try/catch boundary. Wrap the call in a try/catch block, enrich the logs with `DepartmentId` and `UserId`, and return a meaningful error result.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

@Resgrid-Bot

This comment has been minimized.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Repositories/Resgrid.Repositories.DataRepository/IdentityRepository.cs (1)

575-591: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Delete dependent OIDC tokens in one transaction.

OpenIddictTokens.AuthorizationId has a Restrict foreign key to OpenIddictAuthorizations. The token delete filters only Subject, so it can miss tokens that reference the user's authorizations. Include those tokens by AuthorizationId, then delete the authorizations in one transaction in both database branches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Repositories/Resgrid.Repositories.DataRepository/IdentityRepository.cs`
around lines 575 - 591, The OIDC cleanup in both database branches must delete
dependent tokens before authorizations within a single transaction. Update the
SQL in the cleanup block to delete tokens matching the user subject or
referencing that user’s authorization records via AuthorizationId, then delete
OpenIddictAuthorizations using the same transaction connection; apply this
consistently to both the PostgreSQL and SQL Server branches.
🧹 Nitpick comments (1)
Repositories/Resgrid.Repositories.DataRepository/IdentityRepository.cs (1)

468-562: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use asynchronous transaction methods in both provider branches. The project targets net9.0; use BeginTransactionAsync(), CommitAsync(), and await using. Remove the explicit rollback catch because disposal rolls back uncommitted transactions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Repositories/Resgrid.Repositories.DataRepository/IdentityRepository.cs`
around lines 468 - 562, Update both PostgreSQL and SQL Server transaction blocks
to use await using with BeginTransactionAsync(), and await CommitAsync() after
successful operations. Remove the explicit try/catch rollback handling so
transaction disposal rolls back any uncommitted work while preserving exception
propagation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs`:
- Around line 167-168: Update the scheduled-task deletion flow containing the
ExecuteAsync calls to fail closed when departmentId is non-positive. Before
executing SQL, reject departmentId <= 0, or ensure both the scheduledtasks
predicate and its scheduledtasklogs subquery require DepartmentId > 0, while
preserving deletion for valid positive department IDs.
- Around line 145-146: Update the scheduled-task cleanup methods containing the
paired scheduledtasklogs and scheduledtasks DELETE statements, including the
additional affected call sites, to execute both commands within a single
database transaction. Begin the transaction before log deletion, pass it to both
Dapper CommandDefinitions, commit only after both succeed, and roll back on
failure or cancellation so cleanup is atomic.

---

Outside diff comments:
In `@Repositories/Resgrid.Repositories.DataRepository/IdentityRepository.cs`:
- Around line 575-591: The OIDC cleanup in both database branches must delete
dependent tokens before authorizations within a single transaction. Update the
SQL in the cleanup block to delete tokens matching the user subject or
referencing that user’s authorization records via AuthorizationId, then delete
OpenIddictAuthorizations using the same transaction connection; apply this
consistently to both the PostgreSQL and SQL Server branches.

---

Nitpick comments:
In `@Repositories/Resgrid.Repositories.DataRepository/IdentityRepository.cs`:
- Around line 468-562: Update both PostgreSQL and SQL Server transaction blocks
to use await using with BeginTransactionAsync(), and await CommitAsync() after
successful operations. Remove the explicit try/catch rollback handling so
transaction disposal rolls back any uncommitted work while preserving exception
propagation.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 45cd9dfc-516e-463a-9160-92b2098b1bc3

📥 Commits

Reviewing files that changed from the base of the PR and between 4afb2d2 and 2b35e2b.

📒 Files selected for processing (10)
  • Core/Resgrid.Framework/Logging.cs
  • Core/Resgrid.Model/Repositories/IScheduledTasksRepository.cs
  • Core/Resgrid.Services/DeleteService.cs
  • Core/Resgrid.Services/ScheduledTasksService.cs
  • Repositories/Resgrid.Repositories.DataRepository/DeleteRepository.cs
  • Repositories/Resgrid.Repositories.DataRepository/IdentityRepository.cs
  • Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs
  • Web/Resgrid.Web/Areas/User/Controllers/PersonnelController.cs
  • Web/Resgrid.Web/Areas/User/Controllers/ProfileController.cs
  • Workers/Resgrid.Workers.Console/Tasks/CallPruneTask.cs
🚧 Files skipped from review as they are similar to previous changes (6)
  • Web/Resgrid.Web/Areas/User/Controllers/PersonnelController.cs
  • Workers/Resgrid.Workers.Console/Tasks/CallPruneTask.cs
  • Core/Resgrid.Services/ScheduledTasksService.cs
  • Web/Resgrid.Web/Areas/User/Controllers/ProfileController.cs
  • Repositories/Resgrid.Repositories.DataRepository/DeleteRepository.cs
  • Core/Resgrid.Services/DeleteService.cs

Comment thread Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs Outdated
Comment thread Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs Outdated
// operation retryable (mirrors the ordering in RevokeDepartmentAccessAsync).
foreach (var dm in departments)
{
await _personnelRolesService.RemoveUserFromAllRolesAsync(userIdToDelete, dm.DepartmentId, cancellationToken);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

N+1 query pattern identified in the awaited RemoveUserFromAllRolesAsync call inside the foreach loop over departments, multiplying latency and database load linearly with department count. Add a batch overload such as RemoveUserFromAllRolesInAllDepartmentsAsync(userIdToDelete, departmentIds, cancellationToken) that deletes roles for all departments in a single set-based query or batched transaction.

Kody rule violation: Detect N+1 style queries and suggest batching

Prompt for LLM

File Core/Resgrid.Services/DeleteService.cs:

Line 152:

N+1 query pattern identified in the awaited `RemoveUserFromAllRolesAsync` call inside the `foreach` loop over departments, multiplying latency and database load linearly with department count. Add a batch overload such as `RemoveUserFromAllRolesInAllDepartmentsAsync(userIdToDelete, departmentIds, cancellationToken)` that deletes roles for all departments in a single set-based query or batched transaction.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment on lines +150 to +154
foreach (var dm in departments)
{
await _personnelRolesService.RemoveUserFromAllRolesAsync(userIdToDelete, dm.DepartmentId, cancellationToken);
await _departmentGroupsService.DeleteUserFromGroupsAsync(userIdToDelete, dm.DepartmentId, cancellationToken);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Partial cleanup state vulnerability occurs when the loop performs multi-step writes across multiple departments with no wrapping transaction. Wrap the entire loop in a single transaction scope (e.g., await using var tx = _unitOfWork.BeginTransactionAsync(cancellationToken)) to ensure all role and group modifications commit or roll back atomically, or explicitly verify the idempotency guarantees of each service method.

Kody rule violation: Handle transaction rollbacks properly

Prompt for LLM

File Core/Resgrid.Services/DeleteService.cs:

Line 150 to 154:

Partial cleanup state vulnerability occurs when the loop performs multi-step writes across multiple departments with no wrapping transaction. Wrap the entire loop in a single transaction scope (e.g., `await using var tx = _unitOfWork.BeginTransactionAsync(cancellationToken)`) to ensure all role and group modifications commit or roll back atomically, or explicitly verify the idempotency guarantees of each service method.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

{
cancellationToken.ThrowIfCancellationRequested();

var result = await _scheduledTaskRepository.DeleteAllTasksForUserInDepartmentAsync(userId, departmentId, cancellationToken);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unguarded external database call occurs because the _scheduledTaskRepository.DeleteAllTasksForUserInDepartmentAsync invocation is not wrapped in a try/catch. Wrap the call in a try/catch, add meaningful context (userId, departmentId, operation name), and rethrow or return a domain-specific error.

Kody rule violation: Add try-catch blocks for external calls

Prompt for LLM

File Core/Resgrid.Services/ScheduledTasksService.cs:

Line 249:

Unguarded external database call occurs because the `_scheduledTaskRepository.DeleteAllTasksForUserInDepartmentAsync` invocation is not wrapped in a `try/catch`. Wrap the call in a `try/catch`, add meaningful context (`userId`, `departmentId`, operation name), and rethrow or return a domain-specific error.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

new { userId = userId, deleteId = deleteId, normalizedDeleteId = deleteId.ToUpperInvariant(), maskedEmail = maskedEmail, normalizedMaskedEmail = maskedEmail.ToUpperInvariant(), securityStamp = Guid.NewGuid().ToString(), lockoutEnd = new DateTimeOffset(9999, 12, 31, 23, 59, 59, TimeSpan.Zero) });
using (var db = new NpgsqlConnection(DataConfig.CoreConnectionString))
{
await db.OpenAsync();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unguarded connection failure exception propagates because the awaited db.OpenAsync() call sits before the try block. Move db.OpenAsync() inside the existing try block or add a separate try/catch around it to log context and handle the error appropriately.

Kody rule violation: Handle async operations with proper error handling

Prompt for LLM

File Repositories/Resgrid.Repositories.DataRepository/IdentityRepository.cs:

Line 470:

Unguarded connection failure exception propagates because the awaited `db.OpenAsync()` call sits before the `try` block. Move `db.OpenAsync()` inside the existing `try` block or add a separate `try/catch` around it to log context and handle the error appropriately.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.


// The whole de-provision is atomic: either the account is fully fuzzed and all
// re-entry vectors are removed, or nothing changes and the operation can be retried.
using (var transaction = db.BeginTransaction())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Thread blocking violation occurs because the synchronous db.BeginTransaction() is called inside an async method. Replace it with using (var transaction = await db.BeginTransactionAsync()) to await the operation properly.

Kody rule violation: Use Awaitable Methods in Async Code

Prompt for LLM

File Repositories/Resgrid.Repositories.DataRepository/IdentityRepository.cs:

Line 474:

Thread blocking violation occurs because the synchronous `db.BeginTransaction()` is called inside an async method. Replace it with `using (var transaction = await db.BeginTransactionAsync())` to await the operation properly.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

{
// Orphaned pruning row for a deleted department; skip it so we don't
// null-ref in CallPruneLogic or spam error logs every run.
Resgrid.Framework.Logging.LogWarning($"CallPrune::Skipping orphaned pruning settings for deleted DepartmentId:{item.PruneSettings.DepartmentId}");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unstructured logging violation occurs because the warning log embeds context via string interpolation instead of passing structured fields, preventing the logging framework from capturing DepartmentId as a queryable property. Use a message template with a named placeholder and pass the value as an argument, such as Resgrid.Framework.Logging.LogError("CallPrune::Skipping orphaned pruning settings for deleted DepartmentId:{DepartmentId}", item.PruneSettings.DepartmentId);.

Kody rule violation: Include error context in structured logs

Prompt for LLM

File Workers/Resgrid.Workers.Console/Tasks/CallPruneTask.cs:

Line 56:

Unstructured logging violation occurs because the warning log embeds context via string interpolation instead of passing structured fields, preventing the logging framework from capturing `DepartmentId` as a queryable property. Use a message template with a named placeholder and pass the value as an argument, such as `Resgrid.Framework.Logging.LogError("CallPrune::Skipping orphaned pruning settings for deleted DepartmentId:{DepartmentId}", item.PruneSettings.DepartmentId);`.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

@Resgrid-Bot

This comment has been minimized.

Comment on lines +148 to +161
using (var transaction = db.BeginTransaction())
{
try
{
await db.ExecuteAsync(new Dapper.CommandDefinition(@"DELETE FROM scheduledtasklogs WHERE scheduledtaskid IN (SELECT scheduledtaskid FROM scheduledtasks WHERE userid = @userId)", new { userId = userId }, transaction: transaction, cancellationToken: cancellationToken));
await db.ExecuteAsync(new Dapper.CommandDefinition(@"DELETE FROM scheduledtasks WHERE userid = @userId", new { userId = userId }, transaction: transaction, cancellationToken: cancellationToken));

transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Performance high

Sync-over-async blocking I/O occurs because synchronous transaction calls (db.BeginTransaction(), transaction.Commit(), transaction.Rollback()) execute inside async methods, risking thread-pool starvation under concurrent load. Apply the async variants (await db.BeginTransactionAsync(cancellationToken), etc.) to align with the established pattern in UserProfilesRepository.cs:343-346 across all four transaction blocks (lines 148, 171, 209, 232).

using (var transaction = await db.BeginTransactionAsync(cancellationToken))
{
	try
	{
		await db.ExecuteAsync(new Dapper.CommandDefinition(@"DELETE FROM scheduledtasklogs ...", new { userId = userId }, transaction: transaction, cancellationToken: cancellationToken));
		await db.ExecuteAsync(new Dapper.CommandDefinition(@"DELETE FROM scheduledtasks WHERE userid = @userId", new { userId = userId }, transaction: transaction, cancellationToken: cancellationToken));

		await transaction.CommitAsync(cancellationToken);
	}
	catch
	{
		await transaction.RollbackAsync(cancellationToken);
		throw;
	}
}
Prompt for LLM

File Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs:

Line 148 to 161:

Sync-over-async blocking I/O occurs because synchronous transaction calls (`db.BeginTransaction()`, `transaction.Commit()`, `transaction.Rollback()`) execute inside async methods, risking thread-pool starvation under concurrent load. Apply the async variants (`await db.BeginTransactionAsync(cancellationToken)`, etc.) to align with the established pattern in `UserProfilesRepository.cs:343-346` across all four transaction blocks (lines 148, 171, 209, 232).

Suggested Code:

using (var transaction = await db.BeginTransactionAsync(cancellationToken))
{
	try
	{
		await db.ExecuteAsync(new Dapper.CommandDefinition(@"DELETE FROM scheduledtasklogs ...", new { userId = userId }, transaction: transaction, cancellationToken: cancellationToken));
		await db.ExecuteAsync(new Dapper.CommandDefinition(@"DELETE FROM scheduledtasks WHERE userid = @userId", new { userId = userId }, transaction: transaction, cancellationToken: cancellationToken));

		await transaction.CommitAsync(cancellationToken);
	}
	catch
	{
		await transaction.RollbackAsync(cancellationToken);
		throw;
	}
}

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

await db.OpenAsync(cancellationToken);

// Logs and their tasks go together: never leave tasks whose logs are already gone.
using (var transaction = db.BeginTransaction())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Synchronous database call: BeginTransaction() blocks the thread during the network round-trip to issue the BEGIN command inside an async method, reducing scalability. Replace it with await db.BeginTransactionAsync(cancellationToken) on both NpgsqlConnection and SqlConnection types to maintain fully asynchronous execution across lines 171, 209, and 232.

Kody rule violation: Use Awaitable Methods in Async Code

Prompt for LLM

File Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs:

Line 148:

Synchronous database call: `BeginTransaction()` blocks the thread during the network round-trip to issue the BEGIN command inside an async method, reducing scalability. Replace it with `await db.BeginTransactionAsync(cancellationToken)` on both `NpgsqlConnection` and `SqlConnection` types to maintain fully asynchronous execution across lines 171, 209, and 232.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.


transaction.Commit();
}
catch

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Diagnostic context loss: the catch block in DeleteAllTasksForUserInDepartmentAsync performs a silent rethrow, making transaction failures for department-scoped deletes untraceable. Capture the exception and log it using Logging.LogError(ex, "Failed to delete tasks for user {UserId} in department {DepartmentId}", userId, departmentId) before rollback, applying this to lines 157, 180, and 241.

Kody rule violation: Avoid empty catch blocks

Prompt for LLM

File Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs:

Line 218:

Diagnostic context loss: the catch block in `DeleteAllTasksForUserInDepartmentAsync` performs a silent rethrow, making transaction failures for department-scoped deletes untraceable. Capture the exception and log it using `Logging.LogError(ex, "Failed to delete tasks for user {UserId} in department {DepartmentId}", userId, departmentId)` before rollback, applying this to lines 157, 180, and 241.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

@ucswift

ucswift commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

Approve

@github-actions github-actions Bot 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.

This PR is approved.



if (_logger != null)
_logger.Warning(message);
@Resgrid-Bot

Resgrid-Bot commented Aug 11, 2026

Copy link
Copy Markdown

Kody Review Complete

Great news! 🎉
No issues were found that match your current review configurations.

Keep up the excellent work! 🚀

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the @kody start-review command at the root of your PR.

  • Validate Business Logic: Ask Kody to validate your code against business rules by adding a comment with the @kody -v business-logic command.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug
Performance
Security
Business Logic

Access your configuration settings here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs`:
- Line 198: Replace Logging.LogWarning() in
DeleteAllTasksForUserInDepartmentAsync with the approved logging method that
matches this non-fatal skipped-delete event, using LogInfo() for the existing
message and preserving its context.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9c021f88-c8ff-4bcd-848e-ecf3f2d3b22e

📥 Commits

Reviewing files that changed from the base of the PR and between 2b35e2b and ab5355d.

⛔ Files ignored due to path filters (1)
  • Tests/Resgrid.Tests/Mocks/MockScheduledTasksRepository.cs is excluded by !**/Tests/**
📒 Files selected for processing (1)
  • Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs

// which belong to the user across departments and must survive a single-department revoke.
if (departmentId <= 0)
{
Logging.LogWarning($"DeleteAllTasksForUserInDepartmentAsync called with non-positive departmentId {departmentId} for user {userId}; skipping delete.");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use an approved logging method.

Line 198 calls Logging.LogWarning(). Replace it with an approved logging method that matches this event.
As per coding guidelines, use LogException(), LogError(), LogInfo(), or LogDebug() for all logging.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Repositories/Resgrid.Repositories.DataRepository/ScheduledTasksRepository.cs`
at line 198, Replace Logging.LogWarning() in
DeleteAllTasksForUserInDepartmentAsync with the approved logging method that
matches this non-fatal skipped-delete event, using LogInfo() for the existing
message and preserving its context.

Source: Coding guidelines

@ucswift

ucswift commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

Approve

@github-actions github-actions Bot 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.

This PR is approved.

@ucswift
ucswift merged commit d33803b into master Aug 11, 2026
16 of 17 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.

3 participants