TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import type { ConnectorMeta } from '@rareindex/connectors';2import { CertLookupConnector, type PageStatus, type ParsedCert } from '../_g2-grading-lib/cert-base.js';3import { ccgClassify, ccgParse, ccgTrim, pick } from '../_g2-grading-lib/ccg.js';4import { clean, money, parseCoinGrade, yearOf } from '../_g2-grading-lib/text.js';56/**7 * NGC certificate verification — https://www.ngccoin.com/certlookup/<cert>/<grade>/ (public).8 * NGC requires the grade printed on the holder as the second path segment (numeric 1–70, "NGCDetails",9 * "NGCAncients" or "Other"); without it the site renders the empty lookup form. Cert seeds therefore10 * accept "4627835-016/61", "4627835-016 61" or "4627835-016:61"; a bare number yields zero records.11 */12const PARSER_VERSION = '1.0.0';13const PATTERNS = [/^https?:\/\/(?:www\.)?ngccoin\.(?:com|uk|de|hk|in|cn)\/(?:[a-z]{2}-[a-z]{2}\/)?certlookup\/([\d-]{5,20})(?:\/([^/?#]+))?\/?(?:[?#].*)?$/i];1415/** Split "cert/grade" | "cert grade" | "cert:grade" seeds. */16export function splitCert(cert: string): { number: string; grade: string | null } {17 const m = clean(cert).match(/^([\d-]+)(?:[\s/:]+(.+))?$/);18 if (!m) return { number: clean(cert), grade: null };19 return { number: m[1]!, grade: m[2] ? clean(m[2]).replace(/\s+/g, '') : null };20}2122export function certUrl(cert: string): string {23 const { number, grade } = splitCert(cert);24 return `https://www.ngccoin.com/certlookup/${number}/${grade ? `${encodeURIComponent(grade)}/` : ''}`;25}2627export function certFromUrl(url: string): string | null {28 const m = url.match(PATTERNS[0]!);29 if (!m) return null;30 return m[2] ? `${m[1]}/${decodeURIComponent(m[2])}` : m[1]!;31}3233export class NgcCertConnector extends CertLookupConnector {34 readonly version = '1.0.0';35 readonly parserVersion = PARSER_VERSION;36 readonly grader = 'ngc';37 readonly idKey = 'ngc_cert';38 readonly format = 'html' as const;39 override readonly urlPatterns = PATTERNS;4041 certUrl(cert: string): string {42 return certUrl(cert);43 }44 certFromUrl(url: string): string | null {45 return certFromUrl(url);46 }47 override certIdentifier(cert: string): string {48 return splitCert(cert).number;49 }50 classify(doc: string): PageStatus {51 return ccgClassify(doc);52 }53 trim(doc: string): string {54 return ccgTrim(doc);55 }5657 parse(snapshot: string, info: { cert: string; url: string }): ParsedCert | null {58 const page = ccgParse(snapshot);59 const f = page.fields;60 const cert = pick(f, 'Cert #', 'NGC Cert #', 'Cert') ?? splitCert(info.cert).number;61 const description = pick(f, 'Description');62 if (!description) return null;63 const gradeRaw = pick(f, 'Grade');64 const g = parseCoinGrade(gradeRaw);65 const label = pick(f, 'Label');66 const variety = pick(f, 'Variety', 'Designation', 'Pedigree') ?? null;67 const priceGuide = page.stats.find((s) => /^\$[\d,]+/.test(s)) ?? null;68 const isMedal = /\b(medal|token)\b/i.test(description);69 return {70 title: `${description} NGC ${gradeRaw ?? ''}`.trim(),71 attributes: {72 categorySlug: isMedal ? 'medals' : 'coins',73 name: description,74 year: yearOf(description),75 variant: variety,76 identifiers: {},77 metadata: {78 ngc_cert: cert,79 grade_date: pick(f, 'Grade Date'),80 label,81 ngc_price_guide_value_usd: money(priceGuide),82 population_url: page.populationUrl,83 fields: f,84 },85 },86 grade: g.grade,87 qualifier: g.qualifier,88 gradeLabel: g.label || null,89 images: page.images,90 population: page.atGrade !== null ? { gradeKey: g.grade ?? page.popGradeKey ?? g.label, atGrade: page.atGrade, higher: page.higher, url: page.populationUrl } : null,91 confidence: 0.92,92 };93 }94}9596export default (meta: ConnectorMeta) => new NgcCertConnector(meta);97