/** * @ai-summary Fastify route handlers for stations API * @ai-context HTTP request/response handling with Fastify reply methods */ import { FastifyRequest, FastifyReply } from 'fastify'; import { StationsService } from '../domain/stations.service'; import { StationsRepository } from '../data/stations.repository'; import { pool } from '../../../core/config/database'; import { logger } from '../../../core/logging/logger'; import { StationSearchBody, StationMatchBody, SaveStationBody, StationParams, UpdateSavedStationBody } from '../domain/stations.types'; import { googleMapsClient } from '../external/google-maps/google-maps.client'; export class StationsController { private stationsService: StationsService; constructor() { const repository = new StationsRepository(pool); this.stationsService = new StationsService(repository); } async searchStations(request: FastifyRequest<{ Body: StationSearchBody }>, reply: FastifyReply) { try { const userId = request.userContext!.userId; const { latitude, longitude, radius, fuelType } = request.body; if (!latitude || !longitude) { return reply.code(400).send({ error: 'Bad Request', message: 'Latitude and longitude are required' }); } const result = await this.stationsService.searchNearbyStations({ latitude, longitude, radius, fuelType }, userId); return reply.code(200).send(result); } catch (error: any) { logger.error('Error searching stations', { error, userId: request.userContext?.userId }); return reply.code(500).send({ error: 'Internal server error', message: 'Failed to search stations' }); } } async matchStation(request: FastifyRequest<{ Body: StationMatchBody }>, reply: FastifyReply) { try { const { merchantName } = request.body; if (!merchantName || !merchantName.trim()) { return reply.code(400).send({ error: 'Bad Request', message: 'Merchant name is required', }); } const result = await this.stationsService.matchStationFromReceipt(merchantName); return reply.code(200).send(result); } catch (error: any) { logger.error('Error matching station from receipt', { error, merchantName: request.body?.merchantName }); return reply.code(500).send({ error: 'Internal server error', message: 'Failed to match station', }); } } async saveStation(request: FastifyRequest<{ Body: SaveStationBody }>, reply: FastifyReply) { try { const userId = request.userContext!.userId; const { placeId, nickname, notes, isFavorite, has93Octane, has93OctaneEthanolFree } = request.body; if (!placeId) { return reply.code(400).send({ error: 'Bad Request', message: 'Place ID is required' }); } const result = await this.stationsService.saveStation(placeId, userId, { nickname, notes, isFavorite, has93Octane, has93OctaneEthanolFree }); return reply.code(201).send(result); } catch (error: any) { logger.error('Error saving station', { error, userId: request.userContext?.userId }); if (error.message.includes('not found')) { return reply.code(404).send({ error: 'Not Found', message: error.message }); } return reply.code(500).send({ error: 'Internal server error', message: 'Failed to save station' }); } } async updateSavedStation( request: FastifyRequest<{ Params: StationParams; Body: UpdateSavedStationBody }>, reply: FastifyReply ) { try { const userId = request.userContext!.userId; const { placeId } = request.params; const result = await this.stationsService.updateSavedStation(placeId, userId, request.body); return reply.code(200).send(result); } catch (error: any) { logger.error('Error updating saved station', { error, placeId: request.params.placeId, userId: request.userContext?.userId }); if (error.message.includes('not found')) { return reply.code(404).send({ error: 'Not Found', message: error.message }); } return reply.code(500).send({ error: 'Internal server error', message: 'Failed to update saved station' }); } } async getSavedStations(request: FastifyRequest, reply: FastifyReply) { try { const userId = request.userContext!.userId; const result = await this.stationsService.getUserSavedStations(userId); return reply.code(200).send(result); } catch (error: any) { logger.error('Error getting saved stations', { error, userId: request.userContext?.userId }); return reply.code(500).send({ error: 'Internal server error', message: 'Failed to get saved stations' }); } } async removeSavedStation(request: FastifyRequest<{ Params: StationParams }>, reply: FastifyReply) { try { const userId = request.userContext!.userId; const { placeId } = request.params; await this.stationsService.removeSavedStation(placeId, userId); return reply.code(204).send(); } catch (error: any) { logger.error('Error removing saved station', { error, placeId: request.params.placeId, userId: request.userContext?.userId }); if (error.message.includes('not found')) { return reply.code(404).send({ error: 'Not Found', message: error.message }); } return reply.code(500).send({ error: 'Internal server error', message: 'Failed to remove saved station' }); } } async getStationPhoto(request: FastifyRequest<{ Params: { reference: string } }>, reply: FastifyReply) { try { const { reference } = request.params; if (!reference) { return reply.code(400).send({ error: 'Bad Request', message: 'Photo reference is required' }); } const photoBuffer = await googleMapsClient.fetchPhoto(reference); return reply .code(200) .header('Content-Type', 'image/jpeg') .header('Cache-Control', 'public, max-age=86400') .send(photoBuffer); } catch (error: any) { logger.error('Error fetching station photo', { error, reference: request.params.reference }); return reply.code(500).send({ error: 'Internal server error', message: 'Failed to fetch station photo' }); } } }