Merge branch 'master' into shared_balances

Conflicts:
	engine/balances.go
	utils/consts.go
This commit is contained in:
Radu Ioan Fericean
2014-02-07 22:32:03 +02:00
8 changed files with 227 additions and 151 deletions

View File

@@ -138,56 +138,50 @@ func (b *Balance) DebitMinutes(cc *CallCost, count bool, ub *UserBalance, moneyB
if increment.paid {
continue
}
if b.RateSubject == ZEROSECOND || b.RateSubject == "" {
amount := increment.Duration.Seconds()
if b.Value >= amount {
b.Value -= amount
increment.BalanceInfo.MinuteBalanceUuid = b.Uuid
increment.MinuteInfo = &MinuteInfo{cc.Destination, amount}
increment.Cost = 0
increment.paid = true
if count {
ub.countUnits(&Action{BalanceId: MINUTES, Direction: cc.Direction, Balance: &Balance{Value: amount, DestinationId: cc.Destination}})
}
if duration, err := utils.ParseZeroRatingSubject(b.RateSubject); err == nil {
seconds := duration.Seconds()
amount := seconds
if seconds == 1 {
amount = increment.Duration.Seconds()
}
continue
}
if b.RateSubject == ZEROMINUTE {
amount := time.Minute.Seconds()
if b.Value >= amount { // balance has at least 60 seconds
newTs := ts
if incrementIndex != 0 {
// if increment it's not at the begining we must split the timespan
newTs = ts.SplitByIncrement(incrementIndex)
}
newTs.RoundToDuration(time.Minute)
newTs.RateInterval = &RateInterval{
Rating: &RIRate{
Rates: RateGroups{
&Rate{
GroupIntervalStart: 0,
Value: 0,
RateIncrement: time.Minute,
RateUnit: time.Minute,
inc := increment
if seconds > 1 { // we need to recreate increments
if incrementIndex != 0 {
// if increment it's not at the begining we must split the timespan
newTs = ts.SplitByIncrement(incrementIndex)
}
newTs.RoundToDuration(time.Minute)
newTs.RateInterval = &RateInterval{
Rating: &RIRate{
Rates: RateGroups{
&Rate{
GroupIntervalStart: 0,
Value: 0,
RateIncrement: time.Minute,
RateUnit: time.Minute,
},
},
},
},
}
newTs.createIncrementsSlice()
// insert the new timespan
if newTs != ts {
tsIndex++
cc.Timespans = append(cc.Timespans, nil)
copy(cc.Timespans[tsIndex+1:], cc.Timespans[tsIndex:])
cc.Timespans[tsIndex] = newTs
tsWasSplit = true
}
cc.Timespans.RemoveOverlapedFromIndex(tsIndex)
inc = newTs.Increments[0]
}
newTs.createIncrementsSlice()
// insert the new timespan
if newTs != ts {
tsIndex++
cc.Timespans = append(cc.Timespans, nil)
copy(cc.Timespans[tsIndex+1:], cc.Timespans[tsIndex:])
cc.Timespans[tsIndex] = newTs
tsWasSplit = true
}
cc.Timespans.RemoveOverlapedFromIndex(tsIndex)
b.Value -= amount
newTs.Increments[0].BalanceInfo.MinuteBalanceUuid = b.Uuid
newTs.Increments[0].MinuteInfo = &MinuteInfo{cc.Destination, amount}
newTs.Increments[0].Cost = 0
newTs.Increments[0].paid = true
inc.BalanceInfo.MinuteBalanceUuid = b.Uuid
inc.MinuteInfo = &MinuteInfo{cc.Destination, amount}
inc.Cost = 0
inc.paid = true
if count {
ub.countUnits(&Action{BalanceId: MINUTES, Direction: cc.Direction, Balance: &Balance{Value: amount, DestinationId: cc.Destination}})
}

View File

@@ -21,6 +21,7 @@ package engine
import (
"errors"
"fmt"
"log"
"log/syslog"
"time"
//"encoding/json"
@@ -420,7 +421,6 @@ func (cd *CallDescriptor) GetCost() (*CallCost, error) {
/*
Returns the approximate max allowed session for user balance. It will try the max amount received in the call descriptor
and will decrease it by 10% for nine times. So if the user has little credit it will still allow 10% of the initial amount.
If the user has no credit then it will return 0.
If the user has postpayed plan it returns -1.
*/
@@ -449,15 +449,15 @@ func (origCd *CallDescriptor) GetMaxSessionDuration() (time.Duration, error) {
return 0, err
}
//Logger.Debug(fmt.Sprintf("availableDuration: %v, availableCredit: %v", availableDuration, availableCredit))
// check for zero balance
if availableCredit == 0 {
return availableDuration, nil
}
initialDuration := cd.TimeEnd.Sub(cd.TimeStart)
if initialDuration <= availableDuration {
// there are enough minutes for requested interval
return initialDuration, nil
}
// check for zero balance
if availableCredit == 0 {
return utils.MinDuration(initialDuration, availableDuration), nil
}
//Logger.Debug(fmt.Sprintf("initial Duration: %v", initialDuration))
// we must move the timestart for the interval with the available duration because
// that was already checked
@@ -482,7 +482,11 @@ func (origCd *CallDescriptor) GetMaxSessionDuration() (time.Duration, error) {
}
}
}
return availableDuration, nil
log.Print(initialDuration, availableDuration, initialDuration < availableDuration)
if initialDuration < availableDuration {
return initialDuration, nil
}
return utils.MinDuration(initialDuration, availableDuration), nil
}
// Interface method used to add/substract an amount of cents or bonus seconds (as returned by GetCost method)

View File

@@ -296,7 +296,7 @@ func TestMaxSessionTimeWithUserBalance(t *testing.T) {
Destination: "0723",
Amount: 1000}
result, err := cd.GetMaxSessionDuration()
expected := 300 * time.Second
expected := time.Minute
if result != expected || err != nil {
t.Errorf("Expected %v was %v", expected, result)
}
@@ -314,7 +314,7 @@ func TestMaxSessionTimeWithUserBalanceAccount(t *testing.T) {
Destination: "0723",
Amount: 1000}
result, err := cd.GetMaxSessionDuration()
expected := 300 * time.Second
expected := time.Minute
if result != expected || err != nil {
t.Errorf("Expected %v was %v", expected, result)
}
@@ -331,8 +331,8 @@ func TestMaxSessionTimeNoCredit(t *testing.T) {
Destination: "0723",
Amount: 5400}
result, err := cd.GetMaxSessionDuration()
if result != 100*time.Second || err != nil {
t.Errorf("Expected %v was %v", 100, result)
if result != time.Minute || err != nil {
t.Errorf("Expected %v was %v", time.Minute, result)
}
}
@@ -343,7 +343,8 @@ func TestMaxSessionModifiesCallDesc(t *testing.T) {
Direction: "*out",
TOR: "0",
Tenant: "vdf",
Subject: "broker",
Subject: "minu_from_tm",
Account: "minu",
Destination: "0723",
Amount: 5400}
initial := cd.Clone()
@@ -353,6 +354,46 @@ func TestMaxSessionModifiesCallDesc(t *testing.T) {
}
}
func TestMaxDebitDurationNoGreatherThanInitialDuration(t *testing.T) {
cd := &CallDescriptor{
TimeStart: time.Date(2013, 10, 21, 18, 34, 0, 0, time.UTC),
TimeEnd: time.Date(2013, 10, 21, 18, 35, 0, 0, time.UTC),
Direction: "*out",
TOR: "0",
Tenant: "vdf",
Subject: "minu_from_tm",
Account: "minu",
Destination: "0723",
Amount: 1000}
initialDuration := cd.TimeEnd.Sub(cd.TimeStart)
result, _ := cd.GetMaxSessionDuration()
if result > initialDuration {
t.Error("max session duration greather than initial duration", initialDuration, result)
}
}
func TestDebitAndMaxDebit(t *testing.T) {
cd1 := &CallDescriptor{
TimeStart: time.Date(2013, 10, 21, 18, 34, 0, 0, time.UTC),
TimeEnd: time.Date(2013, 10, 21, 18, 35, 0, 0, time.UTC),
Direction: "*out",
TOR: "0",
Tenant: "vdf",
Subject: "minu_from_tm",
Account: "minu",
Destination: "0723",
Amount: 5400}
cd2 := cd1.Clone()
cc1, err1 := cd1.Debit()
cc2, err2 := cd2.MaxDebit()
if err1 != nil || err2 != nil {
t.Error("Error debiting and/or maxdebiting: ", err1, err2)
}
if !reflect.DeepEqual(cc1, cc2) {
t.Errorf("Debit and MaxDebit differ: %+v != %+v", cc1, cc2)
}
}
/*********************************** BENCHMARKS ***************************************/
func BenchmarkStorageGetting(b *testing.B) {
b.StopTimer()

View File

@@ -49,9 +49,6 @@ const (
TRIGGER_MAX_COUNTER = "*max_counter"
TRIGGER_MIN_BALANCE = "*min_balance"
TRIGGER_MAX_BALANCE = "*max_balance"
// minute subjects
ZEROSECOND = "*zerosecond"
ZEROMINUTE = "*zerominute"
)
var (

View File

@@ -214,7 +214,7 @@ func TestDebitNegativeMoneyBalance(t *testing.T) {
}
func TestDebitCreditZeroSecond(t *testing.T) {
b1 := &Balance{Uuid: "testb", Value: 10, Weight: 10, DestinationId: "NAT", RateSubject: ZEROSECOND}
b1 := &Balance{Uuid: "testb", Value: 10, Weight: 10, DestinationId: "NAT", RateSubject: "*zero1s"}
cc := &CallCost{
Direction: OUTBOUND,
Destination: "0723045326",
@@ -243,7 +243,7 @@ func TestDebitCreditZeroSecond(t *testing.T) {
}
func TestDebitCreditZeroMinute(t *testing.T) {
b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationId: "NAT", RateSubject: ZEROMINUTE}
b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationId: "NAT", RateSubject: "*zero1m"}
cc := &CallCost{
Direction: OUTBOUND,
Destination: "0723045326",
@@ -276,8 +276,8 @@ func TestDebitCreditZeroMinute(t *testing.T) {
}
}
func TestDebitCreditZeroMixedMinute(t *testing.T) {
b1 := &Balance{Uuid: "testm", Value: 70, Weight: 5, DestinationId: "NAT", RateSubject: ZEROMINUTE}
b2 := &Balance{Uuid: "tests", Value: 10, Weight: 10, DestinationId: "NAT", RateSubject: ZEROSECOND}
b1 := &Balance{Uuid: "testm", Value: 70, Weight: 5, DestinationId: "NAT", RateSubject: "*zero1m"}
b2 := &Balance{Uuid: "tests", Value: 10, Weight: 10, DestinationId: "NAT", RateSubject: "*zero1s"}
cc := &CallCost{
Direction: OUTBOUND,
Destination: "0723045326",
@@ -311,7 +311,7 @@ func TestDebitCreditZeroMixedMinute(t *testing.T) {
}
func TestDebitCreditNoCredit(t *testing.T) {
b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationId: "NAT", RateSubject: ZEROMINUTE}
b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationId: "NAT", RateSubject: "*zero1m"}
cc := &CallCost{
Direction: OUTBOUND,
Destination: "0723045326",
@@ -351,7 +351,7 @@ func TestDebitCreditNoCredit(t *testing.T) {
}
func TestDebitCreditHasCredit(t *testing.T) {
b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationId: "NAT", RateSubject: ZEROMINUTE}
b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationId: "NAT", RateSubject: "*zero1m"}
cc := &CallCost{
Direction: OUTBOUND,
Destination: "0723045326",
@@ -393,7 +393,7 @@ func TestDebitCreditHasCredit(t *testing.T) {
}
func TestDebitCreditSplitMinutesMoney(t *testing.T) {
b1 := &Balance{Uuid: "testb", Value: 10, Weight: 10, DestinationId: "NAT", RateSubject: ZEROSECOND}
b1 := &Balance{Uuid: "testb", Value: 10, Weight: 10, DestinationId: "NAT", RateSubject: "*zero1s"}
cc := &CallCost{
Direction: OUTBOUND,
Destination: "0723045326",
@@ -430,7 +430,7 @@ func TestDebitCreditSplitMinutesMoney(t *testing.T) {
}
func TestDebitCreditMoreTimespans(t *testing.T) {
b1 := &Balance{Uuid: "testb", Value: 150, Weight: 10, DestinationId: "NAT", RateSubject: ZEROMINUTE}
b1 := &Balance{Uuid: "testb", Value: 150, Weight: 10, DestinationId: "NAT", RateSubject: "*zero1m"}
cc := &CallCost{
Direction: OUTBOUND,
Destination: "0723045326",
@@ -467,8 +467,8 @@ func TestDebitCreditMoreTimespans(t *testing.T) {
}
func TestDebitCreditMoreTimespansMixed(t *testing.T) {
b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationId: "NAT", RateSubject: ZEROMINUTE}
b2 := &Balance{Uuid: "testa", Value: 150, Weight: 5, DestinationId: "NAT", RateSubject: ZEROSECOND}
b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationId: "NAT", RateSubject: "*zero1m"}
b2 := &Balance{Uuid: "testa", Value: 150, Weight: 5, DestinationId: "NAT", RateSubject: "*zero1s"}
cc := &CallCost{
Direction: OUTBOUND,
Destination: "0723045326",
@@ -506,7 +506,7 @@ func TestDebitCreditMoreTimespansMixed(t *testing.T) {
}
func TestDebitCreditNoConectFeeCredit(t *testing.T) {
b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationId: "NAT", RateSubject: ZEROMINUTE}
b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationId: "NAT", RateSubject: "*zero1m"}
cc := &CallCost{
Direction: OUTBOUND,
Destination: "0723045326",

View File

@@ -1,89 +1,90 @@
package utils
const (
VERSION = "0.9.1c4"
POSTGRES = "postgres"
MYSQL = "mysql"
MONGO = "mongo"
REDIS = "redis"
LOCALHOST = "127.0.0.1"
FSCDR_FILE_CSV = "freeswitch_file_csv"
FSCDR_HTTP_JSON = "freeswitch_http_json"
NOT_IMPLEMENTED = "not implemented"
PREPAID = "prepaid"
POSTPAID = "postpaid"
PSEUDOPREPAID = "pseudoprepaid"
RATED = "rated"
ERR_NOT_IMPLEMENTED = "NOT_IMPLEMENTED"
ERR_SERVER_ERROR = "SERVER_ERROR"
ERR_NOT_FOUND = "NOT_FOUND"
ERR_MANDATORY_IE_MISSING = "MANDATORY_IE_MISSING"
ERR_EXISTS = "EXISTS"
ERR_BROKEN_REFERENCE = "BROKEN_REFERENCE"
TBL_TP_TIMINGS = "tp_timings"
TBL_TP_DESTINATIONS = "tp_destinations"
TBL_TP_RATES = "tp_rates"
TBL_TP_DESTINATION_RATES = "tp_destination_rates"
TBL_TP_RATING_PLANS = "tp_rating_plans"
TBL_TP_RATE_PROFILES = "tp_rating_profiles"
TBL_TP_ACTIONS = "tp_actions"
TBL_TP_ACTION_PLANS = "tp_action_plans"
TBL_TP_ACTION_TRIGGERS = "tp_action_triggers"
TBL_TP_ACCOUNT_ACTIONS = "tp_account_actions"
TBL_CDRS_PRIMARY = "cdrs_primary"
TBL_CDRS_EXTRA = "cdrs_extra"
TBL_COST_DETAILS = "cost_details"
TBL_RATED_CDRS = "rated_cdrs"
TIMINGS_CSV = "Timings.csv"
DESTINATIONS_CSV = "Destinations.csv"
RATES_CSV = "Rates.csv"
DESTINATION_RATES_CSV = "DestinationRates.csv"
RATING_PLANS_CSV = "RatingPlans.csv"
RATING_PROFILES_CSV = "RatingProfiles.csv"
SHARED_GROUPS_CSV = "SharedGroups.csv"
ACTIONS_CSV = "Actions.csv"
ACTION_PLANS_CSV = "ActionPlans.csv"
ACTION_TRIGGERS_CSV = "ActionTriggers.csv"
ACCOUNT_ACTIONS_CSV = "AccountActions.csv"
TIMINGS_NRCOLS = 6
DESTINATIONS_NRCOLS = 2
RATES_NRCOLS = 8
DESTINATION_RATES_NRCOLS = 3
DESTRATE_TIMINGS_NRCOLS = 4
RATE_PROFILES_NRCOLS = 7
SHARED_GROUPS_NRCOLS = 5
ACTIONS_NRCOLS = 12
ACTION_PLANS_NRCOLS = 4
ACTION_TRIGGERS_NRCOLS = 8
ACCOUNT_ACTIONS_NRCOLS = 5
ROUNDING_UP = "*up"
ROUNDING_MIDDLE = "*middle"
ROUNDING_DOWN = "*down"
ANY = "*any"
COMMENT_CHAR = '#'
CSV_SEP = ','
FALLBACK_SEP = ';'
JSON = "json"
MSGPACK = "msgpack"
CSV_LOAD = "CSVLOAD"
CGRID = "cgrid"
ACCID = "accid"
CDRHOST = "cdrhost"
CDRSOURCE = "cdrsource"
REQTYPE = "reqtype"
DIRECTION = "direction"
TENANT = "tenant"
TOR = "tor"
ACCOUNT = "account"
SUBJECT = "subject"
DESTINATION = "destination"
ANSWER_TIME = "answer_time"
DURATION = "duration"
DEFAULT_RUNID = "default"
STATIC_VALUE_PREFIX = "^"
CDRE_CSV = "csv"
CDRE_DRYRUN = "dry_run"
INTERNAL = "internal"
VERSION = "0.9.1c4"
POSTGRES = "postgres"
MYSQL = "mysql"
MONGO = "mongo"
REDIS = "redis"
LOCALHOST = "127.0.0.1"
FSCDR_FILE_CSV = "freeswitch_file_csv"
FSCDR_HTTP_JSON = "freeswitch_http_json"
NOT_IMPLEMENTED = "not implemented"
PREPAID = "prepaid"
POSTPAID = "postpaid"
PSEUDOPREPAID = "pseudoprepaid"
RATED = "rated"
ERR_NOT_IMPLEMENTED = "NOT_IMPLEMENTED"
ERR_SERVER_ERROR = "SERVER_ERROR"
ERR_NOT_FOUND = "NOT_FOUND"
ERR_MANDATORY_IE_MISSING = "MANDATORY_IE_MISSING"
ERR_EXISTS = "EXISTS"
ERR_BROKEN_REFERENCE = "BROKEN_REFERENCE"
TBL_TP_TIMINGS = "tp_timings"
TBL_TP_DESTINATIONS = "tp_destinations"
TBL_TP_RATES = "tp_rates"
TBL_TP_DESTINATION_RATES = "tp_destination_rates"
TBL_TP_RATING_PLANS = "tp_rating_plans"
TBL_TP_RATE_PROFILES = "tp_rating_profiles"
TBL_TP_ACTIONS = "tp_actions"
TBL_TP_ACTION_PLANS = "tp_action_plans"
TBL_TP_ACTION_TRIGGERS = "tp_action_triggers"
TBL_TP_ACCOUNT_ACTIONS = "tp_account_actions"
TBL_CDRS_PRIMARY = "cdrs_primary"
TBL_CDRS_EXTRA = "cdrs_extra"
TBL_COST_DETAILS = "cost_details"
TBL_RATED_CDRS = "rated_cdrs"
TIMINGS_CSV = "Timings.csv"
DESTINATIONS_CSV = "Destinations.csv"
RATES_CSV = "Rates.csv"
DESTINATION_RATES_CSV = "DestinationRates.csv"
RATING_PLANS_CSV = "RatingPlans.csv"
RATING_PROFILES_CSV = "RatingProfiles.csv"
SHARED_GROUPS_CSV = "SharedGroups.csv"
ACTIONS_CSV = "Actions.csv"
ACTION_PLANS_CSV = "ActionPlans.csv"
ACTION_TRIGGERS_CSV = "ActionTriggers.csv"
ACCOUNT_ACTIONS_CSV = "AccountActions.csv"
TIMINGS_NRCOLS = 6
DESTINATIONS_NRCOLS = 2
RATES_NRCOLS = 8
DESTINATION_RATES_NRCOLS = 3
DESTRATE_TIMINGS_NRCOLS = 4
RATE_PROFILES_NRCOLS = 7
SHARED_GROUPS_NRCOLS = 5
ACTIONS_NRCOLS = 12
ACTION_PLANS_NRCOLS = 4
ACTION_TRIGGERS_NRCOLS = 8
ACCOUNT_ACTIONS_NRCOLS = 5
ROUNDING_UP = "*up"
ROUNDING_MIDDLE = "*middle"
ROUNDING_DOWN = "*down"
ANY = "*any"
COMMENT_CHAR = '#'
CSV_SEP = ','
FALLBACK_SEP = ';'
JSON = "json"
MSGPACK = "msgpack"
CSV_LOAD = "CSVLOAD"
CGRID = "cgrid"
ACCID = "accid"
CDRHOST = "cdrhost"
CDRSOURCE = "cdrsource"
REQTYPE = "reqtype"
DIRECTION = "direction"
TENANT = "tenant"
TOR = "tor"
ACCOUNT = "account"
SUBJECT = "subject"
DESTINATION = "destination"
ANSWER_TIME = "answer_time"
DURATION = "duration"
DEFAULT_RUNID = "default"
STATIC_VALUE_PREFIX = "^"
CDRE_CSV = "csv"
CDRE_DRYRUN = "dry_run"
INTERNAL = "internal"
ZERO_RATING_SUBJECT_PREFIX = "*zero"
)
var (

View File

@@ -187,3 +187,22 @@ func ParseDurationWithSecs(durStr string) (time.Duration, error) {
func BalanceKey(tenant, account, direction string) string {
return fmt.Sprintf("%s:%s:%s", direction, tenant, account)
}
// returns the minimum duration between the two
func MinDuration(d1, d2 time.Duration) time.Duration {
if d1 < d2 {
return d1
}
return d2
}
func ParseZeroRatingSubject(rateSubj string) (time.Duration, error) {
if rateSubj == "" {
rateSubj = ZERO_RATING_SUBJECT_PREFIX + "1s"
}
if !strings.HasPrefix(rateSubj, ZERO_RATING_SUBJECT_PREFIX) {
return 0, errors.New("malformed rating subject: " + rateSubj)
}
durStr := rateSubj[len(ZERO_RATING_SUBJECT_PREFIX):]
return time.ParseDuration(durStr)
}

View File

@@ -336,3 +336,23 @@ func TestParseDurationWithSecs(t *testing.T) {
t.Error("Parsed different than expected")
}
}
func TestMinDuration(t *testing.T) {
d1, _ := time.ParseDuration("1m")
d2, _ := time.ParseDuration("59s")
minD1 := MinDuration(d1, d2)
minD2 := MinDuration(d2, d1)
if minD1 != d2 || minD2 != d2 {
t.Error("Error getting min duration: ", minD1, minD2)
}
}
func TestParseZeroRatingSubject(t *testing.T) {
subj := []string{"", "*zero1s", "*zero5m", "*zero10h"}
dur := []time.Duration{time.Second, time.Second, 5 * time.Minute, 10 * time.Hour}
for i, s := range subj {
if d, err := ParseZeroRatingSubject(s); err != nil || d != dur[i] {
t.Error("Error parsing rating subject: ", s, d, err)
}
}
}