Files
MyClub/internal/controllers/ticket_checkout_controller.go
T
Tomas Dvorak dfc079288f hot fix #1
2026-01-26 08:13:18 +01:00

283 lines
8.4 KiB
Go

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
}