From 6097b8c8632abfa5a44868c081004b85e824b9d1 Mon Sep 17 00:00:00 2001 From: marcusDenslow Date: Thu, 26 Feb 2026 16:32:48 +0100 Subject: [PATCH] rework: added shipping validation at address creation the last implementation just accepted any input for the shipping validation and then only validated the address on payment. this is now changed to validation upon creation upon finishing the shipping address form. when a user completes the shipping form, a client-side check is first made to just simply check if there are any empty fields, if this check passes it gets check server-side by shippo. If the address is not found it will return an address not found error, and the user will not be able to continue to the payment form. The invalid address will also not be saved to the database. if the check pass, we simply save the address to the database with an ID. this ID is then passe in when doing the payment, instead of validating at the payment phase. this implementation currently only uses Shippo, which i have come to realize does NOT support shipping in countries outside of the US (i should have read the friendly manual before using Shippo). This means that a huge rewrite is due. i will focus on adding address validation for Norway first since thats where i live. i plan on using Bring's api for this validation. i will still keep Shippo for US based address verification even though this project is focused fon nordic shipping - just for a rainy day if some poor soul decides to order from AMERICA --- api/handlers/addresses.go | 66 +++++++++++---- api/handlers/checkout.go | 74 ++++++++++------- api/main.go | 2 +- api/middleware/auth.go | 17 ++-- api/routes/routes.go | 4 +- main.go | 11 ++- pkg/api/client.go | 21 ++--- pkg/config/config.go | 14 +++- pkg/shippo/shippo.go | 13 ++- pkg/tui/menu_test.go | 2 - pkg/tui/model.go | 166 +++++++++++++++----------------------- pkg/tui/model_test.go | 2 - pkg/tui/shipping_form.go | 2 - terminal-shop-source | 2 +- 14 files changed, 211 insertions(+), 185 deletions(-) diff --git a/api/handlers/addresses.go b/api/handlers/addresses.go index 177bc93..d4ba6b4 100644 --- a/api/handlers/addresses.go +++ b/api/handlers/addresses.go @@ -2,6 +2,7 @@ package handlers import ( "encoding/json" + "log" "net/http" "strconv" @@ -10,14 +11,17 @@ import ( "terminalShop/api/middleware" "terminalShop/pkg/database" "terminalShop/pkg/models" + "terminalShop/pkg/shippo" "terminalShop/pkg/utils" ) -type AddressHandler struct{} +type AddressHandler struct { + shippoKey string +} -// Create a new AddressHandler -func NewAddressHandler() *AddressHandler { - return &AddressHandler{} +// NewAddressHandler creates a new AddressHandler with the Shippo API key +func NewAddressHandler(shippoAPIKey string) *AddressHandler { + return &AddressHandler{shippoKey: shippoAPIKey} } // Retrieve all saved addresses for a user @@ -60,16 +64,48 @@ func (h *AddressHandler) CreateAddress(w http.ResponseWriter, r *http.Request) { return } - address := models.Address{ - UserID: userID, - Name: req.Name, - Street: req.Street, - Street2: req.Street2, - City: req.City, - State: req.State, - Zip: req.Zip, - Country: req.Country, - Phone: req.Phone, + // Validate and normalize address via Shippo + var address models.Address + if h.shippoKey != "" { + client := shippo.NewClient(h.shippoKey) + validated, err := client.ValidateAddress(shippo.Address{ + Name: req.Name, + Street1: req.Street, + Street2: req.Street2, + City: req.City, + State: req.State, + Country: req.Country, + Zip: req.Zip, + Phone: req.Phone, + }) + if err != nil { + log.Printf("shippo address validation failed: %v", err) + utils.RespondError(w, http.StatusBadRequest, "INVALID_ADDRESS", "address is invalid", nil) + return + } + address = models.Address{ + UserID: userID, + Name: validated.Name, + Street: validated.Street1, + Street2: validated.Street2, + City: validated.City, + State: validated.State, + Zip: validated.Zip, + Country: validated.Country, + Phone: validated.Phone, + } + } else { + address = models.Address{ + UserID: userID, + Name: req.Name, + Street: req.Street, + Street2: req.Street2, + City: req.City, + State: req.State, + Zip: req.Zip, + Country: req.Country, + Phone: req.Phone, + } } if err := db.Create(&address).Error; err != nil { @@ -82,7 +118,7 @@ func (h *AddressHandler) CreateAddress(w http.ResponseWriter, r *http.Request) { }) } -// Delete a saved address +// DeleteAddress deltes a saved address func (h *AddressHandler) DeleteAddress(w http.ResponseWriter, r *http.Request) { db := database.GetDB() userID := middleware.UserIDFromContext(r.Context()) diff --git a/api/handlers/checkout.go b/api/handlers/checkout.go index 52ce8d1..8ee6f28 100644 --- a/api/handlers/checkout.go +++ b/api/handlers/checkout.go @@ -3,6 +3,8 @@ package handlers import ( "encoding/json" "fmt" + "log" + "math" "net/http" "github.com/stripe/stripe-go/v78" @@ -31,20 +33,13 @@ type CheckoutCartItem struct { } type CheckoutRequest struct { - 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"` - ShippingStreet2 string `json:"shipping_street2"` - ShippingCity string `json:"shipping_city"` - ShippingState string `json:"shipping_state"` - ShippingZip string `json:"shipping_zip"` - ShippingCountry string `json:"shipping_country"` - ShippingPhone string `json:"shipping_phone"` + 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"` + AddressID uint `json:"address_id"` } func (h *CheckoutHandler) Checkout(w http.ResponseWriter, r *http.Request) { @@ -67,6 +62,17 @@ func (h *CheckoutHandler) Checkout(w http.ResponseWriter, r *http.Request) { return } + if req.AddressID == 0 { + utils.RespondError(w, http.StatusBadRequest, "VALIDATION_ERROR", "address_id is required", nil) + return + } + + var address models.Address + if err := db.Where("id = ? AND user_id = ?", req.AddressID, userID).First(&address).Error; err != nil { + utils.RespondError(w, http.StatusBadRequest, "INVALID_ADDRESS", "address not found", nil) + return + } + var user models.User if err := db.First(&user, userID).Error; err != nil { utils.RespondError(w, http.StatusNotFound, "USER_NOT_FOUND", "user not found", nil) @@ -92,20 +98,27 @@ func (h *CheckoutHandler) Checkout(w http.ResponseWriter, r *http.Request) { return } user.StripeCustomerID = cust.ID - db.Save(&user) + if err := db.Save(&user).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "DATABASE_ERROR", "failed to save stripe customer", nil) + return + } } var subtotal int var orderItems []models.OrderItem for _, item := range req.Items { + if item.Quantity <= 0 { + utils.RespondError(w, http.StatusBadRequest, "VALIDATION_ERROR", "item quantity must be at least 1", nil) + return + } 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) + priceInCents := int(math.Round(coffee.Price * 100)) lineTotal := priceInCents * item.Quantity subtotal += lineTotal @@ -135,7 +148,8 @@ func (h *CheckoutHandler) Checkout(w http.ResponseWriter, r *http.Request) { ch, err := charge.New(chargeParams) if err != nil { - utils.RespondError(w, http.StatusPaymentRequired, "PAYMENT_FAILED", fmt.Sprintf("payment failed: %v", err), nil) + log.Printf("stripe charge failed for user %d: %v", user.ID, err) + utils.RespondError(w, http.StatusPaymentRequired, "PAYMENT_FAILED", "payment could not be processed", nil) return } @@ -148,7 +162,10 @@ func (h *CheckoutHandler) Checkout(w http.ResponseWriter, r *http.Request) { ExpYear: req.ExpYear, } - db.Create(&card) + if err := db.Create(&card).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "DATABASE_ERROR", "failed to save card", nil) + return + } order := models.Order{ UserID: user.ID, @@ -157,14 +174,14 @@ func (h *CheckoutHandler) Checkout(w http.ResponseWriter, r *http.Request) { Status: models.OrderStatusPaid, Subtotal: subtotal, Total: total, - ShippingName: req.ShippingName, - ShippingStreet: req.ShippingStreet, - ShippingStreet2: req.ShippingStreet2, - ShippingCity: req.ShippingCity, - ShippingState: req.ShippingState, - ShippingZip: req.ShippingZip, - ShippingCountry: req.ShippingCountry, - ShippingPhone: req.ShippingPhone, + ShippingName: address.Name, + ShippingStreet: address.Street, + ShippingStreet2: address.Street2, + ShippingCity: address.City, + ShippingState: address.State, + ShippingZip: address.Zip, + ShippingCountry: address.Country, + ShippingPhone: address.Phone, Items: orderItems, } @@ -173,7 +190,10 @@ func (h *CheckoutHandler) Checkout(w http.ResponseWriter, r *http.Request) { return } - db.Preload("Items").First(&order, order.ID) + if err := db.Preload("Items").First(&order, order.ID).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "DATABASE_ERROR", "failed to reload order", nil) + return + } utils.RespondSuccess(w, http.StatusCreated, map[string]interface{}{ "order": order, diff --git a/api/main.go b/api/main.go index 5c1d46e..7a27e2c 100644 --- a/api/main.go +++ b/api/main.go @@ -46,7 +46,7 @@ func main() { jwtManager := auth.NewJWTManager(cfg.JWTSecret, 30*time.Minute) // Setup routes - router := routes.SetupRoutes(version, cfg.StripeSecretKey, jwtManager, cfg.AuthFingerprintKey) + router := routes.SetupRoutes(version, cfg.StripeSecretKey, jwtManager, cfg.AuthFingerprintKey, cfg.ShippoAPIKey) // Create HTTP server server := &http.Server{ diff --git a/api/middleware/auth.go b/api/middleware/auth.go index e377a99..af2053c 100644 --- a/api/middleware/auth.go +++ b/api/middleware/auth.go @@ -14,15 +14,15 @@ type contextKey string const userIDKey contextKey = "userID" func Auth(jwtManager *auth.JWTManager) func(http.Handler) http.Handler { - // Auth validates JWT tokens on all requests. - // if a valid token is present, store an userID in context - // if no token, continue as public (no userID). - // If invalid token, returns 401 + // Auth extracts and validates JWT tokens on all requests. + // If a valid token is present, store the userID in context. + // If no token or an invalid token, continue as public (no userID). + // Protected routes use RequireAuth to enforce valid authentication. return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") if authHeader == "" { - // No token - public req + // No token - public request next.ServeHTTP(w, r) return } @@ -30,13 +30,16 @@ func Auth(jwtManager *auth.JWTManager) func(http.Handler) http.Handler { // Expect "Bearer " parts := strings.SplitN(authHeader, " ", 2) if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") { - utils.RespondError(w, http.StatusUnauthorized, "INVALID_AUTH_BEARER", "authorization header must be Bearer ", nil) + // Malformed header - continue as public request + next.ServeHTTP(w, r) return } claims, err := jwtManager.ValidateToken(parts[1]) if err != nil { - utils.RespondError(w, http.StatusUnauthorized, "INVALID_TOKEN", "invalid or expired token", nil) + // Invalid or expired token - continue as public request. + // RequireAuth will reject on protected routes. + next.ServeHTTP(w, r) return } diff --git a/api/routes/routes.go b/api/routes/routes.go index bba5a88..cea864a 100644 --- a/api/routes/routes.go +++ b/api/routes/routes.go @@ -9,7 +9,7 @@ import ( ) // SetupRoutes configures all API routes -func SetupRoutes(version string, stripeSecretKey string, jwtManager *auth.JWTManager, authFingerprintKey string) *chi.Mux { +func SetupRoutes(version string, stripeSecretKey string, jwtManager *auth.JWTManager, authFingerprintKey string, shippoAPIKey string) *chi.Mux { r := chi.NewRouter() // Apply global middleware @@ -24,7 +24,7 @@ func SetupRoutes(version string, stripeSecretKey string, jwtManager *auth.JWTMan authHandler := handlers.NewAuthHandler(jwtManager, authFingerprintKey) checkoutHandler := handlers.NewCheckoutHandler(stripeSecretKey) orderHandler := handlers.NewOrderHandler() - addressHandler := handlers.NewAddressHandler() + addressHandler := handlers.NewAddressHandler(shippoAPIKey) // API v1 routes r.Route("/api/v1", func(r chi.Router) { diff --git a/main.go b/main.go index 598e8ec..1add16b 100644 --- a/main.go +++ b/main.go @@ -27,7 +27,10 @@ const ( port = "23456" ) -var authService *auth.SSHAuthService +var ( + authService *auth.SSHAuthService + apiURL string +) func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) { // Extract SSH public key from session @@ -54,7 +57,7 @@ func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) { } // Create TUI model with user context (no registration screen needed!) - m := tui.NewModelWithAuth(user, false, s.PublicKey(), token) + m := tui.NewModelWithAuth(user, false, s.PublicKey(), token, apiURL) return m, []tea.ProgramOption{tea.WithAltScreen()} } @@ -84,6 +87,10 @@ func main() { // Initialize SSH auth service authService = auth.NewSSHAuthService(db, jwtManager) + // Set API URL from config for TUI connections + apiURL = cfg.APIURL + log.Printf("TUI will connect to API at: %s", apiURL) + // Create SSH server s, err := wish.NewServer( wish.WithAddress(fmt.Sprintf("%s:%s", host, port)), diff --git a/pkg/api/client.go b/pkg/api/client.go index 3b54c40..da98e38 100644 --- a/pkg/api/client.go +++ b/pkg/api/client.go @@ -169,20 +169,13 @@ func (c *Client) RegisterUser(username, sshPublicKey, sshKeyFingerprint string) // CheckoutRequest represents the checkout payload sent to the API type CheckoutRequest struct { - 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"` - ShippingStreet2 string `json:"shipping_street2,omitempty"` - ShippingCountry string `json:"shipping_country"` - ShippingCity string `json:"shipping_city"` - ShippingState string `json:"shipping_state"` - ShippingZip string `json:"shipping_zip"` - ShippingPhone string `json:"shipping_phone,omitempty"` + 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"` + AddressID uint `json:"address_id"` } type CheckoutCartItem struct { diff --git a/pkg/config/config.go b/pkg/config/config.go index 0fdf987..628b4c5 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -10,6 +10,7 @@ import ( // Config holds all application configuration type Config struct { APIPort string + APIURL string SSHPort string Environment string DatabaseURL string @@ -22,16 +23,21 @@ type Config struct { // Load reads configuration from environment variables func Load() (*Config, error) { - // Load .env file in non-production environments + // Load .env file in non-production environments. + // Try current directory first, then parent directory (for when + // running from subdirectories like api/). if os.Getenv("ENVIRONMENT") != "production" { if err := godotenv.Load(); err != nil { - // .env is optional in production - fmt.Println("No .env file found, using environment variables") + if err := godotenv.Load("../.env"); err != nil { + fmt.Println("No .env file found, using environment variables") + } } } + apiPort := getEnvOrDefault("API_PORT", "8080") cfg := &Config{ - APIPort: getEnvOrDefault("API_PORT", "8080"), + APIPort: apiPort, + APIURL: getEnvOrDefault("API_URL", fmt.Sprintf("http://localhost:%s", apiPort)), SSHPort: getEnvOrDefault("SSH_PORT", "23457"), Environment: getEnvOrDefault("ENVIRONMENT", "development"), DatabaseURL: getEnvOrDefault("DATABASE_URL", "terminalshop.db"), diff --git a/pkg/shippo/shippo.go b/pkg/shippo/shippo.go index 6c7562e..ab6bab7 100644 --- a/pkg/shippo/shippo.go +++ b/pkg/shippo/shippo.go @@ -53,8 +53,11 @@ type addressRequest struct { } // validationResult is Shippo's validation response nested under validation_results. +// IsValid is a pointer to distinguish between absent (nil) and explicitly false. +// Shippo returns empty validation_results for international addresses, which +// means "no data to validate" rather than "invalid". type validationResult struct { - IsValid bool `json:"is_valid"` + IsValid *bool `json:"is_valid"` } // addressResponse is the response from Shippo's address endpoint. @@ -118,8 +121,12 @@ func (c *Client) ValidateAddress(addr Address) (*Address, error) { return nil, fmt.Errorf("failed to parse response: %w", err) } - // Accept if test mode or validation says valid - if !result.Test && (result.ValidationResults == nil || !result.ValidationResults.IsValid) { + // TODO: Re-add !result.Test bypass before production if needed. + // Removed test mode bypass so we can verify Shippo validation in development. + // + // Shippo returns empty validation_results (no is_valid field) for international + // addresses. We only reject when is_valid is explicitly false. + if result.ValidationResults != nil && result.ValidationResults.IsValid != nil && !*result.ValidationResults.IsValid { return nil, fmt.Errorf("address is invalid") } diff --git a/pkg/tui/menu_test.go b/pkg/tui/menu_test.go index 0cce043..e5aaa3d 100644 --- a/pkg/tui/menu_test.go +++ b/pkg/tui/menu_test.go @@ -44,7 +44,6 @@ func TestModel_BuildMenuView(t *testing.T) { ShippingView int AddressCursor int StripeKey string - ShippoKey string Orders []models.Order OrdersLoaded bool } @@ -92,7 +91,6 @@ func TestModel_BuildMenuView(t *testing.T) { ShippingView: tt.fields.ShippingView, AddressCursor: tt.fields.AddressCursor, StripeKey: tt.fields.StripeKey, - ShippoKey: tt.fields.ShippoKey, Orders: tt.fields.Orders, OrdersLoaded: tt.fields.OrdersLoaded, } diff --git a/pkg/tui/model.go b/pkg/tui/model.go index c6a4baf..64c9001 100644 --- a/pkg/tui/model.go +++ b/pkg/tui/model.go @@ -3,6 +3,7 @@ package tui import ( "encoding/json" "fmt" + "log" "math" "net/http" "os" @@ -14,7 +15,6 @@ import ( "terminalShop/pkg/api" "terminalShop/pkg/models" - "terminalShop/pkg/shippo" ) // termSize represents the terminal size category for responsive layout. @@ -87,7 +87,6 @@ type Model struct { ShippingView int AddressCursor int StripeKey string // Stripe publishable key for client-side tokenization - ShippoKey string // Shippo API key for address validation // Order history Orders []models.Order @@ -131,10 +130,17 @@ type ProductsMsg struct { // fetchProductsCmd fetches products from the API func (m Model) fetchProductsCmd() tea.Msg { if m.APIClient == nil { + log.Println("[TUI] APIClient is nil, using fallback products") return ProductsMsg{Err: nil} // No API client, use fallback } + log.Printf("[TUI] Fetching products from API at %s", m.APIClient.BaseURL) products, err := m.APIClient.GetProducts() + if err != nil { + log.Printf("[TUI] Failed to fetch products: %v", err) + } else { + log.Printf("[TUI] Loaded %d products from API", len(products)) + } return ProductsMsg{Products: products, Err: err} } @@ -204,17 +210,17 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.ViewingCart && m.CheckoutStep == 1 { switch msg := msg.(type) { case ShippingFormCompleteMsg: - return m, m.validateAddress(msg) - - case ShippoValidatedMsg: - m.ShippingInfo = &msg.Address - m.ShippingForm = nil - m.CheckoutStep = 2 - m.PaymentForm = m.InitPaymentForm() - if msg.SaveAddress { - return m, tea.Batch(m.PaymentForm.form.Init(), m.saveAddressCmd(msg.Address)) + addr := models.Address{ + Name: msg.Name, + Street: msg.Street1, + Street2: msg.Street2, + City: msg.City, + State: msg.State, + Country: msg.Country, + Zip: msg.Zip, + Phone: msg.Phone, } - return m, m.PaymentForm.form.Init() + return m, m.saveAddressCmd(addr) case AddressesMsg: if msg.Err != nil || len(msg.Addresses) == 0 { @@ -229,15 +235,21 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.ShippingForm = nil return m, nil - case ShippoValidationErrMsg: - if m.ShippingForm != nil { - m.ShippingForm.submitting = false - m.ShippingForm.form = m.buildShippingForm(m.ShippingForm) + case AddressSavedMsg: + if msg.Err != nil { + if m.ShippingForm != nil { + m.ShippingForm.submitting = false + m.ShippingForm.form = m.buildShippingForm(m.ShippingForm) + } m.ErrorMsg = fmt.Sprintf("Invalid address: %v", msg.Err) return m, m.ShippingForm.form.Init() } - m.ErrorMsg = fmt.Sprintf("Invalid address: %v", msg.Err) - return m, nil + m.ShippingInfo = &msg.Address + m.ShippingForm = nil + m.CheckoutStep = 2 + m.PaymentForm = m.InitPaymentForm() + return m, m.PaymentForm.form.Init() + case ShippingFormErrorMsg: m.ErrorMsg = msg.Message return m, nil @@ -388,22 +400,36 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case ShippingFormCompleteMsg: - return m, m.validateAddress(msg) + addr := models.Address{ + Name: msg.Name, + Street: msg.Street1, + Street2: msg.Street2, + City: msg.City, + State: msg.State, + Country: msg.Country, + Zip: msg.Zip, + Phone: msg.Phone, + } + return m, m.saveAddressCmd(addr) - case ShippoValidatedMsg: + case AddressSavedMsg: + if msg.Err != nil { + if m.ShippingForm != nil { + m.ShippingForm.submitting = false + m.ShippingForm.form = m.buildShippingForm(m.ShippingForm) + } + m.ErrorMsg = fmt.Sprintf("Invalid address: %v", msg.Err) + if m.ShippingForm != nil { + return m, m.ShippingForm.form.Init() + } + return m, nil + } m.ShippingInfo = &msg.Address m.ShippingForm = nil m.CheckoutStep = 2 m.PaymentForm = m.InitPaymentForm() - if msg.SaveAddress { - return m, tea.Batch(m.PaymentForm.form.Init(), m.saveAddressCmd(msg.Address)) - } return m, m.PaymentForm.form.Init() - case ShippoValidationErrMsg: - m.ErrorMsg = fmt.Sprintf("invalid address: %v", msg.Err) - return m, nil - case ShippingFormErrorMsg: m.ErrorMsg = msg.Message return m, nil @@ -661,68 +687,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -// ShippoValidatedMsg is sent when Shippo address validation succeeds -type ShippoValidatedMsg struct { - Address models.Address - SaveAddress bool -} - -// ShippoValidationErrMsg is sent when Shippo address validation fails -type ShippoValidationErrMsg struct { - Err error -} - -// validateAddress sends the shipping address to Shippo for validation -func (m Model) validateAddress(info ShippingFormCompleteMsg) tea.Cmd { - return func() tea.Msg { - if m.ShippoKey == "" { - // No Shippo key configured, skip validation - return ShippoValidatedMsg{ - Address: models.Address{ - Name: info.Name, - Street: info.Street1, - Street2: info.Street2, - City: info.City, - State: info.State, - Country: info.Country, - Zip: info.Zip, - Phone: info.Phone, - }, - SaveAddress: info.SaveAddress, - } - } - - client := shippo.NewClient(m.ShippoKey) - validated, err := client.ValidateAddress(shippo.Address{ - Name: info.Name, - Street1: info.Street1, - Street2: info.Street2, - City: info.City, - State: info.State, - Country: info.Country, - Zip: info.Zip, - Phone: info.Phone, - }) - if err != nil { - return ShippoValidationErrMsg{Err: err} - } - - return ShippoValidatedMsg{ - Address: models.Address{ - Name: validated.Name, - Street: validated.Street1, - Street2: validated.Street2, - City: validated.City, - State: validated.State, - Country: validated.Country, - Zip: validated.Zip, - Phone: validated.Phone, - }, - SaveAddress: info.SaveAddress, - } - } -} - type StripeTokenMsg struct { TokenID string Last4 string @@ -880,17 +844,10 @@ func (m Model) submitCheckout(tok StripeTokenMsg) tea.Cmd { } req := api.CheckoutRequest{ - StripeToken: tok.TokenID, - Last4: tok.Last4, - Items: cartItems, - ShippingName: m.ShippingInfo.Name, - ShippingStreet: m.ShippingInfo.Street, - ShippingStreet2: m.ShippingInfo.Street2, - ShippingCity: m.ShippingInfo.City, - ShippingState: m.ShippingInfo.State, - ShippingZip: m.ShippingInfo.Zip, - ShippingCountry: m.ShippingInfo.Country, - ShippingPhone: m.ShippingInfo.Phone, + StripeToken: tok.TokenID, + Last4: tok.Last4, + Items: cartItems, + AddressID: m.ShippingInfo.ID, } order, err := m.APIClient.Checkout(req) @@ -999,14 +956,13 @@ func NewModel(username string) Model { Loading: true, APIClient: api.NewClient("http://localhost:8080", ""), StripeKey: os.Getenv("STRIPE_PUBLIC_KEY"), - ShippoKey: os.Getenv("SHIPPO_API_KEY"), } m.updateLayout(120, 30) return m } // NewModelWithAuth creates a new model with user authentication context -func NewModelWithAuth(user *models.User, isNewUser bool, pubKey gossh.PublicKey, token string) Model { +func NewModelWithAuth(user *models.User, isNewUser bool, pubKey gossh.PublicKey, token string, apiURL string) Model { username := "guest" if user != nil { username = user.Name @@ -1017,7 +973,11 @@ func NewModelWithAuth(user *models.User, isNewUser bool, pubKey gossh.PublicKey, m.IsNewUser = isNewUser m.SSHPublicKey = pubKey m.AccessToken = token - m.APIClient = api.NewClient("http://localhost:8080", token) + if apiURL != "" { + m.APIClient = api.NewClient(apiURL, token) + } else { + m.APIClient = api.NewClient("http://localhost:8080", token) + } return m } diff --git a/pkg/tui/model_test.go b/pkg/tui/model_test.go index 5271428..9ca6c8f 100644 --- a/pkg/tui/model_test.go +++ b/pkg/tui/model_test.go @@ -44,7 +44,6 @@ func TestModel_updateLayout(t *testing.T) { ShippingView int AddressCursor int StripeKey string - ShippoKey string Orders []models.Order OrdersLoaded bool } @@ -96,7 +95,6 @@ func TestModel_updateLayout(t *testing.T) { ShippingView: tt.fields.ShippingView, AddressCursor: tt.fields.AddressCursor, StripeKey: tt.fields.StripeKey, - ShippoKey: tt.fields.ShippoKey, Orders: tt.fields.Orders, OrdersLoaded: tt.fields.OrdersLoaded, } diff --git a/pkg/tui/shipping_form.go b/pkg/tui/shipping_form.go index 97edbdd..171e3cc 100644 --- a/pkg/tui/shipping_form.go +++ b/pkg/tui/shipping_form.go @@ -57,7 +57,6 @@ type ShippingFormCompleteMsg struct { Country string Zip string Phone string - SaveAddress bool } // ShippingFormErrorMsg is sent when form validation fails at submission time @@ -227,7 +226,6 @@ func (m Model) UpdateShippingForm(msg tea.Msg, state *ShippingFormState) tea.Cmd Country: state.Country, Zip: state.Zip, Phone: phone, - SaveAddress: true, } } } diff --git a/terminal-shop-source b/terminal-shop-source index caa24e7..697730e 160000 --- a/terminal-shop-source +++ b/terminal-shop-source @@ -1 +1 @@ -Subproject commit caa24e7ffaaf6fa49289ebaf08a1b8c116fd6b0c +Subproject commit 697730e73cba0ef0bab943bbd75e5bcd64f5f5d3