Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Core/Resgrid.Localization/Common.ar.resx
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@
<data name="Connect" xml:space="preserve"><value>اتصال</value></data>
<data name="ContactsModule" xml:space="preserve"><value>جهات الاتصال</value></data>
<data name="ChatModule" xml:space="preserve">
<value>Chat</value>
<value>الدردشة</value>
</data>
<data name="AssistantModule" xml:space="preserve">
<value>Assistant</value>
<value>المساعد</value>
</data>
<data name="ChatModerationModule" xml:space="preserve">
<value>مراجعة المحتوى</value>
Expand Down
2 changes: 1 addition & 1 deletion Core/Resgrid.Localization/Common.de.resx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@
<value>Chat</value>
</data>
<data name="AssistantModule" xml:space="preserve">
<value>Assistant</value>
<value>Assistent</value>
</data>
<data name="ChatModerationModule" xml:space="preserve">
<value>Moderation</value>
Expand Down
4 changes: 2 additions & 2 deletions Core/Resgrid.Localization/Common.es.resx
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@
<value>Certificaciones</value>
</data>
<data name="Close" xml:space="preserve">
<value>Cerca</value>
<value>Cerrar</value>
</data>
<data name="Closed" xml:space="preserve">
<value>Cerrado</value>
Expand All @@ -208,7 +208,7 @@
<value>Chat</value>
</data>
<data name="AssistantModule" xml:space="preserve">
<value>Assistant</value>
<value>Asistente</value>
</data>
<data name="ChatModerationModule" xml:space="preserve">
<value>Moderación</value>
Expand Down
2 changes: 1 addition & 1 deletion Core/Resgrid.Localization/Common.fr.resx
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@
<value>Contacts</value>
</data>
<data name="ChatModule" xml:space="preserve">
<value>Chat</value>
<value>Discussion</value>
</data>
<data name="AssistantModule" xml:space="preserve">
<value>Assistant</value>
Expand Down
2 changes: 1 addition & 1 deletion Core/Resgrid.Localization/Common.it.resx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@
<value>Chat</value>
</data>
<data name="AssistantModule" xml:space="preserve">
<value>Assistant</value>
<value>Assistente</value>
</data>
<data name="ChatModerationModule" xml:space="preserve">
<value>Moderazione</value>
Expand Down
4 changes: 2 additions & 2 deletions Core/Resgrid.Localization/Common.pl.resx
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,10 @@
<value>Kontakty</value>
</data>
<data name="ChatModule" xml:space="preserve">
<value>Chat</value>
<value>Czat</value>
</data>
<data name="AssistantModule" xml:space="preserve">
<value>Assistant</value>
<value>Asystent</value>
</data>
<data name="ChatModerationModule" xml:space="preserve">
<value>Moderacja</value>
Expand Down
4 changes: 2 additions & 2 deletions Core/Resgrid.Localization/Common.sv.resx
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,10 @@
<value>Kontakter</value>
</data>
<data name="ChatModule" xml:space="preserve">
<value>Chat</value>
<value>Chatt</value>
</data>
<data name="AssistantModule" xml:space="preserve">
<value>Assistant</value>
<value>Assistent</value>
</data>
<data name="ChatModerationModule" xml:space="preserve">
<value>Moderering</value>
Expand Down
4 changes: 2 additions & 2 deletions Core/Resgrid.Localization/Common.uk.resx
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,10 @@
<value>Контакти</value>
</data>
<data name="ChatModule" xml:space="preserve">
<value>Chat</value>
<value>Чат</value>
</data>
<data name="AssistantModule" xml:space="preserve">
<value>Assistant</value>
<value>Асистент</value>
</data>
<data name="ChatModerationModule" xml:space="preserve">
<value>Модерація</value>
Expand Down
3 changes: 3 additions & 0 deletions Core/Resgrid.Model/Services/IChatServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ public interface IChatPermissionService
/// </summary>
Task<List<string>> ResolveChannelAudienceUserIdsAsync(ChatChannel channel);

/// <summary>True when the user can administer the department (used to widen provisioning/visibility, e.g. every group default channel).</summary>
Task<bool> IsDepartmentAdminAsync(int departmentId, string userId);

