All files / services/company-profile certifications.service.ts

100% Statements 36/36
100% Branches 43/43
100% Functions 4/4
100% Lines 36/36

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111              36x             11x 11x 1x       10x       10x   3x         3x       7x 1x   6x 1x   5x 4x 4x 1x         4x   4x                   4x               9x 9x 2x       7x 1x   6x 1x   5x 2x               3x       3x 1x     2x       4x 4x 2x     2x      
// Certifications Service - Certifications and awards management
import type { Dal } from "../../dal";
import type { ProCertification } from "../../db/schema";
import type { AddCertificationInput, UpdateCertificationInput } from "./types";
import { NotFoundError, ValidationError } from "../../lib/errors";
 
export class CertificationsService {
	constructor(private dal: Dal) {}
 
	async addCertification(
		proId: string,
		data: AddCertificationInput,
	): Promise<ProCertification> {
		// Verify pro exists
		const pro = await this.dal.pros.findById(proId);
		if (!pro) {
			throw new NotFoundError("Pro not found");
		}
 
		// Per-type limit (10 each for certifications / awards / memberships)
		const count = await this.dal.companyProfile.countCertificationsByType(
			proId,
			data.type,
		);
		if (count >= 10) {
			const label =
				data.type === "award"
					? "awards"
					: data.type === "membership"
						? "memberships"
						: "certifications";
			throw new ValidationError(`You can add up to 10 ${label}`);
		}
 
		// Validate fields
		if (!data.title || data.title.length > 100) {
			throw new ValidationError("Title is required (max 100 characters)");
		}
		if (data.issuer && data.issuer.length > 100) {
			throw new ValidationError("Issuer must be 100 characters or less");
		}
		if (data.year) {
			const currentYear = new Date().getFullYear();
			if (data.year < 1990 || data.year > currentYear) {
				throw new ValidationError("Please enter a valid year");
			}
		}
 
		const maxOrder =
			await this.dal.companyProfile.getMaxCertificationOrder(proId);
 
		const cert = await this.dal.companyProfile.createCertification({
			proId,
			title: data.title,
			issuer: data.issuer,
			year: data.year,
			type: data.type,
			imageUrl: data.imageUrl,
			displayOrder: maxOrder + 1,
		});
 
		return cert;
	}
 
	async updateCertification(
		proId: string,
		certId: number,
		data: UpdateCertificationInput,
	): Promise<ProCertification> {
		const cert = await this.dal.companyProfile.getCertificationById(certId);
		if (!cert || cert.proId !== proId) {
			throw new NotFoundError("Certification not found");
		}
 
		// Validate fields
		if (data.title !== undefined && (!data.title || data.title.length > 100)) {
			throw new ValidationError("Title is required (max 100 characters)");
		}
		if (data.issuer && data.issuer.length > 100) {
			throw new ValidationError("Issuer must be 100 characters or less");
		}
		if (data.year) {
			const currentYear = new Date().getFullYear();
			/* v8 ignore start -- defensive guard: valid year always used in tests */
			if (data.year < 1990 || data.year > currentYear) {
				throw new ValidationError("Please enter a valid year");
			}
			/* v8 ignore stop */
		}
 
		const updated = await this.dal.companyProfile.updateCertification(
			certId,
			data,
		);
		if (!updated) {
			throw new NotFoundError("Certification not found");
		}
 
		return updated;
	}
 
	async deleteCertification(proId: string, certId: number): Promise<void> {
		const cert = await this.dal.companyProfile.getCertificationById(certId);
		if (!cert || cert.proId !== proId) {
			throw new NotFoundError("Certification not found");
		}
 
		await this.dal.companyProfile.deleteCertification(certId);
	}
}