288 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			288 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import { expect, tap } from '@git.zone/tstest/tapbundle';
 | 
						||
import * as opendata from '../ts/index.js';
 | 
						||
 | 
						||
const TEST_USER_AGENT = 'fin.cx test@fin.cx';
 | 
						||
 | 
						||
tap.test('FundamentalsService - Provider Registration', async () => {
 | 
						||
  const service = new opendata.FundamentalsService();
 | 
						||
  const provider = new opendata.SecEdgarProvider({
 | 
						||
    userAgent: TEST_USER_AGENT
 | 
						||
  });
 | 
						||
 | 
						||
  await tap.test('should register provider', async () => {
 | 
						||
    service.register(provider);
 | 
						||
 | 
						||
    const registered = service.getProvider('SEC EDGAR');
 | 
						||
    expect(registered).toBeDefined();
 | 
						||
    expect(registered?.name).toEqual('SEC EDGAR');
 | 
						||
  });
 | 
						||
 | 
						||
  await tap.test('should get all providers', async () => {
 | 
						||
    const providers = service.getAllProviders();
 | 
						||
    expect(providers.length).toBeGreaterThan(0);
 | 
						||
    expect(providers[0].name).toEqual('SEC EDGAR');
 | 
						||
  });
 | 
						||
 | 
						||
  await tap.test('should get enabled providers', async () => {
 | 
						||
    const providers = service.getEnabledProviders();
 | 
						||
    expect(providers.length).toBeGreaterThan(0);
 | 
						||
  });
 | 
						||
 | 
						||
  await tap.test('should unregister provider', async () => {
 | 
						||
    service.unregister('SEC EDGAR');
 | 
						||
 | 
						||
    const registered = service.getProvider('SEC EDGAR');
 | 
						||
    expect(registered).toBeUndefined();
 | 
						||
 | 
						||
    // Re-register for other tests
 | 
						||
    service.register(provider);
 | 
						||
  });
 | 
						||
});
 | 
						||
 | 
						||
tap.test('FundamentalsService - Fetch Fundamentals', async () => {
 | 
						||
  const service = new opendata.FundamentalsService();
 | 
						||
  const provider = new opendata.SecEdgarProvider({
 | 
						||
    userAgent: TEST_USER_AGENT
 | 
						||
  });
 | 
						||
 | 
						||
  service.register(provider);
 | 
						||
 | 
						||
  await tap.test('should fetch fundamentals for single ticker', async () => {
 | 
						||
    const fundamentals = await service.getFundamentals('AAPL');
 | 
						||
 | 
						||
    expect(fundamentals).toBeDefined();
 | 
						||
    expect(fundamentals.ticker).toEqual('AAPL');
 | 
						||
    expect(fundamentals.companyName).toEqual('Apple Inc.');
 | 
						||
    expect(fundamentals.provider).toEqual('SEC EDGAR');
 | 
						||
    expect(fundamentals.earningsPerShareDiluted).toBeGreaterThan(0);
 | 
						||
    expect(fundamentals.sharesOutstanding).toBeGreaterThan(0);
 | 
						||
 | 
						||
    console.log('\n📊 Fetched via Service:');
 | 
						||
    console.log(`  ${fundamentals.ticker}: ${fundamentals.companyName}`);
 | 
						||
    console.log(`  EPS: $${fundamentals.earningsPerShareDiluted?.toFixed(2)}`);
 | 
						||
    console.log(`  Shares: ${(fundamentals.sharesOutstanding! / 1_000_000_000).toFixed(2)}B`);
 | 
						||
  });
 | 
						||
 | 
						||
  await tap.test('should fetch fundamentals for multiple tickers', async () => {
 | 
						||
    const fundamentalsList = await service.getBatchFundamentals(['AAPL', 'MSFT']);
 | 
						||
 | 
						||
    expect(fundamentalsList).toBeInstanceOf(Array);
 | 
						||
    expect(fundamentalsList.length).toEqual(2);
 | 
						||
 | 
						||
    const apple = fundamentalsList.find(f => f.ticker === 'AAPL');
 | 
						||
    const msft = fundamentalsList.find(f => f.ticker === 'MSFT');
 | 
						||
 | 
						||
    expect(apple).toBeDefined();
 | 
						||
    expect(msft).toBeDefined();
 | 
						||
    expect(apple!.companyName).toEqual('Apple Inc.');
 | 
						||
    expect(msft!.companyName).toContain('Microsoft');
 | 
						||
 | 
						||
    console.log('\n📊 Batch Fetch via Service:');
 | 
						||
    fundamentalsList.forEach(f => {
 | 
						||
      console.log(`  ${f.ticker}: ${f.companyName} - EPS: $${f.earningsPerShareDiluted?.toFixed(2)}`);
 | 
						||
    });
 | 
						||
  });
 | 
						||
});
 | 
						||
 | 
						||
