package handler import ( "errors" "net/http" "strings" "github.com/gofrs/uuid/v5" "github.com/saveinmed/backend-go/internal/domain" ) // GetShippingSettings godoc // @Summary Get vendor shipping settings // @Description Returns pickup and delivery settings for a vendor. // @Tags Shipping // @Produce json // @Param vendor_id path string true "Vendor ID" // @Success 200 {object} domain.ShippingSettings // @Failure 400 {object} map[string]string // @Failure 403 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/shipping/settings/{vendor_id} [get] func (h *Handler) GetShippingSettings(w http.ResponseWriter, r *http.Request) { vendorID, err := parseUUIDFromPath(r.URL.Path) if err != nil { writeError(w, http.StatusBadRequest, err) return } // Any authenticated user can view shipping settings (needed for checkout) // No role-based restriction here - shipping settings are public info for buyers settings, err := h.svc.GetShippingSettings(r.Context(), vendorID) if err != nil { // Log error if needed, but for 404/not found we might return empty object writeError(w, http.StatusInternalServerError, err) return } if settings == nil { // Return defaults settings = &domain.ShippingSettings{VendorID: vendorID, Active: false} } writeJSON(w, http.StatusOK, settings) } // UpsertShippingSettings godoc // @Summary Update vendor shipping settings // @Description Stores pickup and delivery settings for a vendor. // @Tags Shipping // @Accept json // @Produce json // @Param vendor_id path string true "Vendor ID" // @Param payload body shippingSettingsRequest true "Shipping settings" // @Success 200 {object} domain.ShippingSettings // @Failure 400 {object} map[string]string // @Failure 403 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/shipping/settings/{vendor_id} [put] func (h *Handler) UpsertShippingSettings(w http.ResponseWriter, r *http.Request) { vendorID, err := parseUUIDFromPath(r.URL.Path) if err != nil { writeError(w, http.StatusBadRequest, err) return } requester, err := getRequester(r) if err != nil { writeError(w, http.StatusBadRequest, err) return } if !strings.EqualFold(requester.Role, "Admin") { if requester.CompanyID == nil || *requester.CompanyID != vendorID { writeError(w, http.StatusForbidden, errors.New("not allowed to update shipping settings")) return } } var req shippingSettingsRequest if err := decodeJSON(r.Context(), r, &req); err != nil { writeError(w, http.StatusBadRequest, err) return } if req.Active { if req.MaxRadiusKm < 0 { writeError(w, http.StatusBadRequest, errors.New("max_radius_km must be >= 0")) return } if req.PricePerKmCents < 0 || req.MinFeeCents < 0 { writeError(w, http.StatusBadRequest, errors.New("pricing fields must be >= 0")) return } } if req.PickupActive { if strings.TrimSpace(req.PickupAddress) == "" || strings.TrimSpace(req.PickupHours) == "" { writeError(w, http.StatusBadRequest, errors.New("pickup_address and pickup_hours are required for active pickup")) return } } settings := &domain.ShippingSettings{ VendorID: vendorID, Active: req.Active, MaxRadiusKm: req.MaxRadiusKm, PricePerKmCents: req.PricePerKmCents, MinFeeCents: req.MinFeeCents, FreeShippingThresholdCents: req.FreeShippingThresholdCents, PickupActive: req.PickupActive, PickupAddress: req.PickupAddress, PickupHours: req.PickupHours, Latitude: req.Latitude, Longitude: req.Longitude, } if err := h.svc.UpsertShippingSettings(r.Context(), settings); err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusOK, settings) } // CalculateShipping godoc // @Summary Calculate shipping options // @Description Calculates shipping or pickup options based on vendor config and buyer location. // @Tags Shipping // @Accept json // @Produce json // @Param payload body shippingCalculateRequest true "Calculation inputs" // @Success 200 {array} domain.ShippingOption // @Failure 400 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/shipping/calculate [post] func (h *Handler) CalculateShipping(w http.ResponseWriter, r *http.Request) { var req shippingCalculateRequest if err := decodeJSON(r.Context(), r, &req); err != nil { writeError(w, http.StatusBadRequest, err) return } if req.VendorID == uuid.Nil { writeError(w, http.StatusBadRequest, errors.New("vendor_id is required")) return } if req.BuyerLatitude == nil || req.BuyerLongitude == nil { if req.AddressID != nil || req.PostalCode != "" { writeError(w, http.StatusBadRequest, errors.New("address_id or postal_code geocoding is not supported; provide buyer_latitude and buyer_longitude")) return } writeError(w, http.StatusBadRequest, errors.New("buyer_latitude and buyer_longitude are required")) return } options, err := h.svc.CalculateShippingOptions(r.Context(), req.VendorID, *req.BuyerLatitude, *req.BuyerLongitude, req.CartTotalCents) if err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusOK, options) } func parseShippingMethodType(value string) (domain.ShippingMethodType, error) { switch strings.ToLower(strings.TrimSpace(value)) { case string(domain.ShippingMethodPickup): return domain.ShippingMethodPickup, nil case string(domain.ShippingMethodOwnDelivery): return domain.ShippingMethodOwnDelivery, nil case string(domain.ShippingMethodThirdParty): return domain.ShippingMethodThirdParty, nil default: return "", errors.New("invalid shipping method type") } } // ListShipments godoc // @Summary List shipments // @Description Returns shipments. Admins see all, Tenants see only their own. // @Tags Shipments // @Security BearerAuth // @Produce json // @Param page query int false "Página" // @Param page_size query int false "Tamanho da página" // @Success 200 {object} domain.ShipmentPage // @Failure 401 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/shipments [get] func (h *Handler) ListShipments(w http.ResponseWriter, r *http.Request) { page, pageSize := parsePagination(r) requester, err := getRequester(r) if err != nil { writeError(w, http.StatusUnauthorized, err) return } filter := domain.ShipmentFilter{} if !strings.EqualFold(requester.Role, "Admin") { if requester.CompanyID == nil { writeError(w, http.StatusForbidden, errors.New("user has no company associated")) return } // Shipments logic: // Shipments are linked to orders, and orders belong to sellers. filter.SellerID = requester.CompanyID } result, err := h.svc.ListShipments(r.Context(), filter, page, pageSize) if err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusOK, result) }