feat: nambahin API route untuk generate certificate bagi Admin/Mentor
This commit is contained in:
@@ -3,7 +3,7 @@ const { response, error } = require('../../../helpers/utils/wrapper');
|
||||
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
|
||||
const { InternalServerError, BadRequestError } = require('../../../helpers/error');
|
||||
const { isValidPayload } = require('../../../helpers/utils/validator');
|
||||
const { claimCertificateModel } = require('../models/certificate.model');
|
||||
const { claimCertificateModel, generateCertificatesModel } = require('../models/certificate.model');
|
||||
|
||||
const getActor = (req) => ({
|
||||
id: req.id,
|
||||
@@ -45,6 +45,39 @@ class CertificateController {
|
||||
}
|
||||
}
|
||||
|
||||
async generateCertificates(req, res) {
|
||||
try {
|
||||
const validatePayload = isValidPayload(req.body, generateCertificatesModel);
|
||||
|
||||
if (validatePayload.err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
{ err: validatePayload.err, data: null },
|
||||
'Data generate certificate tidak valid',
|
||||
ERROR.EXPECTATION_FAILED
|
||||
);
|
||||
}
|
||||
|
||||
const actor = getActor(req);
|
||||
const result = await certificateService.generateCertificates(validatePayload.data, actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Certificates generated successfully', SUCCESS.CREATED);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getMyCertificates(req, res) {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
|
||||
@@ -8,6 +8,20 @@ const claimCertificateModel = joi.object().keys({
|
||||
}),
|
||||
});
|
||||
|
||||
const generateCertificatesModel = joi.object().keys({
|
||||
id_project: joi.number().integer().required().messages({
|
||||
'number.base': 'Project ID harus berupa angka.',
|
||||
'number.integer': 'Project ID harus berupa bilangan bulat.',
|
||||
'any.required': 'Project ID wajib diisi.',
|
||||
}),
|
||||
id_users: joi.array().items(joi.number().integer()).min(1).required().messages({
|
||||
'array.base': 'Intern IDs harus berupa array.',
|
||||
'array.min': 'Pilih minimal satu intern.',
|
||||
'any.required': 'Intern IDs wajib diisi.',
|
||||
}),
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
claimCertificateModel,
|
||||
generateCertificatesModel,
|
||||
};
|
||||
|
||||
@@ -80,6 +80,102 @@ class CertificateService {
|
||||
}
|
||||
}
|
||||
|
||||
async generateCertificates(payload, actor) {
|
||||
try {
|
||||
if (!actor?.id) {
|
||||
return error(new BadRequestError('User ID is required'));
|
||||
}
|
||||
|
||||
if (actor.role !== 'admin' && actor.role !== 'mentor') {
|
||||
return error(
|
||||
new ForbiddenError('Access denied: only admins and mentors can generate certificates'),
|
||||
);
|
||||
}
|
||||
|
||||
const { id_project, id_users } = payload;
|
||||
|
||||
const project = await certificateRepository.findProjectById(id_project);
|
||||
if (!project) {
|
||||
return error(new NotFoundError('Project not found'));
|
||||
}
|
||||
|
||||
// Authorization check:
|
||||
if (actor.role === 'mentor' && project.id_admin !== parseInt(actor.id)) {
|
||||
return error(
|
||||
new ForbiddenError('Access denied: you are not the mentor of this project'),
|
||||
);
|
||||
}
|
||||
|
||||
const validatedUsers = [];
|
||||
for (const userId of id_users) {
|
||||
const existingCert = await certificateRepository.findByUserAndProject(userId, id_project);
|
||||
if (existingCert) {
|
||||
return error(
|
||||
new ConflictError(`Certificate already generated/claimed for user ID ${userId}`),
|
||||
);
|
||||
}
|
||||
|
||||
const projectInfo = await certificateRepository.getProjectTasksAndSubmissions(id_project, userId);
|
||||
if (!projectInfo) {
|
||||
return error(new NotFoundError('Project not found'));
|
||||
}
|
||||
|
||||
if (!projectInfo.members || projectInfo.members.length === 0) {
|
||||
return error(
|
||||
new ForbiddenError(`User ID ${userId} is not an active member of this project`),
|
||||
);
|
||||
}
|
||||
|
||||
if (!projectInfo.tasks || projectInfo.tasks.length === 0) {
|
||||
return error(
|
||||
new BadRequestError(`Project does not have any tasks assigned yet`),
|
||||
);
|
||||
}
|
||||
|
||||
const completedTasks = projectInfo.tasks.filter(
|
||||
(task) => task.submissions && task.submissions.length > 0
|
||||
);
|
||||
|
||||
if (completedTasks.length < projectInfo.tasks.length) {
|
||||
return error(
|
||||
new BadRequestError(
|
||||
`User ID ${userId} is not eligible: completed ${completedTasks.length} out of ${projectInfo.tasks.length} tasks`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
validatedUsers.push(userId);
|
||||
}
|
||||
|
||||
const generatedCertificates = [];
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const date = String(now.getDate()).padStart(2, '0');
|
||||
const dateString = `${year}${month}${date}`;
|
||||
|
||||
for (const userId of validatedUsers) {
|
||||
const certificateNo = `CERT/${dateString}/PRJ${id_project}/USR${userId}`;
|
||||
const certificateData = {
|
||||
id_project: parseInt(id_project),
|
||||
id_user: parseInt(userId),
|
||||
certificate_no: certificateNo,
|
||||
issued_at: now,
|
||||
};
|
||||
|
||||
const certificate = await certificateRepository.create(certificateData);
|
||||
generatedCertificates.push(this.mapCertificate(certificate));
|
||||
}
|
||||
|
||||
return data({
|
||||
message: `${generatedCertificates.length} certificates generated successfully.`,
|
||||
certificates: generatedCertificates,
|
||||
});
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async getMyCertificates(actor) {
|
||||
try {
|
||||
if (!actor?.id) {
|
||||
|
||||
@@ -210,11 +210,25 @@ class ProjectRepository {
|
||||
submission_type: true,
|
||||
created_at: true,
|
||||
updated_at: true,
|
||||
submissions: {
|
||||
select: {
|
||||
id: true,
|
||||
id_user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
deadline_at: "asc",
|
||||
},
|
||||
},
|
||||
certificates: {
|
||||
select: {
|
||||
id_user: true,
|
||||
certificate_no: true,
|
||||
uuid: true,
|
||||
issued_at: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -358,6 +358,7 @@ class ProjectService {
|
||||
members:
|
||||
project.members?.map((member) => this.mapProjectMember(member)) || [],
|
||||
tasks: project.tasks || [],
|
||||
certificates: project.certificates || [],
|
||||
total_members: project.members?.length || 0,
|
||||
total_tasks: project.tasks?.length || 0,
|
||||
created_at: project.created_at,
|
||||
|
||||
@@ -114,6 +114,50 @@ const router = express.Router();
|
||||
*/
|
||||
router.post('/claim', verifyJWT, certificateController.claimCertificate);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /certificate-api/generate:
|
||||
* post:
|
||||
* summary: Batch generate certificates (Admin/Mentor only)
|
||||
* description: Mentors or Admins can batch-generate certificates for selected eligible interns of a project.
|
||||
* tags: [Certificate]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - id_project
|
||||
* - id_users
|
||||
* properties:
|
||||
* id_project:
|
||||
* type: integer
|
||||
* example: 2
|
||||
* id_users:
|
||||
* type: array
|
||||
* items:
|
||||
* type: integer
|
||||
* example: [5, 6]
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Certificates generated successfully
|
||||
* 400:
|
||||
* description: Bad request (not eligible or missing parameters)
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 403:
|
||||
* description: Forbidden (only mentors/admins of the project can generate)
|
||||
* 409:
|
||||
* description: Conflict (certificate already generated for one or more interns)
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.post('/generate', verifyJWT, isMentorOrAdmin, certificateController.generateCertificates);
|
||||
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /certificate-api/my-certificates:
|
||||
|
||||
Reference in New Issue
Block a user