/// <summary>Drops cached permission evaluations for a channel (membership/roles changed) and bumps the channel-list cache version.</summary>
Task InvalidateChannelCacheAsync(string chatChannelId);
}
Expand Down
6 changes: 5 additions & 1 deletion Core/Resgrid.Model/Services/IFeatureToggleService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ public interface IFeatureToggleService
{
#region Evaluation (hot path)

/// <summary>Returns whether a flag is enabled for a department, falling back to defaultValue when unknown.</summary>
/// <summary>
/// Returns whether a flag is enabled for a department, falling back to defaultValue when the flag
/// is unknown. Never throws: if evaluation itself fails (flag store unreachable) the error is
/// logged and the answer is false — gated features fail shut instead of 500ing their callers.
/// </summary>
Task<bool> IsEnabledAsync(string key, int departmentId, bool defaultValue = false, IDictionary<string, string> context = null);
Comment on lines +19 to 24

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

Align the unknown-flag contract with BuildDefault.

When FeatureFlagsConfig.CodeDefaults contains the key, BuildDefault returns that configured value instead of the defaultValue method argument. Update this documentation to describe that precedence, or change the implementation to honor defaultValue.

Proposed documentation correction
-		/// Returns whether a flag is enabled for a department, falling back to defaultValue when the flag
-		/// is unknown. Never throws: if evaluation itself fails (flag store unreachable) the error is
+		/// Returns whether a flag is enabled for a department, falling back to the configured code
+		/// default when present, otherwise to defaultValue, when the flag is unknown. If evaluation
+		/// itself fails (flag store unreachable), the error is
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// <summary>
/// Returns whether a flag is enabled for a department, falling back to defaultValue when the flag
/// is unknown. Never throws: if evaluation itself fails (flag store unreachable) the error is
/// logged and the answer is false — gated features fail shut instead of 500ing their callers.
/// </summary>
Task<bool> IsEnabledAsync(string key, int departmentId, bool defaultValue = false, IDictionary<string, string> context = null);
/// <summary>
/// Returns whether a flag is enabled for a department, falling back to the configured code
/// default when present, otherwise to defaultValue, when the flag is unknown. If evaluation
/// itself fails (flag store unreachable), the error is
/// logged and the answer is false — gated features fail shut instead of 500ing their callers.
/// </summary>
Task<bool> IsEnabledAsync(string key, int departmentId, bool defaultValue = false, IDictionary<string, string> context = null);
🤖 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.Model/Services/IFeatureToggleService.cs` around lines 19 - 24,
Align the IsEnabledAsync contract with BuildDefault: document that a value
configured in FeatureFlagsConfig.CodeDefaults takes precedence over the
defaultValue argument for known keys, or update the implementation to honor
defaultValue instead. Ensure the documentation and behavior consistently
describe the same precedence.


/// <summary>Full evaluation including the value and the reason (source) it resolved that way.</summary>
Expand Down
51 changes: 45 additions & 6 deletions Core/Resgrid.Services/ChatChannelService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading.Tasks;
using Newtonsoft.Json;
using Resgrid.Config;
using Resgrid.Framework;
using Resgrid.Model;
using Resgrid.Model.Events;
using Resgrid.Model.Providers;
Expand Down Expand Up @@ -84,12 +85,51 @@ async Task<List<ChatChannel>> getChannels()
if (departmentChannel != null)
results[departmentChannel.ChatChannelId] = departmentChannel;

var group = await _departmentGroupsService.GetGroupForUserAsync(userId, departmentId);
if (group != null)
// Loaded once: source for the admin group-channel matching here AND the
// implicit-audience pass further down.
var allChannels = await _chatChannelRepository.GetAllByDepartmentIdAsync(departmentId, includeArchived);

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 propagation occurs if GetAllByDepartmentIdAsync rejects. Wrap the repository call in a try/catch block, log the exception with context, and fall back to an empty list on failure.

Kody rule violation: Handle async operations with proper error handling

Prompt for LLM

File Core/Resgrid.Services/ChatChannelService.cs:

Line 90:

Unhandled exception propagation occurs if `GetAllByDepartmentIdAsync` rejects. Wrap the repository call in a try/catch block, log the exception with context, and fall back to an empty list on failure.

Talk to Kody by mentioning @kody

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


// Department admins get every group's default channel; everyone else gets only the group
// they belong to. Existing channels come from the bulk load above — only groups with no
// channel yet hit the provisioning path, and a failure there is contained per group so one
// bad group can never blank the admin's whole channel list.
if (await _chatPermissionService.IsDepartmentAdminAsync(departmentId, userId))
{
var allGroups = await _departmentGroupsService.GetAllGroupsForDepartmentAsync(departmentId);
if (allGroups != null && allGroups.Count > 0)
{
var groupChannelsByGroupId = allChannels?
.Where(c => c.ChannelType == (int)ChatChannelType.GroupDefault && c.GroupId.HasValue)
.GroupBy(c => c.GroupId.Value)
.ToDictionary(g => g.Key, g => g.First());
Comment on lines +101 to +104

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

Complex multi-step LINQ chain reduces readability and debuggability. Extract the Where and GroupBy operations into an intermediate variable before building the dictionary.

Kody rule violation: Limit Lengthy LINQ Chains

Prompt for LLM

File Core/Resgrid.Services/ChatChannelService.cs:

Line 101 to 104:

Complex multi-step LINQ chain reduces readability and debuggability. Extract the `Where` and `GroupBy` operations into an intermediate variable before building the dictionary.

Talk to Kody by mentioning @kody

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


foreach (var departmentGroup in allGroups)
{
try
{
ChatChannel groupChannel;
if (groupChannelsByGroupId == null || !groupChannelsByGroupId.TryGetValue(departmentGroup.DepartmentGroupId, out groupChannel))
groupChannel = await EnsureGroupChannelAsync(departmentGroup);

if (groupChannel != null)
results[groupChannel.ChatChannelId] = groupChannel;
}
catch (Exception ex)
{
Logging.LogException(ex);
}
}
}
Comment on lines +96 to +122

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 medium

Query amplification in GetChannelsForUserAsync causes the department-admin branch to loop over all department groups calling EnsureGroupChannelAsync, which issues a separate GetByGroupIdAsync database query per group. Add a batch repository method like GetExistingGroupChannelsByDepartmentAsync to fetch all existing group-default channels in one query, matching them by GroupId in memory and only calling EnsureGroupChannelAsync for missing channels.

// Fetch all existing group-default channels for this department in a single query.
var existingGroupChannels = await _chatChannelRepository.GetAllGroupChannelsByDepartmentAsync(departmentId);
var channelByGroupId = existingGroupChannels?.ToDictionary(c => c.GroupId.GetValueOrDefault()) ?? new Dictionary<int, ChatChannel>();

var allGroups = await _departmentGroupsService.GetAllGroupsForDepartmentAsync(departmentId);
if (allGroups != null)
{
    foreach (var departmentGroup in allGroups)
    {
        if (channelByGroupId.TryGetValue(departmentGroup.DepartmentGroupId, out var existing) && existing != null)
        {
            results[existing.ChatChannelId] = existing;
            continue;
        }
        // Only provision (query + insert) for groups that don't have a channel yet.
        var groupChannel = await EnsureGroupChannelAsync(departmentGroup);
        if (groupChannel != null)
            results[groupChannel.ChatChannelId] = groupChannel;
    }
}
Prompt for LLM

File Core/Resgrid.Services/ChatChannelService.cs:

Line 89 to 100:

Query amplification in `GetChannelsForUserAsync` causes the department-admin branch to loop over all department groups calling `EnsureGroupChannelAsync`, which issues a separate `GetByGroupIdAsync` database query per group. Add a batch repository method like `GetExistingGroupChannelsByDepartmentAsync` to fetch all existing group-default channels in one query, matching them by `GroupId` in memory and only calling `EnsureGroupChannelAsync` for missing channels.

Suggested Code:

// Fetch all existing group-default channels for this department in a single query.
var existingGroupChannels = await _chatChannelRepository.GetAllGroupChannelsByDepartmentAsync(departmentId);
var channelByGroupId = existingGroupChannels?.ToDictionary(c => c.GroupId.GetValueOrDefault()) ?? new Dictionary<int, ChatChannel>();

var allGroups = await _departmentGroupsService.GetAllGroupsForDepartmentAsync(departmentId);
if (allGroups != null)
{
    foreach (var departmentGroup in allGroups)
    {
        if (channelByGroupId.TryGetValue(departmentGroup.DepartmentGroupId, out var existing) && existing != null)
        {
            results[existing.ChatChannelId] = existing;
            continue;
        }
        // Only provision (query + insert) for groups that don't have a channel yet.
        var groupChannel = await EnsureGroupChannelAsync(departmentGroup);
        if (groupChannel != null)
            results[groupChannel.ChatChannelId] = groupChannel;
    }
}

Talk to Kody by mentioning @kody

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

}
Comment on lines +96 to +123

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

N+1 query issue in GetChannelsForUserAsync causes the department-admin branch to await EnsureGroupChannelAsync once per group, resulting in a GetByGroupIdAsync database round-trip for each group. Batch fetch all of the department's existing GroupDefault channels in a single query, similar to the EnsureLaneChannelsAsync pattern, and provision only the missing groups.

// Fetch all existing GroupDefault channels for the department in one query instead of one
// GetByGroupIdAsync round-trip per group (the per-group EnsureGroupChannelAsync N+1).
if (await _chatPermissionService.IsDepartmentAdminAsync(departmentId, userId))
{
	var allGroups = await _departmentGroupsService.GetAllGroupsForDepartmentAsync(departmentId);
	if (allGroups != null && allGroups.Count > 0)
	{
		var groupIds = allGroups.Select(g => g.DepartmentGroupId).ToHashSet();
		var existingGroupChannels = await _chatChannelRepository.GetByGroupIdsAsync(groupIds);
		var byGroupId = (existingGroupChannels ?? Enumerable.Empty<ChatChannel>())
			.Where(c => c.ChannelType == (int)ChatChannelType.GroupDefault)
			.ToDictionary(c => c.GroupId);

		foreach (var departmentGroup in allGroups)
		{
			if (byGroupId.TryGetValue(departmentGroup.DepartmentGroupId, out var existing) && existing != null)
			{
				results[existing.ChatChannelId] = existing;
			}
			else
			{
				var groupChannel = await EnsureGroupChannelAsync(departmentGroup);
				if (groupChannel != null)
					results[groupChannel.ChatChannelId] = groupChannel;
			}
		}
	}
}
Prompt for LLM

File Core/Resgrid.Services/ChatChannelService.cs:

Line 89 to 101:

N+1 query issue in `GetChannelsForUserAsync` causes the department-admin branch to await `EnsureGroupChannelAsync` once per group, resulting in a `GetByGroupIdAsync` database round-trip for each group. Batch fetch all of the department's existing `GroupDefault` channels in a single query, similar to the `EnsureLaneChannelsAsync` pattern, and provision only the missing groups.

Suggested Code:

// Fetch all existing GroupDefault channels for the department in one query instead of one
// GetByGroupIdAsync round-trip per group (the per-group EnsureGroupChannelAsync N+1).
if (await _chatPermissionService.IsDepartmentAdminAsync(departmentId, userId))
{
	var allGroups = await _departmentGroupsService.GetAllGroupsForDepartmentAsync(departmentId);
	if (allGroups != null && allGroups.Count > 0)
	{
		var groupIds = allGroups.Select(g => g.DepartmentGroupId).ToHashSet();
		var existingGroupChannels = await _chatChannelRepository.GetByGroupIdsAsync(groupIds);
		var byGroupId = (existingGroupChannels ?? Enumerable.Empty<ChatChannel>())
			.Where(c => c.ChannelType == (int)ChatChannelType.GroupDefault)
			.ToDictionary(c => c.GroupId);

		foreach (var departmentGroup in allGroups)
		{
			if (byGroupId.TryGetValue(departmentGroup.DepartmentGroupId, out var existing) && existing != null)
			{
				results[existing.ChatChannelId] = existing;
			}
			else
			{
				var groupChannel = await EnsureGroupChannelAsync(departmentGroup);
				if (groupChannel != null)
					results[groupChannel.ChatChannelId] = groupChannel;
			}
		}
	}
}

Talk to Kody by mentioning @kody

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

else
{
var groupChannel = await EnsureGroupChannelAsync(group);
if (groupChannel != null)
results[groupChannel.ChatChannelId] = groupChannel;
var group = await _departmentGroupsService.GetGroupForUserAsync(userId, departmentId);
if (group != null)
{
var groupChannel = await EnsureGroupChannelAsync(group);
if (groupChannel != null)
results[groupChannel.ChatChannelId] = groupChannel;
Comment on lines +129 to +131

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 low

Code duplication increases maintenance burden because the await EnsureGroupChannelAsync(group); if (channel != null) results[...] = channel sequence is repeated at lines 96-98 and 107-109. Extract a helper like TryAddGroupChannelAsync and call it from both the admin and non-admin branches.

Kody rule violation: Extract duplicated logic into functions

Prompt for LLM

File Core/Resgrid.Services/ChatChannelService.cs:

Line 107 to 109:

Code duplication increases maintenance burden because the `await EnsureGroupChannelAsync(group); if (channel != null) results[...] = channel` sequence is repeated at lines 96-98 and 107-109. Extract a helper like `TryAddGroupChannelAsync` and call it from both the admin and non-admin branches.

Talk to Kody by mentioning @kody

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

}
}

// Chatbot channels are provisioned when a chatbot session starts — the list path only
Expand All @@ -115,7 +155,6 @@ async Task<List<ChatChannel>> getChannels()

// Implicit-audience channels (custom rule-based + active incident channels): evaluate access
// per channel; evaluations are cached by the permission service.
var allChannels = await _chatChannelRepository.GetAllByDepartmentIdAsync(departmentId, includeArchived);
if (allChannels != null)
{
foreach (var channel in allChannels)
Expand Down
2 changes: 1 addition & 1 deletion Core/Resgrid.Services/ChatPermissionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,7 @@ private async Task AddUnitCrewAsync(int unitId, HashSet<string> userIds)
AddIfSet(userIds, role.UserId);
}

private async Task<bool> IsDepartmentAdminAsync(int departmentId, string userId)
public async Task<bool> IsDepartmentAdminAsync(int departmentId, string userId)

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 low

Overly broad access modifier widens the API surface unnecessarily by changing IsDepartmentAdminAsync from private to public. Use internal if only another service in the same assembly requires access, or expose it via a higher-level public API.

Kody rule violation: Use private access modifiers for encapsulation

Prompt for LLM

File Core/Resgrid.Services/ChatPermissionService.cs:

Line 685:

Overly broad access modifier widens the API surface unnecessarily by changing `IsDepartmentAdminAsync` from `private` to `public`. Use `internal` if only another service in the same assembly requires access, or expose it via a higher-level public API.

Talk to Kody by mentioning @kody

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

{
return await _authorizationService.CanUserModifyDepartmentAsync(userId, departmentId);
}
Expand Down
16 changes: 14 additions & 2 deletions Core/Resgrid.Services/FeatureToggleService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,20 @@ public FeatureToggleService(IFeatureFlagRepository featureFlagRepository, IFeatu

public async Task<bool> IsEnabledAsync(string key, int departmentId, bool defaultValue = false, IDictionary<string, string> context = null)
{
var evaluation = await EvaluateInternalAsync(key, departmentId, context, defaultValue, new HashSet<int>());
return evaluation.IsEnabled;
try
{
var evaluation = await EvaluateInternalAsync(key, departmentId, context, defaultValue, new HashSet<int>());
return evaluation.IsEnabled;
}
catch (Exception ex)
{
// Fail shut: a flag-store outage (cache AND database unreachable) must never 500 every
// gated page/endpoint — the feature simply reads as disabled until the store recovers.
// Deliberately ignores defaultValue here: that is the "flag not defined" default, not
// the "evaluation infrastructure down" answer.
Logging.LogException(ex, $"FeatureToggle evaluation failed for '{key}' in department {departmentId}; failing shut (disabled)");

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 detected in FeatureToggleService.cs and ChatChannelService.cs:119. Pass the operation name, key, departmentId, and error as structured fields instead of an interpolated string so logs remain queryable.

Kody rule violation: Include error context in structured logs

Prompt for LLM

File Core/Resgrid.Services/FeatureToggleService.cs:

Line 78:

Unstructured logging detected in `FeatureToggleService.cs` and `ChatChannelService.cs:119`. Pass the operation name, `key`, `departmentId`, and error as structured fields instead of an interpolated string so logs remain queryable.

Talk to Kody by mentioning @kody

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

return false;
}
Comment on lines +67 to +80

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Keep plan-gate failures fail-closed.

IsEnabledAsync catches only exceptions that escape EvaluateInternalAsync. PassesPlanGateAsync catches Billing exceptions and returns true, so a flag with MinimumPlanType can remain enabled during a Billing outage. Return false from that failure path or propagate the exception so the documented fail-closed behavior applies to subscription checks.

Proposed plan-gate failure handling
catch (Exception ex)
{
	Logging.LogException(ex, $"FeatureToggle plan gate check failed for department {departmentId}");
-	return true;
+	return false;
}
🤖 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/FeatureToggleService.cs` around lines 67 - 80, Update
PassesPlanGateAsync so Billing or subscription-check failures return false or
propagate the exception instead of returning true. Ensure IsEnabledAsync’s
existing fail-closed handling keeps flags with MinimumPlanType disabled whenever
the plan gate cannot be evaluated.

}

