/** * Janice API integration for EVE Online market appraisals and pricing. * This module provides interfaces and functions to interact with the Janice API. */ const BASE_URL = 'https://janice.e-351.com/api/rest/v2'; /** * Represents an appraisal from the Janice API. */ export interface Appraisal { /** Unique identifier for the appraisal */ id: number; /** Creation timestamp */ created: string; /** Expiration timestamp */ expires: string; /** Dataset timestamp */ datasetTime: string; /** Appraisal code */ code: string; /** Designation type */ designation: AppraisalDesignation; /** Pricing strategy */ pricing: AppraisalPricing; /** Pricing variant */ pricingVariant: AppraisalPricingVariant; /** Price percentage */ pricePercentage: number; /** Whether the appraisal is compactized */ isCompactized: boolean; /** Failure messages */ failures: string; /** Market information */ market: PricerMarket; /** Total volume */ totalVolume: number; /** Total packaged volume */ totalPackagedVolume: number; /** Effective prices */ effectivePrices: AppraisalValues; /** Immediate prices */ immediatePrices: AppraisalValues; /** Top 5 average prices */ top5AveragePrices: AppraisalValues; /** List of items in the appraisal */ items: AppraisalItem[]; } /** * Price values for an appraisal. */ export interface AppraisalValues { /** Total buy price */ totalBuyPrice: number; /** Total split price */ totalSplitPrice: number; /** Total sell price */ totalSellPrice: number; } /** * Represents an item in an appraisal. */ export interface AppraisalItem { /** Item ID */ id: number; /** Amount of the item */ amount: number; /** Number of buy orders */ buyOrderCount: number; /** Buy volume */ buyVolume: number; /** Number of sell orders */ sellOrderCount: number; /** Sell volume */ sellVolume: number; /** Effective prices for the item */ effectivePrices: AppraisalItemValues; /** Immediate prices for the item */ immediatePrices: AppraisalItemValues; /** Top 5 average prices for the item */ top5AveragePrices: AppraisalItemValues; /** Total volume */ totalVolume: number; /** Total packaged volume */ totalPackagedVolume: number; /** Item type information */ itemType: ItemType; } /** * Represents a pricer item from the API. */ export interface PricerItem { /** Date of the price data */ date: string; /** Market information */ market: PricerMarket; /** Number of buy orders */ buyOrderCount: number; /** Buy volume */ buyVolume: number; /** Number of sell orders */ sellOrderCount: number; /** Sell volume */ sellVolume: number; /** Immediate prices */ immediatePrices: PricerItemValues; /** Top 5 average prices */ top5AveragePrices: PricerItemValues; /** Item type information */ itemType: ItemType; } /** * Price values for a pricer item. */ export interface PricerItemValues { /** Buy price */ buyPrice: number; /** Split price */ splitPrice: number; /** Sell price */ sellPrice: number; /** 5-day median buy price */ buyPrice5DayMedian: number; /** 5-day median split price */ splitPrice5DayMedian: number; /** 5-day median sell price */ sellPrice5DayMedian: number; /** 30-day median buy price */ buyPrice30DayMedian: number; /** 30-day median split price */ splitPrice30DayMedian: number; /** 30-day median sell price */ sellPrice30DayMedian: number; } /** * Extended price values for appraisal items. */ export interface AppraisalItemValues extends PricerItemValues { /** Total buy price */ buyPriceTotal: number; /** Total split price */ splitPriceTotal: number; /** Total sell price */ sellPriceTotal: number; } /** * Represents an item type. */ export interface ItemType { /** EVE item ID */ eid: number; /** Item name (optional) */ name?: string; /** Item volume */ volume: number; /** Packaged volume */ packagedVolume: number; } /** * Enumeration for appraisal designations. */ export enum AppraisalDesignation { Appraisal = 'appraisal', WantToBuy = 'wtb', WantToSell = 'wts', } /** * Enumeration for appraisal pricing strategies. */ export enum AppraisalPricing { Buy = 'buy', Split = 'split', Sell = 'sell', Purchase = 'purchase', } /** * Enumeration for appraisal pricing variants. */ export enum AppraisalPricingVariant { Immediate = 'immediate', Top5Percent = 'top5percent', } /** * Represents a market in the pricer system. */ export interface PricerMarket { /** Market ID */ id: number; /** Market name */ name: string; } /** * Predefined list of available markets. */ export const markets: PricerMarket[] = [ { id: 2, name: 'Jita 4-4' }, { id: 3, name: 'R1O-GN' }, { id: 6, name: 'NPC' }, { id: 114, name: 'MJ-5F9' }, { id: 115, name: 'Amarr' }, { id: 116, name: 'Rens' }, { id: 117, name: 'Dodixie' }, { id: 118, name: 'Hek' }, ] as const; /** * Simple cache for API responses to improve performance. */ const cache = new Map(); const CACHE_TTL = 5 * 60 * 1000; // 5 minutes /** * Clears the internal cache. Useful for testing. */ export function clearCache(): void { cache.clear(); } /** * Validates if a value is a positive number. */ export function isPositiveNumber(value: any): value is number { return typeof value === 'number' && value > 0 && isFinite(value); } /** * Validates if a value is a non-empty string. */ export function isNonEmptyString(value: any): value is string { return typeof value === 'string' && value.trim().length > 0; } /** * Fetches price data for a single item type. * @param type_id - The EVE item type ID * @param market_id - The market ID (default: 2 for Jita) * @returns Promise resolving to PricerItem * @throws Error if API call fails or validation fails */ export const fetchPrice = async (type_id: number, market_id: number = 2): Promise => { if (!isPositiveNumber(type_id)) { throw new Error('Invalid type_id: must be a positive number'); } if (!isPositiveNumber(market_id)) { throw new Error('Invalid market_id: must be a positive number'); } const cacheKey = `price_${type_id}_${market_id}`; const cached = cache.get(cacheKey); if (cached && Date.now() - cached.timestamp < CACHE_TTL) { return cached.data; } try { const response = await fetch(`${BASE_URL}/pricer/${type_id}?market=${market_id}`, { method: 'GET', headers: { 'X-ApiKey': process.env.JANICE_KEY || '', 'Accept': 'application/json', }, }); if (!response.ok) { throw new Error(`API request failed: ${response.status} ${response.statusText}`); } const data = await response.json(); cache.set(cacheKey, { data, timestamp: Date.now() }); return data as PricerItem; } catch (error) { console.error(`Error fetching price for type_id ${type_id}:`, error); throw error; } }; /** * Fetches price data for multiple item types. * @param type_ids - Array of EVE item type IDs * @param market_id - The market ID (default: 2 for Jita) * @returns Promise resolving to array of PricerItem * @throws Error if API call fails or validation fails */ export const fetchPrices = async (type_ids: number[], market_id: number = 2): Promise => { if (!Array.isArray(type_ids) || type_ids.length === 0 || !type_ids.every(isPositiveNumber)) { throw new Error('Invalid type_ids: must be a non-empty array of positive numbers'); } if (!isPositiveNumber(market_id)) { throw new Error('Invalid market_id: must be a positive number'); } const cacheKey = `prices_${type_ids.sort().join('_')}_${market_id}`; const cached = cache.get(cacheKey); if (cached && Date.now() - cached.timestamp < CACHE_TTL) { return cached.data; } try { const response = await fetch(`${BASE_URL}/pricer?market=${market_id}`, { method: 'POST', headers: { 'Content-Type': 'text/plain', 'X-ApiKey': process.env.JANICE_KEY || '', 'Accept': 'application/json', }, body: type_ids.join('\n'), }); if (!response.ok) { throw new Error(`API request failed: ${response.status} ${response.statusText}`); } const data = await response.json(); cache.set(cacheKey, { data, timestamp: Date.now() }); return data as PricerItem[]; } catch (error) { console.error(`Error fetching prices for type_ids ${type_ids}:`, error); throw error; } }; /** * Fetches an appraisal by its code. * @param code - The appraisal code * @returns Promise resolving to Appraisal * @throws Error if API call fails or validation fails */ export const fetchAppraisal = async (code: string): Promise => { if (!isNonEmptyString(code)) { throw new Error('Invalid code: must be a non-empty string'); } const cacheKey = `appraisal_${code}`; const cached = cache.get(cacheKey); if (cached && Date.now() - cached.timestamp < CACHE_TTL) { return cached.data; } try { const response = await fetch(`${BASE_URL}/appraisal/${code}`, { method: 'GET', headers: { 'X-ApiKey': process.env.JANICE_KEY || '', 'Accept': 'application/json', }, }); if (!response.ok) { throw new Error(`API request failed: ${response.status} ${response.statusText}`); } const data = await response.json(); cache.set(cacheKey, { data, timestamp: Date.now() }); return data as Appraisal; } catch (error) { console.error(`Error fetching appraisal for code ${code}:`, error); throw error; } }; /** * Appraises items from text input. * @param text - The text containing items to appraise * @param market_id - The market ID (default: 2 for Jita) * @returns Promise resolving to Appraisal * @throws Error if API call fails or validation fails */ export const appraiseItems = async (text: string, market_id: number = 2, apiKey: string | undefined = process.env.JANICE_KEY): Promise => { if (!isNonEmptyString(text)) { throw new Error('Invalid text: must be a non-empty string'); } if (!isPositiveNumber(market_id)) { throw new Error('Invalid market_id: must be a positive number'); } try { const response = await fetch(`${BASE_URL}/appraisal?market=${market_id}&persist=true&compactize=true&pricePercentage=1`, { method: 'POST', headers: { 'Content-Type': 'text/plain', 'X-ApiKey': apiKey || '', 'Accept': 'application/json', }, body: text, }); if (!response.ok) { throw new Error(`API request failed: ${response.status} ${response.statusText}`); } const data = await response.json(); return data as Appraisal; } catch (error) { console.error('Error appraising items:', error); throw error; } };