Skip to content

Commit a78d96b

Browse files
fix: allow creating new version of policy even if empty or if using pdf
[dev] [Marfuen] mariano/fix-empty-policy-version
1 parent 3b9718e commit a78d96b

2 files changed

Lines changed: 140 additions & 9 deletions

File tree

apps/api/src/policies/policies.service.spec.ts

Lines changed: 138 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Test, type TestingModule } from '@nestjs/testing';
2+
import { NotFoundException } from '@nestjs/common';
23
import { PoliciesService } from './policies.service';
34
import { AttachmentsService } from '../attachments/attachments.service';
45
import { PolicyPdfRendererService } from '../trust-portal/policy-pdf-renderer.service';
@@ -8,10 +9,18 @@ jest.mock('@db', () => ({
89
policy: {
910
findMany: jest.fn(),
1011
findFirst: jest.fn(),
12+
findUnique: jest.fn(),
13+
update: jest.fn(),
14+
},
15+
policyVersion: {
16+
findUnique: jest.fn(),
17+
findFirst: jest.fn(),
18+
create: jest.fn(),
1119
update: jest.fn(),
1220
},
1321
member: {
1422
findMany: jest.fn(),
23+
findFirst: jest.fn(),
1524
},
1625
auditLog: {
1726
createMany: jest.fn(),
@@ -46,8 +55,19 @@ jest.mock('../utils/compliance-filters', () => ({
4655
// eslint-disable-next-line @typescript-eslint/no-require-imports
4756
const { db } = require('@db') as {
4857
db: {
49-
policy: { findMany: jest.Mock; findFirst: jest.Mock; update: jest.Mock };
50-
member: { findMany: jest.Mock };
58+
policy: {
59+
findMany: jest.Mock;
60+
findFirst: jest.Mock;
61+
findUnique: jest.Mock;
62+
update: jest.Mock;
63+
};
64+
policyVersion: {
65+
findUnique: jest.Mock;
66+
findFirst: jest.Mock;
67+
create: jest.Mock;
68+
update: jest.Mock;
69+
};
70+
member: { findMany: jest.Mock; findFirst: jest.Mock };
5171
auditLog: { createMany: jest.Mock };
5272
$transaction: jest.Mock;
5373
};
@@ -61,12 +81,17 @@ const { filterComplianceMembers: mockedFilterComplianceMembers } = require('../u
6181
describe('PoliciesService', () => {
6282
let service: PoliciesService;
6383

84+
const mockAttachmentsService = {
85+
copyPolicyVersionPdf: jest.fn(),
86+
deletePolicyVersionPdf: jest.fn(),
87+
};
88+
6489
beforeEach(async () => {
6590
jest.clearAllMocks();
6691
const module: TestingModule = await Test.createTestingModule({
6792
providers: [
6893
PoliciesService,
69-
{ provide: AttachmentsService, useValue: {} },
94+
{ provide: AttachmentsService, useValue: mockAttachmentsService },
7095
{ provide: PolicyPdfRendererService, useValue: {} },
7196
],
7297
}).compile();
@@ -216,4 +241,114 @@ describe('PoliciesService', () => {
216241
]);
217242
});
218243
});
244+
245+
describe('createVersion', () => {
246+
const organizationId = 'org_123';
247+
const policyId = 'pol_1';
248+
const userId = 'usr_1';
249+
250+
const setupHappyPath = ({
251+
policyContent,
252+
currentVersionContent,
253+
policyPdfUrl,
254+
currentVersionPdfUrl,
255+
}: {
256+
policyContent: unknown[];
257+
currentVersionContent: unknown[] | null;
258+
policyPdfUrl: string | null;
259+
currentVersionPdfUrl: string | null;
260+
}) => {
261+
db.member.findFirst.mockResolvedValue({ id: 'mem_1' });
262+
db.policy.findUnique.mockResolvedValue({
263+
id: policyId,
264+
organizationId,
265+
content: policyContent,
266+
pdfUrl: policyPdfUrl,
267+
currentVersion: currentVersionContent
268+
? {
269+
id: 'pv_1',
270+
content: currentVersionContent,
271+
pdfUrl: currentVersionPdfUrl,
272+
}
273+
: null,
274+
versions: [],
275+
});
276+
db.$transaction.mockImplementation(async (cb: (tx: unknown) => unknown) =>
277+
cb({
278+
policyVersion: {
279+
findFirst: jest.fn().mockResolvedValue({ version: 1 }),
280+
create: jest.fn().mockResolvedValue({ id: 'pv_2' }),
281+
},
282+
}),
283+
);
284+
};
285+
286+
it('creates a version when editor content is empty but a PDF exists', async () => {
287+
setupHappyPath({
288+
policyContent: [],
289+
currentVersionContent: [],
290+
policyPdfUrl: 's3://bucket/policy.pdf',
291+
currentVersionPdfUrl: 's3://bucket/policy.pdf',
292+
});
293+
mockAttachmentsService.copyPolicyVersionPdf.mockResolvedValue(
294+
's3://bucket/new.pdf',
295+
);
296+
297+
const result = await service.createVersion(
298+
policyId,
299+
organizationId,
300+
{},
301+
userId,
302+
);
303+
304+
expect(result).toEqual({ versionId: 'pv_2', version: 2 });
305+
expect(mockAttachmentsService.copyPolicyVersionPdf).toHaveBeenCalled();
306+
});
307+
308+
it('creates a version when both editor content is empty and no PDF exists', async () => {
309+
setupHappyPath({
310+
policyContent: [],
311+
currentVersionContent: [],
312+
policyPdfUrl: null,
313+
currentVersionPdfUrl: null,
314+
});
315+
316+
const result = await service.createVersion(
317+
policyId,
318+
organizationId,
319+
{},
320+
userId,
321+
);
322+
323+
expect(result).toEqual({ versionId: 'pv_2', version: 2 });
324+
expect(mockAttachmentsService.copyPolicyVersionPdf).not.toHaveBeenCalled();
325+
});
326+
327+
it('creates a version with non-empty editor content', async () => {
328+
setupHappyPath({
329+
policyContent: [{ type: 'paragraph' }],
330+
currentVersionContent: [{ type: 'paragraph' }],
331+
policyPdfUrl: null,
332+
currentVersionPdfUrl: null,
333+
});
334+
335+
const result = await service.createVersion(
336+
policyId,
337+
organizationId,
338+
{},
339+
userId,
340+
);
341+
342+
expect(result).toEqual({ versionId: 'pv_2', version: 2 });
343+
});
344+
345+
it('throws NotFound when the policy does not exist', async () => {
346+
db.member.findFirst.mockResolvedValue({ id: 'mem_1' });
347+
db.policy.findUnique.mockResolvedValue(null);
348+
349+
await expect(
350+
service.createVersion(policyId, organizationId, {}, userId),
351+
).rejects.toBeInstanceOf(NotFoundException);
352+
});
353+
});
219354
});

apps/api/src/policies/policies.service.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -599,15 +599,11 @@ export class PoliciesService {
599599
sourceVersion = requestedVersion;
600600
}
601601

602-
const contentForVersion = sourceVersion
602+
const contentForVersion = (sourceVersion
603603
? (sourceVersion.content as Prisma.InputJsonValue[])
604-
: (policy.content as Prisma.InputJsonValue[]);
604+
: (policy.content as Prisma.InputJsonValue[])) ?? [];
605605
const sourcePdfUrl = sourceVersion?.pdfUrl ?? policy.pdfUrl;
606606

607-
if (!contentForVersion || contentForVersion.length === 0) {
608-
throw new BadRequestException('No content to create version from');
609-
}
610-
611607
// S3 copy is done AFTER the transaction to prevent orphaned files on retry
612608
let createdVersion: { versionId: string; version: number } | null = null;
613609

0 commit comments

Comments
 (0)