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, parseNoteGrade, yearOf } from '../_g2-grading-lib/text.js'; /** * PMG banknote certificate verification — https://www.pmgnotes.com/certlookup/// (public). * Like NGC, PMG requires the holder grade as the second path segment (1–70 or "NonNumeric"); a URL * without it renders the empty lookup form. Seeds: "8067968-054/67". */ const PARSER_VERSION = '1.0.0'; const PATTERNS = [/^https?:\/\/(?:www\.)?pmgnotes\.(?:com|uk|de|hk|in|cn)\/(?:[a-z]{2}-[a-z]{2}\/)?certlookup\/([\d-]{5,20})(?:\/([^/?#]+))?\/?(?:[?#].*)?$/i]; 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.pmgnotes.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 PmgCertConnector extends CertLookupConnector { readonly version = '1.0.0'; readonly parserVersion = PARSER_VERSION; readonly grader = 'pmg'; readonly idKey = 'pmg_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, 'PMG Cert', 'Cert #', 'Cert') ?? splitCert(info.cert).number; const description = pick(f, 'Note Description', 'Description'); if (!description) return null; const gradeRaw = pick(f, 'Grade'); const g = parseNoteGrade(gradeRaw); const pickNo = pick(f, 'Note #', 'Pick #', 'Catalog #'); const region = pick(f, 'Region'); const identifiers: Record = {}; if (pickNo) identifiers.pick_number = pickNo; const countryName = description.split(',')[0]?.trim() || null; return { title: `${description} PMG ${gradeRaw ?? ''}`.trim(), attributes: { categorySlug: 'banknotes', name: description, year: yearOf(description), country: countryName, region, identifiers, metadata: { pmg_cert: cert, serial_number: pick(f, 'Serial #'), grade_date: pick(f, 'Grade Date'), signatures_vignettes: pick(f, 'Signatures/Vignettes'), comments: pick(f, 'Comments'), 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, g.qualifier].filter(Boolean).join(' ') || page.popGradeKey || g.label, atGrade: page.atGrade, higher: page.higher, url: page.populationUrl } : null, confidence: pickNo ? 0.93 : 0.85, }; } } export default (meta: ConnectorMeta) => new PmgCertConnector(meta);