feat: nambahin certificate system
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
const certificateService = require('../services/certificate.service');
|
||||
const { response, error } = require('../../../helpers/utils/wrapper');
|
||||
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
|
||||
const { InternalServerError } = require('../../../helpers/error');
|
||||
const { isValidPayload } = require('../../../helpers/utils/validator');
|
||||
const { claimCertificateModel } = require('../models/certificate.model');
|
||||
|
||||
const getActor = (req) => ({
|
||||
id: req.id,
|
||||
role: req.role,
|
||||
email: req.email,
|
||||
});
|
||||
|
||||
class CertificateController {
|
||||
async claimCertificate(req, res) {
|
||||
try {
|
||||
const validatePayload = isValidPayload(req.body, claimCertificateModel);
|
||||
|
||||
if (validatePayload.err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
{ err: validatePayload.err, data: null },
|
||||
'Data claim certificate tidak valid',
|
||||
ERROR.EXPECTATION_FAILED
|
||||
);
|
||||
}
|
||||
|
||||
const actor = getActor(req);
|
||||
const result = await certificateService.claimCertificate(validatePayload.data, actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Certificate claimed 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);
|
||||
const result = await certificateService.getMyCertificates(actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Certificates retrieved successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getCertificateDetail(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const actor = getActor(req);
|
||||
const result = await certificateService.getCertificateDetail(id, actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Certificate details retrieved successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getProjectCertificates(req, res) {
|
||||
try {
|
||||
const { id_project } = req.params;
|
||||
const actor = getActor(req);
|
||||
const result = await certificateService.getProjectCertificates(id_project, actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Project certificates retrieved successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getAllCertificates(req, res) {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const result = await certificateService.getAllCertificates(actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'All certificates retrieved successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async verifyCertificate(req, res) {
|
||||
try {
|
||||
const certificate_no = req.query.certificate_no || req.params.certificate_no;
|
||||
const result = await certificateService.verifyCertificate(certificate_no);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Certificate validated successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async verifyCertificateByUuid(req, res) {
|
||||
try {
|
||||
const { uuid } = req.params;
|
||||
const result = await certificateService.verifyCertificateByUuid(uuid);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Certificate validated successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new CertificateController();
|
||||
@@ -0,0 +1,5 @@
|
||||
const certificateController = require('./controllers/certificate.controller');
|
||||
|
||||
module.exports = {
|
||||
certificateController,
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
const joi = require('joi');
|
||||
|
||||
const claimCertificateModel = 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.',
|
||||
}),
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
claimCertificateModel,
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
const prisma = require('../../../helpers/db/db_connection');
|
||||
|
||||
class CertificateRepository {
|
||||
async create(certificateData) {
|
||||
return prisma.certificate.create({
|
||||
data: certificateData,
|
||||
include: this.certificateDetailInclude(),
|
||||
});
|
||||
}
|
||||
|
||||
async findById(id) {
|
||||
return prisma.certificate.findUnique({
|
||||
where: { id: parseInt(id) },
|
||||
include: this.certificateDetailInclude(),
|
||||
});
|
||||
}
|
||||
|
||||
async findByUserAndProject(id_user, id_project) {
|
||||
return prisma.certificate.findUnique({
|
||||
where: {
|
||||
id_project_id_user: {
|
||||
id_project: parseInt(id_project),
|
||||
id_user: parseInt(id_user),
|
||||
},
|
||||
},
|
||||
include: this.certificateDetailInclude(),
|
||||
});
|
||||
}
|
||||
|
||||
async findUserCertificates(id_user) {
|
||||
return prisma.certificate.findMany({
|
||||
where: { id_user: parseInt(id_user) },
|
||||
include: this.certificateDetailInclude(),
|
||||
orderBy: { issued_at: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
async findProjectCertificates(id_project) {
|
||||
return prisma.certificate.findMany({
|
||||
where: { id_project: parseInt(id_project) },
|
||||
include: this.certificateDetailInclude(),
|
||||
orderBy: { issued_at: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
async findAll() {
|
||||
return prisma.certificate.findMany({
|
||||
include: this.certificateDetailInclude(),
|
||||
orderBy: { issued_at: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
async findByCertificateNo(certificate_no) {
|
||||
return prisma.certificate.findUnique({
|
||||
where: { certificate_no },
|
||||
include: this.certificateDetailInclude(),
|
||||
});
|
||||
}
|
||||
|
||||
async findByUuid(uuid) {
|
||||
return prisma.certificate.findUnique({
|
||||
where: { uuid },
|
||||
include: this.certificateDetailInclude(),
|
||||
});
|
||||
}
|
||||
|
||||
async getProjectTasksAndSubmissions(id_project, id_user) {
|
||||
return prisma.project.findUnique({
|
||||
where: { id: parseInt(id_project) },
|
||||
select: {
|
||||
id: true,
|
||||
project_name: true,
|
||||
tasks: {
|
||||
select: {
|
||||
id: true,
|
||||
submissions: {
|
||||
where: { id_user: parseInt(id_user) },
|
||||
select: {
|
||||
id: true,
|
||||
submitted_at: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
members: {
|
||||
where: {
|
||||
id_user: parseInt(id_user),
|
||||
status: 'active',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
certificateDetailInclude() {
|
||||
return {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
full_name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
project: {
|
||||
select: {
|
||||
id: true,
|
||||
project_name: true,
|
||||
description: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new CertificateRepository();
|
||||
@@ -0,0 +1,225 @@
|
||||
const certificateRepository = require('../repositories/certificate.repository');
|
||||
const { data, error } = require('../../../helpers/utils/wrapper');
|
||||
const {
|
||||
BadRequestError,
|
||||
NotFoundError,
|
||||
ForbiddenError,
|
||||
ConflictError,
|
||||
} = require('../../../helpers/error');
|
||||
|
||||
class CertificateService {
|
||||
async claimCertificate(payload, actor) {
|
||||
try {
|
||||
if (!actor?.id) {
|
||||
return error(new BadRequestError('User ID is required'));
|
||||
}
|
||||
|
||||
if (actor.role !== 'intern') {
|
||||
return error(
|
||||
new ForbiddenError('Access denied: only interns can claim certificates'),
|
||||
);
|
||||
}
|
||||
|
||||
const { id_project } = payload;
|
||||
|
||||
const existingCert = await certificateRepository.findByUserAndProject(actor.id, id_project);
|
||||
if (existingCert) {
|
||||
return error(new ConflictError('Certificate already claimed for this project'));
|
||||
}
|
||||
|
||||
const projectInfo = await certificateRepository.getProjectTasksAndSubmissions(id_project, actor.id);
|
||||
|
||||
if (!projectInfo) {
|
||||
return error(new NotFoundError('Project not found'));
|
||||
}
|
||||
|
||||
if (!projectInfo.members || projectInfo.members.length === 0) {
|
||||
return error(
|
||||
new ForbiddenError('Access denied: you are not an active member of this project'),
|
||||
);
|
||||
}
|
||||
|
||||
if (!projectInfo.tasks || projectInfo.tasks.length === 0) {
|
||||
return error(
|
||||
new BadRequestError('Cannot claim certificate: this 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(
|
||||
`Cannot claim certificate: you have completed ${completedTasks.length} out of ${projectInfo.tasks.length} tasks`
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Generate certificate number berdsarkan format ini: CERT/YYYYMMDD/PRJ{id_project}/USR{id_user} [Rafi 09/06/2026 20:45]
|
||||
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}`;
|
||||
const certificateNo = `CERT/${dateString}/PRJ${id_project}/USR${actor.id}`;
|
||||
|
||||
const certificateData = {
|
||||
id_project: parseInt(id_project),
|
||||
id_user: parseInt(actor.id),
|
||||
certificate_no: certificateNo,
|
||||
issued_at: now,
|
||||
};
|
||||
|
||||
const certificate = await certificateRepository.create(certificateData);
|
||||
|
||||
return data(this.mapCertificate(certificate));
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async getMyCertificates(actor) {
|
||||
try {
|
||||
if (!actor?.id) {
|
||||
return error(new BadRequestError('User ID is required'));
|
||||
}
|
||||
|
||||
if (actor.role !== 'intern') {
|
||||
return error(
|
||||
new ForbiddenError('Access denied: only interns can view their certificates'),
|
||||
);
|
||||
}
|
||||
|
||||
const certificates = await certificateRepository.findUserCertificates(actor.id);
|
||||
return data(certificates.map((cert) => this.mapCertificate(cert)));
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async getCertificateDetail(id, actor) {
|
||||
try {
|
||||
if (!actor?.id) {
|
||||
return error(new BadRequestError('User ID is required'));
|
||||
}
|
||||
|
||||
const certificate = await certificateRepository.findById(id);
|
||||
if (!certificate) {
|
||||
return error(new NotFoundError('Certificate not found'));
|
||||
}
|
||||
|
||||
if (actor.role === 'intern' && certificate.id_user !== actor.id) {
|
||||
return error(
|
||||
new ForbiddenError("Access denied: you cannot view other intern's certificates"),
|
||||
);
|
||||
}
|
||||
|
||||
return data(this.mapCertificate(certificate));
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async getProjectCertificates(id_project, actor) {
|
||||
try {
|
||||
if (!actor?.id) {
|
||||
return error(new BadRequestError('Admin ID is required'));
|
||||
}
|
||||
|
||||
if (actor.role !== 'admin') {
|
||||
return error(
|
||||
new ForbiddenError('Access denied: only admins/mentors can view project certificates'),
|
||||
);
|
||||
}
|
||||
|
||||
const certificates = await certificateRepository.findProjectCertificates(id_project);
|
||||
return data(certificates.map((cert) => this.mapCertificate(cert)));
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async getAllCertificates(actor) {
|
||||
try {
|
||||
if (!actor?.id) {
|
||||
return error(new BadRequestError('Admin ID is required'));
|
||||
}
|
||||
|
||||
if (actor.role !== 'admin') {
|
||||
return error(
|
||||
new ForbiddenError('Access denied: only admins/mentors can view all certificates'),
|
||||
);
|
||||
}
|
||||
|
||||
const certificates = await certificateRepository.findAll();
|
||||
return data(certificates.map((cert) => this.mapCertificate(cert)));
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async verifyCertificate(certificate_no) {
|
||||
try {
|
||||
if (!certificate_no) {
|
||||
return error(new BadRequestError('Certificate number is required'));
|
||||
}
|
||||
|
||||
const certificate = await certificateRepository.findByCertificateNo(certificate_no);
|
||||
if (!certificate) {
|
||||
return error(new NotFoundError('Certificate not found or invalid'));
|
||||
}
|
||||
|
||||
return data(this.mapCertificate(certificate));
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async verifyCertificateByUuid(uuid) {
|
||||
try {
|
||||
if (!uuid) {
|
||||
return error(new BadRequestError('Certificate UUID is required'));
|
||||
}
|
||||
|
||||
const certificate = await certificateRepository.findByUuid(uuid);
|
||||
if (!certificate) {
|
||||
return error(new NotFoundError('Certificate not found or invalid'));
|
||||
}
|
||||
|
||||
return data({
|
||||
uuid: certificate.uuid,
|
||||
intern_name: certificate.user ? certificate.user.full_name : null,
|
||||
project_name: certificate.project ? certificate.project.project_name : null,
|
||||
created_at: certificate.issued_at,
|
||||
});
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
mapCertificate(cert) {
|
||||
if (!cert) return null;
|
||||
return {
|
||||
id: cert.id,
|
||||
uuid: cert.uuid,
|
||||
id_project: cert.id_project,
|
||||
id_user: cert.id_user,
|
||||
certificate_no: cert.certificate_no,
|
||||
issued_at: cert.issued_at,
|
||||
user: cert.user ? {
|
||||
id: cert.user.id,
|
||||
full_name: cert.user.full_name,
|
||||
email: cert.user.email,
|
||||
} : null,
|
||||
project: cert.project ? {
|
||||
id: cert.project.id,
|
||||
project_name: cert.project.project_name,
|
||||
description: cert.project.description,
|
||||
} : null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new CertificateService();
|
||||
@@ -477,19 +477,16 @@ class ProjectService {
|
||||
return error(new BadRequestError("Project ID and User ID are required"));
|
||||
}
|
||||
|
||||
// Check if project exists
|
||||
const project = await projectRepository.findById(id_project);
|
||||
if (!project) {
|
||||
return error(new NotFoundError("Project not found"));
|
||||
}
|
||||
|
||||
// Check if user exists and is active
|
||||
const user = await projectRepository.findUserByIdAndActive(id_user);
|
||||
if (!user) {
|
||||
return error(new NotFoundError("User not found or inactive"));
|
||||
}
|
||||
|
||||
// Check if already a member of this project
|
||||
const existingMembership = await projectRepository.findMembership(id_project, id_user);
|
||||
|
||||
let membership;
|
||||
@@ -497,10 +494,8 @@ class ProjectService {
|
||||
if (existingMembership.status === 'active') {
|
||||
return error(new ConflictError("User is already an active member of this project"));
|
||||
}
|
||||
// Reactivate membership
|
||||
membership = await projectRepository.updateMembershipStatus(existingMembership.id, 'active');
|
||||
} else {
|
||||
// Create new membership
|
||||
membership = await projectRepository.assignMember(id_project, id_user);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user