mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-26 14:42:15 +00:00
- 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
241 lines
6.0 KiB
Go
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'
|
|
}
|