Merge branch 'main' into feat/task

This commit is contained in:
Muhammad Zhafran Ilham
2026-06-10 03:30:32 +07:00
committed by GitHub
10 changed files with 951 additions and 8 deletions
@@ -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();
+5
View File
@@ -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();
@@ -497,19 +497,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;
@@ -517,10 +514,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);
}