From a3459f586565b5aa1c5290b34c486356a49ea284 Mon Sep 17 00:00:00 2001 From: Boki Date: Sun, 22 Jun 2025 18:27:48 -0400 Subject: [PATCH] getting aligned and refactored --- .../document_symbols_cache_v20-05-25.pkl | Bin 180863 -> 246248 bytes apps/data-pipeline/src/container-setup.ts | 6 +- .../handlers/exchanges/exchanges.handler.ts | 4 +- .../clear-postgresql-data.operations.ts | 4 +- .../enhanced-sync-status.operations.ts | 4 +- .../operations/exchange-stats.operations.ts | 4 +- .../provider-mapping-stats.operations.ts | 4 +- .../operations/qm-exchanges.operations.ts | 8 +- .../sync-all-exchanges.operations.ts | 16 +- .../sync-ib-exchanges.operations.ts | 12 +- .../sync-qm-provider-mappings.operations.ts | 44 +- apps/web-api/src/container-setup.ts | 6 +- apps/web-api/src/routes/create-routes.ts | 14 +- apps/web-api/src/routes/exchange.routes.ts | 427 +++++++++--------- apps/web-api/src/routes/index.ts | 2 +- apps/web-api/src/services/exchange.service.ts | 14 +- 16 files changed, 296 insertions(+), 273 deletions(-) diff --git a/.serena/cache/typescript/document_symbols_cache_v20-05-25.pkl b/.serena/cache/typescript/document_symbols_cache_v20-05-25.pkl index 937683cf16efbb6ed17252c71c34c82e78fa9e37..ae8f4cef29631e760e41cffa1399ae4ed0260767 100644 GIT binary patch literal 246248 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 literal 180863 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 diff --git a/apps/data-pipeline/src/container-setup.ts b/apps/data-pipeline/src/container-setup.ts index a1f2638..1482d04 100644 --- a/apps/data-pipeline/src/container-setup.ts +++ b/apps/data-pipeline/src/container-setup.ts @@ -3,7 +3,7 @@ * Configures dependency injection for the data pipeline service */ -import type { ServiceContainer } from '@stock-bot/di'; +import type { IServiceContainer } from '@stock-bot/handlers'; import { getLogger } from '@stock-bot/logger'; import type { AppConfig } from '@stock-bot/config'; @@ -14,8 +14,8 @@ const logger = getLogger('data-pipeline-container'); */ export function setupServiceContainer( config: AppConfig, - container: ServiceContainer -): ServiceContainer { + container: IServiceContainer +): IServiceContainer { logger.info('Configuring data pipeline service container...'); // Data pipeline specific configuration diff --git a/apps/data-pipeline/src/handlers/exchanges/exchanges.handler.ts b/apps/data-pipeline/src/handlers/exchanges/exchanges.handler.ts index cd503c3..55a1c18 100644 --- a/apps/data-pipeline/src/handlers/exchanges/exchanges.handler.ts +++ b/apps/data-pipeline/src/handlers/exchanges/exchanges.handler.ts @@ -1,6 +1,6 @@ import { getLogger } from '@stock-bot/logger'; import { handlerRegistry, createJobHandler, type HandlerConfig, type ScheduledJobConfig } from '@stock-bot/queue'; -import type { ServiceContainer } from '@stock-bot/di'; +import type { IServiceContainer } from '@stock-bot/handlers'; import { exchangeOperations } from './operations'; const logger = getLogger('exchanges-handler'); @@ -52,7 +52,7 @@ const exchangesHandlerConfig: HandlerConfig = { }, }; -export function initializeExchangesHandler(container: ServiceContainer) { +export function initializeExchangesHandler(container: IServiceContainer) { logger.info('Registering exchanges handler...'); // Update operations to use container diff --git a/apps/data-pipeline/src/handlers/exchanges/operations/clear-postgresql-data.operations.ts b/apps/data-pipeline/src/handlers/exchanges/operations/clear-postgresql-data.operations.ts index b47b0cb..733cd35 100644 --- a/apps/data-pipeline/src/handlers/exchanges/operations/clear-postgresql-data.operations.ts +++ b/apps/data-pipeline/src/handlers/exchanges/operations/clear-postgresql-data.operations.ts @@ -1,12 +1,12 @@ import { getLogger } from '@stock-bot/logger'; -import type { ServiceContainer } from '@stock-bot/di'; +import type { IServiceContainer } from '@stock-bot/handlers'; import type { JobPayload } from '../../../types/job-payloads'; const logger = getLogger('enhanced-sync-clear-postgresql-data'); export async function clearPostgreSQLData( payload: JobPayload, - container: ServiceContainer + container: IServiceContainer ): Promise<{ exchangesCleared: number; symbolsCleared: number; diff --git a/apps/data-pipeline/src/handlers/exchanges/operations/enhanced-sync-status.operations.ts b/apps/data-pipeline/src/handlers/exchanges/operations/enhanced-sync-status.operations.ts index f1ab881..96e5ad1 100644 --- a/apps/data-pipeline/src/handlers/exchanges/operations/enhanced-sync-status.operations.ts +++ b/apps/data-pipeline/src/handlers/exchanges/operations/enhanced-sync-status.operations.ts @@ -1,12 +1,12 @@ import { getLogger } from '@stock-bot/logger'; -import type { ServiceContainer } from '@stock-bot/di'; +import type { IServiceContainer } from '@stock-bot/handlers'; import type { JobPayload, SyncStatus } from '../../../types/job-payloads'; const logger = getLogger('enhanced-sync-status'); export async function getSyncStatus( payload: JobPayload, - container: ServiceContainer + container: IServiceContainer ): Promise { logger.info('Getting comprehensive sync status...'); diff --git a/apps/data-pipeline/src/handlers/exchanges/operations/exchange-stats.operations.ts b/apps/data-pipeline/src/handlers/exchanges/operations/exchange-stats.operations.ts index fdc17fc..eeb6c59 100644 --- a/apps/data-pipeline/src/handlers/exchanges/operations/exchange-stats.operations.ts +++ b/apps/data-pipeline/src/handlers/exchanges/operations/exchange-stats.operations.ts @@ -1,12 +1,12 @@ import { getLogger } from '@stock-bot/logger'; -import type { ServiceContainer } from '@stock-bot/di'; +import type { IServiceContainer } from '@stock-bot/handlers'; import type { JobPayload } from '../../../types/job-payloads'; const logger = getLogger('enhanced-sync-exchange-stats'); export async function getExchangeStats( payload: JobPayload, - container: ServiceContainer + container: IServiceContainer ): Promise { logger.info('Getting exchange statistics...'); diff --git a/apps/data-pipeline/src/handlers/exchanges/operations/provider-mapping-stats.operations.ts b/apps/data-pipeline/src/handlers/exchanges/operations/provider-mapping-stats.operations.ts index 9d07412..3a1381a 100644 --- a/apps/data-pipeline/src/handlers/exchanges/operations/provider-mapping-stats.operations.ts +++ b/apps/data-pipeline/src/handlers/exchanges/operations/provider-mapping-stats.operations.ts @@ -1,12 +1,12 @@ import { getLogger } from '@stock-bot/logger'; -import type { ServiceContainer } from '@stock-bot/di'; +import type { IServiceContainer } from '@stock-bot/handlers'; import type { JobPayload } from '../../../types/job-payloads'; const logger = getLogger('enhanced-sync-provider-mapping-stats'); export async function getProviderMappingStats( payload: JobPayload, - container: ServiceContainer + container: IServiceContainer ): Promise { logger.info('Getting provider mapping statistics...'); diff --git a/apps/data-pipeline/src/handlers/exchanges/operations/qm-exchanges.operations.ts b/apps/data-pipeline/src/handlers/exchanges/operations/qm-exchanges.operations.ts index cebea42..e7761cd 100644 --- a/apps/data-pipeline/src/handlers/exchanges/operations/qm-exchanges.operations.ts +++ b/apps/data-pipeline/src/handlers/exchanges/operations/qm-exchanges.operations.ts @@ -1,17 +1,19 @@ import { getLogger } from '@stock-bot/logger'; import { getMongoDBClient, getPostgreSQLClient } from '../../../clients'; +import type { IServiceContainer } from '@stock-bot/handlers'; import type { JobPayload } from '../../../types/job-payloads'; const logger = getLogger('sync-qm-exchanges'); export async function syncQMExchanges( - payload: JobPayload + payload: JobPayload, + container: IServiceContainer ): Promise<{ processed: number; created: number; updated: number }> { logger.info('Starting QM exchanges sync...'); try { - const mongoClient = getMongoDBClient(); - const postgresClient = getPostgreSQLClient(); + const mongoClient = container.mongodb; + const postgresClient = container.postgres; // 1. Get all QM exchanges from MongoDB const qmExchanges = await mongoClient.find('qmExchanges', {}); diff --git a/apps/data-pipeline/src/handlers/exchanges/operations/sync-all-exchanges.operations.ts b/apps/data-pipeline/src/handlers/exchanges/operations/sync-all-exchanges.operations.ts index 7c642b4..9dbfd57 100644 --- a/apps/data-pipeline/src/handlers/exchanges/operations/sync-all-exchanges.operations.ts +++ b/apps/data-pipeline/src/handlers/exchanges/operations/sync-all-exchanges.operations.ts @@ -1,10 +1,10 @@ import { getLogger } from '@stock-bot/logger'; -import type { ServiceContainer } from '@stock-bot/di'; +import type { IServiceContainer } from '@stock-bot/handlers'; import type { JobPayload, SyncResult } from '../../../types/job-payloads'; const logger = getLogger('enhanced-sync-all-exchanges'); -export async function syncAllExchanges(payload: JobPayload, container: ServiceContainer): Promise { +export async function syncAllExchanges(payload: JobPayload, container: IServiceContainer): Promise { const clearFirst = payload.clearFirst || true; logger.info('Starting comprehensive exchange sync...', { clearFirst }); @@ -67,7 +67,7 @@ async function clearPostgreSQLData(postgresClient: any): Promise { logger.info('PostgreSQL data cleared successfully'); } -async function syncEODExchanges(container: ServiceContainer): Promise { +async function syncEODExchanges(container: IServiceContainer): Promise { const mongoClient = container.mongodb; const exchanges = await mongoClient.find('eodExchanges', { active: true }); const result: SyncResult = { processed: 0, created: 0, updated: 0, skipped: 0, errors: 0 }; @@ -96,7 +96,7 @@ async function syncEODExchanges(container: ServiceContainer): Promise { +async function syncIBExchanges(container: IServiceContainer): Promise { const mongoClient = container.mongodb; const exchanges = await mongoClient.find('ibExchanges', {}); const result: SyncResult = { processed: 0, created: 0, updated: 0, skipped: 0, errors: 0 }; @@ -132,7 +132,7 @@ async function createProviderExchangeMapping( countryCode: string | null, currency: string | null, confidence: number, - container: ServiceContainer + container: IServiceContainer ): Promise { if (!providerExchangeCode) { return; @@ -181,7 +181,7 @@ async function findOrCreateMasterExchange( providerName: string, countryCode: string | null, currency: string | null, - container: ServiceContainer + container: IServiceContainer ): Promise { const postgresClient = container.postgres; @@ -237,7 +237,7 @@ function getBasicExchangeMapping(providerCode: string): string | null { async function findProviderExchangeMapping( provider: string, providerExchangeCode: string, - container: ServiceContainer + container: IServiceContainer ): Promise { const postgresClient = container.postgres; const query = @@ -246,7 +246,7 @@ async function findProviderExchangeMapping( return result.rows[0] || null; } -async function findExchangeByCode(code: string, container: ServiceContainer): Promise { +async function findExchangeByCode(code: string, container: IServiceContainer): Promise { const postgresClient = container.postgres; const query = 'SELECT * FROM exchanges WHERE code = $1'; const result = await postgresClient.query(query, [code]); diff --git a/apps/data-pipeline/src/handlers/exchanges/operations/sync-ib-exchanges.operations.ts b/apps/data-pipeline/src/handlers/exchanges/operations/sync-ib-exchanges.operations.ts index d8da00c..09be8f2 100644 --- a/apps/data-pipeline/src/handlers/exchanges/operations/sync-ib-exchanges.operations.ts +++ b/apps/data-pipeline/src/handlers/exchanges/operations/sync-ib-exchanges.operations.ts @@ -1,6 +1,7 @@ import { getLogger } from '@stock-bot/logger'; import type { MasterExchange } from '@stock-bot/mongodb'; import { getMongoDBClient } from '../../../clients'; +import type { IServiceContainer } from '@stock-bot/handlers'; import type { JobPayload } from '../../../types/job-payloads'; const logger = getLogger('sync-ib-exchanges'); @@ -15,12 +16,13 @@ interface IBExchange { } export async function syncIBExchanges( - payload: JobPayload + payload: JobPayload, + container: IServiceContainer ): Promise<{ syncedCount: number; totalExchanges: number }> { logger.info('Syncing IB exchanges from database...'); try { - const mongoClient = getMongoDBClient(); + const mongoClient = container.mongodb; const db = mongoClient.getDatabase(); // Filter by country code US and CA @@ -37,7 +39,7 @@ export async function syncIBExchanges( for (const exchange of ibExchanges) { try { - await createOrUpdateMasterExchange(exchange); + await createOrUpdateMasterExchange(exchange, container); syncedCount++; logger.debug('Synced IB exchange', { @@ -64,8 +66,8 @@ export async function syncIBExchanges( /** * Create or update master exchange record 1:1 from IB exchange */ -async function createOrUpdateMasterExchange(ibExchange: IBExchange): Promise { - const mongoClient = getMongoDBClient(); +async function createOrUpdateMasterExchange(ibExchange: IBExchange, container: IServiceContainer): Promise { + const mongoClient = container.mongodb; const db = mongoClient.getDatabase(); const collection = db.collection('masterExchanges'); diff --git a/apps/data-pipeline/src/handlers/exchanges/operations/sync-qm-provider-mappings.operations.ts b/apps/data-pipeline/src/handlers/exchanges/operations/sync-qm-provider-mappings.operations.ts index ad7900e..7c0bee4 100644 --- a/apps/data-pipeline/src/handlers/exchanges/operations/sync-qm-provider-mappings.operations.ts +++ b/apps/data-pipeline/src/handlers/exchanges/operations/sync-qm-provider-mappings.operations.ts @@ -1,10 +1,14 @@ import { getLogger } from '@stock-bot/logger'; import { getMongoDBClient, getPostgreSQLClient } from '../../../clients'; +import type { IServiceContainer } from '@stock-bot/handlers'; import type { JobPayload, SyncResult } from '../../../types/job-payloads'; const logger = getLogger('enhanced-sync-qm-provider-mappings'); -export async function syncQMProviderMappings(payload: JobPayload): Promise { +export async function syncQMProviderMappings( + payload: JobPayload, + container: IServiceContainer +): Promise { logger.info('Starting QM provider exchange mappings sync...'); const result: SyncResult = { @@ -16,8 +20,8 @@ export async function syncQMProviderMappings(payload: JobPayload): Promise { if (!providerExchangeCode) { return; } - const postgresClient = getPostgreSQLClient(); + const postgresClient = container.postgres; // Check if mapping already exists - const existingMapping = await findProviderExchangeMapping(provider, providerExchangeCode); + const existingMapping = await findProviderExchangeMapping(provider, providerExchangeCode, container); if (existingMapping) { // Don't override existing mappings to preserve manual work return; @@ -108,7 +115,8 @@ async function createProviderExchangeMapping( providerExchangeCode, providerExchangeName, countryCode, - currency + currency, + container ); // Create the provider exchange mapping @@ -133,9 +141,10 @@ async function createProviderExchangeMapping( async function findProviderExchangeMapping( provider: string, - providerExchangeCode: string + providerExchangeCode: string, + container: IServiceContainer ): Promise { - const postgresClient = getPostgreSQLClient(); + const postgresClient = container.postgres; const query = 'SELECT * FROM provider_exchange_mappings WHERE provider = $1 AND provider_exchange_code = $2'; const result = await postgresClient.query(query, [provider, providerExchangeCode]); @@ -146,12 +155,13 @@ async function findOrCreateMasterExchange( providerCode: string, providerName: string, countryCode: string | null, - currency: string | null + currency: string | null, + container: IServiceContainer ): Promise { - const postgresClient = getPostgreSQLClient(); + const postgresClient = container.postgres; // First, try to find exact match - let masterExchange = await findExchangeByCode(providerCode); + let masterExchange = await findExchangeByCode(providerCode, container); if (masterExchange) { return masterExchange; @@ -160,7 +170,7 @@ async function findOrCreateMasterExchange( // Try to find by similar codes (basic mapping) const basicMapping = getBasicExchangeMapping(providerCode); if (basicMapping) { - masterExchange = await findExchangeByCode(basicMapping); + masterExchange = await findExchangeByCode(basicMapping, container); if (masterExchange) { return masterExchange; } @@ -199,8 +209,8 @@ function getBasicExchangeMapping(providerCode: string): string | null { return mappings[providerCode.toUpperCase()] || null; } -async function findExchangeByCode(code: string): Promise { - const postgresClient = getPostgreSQLClient(); +async function findExchangeByCode(code: string, container: IServiceContainer): Promise { + const postgresClient = container.postgres; const query = 'SELECT * FROM exchanges WHERE code = $1'; const result = await postgresClient.query(query, [code]); return result.rows[0] || null; diff --git a/apps/web-api/src/container-setup.ts b/apps/web-api/src/container-setup.ts index 2e71f0f..2cec315 100644 --- a/apps/web-api/src/container-setup.ts +++ b/apps/web-api/src/container-setup.ts @@ -3,7 +3,7 @@ * Configures dependency injection for the web API service */ -import type { ServiceContainer } from '@stock-bot/di'; +import type { IServiceContainer } from '@stock-bot/handlers'; import { getLogger } from '@stock-bot/logger'; import type { AppConfig } from '@stock-bot/config'; @@ -14,8 +14,8 @@ const logger = getLogger('web-api-container'); */ export function setupServiceContainer( config: AppConfig, - container: ServiceContainer -): ServiceContainer { + container: IServiceContainer +): IServiceContainer { logger.info('Configuring web API service container...'); // Web API specific configuration diff --git a/apps/web-api/src/routes/create-routes.ts b/apps/web-api/src/routes/create-routes.ts index 11867ca..3411b9a 100644 --- a/apps/web-api/src/routes/create-routes.ts +++ b/apps/web-api/src/routes/create-routes.ts @@ -4,17 +4,15 @@ */ import { Hono } from 'hono'; -import type { ServiceContainer } from '@stock-bot/di'; -import { healthRoutes, exchangeRoutes } from './index'; +import type { IServiceContainer } from '@stock-bot/handlers'; +import { healthRoutes } from './health.routes'; +import { createExchangeRoutes } from './exchange.routes'; -export function createRoutes(container: ServiceContainer): Hono { +export function createRoutes(container: IServiceContainer): Hono { const app = new Hono(); - // Add container to context for all routes - app.use('*', async (c, next) => { - c.set('container', container); - await next(); - }); + // Create routes with container + const exchangeRoutes = createExchangeRoutes(container); // Mount routes app.route('/health', healthRoutes); diff --git a/apps/web-api/src/routes/exchange.routes.ts b/apps/web-api/src/routes/exchange.routes.ts index 666b411..fd33cf6 100644 --- a/apps/web-api/src/routes/exchange.routes.ts +++ b/apps/web-api/src/routes/exchange.routes.ts @@ -3,7 +3,8 @@ */ import { Hono } from 'hono'; import { getLogger } from '@stock-bot/logger'; -import { exchangeService } from '../services/exchange.service'; +import type { IServiceContainer } from '@stock-bot/handlers'; +import { createExchangeService } from '../services/exchange.service'; import { createSuccessResponse, handleError } from '../utils/error-handler'; import { validateCreateExchange, @@ -13,243 +14,249 @@ import { } from '../utils/validation'; const logger = getLogger('exchange-routes'); -export const exchangeRoutes = new Hono(); -// Get all exchanges with provider mapping counts and mappings -exchangeRoutes.get('/', async c => { - logger.debug('Getting all exchanges'); - try { - const exchanges = await exchangeService.getAllExchanges(); - logger.info('Successfully retrieved exchanges', { count: exchanges.length }); - return c.json(createSuccessResponse(exchanges, undefined, exchanges.length)); - } catch (error) { - logger.error('Failed to get exchanges', { error }); - return handleError(c, error, 'to get exchanges'); - } -}); +export function createExchangeRoutes(container: IServiceContainer) { + const exchangeRoutes = new Hono(); + const exchangeService = createExchangeService(container); -// Get exchange by ID with detailed provider mappings -exchangeRoutes.get('/:id', async c => { - const exchangeId = c.req.param('id'); - logger.debug('Getting exchange by ID', { exchangeId }); - - try { - const result = await exchangeService.getExchangeById(exchangeId); - - if (!result) { - logger.warn('Exchange not found', { exchangeId }); - return c.json(createSuccessResponse(null, 'Exchange not found'), 404); + // Get all exchanges with provider mapping counts and mappings + exchangeRoutes.get('/', async c => { + logger.debug('Getting all exchanges'); + try { + const exchanges = await exchangeService.getAllExchanges(); + logger.info('Successfully retrieved exchanges', { count: exchanges.length }); + return c.json(createSuccessResponse(exchanges, undefined, exchanges.length)); + } catch (error) { + logger.error('Failed to get exchanges', { error }); + return handleError(c, error, 'to get exchanges'); } + }); - logger.info('Successfully retrieved exchange details', { - exchangeId, - exchangeCode: result.exchange.code, - mappingCount: result.provider_mappings.length, - }); - return c.json(createSuccessResponse(result)); - } catch (error) { - logger.error('Failed to get exchange details', { error, exchangeId }); - return handleError(c, error, 'to get exchange details'); - } -}); + // Get exchange by ID with detailed provider mappings + exchangeRoutes.get('/:id', async c => { + const exchangeId = c.req.param('id'); + logger.debug('Getting exchange by ID', { exchangeId }); -// Create new exchange -exchangeRoutes.post('/', async c => { - logger.debug('Creating new exchange'); + try { + const result = await exchangeService.getExchangeById(exchangeId); - try { - const body = await c.req.json(); - logger.debug('Received exchange creation request', { requestBody: body }); + if (!result) { + logger.warn('Exchange not found', { exchangeId }); + return c.json(createSuccessResponse(null, 'Exchange not found'), 404); + } - const validatedData = validateCreateExchange(body); - logger.debug('Exchange data validated successfully', { validatedData }); - - const exchange = await exchangeService.createExchange(validatedData); - logger.info('Exchange created successfully', { - exchangeId: exchange.id, - code: exchange.code, - name: exchange.name, - }); - - return c.json(createSuccessResponse(exchange, 'Exchange created successfully'), 201); - } catch (error) { - logger.error('Failed to create exchange', { error }); - return handleError(c, error, 'to create exchange'); - } -}); - -// Update exchange (activate/deactivate, rename, etc.) -exchangeRoutes.patch('/:id', async c => { - const exchangeId = c.req.param('id'); - logger.debug('Updating exchange', { exchangeId }); - - try { - const body = await c.req.json(); - logger.debug('Received exchange update request', { exchangeId, updates: body }); - - const validatedUpdates = validateUpdateExchange(body); - logger.debug('Exchange update data validated', { exchangeId, validatedUpdates }); - - const exchange = await exchangeService.updateExchange(exchangeId, validatedUpdates); - - if (!exchange) { - logger.warn('Exchange not found for update', { exchangeId }); - return c.json(createSuccessResponse(null, 'Exchange not found'), 404); + logger.info('Successfully retrieved exchange details', { + exchangeId, + exchangeCode: result.exchange.code, + mappingCount: result.provider_mappings.length, + }); + return c.json(createSuccessResponse(result)); + } catch (error) { + logger.error('Failed to get exchange details', { error, exchangeId }); + return handleError(c, error, 'to get exchange details'); } + }); - logger.info('Exchange updated successfully', { - exchangeId, - code: exchange.code, - updates: validatedUpdates, - }); + // Create new exchange + exchangeRoutes.post('/', async c => { + logger.debug('Creating new exchange'); - // Log special actions - if (validatedUpdates.visible === false) { - logger.warn('Exchange marked as hidden - provider mappings will be deleted', { + try { + const body = await c.req.json(); + logger.debug('Received exchange creation request', { requestBody: body }); + + const validatedData = validateCreateExchange(body); + logger.debug('Exchange data validated successfully', { validatedData }); + + const exchange = await exchangeService.createExchange(validatedData); + logger.info('Exchange created successfully', { + exchangeId: exchange.id, + code: exchange.code, + name: exchange.name, + }); + + return c.json(createSuccessResponse(exchange, 'Exchange created successfully'), 201); + } catch (error) { + logger.error('Failed to create exchange', { error }); + return handleError(c, error, 'to create exchange'); + } + }); + + // Update exchange (activate/deactivate, rename, etc.) + exchangeRoutes.patch('/:id', async c => { + const exchangeId = c.req.param('id'); + logger.debug('Updating exchange', { exchangeId }); + + try { + const body = await c.req.json(); + logger.debug('Received exchange update request', { exchangeId, updates: body }); + + const validatedUpdates = validateUpdateExchange(body); + logger.debug('Exchange update data validated', { exchangeId, validatedUpdates }); + + const exchange = await exchangeService.updateExchange(exchangeId, validatedUpdates); + + if (!exchange) { + logger.warn('Exchange not found for update', { exchangeId }); + return c.json(createSuccessResponse(null, 'Exchange not found'), 404); + } + + logger.info('Exchange updated successfully', { exchangeId, code: exchange.code, + updates: validatedUpdates, }); + + // Log special actions + if (validatedUpdates.visible === false) { + logger.warn('Exchange marked as hidden - provider mappings will be deleted', { + exchangeId, + code: exchange.code, + }); + } + + return c.json(createSuccessResponse(exchange, 'Exchange updated successfully')); + } catch (error) { + logger.error('Failed to update exchange', { error, exchangeId }); + return handleError(c, error, 'to update exchange'); } + }); - return c.json(createSuccessResponse(exchange, 'Exchange updated successfully')); - } catch (error) { - logger.error('Failed to update exchange', { error, exchangeId }); - return handleError(c, error, 'to update exchange'); - } -}); + // Get all provider mappings + exchangeRoutes.get('/provider-mappings/all', async c => { + logger.debug('Getting all provider mappings'); -// Get all provider mappings -exchangeRoutes.get('/provider-mappings/all', async c => { - logger.debug('Getting all provider mappings'); - - try { - const mappings = await exchangeService.getAllProviderMappings(); - logger.info('Successfully retrieved all provider mappings', { count: mappings.length }); - return c.json(createSuccessResponse(mappings, undefined, mappings.length)); - } catch (error) { - logger.error('Failed to get provider mappings', { error }); - return handleError(c, error, 'to get provider mappings'); - } -}); - -// Get provider mappings by provider -exchangeRoutes.get('/provider-mappings/:provider', async c => { - const provider = c.req.param('provider'); - logger.debug('Getting provider mappings by provider', { provider }); - - try { - const mappings = await exchangeService.getProviderMappingsByProvider(provider); - logger.info('Successfully retrieved provider mappings', { provider, count: mappings.length }); - - return c.json(createSuccessResponse(mappings, undefined, mappings.length)); - } catch (error) { - logger.error('Failed to get provider mappings', { error, provider }); - return handleError(c, error, 'to get provider mappings'); - } -}); - -// Update provider mapping (activate/deactivate, verify, change confidence) -exchangeRoutes.patch('/provider-mappings/:id', async c => { - const mappingId = c.req.param('id'); - logger.debug('Updating provider mapping', { mappingId }); - - try { - const body = await c.req.json(); - logger.debug('Received provider mapping update request', { mappingId, updates: body }); - - const validatedUpdates = validateUpdateProviderMapping(body); - logger.debug('Provider mapping update data validated', { mappingId, validatedUpdates }); - - const mapping = await exchangeService.updateProviderMapping(mappingId, validatedUpdates); - - if (!mapping) { - logger.warn('Provider mapping not found for update', { mappingId }); - return c.json(createSuccessResponse(null, 'Provider mapping not found'), 404); + try { + const mappings = await exchangeService.getAllProviderMappings(); + logger.info('Successfully retrieved all provider mappings', { count: mappings.length }); + return c.json(createSuccessResponse(mappings, undefined, mappings.length)); + } catch (error) { + logger.error('Failed to get provider mappings', { error }); + return handleError(c, error, 'to get provider mappings'); } + }); - logger.info('Provider mapping updated successfully', { - mappingId, - provider: mapping.provider, - providerExchangeCode: mapping.provider_exchange_code, - updates: validatedUpdates, - }); + // Get provider mappings by provider + exchangeRoutes.get('/provider-mappings/:provider', async c => { + const provider = c.req.param('provider'); + logger.debug('Getting provider mappings by provider', { provider }); - return c.json(createSuccessResponse(mapping, 'Provider mapping updated successfully')); - } catch (error) { - logger.error('Failed to update provider mapping', { error, mappingId }); - return handleError(c, error, 'to update provider mapping'); - } -}); + try { + const mappings = await exchangeService.getProviderMappingsByProvider(provider); + logger.info('Successfully retrieved provider mappings', { provider, count: mappings.length }); -// Create new provider mapping -exchangeRoutes.post('/provider-mappings', async c => { - logger.debug('Creating new provider mapping'); + return c.json(createSuccessResponse(mappings, undefined, mappings.length)); + } catch (error) { + logger.error('Failed to get provider mappings', { error, provider }); + return handleError(c, error, 'to get provider mappings'); + } + }); - try { - const body = await c.req.json(); - logger.debug('Received provider mapping creation request', { requestBody: body }); + // Update provider mapping (activate/deactivate, verify, change confidence) + exchangeRoutes.patch('/provider-mappings/:id', async c => { + const mappingId = c.req.param('id'); + logger.debug('Updating provider mapping', { mappingId }); - const validatedData = validateCreateProviderMapping(body); - logger.debug('Provider mapping data validated successfully', { validatedData }); + try { + const body = await c.req.json(); + logger.debug('Received provider mapping update request', { mappingId, updates: body }); - const mapping = await exchangeService.createProviderMapping(validatedData); - logger.info('Provider mapping created successfully', { - mappingId: mapping.id, - provider: mapping.provider, - providerExchangeCode: mapping.provider_exchange_code, - masterExchangeId: mapping.master_exchange_id, - }); + const validatedUpdates = validateUpdateProviderMapping(body); + logger.debug('Provider mapping update data validated', { mappingId, validatedUpdates }); - return c.json(createSuccessResponse(mapping, 'Provider mapping created successfully'), 201); - } catch (error) { - logger.error('Failed to create provider mapping', { error }); - return handleError(c, error, 'to create provider mapping'); - } -}); + const mapping = await exchangeService.updateProviderMapping(mappingId, validatedUpdates); -// Get all available providers -exchangeRoutes.get('/providers/list', async c => { - logger.debug('Getting providers list'); + if (!mapping) { + logger.warn('Provider mapping not found for update', { mappingId }); + return c.json(createSuccessResponse(null, 'Provider mapping not found'), 404); + } - try { - const providers = await exchangeService.getProviders(); - logger.info('Successfully retrieved providers list', { count: providers.length, providers }); - return c.json(createSuccessResponse(providers)); - } catch (error) { - logger.error('Failed to get providers list', { error }); - return handleError(c, error, 'to get providers list'); - } -}); + logger.info('Provider mapping updated successfully', { + mappingId, + provider: mapping.provider, + providerExchangeCode: mapping.provider_exchange_code, + updates: validatedUpdates, + }); -// Get unmapped provider exchanges by provider -exchangeRoutes.get('/provider-exchanges/unmapped/:provider', async c => { - const provider = c.req.param('provider'); - logger.debug('Getting unmapped provider exchanges', { provider }); + return c.json(createSuccessResponse(mapping, 'Provider mapping updated successfully')); + } catch (error) { + logger.error('Failed to update provider mapping', { error, mappingId }); + return handleError(c, error, 'to update provider mapping'); + } + }); - try { - const exchanges = await exchangeService.getUnmappedProviderExchanges(provider); - logger.info('Successfully retrieved unmapped provider exchanges', { - provider, - count: exchanges.length, - }); + // Create new provider mapping + exchangeRoutes.post('/provider-mappings', async c => { + logger.debug('Creating new provider mapping'); - return c.json(createSuccessResponse(exchanges, undefined, exchanges.length)); - } catch (error) { - logger.error('Failed to get unmapped provider exchanges', { error, provider }); - return handleError(c, error, 'to get unmapped provider exchanges'); - } -}); + try { + const body = await c.req.json(); + logger.debug('Received provider mapping creation request', { requestBody: body }); -// Get exchange statistics -exchangeRoutes.get('/stats/summary', async c => { - logger.debug('Getting exchange statistics'); + const validatedData = validateCreateProviderMapping(body); + logger.debug('Provider mapping data validated successfully', { validatedData }); - try { - const stats = await exchangeService.getExchangeStats(); - logger.info('Successfully retrieved exchange statistics', { stats }); - return c.json(createSuccessResponse(stats)); - } catch (error) { - logger.error('Failed to get exchange statistics', { error }); - return handleError(c, error, 'to get exchange statistics'); - } -}); + const mapping = await exchangeService.createProviderMapping(validatedData); + logger.info('Provider mapping created successfully', { + mappingId: mapping.id, + provider: mapping.provider, + providerExchangeCode: mapping.provider_exchange_code, + masterExchangeId: mapping.master_exchange_id, + }); + + return c.json(createSuccessResponse(mapping, 'Provider mapping created successfully'), 201); + } catch (error) { + logger.error('Failed to create provider mapping', { error }); + return handleError(c, error, 'to create provider mapping'); + } + }); + + // Get all available providers + exchangeRoutes.get('/providers/list', async c => { + logger.debug('Getting providers list'); + + try { + const providers = await exchangeService.getProviders(); + logger.info('Successfully retrieved providers list', { count: providers.length, providers }); + return c.json(createSuccessResponse(providers)); + } catch (error) { + logger.error('Failed to get providers list', { error }); + return handleError(c, error, 'to get providers list'); + } + }); + + // Get unmapped provider exchanges by provider + exchangeRoutes.get('/provider-exchanges/unmapped/:provider', async c => { + const provider = c.req.param('provider'); + logger.debug('Getting unmapped provider exchanges', { provider }); + + try { + const exchanges = await exchangeService.getUnmappedProviderExchanges(provider); + logger.info('Successfully retrieved unmapped provider exchanges', { + provider, + count: exchanges.length, + }); + + return c.json(createSuccessResponse(exchanges, undefined, exchanges.length)); + } catch (error) { + logger.error('Failed to get unmapped provider exchanges', { error, provider }); + return handleError(c, error, 'to get unmapped provider exchanges'); + } + }); + + // Get exchange statistics + exchangeRoutes.get('/stats/summary', async c => { + logger.debug('Getting exchange statistics'); + + try { + const stats = await exchangeService.getExchangeStats(); + logger.info('Successfully retrieved exchange statistics', { stats }); + return c.json(createSuccessResponse(stats)); + } catch (error) { + logger.error('Failed to get exchange statistics', { error }); + return handleError(c, error, 'to get exchange statistics'); + } + }); + + return exchangeRoutes; +} \ No newline at end of file diff --git a/apps/web-api/src/routes/index.ts b/apps/web-api/src/routes/index.ts index 8a1e802..61bb46e 100644 --- a/apps/web-api/src/routes/index.ts +++ b/apps/web-api/src/routes/index.ts @@ -1,5 +1,5 @@ /** * Routes index - exports all route modules */ -export { exchangeRoutes } from './exchange.routes'; +export { createExchangeRoutes } from './exchange.routes'; export { healthRoutes } from './health.routes'; diff --git a/apps/web-api/src/services/exchange.service.ts b/apps/web-api/src/services/exchange.service.ts index 95eafad..cb48694 100644 --- a/apps/web-api/src/services/exchange.service.ts +++ b/apps/web-api/src/services/exchange.service.ts @@ -1,5 +1,5 @@ import { getLogger } from '@stock-bot/logger'; -import { getMongoDBClient, getPostgreSQLClient } from '../clients'; +import type { IServiceContainer } from '@stock-bot/handlers'; import { CreateExchangeRequest, CreateProviderMappingRequest, @@ -15,12 +15,14 @@ import { const logger = getLogger('exchange-service'); export class ExchangeService { + constructor(private container: IServiceContainer) {} + private get postgresClient() { - return getPostgreSQLClient(); + return this.container.postgres; } private get mongoClient() { - return getMongoDBClient(); + return this.container.mongodb; } // Exchanges @@ -375,5 +377,7 @@ export class ExchangeService { } } -// Export singleton instance -export const exchangeService = new ExchangeService(); +// Export function to create service instance with container +export function createExchangeService(container: IServiceContainer): ExchangeService { + return new ExchangeService(container); +} \ No newline at end of file