Skip to content

Commit c59d30a

Browse files
committed
cohrot certificate pr draft
1 parent 98dadd3 commit c59d30a

27 files changed

+280
-758
lines changed

src/app.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { TaskProcessorModule } from '@/task-processor/task-processor.module';
2424
import { WinstonModule } from 'nest-winston';
2525
import { winstonConfig } from '@/common/logger.config';
2626
import { FeedbackModule } from '@/feedback/feedback.module';
27-
import { CertificatesModule } from '@/certificates/certificates.module';
27+
import { CertificatesModule } from '@/cetificate/certificates.module';
2828

2929
@Module({
3030
imports: [

src/services/cetificate/automated-certificate.service.ts renamed to src/cetificate/automated-certificate.service.ts

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,23 @@ import { Injectable, Logger } from '@nestjs/common';
22
import { InjectRepository } from '@nestjs/typeorm';
33
import { Repository } from 'typeorm';
44
import { Cohort } from '@/entities/cohort.entity';
5-
import { User } from '@/entities/user.entity';
65
import { writeFileSync, mkdirSync, existsSync } from 'fs';
76
import { join } from 'path';
87
import { ServiceError } from '@/common/errors';
8+
import { CertificateServiceProvider } from '@/cetificate/certificate.service.provider';
99
import {
10-
CertificateServiceProvider,
1110
CertificateType,
12-
} from '@/services/cetificate/certificate.service.provider';
13-
14-
interface UserWithScore {
15-
user: User;
16-
totalScore: number;
17-
}
18-
19-
interface GeneratedCertificate {
20-
userId: string;
21-
userName: string;
22-
certificateType: CertificateType;
23-
filePath: string;
24-
downloadPath: string;
25-
}
11+
CertificateRank,
12+
} from '@/cetificate/enums/certificate.enum';
13+
import {
14+
UserWithScore,
15+
GeneratedCertificate,
16+
} from '@/cetificate/types/certificate.interface';
17+
import {
18+
CERTIFICATE_PATHS,
19+
generateCertificateFileName,
20+
generateCertificateDownloadPath,
21+
} from '@/cetificate/constants/certificate.constants';
2622

2723
@Injectable()
2824
export class AutomatedCertificateService {
@@ -80,10 +76,10 @@ export class AutomatedCertificateService {
8076
});
8177
}
8278

83-
// Sort by total score in descending order and take top 10
79+
// Sort by total score in descending order and take top performers
8480
return usersWithScores
8581
.sort((a, b) => b.totalScore - a.totalScore)
86-
.slice(0, 10);
82+
.slice(0, CertificateRank.TOP_TEN);
8783
}
8884

