something
This commit is contained in:
179
api/handlers/checkout.go
Normal file
179
api/handlers/checkout.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/stripe/stripe-go/v78"
|
||||
"github.com/stripe/stripe-go/v78/charge"
|
||||
"github.com/stripe/stripe-go/v78/customer"
|
||||
|
||||
"terminalShop/pkg/database"
|
||||
"terminalShop/pkg/models"
|
||||
"terminalShop/pkg/utils"
|
||||
)
|
||||
|
||||
// CheckoutHandler handles checkout and order creation.
|
||||
type CheckoutHandler struct {
|
||||
stripeKey string
|
||||
}
|
||||
|
||||
// NewCheckoutHandler creates a new checkout handler with the Stripe secret key.
|
||||
func NewCheckoutHandler(stripeSecretKey string) *CheckoutHandler {
|
||||
return &CheckoutHandler{stripeKey: stripeSecretKey}
|
||||
}
|
||||
|
||||
type CheckoutCartItem struct {
|
||||
CoffeeID uint `json:"coffee_id"`
|
||||
Quantity int `json:"quantity"`
|
||||
}
|
||||
|
||||
type CheckoutRequest struct {
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
StripeToken string `json:"stripe_token"`
|
||||
Last4 string `json:"last4"`
|
||||
Brand string `json:"brand"`
|
||||
ExpMonth int `json:"exp_month"`
|
||||
ExpYear int `json:"exp_year"`
|
||||
Items []CheckoutCartItem `json:"items"`
|
||||
ShippingName string `json:"shipping_name"`
|
||||
ShippingStreet string `json:"shipping_street"`
|
||||
ShippingCity string `json:"shipping_city"`
|
||||
ShippingState string `json:"shipping_state"`
|
||||
ShippingZip string `json:"shipping_zip"`
|
||||
}
|
||||
|
||||
func (h *CheckoutHandler) Checkout(w http.ResponseWriter, r *http.Request) {
|
||||
db := database.GetDB()
|
||||
|
||||
var req CheckoutRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
utils.RespondError(w, http.StatusBadRequest, "INVALID_JSON", "invalid request body", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Fingerprint == "" {
|
||||
utils.RespondError(w, http.StatusBadRequest, "VALIDATION_ERROR", "fingerprint is required", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if req.StripeToken == "" {
|
||||
utils.RespondError(w, http.StatusBadRequest, "VALIDATION_ERROR", "stripe token is required", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Items) <= 0 {
|
||||
utils.RespondError(w, http.StatusBadRequest, "VALIDATION_ERROR", "cart is empty", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := db.Where("ssh_key_fingerprint = ?", req.Fingerprint).First(&user).Error; err != nil {
|
||||
utils.RespondError(w, http.StatusNotFound, "USER_NOT_FOUND", "user not found", nil)
|
||||
return
|
||||
}
|
||||
|
||||
stripe.Key = h.stripeKey
|
||||
|
||||
if user.StripeCustomerID == "" {
|
||||
params := &stripe.CustomerParams{
|
||||
Description: stripe.String(fmt.Sprintf("terminal.shop user %d", user.ID)),
|
||||
}
|
||||
|
||||
if user.Email != "" {
|
||||
params.Email = stripe.String(user.Email)
|
||||
}
|
||||
if user.Name != "" {
|
||||
params.Name = stripe.String(user.Name)
|
||||
}
|
||||
cust, err := customer.New(params)
|
||||
if err != nil {
|
||||
utils.RespondError(w, http.StatusInternalServerError, "STRIPE_ERROR", "failed to create stripe customer", nil)
|
||||
return
|
||||
}
|
||||
user.StripeCustomerID = cust.ID
|
||||
db.Save(&user)
|
||||
}
|
||||
|
||||
var subtotal int
|
||||
var orderItems []models.OrderItem
|
||||
|
||||
for _, item := range req.Items {
|
||||
var coffee models.Coffee
|
||||
if err := db.First(&coffee, item.CoffeeID).Error; err != nil {
|
||||
utils.RespondError(w, http.StatusBadRequest, "INVALID_PRODUCT", fmt.Sprintf("product %d not found", item.CoffeeID), nil)
|
||||
return
|
||||
}
|
||||
|
||||
priceInCents := int(coffee.Price * 100)
|
||||
lineTotal := priceInCents * item.Quantity
|
||||
subtotal += lineTotal
|
||||
|
||||
orderItems = append(orderItems, models.OrderItem{
|
||||
CoffeeID: coffee.ID,
|
||||
Name: coffee.Name,
|
||||
Price: priceInCents,
|
||||
Quantity: item.Quantity,
|
||||
})
|
||||
}
|
||||
|
||||
total := subtotal
|
||||
|
||||
chargeParams := &stripe.ChargeParams{
|
||||
Amount: stripe.Int64(int64(total)),
|
||||
Currency: stripe.String(string(stripe.CurrencyUSD)),
|
||||
Customer: stripe.String(user.StripeCustomerID),
|
||||
Description: stripe.String(fmt.Sprintf("terminal.shop order for user %d", user.ID)),
|
||||
}
|
||||
chargeParams.SetSource(req.StripeToken)
|
||||
|
||||
// Stripe minimum charge is $0.50 (50 cents)
|
||||
if total < 50 {
|
||||
utils.RespondError(w, http.StatusBadRequest, "VALIDATION_ERROR", "order total must be at least $0.50", nil)
|
||||
return
|
||||
}
|
||||
|
||||
ch, err := charge.New(chargeParams)
|
||||
if err != nil {
|
||||
utils.RespondError(w, http.StatusPaymentRequired, "PAYMENT_FAILED", fmt.Sprintf("payment failed: %v", err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
card := models.Card{
|
||||
UserID: user.ID,
|
||||
StripePaymentID: req.StripeToken,
|
||||
Last4: req.Last4,
|
||||
Brand: req.Brand,
|
||||
ExpMonth: req.ExpMonth,
|
||||
ExpYear: req.ExpYear,
|
||||
}
|
||||
|
||||
db.Create(&card)
|
||||
|
||||
order := models.Order{
|
||||
UserID: user.ID,
|
||||
CardID: card.ID,
|
||||
StripePaymentID: ch.ID,
|
||||
Status: models.OrderStatusPaid,
|
||||
Subtotal: subtotal,
|
||||
Total: total,
|
||||
ShippingName: req.ShippingName,
|
||||
ShippingStreet: req.ShippingStreet,
|
||||
ShippingCity: req.ShippingCity,
|
||||
ShippingState: req.ShippingState,
|
||||
ShippingZip: req.ShippingZip,
|
||||
Items: orderItems,
|
||||
}
|
||||
|
||||
if err := db.Create(&order).Error; err != nil {
|
||||
utils.RespondError(w, http.StatusInternalServerError, "DATABASE_ERROR", "failed to create order", nil)
|
||||
return
|
||||
}
|
||||
|
||||
db.Preload("Items").First(&order, order.ID)
|
||||
|
||||
utils.RespondSuccess(w, http.StatusCreated, map[string]interface{}{
|
||||
"order": order,
|
||||
})
|
||||
}
|
||||
40
api/handlers/orders.go
Normal file
40
api/handlers/orders.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"terminalShop/pkg/database"
|
||||
"terminalShop/pkg/models"
|
||||
"terminalShop/pkg/utils"
|
||||
)
|
||||
|
||||
|
||||
type OrderHandler struct {}
|
||||
|
||||
|
||||
func NewOrderHandler() *OrderHandler {
|
||||
return &OrderHandler{}
|
||||
}
|
||||
|
||||
|
||||
func (h *OrderHandler) GetOrders(w http.ResponseWriter, r *http.Request) {
|
||||
db := database.GetDB()
|
||||
|
||||
fingerprint := r.URL.Query().Get("fingerprint")
|
||||
if fingerprint == "" {
|
||||
utils.RespondError(w, http.StatusBadRequest, "MISSING_FINGERPRINT", "fingerprint query parameter is required", nil)
|
||||
return
|
||||
}
|
||||
var user models.User
|
||||
if err := db.Where("ssh_key_fingerprint = ?", fingerprint).First(&user).Error; err != nil {
|
||||
utils.RespondError(w, http.StatusNotFound, "USER_NOT_FOUND", "user not found", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var orders []models.Order
|
||||
db.Where("user_id = ?", user.ID).Preload("Items").Order("created_at desc").Find(&orders)
|
||||
|
||||
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
|
||||
"orders": orders,
|
||||
})
|
||||
}
|
||||
@@ -42,7 +42,7 @@ func main() {
|
||||
}
|
||||
|
||||
// Setup routes
|
||||
router := routes.SetupRoutes(version)
|
||||
router := routes.SetupRoutes(version, cfg.StripeSecretKey)
|
||||
|
||||
// Create HTTP server
|
||||
server := &http.Server{
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// SetupRoutes configures all API routes
|
||||
func SetupRoutes(version string) *chi.Mux {
|
||||
func SetupRoutes(version string, stripeSecretKey string) *chi.Mux {
|
||||
r := chi.NewRouter()
|
||||
|
||||
// Apply global middleware
|
||||
@@ -20,6 +20,8 @@ func SetupRoutes(version string) *chi.Mux {
|
||||
healthHandler := handlers.NewHealthHandler(version)
|
||||
productHandler := handlers.NewProductHandler()
|
||||
authHandler := handlers.NewAuthHandler()
|
||||
checkoutHandler := handlers.NewCheckoutHandler(stripeSecretKey)
|
||||
orderHandler := handlers.NewOrderHandler()
|
||||
|
||||
// API v1 routes
|
||||
r.Route("/api/v1", func(r chi.Router) {
|
||||
@@ -33,12 +35,17 @@ func SetupRoutes(version string) *chi.Mux {
|
||||
|
||||
// Product endpoints (full CRUD)
|
||||
r.Route("/products", func(r chi.Router) {
|
||||
r.Get("/", productHandler.GetProducts) // GET /api/v1/products
|
||||
r.Post("/", productHandler.CreateProduct) // POST /api/v1/products
|
||||
r.Get("/{id}", productHandler.GetProduct) // GET /api/v1/products/:id
|
||||
r.Put("/{id}", productHandler.UpdateProduct) // PUT /api/v1/products/:id
|
||||
r.Get("/", productHandler.GetProducts) // GET /api/v1/products
|
||||
r.Post("/", productHandler.CreateProduct) // POST /api/v1/products
|
||||
r.Get("/{id}", productHandler.GetProduct) // GET /api/v1/products/:id
|
||||
r.Put("/{id}", productHandler.UpdateProduct) // PUT /api/v1/products/:id
|
||||
r.Delete("/{id}", productHandler.DeleteProduct) // DELETE /api/v1/products/:id
|
||||
})
|
||||
// Checkout endpoint
|
||||
r.Post("/checkout", checkoutHandler.Checkout)
|
||||
|
||||
// Order endpoints
|
||||
r.Get("/orders", orderHandler.GetOrders)
|
||||
})
|
||||
|
||||
return r
|
||||
|
||||
Reference in New Issue
Block a user