getcert-backend / api /v2 /_services /certificationSystem.ts
ggop213's picture
fix(cert): auto-issue certificate on payment confirmation & sync course to 100% certified
46ab86e
Raw History Blame Contribute Delete
5.3 kB
import type { CertificatePayload, CertificationTrack, ScoreBreakdown, ValidationSummary } from '../_types.js';
import { getSupabaseAdmin, getSupabaseClientForRequest } from '../_shared/supabaseAdmin.js';
function makeCertificateId(skill: string, track: CertificationTrack = 'verifiedcert'): string {
const date = new Date();
const y = date.getUTCFullYear();
const m = String(date.getUTCMonth() + 1).padStart(2, '0');
const d = String(date.getUTCDate()).padStart(2, '0');
const suffix = Math.random().toString(36).slice(2, 10).toUpperCase();
const normalizedSkill = skill.replace(/[^a-zA-Z0-9]/g, '').slice(0, 6).toUpperCase() || 'SKILL';
const prefix = track === 'free_completion' ? 'FC' : track === 'quickcert' ? 'QC' : track === 'ytcert' ? 'YT' : 'VC';
return `${prefix}-${normalizedSkill}-${y}${m}${d}-${suffix}`;
}
export async function issueCertificate(input: {
userId: string;
name: string;
email: string;
skill: string;
projectTitle: string;
githubUrl: string;
proofUrl: string;
liveDemoUrl?: string;
finalScore?: number;
scoreBreakdown?: ScoreBreakdown;
validationSummary?: ValidationSummary;
projectType?: string;
commitCount?: number;
paymentRequestId?: string;
paymentTxnId?: string | null;
extraMetadata?: Record<string, unknown>;
authToken?: string | null;
track?: CertificationTrack;
}): Promise<CertificatePayload> {
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
const supabase = (!input.authToken && serviceRoleKey) ? getSupabaseAdmin() : getSupabaseClientForRequest(input.authToken);
const certTrack = input.track || 'verifiedcert';
if (input.paymentRequestId) {
const { data: existing } = await supabase
.from('certificates')
.select('user_id, topic, project_title, issued_at, metadata')
.eq('metadata->>payment_request_id', input.paymentRequestId)
.order('issued_at', { ascending: false })
.limit(1)
.maybeSingle();
if (existing) {
return {
certificateId: (existing.metadata as any)?.certificate_id || makeCertificateId(existing.topic || input.skill, certTrack),
userId: existing.user_id,
name: (existing.metadata as any)?.username || (existing as any).username || input.name,
skill: existing.topic,
projectTitle: existing.project_title || input.projectTitle,
githubUrl: (existing.metadata as any)?.github_repo_url || (existing.metadata as any)?.github_url || input.githubUrl,
proofUrl: (existing.metadata as any)?.live_demo_url || (existing.metadata as any)?.proof_url || input.proofUrl,
issuedAt: existing.issued_at,
track: certTrack,
};
}
}
const certificateId = makeCertificateId(input.skill, certTrack);
const payload: CertificatePayload = {
certificateId,
userId: input.userId,
name: input.name,
skill: input.skill,
projectTitle: input.projectTitle,
githubUrl: input.githubUrl,
proofUrl: input.proofUrl,
issuedAt: new Date().toISOString(),
track: certTrack,
};
const normalizedScoreBreakdown = input.scoreBreakdown
? {
interview_score: input.scoreBreakdown.interviewScore,
github_validation_score: input.scoreBreakdown.githubValidationScore,
proof_validation_score: input.scoreBreakdown.proofValidationScore,
commit_score: input.scoreBreakdown.commitScore,
structure_score: input.scoreBreakdown.structureScore,
readme_score: input.scoreBreakdown.readmeScore,
}
: null;
const normalizedValidationSummary = input.validationSummary
? {
overall_passed: input.validationSummary.overallPassed,
commit_passed: input.validationSummary.commitPassed,
readme_passed: input.validationSummary.readmePassed,
proof_passed: input.validationSummary.proofPassed,
commit_count: input.validationSummary.commitCount,
file_count: input.validationSummary.fileCount,
meaningful_file_count: input.validationSummary.meaningfulFileCount,
readme_richness: input.validationSummary.readmeRichness,
suspicious_signals: input.validationSummary.suspiciousSignals,
}
: null;
const { error } = await supabase.from('certificates').insert({
user_id: input.userId,
topic: input.skill,
project_title: input.projectTitle,
metadata: {
certificate_id: certificateId,
username: input.name,
email: input.email,
course_topic: input.skill,
project_topic: input.projectTitle,
github_repo_url: input.githubUrl,
live_demo_url: input.liveDemoUrl || input.proofUrl,
final_score: input.finalScore ?? null,
score_breakdown: normalizedScoreBreakdown,
project_type: input.projectType || null,
commit_count: input.commitCount ?? null,
validation_summary: normalizedValidationSummary,
github_url: input.githubUrl,
proof_url: input.proofUrl,
getcert_version: 'v2',
certification_track: certTrack,
payment_request_id: input.paymentRequestId || null,
payment_txn_id: input.paymentTxnId || null,
...(input.extraMetadata || {}),
},
issued_at: payload.issuedAt,
});
if (error) {
throw new Error(`Failed to issue certificate: ${error.message}`);
}
return payload;
}