mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-28 07:32:15 +00:00
- 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
213 lines
5.5 KiB
Go
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
|
|
}
|