public async Task<FeatureFlagEvaluation> EvaluateAsync(string key, int departmentId, IDictionary<string, string> context = null)
Expand Down
101 changes: 101 additions & 0 deletions Tests/Resgrid.Tests/Services/ChatChannelServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,107 @@ public async Task implicit_channel_without_membership_should_create_row()
}
}

[TestFixture]
public class when_listing_channels_for_users : with_the_chat_channel_service
{
private ChatChannel SetupDepartmentChannel()
{
var departmentChannel = new ChatChannel
{
ChatChannelId = "dept-chan",
DepartmentId = 1,
ChannelType = (int)ChatChannelType.DepartmentDefault,
Name = "First Battalion",
CreatedOn = DateTime.UtcNow
};
_chatChannelRepositoryMock.Setup(x => x.GetDepartmentDefaultAsync(1)).ReturnsAsync(departmentChannel);
return departmentChannel;
}

[Test]
public async Task department_admin_should_get_every_group_channel_provisioned()
{
SetupDepartmentChannel();
_chatPermissionServiceMock.Setup(x => x.IsDepartmentAdminAsync(1, "admin-a")).ReturnsAsync(true);
_departmentGroupsServiceMock.Setup(x => x.GetAllGroupsForDepartmentAsync(1)).ReturnsAsync(new List<DepartmentGroup>
{
new DepartmentGroup { DepartmentGroupId = 9, DepartmentId = 1, Name = "Station 1" },
new DepartmentGroup { DepartmentGroupId = 10, DepartmentId = 1, Name = "Station 2" }
});
_chatChannelRepositoryMock.Setup(x => x.GetByGroupIdAsync(It.IsAny<int>())).ReturnsAsync((ChatChannel)null);

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 low

Type casting violation requires using the as operator or pattern matching for safe casts and guarding null results before usage.

Kody rule violation: Use safe type casting with as operator

Prompt for LLM

File Tests/Resgrid.Tests/Services/ChatChannelServiceTests.cs:

Line 554:

Type casting violation requires using the `as` operator or pattern matching for safe casts and guarding null results before usage.

Talk to Kody by mentioning @kody

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


var result = await _chatChannelService.GetChannelsForUserAsync(1, "admin-a", null);

result.Should().Contain(c => c.ChannelType == (int)ChatChannelType.GroupDefault && c.GroupId == 9);
result.Should().Contain(c => c.ChannelType == (int)ChatChannelType.GroupDefault && c.GroupId == 10);
_chatChannelRepositoryMock.Verify(x => x.InsertAsync(It.Is<ChatChannel>(c => c.ChannelType == (int)ChatChannelType.GroupDefault),
It.IsAny<CancellationToken>(), It.IsAny<bool>()), Times.Exactly(2));
_departmentGroupsServiceMock.Verify(x => x.GetGroupForUserAsync(It.IsAny<string>(), It.IsAny<int>()), Times.Never);
}

[Test]
public async Task admin_existing_group_channels_should_come_from_bulk_load_without_per_group_lookups()
{
SetupDepartmentChannel();
_chatPermissionServiceMock.Setup(x => x.IsDepartmentAdminAsync(1, "admin-a")).ReturnsAsync(true);
_departmentGroupsServiceMock.Setup(x => x.GetAllGroupsForDepartmentAsync(1)).ReturnsAsync(new List<DepartmentGroup>
{
new DepartmentGroup { DepartmentGroupId = 9, DepartmentId = 1, Name = "Station 1" },
new DepartmentGroup { DepartmentGroupId = 10, DepartmentId = 1, Name = "Station 2" }
});
_chatChannelRepositoryMock.Setup(x => x.GetAllByDepartmentIdAsync(1, false)).ReturnsAsync(new List<ChatChannel>
{
new ChatChannel { ChatChannelId = "group-9", DepartmentId = 1, ChannelType = (int)ChatChannelType.GroupDefault, GroupId = 9, Name = "Station 1" },
new ChatChannel { ChatChannelId = "group-10", DepartmentId = 1, ChannelType = (int)ChatChannelType.GroupDefault, GroupId = 10, Name = "Station 2" }
});

var result = await _chatChannelService.GetChannelsForUserAsync(1, "admin-a", null);

result.Should().Contain(c => c.ChatChannelId == "group-9");
result.Should().Contain(c => c.ChatChannelId == "group-10");
_chatChannelRepositoryMock.Verify(x => x.GetByGroupIdAsync(It.IsAny<int>()), Times.Never);
_chatChannelRepositoryMock.Verify(x => x.InsertAsync(It.IsAny<ChatChannel>(), It.IsAny<CancellationToken>(), It.IsAny<bool>()), Times.Never);
}

[Test]
public async Task admin_single_group_provisioning_failure_should_not_abort_the_channel_list()
{
SetupDepartmentChannel();
_chatPermissionServiceMock.Setup(x => x.IsDepartmentAdminAsync(1, "admin-a")).ReturnsAsync(true);
_departmentGroupsServiceMock.Setup(x => x.GetAllGroupsForDepartmentAsync(1)).ReturnsAsync(new List<DepartmentGroup>
{
new DepartmentGroup { DepartmentGroupId = 9, DepartmentId = 1, Name = "Station 1" },
new DepartmentGroup { DepartmentGroupId = 10, DepartmentId = 1, Name = "Station 2" }
});
_chatChannelRepositoryMock.Setup(x => x.GetByGroupIdAsync(9)).ThrowsAsync(new InvalidOperationException("db down"));
_chatChannelRepositoryMock.Setup(x => x.GetByGroupIdAsync(10)).ReturnsAsync((ChatChannel)null);

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 low

Type casting rule violation detected. Use the as operator or pattern matching for safe casts and guard null results before usage.

Kody rule violation: Use safe type casting with as operator

Prompt for LLM

File Tests/Resgrid.Tests/Services/ChatChannelServiceTests.cs:

Line 600:

Type casting rule violation detected. Use the `as` operator or pattern matching for safe casts and guard null results before usage.

Talk to Kody by mentioning @kody

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


var result = await _chatChannelService.GetChannelsForUserAsync(1, "admin-a", null);

result.Should().Contain(c => c.ChannelType == (int)ChatChannelType.GroupDefault && c.GroupId == 10);
result.Should().Contain(c => c.ChannelType == (int)ChatChannelType.DepartmentDefault);
}

[Test]
public async Task non_admin_should_only_get_their_own_group_channel()
{
SetupDepartmentChannel();
_chatPermissionServiceMock.Setup(x => x.IsDepartmentAdminAsync(1, "user-a")).ReturnsAsync(false);
_departmentGroupsServiceMock.Setup(x => x.GetGroupForUserAsync("user-a", 1)).ReturnsAsync(new DepartmentGroup
{
DepartmentGroupId = 9,
DepartmentId = 1,
Name = "Station 1"
});
_chatChannelRepositoryMock.Setup(x => x.GetByGroupIdAsync(9)).ReturnsAsync((ChatChannel)null);

var result = await _chatChannelService.GetChannelsForUserAsync(1, "user-a", null);

result.Should().ContainSingle(c => c.ChannelType == (int)ChatChannelType.GroupDefault).Which.GroupId.Should().Be(9);
_departmentGroupsServiceMock.Verify(x => x.GetAllGroupsForDepartmentAsync(It.IsAny<int>()), Times.Never);
}
}

[TestFixture]
public class when_creating_ad_hoc_group_channels : with_the_chat_channel_service
{
Expand Down
Loading
Loading