import type { ConnectorMeta } from '@rareindex/connectors'; import { CertLookupConnector, type PageStatus, type ParsedCert } from '../_g2-grading-lib/cert-base.js'; import { ccgClassify, ccgParse, ccgTrim, pick } from '../_g2-grading-lib/ccg.js'; import { clean, money, parseCoinGrade, yearOf } from '../_g2-grading-lib/text.js'; /** * NGC certificate verification — https://www.ngccoin.com/certlookup/// (public). * NGC requires the grade printed on the holder as the second path segment (numeric 1–70, "NGCDetails", * "NGCAncients" or "Other"); without it the site renders the empty lookup form. Cert seeds therefore * accept "4627835-016/61", "4627835-016 61" or "4627835-016:61"; a bare number yields zero records. */ const PARSER_VERSION = '1.0.0'; const PATTERNS = [/^https?:\/\/(?:www\.)?ngccoin\.(?:com|uk|de|hk|in|cn)\/(?:[a-z]{2}-[a-z]{2}\/)?certlookup\/([\d-]{5,20})(?:\/([^/?#]+))?\/?(?:[?#].*)?$/i]; /** Split "cert/grade" | "cert grade" | "cert:grade" seeds. */ export function splitCert(cert: string): { number: string; grade: string | null } { const m = clean(cert).match(/^([\d-]+)(?:[\s/:]+(.+))?$/); if (!m) return { number: clean(cert), grade: null }; return { number: m[1]!, grade: m[2] ? clean(m[2]).replace(/\s+/g, '') : null }; } export function certUrl(cert: string): string { const { number, grade } = splitCert(cert); return `https://www.ngccoin.com/certlookup/${number}/${grade ? `${encodeURIComponent(grade)}/` : ''}`; } export function certFromUrl(url: string): string | null { const m = url.match(PATTERNS[0]!); if (!m) return null; return m[2] ? `${m[1]}/${decodeURIComponent(m[2])}` : m[1]!; } export class NgcCertConnector extends CertLookupConnector { readonly version = '1.0.0'; readonly parserVersion = PARSER_VERSION; readonly grader = 'ngc'; readonly idKey = 'ngc_cert'; readonly format = 'html' as const; override readonly urlPatterns = PATTERNS; certUrl(cert: string): string { return certUrl(cert); } certFromUrl(url: string): string | null { return certFromUrl(url); } override certIdentifier(cert: string): string { return splitCert(cert).number; } classify(doc: string): PageStatus { return ccgClassify(doc); } trim(doc: string): string { return ccgTrim(doc); } parse(snapshot: string, info: { cert: string; url: string }): ParsedCert | null { const page = ccgParse(snapshot); const f = page.fields; const cert = pick(f, 'Cert #', 'NGC Cert #', 'Cert') ?? splitCert(info.cert).number; const description = pick(f, 'Description'); if (!description) return null; const gradeRaw = pick(f, 'Grade'); const g = parseCoinGrade(gradeRaw); const label = pick(f, 'Label'); const variety = pick(f, 'Variety', 'Designation', 'Pedigree') ?? null; const priceGuide = page.stats.find((s) => /^\$[\d,]+/.test(s)) ?? null; const isMedal = /\b(medal|token)\b/i.test(description); return { title: `${description} NGC ${gradeRaw ?? ''}`.trim(), attributes: { categorySlug: isMedal ? 'medals' : 'coins', name: description, year: yearOf(description), variant: variety, identifiers: {}, metadata: { ngc_cert: cert, grade_date: pick(f, 'Grade Date'), label, ngc_price_guide_value_usd: money(priceGuide), population_url: page.populationUrl, fields: f, }, }, grade: g.grade, qualifier: g.qualifier, gradeLabel: g.label || null, images: page.images, population: page.atGrade !== null ? { gradeKey: g.grade ?? page.popGradeKey ?? g.label, atGrade: page.atGrade, higher: page.higher, url: page.populationUrl } : null, confidence: 0.92, }; } } export default (meta: ConnectorMeta) => new NgcCertConnector(meta);