saveinmed/backend-old/internal/http/handler/shipping_handler.go
2026-01-16 10:51:52 -03:00

201 lines
6.4 KiB
Go

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)
}
// 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)
}