8985
async generateCertificatesForCohort(
@@ -109,8 +105,7 @@ export class AutomatedCertificateService {
109105

110106
const outputDir = join(
111107
process.cwd(),
112-
'outputs',
113-
'certificates',
108+
CERTIFICATE_PATHS.outputPath,
114109
cohortId,
115110
);
116111

@@ -123,7 +118,7 @@ export class AutomatedCertificateService {
123118
for (let i = 0; i < topPerformers.length; i++) {
124119
const { user } = topPerformers[i];
125120
const certificateType: CertificateType =
126-
i < 3
121+
i < CertificateRank.TOP_THREE
127122
? CertificateType.PERFORMER
128123
: CertificateType.PARTICIPANT;
129124

@@ -140,7 +135,10 @@ export class AutomatedCertificateService {
140135
certificateType,
141136
);
142137

143-
const fileName = `${user.id}_${certificateType}.pdf`;
138+
const fileName = generateCertificateFileName(
139+
user.id,
140+
certificateType,
141+
);
144142
const filePath = join(outputDir, fileName);
145143

146144
writeFileSync(filePath, new Uint8Array(pdfBuffer));
@@ -150,7 +148,10 @@ export class AutomatedCertificateService {
150148
userName,
151149
certificateType,
152150
filePath,
153-
downloadPath: `/certificates/${cohortId}/${fileName}`,
151+
downloadPath: generateCertificateDownloadPath(
152+
cohortId,
153+
fileName,
154+
),
154155
});
155156

156157
this.logger.log(
@@ -194,7 +195,7 @@ export class AutomatedCertificateService {
194195

195196
const { user } = topPerformers[userIndex];
196197
const certificateType: CertificateType =
197-
userIndex < 3
198+
userIndex < CertificateRank.TOP_THREE
198199
? CertificateType.PERFORMER
199200
: CertificateType.PARTICIPANT;
200201

@@ -213,16 +214,18 @@ export class AutomatedCertificateService {
213214

214215
const outputDir = join(
215216
process.cwd(),
216-
'outputs',
217-
'certificates',
217+
CERTIFICATE_PATHS.outputPath,
218218
cohortId,
219219
);
220220

221221
if (!existsSync(outputDir)) {
222222
mkdirSync(outputDir, { recursive: true });
223223
}
224224

225-
const fileName = `${user.id}_${certificateType}.pdf`;
225+
const fileName = generateCertificateFileName(
226+
user.id,
227+
certificateType,
228+
);
226229
const filePath = join(outputDir, fileName);
227230

228231
writeFileSync(filePath, new Uint8Array(pdfBuffer));
@@ -236,7 +239,10 @@ export class AutomatedCertificateService {
236239
userName,
237240
certificateType,
238241
filePath,
239-
downloadPath: `/certificates/${cohortId}/${fileName}`,
242+
downloadPath: generateCertificateDownloadPath(
243+
cohortId,
244+
fileName,
245+
),
240246
};
241247
} catch (error) {
242248
this.logger.error(

src/services/cetificate/certificate.module.ts renamed to src/cetificate/certificate.module.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Module } from '@nestjs/common';
22
import { TypeOrmModule } from '@nestjs/typeorm';
33
import { Cohort } from '@/entities/cohort.entity';
4-
import { CertificateServiceProvider } from '@/services/cetificate/certificate.service.provider';
5-
import { AutomatedCertificateService } from '@/services/cetificate/automated-certificate.service';
4+
import { CertificateServiceProvider } from '@/cetificate/certificate.service.provider';
5+
import { AutomatedCertificateService } from '@/cetificate/automated-certificate.service';
66

77
@Module({
88
imports: [TypeOrmModule.forFeature([Cohort])],

src/services/cetificate/certificate.service.provider.ts renamed to src/cetificate/certificate.service.provider.ts

Lines changed: 48 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@ import fontkit from '@pdf-lib/fontkit';
44
import { readFileSync } from 'fs';
55
import { join } from 'path';
66
import { CohortType } from '@/common/enum';
7-
8-
export enum CertificateType {
9-
PARTICIPANT = 'participant',
10-
PERFORMER = 'performer',
11-
}
7+
import {
8+
CertificateType,
9+
CertificateFontSize,
10+
} from '@/cetificate/enums/certificate.enum';
11+
import {
12+
getCohortTypeAbbreviation,
13+
CERTIFICATE_NAME_POSITION,
14+
CERTIFICATE_DATE_POSITION,
15+
CERTIFICATE_TEXT_COLOR,
16+
CERTIFICATE_PATHS,
17+
CERTIFICATE_FONTS,
18+
CERTIFICATE_DATE_FORMAT,
19+
} from '@/cetificate/constants/certificate.constants';
1220

1321
@Injectable()
1422
export class CertificateServiceProvider {
@@ -17,36 +25,29 @@ export class CertificateServiceProvider {
1725
private readonly robotoFontBytes: Buffer;
1826

1927
constructor() {
20-
const fontsPath = join(process.cwd(), 'assets', 'fonts');
28+
const fontsPath = join(process.cwd(), CERTIFICATE_PATHS.fontsPath);
2129
this.nautigalFontBytes = readFileSync(
22-
join(fontsPath, 'TheNautigal-Regular.ttf'),
30+
join(fontsPath, CERTIFICATE_FONTS.nautigal),
2331
);
2432
this.robotoFontBytes = readFileSync(
25-
join(fontsPath, 'Roboto-VariableFont_wdth,wght.ttf'),
33+
join(fontsPath, CERTIFICATE_FONTS.roboto),
2634
);
2735
}
2836

29-
private getCohortTypeAbbreviation(cohortType: CohortType): string {
30-
const mapping: Record<CohortType, string> = {
31-
[CohortType.MASTERING_BITCOIN]: 'MB',
32-
[CohortType.LEARNING_BITCOIN_FROM_COMMAND_LINE]: 'CLI',
33-
[CohortType.PROGRAMMING_BITCOIN]: 'PB',
34-
[CohortType.BITCOIN_PROTOCOL_DEVELOPMENT]: 'BPD',
35-
};
36-
return mapping[cohortType];
37-
}
38-
3937
private calculateFontSize(
4038
text: string,
4139
maxWidth: number,
4240
font: any,
4341
): number {
44-
let fontSize = 240; // Start with a large font size
42+
let fontSize = CertificateFontSize.NAME_MAX;
4543
let textWidth = font.widthOfTextAtSize(text, fontSize);
4644

4745
// Decrease font size until the text fits within maxWidth
48-
while (textWidth > maxWidth && fontSize > 10) {
49-
fontSize -= 4;
46+
while (
47+
textWidth > maxWidth &&
48+
fontSize > CertificateFontSize.NAME_MIN
49+
) {
50+
fontSize -= CertificateFontSize.NAME_STEP;
5051
textWidth = font.widthOfTextAtSize(text, fontSize);
5152
}
5253

@@ -59,11 +60,10 @@ export class CertificateServiceProvider {
5960
certificateType: CertificateType,
6061
): Promise<Buffer> {
6162
try {
62-
const cohortAbbr = this.getCohortTypeAbbreviation(cohortType);
63+
const cohortAbbr = getCohortTypeAbbreviation(cohortType);
6364
const certificatesPath = join(
6465
process.cwd(),
65-
'assets',
66-
'certificates',
66+
CERTIFICATE_PATHS.certificatesPath,
6767
certificateType,
6868
);
6969
const templatePath = join(certificatesPath, `${cohortAbbr}.pdf`);
@@ -92,7 +92,7 @@ export class CertificateServiceProvider {
9292
// Create a string of name and measure its width and height in our custom font
9393
const nameFontSize = this.calculateFontSize(
9494
name,
95-
1774,
95+
CERTIFICATE_NAME_POSITION.maxWidth!,
9696
nautigalFont,
9797
);
9898
const nameWidth = nautigalFont.widthOfTextAtSize(
@@ -102,29 +102,38 @@ export class CertificateServiceProvider {
102102

103103
// Write name on the page
104104
page.drawText(name, {
105-
x: 1510 - nameWidth / 2,
106-
y: 974,
105+
x: CERTIFICATE_NAME_POSITION.x - nameWidth / 2,
106+
y: CERTIFICATE_NAME_POSITION.y,
107107
size: nameFontSize,
108108
font: nautigalFont,
109-
color: rgb(0.2, 0.2, 0.2),
109+
color: rgb(
110+
CERTIFICATE_TEXT_COLOR.r,
111+
CERTIFICATE_TEXT_COLOR.g,
112+
CERTIFICATE_TEXT_COLOR.b,
113+
),
110114
});
111115

112116
const now = new Date();
113-
const date = now.toLocaleDateString('en-us', {
114-
year: 'numeric',
115-
month: 'long',
116-
day: 'numeric',
117-
});
118-
const dateFontSize = 48;
119-
const dateWidth = robotoFont.widthOfTextAtSize(date, dateFontSize);
117+
const date = now.toLocaleDateString(
118+
'en-us',
119+
CERTIFICATE_DATE_FORMAT,
120+
);
121+
const dateWidth = robotoFont.widthOfTextAtSize(
122+
date,
123+
CERTIFICATE_DATE_POSITION.size,
124+
);
120125

121126
// Write date on the page
122127
page.drawText(date, {
123-
x: 848 - dateWidth / 2,
124-
y: 244,
125-
size: dateFontSize,
128+
x: CERTIFICATE_DATE_POSITION.x - dateWidth / 2,
129+
y: CERTIFICATE_DATE_POSITION.y,
130+
size: CERTIFICATE_DATE_POSITION.size,
126131
font: robotoFont,
127-
color: rgb(0.2, 0.2, 0.2),
132+
color: rgb(
133+
CERTIFICATE_TEXT_COLOR.r,
134+
CERTIFICATE_TEXT_COLOR.g,
135+
CERTIFICATE_TEXT_COLOR.b,
136+
),
128137
});
129138

130139
// Serialize the PDFDocument to bytes (a Uint8Array)

src/certificates/certificates.controller.ts renamed to src/cetificate/certificates.controller.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,19 @@ import {
1010
} from '@nestjs/common';
1111
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
1212
import { Response } from 'express';
13-
import { AutomatedCertificateService } from '@/services/cetificate/automated-certificate.service';
13+
import { AutomatedCertificateService } from '@/cetificate/automated-certificate.service';
1414
import { Roles } from '@/auth/roles.decorator';
1515
import { UserRole } from '@/common/enum';
1616
import { GetUser } from '@/decorators/user.decorator';
1717
import { User } from '@/entities/user.entity';
1818
import { readFileSync, existsSync } from 'fs';
1919
import { join } from 'path';
20+
import { CertificateType } from '@/cetificate/enums/certificate.enum';
21+
import {
22+
CERTIFICATE_PATHS,
23+
generateCertificateFileName,
24+
generateCertificateDownloadPath,
25+
} from '@/cetificate/constants/certificate.constants';
2026

2127
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
2228
@ApiTags('Certificates')
@@ -97,8 +103,7 @@ export class CertificatesController {
97103

98104
const filePath = join(
99105
process.cwd(),
100-
'outputs',
101-
'certificates',
106+
CERTIFICATE_PATHS.outputPath,
102107
cohortId,
103108
fileName,
104109
);
@@ -126,8 +131,7 @@ export class CertificatesController {
126131
) {
127132
const outputDir = join(
128133
process.cwd(),
129-
'outputs',
130-
'certificates',
134+
CERTIFICATE_PATHS.outputPath,
131135
cohortId,
132136
);
133137

@@ -138,9 +142,15 @@ export class CertificatesController {
138142
};
139143
}
140144

141-
// Check for participant certificate
142-
const participantFile = `${user.id}_participant.pdf`;
143-
const performerFile = `${user.id}_performer.pdf`;
145+
// Check for participant and performer certificates
146+
const participantFile = generateCertificateFileName(
147+
user.id,
148+
CertificateType.PARTICIPANT,
149+
);
150+
const performerFile = generateCertificateFileName(
151+
user.id,
152+
CertificateType.PERFORMER,
153+
);
144154

145155
const participantPath = join(outputDir, participantFile);
146156
const performerPath = join(outputDir, performerFile);
@@ -149,8 +159,11 @@ export class CertificatesController {
149159
return {
150160
message: 'Certificate found',
151161
certificate: {
152-
certificateType: 'performer',
153-
downloadUrl: `/certificates/${cohortId}/${performerFile}`,
162+
certificateType: CertificateType.PERFORMER,
163+
downloadUrl: generateCertificateDownloadPath(
164+
cohortId,
165+
performerFile,
166+
),
154167
},
155168
};
156169
}
@@ -159,8 +172,11 @@ export class CertificatesController {
159172
return {
160173
message: 'Certificate found',
161174
certificate: {
162-
certificateType: 'participant',
163-
downloadUrl: `/certificates/${cohortId}/${participantFile}`,
175+
certificateType: CertificateType.PARTICIPANT,
176+
downloadUrl: generateCertificateDownloadPath(
177+
cohortId,
178+
participantFile,
179+
),
164180
},
165181
};
166182
}

0 commit comments

Comments
 (0)