/**
* Pricing Fetcher Module
*
* Two-tier pricing system:
* 1. Static JSON file (default) - Fast, no auth required
* 2. DMAPI real-time (optional) - Requires Joker.com credentials
*/
import { dmapiRequest } from './dmapi-client.js';
import { getStaticPricing } from './static-pricing.js';
import type { JokerPricingResult } from './price-cache.js';
/**
* Raw price list entry from DMAPI
* Format: type,operation,tld,currency,vat,price,standard_price,retail_price,min_period,max_period,period_type,valid_from,valid_to
*/
interface PriceListEntry {
type: string; // e.g., "domain"
operation: string; // e.g., "create", "renew", "transfer"
tld: string; // e.g., "com", "org"
currency: string; // e.g., "EUR", "USD"
vat: number; // VAT percentage
price: number; // Reseller price (with discount)
standard_price: number; // Standard price
retail_price: number; // Retail price
min_period: number; // Minimum period
max_period: number; // Maximum period
period_type: string; // e.g., "flex"
valid_from: string; // ISO date
valid_to: string; // ISO date
}
/**
* Parse TSV line from price list response
*/
function parsePriceListLine(line: string): PriceListEntry | null {
const parts = line.split('\t');
if (parts.length < 13) {
return null; // Invalid line
}
return {
type: parts[0],
operation: parts[1],
tld: parts[2],
currency: parts[3],
vat: parseFloat(parts[4]),
price: parseFloat(parts[5]),
standard_price: parseFloat(parts[6]),
retail_price: parseFloat(parts[7]),
min_period: parseInt(parts[8], 10),
max_period: parseInt(parts[9], 10),
period_type: parts[10],
valid_from: parts[11],
valid_to: parts[12],
};
}
/**
* Fetch and parse complete price list from DMAPI
*/
async function fetchPriceList(): Promise<PriceListEntry[]> {
try {
const response = await dmapiRequest('query-price-list');
// Parse TSV response
const lines = response.trim().split('\n');
const entries: PriceListEntry[] = [];
for (const line of lines) {
// Skip header line if present
if (line.startsWith('type') || line.startsWith('#')) {
continue;
}
const entry = parsePriceListLine(line);
if (entry) {
entries.push(entry);
}
}
return entries;
} catch (error) {
throw new Error(`Failed to fetch price list: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Extract TLD from domain name
* Examples: "example.com" → "com", "test.co.uk" → "co.uk"
*/
function extractTLD(domain: string): string {
const parts = domain.split('.');
// Handle multi-part TLDs (e.g., co.uk, com.au)
if (parts.length >= 3) {
const secondLevelTLD = `${parts[parts.length - 2]}.${parts[parts.length - 1]}`;
// Common second-level TLDs
const commonSecondLevel = ['co.uk', 'com.au', 'co.nz', 'co.za', 'org.uk', 'ac.uk'];
if (commonSecondLevel.includes(secondLevelTLD)) {
return secondLevelTLD;
}
}
// Standard single-part TLD
return parts[parts.length - 1];
}
/**
* Check if DMAPI credentials are configured
*/
function isDmapiConfigured(): boolean {
const apiKey = process.env.MCP_JOKER_API_KEY;
const username = process.env.MCP_JOKER_USERNAME;
const password = process.env.MCP_JOKER_PASSWORD;
return !!(apiKey || (username && password));
}
/**
* Fetch pricing for a specific domain
*
* Uses static pricing by default (fast, no auth).
* Falls back to DMAPI if use_api=true and credentials configured.
*/
export async function fetchJokerPricing(
domain: string,
options?: { use_api?: boolean }
): Promise<JokerPricingResult> {
const normalizedDomain = domain.toLowerCase().trim();
const tld = extractTLD(normalizedDomain);
// Use static pricing by default
if (!options?.use_api) {
return getStaticPricing(normalizedDomain);
}
// Check if DMAPI is configured
if (!isDmapiConfigured()) {
console.error('[Pricing] DMAPI requested but not configured, using static pricing');
return getStaticPricing(normalizedDomain);
}
// Fetch from DMAPI
try {
// Fetch complete price list
const priceList = await fetchPriceList();
// Filter entries for this TLD
const tldEntries = priceList.filter(entry =>
entry.type === 'domain' && entry.tld === tld
);
if (tldEntries.length === 0) {
return {
domain: normalizedDomain,
available: false, // We don't know availability from price list alone
source_url: 'https://dmapi.joker.com/request/query-price-list',
last_updated: new Date().toISOString(),
error: `No pricing data found for TLD .${tld}`,
};
}
// Extract pricing for different operations
const registration = tldEntries.find(e => e.operation === 'create');
const renewal = tldEntries.find(e => e.operation === 'renew');
const transfer = tldEntries.find(e => e.operation === 'transfer');
// Use the currency from the first entry
const currency = tldEntries[0].currency;
// Check for special offers (prices with valid_to dates in the future)
const now = new Date();
const specialOffers = tldEntries
.filter(e => {
const validTo = new Date(e.valid_to);
return validTo > now && e.price < (e.standard_price || e.price);
})
.map(e => ({
description: `${e.operation} discount until ${new Date(e.valid_to).toLocaleDateString()}`,
discount_price: e.price,
}));
return {
domain: normalizedDomain,
available: false, // Note: query-price-list doesn't check availability
pricing: {
registration_1yr: registration?.price,
renewal_1yr: renewal?.price,
transfer: transfer?.price,
currency,
},
special_offers: specialOffers.length > 0 ? specialOffers : undefined,
source_url: 'https://dmapi.joker.com/request/query-price-list',
last_updated: new Date().toISOString(),
};
} catch (error) {
return {
domain: normalizedDomain,
available: false,
source_url: 'https://dmapi.joker.com/request/query-price-list',
last_updated: new Date().toISOString(),
error: error instanceof Error ? error.message : String(error),
};
}
}