refactor(handler): extract DTOs and helpers to dto.go
- Move 18 request/response structs to dto.go - Move utility functions (writeJSON, decodeJSON, parseUUID, etc) - Reduce handler.go from 1471 to 1254 lines (~15% reduction) - All tests passing
This commit is contained in:
parent
b8973739ab
commit
23df78d9c3
2 changed files with 233 additions and 217 deletions
233
backend/internal/http/handler/dto.go
Normal file
233
backend/internal/http/handler/dto.go
Normal file
|
|
@ -0,0 +1,233 @@
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"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"`
|
||||||
|
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"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type registerCompanyTarget struct {
|
||||||
|
ID uuid.UUID `json:"id,omitempty"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
CNPJ string `json:"cnpj"`
|
||||||
|
CorporateName string `json:"corporate_name"`
|
||||||
|
LicenseNumber string `json:"license_number"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type loginRequest struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type authResponse struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
Email *string `json:"email,omitempty"`
|
||||||
|
Password *string `json:"password,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type requester struct {
|
||||||
|
Role string
|
||||||
|
CompanyID *uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
type registerCompanyRequest struct {
|
||||||
|
Role string `json:"role"`
|
||||||
|
CNPJ string `json:"cnpj"`
|
||||||
|
CorporateName string `json:"corporate_name"`
|
||||||
|
LicenseNumber string `json:"license_number"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type updateCompanyRequest struct {
|
||||||
|
Role *string `json:"role,omitempty"`
|
||||||
|
CNPJ *string `json:"cnpj,omitempty"`
|
||||||
|
CorporateName *string `json:"corporate_name,omitempty"`
|
||||||
|
LicenseNumber *string `json:"license_number,omitempty"`
|
||||||
|
IsVerified *bool `json:"is_verified,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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 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
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
@ -1252,219 +1251,3 @@ func (h *Handler) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
type createUserRequest struct {
|
|
||||||
CompanyID uuid.UUID `json:"company_id"`
|
|
||||||
Role string `json:"role"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
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"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type registerCompanyTarget struct {
|
|
||||||
ID uuid.UUID `json:"id,omitempty"`
|
|
||||||
Role string `json:"role"`
|
|
||||||
CNPJ string `json:"cnpj"`
|
|
||||||
CorporateName string `json:"corporate_name"`
|
|
||||||
LicenseNumber string `json:"license_number"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type loginRequest struct {
|
|
||||||
Email string `json:"email"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type authResponse struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
ExpiresAt time.Time `json:"expires_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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"`
|
|
||||||
Email *string `json:"email,omitempty"`
|
|
||||||
Password *string `json:"password,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type requester struct {
|
|
||||||
Role string
|
|
||||||
CompanyID *uuid.UUID
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
type registerCompanyRequest struct {
|
|
||||||
Role string `json:"role"`
|
|
||||||
CNPJ string `json:"cnpj"`
|
|
||||||
CorporateName string `json:"corporate_name"`
|
|
||||||
LicenseNumber string `json:"license_number"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type updateCompanyRequest struct {
|
|
||||||
Role *string `json:"role,omitempty"`
|
|
||||||
CNPJ *string `json:"cnpj,omitempty"`
|
|
||||||
CorporateName *string `json:"corporate_name,omitempty"`
|
|
||||||
LicenseNumber *string `json:"license_number,omitempty"`
|
|
||||||
IsVerified *bool `json:"is_verified,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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue