package controllers import ( "fmt" "net/http" "time" "fotbal-club/internal/models" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // TicketCheckoutController handles ticket purchases integrated with e-shop type TicketCheckoutController struct { DB *gorm.DB } func NewTicketCheckoutController(db *gorm.DB) *TicketCheckoutController { return &TicketCheckoutController{DB: db} } // TicketCheckoutRequest represents a ticket purchase request type TicketCheckoutRequest struct { // Customer info FirstName string `json:"first_name" binding:"required"` LastName string `json:"last_name" binding:"required"` Email string `json:"email" binding:"required,email"` Phone string `json:"phone"` // Ticket reservations TicketReservations []TicketReservationRequest `json:"ticket_reservations" binding:"required,min=1"` // Payment method PaymentMethod string `json:"payment_method" binding:"required,oneof=stripe gopay"` } type TicketReservationRequest struct { TicketID uint `json:"ticket_id" binding:"required"` // Note: Quantity is fixed per reservation, but we allow multiple reservations per order } // CreateTicketOrder creates an e-shop order from ticket reservations func (tcc *TicketCheckoutController) CreateTicketOrder(c *gin.Context) { var req TicketCheckoutRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Get user info if logged in userIDVal, _ := c.Get("userID") var userID *uint if u, ok := userIDVal.(uint); ok { userID = &u } // Validate all ticket reservations exist and are available var ticketIDs []uint for _, tr := range req.TicketReservations { ticketIDs = append(ticketIDs, tr.TicketID) } var tickets []models.Ticket if err := tcc.DB.Where("id IN ? AND status = ?", ticketIDs, "reserved"). Preload("Campaign").Preload("TicketType").Find(&tickets).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch tickets"}) return } if len(tickets) != len(req.TicketReservations) { c.JSON(http.StatusBadRequest, gin.H{"error": "Some tickets not found or not available"}) return } // Verify all tickets belong to the same customer (email) for _, ticket := range tickets { if ticket.HolderEmail != req.Email { c.JSON(http.StatusBadRequest, gin.H{"error": "Ticket holder email doesn't match customer email"}) return } } // Calculate total amount var totalAmount int64 for _, ticket := range tickets { totalAmount += ticket.TotalPriceCents } // Generate order number orderNumber := fmt.Sprintf("%s%d", time.Now().Format("200601"), time.Now().Unix()%100000) // Create e-shop order ticketOrderFlag := uint(1) order := models.EshopOrder{ OrderNumber: orderNumber, UserID: userID, Email: req.Email, FirstName: req.FirstName, LastName: req.LastName, Status: "awaiting_payment", TotalAmountCents: totalAmount, Currency: "CZK", // Tickets are always in CZK for now TicketOrder: &ticketOrderFlag, ShippingMethod: "digital", // Tickets are digital ShippingPriceCents: 0, // No shipping for digital tickets } tx := tcc.DB.Begin() // Create order if err := tx.Create(&order).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create order"}) return } // Create order items for tickets for _, ticket := range tickets { itemName := fmt.Sprintf("%s - %s", ticket.Campaign.Title, ticket.TicketType.Name) if ticket.Campaign.HomeTeam != nil && ticket.Campaign.AwayTeam != nil { itemName = fmt.Sprintf("%s %s vs %s - %s", itemName, *ticket.Campaign.HomeTeam, *ticket.Campaign.AwayTeam, ticket.TicketType.Name) } orderItem := models.EshopOrderItem{ OrderID: order.ID, Name: itemName, Quantity: ticket.Quantity, UnitPriceCents: ticket.UnitPriceCents, Currency: ticket.Currency, VATRate: 0.21, // 21% VAT for tickets TicketID: &ticket.ID, // Link to ticket } if err := tx.Create(&orderItem).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create order item"}) return } // Update ticket to link with order if err := tx.Model(&ticket).Update("order_id", order.ID).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to link ticket to order"}) return } } tx.Commit() // Create payment based on method var paymentResult interface{} var err error switch req.PaymentMethod { case "stripe": paymentResult, err = tcc.createStripePayment(&order) case "gopay": paymentResult, err = tcc.createGoPayPayment(&order) default: c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported payment method"}) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create payment", "details": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{ "order": order, "payment": paymentResult, }) } // CompleteTicketOrder handles successful payment completion for tickets func (tcc *TicketCheckoutController) CompleteTicketOrder(c *gin.Context) { orderID := c.Param("order_id") var order models.EshopOrder if err := tcc.DB.Where("id = ? AND ticket_order = ?", orderID, true).First(&order).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Ticket order not found"}) return } if order.Status != "paid" { c.JSON(http.StatusBadRequest, gin.H{"error": "Order not paid"}) return } // Get all tickets for this order var tickets []models.Ticket if err := tcc.DB.Where("order_id = ?", order.ID).Find(&tickets).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch tickets"}) return } // Confirm all tickets tx := tcc.DB.Begin() for _, ticket := range tickets { // Update ticket status to paid if err := tx.Model(&ticket).Updates(map[string]interface{}{ "status": "paid", }).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to confirm ticket"}) return } // Update availability if err := tx.Model(&models.TicketAvailability{}). Where("campaign_id = ? AND ticket_type_id = ?", ticket.CampaignID, ticket.TicketTypeID). Updates(map[string]interface{}{ "sold_quantity": gorm.Expr("sold_quantity + ?", ticket.Quantity), "reserved_quantity": gorm.Expr("reserved_quantity - ?", ticket.Quantity), }).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update availability"}) return } // Send ticket email (async) go func(t models.Ticket) { // TODO: Implement ticket confirmation email with barcode/QR // For now, just log the action fmt.Printf("Ticket confirmation email sent to %s for ticket ID %d\n", t.HolderEmail, t.ID) }(ticket) } tx.Commit() c.JSON(http.StatusOK, gin.H{ "message": "Ticket order completed successfully", "order": order, "tickets": tickets, }) } // GetTicketOrders returns all ticket orders for a user func (tcc *TicketCheckoutController) GetTicketOrders(c *gin.Context) { userIDVal, _ := c.Get("userID") userID, ok := userIDVal.(uint) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) return } var orders []models.EshopOrder if err := tcc.DB.Where("user_id = ? AND ticket_order = ?", userID, true). Preload("Items.Ticket"). Preload("Items.Ticket.Campaign"). Preload("Items.Ticket.TicketType"). Order("created_at DESC"). Find(&orders).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch ticket orders"}) return } c.JSON(http.StatusOK, orders) } // Helper methods for payment creation func (tcc *TicketCheckoutController) createStripePayment(order *models.EshopOrder) (interface{}, error) { // This would integrate with the existing Stripe service // For now, return a mock response return gin.H{ "payment_intent_id": "pi_mock_" + order.OrderNumber, "client_secret": "pi_mock_secret_" + order.OrderNumber, }, nil } func (tcc *TicketCheckoutController) createGoPayPayment(order *models.EshopOrder) (interface{}, error) { // This would integrate with the existing GoPay service // For now, return a mock response return gin.H{ "payment_id": fmt.Sprintf("gopay_%d", order.ID), "redirect_url": fmt.Sprintf("https://gate.gopay.cz/gw/v3/payment/%d", order.ID), }, nil }