451 lines
14 KiB
Go
451 lines
14 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"portfolio-tracker/internal/model"
|
|
"portfolio-tracker/internal/service"
|
|
"portfolio-tracker/internal/web/templates"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/a-h/templ"
|
|
)
|
|
|
|
func PortfolioHandler(w http.ResponseWriter, r *http.Request) {
|
|
auth, username, err := getSessionInfo(r)
|
|
if err != nil {
|
|
fmt.Printf("Session error in PortfolioHandler: %v\n", err)
|
|
}
|
|
|
|
portfolios := getUserPortfolios(username)
|
|
|
|
component := templates.Portfolio(auth, username, portfolios)
|
|
templ.Handler(component).ServeHTTP(w, r)
|
|
}
|
|
|
|
func PortfolioDetailHandler(w http.ResponseWriter, r *http.Request) {
|
|
auth, username, err := getSessionInfo(r)
|
|
if err != nil {
|
|
fmt.Printf("Session error in PortfolioDetailHandler: %v\n", err)
|
|
}
|
|
|
|
// Extract portfolio ID from URL path
|
|
path := r.URL.Path
|
|
portfolioIDStr := strings.TrimPrefix(path, "/portfolio/")
|
|
portfolioID, err := strconv.ParseUint(portfolioIDStr, 10, 32)
|
|
if err != nil {
|
|
http.Error(w, "Ungültige Portfolio-ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get user
|
|
var user model.User
|
|
if err := DB.Where("username = ?", username).First(&user).Error; err != nil {
|
|
http.Error(w, "Benutzer nicht gefunden", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Get portfolio with activities
|
|
var portfolio model.Portfolio
|
|
if err := DB.Preload("Activities").Where("id = ? AND user_id = ?", portfolioID, user.ID).First(&portfolio).Error; err != nil {
|
|
http.Error(w, "Portfolio nicht gefunden oder keine Berechtigung", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
// Calculate positions
|
|
fmt.Printf("DEBUG: Starting position calculation for portfolio %d with %d activities\n", portfolio.ID, len(portfolio.Activities))
|
|
positionCalculator := service.NewPositionCalculator()
|
|
positions, positionSummary, err := positionCalculator.CalculatePositions(portfolio)
|
|
if err != nil {
|
|
fmt.Printf("Error calculating positions: %v\n", err)
|
|
// Continue with empty positions rather than failing
|
|
positions = []model.Position{}
|
|
positionSummary = model.PositionSummary{
|
|
Currency: portfolio.BaseCurrency,
|
|
LastUpdated: time.Now(),
|
|
}
|
|
}
|
|
|
|
fmt.Printf("DEBUG: Position calculation completed. Found %d positions\n", len(positions))
|
|
for i, pos := range positions {
|
|
fmt.Printf("DEBUG: Position %d: %s - Shares: %.4f, Open: %v\n", i+1, pos.Stock, pos.Shares, pos.IsOpen())
|
|
}
|
|
fmt.Printf("DEBUG: Position summary - Total Value: %.2f, Total Cost: %.2f\n", positionSummary.TotalValue, positionSummary.TotalCostBasis)
|
|
|
|
// Get all user portfolios for navigation
|
|
portfolios := getUserPortfolios(username)
|
|
|
|
component := templates.PortfolioDetail(auth, username, portfolio, portfolios, positions, positionSummary)
|
|
templ.Handler(component).ServeHTTP(w, r)
|
|
}
|
|
|
|
func CreatePortfolioHandler(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Nur POST erlaubt", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
auth, username, err := getSessionInfo(r)
|
|
if err != nil {
|
|
fmt.Printf("Session error in CreatePortfolioHandler: %v\n", err)
|
|
}
|
|
|
|
if !auth {
|
|
http.Error(w, "Nicht angemeldet", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
if username == "" {
|
|
http.Error(w, "Benutzer nicht gefunden", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get form values
|
|
name := r.FormValue("name")
|
|
baseCurrency := r.FormValue("base_currency")
|
|
description := r.FormValue("description")
|
|
|
|
// Basic validation
|
|
if name == "" || baseCurrency == "" {
|
|
http.Error(w, "Name und Basiswährung sind erforderlich", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get user from database
|
|
var user model.User
|
|
if err := DB.Where("username = ?", username).First(&user).Error; err != nil {
|
|
http.Error(w, "Benutzer nicht gefunden", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Create new portfolio
|
|
portfolio := model.Portfolio{
|
|
UserID: user.ID,
|
|
Name: name,
|
|
BaseCurrency: baseCurrency,
|
|
Description: description,
|
|
}
|
|
|
|
if err := DB.Create(&portfolio).Error; err != nil {
|
|
fmt.Printf("Error creating portfolio: %v\n", err)
|
|
http.Error(w, "Fehler beim Erstellen des Portfolios", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("Portfolio created successfully: ID=%d, Name=%s, User=%s\n", portfolio.ID, portfolio.Name, username)
|
|
|
|
// Redirect to the new portfolio
|
|
http.Redirect(w, r, fmt.Sprintf("/portfolio/%d", portfolio.ID), http.StatusSeeOther)
|
|
}
|
|
|
|
func PortfolioTransactionHandler(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Nur POST erlaubt", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
auth, username, err := getSessionInfo(r)
|
|
if err != nil {
|
|
fmt.Printf("Session error in PortfolioTransactionHandler: %v\n", err)
|
|
}
|
|
|
|
if !auth {
|
|
http.Error(w, "Nicht angemeldet", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
if username == "" {
|
|
http.Error(w, "Benutzer nicht gefunden", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get form values
|
|
portfolioIDStr := r.FormValue("portfolio_id")
|
|
transactionType := r.FormValue("type")
|
|
stock := r.FormValue("stock")
|
|
amountStr := r.FormValue("amount")
|
|
priceStr := r.FormValue("price")
|
|
dateStr := r.FormValue("date")
|
|
note := r.FormValue("note")
|
|
|
|
// Basic validation
|
|
if transactionType == "" || stock == "" || amountStr == "" || priceStr == "" || dateStr == "" {
|
|
http.Error(w, "Alle Pflichtfelder müssen ausgefüllt werden", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Parse form values
|
|
amount, err := strconv.ParseFloat(amountStr, 64)
|
|
if err != nil {
|
|
http.Error(w, "Ungültiger Betrag", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
price, err := strconv.ParseFloat(priceStr, 64)
|
|
if err != nil {
|
|
http.Error(w, "Ungültiger Preis", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
date, err := time.Parse("2006-01-02", dateStr)
|
|
if err != nil {
|
|
http.Error(w, "Ungültiges Datum", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
portfolioID, err := strconv.ParseUint(portfolioIDStr, 10, 32)
|
|
if err != nil {
|
|
http.Error(w, "Ungültige Portfolio-ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Verify portfolio belongs to user
|
|
var portfolio model.Portfolio
|
|
if err := DB.Joins("User").Where("portfolios.id = ? AND User.username = ?", portfolioID, username).First(&portfolio).Error; err != nil {
|
|
http.Error(w, "Portfolio nicht gefunden oder keine Berechtigung", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Fetch stock currency from Yahoo Finance
|
|
stockCurrency, err := getStockCurrency(stock)
|
|
if err != nil {
|
|
fmt.Printf("Warning: Could not fetch currency for stock %s: %v. Using portfolio base currency.\n", stock, err)
|
|
stockCurrency = portfolio.BaseCurrency
|
|
}
|
|
|
|
fmt.Printf("Stock %s currency: %s\n", stock, stockCurrency)
|
|
|
|
// Handle GBp (pence) to GBP (pounds) conversion
|
|
// If the stock is quoted in pence, convert price to pounds
|
|
if stockCurrency == "GBp" {
|
|
price = price / 100.0
|
|
stockCurrency = "GBP"
|
|
fmt.Printf("Converted GBp price %.2f to GBP price %.2f\n", price*100, price)
|
|
}
|
|
|
|
// Create activity
|
|
activity := model.Activity{
|
|
PortfolioID: uint(portfolioID),
|
|
Stock: stock,
|
|
Type: model.ActivityType(transactionType),
|
|
Amount: amount,
|
|
Price: price,
|
|
Currency: stockCurrency, // Use the fetched stock currency
|
|
Date: date,
|
|
Note: note,
|
|
}
|
|
|
|
if err := DB.Create(&activity).Error; err != nil {
|
|
fmt.Printf("Error creating activity: %v\n", err)
|
|
http.Error(w, "Fehler beim Speichern der Transaktion", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("Activity created successfully: ID=%d, Type=%s, Stock=%s, Currency=%s, Portfolio=%d\n",
|
|
activity.ID, activity.Type, activity.Stock, activity.Currency, activity.PortfolioID)
|
|
|
|
// Redirect to portfolio page
|
|
http.Redirect(w, r, fmt.Sprintf("/portfolio/%d", portfolioID), http.StatusSeeOther)
|
|
}
|
|
|
|
func EditTransactionHandler(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Nur POST erlaubt", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
auth, username, err := getSessionInfo(r)
|
|
if err != nil {
|
|
fmt.Printf("Session error in EditTransactionHandler: %v\n", err)
|
|
}
|
|
|
|
if !auth {
|
|
http.Error(w, "Nicht angemeldet", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
if username == "" {
|
|
http.Error(w, "Benutzer nicht gefunden", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get form values
|
|
activityIDStr := r.FormValue("activity_id")
|
|
portfolioIDStr := r.FormValue("portfolio_id")
|
|
transactionType := r.FormValue("type")
|
|
stock := r.FormValue("stock")
|
|
amountStr := r.FormValue("amount")
|
|
priceStr := r.FormValue("price")
|
|
dateStr := r.FormValue("date")
|
|
note := r.FormValue("note")
|
|
|
|
// Basic validation
|
|
if activityIDStr == "" || transactionType == "" || stock == "" || amountStr == "" || priceStr == "" || dateStr == "" {
|
|
http.Error(w, "Alle Pflichtfelder müssen ausgefüllt werden", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Parse form values
|
|
activityID, err := strconv.ParseUint(activityIDStr, 10, 32)
|
|
if err != nil {
|
|
http.Error(w, "Ungültige Aktivitäts-ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
portfolioID, err := strconv.ParseUint(portfolioIDStr, 10, 32)
|
|
if err != nil {
|
|
http.Error(w, "Ungültige Portfolio-ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
amount, err := strconv.ParseFloat(amountStr, 64)
|
|
if err != nil {
|
|
http.Error(w, "Ungültiger Betrag", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
price, err := strconv.ParseFloat(priceStr, 64)
|
|
if err != nil {
|
|
http.Error(w, "Ungültiger Preis", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
date, err := time.Parse("2006-01-02", dateStr)
|
|
if err != nil {
|
|
http.Error(w, "Ungültiges Datum", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get existing activity and verify ownership using subquery
|
|
var activity model.Activity
|
|
if err := DB.Where("id = ? AND portfolio_id IN (SELECT id FROM portfolios WHERE user_id = (SELECT id FROM users WHERE username = ?))",
|
|
activityID, username).First(&activity).Error; err != nil {
|
|
http.Error(w, "Aktivität nicht gefunden oder keine Berechtigung", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Verify the activity belongs to the specified portfolio
|
|
if activity.PortfolioID != uint(portfolioID) {
|
|
http.Error(w, "Aktivität gehört nicht zu diesem Portfolio", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get portfolio to access base currency
|
|
var portfolio model.Portfolio
|
|
if err := DB.Where("id = ?", portfolioID).First(&portfolio).Error; err != nil {
|
|
http.Error(w, "Portfolio nicht gefunden", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Fetch stock currency from Yahoo Finance (or use portfolio base currency as fallback)
|
|
stockCurrency, err := getStockCurrency(stock)
|
|
if err != nil {
|
|
fmt.Printf("Warning: Could not fetch currency for stock %s: %v. Using portfolio base currency.\n", stock, err)
|
|
stockCurrency = portfolio.BaseCurrency
|
|
}
|
|
|
|
// Handle GBp (pence) to GBP (pounds) conversion
|
|
// If the stock is quoted in pence, convert price to pounds
|
|
if stockCurrency == "GBp" {
|
|
price = price / 100.0
|
|
stockCurrency = "GBP"
|
|
fmt.Printf("Converted GBp price %.2f to GBP price %.2f\n", price*100, price)
|
|
}
|
|
|
|
// Update activity fields
|
|
activity.Type = model.ActivityType(transactionType)
|
|
activity.Stock = stock
|
|
activity.Amount = amount
|
|
activity.Price = price
|
|
activity.Currency = stockCurrency
|
|
activity.Date = date
|
|
activity.Note = note
|
|
|
|
// Save updated activity
|
|
if err := DB.Save(&activity).Error; err != nil {
|
|
fmt.Printf("Error updating activity: %v\n", err)
|
|
http.Error(w, "Fehler beim Aktualisieren der Transaktion", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("Activity updated successfully: ID=%d, Type=%s, Stock=%s, Portfolio=%d\n",
|
|
activity.ID, activity.Type, activity.Stock, activity.PortfolioID)
|
|
|
|
// Redirect to portfolio page
|
|
http.Redirect(w, r, fmt.Sprintf("/portfolio/%d", portfolioID), http.StatusSeeOther)
|
|
}
|
|
|
|
func DeleteTransactionHandler(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Nur POST erlaubt", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
auth, username, err := getSessionInfo(r)
|
|
if err != nil {
|
|
fmt.Printf("Session error in DeleteTransactionHandler: %v\n", err)
|
|
}
|
|
|
|
if !auth {
|
|
http.Error(w, "Nicht angemeldet", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
if username == "" {
|
|
http.Error(w, "Benutzer nicht gefunden", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get form values
|
|
activityIDStr := r.FormValue("activity_id")
|
|
portfolioIDStr := r.FormValue("portfolio_id")
|
|
|
|
// Basic validation
|
|
if activityIDStr == "" || portfolioIDStr == "" {
|
|
http.Error(w, "Aktivitäts-ID und Portfolio-ID sind erforderlich", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Parse form values
|
|
activityID, err := strconv.ParseUint(activityIDStr, 10, 32)
|
|
if err != nil {
|
|
http.Error(w, "Ungültige Aktivitäts-ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
portfolioID, err := strconv.ParseUint(portfolioIDStr, 10, 32)
|
|
if err != nil {
|
|
http.Error(w, "Ungültige Portfolio-ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get existing activity and verify ownership using subquery
|
|
var activity model.Activity
|
|
if err := DB.Where("id = ? AND portfolio_id IN (SELECT id FROM portfolios WHERE user_id = (SELECT id FROM users WHERE username = ?))",
|
|
activityID, username).First(&activity).Error; err != nil {
|
|
http.Error(w, "Aktivität nicht gefunden oder keine Berechtigung", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Verify the activity belongs to the specified portfolio
|
|
if activity.PortfolioID != uint(portfolioID) {
|
|
http.Error(w, "Aktivität gehört nicht zu diesem Portfolio", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Delete the activity
|
|
if err := DB.Delete(&activity).Error; err != nil {
|
|
fmt.Printf("Error deleting activity: %v\n", err)
|
|
http.Error(w, "Fehler beim Löschen der Transaktion", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("Activity deleted successfully: ID=%d, Type=%s, Stock=%s, Portfolio=%d\n",
|
|
activity.ID, activity.Type, activity.Stock, activity.PortfolioID)
|
|
|
|
// Redirect to portfolio page
|
|
http.Redirect(w, r, fmt.Sprintf("/portfolio/%d", portfolioID), http.StatusSeeOther)
|
|
}
|