Skip to content

Commit f1b02c2

Browse files
authored
Merge pull request #2732 from trycompai/main
[comp] Production Deploy
2 parents 6c8c111 + a1a6586 commit f1b02c2

195 files changed

Lines changed: 3083 additions & 866 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/api/src/admin-organizations/admin-organizations.controller.spec.ts‎

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ describe('AdminOrganizationsController', () => {
4343
const mockService = {
4444
listOrganizations: jest.fn(),
4545
getOrganization: jest.fn(),
46-
setAccess: jest.fn(),
46+
updateOrganization: jest.fn(),
4747
inviteMember: jest.fn(),
4848
listInvitations: jest.fn(),
4949
revokeInvitation: jest.fn(),
@@ -134,24 +134,54 @@ describe('AdminOrganizationsController', () => {
134134
});
135135
});
136136

137-
describe('activate', () => {
138-
it('should call setAccess with true', async () => {
139-
mockService.setAccess.mockResolvedValue({ success: true });
137+
describe('update', () => {
138+
it('PATCH with { hasAccess: true } calls service with (id, { hasAccess: true })', async () => {
139+
mockService.updateOrganization.mockResolvedValue({ success: true });
140140

141-
const result = await controller.activate('org_1');
141+
const result = await controller.update('org_1', { hasAccess: true });
142142

143-
expect(mockService.setAccess).toHaveBeenCalledWith('org_1', true);
143+
expect(mockService.updateOrganization).toHaveBeenCalledWith('org_1', {
144+
hasAccess: true,
145+
});
144146
expect(result).toEqual({ success: true });
145147
});
146-
});
147148

148-
describe('deactivate', () => {
149-
it('should call setAccess with false', async () => {
150-
mockService.setAccess.mockResolvedValue({ success: true });
149+
it('PATCH with { hasAccess: false } calls service with (id, { hasAccess: false })', async () => {
150+
mockService.updateOrganization.mockResolvedValue({ success: true });
151+
152+
const result = await controller.update('org_1', { hasAccess: false });
151153

152-
const result = await controller.deactivate('org_1');
154+
expect(mockService.updateOrganization).toHaveBeenCalledWith('org_1', {
155+
hasAccess: false,
156+
});
157+
expect(result).toEqual({ success: true });
158+
});
159+
160+
it('PATCH with { backgroundCheckStepEnabled: false } calls service with correct body', async () => {
161+
mockService.updateOrganization.mockResolvedValue({ success: true });
162+
163+
const result = await controller.update('org_1', {
164+
backgroundCheckStepEnabled: false,
165+
});
153166

154-
expect(mockService.setAccess).toHaveBeenCalledWith('org_1', false);
167+
expect(mockService.updateOrganization).toHaveBeenCalledWith('org_1', {
168+
backgroundCheckStepEnabled: false,
169+
});
170+
expect(result).toEqual({ success: true });
171+
});
172+
173+
it('PATCH with multiple fields passes both through', async () => {
174+
mockService.updateOrganization.mockResolvedValue({ success: true });
175+
176+
const result = await controller.update('org_1', {
177+
hasAccess: true,
178+
backgroundCheckStepEnabled: false,
179+
});
180+
181+
expect(mockService.updateOrganization).toHaveBeenCalledWith('org_1', {
182+
hasAccess: true,
183+
backgroundCheckStepEnabled: false,
184+
});
155185
expect(result).toEqual({ success: true });
156186
});
157187
});

‎apps/api/src/admin-organizations/admin-organizations.controller.ts‎

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { PurgeOrganizationService } from './purge-organization.service';
2121
import { AdminAuditLogInterceptor } from './admin-audit-log.interceptor';
2222
import { SkipAdminAuditLog } from './skip-admin-audit-log.decorator';
2323
import { InviteMemberDto } from './dto/invite-member.dto';
24+
import { UpdateAdminOrganizationDto } from './dto/update-admin-organization.dto';
2425
import { PurgeOrganizationDto } from './dto/purge-organization.dto';
2526

2627
@ApiExcludeController()
@@ -100,18 +101,21 @@ export class AdminOrganizationsController {
100101
return this.service.getOrganization(id);
101102
}
102103

103-
@Patch(':id/activate')
104-
@ApiOperation({ summary: 'Activate organization access (platform admin)' })
105-
@Throttle({ default: { ttl: 60000, limit: 5 } })
106-
async activate(@Param('id') id: string) {
107-
return this.service.setAccess(id, true);
108-
}
109-
110-
@Patch(':id/deactivate')
111-
@ApiOperation({ summary: 'Deactivate organization access (platform admin)' })
112-
@Throttle({ default: { ttl: 60000, limit: 5 } })
113-
async deactivate(@Param('id') id: string) {
114-
return this.service.setAccess(id, false);
104+
@Patch(':id')
105+
@ApiOperation({ summary: 'Update organization fields (platform admin)' })
106+
@Throttle({ default: { ttl: 60000, limit: 10 } })
107+
@UsePipes(
108+
new ValidationPipe({
109+
whitelist: true,
110+
forbidNonWhitelisted: true,
111+
transform: true,
112+
}),
113+
)
114+
async update(
115+
@Param('id') id: string,
116+
@Body() body: UpdateAdminOrganizationDto,
117+
) {
118+
return this.service.updateOrganization(id, body);
115119
}
116120

117121
@Post(':id/invite')

‎apps/api/src/admin-organizations/admin-organizations.service.spec.ts‎

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,11 @@ describe('AdminOrganizationsService', () => {
183183
const result = await service.getOrganization('org_1');
184184
expect(result.id).toBe('org_1');
185185
expect(result.members).toHaveLength(1);
186+
expect(mockDb.organization.findUnique).toHaveBeenCalledWith(
187+
expect.objectContaining({
188+
select: expect.objectContaining({ backgroundCheckStepEnabled: true }),
189+
}),
190+
);
186191
});
187192

188193
it('should throw NotFoundException for missing org', async () => {
@@ -194,8 +199,8 @@ describe('AdminOrganizationsService', () => {
194199
});
195200
});
196201

197-
describe('setAccess', () => {
198-
it('should activate an organization', async () => {
202+
describe('updateOrganization', () => {
203+
it('single-field update (hasAccess)', async () => {
199204
(mockDb.organization.findUnique as jest.Mock).mockResolvedValue({
200205
id: 'org_1',
201206
});
@@ -204,7 +209,9 @@ describe('AdminOrganizationsService', () => {
204209
hasAccess: true,
205210
});
206211

207-
const result = await service.setAccess('org_1', true);
212+
const result = await service.updateOrganization('org_1', {
213+
hasAccess: true,
214+
});
208215

209216
expect(result.success).toBe(true);
210217
expect(mockDb.organization.update).toHaveBeenCalledWith({
@@ -213,30 +220,54 @@ describe('AdminOrganizationsService', () => {
213220
});
214221
});
215222

216-
it('should deactivate an organization', async () => {
223+
it('single-field update (backgroundCheckStepEnabled)', async () => {
217224
(mockDb.organization.findUnique as jest.Mock).mockResolvedValue({
218225
id: 'org_1',
219226
});
220227
(mockDb.organization.update as jest.Mock).mockResolvedValue({
221228
id: 'org_1',
222-
hasAccess: false,
229+
backgroundCheckStepEnabled: false,
223230
});
224231

225-
const result = await service.setAccess('org_1', false);
232+
const result = await service.updateOrganization('org_1', {
233+
backgroundCheckStepEnabled: false,
234+
});
226235

227236
expect(result.success).toBe(true);
228237
expect(mockDb.organization.update).toHaveBeenCalledWith({
229238
where: { id: 'org_1' },
230-
data: { hasAccess: false },
239+
data: { backgroundCheckStepEnabled: false },
231240
});
232241
});
233242

234-
it('should throw NotFoundException for missing org', async () => {
243+
it('multi-field update', async () => {
244+
(mockDb.organization.findUnique as jest.Mock).mockResolvedValue({
245+
id: 'org_1',
246+
});
247+
(mockDb.organization.update as jest.Mock).mockResolvedValue({
248+
id: 'org_1',
249+
hasAccess: true,
250+
backgroundCheckStepEnabled: false,
251+
});
252+
253+
const result = await service.updateOrganization('org_1', {
254+
hasAccess: true,
255+
backgroundCheckStepEnabled: false,
256+
});
257+
258+
expect(result.success).toBe(true);
259+
expect(mockDb.organization.update).toHaveBeenCalledWith({
260+
where: { id: 'org_1' },
261+
data: { hasAccess: true, backgroundCheckStepEnabled: false },
262+
});
263+
});
264+
265+
it('throws NotFoundException for missing org', async () => {
235266
(mockDb.organization.findUnique as jest.Mock).mockResolvedValue(null);
236267

237-
await expect(service.setAccess('org_missing', true)).rejects.toThrow(
238-
NotFoundException,
239-
);
268+
await expect(
269+
service.updateOrganization('org_missing', { hasAccess: true }),
270+
).rejects.toThrow(NotFoundException);
240271
});
241272
});
242273

‎apps/api/src/admin-organizations/admin-organizations.service.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
import { AuditLogEntityType, db } from '@db';
88
import { triggerEmail } from '../email/trigger-email';
99
import { InviteEmail } from '../email/templates/invite-member';
10+
import { UpdateAdminOrganizationDto } from './dto/update-admin-organization.dto';
1011

1112
@Injectable()
1213
export class AdminOrganizationsService {
@@ -238,6 +239,7 @@ export class AdminOrganizationsService {
238239
hasAccess: true,
239240
onboardingCompleted: true,
240241
website: true,
242+
backgroundCheckStepEnabled: true,
241243
members: {
242244
where: { isActive: true, deactivated: false },
243245
select: {
@@ -265,7 +267,7 @@ export class AdminOrganizationsService {
265267
return org;
266268
}
267269

268-
async setAccess(id: string, hasAccess: boolean) {
270+
async updateOrganization(id: string, data: UpdateAdminOrganizationDto) {
269271
const org = await db.organization.findUnique({ where: { id } });
270272

271273
if (!org) {
@@ -274,7 +276,7 @@ export class AdminOrganizationsService {
274276

275277
await db.organization.update({
276278
where: { id },
277-
data: { hasAccess },
279+
data,
278280
});
279281

280282
return { success: true };

‎apps/api/src/admin-organizations/admin-security.spec.ts‎

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -155,24 +155,14 @@ describe('Admin controllers security baseline', () => {
155155
expect(AdminIntegrationsController).toBeDefined();
156156
});
157157

158-
describe('destructive endpoints have tighter rate limits', () => {
159-
it('activate has a limit of 5 per minute', () => {
158+
describe('update endpoint rate limits', () => {
159+
it('update has a limit of 10 per minute', () => {
160160
const metadata = Reflect.getMetadata(
161161
'THROTTLER:LIMIT',
162-
AdminOrganizationsController.prototype.activate,
162+
AdminOrganizationsController.prototype.update,
163163
);
164164
if (metadata) {
165-
expect(metadata).toBeLessThanOrEqual(5);
166-
}
167-
});
168-
169-
it('deactivate has a limit of 5 per minute', () => {
170-
const metadata = Reflect.getMetadata(
171-
'THROTTLER:LIMIT',
172-
AdminOrganizationsController.prototype.deactivate,
173-
);
174-
if (metadata) {
175-
expect(metadata).toBeLessThanOrEqual(5);
165+
expect(metadata).toBeLessThanOrEqual(10);
176166
}
177167
});
178168
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ApiPropertyOptional } from '@nestjs/swagger';
2+
import { IsBoolean, IsOptional } from 'class-validator';
3+
4+
export class UpdateAdminOrganizationDto {
5+
@ApiPropertyOptional({
6+
description:
7+
'Whether the organization has platform access (controls dashboard login).',
8+
})
9+
@IsOptional()
10+
@IsBoolean()
11+
hasAccess?: boolean;
12+
13+
@ApiPropertyOptional({
14+
description:
15+
'When true, the organization requires background checks for people completion. When false, BG checks are bypassed and excluded from counts.',
16+
})
17+
@IsOptional()
18+
@IsBoolean()
19+
backgroundCheckStepEnabled?: boolean;
20+
}

‎apps/api/src/frameworks/frameworks-people-score.helper.spec.ts‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ describe('computePeopleScore', () => {
6262
employees: members,
6363
securityTrainingStepEnabled: false,
6464
deviceAgentStepEnabled: false,
65+
backgroundCheckStepEnabled: true,
6566
hasHipaaFramework: false,
6667
});
6768

@@ -76,4 +77,52 @@ describe('computePeopleScore', () => {
7677
distinct: ['memberId'],
7778
});
7879
});
80+
81+
it('counts all employees when BG checks are enabled and all have completed checks', async () => {
82+
(mockDb.backgroundCheckRequest.findMany as jest.Mock).mockResolvedValue([
83+
{ memberId: 'mem_1' },
84+
{ memberId: 'mem_2' },
85+
]);
86+
87+
const score = await computePeopleScore({
88+
organizationId: 'org_1',
89+
allPolicies: [],
90+
employees: members,
91+
securityTrainingStepEnabled: false,
92+
deviceAgentStepEnabled: false,
93+
backgroundCheckStepEnabled: true,
94+
hasHipaaFramework: false,
95+
});
96+
97+
expect(score).toEqual({ total: 2, completed: 2 });
98+
});
99+
100+
it('treats employees as complete without a BG check when backgroundCheckStepEnabled is false', async () => {
101+
// BG-check mock value is irrelevant — the bypass path never calls findMany.
102+
const score = await computePeopleScore({
103+
organizationId: 'org_1',
104+
allPolicies: [],
105+
employees: members,
106+
securityTrainingStepEnabled: false,
107+
deviceAgentStepEnabled: false,
108+
backgroundCheckStepEnabled: false,
109+
hasHipaaFramework: false,
110+
});
111+
112+
expect(score).toEqual({ total: 2, completed: 2 });
113+
});
114+
115+
it('skips the BG-check query entirely when backgroundCheckStepEnabled is false', async () => {
116+
await computePeopleScore({
117+
organizationId: 'org_1',
118+
allPolicies: [],
119+
employees: members,
120+
securityTrainingStepEnabled: false,
121+
deviceAgentStepEnabled: false,
122+
backgroundCheckStepEnabled: false,
123+
hasHipaaFramework: false,
124+
});
125+
126+
expect(mockDb.backgroundCheckRequest.findMany).not.toHaveBeenCalled();
127+
});
79128
});

0 commit comments

Comments
 (0)