Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
113 commits
Select commit Hold shift + click to select a range
5fb026e
feat(db): add timeline models and enums for compliance timeline feature
Marfuen Apr 7, 2026
30b1845
feat(api): add default timeline template definitions as code constants
Marfuen Apr 7, 2026
dfd5a79
feat(api): add timeline date recalculation helper with tests
Marfuen Apr 7, 2026
d97509d
feat(api): add timelines service with CRUD, activation, pause/resume,…
Marfuen Apr 7, 2026
624bc37
refactor(api): split timelines service into focused modules under 300…
Marfuen Apr 7, 2026
3d8bd18
feat(api): add timeline DTOs, controllers, module registration, and S…
Marfuen Apr 7, 2026
03f1859
feat(api): auto-create timeline on framework add and auto-complete ph…
Marfuen Apr 7, 2026
c16b176
feat(app): add SWR hooks for timelines and admin timeline management
Marfuen Apr 7, 2026
703ffe6
feat(app): add reusable TimelinePhaseBar component
Marfuen Apr 7, 2026
36cab56
fix: add generated prisma to gitignore and add completedBy include to…
Marfuen Apr 7, 2026
96366b4
feat(app): add TimelineOverview component to Overview page
Marfuen Apr 7, 2026
a5e2511
feat(app): add expanded timeline view to framework detail page
Marfuen Apr 7, 2026
ec1a5c9
feat(app): add admin timeline template management page
Marfuen Apr 7, 2026
3dde499
feat(app): add Timeline tab to admin org detail page
Marfuen Apr 7, 2026
ec851da
fix(api): auto-create timelines for existing framework instances on f…
Marfuen Apr 7, 2026
cd7ba20
fix(api): fix type error in date helper tests
Marfuen Apr 7, 2026
343768b
feat(api): smart timeline backfill using trust status, scores, and ta…
Marfuen Apr 7, 2026
12ea2a7
fix(app): use design system tokens, remove outer card, add next cycle…
Marfuen Apr 7, 2026
e7694dc
fix(app): improve phase bar with progress indicator and add timeline …
Marfuen Apr 7, 2026
e9f9459
fix(api): use 'Attestation' instead of 'Certification' for HIPAA and …
Marfuen Apr 7, 2026
c5c9ff8
feat: separate SOC 2 Type 1 and Type 2 timelines, add date markers to…
Marfuen Apr 7, 2026
c8ea360
fix: replace em dashes with hyphens in template names
Marfuen Apr 7, 2026
2843d84
fix(api): wrap admin timeline endpoints in { data, count } response f…
Marfuen Apr 7, 2026
da8d20a
fix(app): use correct timeline status values (DRAFT/ACTIVE/PAUSED/COM…
Marfuen Apr 7, 2026
649c211
fix(app): fix missing closing brace in TimelineCard
Marfuen Apr 7, 2026
c91d69b
fix(app): replace table layout with stacked phase cards in admin time…
Marfuen Apr 7, 2026
6e4fc58
feat(app): add tabs to Overview page with Timeline tab and compact te…
Marfuen Apr 8, 2026
32301c1
fix(app): simplify timeline teaser to clean single-line banner withou…
Marfuen Apr 8, 2026
3e9bf2b
fix(app): remove timeline teaser from Overview tab
Marfuen Apr 8, 2026
2fa6761
fix(app): remove redundant header and summary from Timeline tab
Marfuen Apr 8, 2026
a28c12d
fix(app): use defaultDurationWeeks for template phase duration (fixes…
Marfuen Apr 8, 2026
05b531c
fix: recalculate end date and downstream phases when duration or star…
Marfuen Apr 8, 2026
283c2dd
feat(api): auto-complete timeline phases immediately when tasks are m…
Marfuen Apr 8, 2026
0b934d9
feat: add phase grouping with groupLabel for SOC 2 Type 2 sub-phases
Marfuen Apr 8, 2026
8216d04
feat(admin): add delete, reset, and recreate timeline actions with co…
Marfuen Apr 8, 2026
e8f30e0
fix(app): close confirmation dialogs immediately on confirm
Marfuen Apr 8, 2026
74941dc
fix(api): recreate also deletes DB templates so code defaults are re-…
Marfuen Apr 8, 2026
842c000
fix(api): force-refresh templates from code defaults during recreate
Marfuen Apr 8, 2026
d1f9521
fix(app): add bracket lines connecting group label to phase edges
Marfuen Apr 8, 2026
33c7c4d
fix(app): render grouped phases as cohesive blocks with no internal gaps
Marfuen Apr 8, 2026
a8dcb86
fix(app): show one start/end date per group instead of per sub-phase
Marfuen Apr 8, 2026
98a718d
feat: add AUTO_POLICIES and AUTO_PEOPLE completion types for independ…
Marfuen Apr 8, 2026
7c80fc3
fix(api): use getOverviewScores for AUTO_POLICIES and AUTO_PEOPLE checks
Marfuen Apr 8, 2026
d6698b3
fix(api): use correct property names from scores (total/published)
Marfuen Apr 8, 2026
8420dd4
fix(app): wrap phase groups to new rows on small screens
Marfuen Apr 8, 2026
561989e
fix(app): vertical phase stack on mobile, horizontal bar on lg+ screens
Marfuen Apr 8, 2026
88ddc1d
fix(app): keep grouped phases in one row on mobile, only ungrouped ph…
Marfuen Apr 8, 2026
2b4d0cb
fix(app): treat ungrouped phases as individual groups so each gets it…
Marfuen Apr 8, 2026
0f583d2
fix(app): show group labels and dates per row on mobile phase bar
Marfuen Apr 8, 2026
9ccab6a
fix(app): replace mobile phase bars with clean vertical step list
Marfuen Apr 8, 2026
9e75332
fix(app): compact horizontal bars on mobile without per-phase dates
Marfuen Apr 8, 2026
4914e75
fix(app): remove checkmarks from completed phase bars
Marfuen Apr 8, 2026
82a4f75
fix(app): add diagonal stripe pattern to unfilled portion of active p…
Marfuen Apr 8, 2026
749eb68
fix(app): use primary color for stripe pattern on active phase
Marfuen Apr 8, 2026
8b6bde2
fix(app): use oklch color-mix for stripe pattern instead of hsl wrapper
Marfuen Apr 8, 2026
35bcfce
fix(app): reduce stripe opacity to 10% for subtler effect
Marfuen Apr 8, 2026
74076f0
fix(app): reduce gap between phases from 3px to 1px
Marfuen Apr 8, 2026
5f265ad
fix(api): rename SOC 2 Type 2 - Year 1 to SOC 2 Type 2
Marfuen Apr 8, 2026
aafdcd3
fix(app): remove redundant Started/Est. completion text from timeline…
Marfuen Apr 8, 2026
8765217
feat: live completion percentages for AUTO_* phases, revert if metric…
Marfuen Apr 8, 2026
06690aa
fix(api): rename SOC 2 sub-phases to Policies, Evidence, People
Marfuen Apr 8, 2026
cc92e9c
feat(app): show live completion % and fill for all AUTO_* sub-phases …
Marfuen Apr 8, 2026
378714c
feat(app): show avg completion % on group label, single cohesive fill…
Marfuen Apr 8, 2026
b09e9ef
fix(app): show individual sub-phase percentages inside grouped bar
Marfuen Apr 8, 2026
440c043
fix(app): add pulsing progress marker to grouped phase bar
Marfuen Apr 8, 2026
3214d6e
feat(app): add dedicated full-page timeline template editor
Marfuen Apr 8, 2026
4b6c0aa
feat(app): framework dropdown in template creation and editing
Marfuen Apr 8, 2026
0623660
feat(api): centralized Slack notifications for phase completion, time…
Marfuen Apr 8, 2026
53b24ff
fix(api): assign transaction result to txResult variable
Marfuen Apr 8, 2026
673cee7
fix(api): use template name in Slack notifications to distinguish SOC…
Marfuen Apr 8, 2026
1bad711
fix(api): use Slack Block Kit for cleaner notification formatting
Marfuen Apr 8, 2026
4245d14
feat(api): add clickable org link to Slack notifications with admin t…
Marfuen Apr 8, 2026
b52d19b
fix(api): show org ID in Slack notification messages
Marfuen Apr 8, 2026
c52372a
fix(app): only show stripes on IN_PROGRESS phases, plain muted for PE…
Marfuen Apr 8, 2026
0dccb27
fix(api): only advance next phase to IN_PROGRESS when all prior phase…
Marfuen Apr 8, 2026
214140e
fix(app): show framework name instead of ID in template editor dropdown
Marfuen Apr 8, 2026
c0ba9a1
feat(app): group sub-phases under parent card in template editor
Marfuen Apr 8, 2026
480c5bb
feat(api): add Auditor Review + Draft Report phases to SOC 2 templates
Marfuen Apr 8, 2026
44039eb
fix(app): phase numbering counts groups as 1, label below Phase N, ad…
Marfuen Apr 8, 2026
cf003a0
fix(app): use DS Text component for weeks label instead of custom span
Marfuen Apr 8, 2026
030a672
fix(app): add column labels (Name, Duration, Completion) above sub-ph…
Marfuen Apr 8, 2026
4ddbba4
feat(app): add confirmation dialogs to all delete actions in template…
Marfuen Apr 8, 2026
26249bb
fix(api): add groupLabel to phase template DTOs and service
Marfuen Apr 8, 2026
4cc3eb4
fix(api): remove AdminAuditLogInterceptor from template controller (n…
Marfuen Apr 8, 2026
c8ff63c
fix(api): pass groupLabel from DTO to service in addPhase and updateP…
Marfuen Apr 8, 2026
3c86132
feat(api): add timeline auto-completion hooks to policies, members, t…
Marfuen Apr 8, 2026
d09cd3c
feat(api): add Preparing for Audit group (Policies, Evidence, People)…
Marfuen Apr 8, 2026
5bfaa90
fix(api): auto-complete phases at 100% on page load, not just on events
Marfuen Apr 8, 2026
27c73e0
fix(app): group phase cards visually in admin timeline, show template…
Marfuen Apr 8, 2026
aeadaae
fix(api): use actual completion date as anchor for downstream phase r…
Marfuen Apr 8, 2026
31dbc40
chore(db): add migration for AUTO_POLICIES and AUTO_PEOPLE completion…
Marfuen Apr 8, 2026
a93ee64
feat: add Start Next Cycle action for completed timelines (admin only)
Marfuen Apr 9, 2026
bb23b9f
fix(app): use render prop on AlertDialogTrigger to avoid nested buttons
Marfuen Apr 9, 2026
6ea12a5
fix(app): destructure onStartNextCycle from TimelineActions props
Marfuen Apr 9, 2026
40b7708
feat(app): group timeline cycles by framework with stacked card effec…
Marfuen Apr 9, 2026
55c3e54
fix(app): fix stacked card effect to show behind the main card
Marfuen Apr 9, 2026
3256d0a
fix(app): stacked card effect as thin strips peeking below main card
Marfuen Apr 9, 2026
0b92fda
fix(app): simple lip button below card for showing previous cycles
Marfuen Apr 9, 2026
6a1f392
fix(app): remove bottom border radius from card when previous cycles …
Marfuen Apr 9, 2026
dbbbb99
fix(app): subtle muted background on previous cycles lip
Marfuen Apr 9, 2026
3f4fd05
fix(app): reduce lip padding and font size for sleeker look
Marfuen Apr 9, 2026
b94fae2
refactor(app): migrate TimelineOverview to DS components (Card, Badge…
Marfuen Apr 9, 2026
a78c433
fix(app): improve timeline card padding, title size, and spacing
Marfuen Apr 9, 2026
025d2c6
fix(app): remove bg from previous cycles lip, only show on hover
Marfuen Apr 9, 2026
b88f6ac
fix(app): remove lip, show cycle badge (Year 2) when multiple cycles …
Marfuen Apr 9, 2026
89075be
fix(app): show Year badge on all timelines, computed from cycle count…
Marfuen Apr 9, 2026
f340da8
fix(api): always use now() for completedAt and endDate when auto-comp…
Marfuen Apr 9, 2026
47f38af
fix(api): pin phase dates on early completion so recalculation doesn'…
Marfuen Apr 9, 2026
278c36d
fix(api): use min(endDate, now) for completedAt in backfill to avoid …
Marfuen Apr 9, 2026
3aced7e
chore: update OpenAPI spec with timeline endpoints
Marfuen Apr 10, 2026
fd1535e
Merge branch 'main' of https://github.com/trycompai/comp into mariano…
Marfuen Apr 13, 2026
93ab1dd
feat(timelines): add track-based SOC2 timeline model
Marfuen Apr 13, 2026
7945e11
feat(findings): integrate timelines module and add AUTO_FINDINGS phas…
Marfuen Apr 15, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ packages/*/dist

# Generated Prisma Client
**/src/db/generated/
packages/db/prisma/src/generated/

# Release script
scripts/sync-release-branch.sh
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/admin-organizations/admin-context.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ import {
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { ApiExcludeController, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import { PlatformAdminGuard } from '../auth/platform-admin.guard';
import { ContextService } from '../context/context.service';
import { CreateContextDto } from '../context/dto/create-context.dto';
import { UpdateContextDto } from '../context/dto/update-context.dto';
import { AdminAuditLogInterceptor } from './admin-audit-log.interceptor';

@ApiExcludeController()
@ApiTags('Admin - Context')
@Controller({ path: 'admin/organizations', version: '1' })
@UseGuards(PlatformAdminGuard)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
UseInterceptors,
BadRequestException,
} from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { ApiExcludeController, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import { PlatformAdminGuard } from '../auth/platform-admin.guard';
import { EvidenceFormsService } from '../evidence-forms/evidence-forms.service';
Expand All @@ -18,6 +18,7 @@ import {
buildPlatformAdminAuthContext,
} from './platform-admin-auth-context';

@ApiExcludeController()
@ApiTags('Admin - Evidence')
@Controller({ path: 'admin/organizations', version: '1' })
@UseGuards(PlatformAdminGuard)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
ValidationPipe,
BadRequestException,
} from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { ApiExcludeController, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import { FindingStatus } from '@db';
import { PlatformAdminGuard } from '../auth/platform-admin.guard';
Expand All @@ -23,6 +23,7 @@ import { UpdateFindingDto } from '../findings/dto/update-finding.dto';
import { AdminAuditLogInterceptor } from './admin-audit-log.interceptor';
import type { AdminRequest } from './platform-admin-auth-context';

@ApiExcludeController()
@ApiTags('Admin - Findings')
@Controller({ path: 'admin/organizations', version: '1' })
@UseGuards(PlatformAdminGuard)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ import {
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
import { ApiExcludeController, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import { PlatformAdminGuard } from '../auth/platform-admin.guard';
import { AdminOrganizationsService } from './admin-organizations.service';
import { AdminAuditLogInterceptor } from './admin-audit-log.interceptor';
import { InviteMemberDto } from './dto/invite-member.dto';

@ApiExcludeController()
@ApiTags('Admin - Organizations')
@Controller({ path: 'admin/organizations', version: '1' })
@UseGuards(PlatformAdminGuard)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
ValidationPipe,
BadRequestException,
} from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { ApiExcludeController, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import { db } from '@db';
import {
Expand All @@ -32,6 +32,7 @@ interface UpdatePolicyBody {
frequency?: string | null;
}

@ApiExcludeController()
@ApiTags('Admin - Policies')
@Controller({ path: 'admin/organizations', version: '1' })
@UseGuards(PlatformAdminGuard)
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/admin-organizations/admin-tasks.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
ValidationPipe,
BadRequestException,
} from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { ApiExcludeController, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import {
TaskStatus,
Expand All @@ -36,6 +36,7 @@ interface UpdateTaskBody {
frequency?: string | null;
}

@ApiExcludeController()
@ApiTags('Admin - Tasks')
@Controller({ path: 'admin/organizations', version: '1' })
@UseGuards(PlatformAdminGuard)
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/admin-organizations/admin-vendors.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
ValidationPipe,
BadRequestException,
} from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { ApiExcludeController, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import { VendorCategory, VendorStatus } from '@db';
import { PlatformAdminGuard } from '../auth/platform-admin.guard';
Expand All @@ -26,6 +26,7 @@ interface UpdateVendorBody {
category?: string;
}

@ApiExcludeController()
@ApiTags('Admin - Vendors')
@Controller({ path: 'admin/organizations', version: '1' })
@UseGuards(PlatformAdminGuard)
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { SecretsModule } from './secrets/secrets.module';
import { SecurityPenetrationTestsModule } from './security-penetration-tests/security-penetration-tests.module';
import { StripeModule } from './stripe/stripe.module';
import { AdminOrganizationsModule } from './admin-organizations/admin-organizations.module';
import { TimelinesModule } from './timelines/timelines.module';

@Module({
imports: [
Expand Down Expand Up @@ -110,6 +111,7 @@ import { AdminOrganizationsModule } from './admin-organizations/admin-organizati
SecurityPenetrationTestsModule,
StripeModule,
AdminOrganizationsModule,
TimelinesModule,
],
controllers: [AppController],
providers: [
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/evidence-forms/evidence-forms.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Module } from '@nestjs/common';
import { AttachmentsModule } from '@/attachments/attachments.module';
import { AuthModule } from '@/auth/auth.module';
import { TimelinesModule } from '../timelines/timelines.module';
import { EvidenceFormsController } from './evidence-forms.controller';
import { EvidenceFormsService } from './evidence-forms.service';

@Module({
imports: [AuthModule, AttachmentsModule],
imports: [AuthModule, AttachmentsModule, TimelinesModule],
controllers: [EvidenceFormsController],
providers: [EvidenceFormsService],
exports: [EvidenceFormsService],
Expand Down
4 changes: 3 additions & 1 deletion apps/api/src/evidence-forms/evidence-forms.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ describe('EvidenceFormsService', () => {
getPresignedDownloadUrl: jest.fn(),
} as unknown as AttachmentsService;

const service = new EvidenceFormsService(attachmentsServiceMock);
const timelinesServiceMock = {} as unknown as import('../timelines/timelines.service').TimelinesService;

const service = new EvidenceFormsService(attachmentsServiceMock, timelinesServiceMock);
const mockedDb = db as unknown as MockDb;

beforeEach(() => {
Expand Down
33 changes: 30 additions & 3 deletions apps/api/src/evidence-forms/evidence-forms.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
type EvidenceFormFieldDefinition,
type EvidenceFormType,
} from './evidence-forms.definitions';
import { checkAutoCompletePhases } from '../frameworks/frameworks-timeline.helper';
import { TimelinesService } from '../timelines/timelines.service';

const listQuerySchema = z.object({
search: z.string().trim().optional(),
Expand Down Expand Up @@ -128,7 +130,10 @@ function normalizeSubmissionFormType<

@Injectable()
export class EvidenceFormsService {
constructor(private readonly attachmentsService: AttachmentsService) {}
constructor(
private readonly attachmentsService: AttachmentsService,
private readonly timelinesService: TimelinesService,
) {}

private requireJwtUser(authContext: AuthContext): string {
if (authContext.isApiKey || authContext.authType === 'api-key') {
Expand Down Expand Up @@ -406,6 +411,12 @@ export class EvidenceFormsService {
where: { id: params.submissionId },
});

// Check timeline auto-completion after evidence deletion
checkAutoCompletePhases({
organizationId: params.organizationId,
timelinesService: this.timelinesService,
}).catch(() => {});

return { success: true, id: params.submissionId };
}

Expand Down Expand Up @@ -463,7 +474,7 @@ export class EvidenceFormsService {
throw new BadRequestException(message);
}

return await db.evidenceSubmission
const submission = await db.evidenceSubmission
.create({
data: {
organizationId: params.organizationId,
Expand All @@ -482,6 +493,14 @@ export class EvidenceFormsService {
},
})
.then(normalizeSubmissionFormType);

// Check timeline auto-completion after evidence submission
checkAutoCompletePhases({
organizationId: params.organizationId,
timelinesService: this.timelinesService,
}).catch(() => {});

return submission;
}

async uploadFile(params: {
Expand Down Expand Up @@ -575,7 +594,7 @@ export class EvidenceFormsService {
const downloadUrl =
await this.attachmentsService.getPresignedDownloadUrl(fileKey);

return await db.evidenceSubmission
const submission = await db.evidenceSubmission
.create({
data: {
organizationId: params.organizationId,
Expand All @@ -601,6 +620,14 @@ export class EvidenceFormsService {
},
})
.then(normalizeSubmissionFormType);

// Check timeline auto-completion after evidence upload submission
checkAutoCompletePhases({
organizationId: params.organizationId,
timelinesService: this.timelinesService,
}).catch(() => {});

return submission;
}

async exportCsv(params: {
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/findings/findings.module.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Module } from '@nestjs/common';
import { AuthModule } from '../auth/auth.module';
import { TimelinesModule } from '../timelines/timelines.module';
import { NovuService } from '../notifications/novu.service';
import { FindingAuditService } from './finding-audit.service';
import { FindingNotifierService } from './finding-notifier.service';
import { FindingsController } from './findings.controller';
import { FindingsService } from './findings.service';

@Module({
imports: [AuthModule],
imports: [AuthModule, TimelinesModule],
controllers: [FindingsController],
providers: [
FindingsService,
Expand Down
102 changes: 102 additions & 0 deletions apps/api/src/findings/findings.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { FindingsService } from './findings.service';

jest.mock('@db', () => ({
db: {
finding: {
update: jest.fn(),
},
user: {
findUnique: jest.fn(),
},
},
EvidenceFormType: {},
FindingStatus: {
open: 'open',
ready_for_review: 'ready_for_review',
needs_revision: 'needs_revision',
closed: 'closed',
},
FindingType: {
soc2: 'soc2',
iso27001: 'iso27001',
},
}));

jest.mock('../frameworks/frameworks-timeline.helper', () => ({
checkAutoCompletePhases: jest.fn().mockResolvedValue(undefined),
}));

jest.mock('../timelines/timelines.service', () => ({
TimelinesService: class TimelinesService {},
}));

import { db } from '@db';
import { checkAutoCompletePhases } from '../frameworks/frameworks-timeline.helper';

const mockDb = db as jest.Mocked<typeof db>;

describe('FindingsService', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('triggers timeline AUTO_FINDINGS check when a finding is closed', async () => {
const findingAuditService = {
logFindingStatusChanged: jest.fn().mockResolvedValue(undefined),
logFindingTypeChanged: jest.fn().mockResolvedValue(undefined),
logFindingContentUpdated: jest.fn().mockResolvedValue(undefined),
};
const findingNotifierService = {
notifyReadyForReview: jest.fn(),
notifyNeedsRevision: jest.fn(),
notifyFindingClosed: jest.fn(),
};
const timelinesService = {};

const service = new FindingsService(
findingAuditService as any,
findingNotifierService as any,
timelinesService as any,
);

jest.spyOn(service, 'findById').mockResolvedValue({
id: 'fnd_1',
status: 'open',
type: 'soc2',
content: 'Issue content',
task: { id: 'tsk_1', title: 'Collect evidence' },
evidenceSubmission: null,
evidenceFormType: null,
createdById: 'mem_1',
} as any);

(mockDb.finding.update as jest.Mock).mockResolvedValue({
id: 'fnd_1',
status: 'closed',
type: 'soc2',
content: 'Issue content',
createdBy: null,
template: null,
task: { id: 'tsk_1', title: 'Collect evidence' },
});
(mockDb.user.findUnique as jest.Mock).mockResolvedValue({
name: 'Auditor',
email: 'auditor@example.com',
});

await service.update(
'org_1',
'fnd_1',
{ status: 'closed' } as any,
['auditor'],
false,
'usr_1',
'mem_1',
);

expect(checkAutoCompletePhases).toHaveBeenCalledWith({
organizationId: 'org_1',
timelinesService,
});
});
});
Loading
Loading