package handlers import ( "context" "fmt" "net/http" "strconv" "strings" "github.com/labstack/echo/v4" appmiddleware "git.ma-al.com/goc_marek/ps_shop/internal/http/middleware" pscart "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cart" pscookie "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cookie" ) type cartSessionService interface { RefreshExpiry(ctx context.Context, session *pscookie.SessionContext) error ResolveCookiePath(ctx context.Context, req *http.Request) (string, error) } type CartHandler struct { carts *pscart.Service codec pscookie.Codec sessions cartSessionService } func NewCartHandler(carts *pscart.Service, codec pscookie.Codec, sessions cartSessionService) *CartHandler { return &CartHandler{ carts: carts, codec: codec, sessions: sessions, } } func (h *CartHandler) Handle(c echo.Context) error { if h == nil || h.carts == nil || h.codec == nil || h.sessions == nil { return echo.NewHTTPError(http.StatusInternalServerError, "cart handler is not initialized") } session := appmiddleware.GetSession(c) action := cartActionFromRequest(c) input, err := cartMutationInput(c, session) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } var result *pscart.MutationResult switch action { case cartActionDelete: result, err = h.carts.DeleteProduct(c.Request().Context(), input) case cartActionUpdate: result, err = h.carts.UpdateProduct(c.Request().Context(), input) default: result, err = h.carts.AddProduct(c.Request().Context(), input) } if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "cart mutation failed: "+err.Error()) } syncSessionCartID(session, result.CartID) if err := h.writeSessionCookie(c, session); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "cart cookie update failed: "+err.Error()) } if wantsJSON(c.Request()) { return c.JSON(http.StatusOK, result) } return c.Redirect(http.StatusSeeOther, cartRedirectTarget(c.Request())) } type cartAction string const ( cartActionAdd cartAction = "add" cartActionUpdate cartAction = "update" cartActionDelete cartAction = "delete" ) func cartActionFromRequest(c echo.Context) cartAction { switch c.Request().Method { case http.MethodDelete: return cartActionDelete case http.MethodPut, http.MethodPatch: return cartActionUpdate } action := strings.ToLower(strings.TrimSpace(c.FormValue("action"))) switch action { case string(cartActionDelete): return cartActionDelete case string(cartActionUpdate): return cartActionUpdate } switch { case c.FormValue("delete") != "": return cartActionDelete case c.FormValue("update") != "": return cartActionUpdate default: return cartActionAdd } } func cartMutationInput(c echo.Context, session *pscookie.SessionContext) (pscart.MutationInput, error) { productID, err := formInt64(c, "id_product") if err != nil || productID == 0 { return pscart.MutationInput{}, fmt.Errorf("valid id_product is required") } quantity := int64(1) if raw := strings.TrimSpace(c.FormValue("qty")); raw != "" { quantity, err = strconv.ParseInt(raw, 10, 64) if err != nil || quantity < 0 { return pscart.MutationInput{}, fmt.Errorf("valid qty is required") } } productAttributeID, err := firstFormInt64(c, "id_product_attribute", "ipa") if err != nil { return pscart.MutationInput{}, fmt.Errorf("valid id_product_attribute is required") } customizationID, err := firstFormInt64(c, "id_customization", "customization_id") if err != nil { return pscart.MutationInput{}, fmt.Errorf("valid id_customization is required") } return pscart.MutationInput{ CartID: int64Value(session.CartID), ProductID: productID, ProductAttributeID: productAttributeID, CustomizationID: customizationID, Quantity: quantity, CustomerID: int64Value(session.CustomerID), GuestID: int64Value(session.GuestID), LanguageID: int64Value(session.LanguageID), CurrencyID: int64Value(session.CurrencyID), ShopID: int64Value(session.ShopID), }, nil } func formInt64(c echo.Context, key string) (int64, error) { raw := strings.TrimSpace(c.FormValue(key)) if raw == "" { return 0, nil } return strconv.ParseInt(raw, 10, 64) } func firstFormInt64(c echo.Context, keys ...string) (int64, error) { for _, key := range keys { value, err := formInt64(c, key) if err != nil { return 0, err } if value != 0 { return value, nil } } return 0, nil } func syncSessionCartID(session *pscookie.SessionContext, cartID int64) { if session == nil || cartID == 0 { return } if session.Values == nil { session.Values = map[string]string{} } session.CartID = int64Ptr(cartID) session.Values["id_cart"] = strconv.FormatInt(cartID, 10) session.OrderedKeys = appendOrderedKeyIfMissing(session.OrderedKeys, "id_cart") session.OrderedKeys = moveOrderedKeyToEnd(session.OrderedKeys, "checksum") session.Plaintext = "" session.RawCookie = "" } func appendOrderedKeyIfMissing(keys []string, key string) []string { for _, existing := range keys { if existing == key { return keys } } return append(keys, key) } func moveOrderedKeyToEnd(keys []string, key string) []string { for i, existing := range keys { if existing == key { keys = append(keys[:i], keys[i+1:]...) return append(keys, key) } } return keys } func int64Ptr(value int64) *int64 { if value == 0 { return nil } v := value return &v } func int64Value(value *int64) int64 { if value == nil { return 0 } return *value } func (h *CartHandler) writeSessionCookie(c echo.Context, session *pscookie.SessionContext) error { if err := h.sessions.RefreshExpiry(c.Request().Context(), session); err != nil { return err } cookiePath, err := h.sessions.ResolveCookiePath(c.Request().Context(), c.Request()) if err != nil { return err } encoded, err := h.codec.Encode(session) if err != nil { return err } session.RawCookie = encoded writeSessionCookie(c.Request(), c.Response(), session, session.CookieName, encoded, cookiePath) return nil } func writeSessionCookie(req *http.Request, res *echo.Response, session *pscookie.SessionContext, name, value, path string) { maxAge := 1 if session != nil && session.ExpiresAt != nil { maxAge = int(session.ExpiresAt.UTC().Unix()) } if strings.TrimSpace(path) == "" { path = "/" } header := fmt.Sprintf("%s=%s; path=%s; max-age=%d; HttpOnly; SameSite=Lax", name, value, path, maxAge) if requestCookieSecure(req) { header += "; Secure" } res.Header().Add(echo.HeaderSetCookie, header) } func requestCookieSecure(req *http.Request) bool { if req == nil { return false } if req.TLS != nil { return true } forwarded := req.Header.Get("X-Forwarded-Proto") if strings.Contains(forwarded, ",") { forwarded = strings.TrimSpace(strings.Split(forwarded, ",")[0]) } return strings.EqualFold(forwarded, "https") } func wantsJSON(req *http.Request) bool { if req == nil { return false } return strings.Contains(strings.ToLower(req.Header.Get(echo.HeaderAccept)), "application/json") } func cartRedirectTarget(req *http.Request) string { if req == nil { return "/cart?action=show" } referer := strings.TrimSpace(req.Referer()) if referer != "" { return referer } path := requestLanguagePrefix(req) + "/cart" if path == "/cart" { return "/cart?action=show" } return path + "?action=show" }