package validator import ( "errors" "strings" "apskel-pos-be/internal/constants" "apskel-pos-be/internal/contract" "github.com/google/uuid" ) type ProductValidator interface { ValidateCreateProductRequest(req *contract.CreateProductRequest) (error, string) ValidateUpdateProductRequest(req *contract.UpdateProductRequest) (error, string) ValidateListProductsRequest(req *contract.ListProductsRequest) (error, string) } type ProductValidatorImpl struct{} func NewProductValidator() *ProductValidatorImpl { return &ProductValidatorImpl{} } func (v *ProductValidatorImpl) ValidateCreateProductRequest(req *contract.CreateProductRequest) (error, string) { if req == nil { return errors.New("request body is required"), constants.MissingFieldErrorCode } if req.CategoryID == uuid.Nil { return errors.New("category_id is required"), constants.MissingFieldErrorCode } if strings.TrimSpace(req.Name) == "" { return errors.New("name is required"), constants.MissingFieldErrorCode } if len(req.Name) < 1 || len(req.Name) > 255 { return errors.New("name must be between 1 and 255 characters"), constants.MalformedFieldErrorCode } if req.Price < 0 { return errors.New("price must be non-negative"), constants.MalformedFieldErrorCode } if req.Cost != nil && *req.Cost < 0 { return errors.New("cost must be non-negative"), constants.MalformedFieldErrorCode } if req.SKU != nil && len(*req.SKU) > 100 { return errors.New("sku cannot exceed 100 characters"), constants.MalformedFieldErrorCode } if req.Description != nil && len(*req.Description) > 1000 { return errors.New("description cannot exceed 1000 characters"), constants.MalformedFieldErrorCode } if req.ImageURL != nil && len(*req.ImageURL) > 500 { return errors.New("image_url cannot exceed 500 characters"), constants.MalformedFieldErrorCode } if err, code := validatePrinterTypes(req.PrinterTypes); err != nil { return err, code } if err, code := validateSellBy(req.SellBy, req.UnitID); err != nil { return err, code } return nil, "" } // validatePrinterTypes holds each printer to the 50 characters the single printer_type // column allowed. func validatePrinterTypes(printerTypes []string) (error, string) { for _, printerType := range printerTypes { if len(strings.TrimSpace(printerType)) > 50 { return errors.New("each printer_types entry cannot exceed 50 characters"), constants.MalformedFieldErrorCode } } return nil, "" } // validateSellBy checks how a product is sold and that it carries what that choice // needs. A weight-based product without a unit would produce order lines with no unit // to print, so the receipt could show "4,2" with no idea of what. // // unitID is the unit the request would leave on the product: for an update that does // not touch unit_id, pass the product's current one. func validateSellBy(sellBy *string, unitID *uuid.UUID) (error, string) { if sellBy == nil { return nil, "" } if !constants.IsValidSellBy(*sellBy) { return errors.New("sell_by must be either 'unit' or 'weight'"), constants.MalformedFieldErrorCode } if *sellBy == constants.SellByWeight && unitID == nil { return errors.New("unit_id is required when sell_by is 'weight'"), constants.MissingFieldErrorCode } return nil, "" } func (v *ProductValidatorImpl) ValidateUpdateProductRequest(req *contract.UpdateProductRequest) (error, string) { if req == nil { return errors.New("request body is required"), constants.MissingFieldErrorCode } // At least one field should be provided for update if req.CategoryID == nil && req.SKU == nil && req.Name == nil && req.Description == nil && req.Price == nil && req.Cost == nil && req.BusinessType == nil && req.ImageURL == nil && req.PrinterTypes == nil && req.PrintToChecker == nil && req.UnitID == nil && req.SellBy == nil && req.Metadata == nil && req.IsActive == nil { return errors.New("at least one field must be provided for update"), constants.MissingFieldErrorCode } if req.Name != nil { if strings.TrimSpace(*req.Name) == "" { return errors.New("name cannot be empty"), constants.MalformedFieldErrorCode } if len(*req.Name) < 1 || len(*req.Name) > 255 { return errors.New("name must be between 1 and 255 characters"), constants.MalformedFieldErrorCode } } if req.Price != nil && *req.Price < 0 { return errors.New("price must be non-negative"), constants.MalformedFieldErrorCode } if req.Cost != nil && *req.Cost < 0 { return errors.New("cost must be non-negative"), constants.MalformedFieldErrorCode } if req.SKU != nil && len(*req.SKU) > 100 { return errors.New("sku cannot exceed 100 characters"), constants.MalformedFieldErrorCode } if req.Description != nil && len(*req.Description) > 1000 { return errors.New("description cannot exceed 1000 characters"), constants.MalformedFieldErrorCode } if req.ImageURL != nil && len(*req.ImageURL) > 500 { return errors.New("image_url cannot exceed 500 characters"), constants.MalformedFieldErrorCode } if err, code := validatePrinterTypes(req.PrinterTypes); err != nil { return err, code } // Only the value is checked here. Whether the product ends up with a unit depends on // what it already has, which this request cannot see — the processor checks that // against the stored product. if req.SellBy != nil && !constants.IsValidSellBy(*req.SellBy) { return errors.New("sell_by must be either 'unit' or 'weight'"), constants.MalformedFieldErrorCode } return nil, "" } func (v *ProductValidatorImpl) ValidateListProductsRequest(req *contract.ListProductsRequest) (error, string) { if req == nil { return errors.New("request is required"), constants.MissingFieldErrorCode } if req.Page < 1 { return errors.New("page must be at least 1"), constants.MalformedFieldErrorCode } if req.Limit < 1 || req.Limit > 100 { return errors.New("limit must be between 1 and 100"), constants.MalformedFieldErrorCode } if req.MinPrice != nil && *req.MinPrice < 0 { return errors.New("min_price must be non-negative"), constants.MalformedFieldErrorCode } if req.MaxPrice != nil && *req.MaxPrice < 0 { return errors.New("max_price must be non-negative"), constants.MalformedFieldErrorCode } if req.MinPrice != nil && req.MaxPrice != nil && *req.MinPrice > *req.MaxPrice { return errors.New("min_price cannot be greater than max_price"), constants.MalformedFieldErrorCode } return nil, "" }