Compare commits

...

3 Commits

Author SHA1 Message Date
f9c9b7dff5 chore(repo): change all instances of interface{} to any as of newer go standards
Some checks failed
CI / check (pull_request) Has been cancelled
2026-06-06 13:53:50 +02:00
135f1361e4 fix(products): update pricing on products (#34)
* fix(products): update pricing on products

* fix(test): pin coffee price in cart-limit fixture

TestConvertCartLimit's quantities and expected totals were keyed to
the original 500-cent latte price. After the product price bump in
1ce63fb, coffee_id=4 became 2500 cents, so under_cap/at_cap subtests
overshot the $200 cap and over_cap's expected total magnitude was
wrong by 5x. Override coffee_id=4's price to 500 in test setup so the
cap math stays stable regardless of seed changes.
2026-06-05 23:49:03 +02:00
64deba7e03 feat(sca): wire retry-auth keybind on authfailed review screen (#33) 2026-06-05 22:52:54 +02:00
18 changed files with 604 additions and 99 deletions

View File

@@ -42,7 +42,7 @@ func (h *AddressHandler) GetAddresses(w http.ResponseWriter, r *http.Request) {
var addresses []models.Address
db.Where("user_id = ?", userID).Order("is_default DESC, created_at DESC").Find(&addresses)
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
utils.RespondSuccess(w, http.StatusOK, map[string]any{
"addresses": addresses,
})
}
@@ -154,7 +154,7 @@ func (h *AddressHandler) CreateAddress(w http.ResponseWriter, r *http.Request) {
return
}
utils.RespondSuccess(w, http.StatusCreated, map[string]interface{}{
utils.RespondSuccess(w, http.StatusCreated, map[string]any{
"address": address,
})
}
@@ -183,7 +183,7 @@ func (h *AddressHandler) DeleteAddress(w http.ResponseWriter, r *http.Request) {
return
}
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
utils.RespondSuccess(w, http.StatusOK, map[string]any{
"message": "address successfully deleted",
})
}
@@ -218,7 +218,7 @@ func (h *AddressHandler) SetDefaultAddress(w http.ResponseWriter, r *http.Reques
return
}
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
utils.RespondSuccess(w, http.StatusOK, map[string]any{
"address": address,
})
}

View File

@@ -58,7 +58,7 @@ func TestGetAddressesEmpty(t *testing.T) {
var resp struct {
Success bool `json:"success"`
Data struct {
Addresses []interface{} `json:"addresses"`
Addresses []any `json:"addresses"`
} `json:"data"`
}
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {

View File

@@ -108,7 +108,7 @@ func (h *AuthHandler) GetToken(w http.ResponseWriter, r *http.Request) {
}
middleware.RecordAuthAttempt("success")
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
utils.RespondSuccess(w, http.StatusOK, map[string]any{
"access_token": token,
"token_type": "Bearer",
"expires_in": 1800, // 30 minutes in seconds
@@ -146,7 +146,7 @@ func (h *AuthHandler) RegisterWithSSHKey(w http.ResponseWriter, r *http.Request)
}
if len(errors) > 0 {
utils.RespondError(w, http.StatusBadRequest, "VALIDATION_ERROR", "validation failed", map[string]interface{}{
utils.RespondError(w, http.StatusBadRequest, "VALIDATION_ERROR", "validation failed", map[string]any{
"errors": errors,
})
return
@@ -173,7 +173,7 @@ func (h *AuthHandler) RegisterWithSSHKey(w http.ResponseWriter, r *http.Request)
return
}
utils.RespondSuccess(w, http.StatusCreated, map[string]interface{}{
utils.RespondSuccess(w, http.StatusCreated, map[string]any{
"user": user.ToPublic(),
})
}
@@ -195,7 +195,7 @@ func (h *AuthHandler) GetUserBySSHKey(w http.ResponseWriter, r *http.Request) {
return
}
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
utils.RespondSuccess(w, http.StatusOK, map[string]any{
"user": user.ToPublic(),
})
}

View File

@@ -89,12 +89,12 @@ func (h *CardHandler) GetCards(w http.ResponseWriter, r *http.Request) {
userID, time.Now(),
).Order("is_default DESC, created_at DESC").Find(&cards)
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
utils.RespondSuccess(w, http.StatusOK, map[string]any{
"cards": cards,
})
}
// func utils.RespondError(w http.ResponseWriter, statusCode int, code string, message string, details map[string]interface{})
// func utils.RespondError(w http.ResponseWriter, statusCode int, code string, message string, details map[string]any)
// GetCard returns a single saved card by ID.
func (h *CardHandler) GetCard(w http.ResponseWriter, r *http.Request) {
@@ -114,7 +114,7 @@ func (h *CardHandler) GetCard(w http.ResponseWriter, r *http.Request) {
return
}
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
utils.RespondSuccess(w, http.StatusOK, map[string]any{
"card": card,
})
}
@@ -156,7 +156,7 @@ func (h *CardHandler) SetDefaultCard(w http.ResponseWriter, r *http.Request) {
return
}
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
utils.RespondSuccess(w, http.StatusOK, map[string]any{
"card": card,
})
}
@@ -197,7 +197,7 @@ func (h *CardHandler) CollectCard(w http.ResponseWriter, r *http.Request) {
utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to generate redirect token", nil)
return
}
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
utils.RespondSuccess(w, http.StatusOK, map[string]any{
"url": fmt.Sprintf("%s/pay/%s", h.appURL, token),
})
}
@@ -302,7 +302,7 @@ func (h *CardHandler) SaveCard(w http.ResponseWriter, r *http.Request) {
audit.CardAdded(userID, card.ID, card.Brand, card.Last4)
utils.RespondSuccess(w, http.StatusCreated, map[string]interface{}{
utils.RespondSuccess(w, http.StatusCreated, map[string]any{
"card": card,
})
}
@@ -347,7 +347,7 @@ func (h *CardHandler) DeleteCard(w http.ResponseWriter, r *http.Request) {
audit.CardDeleted(userID, card.ID)
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
utils.RespondSuccess(w, http.StatusOK, map[string]any{
"message": "card deleted",
})
}

View File

@@ -55,18 +55,18 @@ func getOrCreateCart(ctx context.Context, userID uint) (*models.Cart, error) {
return &cart, nil
}
func cartResponse(ctx context.Context, cart *models.Cart) map[string]interface{} {
func cartResponse(ctx context.Context, cart *models.Cart) map[string]any {
db := database.GetDB().WithContext(ctx)
var items []models.CartItem
db.Where("cart_id = ? AND quantity > 0", cart.ID).Preload("Coffee").Find(&items)
subtotal := 0
itemsData := make([]map[string]interface{}, 0, len(items))
itemsData := make([]map[string]any, 0, len(items))
for _, item := range items {
lineTotal := item.Coffee.Price * item.Quantity
subtotal += lineTotal
itemsData = append(itemsData, map[string]interface{}{
itemsData = append(itemsData, map[string]any{
"id": item.ID,
"coffee_id": item.CoffeeID,
"quantity": item.Quantity,
@@ -75,7 +75,7 @@ func cartResponse(ctx context.Context, cart *models.Cart) map[string]interface{}
})
}
return map[string]interface{}{
return map[string]any{
"items": itemsData,
"subtotal": subtotal,
"address_id": cart.AddressID,
@@ -94,7 +94,7 @@ func (h *CartHandler) GetCart(w http.ResponseWriter, r *http.Request) {
return
}
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
utils.RespondSuccess(w, http.StatusOK, map[string]any{
"cart": cartResponse(r.Context(), cart),
})
}
@@ -157,7 +157,7 @@ func (h *CartHandler) SetItem(w http.ResponseWriter, r *http.Request) {
}
}
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
utils.RespondSuccess(w, http.StatusOK, map[string]any{
"cart": cartResponse(r.Context(), cart),
})
}
@@ -193,7 +193,7 @@ func (h *CartHandler) SetAddress(w http.ResponseWriter, r *http.Request) {
return
}
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
utils.RespondSuccess(w, http.StatusOK, map[string]any{
"message": "address set",
})
}
@@ -238,7 +238,7 @@ func (h *CartHandler) SetCard(w http.ResponseWriter, r *http.Request) {
return
}
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
utils.RespondSuccess(w, http.StatusOK, map[string]any{
"message": "card set",
})
}
@@ -258,7 +258,7 @@ func (h *CartHandler) ClearCart(w http.ResponseWriter, r *http.Request) {
return
}
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
utils.RespondSuccess(w, http.StatusOK, map[string]any{
"message": "cart cleared",
})
}
@@ -386,7 +386,7 @@ func (h *CartHandler) ConvertCart(w http.ResponseWriter, r *http.Request) {
middleware.RecordCartConversion("validation_over_limit")
utils.RespondError(w, http.StatusBadRequest, "CART_OVER_LIMIT",
fmt.Sprintf("order total must be at most $%.2f", float64(h.maxOrderCents)/100),
map[string]interface{}{
map[string]any{
"total_cents": total,
"limit_cents": h.maxOrderCents,
})
@@ -507,7 +507,7 @@ func (h *CartHandler) ConvertCart(w http.ResponseWriter, r *http.Request) {
if err := tx.Where("cart_id = ?", cart.ID).Delete(&models.CartItem{}).Error; err != nil {
return err
}
return tx.Model(cart).Updates(map[string]interface{}{
return tx.Model(cart).Updates(map[string]any{
"address_id": nil,
"card_id": nil,
}).Error
@@ -534,7 +534,7 @@ func (h *CartHandler) ConvertCart(w http.ResponseWriter, r *http.Request) {
}
go notify.SlackOrderPaid(&order)
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
utils.RespondSuccess(w, http.StatusOK, map[string]any{
"order": order,
})
}

View File

@@ -83,8 +83,8 @@ func TestGetCartEmpty(t *testing.T) {
Success bool `json:"success"`
Data struct {
Cart struct {
Items []interface{} `json:"items"`
Subtotal int `json:"subtotal"`
Items []any `json:"items"`
Subtotal int `json:"subtotal"`
} `json:"cart"`
} `json:"data"`
}
@@ -110,7 +110,7 @@ func TestSetItemAndGetCart(t *testing.T) {
handler := NewCartHandler("", "", 0)
// Add item (CoffeeID=1, Quantity=2)
body, _ := json.Marshal(map[string]interface{}{
body, _ := json.Marshal(map[string]any{
"coffee_id": 1,
"quantity": 2,
})
@@ -166,13 +166,13 @@ func TestSetItemUpdateQuantity(t *testing.T) {
handler := NewCartHandler("", "", 0)
// Add item
body, _ := json.Marshal(map[string]interface{}{"coffee_id": 1, "quantity": 2})
body, _ := json.Marshal(map[string]any{"coffee_id": 1, "quantity": 2})
req := authRequest("PUT", "/api/v1/cart/item", body, user.ID)
w := httptest.NewRecorder()
handler.SetItem(w, req)
// Update quantity
body, _ = json.Marshal(map[string]interface{}{"coffee_id": 1, "quantity": 5})
body, _ = json.Marshal(map[string]any{"coffee_id": 1, "quantity": 5})
req = authRequest("PUT", "/api/v1/cart/item", body, user.ID)
w = httptest.NewRecorder()
handler.SetItem(w, req)
@@ -211,13 +211,13 @@ func TestSetItemRemove(t *testing.T) {
handler := NewCartHandler("", "", 0)
// Add item
body, _ := json.Marshal(map[string]interface{}{"coffee_id": 1, "quantity": 3})
body, _ := json.Marshal(map[string]any{"coffee_id": 1, "quantity": 3})
req := authRequest("PUT", "/api/v1/cart/item", body, user.ID)
w := httptest.NewRecorder()
handler.SetItem(w, req)
// Remove item (quantity 0)
body, _ = json.Marshal(map[string]interface{}{"coffee_id": 1, "quantity": 0})
body, _ = json.Marshal(map[string]any{"coffee_id": 1, "quantity": 0})
req = authRequest("PUT", "/api/v1/cart/item", body, user.ID)
w = httptest.NewRecorder()
handler.SetItem(w, req)
@@ -231,7 +231,7 @@ func TestSetItemRemove(t *testing.T) {
Success bool `json:"success"`
Data struct {
Cart struct {
Items []interface{} `json:"items"`
Items []any `json:"items"`
} `json:"cart"`
} `json:"data"`
}
@@ -249,7 +249,7 @@ func TestSetItemInvalidProduct(t *testing.T) {
handler := NewCartHandler("", "", 0)
body, _ := json.Marshal(map[string]interface{}{"coffee_id": 999, "quantity": 1})
body, _ := json.Marshal(map[string]any{"coffee_id": 999, "quantity": 1})
req := authRequest("PUT", "/api/v1/cart/item", body, user.ID)
w := httptest.NewRecorder()
handler.SetItem(w, req)
@@ -266,7 +266,7 @@ func TestSetItemMissingCoffeeID(t *testing.T) {
handler := NewCartHandler("", "", 0)
body, _ := json.Marshal(map[string]interface{}{"quantity": 1})
body, _ := json.Marshal(map[string]any{"quantity": 1})
req := authRequest("PUT", "/api/v1/cart/item", body, user.ID)
w := httptest.NewRecorder()
handler.SetItem(w, req)
@@ -285,7 +285,7 @@ func TestClearCart(t *testing.T) {
// Add two items
for _, coffeeID := range []uint{1, 2} {
body, _ := json.Marshal(map[string]interface{}{"coffee_id": coffeeID, "quantity": 1})
body, _ := json.Marshal(map[string]any{"coffee_id": coffeeID, "quantity": 1})
req := authRequest("PUT", "/api/v1/cart/item", body, user.ID)
w := httptest.NewRecorder()
handler.SetItem(w, req)
@@ -309,7 +309,7 @@ func TestClearCart(t *testing.T) {
Success bool `json:"success"`
Data struct {
Cart struct {
Items []interface{} `json:"items"`
Items []any `json:"items"`
} `json:"cart"`
} `json:"data"`
}
@@ -339,7 +339,7 @@ func TestSetAddress(t *testing.T) {
handler := NewCartHandler("", "", 0)
body, _ := json.Marshal(map[string]interface{}{"address_id": addr.ID})
body, _ := json.Marshal(map[string]any{"address_id": addr.ID})
req := authRequest("PUT", "/api/v1/cart/address", body, user.ID)
w := httptest.NewRecorder()
handler.SetAddress(w, req)
@@ -393,7 +393,7 @@ func TestSetAddressWrongUser(t *testing.T) {
handler := NewCartHandler("", "", 0)
body, _ := json.Marshal(map[string]interface{}{"address_id": addr.ID})
body, _ := json.Marshal(map[string]any{"address_id": addr.ID})
req := authRequest("PUT", "/api/v1/cart/address", body, user.ID)
w := httptest.NewRecorder()
handler.SetAddress(w, req)
@@ -421,7 +421,7 @@ func TestSetCard(t *testing.T) {
handler := NewCartHandler("", "", 0)
body, _ := json.Marshal(map[string]interface{}{"card_id": card.ID})
body, _ := json.Marshal(map[string]any{"card_id": card.ID})
req := authRequest("PUT", "/api/v1/cart/card", body, user.ID)
w := httptest.NewRecorder()
handler.SetCard(w, req)
@@ -473,7 +473,7 @@ func TestSetCardWrongUser(t *testing.T) {
handler := NewCartHandler("", "", 0)
body, _ := json.Marshal(map[string]interface{}{"card_id": card.ID})
body, _ := json.Marshal(map[string]any{"card_id": card.ID})
req := authRequest("PUT", "/api/v1/cart/card", body, user.ID)
w := httptest.NewRecorder()
handler.SetCard(w, req)
@@ -491,7 +491,7 @@ func TestConvertCartMissingAddress(t *testing.T) {
handler := NewCartHandler("", "", 0)
// Add item but no address or card
body, _ := json.Marshal(map[string]interface{}{"coffee_id": 1, "quantity": 1})
body, _ := json.Marshal(map[string]any{"coffee_id": 1, "quantity": 1})
req := authRequest("PUT", "/api/v1/cart/item", body, user.ID)
w := httptest.NewRecorder()
handler.SetItem(w, req)
@@ -515,14 +515,14 @@ func TestConvertCartMissingCard(t *testing.T) {
db := database.GetDB()
// Add item and address
body, _ := json.Marshal(map[string]interface{}{"coffee_id": 1, "quantity": 1})
body, _ := json.Marshal(map[string]any{"coffee_id": 1, "quantity": 1})
req := authRequest("PUT", "/api/v1/cart/item", body, user.ID)
w := httptest.NewRecorder()
handler.SetItem(w, req)
addr := models.Address{UserID: user.ID, Name: "Test", Street: "123 St", City: "PDX", State: "OR", Zip: "97201", Country: "US"}
db.Create(&addr)
body, _ = json.Marshal(map[string]interface{}{"address_id": addr.ID})
body, _ = json.Marshal(map[string]any{"address_id": addr.ID})
req = authRequest("PUT", "/api/v1/cart/address", body, user.ID)
w = httptest.NewRecorder()
handler.SetAddress(w, req)
@@ -563,7 +563,7 @@ func TestMultipleItems(t *testing.T) {
// Add three different items
for i := uint(1); i <= 3; i++ {
body, _ := json.Marshal(map[string]interface{}{"coffee_id": i, "quantity": int(i)})
body, _ := json.Marshal(map[string]any{"coffee_id": i, "quantity": int(i)})
req := authRequest("PUT", "/api/v1/cart/item", body, user.ID)
w := httptest.NewRecorder()
handler.SetItem(w, req)
@@ -581,8 +581,8 @@ func TestMultipleItems(t *testing.T) {
Success bool `json:"success"`
Data struct {
Cart struct {
Items []interface{} `json:"items"`
Subtotal int `json:"subtotal"`
Items []any `json:"items"`
Subtotal int `json:"subtotal"`
} `json:"cart"`
} `json:"data"`
}
@@ -614,13 +614,13 @@ func TestCartIsolation(t *testing.T) {
r.Get("/cart", handler.GetCart)
// User 1 adds item
body, _ := json.Marshal(map[string]interface{}{"coffee_id": 1, "quantity": 5})
body, _ := json.Marshal(map[string]any{"coffee_id": 1, "quantity": 5})
req := authRequest("PUT", "/cart/item", body, user1.ID)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
// User 2 adds different item
body, _ = json.Marshal(map[string]interface{}{"coffee_id": 2, "quantity": 3})
body, _ = json.Marshal(map[string]any{"coffee_id": 2, "quantity": 3})
req = authRequest("PUT", "/cart/item", body, user2.ID)
w = httptest.NewRecorder()
r.ServeHTTP(w, req)
@@ -709,9 +709,17 @@ func TestConvertCartLimit(t *testing.T) {
t.Fatalf("seed card: %v", err)
}
// Pin coffee_id=4's price so cart-cap math stays stable across
// seed-price changes. The cap test is about (price * quantity)
// vs cap, not which coffee or what the menu currently charges.
const pinnedPrice = 500
if err := db.Model(&models.Coffee{}).Where("id = ?", 4).Update("price", pinnedPrice).Error; err != nil {
t.Fatalf("pin coffee price: %v", err)
}
handler := NewCartHandler("", "", tc.capCents)
body, _ := json.Marshal(map[string]interface{}{"coffee_id": 4, "quantity": tc.quantity})
body, _ := json.Marshal(map[string]any{"coffee_id": 4, "quantity": tc.quantity})
req := authRequest("PUT", "/api/v1/cart/item", body, user.ID)
w := httptest.NewRecorder()
handler.SetItem(w, req)
@@ -720,7 +728,7 @@ func TestConvertCartLimit(t *testing.T) {
}
// set address
body, _ = json.Marshal(map[string]interface{}{"address_id": addr.ID})
body, _ = json.Marshal(map[string]any{"address_id": addr.ID})
req = authRequest("PUT", "/api/v1/cart/address", body, user.ID)
w = httptest.NewRecorder()
handler.SetAddress(w, req)
@@ -729,7 +737,7 @@ func TestConvertCartLimit(t *testing.T) {
}
// set card
body, _ = json.Marshal(map[string]interface{}{"card_id": card.ID})
body, _ = json.Marshal(map[string]any{"card_id": card.ID})
req = authRequest("PUT", "/api/v1/cart/card", body, user.ID)
w = httptest.NewRecorder()
handler.SetCard(w, req)
@@ -745,9 +753,9 @@ func TestConvertCartLimit(t *testing.T) {
var resp struct {
Success bool `json:"success"`
Error struct {
Code string `json:"code"`
Message string `json:"message"`
Details map[string]interface{} `json:"details"`
Code string `json:"code"`
Message string `json:"message"`
Details map[string]any `json:"details"`
} `json:"error"`
}
_ = json.NewDecoder(w.Body).Decode(&resp)
@@ -760,8 +768,8 @@ func TestConvertCartLimit(t *testing.T) {
if got, _ := resp.Error.Details["limit_cents"].(float64); int(got) != tc.capCents {
t.Errorf("limit_cents in details: want %d, got %v", tc.capCents, resp.Error.Details["limit_cents"])
}
if got, _ := resp.Error.Details["total_cents"].(float64); int(got) != tc.quantity*500 {
t.Errorf("total_cents in details: want %d, got %v", tc.quantity*500, resp.Error.Details["total_cents"])
if got, _ := resp.Error.Details["total_cents"].(float64); int(got) != tc.quantity*pinnedPrice {
t.Errorf("total_cents in details: want %d, got %v", tc.quantity*pinnedPrice, resp.Error.Details["total_cents"])
}
}
})
@@ -787,7 +795,7 @@ func TestSetCard_ExpiredReturns410(t *testing.T) {
}
handler := NewCartHandler("", "", 0)
body, _ := json.Marshal(map[string]interface{}{"card_id": card.ID})
body, _ := json.Marshal(map[string]any{"card_id": card.ID})
req := authRequest("PUT", "/api/v1/cart/card", body, user.ID)
w := httptest.NewRecorder()
handler.SetCard(w, req)
@@ -824,7 +832,7 @@ func TestConvertCart_ExpiredReturns410(t *testing.T) {
db := database.GetDB()
body, _ := json.Marshal(map[string]interface{}{"coffee_id": 1, "quantity": 1})
body, _ := json.Marshal(map[string]any{"coffee_id": 1, "quantity": 1})
req := authRequest("PUT", "/api/v1/vart/item", body, user.ID)
w := httptest.NewRecorder()
NewCartHandler("", "", 0).SetItem(w, req)
@@ -834,7 +842,7 @@ func TestConvertCart_ExpiredReturns410(t *testing.T) {
City: "PDX", State: "OR", Zip: "97201", Country: "US",
}
db.Create(&addr)
body, _ = json.Marshal(map[string]interface{}{"address_id": addr.ID})
body, _ = json.Marshal(map[string]any{"address_id": addr.ID})
req = authRequest("PUT", "/api/v1/cart/address", body, user.ID)
w = httptest.NewRecorder()
NewCartHandler("", "", 0).SetAddress(w, req)

View File

@@ -221,7 +221,7 @@ func (h *OrderHandler) RefundOrder(w http.ResponseWriter, r *http.Request) {
audit.OrderRefunded(userID, order.ID, order.Total, rf.ID)
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
utils.RespondSuccess(w, http.StatusOK, map[string]any{
"refund_id": rf.ID,
"status": "refunded",
})
@@ -287,7 +287,7 @@ func (h *OrderHandler) UpdateTracking(w http.ResponseWriter, r *http.Request) {
audit.OrderShipped(order.UserID, order.ID, req.Carrier, req.TrackingNumber)
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
utils.RespondSuccess(w, http.StatusOK, map[string]any{
"order_id": order.ID,
"carrier": req.Carrier,
"tracking_number": req.TrackingNumber,

View File

@@ -30,7 +30,7 @@ func (h *ProductHandler) GetProducts(w http.ResponseWriter, r *http.Request) {
return
}
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
utils.RespondSuccess(w, http.StatusOK, map[string]any{
"products": products,
})
}
@@ -53,7 +53,7 @@ func (h *ProductHandler) GetProduct(w http.ResponseWriter, r *http.Request) {
return
}
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
utils.RespondSuccess(w, http.StatusOK, map[string]any{
"product": product,
})
}

View File

@@ -49,7 +49,7 @@ func ReconcileOrders(stripeKey string) {
"order_id", order.ID, "pi", pi.ID)
txErr := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&order).Updates(map[string]interface{}{
if err := tx.Model(&order).Updates(map[string]any{
"status": models.OrderStatusPaid,
"stripe_payment_id": pi.ID,
}).Error; err != nil {
@@ -62,7 +62,7 @@ func ReconcileOrders(stripeKey string) {
if err := tx.Where("cart_id = ?", cart.ID).Delete(&models.CartItem{}).Error; err != nil {
return err
}
return tx.Model(&cart).Updates(map[string]interface{}{
return tx.Model(&cart).Updates(map[string]any{
"address_id": nil,
"card_id": nil,
}).Error

View File

@@ -54,7 +54,7 @@ func (h *ViewHandler) GetViewInit(w http.ResponseWriter, r *http.Request) {
db.Where("cart_id = ? AND quantity > 0", cart.ID).Preload("Coffee").Find(&cartItems)
}
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
utils.RespondSuccess(w, http.StatusOK, map[string]any{
"user": user.ToPublic(),
"products": products,
"cart": cartItems,

View File

@@ -125,7 +125,7 @@ func (h *WebhookHandler) handlePaymentIntentSucceeded(ctx context.Context, event
}
txErr := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&order).Updates(map[string]interface{}{
if err := tx.Model(&order).Updates(map[string]any{
"status": models.OrderStatusPaid,
"stripe_payment_id": pi.ID,
}).Error; err != nil {
@@ -139,7 +139,7 @@ func (h *WebhookHandler) handlePaymentIntentSucceeded(ctx context.Context, event
if err := tx.Where("cart_id = ?", cart.ID).Delete(&models.CartItem{}).Error; err != nil {
return fmt.Errorf("failed to delete cart item: %w", err)
}
return tx.Model(&cart).Updates(map[string]interface{}{
return tx.Model(&cart).Updates(map[string]any{
"address_id": nil,
"card_id": nil,
}).Error

View File

@@ -104,16 +104,16 @@ func (c *Client) RefreshToken(fingerprint, clientSecret string) (string, error)
// APIResponse represents the standard API response format
type APIResponse struct {
Success bool `json:"success"`
Data map[string]interface{} `json:"data"`
Error *APIError `json:"error,omitempty"`
Success bool `json:"success"`
Data map[string]any `json:"data"`
Error *APIError `json:"error,omitempty"`
}
// APIError represents an error from the API
type APIError struct {
Code string `json:"code"`
Message string `json:"message"`
Details map[string]interface{} `json:"details,omitempty"`
Code string `json:"code"`
Message string `json:"message"`
Details map[string]any `json:"details,omitempty"`
}
// ProductsResponse represents the products API response
@@ -339,7 +339,7 @@ func (c *Client) GetCart() (*CartData, error) {
func (c *Client) SetCartItem(coffeeID uint, quantity int) (*CartData, error) {
url := fmt.Sprintf("%s/api/v1/cart/item", c.BaseURL)
body := map[string]interface{}{
body := map[string]any{
"coffee_id": coffeeID,
"quantity": quantity,
}
@@ -379,7 +379,7 @@ func (c *Client) SetCartItem(coffeeID uint, quantity int) (*CartData, error) {
func (c *Client) SetCartAddress(addressID uint) error {
url := fmt.Sprintf("%s/api/v1/cart/address", c.BaseURL)
body := map[string]interface{}{
body := map[string]any{
"address_id": addressID,
}
jsonData, err := json.Marshal(body)
@@ -418,7 +418,7 @@ func (c *Client) SetCartAddress(addressID uint) error {
func (c *Client) SetCartCard(cardID uint) error {
url := fmt.Sprintf("%s/api/v1/cart/card", c.BaseURL)
body := map[string]interface{}{
body := map[string]any{
"card_id": cardID,
}
jsonData, err := json.Marshal(body)

View File

@@ -53,7 +53,7 @@ func (m *JWTManager) GenerateToken(userID uint, email, username string) (string,
// ValidateToken validates a JWT token and returns the claims
func (m *JWTManager) ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (any, error) {
// Verify signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])

View File

@@ -4,8 +4,9 @@ import (
"fmt"
"log"
"gorm.io/gorm"
"terminalShop/pkg/models"
"gorm.io/gorm"
)
// Seed populates the database with initial data
@@ -27,7 +28,7 @@ func Seed(db *gorm.DB) error {
RoastType: "Dark Roast",
Ounces: 2,
BeanType: "Arabica",
Price: 350, // $3.50
Price: 1800, // $18.00
Color: "#8B4513",
Description: "Bold and intense coffee with a rich crema",
},
@@ -36,7 +37,7 @@ func Seed(db *gorm.DB) error {
RoastType: "Medium Roast",
Ounces: 8,
BeanType: "Arabica",
Price: 400, // $4.00
Price: 2200, // $22.00
Color: "#A0522D",
Description: "Smooth and balanced espresso diluted with hot water",
},
@@ -45,7 +46,7 @@ func Seed(db *gorm.DB) error {
RoastType: "Medium Roast",
Ounces: 6,
BeanType: "Arabica",
Price: 450, // $4.50
Price: 2200, // $22.00
Color: "#C4A574",
Description: "Espresso with steamed milk and a thick layer of foam",
},
@@ -54,7 +55,7 @@ func Seed(db *gorm.DB) error {
RoastType: "Medium Roast",
Ounces: 12,
BeanType: "Arabica",
Price: 500, // $5.00
Price: 2500, // $25.00
Color: "#D2B48C",
Description: "Creamy espresso with plenty of steamed milk and a touch of foam",
},
@@ -63,7 +64,7 @@ func Seed(db *gorm.DB) error {
RoastType: "Dark Roast",
Ounces: 12,
BeanType: "Arabica",
Price: 550, // $5.50
Price: 3000, // $30.00
Color: "#8B4513",
Description: "Chocolate and espresso combined with steamed milk. Sweet and indulgent.",
},
@@ -72,7 +73,7 @@ func Seed(db *gorm.DB) error {
RoastType: "Dark Roast",
Ounces: 3,
BeanType: "Robusta Blend",
Price: 425, // $4.25
Price: 1800, // $18.00
Color: "#DAA520",
Description: "Espresso 'marked' with a dollop of foamed milk. Small but mighty, like a tiny caffeinated warrior.",
},

View File

@@ -170,8 +170,20 @@ func (m Model) ReviewView() string {
if m.CheckingOut {
return m.theme.TextAccent().Bold(true).Padding(1).Render(" submitting order...")
}
if m.review.retrying {
return m.theme.TextAccent().Bold(true).Padding(1).Render(" retrying authentication...")
}
if m.review.retryExhausted {
content := m.theme.TextError().Bold(true).Padding(1).Render("too many authentication attempts") + "\n" + m.theme.TextLabel().Padding(0, 1).Render("press esc to start a new order")
return lipgloss.Place(
m.widthContainer,
lipgloss.Height(content),
lipgloss.Center, lipgloss.Center,
content,
)
}
if m.review.authFailed {
content := m.theme.TextError().Bold(true).Padding(1).Render("authentication failed — press esc to try again")
content := m.theme.TextError().Bold(true).Padding(1).Render("authentication failed — press r to retry, esc to go back")
return lipgloss.Place(
m.widthContainer,
lipgloss.Height(content),
@@ -208,7 +220,7 @@ func (m Model) renderRequiresActionView() string {
header := m.theme.TextAccent().Bold(true).Render("your bank wants to verify this charge")
if m.review.redirectURL == "" {
return base.Render(" waiting for authentication link...")
return base.Render(" verifying with your bank...")
}
qr, qrSize, err := qrfefe.Generate(m.review.redirectURL)
@@ -274,17 +286,65 @@ func (m Model) ReviewUpdate(msg tea.Msg) (Model, tea.Cmd) {
m.review.requiresAction = false
m.review.redirectURL = ""
m.review.authFailed = true
m.footer = []footerCommand{{key: "r", value: "retry"}, {key: "esc", value: "back"}}
return m, nil
default:
return m, m.schedulePollOrderStatus(msg.OrderID)
}
case RetryAuthResultMsg:
if m.review.orderID != 0 && msg.OrderID != m.review.orderID {
return m, nil
}
m.review.retrying = false
if msg.Err != nil {
s := msg.Err.Error()
switch {
case strings.Contains(s, "RETRY_LIMIT"):
m.review.authFailed = false
m.review.retryExhausted = true
m.footer = []footerCommand{{key: "esc", value: "back"}}
return m, nil
case strings.Contains(s, "CARD_NO_LONGER_AVAILABLE"):
m.review.authFailed = false
m.review.requiresAction = false
m.review.redirectURL = ""
m.review.orderID = 0
m = m.SwitchPage(paymentPage)
m.payment.view = 0
m.payment.form = nil
m.error = &VisibleError{message: "saved card is no longer available. add it again to try."}
return m, nil
default:
m.error = &VisibleError{message: friendlyCheckoutError(msg.Err)}
return m, nil
}
}
m.review.authFailed = false
m.review.retryExhausted = false
m.review.requiresAction = true
m.review.redirectURL = msg.RedirectURL
m.review.orderID = msg.OrderID
m.footer = []footerCommand{{key: "esc", value: "cancel"}}
return m, m.schedulePollOrderStatus(msg.OrderID)
case tea.KeyMsg:
if m.CheckingOut {
return m, nil
}
if m.review.retrying {
return m, nil
}
if m.review.authFailed {
if msg.String() == "esc" || msg.String() == "enter" {
switch msg.String() {
case "r":
if m.review.orderID == 0 {
return m, nil
}
m.review.retrying = true
m.error = nil
return m, m.retryAuthCmd(m.review.orderID)
case "esc":
m.review.authFailed = false
m = m.SwitchPage(paymentPage)
m.payment.view = 0
@@ -293,6 +353,17 @@ func (m Model) ReviewUpdate(msg tea.Msg) (Model, tea.Cmd) {
}
return m, nil
}
if m.review.retryExhausted {
if msg.String() == "esc" {
m.review.retryExhausted = false
m.review.orderID = 0
m = m.SwitchPage(paymentPage)
m.payment.view = 0
m.payment.form = nil
return m, nil
}
return m, nil
}
if m.review.requiresAction {
if msg.String() == "esc" {
m.review.requiresAction = false

View File

@@ -0,0 +1,394 @@
package tui
import (
"errors"
"strings"
"testing"
tea "charm.land/bubbletea/v2"
)
// newReviewModel returns a Model sitting on the review page with the default
// layout already applied. Use as the starting point for every retry-auth
// state machine test so transitions assert from a realistic baseline.
func newReviewModel(t *testing.T) Model {
t.Helper()
m := NewModel("test")
m.currentPage = reviewPage
return m
}
// keyR / keyEsc / keyJ build the bubbletea v2 key events Update expects.
// KeyMsg is an interface; KeyPressMsg satisfies it. Key.String() returns
// Text when non-empty, otherwise the keystroke (which is why esc relies on
// Code: tea.KeyEsc rather than Text).
func keyR() tea.KeyMsg { return tea.KeyPressMsg{Text: "r", Code: 'r'} }
func keyEsc() tea.KeyMsg { return tea.KeyPressMsg{Code: tea.KeyEsc} }
func keyJ() tea.KeyMsg { return tea.KeyPressMsg{Text: "j", Code: 'j'} }
func TestRetryAuthCmd_NilClient(t *testing.T) {
m := NewModel("test")
m.APIClient = nil
cmd := m.retryAuthCmd(42)
if cmd == nil {
t.Fatal("retryAuthCmd returned nil cmd")
}
msg := cmd()
rm, ok := msg.(RetryAuthResultMsg)
if !ok {
t.Fatalf("expected RetryAuthResultMsg, got %T", msg)
}
if rm.OrderID != 42 {
t.Errorf("OrderID = %d, want 42", rm.OrderID)
}
if rm.Err == nil || !strings.Contains(rm.Err.Error(), "api client unavailable") {
t.Errorf("expected api-client-unavailable err, got %v", rm.Err)
}
}
func TestReviewView_RetryingShowsCopy(t *testing.T) {
m := newReviewModel(t)
m.review.retrying = true
out := m.ReviewView()
if !strings.Contains(out, "retrying authentication") {
t.Fatalf("missing retrying copy in: %q", out)
}
}
func TestReviewView_RetryExhaustedShowsCopy(t *testing.T) {
m := newReviewModel(t)
m.review.retryExhausted = true
out := m.ReviewView()
if !strings.Contains(out, "too many authentication attempts") {
t.Fatalf("missing retry-exhausted copy in: %q", out)
}
if !strings.Contains(out, "press esc to start a new order") {
t.Fatalf("missing recovery hint in: %q", out)
}
}
func TestReviewView_AuthFailedAdvertisesRetry(t *testing.T) {
m := newReviewModel(t)
m.review.authFailed = true
out := m.ReviewView()
if !strings.Contains(out, "press r to retry") {
t.Fatalf("authFailed copy must advertise r: %q", out)
}
if !strings.Contains(out, "esc to go back") {
t.Fatalf("authFailed copy must advertise esc: %q", out)
}
}
func TestRenderRequiresActionView_FrictionlessShowsVerifying(t *testing.T) {
m := newReviewModel(t)
m.review.requiresAction = true
m.review.redirectURL = ""
out := m.renderRequiresActionView()
if !strings.Contains(out, "verifying with your bank") {
t.Fatalf("frictionless retry-success path must show verifying copy: %q", out)
}
}
func TestReviewUpdate_AuthFailed_PressR_StartsRetry(t *testing.T) {
m := newReviewModel(t)
m.review.authFailed = true
m.review.orderID = 7
m.error = &VisibleError{message: "leftover banner"}
out, cmd := m.ReviewUpdate(keyR())
if !out.review.retrying {
t.Fatal("expected retrying=true after r press")
}
if out.error != nil {
t.Fatal("stale banner should be cleared when retry starts")
}
if cmd == nil {
t.Fatal("expected non-nil cmd to call retryAuth")
}
}
func TestReviewUpdate_AuthFailed_PressR_ZeroOrderID_NoOp(t *testing.T) {
m := newReviewModel(t)
m.review.authFailed = true
m.review.orderID = 0
out, cmd := m.ReviewUpdate(keyR())
if out.review.retrying {
t.Fatal("retry must not start without an order id")
}
if cmd != nil {
t.Fatal("expected nil cmd when orderID=0")
}
}
func TestReviewUpdate_AuthFailed_PressEsc_BouncesToPayment(t *testing.T) {
m := newReviewModel(t)
m.review.authFailed = true
m.review.orderID = 7
out, _ := m.ReviewUpdate(keyEsc())
if out.review.authFailed {
t.Fatal("authFailed must clear on esc")
}
if out.currentPage != paymentPage {
t.Fatalf("currentPage = %v, want paymentPage", out.currentPage)
}
}
func TestReviewUpdate_AuthFailed_OtherKey_NoOp(t *testing.T) {
m := newReviewModel(t)
m.review.authFailed = true
m.review.orderID = 7
out, cmd := m.ReviewUpdate(keyJ())
if !out.review.authFailed {
t.Fatal("unrelated key must not clear authFailed")
}
if out.review.retrying {
t.Fatal("unrelated key must not start retry")
}
if cmd != nil {
t.Fatal("unrelated key must not produce a cmd")
}
}
func TestReviewUpdate_Retrying_BlocksKeys(t *testing.T) {
m := newReviewModel(t)
m.review.retrying = true
m.review.authFailed = true
m.review.orderID = 7
out, cmd := m.ReviewUpdate(keyR())
if !out.review.retrying {
t.Fatal("retrying flag should persist while a retry is in flight")
}
if cmd != nil {
t.Fatal("r press while retrying must not spawn a second cmd")
}
out, cmd = m.ReviewUpdate(keyEsc())
if out.currentPage != reviewPage {
t.Fatalf("currentPage changed while retrying: %v", out.currentPage)
}
if cmd != nil {
t.Fatal("esc while retrying must not navigate away")
}
}
func TestReviewUpdate_RetryAuthResult_RequiresAction(t *testing.T) {
m := newReviewModel(t)
m.review.retrying = true
m.review.authFailed = true
m.review.orderID = 7
out, cmd := m.ReviewUpdate(RetryAuthResultMsg{
OrderID: 7,
Status: "requires_action",
RedirectURL: "https://stripe/auth/abc",
})
if out.review.retrying {
t.Fatal("retrying must clear when the result arrives")
}
if out.review.authFailed {
t.Fatal("authFailed must clear on retry success")
}
if !out.review.requiresAction {
t.Fatal("expected requiresAction=true so the QR view renders")
}
if out.review.redirectURL != "https://stripe/auth/abc" {
t.Fatalf("redirectURL = %q", out.review.redirectURL)
}
if cmd == nil {
t.Fatal("expected schedulePollOrderStatus cmd")
}
if len(out.footer) != 1 || out.footer[0].key != "esc" || out.footer[0].value != "cancel" {
t.Fatalf("footer = %+v, want only esc=cancel", out.footer)
}
}
func TestReviewUpdate_RetryAuthResult_FrictionlessSuccess(t *testing.T) {
m := newReviewModel(t)
m.review.retrying = true
m.review.authFailed = true
m.review.orderID = 9
out, cmd := m.ReviewUpdate(RetryAuthResultMsg{
OrderID: 9,
Status: "succeeded",
})
if !out.review.requiresAction {
t.Fatal("frictionless retry must set requiresAction=true to render the verifying view")
}
if out.review.redirectURL != "" {
t.Fatalf("redirectURL must stay empty for frictionless retry, got %q", out.review.redirectURL)
}
if cmd == nil {
t.Fatal("expected poll cmd to wait for the webhook")
}
}
func TestReviewUpdate_RetryAuthResult_RetryLimit(t *testing.T) {
m := newReviewModel(t)
m.review.retrying = true
m.review.authFailed = true
m.review.orderID = 7
out, cmd := m.ReviewUpdate(RetryAuthResultMsg{
OrderID: 7,
Err: errors.New("RETRY_LIMIT: too many authentication attempts, please use a different card"),
})
if out.review.retrying {
t.Fatal("retrying must clear after a result")
}
if !out.review.retryExhausted {
t.Fatal("expected retryExhausted=true on RETRY_LIMIT")
}
if out.review.authFailed {
t.Fatal("authFailed must clear when retryExhausted takes over")
}
if cmd != nil {
t.Fatal("RETRY_LIMIT is terminal — no follow-up cmd")
}
if len(out.footer) != 1 || out.footer[0].key != "esc" {
t.Fatalf("footer = %+v, want only esc=back", out.footer)
}
}
func TestReviewUpdate_RetryAuthResult_CardGone(t *testing.T) {
m := newReviewModel(t)
m.review.retrying = true
m.review.authFailed = true
m.review.orderID = 7
out, _ := m.ReviewUpdate(RetryAuthResultMsg{
OrderID: 7,
Err: errors.New("CARD_NO_LONGER_AVAILABLE: saved card is no longer available, add it again to retry"),
})
if out.currentPage != paymentPage {
t.Fatalf("currentPage = %v, want paymentPage", out.currentPage)
}
if out.review.authFailed || out.review.requiresAction {
t.Fatal("auth state must reset when bouncing to payment")
}
if out.review.orderID != 0 {
t.Fatalf("orderID should reset to 0, got %d", out.review.orderID)
}
if out.error == nil || !strings.Contains(out.error.message, "saved card is no longer available") {
t.Fatalf("error banner missing or wrong copy: %+v", out.error)
}
}
func TestReviewUpdate_RetryAuthResult_Transient(t *testing.T) {
m := newReviewModel(t)
m.review.retrying = true
m.review.authFailed = true
m.review.orderID = 7
out, cmd := m.ReviewUpdate(RetryAuthResultMsg{
OrderID: 7,
Err: errors.New("CARD_DECLINED: card declined"),
})
if out.review.retrying {
t.Fatal("retrying must clear after a result")
}
if !out.review.authFailed {
t.Fatal("authFailed must persist so the user can retry again")
}
if out.review.retryExhausted {
t.Fatal("transient errors must not flip retryExhausted")
}
if out.error == nil {
t.Fatal("expected friendly banner for transient error")
}
if cmd != nil {
t.Fatal("transient error must not start a poll")
}
}
func TestReviewUpdate_RetryAuthResult_OrderIDMismatch(t *testing.T) {
m := newReviewModel(t)
m.review.retrying = true
m.review.authFailed = true
m.review.orderID = 7
out, cmd := m.ReviewUpdate(RetryAuthResultMsg{
OrderID: 99,
Status: "requires_action",
RedirectURL: "https://wrong",
})
if !out.review.retrying {
t.Fatal("mismatched orderID must leave retrying unchanged")
}
if out.review.requiresAction {
t.Fatal("mismatched orderID must not flip requiresAction")
}
if out.review.redirectURL == "https://wrong" {
t.Fatal("mismatched orderID must not adopt the foreign URL")
}
if cmd != nil {
t.Fatal("mismatched orderID must not start a poll")
}
}
func TestReviewUpdate_RetryExhausted_PressEsc_BouncesToPayment(t *testing.T) {
m := newReviewModel(t)
m.review.retryExhausted = true
m.review.orderID = 7
out, _ := m.ReviewUpdate(keyEsc())
if out.review.retryExhausted {
t.Fatal("retryExhausted must clear on esc")
}
if out.review.orderID != 0 {
t.Fatalf("orderID should reset to 0, got %d", out.review.orderID)
}
if out.currentPage != paymentPage {
t.Fatalf("currentPage = %v, want paymentPage", out.currentPage)
}
}
func TestReviewUpdate_RetryExhausted_OtherKey_NoOp(t *testing.T) {
m := newReviewModel(t)
m.review.retryExhausted = true
out, cmd := m.ReviewUpdate(keyR())
if !out.review.retryExhausted {
t.Fatal("unrelated key must not clear retryExhausted")
}
if cmd != nil {
t.Fatal("unrelated key must not produce a cmd")
}
}
func TestReviewUpdate_OrderStatusFailed_SetsRetryFooter(t *testing.T) {
m := newReviewModel(t)
m.review.requiresAction = true
m.review.orderID = 7
out, _ := m.ReviewUpdate(OrderStatusMsg{OrderID: 7, Status: "failed"})
if !out.review.authFailed {
t.Fatal("expected authFailed=true after failed status")
}
if out.review.requiresAction {
t.Fatal("requiresAction must clear when poll resolves to failed")
}
hasR, hasEsc := false, false
for _, c := range out.footer {
if c.key == "r" && c.value == "retry" {
hasR = true
}
if c.key == "esc" && c.value == "back" {
hasEsc = true
}
}
if !hasR || !hasEsc {
t.Fatalf("footer missing r/esc retry hints: %+v", out.footer)
}
}

View File

@@ -211,6 +211,8 @@ type reviewState struct {
redirectURL string
orderID uint
authFailed bool
retrying bool
retryExhausted bool
}
type confirmState struct {
@@ -442,6 +444,18 @@ type OrderStatusMsg struct {
Err error
}
// RetryAuthResultMsg carries the outcome of POST /orders/{id}/retry-auth.
// Status "requires_action" -> RedirectURL is a fresh challenge URL; resume
// the QR + poll loop. Status "succeeded" -> frictionless retry approved;
// server's APIError string ("RETRY_LIMIT" / "CARD_NO_LONGER_AVAILABLE" /
// transient) - the Update branch matches on substring.
type RetryAuthResultMsg struct {
OrderID uint
Status string
RedirectURL string
Err error
}
// CartSyncedMsg is recieved when the server responds to a cart item update
type CartSyncedMsg struct {
UpdateID int64
@@ -682,6 +696,23 @@ func (m Model) schedulePollOrderStatus(orderID uint) tea.Cmd {
})
}
func (m Model) retryAuthCmd(orderID uint) tea.Cmd {
return func() tea.Msg {
if m.APIClient == nil {
return RetryAuthResultMsg{OrderID: orderID, Err: fmt.Errorf("api client unavailable")}
}
outcome, err := m.APIClient.RetryAuth(orderID)
if err != nil {
return RetryAuthResultMsg{OrderID: orderID, Err: err}
}
return RetryAuthResultMsg{
OrderID: orderID,
Status: outcome.Status,
RedirectURL: outcome.RedirectURL,
}
}
}
func (m Model) syncCartItemCmd(coffeeID uint, quantity int) (Model, tea.Cmd) {
if m.APIClient == nil || m.User == nil {
return m, nil

View File

@@ -8,8 +8,8 @@ import (
// SuccessResponse represents a successful API response
type SuccessResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data"`
Success bool `json:"success"`
Data any `json:"data"`
}
// ErrorResponse represents an error API response
@@ -20,13 +20,13 @@ type ErrorResponse struct {
// Error contains error details
type Error struct {
Code string `json:"code"`
Message string `json:"message"`
Details map[string]interface{} `json:"details,omitempty"`
Code string `json:"code"`
Message string `json:"message"`
Details map[string]any `json:"details,omitempty"`
}
// RespondSuccess sends a successful JSON response
func RespondSuccess(w http.ResponseWriter, statusCode int, data interface{}) {
func RespondSuccess(w http.ResponseWriter, statusCode int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
@@ -39,7 +39,7 @@ func RespondSuccess(w http.ResponseWriter, statusCode int, data interface{}) {
}
// RespondError sends an error JSON response
func RespondError(w http.ResponseWriter, statusCode int, code, message string, details map[string]interface{}) {
func RespondError(w http.ResponseWriter, statusCode int, code, message string, details map[string]any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)