diff --git a/apps/api/src/frameworks/frameworks-people-score.helper.spec.ts b/apps/api/src/frameworks/frameworks-people-score.helper.spec.ts
index ee76cc0f2..da98db0d5 100644
--- a/apps/api/src/frameworks/frameworks-people-score.helper.spec.ts
+++ b/apps/api/src/frameworks/frameworks-people-score.helper.spec.ts
@@ -187,14 +187,14 @@ describe('computePeopleScore', () => {
it('counts every member as complete when all members are exempt', async () => {
(mockDb.backgroundCheckRequest.findMany as jest.Mock).mockResolvedValue([]);
- (mockDb.member.findMany as jest.Mock).mockImplementation(async (args: {
- where?: { backgroundCheckExempt?: boolean };
- }) => {
- if (args?.where?.backgroundCheckExempt === true) {
- return [{ id: 'mem_1' }, { id: 'mem_2' }];
- }
- return [];
- });
+ (mockDb.member.findMany as jest.Mock).mockImplementation(
+ async (args: { where?: { backgroundCheckExempt?: boolean } }) => {
+ if (args?.where?.backgroundCheckExempt === true) {
+ return [{ id: 'mem_1' }, { id: 'mem_2' }];
+ }
+ return [];
+ },
+ );
const score = await computePeopleScore({
organizationId: 'org_1',
@@ -209,6 +209,64 @@ describe('computePeopleScore', () => {
expect(score).toEqual({ total: 2, completed: 2 });
});
+ it('does not require a background check for auditor-only members', async () => {
+ const auditorMembers = [
+ {
+ id: 'mem_1',
+ role: 'auditor',
+ user: { id: 'usr_1', email: 'a@example.com', role: 'auditor' },
+ },
+ {
+ id: 'mem_2',
+ role: 'owner',
+ user: { id: 'usr_2', email: 'b@example.com', role: 'owner' },
+ },
+ ];
+ mockFilterComplianceMembers.mockResolvedValue(auditorMembers);
+ (mockDb.backgroundCheckRequest.findMany as jest.Mock).mockResolvedValue([]);
+ (mockDb.member.findMany as jest.Mock).mockResolvedValue([]);
+
+ const score = await computePeopleScore({
+ organizationId: 'org_1',
+ allPolicies: [],
+ employees: auditorMembers,
+ securityTrainingStepEnabled: false,
+ deviceAgentStepEnabled: false,
+ backgroundCheckStepEnabled: true,
+ hasHipaaFramework: false,
+ });
+
+ // mem_1 (auditor-only) → no BG check required → complete
+ // mem_2 (owner) → no BG check, not exempt → not complete
+ expect(score).toEqual({ total: 2, completed: 1 });
+ });
+
+ it('still requires a background check for members with auditor plus another role', async () => {
+ const mixedMembers = [
+ {
+ id: 'mem_1',
+ role: 'auditor,employee',
+ user: { id: 'usr_1', email: 'a@example.com', role: 'employee' },
+ },
+ ];
+ mockFilterComplianceMembers.mockResolvedValue(mixedMembers);
+ (mockDb.backgroundCheckRequest.findMany as jest.Mock).mockResolvedValue([]);
+ (mockDb.member.findMany as jest.Mock).mockResolvedValue([]);
+
+ const score = await computePeopleScore({
+ organizationId: 'org_1',
+ allPolicies: [],
+ employees: mixedMembers,
+ securityTrainingStepEnabled: false,
+ deviceAgentStepEnabled: false,
+ backgroundCheckStepEnabled: true,
+ hasHipaaFramework: false,
+ });
+
+ // auditor+employee is NOT auditor-only → still requires a BG check → not complete
+ expect(score).toEqual({ total: 1, completed: 0 });
+ });
+
it('skips the exempt query entirely when backgroundCheckStepEnabled is false', async () => {
(mockDb.member.findMany as jest.Mock).mockClear();
diff --git a/apps/api/src/frameworks/frameworks-people-score.helper.ts b/apps/api/src/frameworks/frameworks-people-score.helper.ts
index 427ff88da..ac0b11671 100644
--- a/apps/api/src/frameworks/frameworks-people-score.helper.ts
+++ b/apps/api/src/frameworks/frameworks-people-score.helper.ts
@@ -8,6 +8,20 @@ const COMPLETED_BACKGROUND_CHECK_STATUSES = [
BackgroundCheckStatus.completed_with_flags,
];
+/**
+ * Auditor-only members are not subject to people-security requirements like
+ * background checks (CS-416). A member counts as auditor-only when every one of
+ * their roles is `auditor` — a member with `auditor` plus another role still
+ * carries that other role's obligations.
+ */
+function isAuditorOnly(role: string): boolean {
+ const roles = role
+ .split(',')
+ .map((r) => r.trim())
+ .filter(Boolean);
+ return roles.length > 0 && roles.every((r) => r === 'auditor');
+}
+
interface ScorePolicy {
isRequiredToSign: boolean;
status: string;
@@ -106,7 +120,9 @@ export async function computePeopleScore({
? membersWithInstalledDevices.has(employee.id)
: true;
const memberRequiresBgCheck =
- backgroundCheckStepEnabled && !exemptMemberIds.has(employee.id);
+ backgroundCheckStepEnabled &&
+ !exemptMemberIds.has(employee.id) &&
+ !isAuditorOnly(employee.role);
const hasCompletedBackgroundCheck = memberRequiresBgCheck
? membersWithCompletedBackgroundChecks.has(employee.id)
: true;
diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Employee.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Employee.tsx
index e5757f902..c526b494e 100644
--- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Employee.tsx
+++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Employee.tsx
@@ -20,6 +20,7 @@ import { EmployeeDevice } from './EmployeeDevice';
import { EmployeePageHeader } from './EmployeePageHeader';
import { EmployeePolicies } from './EmployeePolicies';
import { EmployeeHipaaTraining, EmployeeTrainingVideos } from './EmployeeTraining';
+import { isAuditorOnly } from './isAuditorOnly';
import { OffboardingChecklist } from './OffboardingChecklist';
type EmployeeTab =
@@ -74,19 +75,23 @@ export function Employee({
const pathname = usePathname();
const router = useRouter();
+ // CS-416: auditor-only members aren't subject to background checks, so the
+ // tab, header status, and content are hidden for them.
+ const showBackgroundCheck = backgroundCheckStepEnabled && !isAuditorOnly(employee.role);
+
const availableTabs: EmployeeTab[] = [
'details',
'policies',
'training',
...(hasHipaaFramework ? (['hipaa'] as EmployeeTab[]) : []),
'device',
- ...(backgroundCheckStepEnabled ? (['background-check'] as EmployeeTab[]) : []),
+ ...(showBackgroundCheck ? (['background-check'] as EmployeeTab[]) : []),
...(employee.offboardDate ? (['offboarding'] as EmployeeTab[]) : []),
];
const resolveTab = (): EmployeeTab => {
if (
- backgroundCheckStepEnabled &&
+ showBackgroundCheck &&
(searchParams.get('background_check_step') || searchParams.get('background_check_billing'))
) {
return 'background-check';
@@ -131,7 +136,7 @@ export function Employee({
employeeName={employee.user.name ?? 'Employee'}
orgId={orgId}
backgroundCheck={initialBackgroundCheck}
- backgroundCheckStepEnabled={backgroundCheckStepEnabled}
+ backgroundCheckStepEnabled={showBackgroundCheck}
memberBackgroundCheckExempt={memberExempt}
/>
}
@@ -149,12 +154,10 @@ export function Employee({
Training Videos
{hasHipaaFramework && HIPAA Training}
Device
- {backgroundCheckStepEnabled && (
+ {showBackgroundCheck && (
Background Check
)}
- {employee.offboardDate && (
- Offboarding
- )}
+ {employee.offboardDate && Offboarding}
@@ -195,7 +198,7 @@ export function Employee({
/>
)}
- {backgroundCheckStepEnabled && (
+ {showBackgroundCheck && (
{
+ it('is true when the only role is auditor', () => {
+ expect(isAuditorOnly('auditor')).toBe(true);
+ });
+
+ it('is true when every role entry is auditor', () => {
+ expect(isAuditorOnly('auditor, auditor')).toBe(true);
+ });
+
+ it('is false when another role is present alongside auditor', () => {
+ expect(isAuditorOnly('auditor,employee')).toBe(false);
+ expect(isAuditorOnly('owner')).toBe(false);
+ });
+
+ it('is false for empty or missing roles', () => {
+ expect(isAuditorOnly('')).toBe(false);
+ expect(isAuditorOnly(null)).toBe(false);
+ expect(isAuditorOnly(undefined)).toBe(false);
+ });
+});
diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/isAuditorOnly.ts b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/isAuditorOnly.ts
new file mode 100644
index 000000000..2bb83019b
--- /dev/null
+++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/isAuditorOnly.ts
@@ -0,0 +1,13 @@
+/**
+ * A member is "auditor-only" when every one of their (comma-separated) roles is
+ * `auditor`. Auditor-only members are external reviewers and are not subject to
+ * people-security requirements such as background checks (CS-416). A member with
+ * `auditor` plus another role still carries that other role's obligations.
+ */
+export function isAuditorOnly(role: string | null | undefined): boolean {
+ const roles = (role ?? '')
+ .split(',')
+ .map((r) => r.trim())
+ .filter(Boolean);
+ return roles.length > 0 && roles.every((r) => r === 'auditor');
+}