package handler import ( "context" "errors" "net/http" "strconv" "strings" "time" "github.com/gofrs/uuid/v5" "github.com/saveinmed/backend-go/internal/domain" "github.com/saveinmed/backend-go/internal/http/middleware" ) // --- Request DTOs --- type createUserRequest struct { CompanyID uuid.UUID `json:"company_id"` Role string `json:"role"` Name string `json:"name"` Username string `json:"username"` Email string `json:"email"` Password string `json:"password"` } type registerAuthRequest struct { CompanyID *uuid.UUID `json:"company_id,omitempty"` Company *registerCompanyTarget `json:"company,omitempty"` Role string `json:"role"` Name string `json:"name"` Username string `json:"username"` Email string `json:"email"` Password string `json:"password"` } type registerCompanyTarget struct { ID uuid.UUID `json:"id,omitempty"` Category string `json:"category"` CNPJ string `json:"cnpj"` CorporateName string `json:"corporate_name"` LicenseNumber string `json:"license_number"` Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` City string `json:"city"` State string `json:"state"` } type loginRequest struct { Username string `json:"username,omitempty"` Email string `json:"email,omitempty"` Password string `json:"password"` } type forgotPasswordRequest struct { Email string `json:"email"` } type resetPasswordRequest struct { Token string `json:"token"` Password string `json:"password"` } type verifyEmailRequest struct { Token string `json:"token"` } type authResponse struct { Token string `json:"token"` ExpiresAt time.Time `json:"expires_at"` } type messageResponse struct { Message string `json:"message"` } type resetTokenResponse struct { Message string `json:"message"` ResetToken string `json:"reset_token,omitempty"` ExpiresAt *time.Time `json:"expires_at,omitempty"` } type inventoryAdjustRequest struct { ProductID uuid.UUID `json:"product_id"` Delta int64 `json:"delta"` Reason string `json:"reason"` } type addCartItemRequest struct { ProductID uuid.UUID `json:"product_id"` Quantity int64 `json:"quantity"` } type createReviewRequest struct { OrderID uuid.UUID `json:"order_id"` Rating int `json:"rating"` Comment string `json:"comment"` } type updateUserRequest struct { CompanyID *uuid.UUID `json:"company_id,omitempty"` Role *string `json:"role,omitempty"` Name *string `json:"name,omitempty"` Username *string `json:"username,omitempty"` Email *string `json:"email,omitempty"` Password *string `json:"password,omitempty"` } type requester struct { Role string CompanyID *uuid.UUID } type registerCompanyRequest struct { Category string `json:"category"` CNPJ string `json:"cnpj"` CorporateName string `json:"corporate_name"` LicenseNumber string `json:"license_number"` Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` City string `json:"city"` State string `json:"state"` } type updateCompanyRequest struct { Category *string `json:"category,omitempty"` CNPJ *string `json:"cnpj,omitempty"` CorporateName *string `json:"corporate_name,omitempty"` LicenseNumber *string `json:"license_number,omitempty"` IsVerified *bool `json:"is_verified,omitempty"` Latitude *float64 `json:"latitude,omitempty"` Longitude *float64 `json:"longitude,omitempty"` City *string `json:"city,omitempty"` State *string `json:"state,omitempty"` } type registerProductRequest struct { SellerID uuid.UUID `json:"seller_id"` Name string `json:"name"` Description string `json:"description"` Batch string `json:"batch"` ExpiresAt time.Time `json:"expires_at"` PriceCents int64 `json:"price_cents"` Stock int64 `json:"stock"` } type updateProductRequest struct { SellerID *uuid.UUID `json:"seller_id,omitempty"` Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` Batch *string `json:"batch,omitempty"` ExpiresAt *time.Time `json:"expires_at,omitempty"` PriceCents *int64 `json:"price_cents,omitempty"` Stock *int64 `json:"stock,omitempty"` } type createOrderRequest struct { BuyerID uuid.UUID `json:"buyer_id"` SellerID uuid.UUID `json:"seller_id"` Items []domain.OrderItem `json:"items"` Shipping domain.ShippingAddress `json:"shipping"` } type createShipmentRequest struct { OrderID uuid.UUID `json:"order_id"` Carrier string `json:"carrier"` TrackingCode string `json:"tracking_code"` ExternalTracking string `json:"external_tracking"` } type updateStatusRequest struct { Status string `json:"status"` } type shippingSettingsRequest struct { Active bool `json:"active"` MaxRadiusKm float64 `json:"max_radius_km"` PricePerKmCents int64 `json:"price_per_km_cents"` MinFeeCents int64 `json:"min_fee_cents"` FreeShippingThresholdCents *int64 `json:"free_shipping_threshold_cents,omitempty"` PickupActive bool `json:"pickup_active"` PickupAddress string `json:"pickup_address,omitempty"` PickupHours string `json:"pickup_hours,omitempty"` Latitude float64 `json:"latitude"` // Store location for radius calc Longitude float64 `json:"longitude"` } type shippingCalculateRequest struct { VendorID uuid.UUID `json:"vendor_id"` CartTotalCents int64 `json:"cart_total_cents"` BuyerLatitude *float64 `json:"buyer_latitude,omitempty"` BuyerLongitude *float64 `json:"buyer_longitude,omitempty"` AddressID *uuid.UUID `json:"address_id,omitempty"` PostalCode string `json:"postal_code,omitempty"` } // --- Utility Functions --- func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } func writeError(w http.ResponseWriter, status int, err error) { writeJSON(w, status, map[string]string{"error": err.Error()}) } func decodeJSON(ctx context.Context, r *http.Request, v any) error { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() dec := json.NewDecoder(r.Body) dec.DisallowUnknownFields() if err := dec.Decode(v); err != nil { return err } return ctx.Err() } func parseUUIDFromPath(path string) (uuid.UUID, error) { parts := splitPath(path) for i := len(parts) - 1; i >= 0; i-- { if id, err := uuid.FromString(parts[i]); err == nil { return id, nil } } return uuid.UUID{}, errors.New("missing resource id") } func splitPath(p string) []string { var parts []string start := 0 for i := 0; i < len(p); i++ { if p[i] == '/' { if i > start { parts = append(parts, p[start:i]) } start = i + 1 } } if start < len(p) { parts = append(parts, p[start:]) } return parts } func isValidStatus(status string) bool { switch domain.OrderStatus(status) { case domain.OrderStatusPending, domain.OrderStatusPaid, domain.OrderStatusInvoiced, domain.OrderStatusDelivered: return true default: return false } } func parsePagination(r *http.Request) (int, int) { page := 1 pageSize := 20 if v := r.URL.Query().Get("page"); v != "" { if p, err := strconv.Atoi(v); err == nil && p > 0 { page = p } } if v := r.URL.Query().Get("page_size"); v != "" { if ps, err := strconv.Atoi(v); err == nil && ps > 0 { pageSize = ps } } return page, pageSize } func getRequester(r *http.Request) (requester, error) { if claims, ok := middleware.GetClaims(r.Context()); ok { return requester{Role: claims.Role, CompanyID: claims.CompanyID}, nil } role := r.Header.Get("X-User-Role") if role == "" { role = "Admin" } var companyID *uuid.UUID if cid := r.Header.Get("X-Company-ID"); cid != "" { id, err := uuid.FromString(cid) if err != nil { return requester{}, errors.New("invalid X-Company-ID header") } companyID = &id } return requester{Role: role, CompanyID: companyID}, nil } func parseBearerToken(r *http.Request) (string, error) { authHeader := r.Header.Get("Authorization") if authHeader == "" { return "", errors.New("missing Authorization header") } parts := strings.SplitN(authHeader, " ", 2) if len(parts) != 2 || !strings.EqualFold(parts[0], "bearer") { return "", errors.New("invalid Authorization header") } token := strings.TrimSpace(parts[1]) if token == "" { return "", errors.New("token is required") } return token, nil }