TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1/**2 * Buyer's premium and all-in cost (§34–§35). A hammer price is never comparable with a marketplace3 * price: RareIndex adds the house's buyer premium (published or approximate schedule, marginal tiers)4 * and labels the basis of every all-in figure. VAT/sales tax on the premium, duties and shipping are5 * out of scope and stated as such.6 */7import feesJson from '../../../data/fees/auction-houses.json' with { type: 'json' };8import { round } from '@rareindex/shared';910export type FeeConfidence = 'published' | 'approximate' | 'none' | 'default' | 'unknown';11/** How an all-in figure was obtained. */12export type FeeBasis = 'included' | 'added_published' | 'added_approximate' | 'added_default' | 'none' | 'unknown';1314export interface FeeTier {15 /** upper bound of the tier in the schedule currency (null = open-ended) */16 upTo: number | null;17 rate: number;18}19export interface FeeSchedule {20 id: string;21 name: string;22 aliases: string[];23 currency: string;24 tiers: FeeTier[];25 minimum?: number;26 maximum?: number;27 fixedFee?: number;28 confidence: FeeConfidence;29 source?: string;30 note?: string;31}3233interface FeesFile {34 version: number;35 as_of: string;36 default: { auction_house: { rate: number; confidence: FeeConfidence; note: string }; marketplace: { rate: number; confidence: FeeConfidence; note: string } };37 houses: FeeSchedule[];38}39const FEES = feesJson as unknown as FeesFile;40export const FEE_SCHEDULE_AS_OF = FEES.as_of;41export const FEE_SCHEDULES: readonly FeeSchedule[] = FEES.houses;4243const byAlias = new Map<string, FeeSchedule>();44const norm = (s: string) => s.trim().toLowerCase().replace(/[’']/g, "'").replace(/\s+/g, ' ');45for (const h of FEES.houses) {46 byAlias.set(norm(h.id), h);47 byAlias.set(norm(h.name), h);48 for (const a of h.aliases) byAlias.set(norm(a), h);49}5051/** Find a schedule by house name, connector id or alias; undefined when the house is not listed. */52export function feeScheduleFor(house: string | null | undefined): FeeSchedule | undefined {53 if (!house) return undefined;54 const k = norm(house);55 const hit = byAlias.get(k);56 if (hit) return hit;57 // tolerant match: "Sotheby's Hong Kong" → Sotheby's58 for (const [alias, h] of byAlias) if (alias.length >= 5 && (k.startsWith(alias) || k.includes(alias))) return h;59 return undefined;60}6162export interface PremiumResult {63 /** premium amount in the hammer's currency */64 premium: number;65 /** effective rate = premium / hammer (0 when hammer is 0) */66 rate: number;67 allIn: number;68 confidence: FeeConfidence;69 scheduleId: string | null;70 note?: string;71}7273/**74 * Buyer premium on a hammer price, applying marginal tiers, minimum/maximum and fixed fees.75 * `fxToScheduleCurrency` converts the hammer into the schedule currency for tier boundaries when the76 * lot is billed in another currency (1 when not provided); the premium is returned in the hammer's currency.77 */78export function buyerPremium(hammer: number, schedule: FeeSchedule | undefined, opts: { fxToScheduleCurrency?: number; fallback?: 'auction_house' | 'marketplace' } = {}): PremiumResult {79 if (!(hammer > 0)) return { premium: 0, rate: 0, allIn: Math.max(0, hammer), confidence: schedule?.confidence ?? 'unknown', scheduleId: schedule?.id ?? null };80 if (!schedule) {81 const d = FEES.default[opts.fallback ?? 'auction_house'];82 const premium = round(hammer * d.rate, 2);83 return { premium, rate: d.rate, allIn: round(hammer + premium, 2), confidence: d.confidence, scheduleId: null, note: d.note };84 }85 const fx = opts.fxToScheduleCurrency ?? 1;86 const inSchedule = hammer * fx;87 let remaining = inSchedule;88 let lower = 0;89 let premiumSched = 0;90 for (const t of schedule.tiers) {91 const cap = t.upTo ?? Number.POSITIVE_INFINITY;92 const slice = Math.max(0, Math.min(inSchedule, cap) - lower);93 premiumSched += slice * t.rate;94 remaining -= slice;95 lower = cap;96 if (remaining <= 0) break;97 }98 if (schedule.fixedFee) premiumSched += schedule.fixedFee;99 if (schedule.minimum !== undefined) premiumSched = Math.max(premiumSched, schedule.minimum);100 if (schedule.maximum !== undefined) premiumSched = Math.min(premiumSched, schedule.maximum);101 const premium = round(premiumSched / fx, 2);102 return { premium, rate: hammer ? round(premium / hammer, 4) : 0, allIn: round(hammer + premium, 2), confidence: schedule.confidence, scheduleId: schedule.id, note: schedule.note };103}104105export interface AllInInput {106 price: number;107 /** connector-reported flag: true = price already includes the buyer premium; false = hammer only; null = unknown */108 buyerPremiumIncluded: boolean | null | undefined;109 /** auction house name / connector id for the schedule lookup */110 house?: string | null;111 /** 'auction' | 'fixed_price' | … — marketplaces have no buyer premium */112 saleType?: string | null;113 sourceType?: string | null;114 fxToScheduleCurrency?: number;115}116117export interface AllInResult {118 allIn: number;119 rate: number;120 basis: FeeBasis;121 scheduleId: string | null;122 note?: string;123}124125/**126 * All-in (buyer pays) price for a sale or a bid. Rules:127 * - premium already included → unchanged (`included`);128 * - fixed-price / marketplace records → unchanged (`none`);129 * - hammer only (flag false) → add the house schedule (`added_published|approximate|default`);130 * - flag unknown → add the schedule only when the house is listed with a non-zero schedule and the131 * record is an auction; otherwise unchanged and `unknown` so the UI can say "fees unknown".132 */133export function allInPrice(i: AllInInput): AllInResult {134 const isAuction = (i.saleType ?? 'auction') === 'auction' || i.sourceType === 'auction_house';135 if (i.buyerPremiumIncluded === true) return { allIn: i.price, rate: 0, basis: 'included', scheduleId: null };136 if (!isAuction || i.sourceType === 'marketplace' || i.sourceType === 'dealer' || i.sourceType === 'pricing_guide') return { allIn: i.price, rate: 0, basis: 'none', scheduleId: null };137 const schedule = feeScheduleFor(i.house);138 if (i.buyerPremiumIncluded === false) {139 const p = buyerPremium(i.price, schedule, { fxToScheduleCurrency: i.fxToScheduleCurrency });140 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 };141 }142 // unknown flag143 if (schedule && schedule.tiers.some((t) => t.rate > 0)) {144 const p = buyerPremium(i.price, schedule, { fxToScheduleCurrency: i.fxToScheduleCurrency });145 return { allIn: p.allIn, rate: p.rate, basis: schedule.confidence === 'published' ? 'added_published' : 'added_approximate', scheduleId: p.scheduleId, note: p.note };146 }147 if (schedule) return { allIn: i.price, rate: 0, basis: 'none', scheduleId: schedule.id, note: schedule.note };148 return { allIn: i.price, rate: 0, basis: 'unknown', scheduleId: null };149}150151/** Human label for a fee basis, for tables and tooltips. */152export function feeBasisLabel(basis: FeeBasis | string | null | undefined): string {153 switch (basis) {154 case 'included':155 return 'premium included';156 case 'added_published':157 return 'premium added (published schedule)';158 case 'added_approximate':159 return 'premium added (≈ estimated schedule)';160 case 'added_default':161 return 'premium added (default 22 %, house unknown)';162 case 'none':163 return 'no buyer premium';164 default:165 return 'fees unknown';166 }167}168