Files
stock-tax-analyzer/backend/main.go
2025-10-12 00:44:13 +02:00

181 lines
4.2 KiB
Go

package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
)
type StockPriceRequest struct {
Tickers []string `json:"tickers"`
}
type StockPrice struct {
Ticker string `json:"ticker"`
USD float64 `json:"usd"`
EUR float64 `json:"eur"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}
type StockPricesResponse struct {
Prices map[string]StockPrice `json:"prices"`
ExchangeRate float64 `json:"exchangeRate"`
}
type YahooChartResponse struct {
Chart struct {
Result []struct {
Meta struct {
RegularMarketPrice float64 `json:"regularMarketPrice"`
} `json:"meta"`
} `json:"result"`
Error *struct {
Description string `json:"description"`
} `json:"error"`
} `json:"chart"`
}
type ExchangeRateResponse struct {
Rates map[string]float64 `json:"rates"`
}
func fetchExchangeRate() (float64, error) {
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get("https://api.exchangerate-api.com/v4/latest/USD")
if err != nil {
return 0.92, fmt.Errorf("failed to fetch exchange rate: %w", err)
}
defer resp.Body.Close()
var rateData ExchangeRateResponse
if err := json.NewDecoder(resp.Body).Decode(&rateData); err != nil {
return 0.92, fmt.Errorf("failed to decode exchange rate: %w", err)
}
if eurRate, ok := rateData.Rates["EUR"]; ok {
return eurRate, nil
}
return 0.92, fmt.Errorf("EUR rate not found")
}
func fetchStockPrice(ticker string) (float64, error) {
client := &http.Client{Timeout: 10 * time.Second}
// Try Yahoo Finance API
url := fmt.Sprintf("https://query1.finance.yahoo.com/v8/finance/chart/%s?interval=1d&range=1d", ticker)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return 0, err
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
resp, err := client.Do(req)
if err != nil {
return 0, fmt.Errorf("failed to fetch from Yahoo: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, fmt.Errorf("failed to read response: %w", err)
}
var chartData YahooChartResponse
if err := json.Unmarshal(body, &chartData); err != nil {
return 0, fmt.Errorf("failed to decode Yahoo response: %w", err)
}
if chartData.Chart.Error != nil {
return 0, fmt.Errorf("Yahoo API error: %s", chartData.Chart.Error.Description)
}
if len(chartData.Chart.Result) > 0 {
price := chartData.Chart.Result[0].Meta.RegularMarketPrice
if price > 0 {
return price, nil
}
}
return 0, fmt.Errorf("price not found in response")
}
func stockPricesHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req StockPriceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// Fetch exchange rate
exchangeRate, err := fetchExchangeRate()
if err != nil {
log.Printf("Warning: Using fallback exchange rate: %v", err)
exchangeRate = 0.92 // Fallback
}
prices := make(map[string]StockPrice)
// Fetch prices for each ticker
for _, ticker := range req.Tickers {
ticker = strings.TrimSpace(ticker)
if ticker == "" {
continue
}
usdPrice, err := fetchStockPrice(ticker)
if err != nil {
log.Printf("Error fetching price for %s: %v", ticker, err)
prices[ticker] = StockPrice{
Ticker: ticker,
Success: false,
Error: err.Error(),
}
} else {
prices[ticker] = StockPrice{
Ticker: ticker,
USD: usdPrice,
EUR: usdPrice * exchangeRate,
Success: true,
}
}
// Small delay to avoid rate limiting
time.Sleep(100 * time.Millisecond)
}
response := StockPricesResponse{
Prices: prices,
ExchangeRate: exchangeRate,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func main() {
// API endpoint
http.HandleFunc("/api/stock-prices", stockPricesHandler)
// Serve static files from /app/static directory
fs := http.FileServer(http.Dir("/app/static"))
http.Handle("/", fs)
port := ":8080"
log.Printf("Server starting on port %s", port)
if err := http.ListenAndServe(port, nil); err != nil {
log.Fatal(err)
}
}