package contract import ( "bytes" "encoding/json" "time" "github.com/google/uuid" ) type CreateCategoryRequest struct { Name string `json:"name" validate:"required,min=1,max=255"` Description *string `json:"description,omitempty"` BusinessType *string `json:"business_type,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"` ParentID *uuid.UUID `json:"parent_id,omitempty"` Order *int `json:"order,omitempty"` OwnerFeePercent *float64 `json:"owner_fee_percent,omitempty"` IsTeam *bool `json:"is_team,omitempty"` Metadata map[string]interface{} `json:"metadata,omitempty"` } type UpdateCategoryRequest struct { Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"` Description *string `json:"description,omitempty"` BusinessType *string `json:"business_type,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"` ParentID *uuid.UUID `json:"parent_id,omitempty"` Order *int `json:"order,omitempty"` OwnerFeePercent *float64 `json:"owner_fee_percent,omitempty"` IsTeam *bool `json:"is_team,omitempty"` Metadata map[string]interface{} `json:"metadata,omitempty"` // Set when the field is sent as null (or "" for parent_id), which asks for the // value to be removed. A field that is left out stays unchanged. ClearParentID bool `json:"-"` ClearOwnerFeePercent bool `json:"-"` } // UnmarshalJSON tells an explicit null apart from a field that was left out, so a // category can be detached from its parent and an owner fee override can be removed. func (r *UpdateCategoryRequest) UnmarshalJSON(data []byte) error { var raw map[string]json.RawMessage if err := json.Unmarshal(data, &raw); err != nil { return err } isEmpty := func(key string, allowEmptyString bool) bool { value, ok := raw[key] if !ok { return false } value = bytes.TrimSpace(value) return bytes.Equal(value, []byte("null")) || (allowEmptyString && bytes.Equal(value, []byte(`""`))) } clearParentID := isEmpty("parent_id", true) clearOwnerFeePercent := isEmpty("owner_fee_percent", false) if clearParentID { // An empty string is not a valid UUID, so keep it away from the decoder delete(raw, "parent_id") cleaned, err := json.Marshal(raw) if err != nil { return err } data = cleaned } type plain UpdateCategoryRequest var decoded plain if err := json.Unmarshal(data, &decoded); err != nil { return err } *r = UpdateCategoryRequest(decoded) r.ClearParentID = clearParentID r.ClearOwnerFeePercent = clearOwnerFeePercent return nil } type ListCategoriesRequest struct { OrganizationID *uuid.UUID `json:"organization_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"` ParentID *uuid.UUID `json:"parent_id,omitempty"` Type string `json:"type,omitempty" validate:"omitempty,oneof=parent child"` BusinessType string `json:"business_type,omitempty"` Search string `json:"search,omitempty"` Page int `json:"page" validate:"required,min=1"` Limit int `json:"limit" validate:"required,min=1,max=100"` } // Category Response DTOs type CategoryResponse struct { ID uuid.UUID `json:"id"` OrganizationID uuid.UUID `json:"organization_id"` OutletID *uuid.UUID `json:"outlet_id"` ParentID *uuid.UUID `json:"parent_id,omitempty"` ParentName *string `json:"parent_name,omitempty"` Name string `json:"name"` Description *string `json:"description"` BusinessType string `json:"business_type"` Order int `json:"order"` OwnerFeePercent *float64 `json:"owner_fee_percent"` IsTeam bool `json:"is_team"` Metadata map[string]interface{} `json:"metadata"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type ListCategoriesResponse struct { Categories []CategoryResponse `json:"categories"` TotalCount int `json:"total_count"` Page int `json:"page"` Limit int `json:"limit"` TotalPages int `json:"total_pages"` }