tap.test('FundamentalsService - Caching', async () => {
 | 
						||
  const service = new opendata.FundamentalsService({
 | 
						||
    ttl: 60000, // 60 seconds for testing
 | 
						||
    maxEntries: 100
 | 
						||
  });
 | 
						||
 | 
						||
  const provider = new opendata.SecEdgarProvider({
 | 
						||
    userAgent: TEST_USER_AGENT
 | 
						||
  });
 | 
						||
 | 
						||
  service.register(provider);
 | 
						||
 | 
						||
  await tap.test('should cache fundamentals data', async () => {
 | 
						||
    // Clear cache first
 | 
						||
    service.clearCache();
 | 
						||
 | 
						||
    let stats = service.getCacheStats();
 | 
						||
    expect(stats.size).toEqual(0);
 | 
						||
 | 
						||
    // First fetch (should hit API)
 | 
						||
    const start1 = Date.now();
 | 
						||
    await service.getFundamentals('AAPL');
 | 
						||
    const duration1 = Date.now() - start1;
 | 
						||
 | 
						||
    stats = service.getCacheStats();
 | 
						||
    expect(stats.size).toEqual(1);
 | 
						||
 | 
						||
    // Second fetch (should hit cache - much faster)
 | 
						||
    const start2 = Date.now();
 | 
						||
    await service.getFundamentals('AAPL');
 | 
						||
    const duration2 = Date.now() - start2;
 | 
						||
 | 
						||
    expect(duration2).toBeLessThan(duration1);
 | 
						||
 | 
						||
    console.log('\n⚡ Cache Performance:');
 | 
						||
    console.log(`  First fetch: ${duration1}ms`);
 | 
						||
    console.log(`  Cached fetch: ${duration2}ms`);
 | 
						||
    console.log(`  Speedup: ${Math.round(duration1 / duration2)}x`);
 | 
						||
  });
 | 
						||
 | 
						||
  await tap.test('should respect cache TTL', async () => {
 | 
						||
    // Set very short TTL
 | 
						||
    service.setCacheTTL(100); // 100ms
 | 
						||
 | 
						||
    // Fetch and cache
 | 
						||
    await service.getFundamentals('MSFT');
 | 
						||
 | 
						||
    // Wait for TTL to expire
 | 
						||
    await new Promise(resolve => setTimeout(resolve, 150));
 | 
						||
 | 
						||
    // This should fetch again (cache expired)
 | 
						||
    const stats = service.getCacheStats();
 | 
						||
    console.log(`\n⏱️  Cache TTL: ${stats.ttl}ms`);
 | 
						||
  });
 | 
						||
 | 
						||
  await tap.test('should clear cache', async () => {
 | 
						||
    service.clearCache();
 | 
						||
 | 
						||
    const stats = service.getCacheStats();
 | 
						||
    expect(stats.size).toEqual(0);
 | 
						||
  });
 | 
						||
});
 | 
						||
 | 
						||
tap.test('FundamentalsService - Price Enrichment', async () => {
 | 
						||
  const service = new opendata.FundamentalsService();
 | 
						||
  const provider = new opendata.SecEdgarProvider({
 | 
						||
    userAgent: TEST_USER_AGENT
 | 
						||
  });
 | 
						||
 | 
						||
  service.register(provider);
 | 
						||
 | 
						||
  await tap.test('should enrich fundamentals with price to calculate market cap', async () => {
 | 
						||
    const fundamentals = await service.getFundamentals('AAPL');
 | 
						||
 | 
						||
    // Simulate current price
 | 
						||
    const currentPrice = 270.37;
 | 
						||
 | 
						||
    const enriched = await service.enrichWithPrice(fundamentals, currentPrice);
 | 
						||
 | 
						||
    expect(enriched.marketCap).toBeDefined();
 | 
						||
    expect(enriched.priceToEarnings).toBeDefined();
 | 
						||
    expect(enriched.priceToBook).toBeDefined();
 | 
						||
 | 
						||
    expect(enriched.marketCap).toBeGreaterThan(0);
 | 
						||
    expect(enriched.priceToEarnings).toBeGreaterThan(0);
 | 
						||
 | 
						||
    console.log('\n💰 Enriched with Price ($270.37):');
 | 
						||
    console.log(`  Market Cap: $${(enriched.marketCap! / 1_000_000_000_000).toFixed(2)}T`);
 | 
						||
    console.log(`  P/E Ratio: ${enriched.priceToEarnings!.toFixed(2)}`);
 | 
						||
    console.log(`  P/B Ratio: ${enriched.priceToBook?.toFixed(2) || 'N/A'}`);
 | 
						||
 | 
						||
    // Verify calculations
 | 
						||
    const expectedMarketCap = fundamentals.sharesOutstanding! * currentPrice;
 | 
						||
    expect(Math.abs(enriched.marketCap! - expectedMarketCap)).toBeLessThan(1); // Allow for rounding
 | 
						||
 | 
						||
    const expectedPE = currentPrice / fundamentals.earningsPerShareDiluted!;
 | 
						||
    expect(Math.abs(enriched.priceToEarnings! - expectedPE)).toBeLessThan(0.01);
 | 
						||
  });
 | 
						||
 | 
						||
  await tap.test('should enrich batch fundamentals with prices', async () => {
 | 
						||
    const fundamentalsList = await service.getBatchFundamentals(['AAPL', 'MSFT']);
 | 
						||
 | 
						||
    const priceMap = new Map<string, number>([
 | 
						||
      ['AAPL', 270.37],
 | 
						||
      ['MSFT', 425.50]
 | 
						||
    ]);
 | 
						||
 | 
						||
    const enriched = await service.enrichBatchWithPrices(fundamentalsList, priceMap);
 | 
						||
 | 
						||
    expect(enriched.length).toEqual(2);
 | 
						||
 | 
						||
    const apple = enriched.find(f => f.ticker === 'AAPL')!;
 | 
						||
    const msft = enriched.find(f => f.ticker === 'MSFT')!;
 | 
						||
 | 
						||
    expect(apple.marketCap).toBeGreaterThan(0);
 | 
						||
    expect(msft.marketCap).toBeGreaterThan(0);
 | 
						||
 | 
						||
    console.log('\n💰 Batch Enrichment:');
 | 
						||
    console.log(`  AAPL: Market Cap $${(apple.marketCap! / 1_000_000_000_000).toFixed(2)}T, P/E ${apple.priceToEarnings!.toFixed(2)}`);
 | 
						||
    console.log(`  MSFT: Market Cap $${(msft.marketCap! / 1_000_000_000_000).toFixed(2)}T, P/E ${msft.priceToEarnings!.toFixed(2)}`);
 | 
						||
  });
 | 
						||
});
 | 
						||
 | 
						||
tap.test('FundamentalsService - Provider Health', async () => {
 | 
						||
  const service = new opendata.FundamentalsService();
 | 
						||
  const provider = new opendata.SecEdgarProvider({
 | 
						||
    userAgent: TEST_USER_AGENT
 | 
						||
  });
 | 
						||
 | 
						||
  service.register(provider);
 | 
						||
 | 
						||
  await tap.test('should check provider health', async () => {
 | 
						||
    const health = await service.checkProvidersHealth();
 | 
						||
 | 
						||
    expect(health.size).toEqual(1);
 | 
						||
    expect(health.get('SEC EDGAR')).toBe(true);
 | 
						||
 | 
						||
    console.log('\n💚 Provider Health:');
 | 
						||
    health.forEach((isHealthy, name) => {
 | 
						||
      console.log(`  ${name}: ${isHealthy ? '✅ Healthy' : '❌ Unhealthy'}`);
 | 
						||
    });
 | 
						||
  });
 | 
						||
});
 | 
						||
 | 
						||
tap.test('FundamentalsService - Provider Statistics', async () => {
 | 
						||
  const service = new opendata.FundamentalsService();
 | 
						||
  const provider = new opendata.SecEdgarProvider({
 | 
						||
    userAgent: TEST_USER_AGENT
 | 
						||
  });
 | 
						||
 | 
						||
  service.register(provider);
 | 
						||
 | 
						||
  await tap.test('should track provider statistics', async () => {
 | 
						||
    // Make some requests
 | 
						||
    await service.getFundamentals('AAPL');
 | 
						||
    await service.getFundamentals('MSFT');
 | 
						||
 | 
						||
    const stats = service.getProviderStats();
 | 
						||
 | 
						||
    expect(stats.size).toEqual(1);
 | 
						||
 | 
						||
    const secStats = stats.get('SEC EDGAR');
 | 
						||
    expect(secStats).toBeDefined();
 | 
						||
    expect(secStats!.successCount).toBeGreaterThan(0);
 | 
						||
 | 
						||
    console.log('\n📈 Provider Stats:');
 | 
						||
    console.log(`  Success Count: ${secStats!.successCount}`);
 | 
						||
    console.log(`  Error Count: ${secStats!.errorCount}`);
 | 
						||
  });
 | 
						||
});
 | 
						||
 | 
						||
tap.test('FundamentalsService - Error Handling', async () => {
 | 
						||
  const service = new opendata.FundamentalsService();
 | 
						||
  const provider = new opendata.SecEdgarProvider({
 | 
						||
    userAgent: TEST_USER_AGENT
 | 
						||
  });
 | 
						||
 | 
						||
  service.register(provider);
 | 
						||
 | 
						||
  await tap.test('should throw error for invalid ticker', async () => {
 | 
						||
    try {
 | 
						||
      await service.getFundamentals('INVALIDTICKER123456');
 | 
						||
      throw new Error('Should have thrown error');
 | 
						||
    } catch (error) {
 | 
						||
      expect(error.message).toContain('CIK not found');
 | 
						||
    }
 | 
						||
  });
 | 
						||
 | 
						||
  await tap.test('should throw error when no providers available', async () => {
 | 
						||
    const emptyService = new opendata.FundamentalsService();
 | 
						||
 | 
						||
    try {
 | 
						||
      await emptyService.getFundamentals('AAPL');
 | 
						||
      throw new Error('Should have thrown error');
 | 
						||
    } catch (error) {
 | 
						||
      expect(error.message).toContain('No fundamentals providers available');
 | 
						||
    }
 | 
						||
  });
 | 
						||
});
 | 
						||
 | 
						||
export default tap.start();
 |