Files
telecom-platform/apps/carrier-connector/internal/services/pricing_rule_engine.go
T
nutcas3 d2bd13eff6 feat: Add pricing repository, tenant-aware transaction support, and pricing integration service
- Add GormPricingRepository with CreateRule, GetRule, UpdateRule, DeleteRule, ListRules, CountRules methods
- Add GetActiveRules, GetRulesByType methods for retrieving active pricing rules by tenant and type
- Add TenantScopedDB wrapper with automatic tenant filtering for tenant-aware database operations
- Implement TenantScopedTransaction with tenant context propagation and automatic filtering
2026-05-02 04:44:31 +03:00

213 lines
5.5 KiB
Go

package services
import (
"context"
"fmt"
"strings"
"time"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/pricing"
)
// PricingRuleEngine implements the rule evaluation logic
type PricingRuleEngine struct {
validator pricing.PricingValidator
}
// NewPricingRuleEngine creates a new pricing rule engine
func NewPricingRuleEngine(validator pricing.PricingValidator) pricing.RuleEngine {
return &PricingRuleEngine{
validator: validator,
}
}
// EvaluateRule checks if a rule should be applied to the given context
func (e *PricingRuleEngine) EvaluateRule(ctx context.Context, rule *pricing.PricingRule, pricingCtx *pricing.PricingContext) (bool, error) {
if !rule.IsActive {
return false, nil
}
// Validate conditions
matches, err := e.ValidateConditions(ctx, rule.Conditions, pricingCtx)
if err != nil {
return false, fmt.Errorf("failed to validate conditions: %w", err)
}
return matches, nil
}
// ApplyRule applies a rule to adjust the price
func (e *PricingRuleEngine) ApplyRule(ctx context.Context, rule *pricing.PricingRule, pricingCtx *pricing.PricingContext, currentPrice float64) (float64, error) {
adjustedPrice, err := e.ExecuteActions(ctx, rule.Actions, currentPrice)
if err != nil {
return currentPrice, fmt.Errorf("failed to execute actions: %w", err)
}
return adjustedPrice, nil
}
// ValidateConditions checks if the conditions match the pricing context
func (e *PricingRuleEngine) ValidateConditions(ctx context.Context, conditions pricing.RuleConditions, pricingCtx *pricing.PricingContext) (bool, error) {
// Check time range
if conditions.TimeRange != nil {
if !e.isWithinTimeRange(conditions.TimeRange, pricingCtx.Time) {
return false, nil
}
}
// Check geography
if len(conditions.Geography) > 0 {
if !e.isInGeography(conditions.Geography, pricingCtx.Location) {
return false, nil
}
}
// Check customer type
if len(conditions.CustomerType) > 0 {
customerType, ok := pricingCtx.Metadata["customer_type"].(string)
if !ok || !e.isInCustomerType(conditions.CustomerType, customerType) {
return false, nil
}
}
// Check volume
if conditions.Volume != nil {
if !e.isWithinVolumeRange(conditions.Volume, pricingCtx.Quantity) {
return false, nil
}
}
// Check usage pattern
if conditions.UsagePattern != nil {
if !e.matchesUsagePattern(conditions.UsagePattern, pricingCtx.Time) {
return false, nil
}
}
return true, nil
}
// ExecuteActions applies the pricing adjustments
func (e *PricingRuleEngine) ExecuteActions(ctx context.Context, actions pricing.RuleActions, currentPrice float64) (float64, error) {
switch actions.AdjustmentType {
case pricing.AdjustmentTypePercentage:
adjustedPrice := currentPrice * (1 - actions.Value/100)
if actions.Limit != nil && adjustedPrice < *actions.Limit {
adjustedPrice = *actions.Limit
}
return adjustedPrice, nil
case pricing.AdjustmentTypeFixed:
adjustedPrice := currentPrice - actions.Value
if actions.Limit != nil && adjustedPrice < *actions.Limit {
adjustedPrice = *actions.Limit
}
return adjustedPrice, nil
case pricing.AdjustmentTypeMultiply:
adjustedPrice := currentPrice * actions.Value
if actions.Limit != nil && adjustedPrice > *actions.Limit {
adjustedPrice = *actions.Limit
}
return adjustedPrice, nil
case pricing.AdjustmentTypeOverride:
if actions.NewPrice != nil {
return *actions.NewPrice, nil
}
return currentPrice, fmt.Errorf("override action requires new_price")
default:
return currentPrice, fmt.Errorf("unknown adjustment type: %s", actions.AdjustmentType)
}
}
// Helper methods for condition validation
func (e *PricingRuleEngine) isWithinTimeRange(timeRange *pricing.TimeRange, checkTime time.Time) bool {
if checkTime.Before(timeRange.Start) || checkTime.After(timeRange.End) {
return false
}
if len(timeRange.Days) > 0 {
day := strings.ToLower(checkTime.Weekday().String())
for _, allowedDay := range timeRange.Days {
if strings.ToLower(allowedDay) == day {
return true
}
}
return false
}
return true
}
func (e *PricingRuleEngine) isInGeography(geography []string, location string) bool {
if location == "" {
return false
}
for _, geo := range geography {
if strings.EqualFold(geo, location) {
return true
}
}
return false
}
func (e *PricingRuleEngine) isInCustomerType(customerTypes []string, customerType string) bool {
for _, ct := range customerTypes {
if strings.EqualFold(ct, customerType) {
return true
}
}
return false
}
func (e *PricingRuleEngine) isWithinVolumeRange(volumeRange *pricing.VolumeRange, quantity int) bool {
return quantity >= volumeRange.Min && quantity <= volumeRange.Max
}
func (e *PricingRuleEngine) matchesUsagePattern(pattern *pricing.UsagePattern, checkTime time.Time) bool {
hour := checkTime.Hour()
// Check peak hours
if len(pattern.PeakHours) > 0 {
for _, peakHour := range pattern.PeakHours {
if e.isHourInRange(hour, peakHour) {
return true
}
}
}
// Check off-peak hours
if len(pattern.OffPeakHours) > 0 {
for _, offPeakHour := range pattern.OffPeakHours {
if e.isHourInRange(hour, offPeakHour) {
return true
}
}
}
return false
}
func (e *PricingRuleEngine) isHourInRange(hour int, hourRange string) bool {
// Parse hour range like "9-17", "18-23", etc.
parts := strings.Split(hourRange, "-")
if len(parts) != 2 {
return false
}
start := e.parseHour(parts[0])
end := e.parseHour(parts[1])
return hour >= start && hour <= end
}
func (e *PricingRuleEngine) parseHour(hourStr string) int {
hour := 0
fmt.Sscanf(hourStr, "%d", &hour)
return hour
}