Files
telecom-platform/apps/carrier-connector/internal/services/pricing_validator.go
T
nutcas3 ef4e8a05cd refactor: Replace interface{} with any and optimize slice operations
- Replace map[string]interface{} with map[string]any in UpdateCarrierMetrics
- Replace interface{} with any for GeneratedAt field in PricingEffectiveness struct
- Replace loop-based empty string checks with slices.Contains in ValidateConditions
- Replace range loop index initialization with range-only syntax in calculateAPIPerformance
- Add slices import for standard library slice utilities
2026-05-02 17:57:20 +03:00

241 lines
6.0 KiB
Go

package services
import (
"context"
"errors"
"fmt"
"slices"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/pricing"
)
// PricingValidator implements validation for pricing rules and contexts
type PricingValidator struct{}
// NewPricingValidator creates a new pricing validator
func NewPricingValidator() pricing.PricingValidator {
return &PricingValidator{}
}
// ValidateRule validates a pricing rule
func (v *PricingValidator) ValidateRule(ctx context.Context, rule *pricing.PricingRule) error {
if rule.ID == "" {
return errors.New("rule ID is required")
}
if rule.Name == "" {
return errors.New("rule name is required")
}
if rule.TenantID == "" {
return errors.New("tenant ID is required")
}
if rule.Type == "" {
return errors.New("rule type is required")
}
// Validate rule type
validTypes := map[pricing.RuleType]bool{
pricing.RuleTypePercentageDiscount: true,
pricing.RuleTypeFixedDiscount: true,
pricing.RuleTypeMultiplier: true,
pricing.RuleTypeTieredPricing: true,
pricing.RuleTypeDynamicPricing: true,
pricing.RuleTypeConditionalPricing: true,
}
if !validTypes[rule.Type] {
return fmt.Errorf("invalid rule type: %s", rule.Type)
}
// Validate conditions
if err := v.ValidateConditions(ctx, rule.Conditions); err != nil {
return fmt.Errorf("invalid conditions: %w", err)
}
// Validate actions
if err := v.ValidateActions(ctx, rule.Actions); err != nil {
return fmt.Errorf("invalid actions: %w", err)
}
return nil
}
// ValidateContext validates a pricing context
func (v *PricingValidator) ValidateContext(ctx context.Context, pricingCtx *pricing.PricingContext) error {
if pricingCtx.TenantID == "" {
return errors.New("tenant ID is required")
}
if pricingCtx.ProductID == "" {
return errors.New("product ID is required")
}
if pricingCtx.BasePrice < 0 {
return errors.New("base price cannot be negative")
}
if pricingCtx.Currency == "" {
return errors.New("currency is required")
}
if pricingCtx.Quantity <= 0 {
return errors.New("quantity must be positive")
}
if pricingCtx.Time.IsZero() {
return errors.New("time is required")
}
return nil
}
// ValidateConditions validates rule conditions
func (v *PricingValidator) ValidateConditions(ctx context.Context, conditions pricing.RuleConditions) error {
// Validate time range
if conditions.TimeRange != nil {
if conditions.TimeRange.Start.IsZero() || conditions.TimeRange.End.IsZero() {
return errors.New("time range requires both start and end times")
}
if conditions.TimeRange.Start.After(conditions.TimeRange.End) {
return errors.New("time range start must be before end")
}
// Validate days
validDays := map[string]bool{
"monday": true,
"tuesday": true,
"wednesday": true,
"thursday": true,
"friday": true,
"saturday": true,
"sunday": true,
}
for _, day := range conditions.TimeRange.Days {
if !validDays[day] {
return fmt.Errorf("invalid day: %s", day)
}
}
}
// Validate geography
if slices.Contains(conditions.Geography, "") {
return errors.New("geography cannot be empty")
}
// Validate customer type
if slices.Contains(conditions.CustomerType, "") {
return errors.New("customer type cannot be empty")
}
// Validate volume range
if conditions.Volume != nil {
if conditions.Volume.Min < 0 || conditions.Volume.Max < 0 {
return errors.New("volume range values must be non-negative")
}
if conditions.Volume.Min > conditions.Volume.Max {
return errors.New("volume min cannot be greater than max")
}
}
// Validate usage pattern
if conditions.UsagePattern != nil {
if len(conditions.UsagePattern.PeakHours) == 0 && len(conditions.UsagePattern.OffPeakHours) == 0 {
return errors.New("usage pattern must have either peak or off-peak hours")
}
// Validate hour formats
allHours := append(conditions.UsagePattern.PeakHours, conditions.UsagePattern.OffPeakHours...)
for _, hour := range allHours {
if !v.isValidHourRange(hour) {
return fmt.Errorf("invalid hour range: %s", hour)
}
}
}
return nil
}
// ValidateActions validates rule actions
func (v *PricingValidator) ValidateActions(ctx context.Context, actions pricing.RuleActions) error {
if actions.AdjustmentType == "" {
return errors.New("adjustment type is required")
}
// Validate adjustment type
validTypes := map[pricing.AdjustmentType]bool{
pricing.AdjustmentTypePercentage: true,
pricing.AdjustmentTypeFixed: true,
pricing.AdjustmentTypeMultiply: true,
pricing.AdjustmentTypeOverride: true,
}
if !validTypes[actions.AdjustmentType] {
return fmt.Errorf("invalid adjustment type: %s", actions.AdjustmentType)
}
// Validate value based on adjustment type
switch actions.AdjustmentType {
case pricing.AdjustmentTypePercentage:
if actions.Value < 0 || actions.Value > 100 {
return errors.New("percentage value must be between 0 and 100")
}
case pricing.AdjustmentTypeFixed:
if actions.Value < 0 {
return errors.New("fixed discount value must be non-negative")
}
case pricing.AdjustmentTypeMultiply:
if actions.Value <= 0 {
return errors.New("multiplier value must be positive")
}
case pricing.AdjustmentTypeOverride:
if actions.NewPrice == nil || *actions.NewPrice < 0 {
return errors.New("override action requires a valid new price")
}
}
// Validate limit
if actions.Limit != nil && *actions.Limit < 0 {
return errors.New("limit cannot be negative")
}
return nil
}
// Helper methods
func (v *PricingValidator) isValidHourRange(hourRange string) bool {
// Should be in format "start-end" like "9-17", "18-23", etc.
if len(hourRange) < 3 {
return false
}
for i, char := range hourRange {
if i == 0 && !v.isDigit(char) {
return false
}
if i == len(hourRange)-1 && !v.isDigit(char) {
return false
}
if i == 1 && char != '-' {
return false
}
if i > 1 && i < len(hourRange)-1 && !v.isDigit(char) {
return false
}
}
return true
}
func (v *PricingValidator) isDigit(char rune) bool {
return char >= '0' && char <= '9'
}