/** * Buyer's premium and all-in cost (§34–§35). A hammer price is never comparable with a marketplace * price: RareIndex adds the house's buyer premium (published or approximate schedule, marginal tiers) * and labels the basis of every all-in figure. VAT/sales tax on the premium, duties and shipping are * out of scope and stated as such. */ import feesJson from '../../../data/fees/auction-houses.json' with { type: 'json' }; import { round } from '@rareindex/shared'; export type FeeConfidence = 'published' | 'approximate' | 'none' | 'default' | 'unknown'; /** How an all-in figure was obtained. */ export type FeeBasis = 'included' | 'added_published' | 'added_approximate' | 'added_default' | 'none' | 'unknown'; export interface FeeTier { /** upper bound of the tier in the schedule currency (null = open-ended) */ upTo: number | null; rate: number; } export interface FeeSchedule { id: string; name: string; aliases: string[]; currency: string; tiers: FeeTier[]; minimum?: number; maximum?: number; fixedFee?: number; confidence: FeeConfidence; source?: string; note?: string; } interface FeesFile { version: number; as_of: string; default: { auction_house: { rate: number; confidence: FeeConfidence; note: string }; marketplace: { rate: number; confidence: FeeConfidence; note: string } }; houses: FeeSchedule[]; } const FEES = feesJson as unknown as FeesFile; export const FEE_SCHEDULE_AS_OF = FEES.as_of; export const FEE_SCHEDULES: readonly FeeSchedule[] = FEES.houses; const byAlias = new Map(); const norm = (s: string) => s.trim().toLowerCase().replace(/[’']/g, "'").replace(/\s+/g, ' '); for (const h of FEES.houses) { byAlias.set(norm(h.id), h); byAlias.set(norm(h.name), h); for (const a of h.aliases) byAlias.set(norm(a), h); } /** Find a schedule by house name, connector id or alias; undefined when the house is not listed. */ export function feeScheduleFor(house: string | null | undefined): FeeSchedule | undefined { if (!house) return undefined; const k = norm(house); const hit = byAlias.get(k); if (hit) return hit; // tolerant match: "Sotheby's Hong Kong" → Sotheby's for (const [alias, h] of byAlias) if (alias.length >= 5 && (k.startsWith(alias) || k.includes(alias))) return h; return undefined; } export interface PremiumResult { /** premium amount in the hammer's currency */ premium: number; /** effective rate = premium / hammer (0 when hammer is 0) */ rate: number; allIn: number; confidence: FeeConfidence; scheduleId: string | null; note?: string; } /** * Buyer premium on a hammer price, applying marginal tiers, minimum/maximum and fixed fees. * `fxToScheduleCurrency` converts the hammer into the schedule currency for tier boundaries when the * lot is billed in another currency (1 when not provided); the premium is returned in the hammer's currency. */ export function buyerPremium(hammer: number, schedule: FeeSchedule | undefined, opts: { fxToScheduleCurrency?: number; fallback?: 'auction_house' | 'marketplace' } = {}): PremiumResult { if (!(hammer > 0)) return { premium: 0, rate: 0, allIn: Math.max(0, hammer), confidence: schedule?.confidence ?? 'unknown', scheduleId: schedule?.id ?? null }; if (!schedule) { const d = FEES.default[opts.fallback ?? 'auction_house']; const premium = round(hammer * d.rate, 2); return { premium, rate: d.rate, allIn: round(hammer + premium, 2), confidence: d.confidence, scheduleId: null, note: d.note }; } const fx = opts.fxToScheduleCurrency ?? 1; const inSchedule = hammer * fx; let remaining = inSchedule; let lower = 0; let premiumSched = 0; for (const t of schedule.tiers) { const cap = t.upTo ?? Number.POSITIVE_INFINITY; const slice = Math.max(0, Math.min(inSchedule, cap) - lower); premiumSched += slice * t.rate; remaining -= slice; lower = cap; if (remaining <= 0) break; } if (schedule.fixedFee) premiumSched += schedule.fixedFee; if (schedule.minimum !== undefined) premiumSched = Math.max(premiumSched, schedule.minimum); if (schedule.maximum !== undefined) premiumSched = Math.min(premiumSched, schedule.maximum); const premium = round(premiumSched / fx, 2); return { premium, rate: hammer ? round(premium / hammer, 4) : 0, allIn: round(hammer + premium, 2), confidence: schedule.confidence, scheduleId: schedule.id, note: schedule.note }; } export interface AllInInput { price: number; /** connector-reported flag: true = price already includes the buyer premium; false = hammer only; null = unknown */ buyerPremiumIncluded: boolean | null | undefined; /** auction house name / connector id for the schedule lookup */ house?: string | null; /** 'auction' | 'fixed_price' | … — marketplaces have no buyer premium */ saleType?: string | null; sourceType?: string | null; fxToScheduleCurrency?: number; } export interface AllInResult { allIn: number; rate: number; basis: FeeBasis; scheduleId: string | null; note?: string; } /** * All-in (buyer pays) price for a sale or a bid. Rules: * - premium already included → unchanged (`included`); * - fixed-price / marketplace records → unchanged (`none`); * - hammer only (flag false) → add the house schedule (`added_published|approximate|default`); * - flag unknown → add the schedule only when the house is listed with a non-zero schedule and the * record is an auction; otherwise unchanged and `unknown` so the UI can say "fees unknown". */ export function allInPrice(i: AllInInput): AllInResult { const isAuction = (i.saleType ?? 'auction') === 'auction' || i.sourceType === 'auction_house'; if (i.buyerPremiumIncluded === true) return { allIn: i.price, rate: 0, basis: 'included', scheduleId: null }; if (!isAuction || i.sourceType === 'marketplace' || i.sourceType === 'dealer' || i.sourceType === 'pricing_guide') return { allIn: i.price, rate: 0, basis: 'none', scheduleId: null }; const schedule = feeScheduleFor(i.house); if (i.buyerPremiumIncluded === false) { const p = buyerPremium(i.price, schedule, { fxToScheduleCurrency: i.fxToScheduleCurrency }); return { allIn: p.allIn, rate: p.rate, basis: schedule ? (schedule.confidence === 'published' ? 'added_published' : schedule.confidence === 'none' ? 'none' : 'added_approximate') : 'added_default', scheduleId: p.scheduleId, note: p.note }; } // unknown flag if (schedule && schedule.tiers.some((t) => t.rate > 0)) { const p = buyerPremium(i.price, schedule, { fxToScheduleCurrency: i.fxToScheduleCurrency }); return { allIn: p.allIn, rate: p.rate, basis: schedule.confidence === 'published' ? 'added_published' : 'added_approximate', scheduleId: p.scheduleId, note: p.note }; } if (schedule) return { allIn: i.price, rate: 0, basis: 'none', scheduleId: schedule.id, note: schedule.note }; return { allIn: i.price, rate: 0, basis: 'unknown', scheduleId: null }; } /** Human label for a fee basis, for tables and tooltips. */ export function feeBasisLabel(basis: FeeBasis | string | null | undefined): string { switch (basis) { case 'included': return 'premium included'; case 'added_published': return 'premium added (published schedule)'; case 'added_approximate': return 'premium added (≈ estimated schedule)'; case 'added_default': return 'premium added (default 22 %, house unknown)'; case 'none': return 'no buyer premium'; default: return 'fees unknown'; } }