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

98.33% Statements 59/60
97.77% Branches 88/90
100% Functions 3/3
98.24% Lines 56/57

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 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131              39x     7x 7x 1x     6x           6x                                                     19x 19x 1x       18x 1x   17x 1x   16x 6x 6x 2x     14x 1x   13x 1x         12x 2x       12x                 12x 84x 6x         12x 12x 12x 12x 12x 12x 2x 12x 2x 12x 12x 12x 12x 12x 4x 12x 3x 12x 12x 12x 3x 12x 3x 12x 12x     12x 12x 1x   11x      
// Profile Service - Core company profile CRUD operations
import type { Dal } from "../../dal";
import type { Pro } from "../../db/schema";
import type { UpdateProfileInput, CompanyProfileResponse } from "./types";
import { NotFoundError, ValidationError } from "../../lib/errors";
 
export class ProfileService {
	constructor(private dal: Dal) {}
 
	async getCompanyProfile(proId: string): Promise<CompanyProfileResponse> {
		const pro = await this.dal.pros.findById(proId);
		if (!pro) {
			throw new NotFoundError("Pro not found");
		}
 
		const [leadership, certifications, testimonials] = await Promise.all([
			this.dal.companyProfile.getLeadership(proId),
			this.dal.companyProfile.getCertifications(proId),
			this.dal.companyProfile.getTestimonials(proId),
		]);
 
		return {
			tagline: pro.tagline ?? null,
			about: pro.about ?? null,
			establishedYear: pro.establishedYear ?? null,
			teamSizeCategory: pro.teamSize ?? null,
			areasServed: pro.serviceAreaIds ?? null,
			processDescription: pro.processDescription ?? null,
			socialLinks: {
				instagramHandle: pro.instagramHandle ?? null,
				facebookUrl: pro.facebookUrl ?? null,
				youtubeUrl: pro.youtubeUrl ?? null,
				googleBusinessUrl: pro.googleBusinessUrl ?? null,
				websiteUrl: pro.websiteUrl ?? null,
				linkedinUrl: pro.linkedinUrl ?? null,
				twitterUrl: pro.twitterUrl ?? null,
				pinterestUrl: pro.pinterestUrl ?? null,
			},
			leadershipTeam: leadership,
			certifications,
			testimonials,
		};
	}
 
	async updateProfile(
		proId: string,
		data: UpdateProfileInput,
	): Promise<Pro> {
		const pro = await this.dal.pros.findById(proId);
		if (!pro) {
			throw new NotFoundError("Pro not found");
		}
 
		// Validate fields
		if (data.tagline && data.tagline.length > 120) {
			throw new ValidationError("Tagline must be 120 characters or less");
		}
		if (data.about && data.about.length > 1000) {
			throw new ValidationError("About must be 1000 characters or less");
		}
		if (data.establishedYear) {
			const currentYear = new Date().getFullYear();
			if (data.establishedYear < 1950 || data.establishedYear > currentYear) {
				throw new ValidationError("Please enter a valid year");
			}
		}
		if (data.areasServed && data.areasServed.length > 10) {
			throw new ValidationError("You can add up to 10 areas");
		}
		if (data.processDescription && data.processDescription.length > 500) {
			throw new ValidationError(
				"Process description must be 500 characters or less",
			);
		}
		// Clean Instagram handle (remove @ if present)
		if (data.instagramHandle) {
			data.instagramHandle = data.instagramHandle.replace(/^@/, "");
		}
 
		// Ensure URLs have protocol
		const urlFields = [
			"facebookUrl",
			"youtubeUrl",
			"googleBusinessUrl",
			"websiteUrl",
			"linkedinUrl",
			"twitterUrl",
			"pinterestUrl",
		] as const;
		for (const field of urlFields) {
			if (data[field] && !data[field]?.startsWith("http")) {
				data[field] = `https://${data[field]}`;
			}
		}
 
		// Map UpdateProfileInput fields to pro column names
		const proUpdate: Record<string, unknown> = {};
		if (data.tagline !== undefined) proUpdate.tagline = data.tagline;
		if (data.about !== undefined) proUpdate.about = data.about;
		if (data.establishedYear !== undefined)
			proUpdate.establishedYear = data.establishedYear;
		if (data.teamSizeCategory !== undefined)
			proUpdate.teamSize = data.teamSizeCategory;
		if (data.areasServed !== undefined)
			proUpdate.serviceAreaIds = data.areasServed;
		if (data.processDescription !== undefined)
			proUpdate.processDescription = data.processDescription;
		if (data.instagramHandle !== undefined)
			proUpdate.instagramHandle = data.instagramHandle;
		if (data.facebookUrl !== undefined)
			proUpdate.facebookUrl = data.facebookUrl;
		if (data.youtubeUrl !== undefined)
			proUpdate.youtubeUrl = data.youtubeUrl;
		if (data.googleBusinessUrl !== undefined)
			proUpdate.googleBusinessUrl = data.googleBusinessUrl;
		if (data.websiteUrl !== undefined)
			proUpdate.websiteUrl = data.websiteUrl;
		if (data.linkedinUrl !== undefined)
			proUpdate.linkedinUrl = data.linkedinUrl;
		Iif (data.twitterUrl !== undefined) proUpdate.twitterUrl = data.twitterUrl;
		Iif (data.pinterestUrl !== undefined)
			proUpdate.pinterestUrl = data.pinterestUrl;
 
		const updated = await this.dal.pros.update(proId, proUpdate);
		if (!updated) {
			throw new NotFoundError("Pro not found");
		}
		return updated;
	}
}