diff --git a/engine/actions_test.go b/engine/actions_test.go index 130a4a730..7c98d3ea9 100644 --- a/engine/actions_test.go +++ b/engine/actions_test.go @@ -656,7 +656,7 @@ func TestActionTopupResetCredit(t *testing.T) { len(ub.UnitCounters) != 1 || len(ub.BalanceMap[MINUTES+OUTBOUND]) != 2 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { - t.Errorf("Topup reset action failed: %#v", ub.BalanceMap[CREDIT+OUTBOUND].GetTotalValue()) + t.Errorf("Topup reset action failed: %+v", ub.BalanceMap[CREDIT+OUTBOUND][0]) } } @@ -678,7 +678,7 @@ func TestActionTopupResetMinutes(t *testing.T) { len(ub.UnitCounters) != 1 || len(ub.BalanceMap[MINUTES+OUTBOUND]) != 1 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { - t.Errorf("Topup reset minutes action failed: %v", ub.BalanceMap[MINUTES+OUTBOUND][0]) + t.Errorf("Topup reset minutes action failed: %+v", ub.BalanceMap[MINUTES+OUTBOUND][0]) } } diff --git a/engine/balances.go b/engine/balances.go index 12387c3c1..2ace5b48a 100644 --- a/engine/balances.go +++ b/engine/balances.go @@ -34,20 +34,29 @@ type Balance struct { ExpirationDate time.Time Weight float64 GroupIds []string - //SpecialPriceType string - //SpecialPrice float64 // absolute for minutes and percent for monetary (can be positive or negative) - DestinationId string - RateSubject string - precision int + DestinationId string + RateSubject string + precision int } func (b *Balance) Equal(o *Balance) bool { + if b.DestinationId == "" { + b.DestinationId = utils.ANY + } + if o.DestinationId == "" { + o.DestinationId = utils.ANY + } return b.ExpirationDate.Equal(o.ExpirationDate) && b.Weight == o.Weight && b.DestinationId == o.DestinationId && b.RateSubject == o.RateSubject } +// the default balance has no destinationid, Expirationdate or ratesubject +func (b *Balance) IsDefault() bool { + return (b.DestinationId == "" || b.DestinationId == utils.ANY) && b.RateSubject == "" && b.ExpirationDate.IsZero() +} + func (b *Balance) IsExpired() bool { return !b.ExpirationDate.IsZero() && b.ExpirationDate.Before(time.Now()) } @@ -100,6 +109,198 @@ func (b *Balance) GetCost(cd *CallDescriptor) (*CallCost, error) { return cc, nil } +func (b *Balance) DebitMinutes(cc *CallCost, count bool, ub *UserBalance, moneyBalances BalanceChain) error { + for tsIndex := 0; tsIndex < len(cc.Timespans); tsIndex++ { + ts := cc.Timespans[tsIndex] + if ts.Increments == nil { + ts.createIncrementsSlice() + } + if paid, _ := ts.IsPaid(); paid { + continue + } + tsWasSplit := false + for incrementIndex, increment := range ts.Increments { + if tsWasSplit { + break + } + if increment.paid { + continue + } + if b.RateSubject == ZEROSECOND || b.RateSubject == "" { + amount := increment.Duration.Seconds() + if b.Value >= amount { + b.Value -= amount + increment.BalanceUuids = append(increment.BalanceUuids, b.Uuid) + increment.MinuteInfo = &MinuteInfo{cc.Destination, amount, 0} + increment.paid = true + if count { + ub.countUnits(&Action{BalanceId: MINUTES, Direction: cc.Direction, Balance: &Balance{Value: amount, DestinationId: cc.Destination}}) + } + } + 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, + }, + }, + }, + } + 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].BalanceUuids = append(newTs.Increments[0].BalanceUuids, b.Uuid) + newTs.Increments[0].MinuteInfo = &MinuteInfo{cc.Destination, amount, 0} + newTs.Increments[0].paid = true + if count { + ub.countUnits(&Action{BalanceId: MINUTES, Direction: cc.Direction, Balance: &Balance{Value: amount, DestinationId: cc.Destination}}) + } + } + continue + } + // get the new rate + cd := cc.CreateCallDescriptor() + cd.Subject = b.RateSubject + cd.TimeStart = ts.GetTimeStartForIncrement(incrementIndex) + cd.TimeEnd = cc.Timespans[len(cc.Timespans)-1].TimeEnd + cd.CallDuration = cc.Timespans[len(cc.Timespans)-1].CallDuration + newCC, err := b.GetCost(cd) + if err != nil { + Logger.Err(fmt.Sprintf("Error getting new cost for balance subject: %v", err)) + continue + } + //debit new callcost + var paidTs []*TimeSpan + for _, nts := range newCC.Timespans { + nts.createIncrementsSlice() + paidTs = append(paidTs, nts) + for _, nInc := range nts.Increments { + // debit minutes and money + seconds := nInc.Duration.Seconds() + cost := nInc.Cost + var moneyBal *Balance + for _, mb := range moneyBalances { + if mb.Value >= cost { + moneyBal = mb + break + } + } + if moneyBal != nil && b.Value >= seconds { + b.Value -= seconds + moneyBal.Value -= cost + nInc.BalanceUuids = append(nInc.BalanceUuids, b.Uuid) + nInc.BalanceUuids = append(nInc.BalanceUuids, moneyBal.Uuid) + nInc.MinuteInfo = &MinuteInfo{newCC.Destination, seconds, 0} + nInc.paid = true + if count { + ub.countUnits(&Action{BalanceId: MINUTES, Direction: newCC.Direction, Balance: &Balance{Value: seconds, DestinationId: newCC.Destination}}) + ub.countUnits(&Action{BalanceId: CREDIT, Direction: newCC.Direction, Balance: &Balance{Value: cost, DestinationId: newCC.Destination}}) + } + } else { + increment.paid = false + break + } + } + } + newTs := ts.SplitByIncrement(incrementIndex) + increment.paid = (&cc.Timespans).OverlapWithTimeSpans(paidTs, newTs, tsIndex) + tsWasSplit = increment.paid + } + } + return nil +} + +func (b *Balance) DebitMoney(cc *CallCost, count bool, ub *UserBalance) error { + for tsIndex := 0; tsIndex < len(cc.Timespans); tsIndex++ { + ts := cc.Timespans[tsIndex] + if ts.Increments == nil { + ts.createIncrementsSlice() + } + if paid, _ := ts.IsPaid(); paid { + continue + } + tsWasSplit := false + for incrementIndex, increment := range ts.Increments { + if tsWasSplit { + break + } + if increment.paid { + continue + } + // check standard subject tags + if b.RateSubject == "" { + amount := increment.Cost + if b.Value >= amount { + b.Value -= amount + increment.BalanceUuids = append(increment.BalanceUuids, b.Uuid) + increment.paid = true + if count { + ub.countUnits(&Action{BalanceId: CREDIT, Direction: cc.Direction, Balance: &Balance{Value: amount, DestinationId: cc.Destination}}) + } + } + } else { + // get the new rate + cd := cc.CreateCallDescriptor() + cd.Subject = b.RateSubject + cd.TimeStart = ts.GetTimeStartForIncrement(incrementIndex) + cd.TimeEnd = cc.Timespans[len(cc.Timespans)-1].TimeEnd + cd.CallDuration = cc.Timespans[len(cc.Timespans)-1].CallDuration + newCC, err := b.GetCost(cd) + if err != nil { + Logger.Err(fmt.Sprintf("Error getting new cost for balance subject: %v", err)) + continue + } + //debit new callcost + var paidTs []*TimeSpan + for _, nts := range newCC.Timespans { + nts.createIncrementsSlice() + paidTs = append(paidTs, nts) + for nIdx, nInc := range nts.Increments { + // debit money + amount := nInc.Cost + if b.Value >= amount { + b.Value -= amount + nInc.BalanceUuids = append(nInc.BalanceUuids, b.Uuid) + nInc.paid = true + if count { + ub.countUnits(&Action{BalanceId: CREDIT, Direction: newCC.Direction, Balance: &Balance{Value: amount, DestinationId: newCC.Destination}}) + } + } else { + nts.SplitByIncrement(nIdx) + } + } + } + newTs := ts.SplitByIncrement(incrementIndex) + increment.paid = (&cc.Timespans).OverlapWithTimeSpans(paidTs, newTs, tsIndex) + tsWasSplit = increment.paid + } + } + } + return nil +} + /* Structure to store minute buckets according to weight, precision or price. */ diff --git a/engine/calldesc.go b/engine/calldesc.go index 777324923..0241e365c 100644 --- a/engine/calldesc.go +++ b/engine/calldesc.go @@ -347,9 +347,6 @@ func (cd *CallDescriptor) splitInTimeSpans(firstSpan *TimeSpan) (timespans []*Ti func (cd *CallDescriptor) roundTimeSpansToIncrement(timespans TimeSpans) []*TimeSpan { for i := 0; i < len(timespans); i++ { ts := timespans[i] - if ts.overlapped { - continue - } if ts.RateInterval != nil { _, rateIncrement, _ := ts.RateInterval.GetRateParameters(ts.GetGroupStart()) // if the timespan duration is larger than the rate increment make sure it is a multiple of it diff --git a/engine/realcalls_test.go b/engine/realcalls_test.go index e0375ed83..a9afe57e3 100644 --- a/engine/realcalls_test.go +++ b/engine/realcalls_test.go @@ -49,7 +49,7 @@ var balanceInsufficient = `{"Id":"*out:192.168.56.66:dan","Type":"*prepaid","Bal var costInsufficient = `{"Direction":"*out","TOR":"call","Tenant":"192.168.56.66","Subject":"dan","Account":"dan","Destination":"+4986517174963","Cost":1,"ConnectFee":3,"Timespans":[{"TimeStart":"2013-12-05T09:52:17+01:00","TimeEnd":"2013-12-05T09:53:17+01:00","Cost":1,"RateInterval":{"Timing":{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[],"StartTime":"00:00:00","EndTime":""},"Rating":{"ConnectFee":3,"Rates":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":60000000000,"RateUnit":60000000000}],"RoundingMethod":"*up","RoundingDecimals":2},"Weight":10},"CallDuration":60000000000,"Increments":null,"MatchedSubject":"*out:192.168.56.66:call:*any","MatchedPrefix":"+49"}]}` -func FIXMETestDebitInsufficientBalance(t *testing.T) { +func TestDebitInsufficientBalance(t *testing.T) { b1 := new(UserBalance) if err := json.Unmarshal([]byte(balanceInsufficient), b1); err != nil { t.Error("Error restoring balance1: ", err) @@ -59,10 +59,11 @@ func FIXMETestDebitInsufficientBalance(t *testing.T) { t.Error("Error restoring callCost1: ", err) } err := b1.debitCreditBalance(cc1, false) - if err != nil { - t.Error("Error debiting balance: ", err) + if err == nil { + t.Error("Error showing debiting balance error: ", err) } - if b1.BalanceMap[CREDIT+OUTBOUND][0].Value != -3 { - t.Error("Error debiting from balance: ", b1.BalanceMap[CREDIT+OUTBOUND][0]) + if b1.BalanceMap[CREDIT+OUTBOUND].GetTotalValue() != -3 { + t.Logf("CC: %+v", cc1.Cost) + t.Errorf("Error debiting from balance: %+v", b1.BalanceMap[CREDIT+OUTBOUND]) } } diff --git a/engine/timespans.go b/engine/timespans.go index a63fd3ac7..7fed9c782 100644 --- a/engine/timespans.go +++ b/engine/timespans.go @@ -36,7 +36,6 @@ type TimeSpan struct { RateInterval *RateInterval CallDuration time.Duration // the call duration so far till TimeEnd Increments Increments - overlapped bool MatchedSubject, MatchedPrefix string } @@ -46,6 +45,7 @@ type Increment struct { BalanceUuids []string // need more than one for minutes with cost BalanceRateInterval *RateInterval MinuteInfo *MinuteInfo + paid bool } // Holds the minute information related to a specified timespan @@ -77,7 +77,9 @@ func (timespans *TimeSpans) RemoveOverlapedFromIndex(index int) { tss[i] = nil } *timespans = tss[:newSliceEnd] + return } + *timespans = tss } // The paidTs will replace the timespans that are exactly under them from the reciver list @@ -208,6 +210,21 @@ func (ts *TimeSpan) createIncrementsSlice() { ts.Increments = append(ts.Increments, &Increment{Duration: rateIncrement, Cost: incrementCost}) totalCost += incrementCost } + ts.Cost = totalCost +} + +// returns whether the timespan has all increments marked as paid and if not +// it also returns the first unpaied increment +func (ts *TimeSpan) IsPaid() (bool, int) { + if len(ts.Increments) == 0 { + return false, 0 + } + for incrementIndex, increment := range ts.Increments { + if !increment.paid { + return false, incrementIndex + } + } + return true, len(ts.Increments) } /* @@ -218,11 +235,9 @@ The interval will attach itself to the timespan that overlaps the interval. */ func (ts *TimeSpan) SplitByRateInterval(i *RateInterval) (nts *TimeSpan) { //Logger.Debug("here: ", ts, " +++ ", i) - //log.Printf("TS: %+v", ts) // if the span is not in interval return nil if !(i.Contains(ts.TimeStart, false) || i.Contains(ts.TimeEnd, true)) { //Logger.Debug("Not in interval") - //log.Printf("NOT in interval: %+v", i) return } //Logger.Debug(fmt.Sprintf("TS: %+v", ts)) @@ -235,7 +250,6 @@ func (ts *TimeSpan) SplitByRateInterval(i *RateInterval) (nts *TimeSpan) { // Logger.Debug(fmt.Sprintf("Splitting")) ts.SetRateInterval(i) splitTime := ts.TimeStart.Add(rate.GroupIntervalStart - ts.GetGroupStart()) - //log.Print("SPLIT: ", splitTime) nts = &TimeSpan{ TimeStart: splitTime, TimeEnd: ts.TimeEnd, @@ -246,12 +260,10 @@ func (ts *TimeSpan) SplitByRateInterval(i *RateInterval) (nts *TimeSpan) { nts.CallDuration = ts.CallDuration ts.SetNewCallDuration(nts) // Logger.Debug(fmt.Sprintf("Group splitting: %+v %+v", ts, nts)) - //log.Printf("Group splitting: %+v %+v", ts, nts) return } } } - //log.Printf("*************TS: %+v", ts) // if the span is enclosed in the interval try to set as new interval and return nil if i.Contains(ts.TimeStart, false) && i.Contains(ts.TimeEnd, true) { //Logger.Debug("All in interval") @@ -276,7 +288,6 @@ func (ts *TimeSpan) SplitByRateInterval(i *RateInterval) (nts *TimeSpan) { nts.CallDuration = ts.CallDuration ts.SetNewCallDuration(nts) // Logger.Debug(fmt.Sprintf("right: %+v %+v", ts, nts)) - //log.Printf("right: %+v %+v", ts, nts) return } // if only the end time is in the interval split the interval to the left @@ -299,7 +310,6 @@ func (ts *TimeSpan) SplitByRateInterval(i *RateInterval) (nts *TimeSpan) { nts.CallDuration = ts.CallDuration ts.SetNewCallDuration(nts) // Logger.Debug(fmt.Sprintf("left: %+v %+v", ts, nts)) - //log.Printf("left: %+v %+v", ts, nts) return } return @@ -374,7 +384,6 @@ func (ts *TimeSpan) SplitByRatingPlan(rp *RatingInfo) (newTs *TimeSpan) { ts.TimeEnd = rp.ActivationTime ts.SetNewCallDuration(newTs) // Logger.Debug(fmt.Sprintf("RP SPLITTING: %+v %+v", ts, newTs)) - //log.Printf("RP SPLITTING: %+v %+v", ts, newTs) return } diff --git a/engine/timespans_test.go b/engine/timespans_test.go index 5a002b9e3..631aa57ed 100644 --- a/engine/timespans_test.go +++ b/engine/timespans_test.go @@ -561,7 +561,7 @@ func TestTimespanExpandingRoundingPastEnd(t *testing.T) { cd := &CallDescriptor{} timespans = cd.roundTimeSpansToIncrement(timespans) if len(timespans) != 2 { - t.Error("Error removing overlaped intervals: ", timespans) + t.Error("Error removing overlaped intervals: ", timespans[0]) } if !timespans[0].TimeEnd.Equal(time.Date(2013, 9, 10, 14, 30, 30, 0, time.UTC)) { t.Error("Error expanding timespan: ", timespans[0]) @@ -1456,3 +1456,27 @@ func TestOverlapWithTimeSpansAllPast(t *testing.T) { t.Error("Error overlaping with timespans timespans: ", tss) } } + +func TestOverlapWithTimeSpansOne(t *testing.T) { + tss := TimeSpans{ + &TimeSpan{ + TimeStart: time.Date(2013, 12, 5, 15, 45, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 12, 5, 15, 49, 0, 0, time.UTC), + }, + } + newTss := TimeSpans{ + &TimeSpan{ + TimeStart: time.Date(2013, 12, 5, 15, 45, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 12, 5, 15, 47, 30, 0, time.UTC), + }, + } + (&tss).OverlapWithTimeSpans(newTss, nil, 0) + if len(tss) != 2 || + tss[0].TimeEnd != time.Date(2013, 12, 5, 15, 47, 30, 0, time.UTC) || + tss[1].TimeEnd != time.Date(2013, 12, 5, 15, 49, 0, 0, time.UTC) { + for _, ts := range tss { + t.Logf("TS: %v", ts) + } + t.Error("Error overlaping with timespans timespans: ", tss) + } +} diff --git a/engine/userbalance.go b/engine/userbalance.go index 942a57ec9..ce1285e86 100644 --- a/engine/userbalance.go +++ b/engine/userbalance.go @@ -21,12 +21,10 @@ package engine import ( "errors" "fmt" - "log" "github.com/cgrates/cgrates/utils" "strings" - "time" ) const ( @@ -152,14 +150,9 @@ func (ub *UserBalance) getBalancesForPrefix(prefix string, balances BalanceChain return usefulBalances } -/* -This method is the core of userbalance debiting: don't panic just follow the branches -*/ func (ub *UserBalance) debitCreditBalance(cc *CallCost, count bool) error { - minuteBalances := ub.BalanceMap[MINUTES+cc.Direction] - moneyBalances := ub.BalanceMap[CREDIT+cc.Direction] - usefulMinuteBalances := ub.getBalancesForPrefix(cc.Destination, minuteBalances) - usefulMoneyBalances := ub.getBalancesForPrefix(cc.Destination, moneyBalances) + usefulMinuteBalances := ub.getBalancesForPrefix(cc.Destination, ub.BalanceMap[MINUTES+cc.Direction]) + usefulMoneyBalances := ub.getBalancesForPrefix(cc.Destination, ub.BalanceMap[CREDIT+cc.Direction]) // debit connect fee if cc.ConnectFee > 0 { amount := cc.ConnectFee @@ -176,218 +169,84 @@ func (ub *UserBalance) debitCreditBalance(cc *CallCost, count bool) error { } } if !paid { - // there are no money for the connect fee; abort mission - cc.Timespans = make([]*TimeSpan, 0) - return nil + // there are no money for the connect fee; go negative + moneyBalance := ub.GetDefaultMoneyBalance(cc.Direction) + moneyBalance.Value -= amount + // the conect fee is not refoundable! + if count { + ub.countUnits(&Action{BalanceId: CREDIT, Direction: cc.Direction, Balance: &Balance{Value: amount, DestinationId: cc.Destination}}) + } } } + // debit minutes + for _, balance := range usefulMinuteBalances { + balance.DebitMinutes(cc, count, ub, usefulMoneyBalances) + } + + allPaidWithMinutes := true for tsIndex := 0; tsIndex < len(cc.Timespans); tsIndex++ { ts := cc.Timespans[tsIndex] - ts.createIncrementsSlice() - tsWasSplit := false - log.Print("TS: ", ts) - for incrementIndex, increment := range ts.Increments { - if tsWasSplit { - break - } - paid := false - // debit minutes - log.Print("Debit minutes") - for _, b := range usefulMinuteBalances { - // check standard subject tags - if b.RateSubject == ZEROSECOND || b.RateSubject == "" { - amount := increment.Duration.Seconds() - if b.Value >= amount { - b.Value -= amount - increment.BalanceUuids = append(increment.BalanceUuids, b.Uuid) - increment.MinuteInfo = &MinuteInfo{cc.Destination, amount, 0} - paid = true - if count { - ub.countUnits(&Action{BalanceId: MINUTES, Direction: cc.Direction, Balance: &Balance{Value: amount, DestinationId: cc.Destination}}) - } - break - } - 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, - }, - }, - }, - } - 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].BalanceUuids = append(newTs.Increments[0].BalanceUuids, b.Uuid) - newTs.Increments[0].MinuteInfo = &MinuteInfo{cc.Destination, amount, 0} - paid = true - if count { - ub.countUnits(&Action{BalanceId: MINUTES, Direction: cc.Direction, Balance: &Balance{Value: amount, DestinationId: cc.Destination}}) - } - break - } - continue - } - - // get the new rate - cd := cc.CreateCallDescriptor() - cd.Subject = b.RateSubject - cd.TimeStart = ts.GetTimeStartForIncrement(incrementIndex) - cd.TimeEnd = cc.Timespans[len(cc.Timespans)-1].TimeEnd - cd.CallDuration = cc.Timespans[len(cc.Timespans)-1].CallDuration - newCC, err := b.GetCost(cd) - if err != nil { - Logger.Err(fmt.Sprintf("Error getting new cost for balance subject: %v", err)) - continue - } - //debit new callcost - var paidTs []*TimeSpan - for _, nts := range newCC.Timespans { - nts.createIncrementsSlice() - log.Printf("XXX: %+v", nts) - paidTs = append(paidTs, nts) - for nIdx, nInc := range nts.Increments { - // debit minutes and money - seconds := nInc.Duration.Seconds() - cost := nInc.Cost - var moneyBal *Balance - for _, mb := range moneyBalances { - if mb.Value >= cost { - mb.Value -= cost - moneyBal = mb - } - } - if moneyBal != nil && b.Value >= seconds { - b.Value -= seconds - nInc.BalanceUuids = append(nInc.BalanceUuids, b.Uuid) - nInc.BalanceUuids = append(nInc.BalanceUuids, moneyBal.Uuid) - nInc.MinuteInfo = &MinuteInfo{newCC.Destination, seconds, 0} - paid = true - if count { - ub.countUnits(&Action{BalanceId: MINUTES, Direction: newCC.Direction, Balance: &Balance{Value: seconds, DestinationId: newCC.Destination}}) - ub.countUnits(&Action{BalanceId: CREDIT, Direction: newCC.Direction, Balance: &Balance{Value: cost, DestinationId: newCC.Destination}}) - } - } else { - paid = false - nts.SplitByIncrement(nIdx) - } - } - } - newTs := ts.SplitByIncrement(incrementIndex) - overlapped := (&cc.Timespans).OverlapWithTimeSpans(paidTs, newTs, tsIndex) - paid, tsWasSplit = overlapped, overlapped - } - if paid { - continue - } else { - // Split if some increments were processed by minutes - if incrementIndex > 0 && ts.Increments[incrementIndex-1].MinuteInfo != nil { - newTs := ts.SplitByIncrement(incrementIndex) - if newTs != nil { - idx := tsIndex + 1 - cc.Timespans = append(cc.Timespans, nil) - copy(cc.Timespans[idx+1:], cc.Timespans[idx:]) - cc.Timespans[idx] = newTs - newTs.createIncrementsSlice() - tsWasSplit = true - } - log.Print("BREAK ON MINUTES") - break - } - } - log.Print("Debit money") - // debit monetary - for _, b := range usefulMoneyBalances { - // check standard subject tags - if b.RateSubject == "" { - amount := increment.Cost - if b.Value >= amount { - b.Value -= amount - increment.BalanceUuids = append(increment.BalanceUuids, b.Uuid) - paid = true - if count { - ub.countUnits(&Action{BalanceId: CREDIT, Direction: cc.Direction, Balance: &Balance{Value: amount, DestinationId: cc.Destination}}) - } - break - } - } else { - // get the new rate - cd := cc.CreateCallDescriptor() - cd.Subject = b.RateSubject - cd.TimeStart = ts.GetTimeStartForIncrement(incrementIndex) - cd.TimeEnd = cc.Timespans[len(cc.Timespans)-1].TimeEnd - cd.CallDuration = cc.Timespans[len(cc.Timespans)-1].CallDuration - newCC, err := b.GetCost(cd) - if err != nil { - Logger.Err(fmt.Sprintf("Error getting new cost for balance subject: %v", err)) - continue - } - //debit new callcost - var paidTs []*TimeSpan - for _, nts := range newCC.Timespans { - nts.createIncrementsSlice() - log.Printf("COST: %+v", nts) - paidTs = append(paidTs, nts) - for nIdx, nInc := range nts.Increments { - // debit money - amount := nInc.Cost - if b.Value >= amount { - b.Value -= amount - nInc.BalanceUuids = append(nInc.BalanceUuids, b.Uuid) - if count { - ub.countUnits(&Action{BalanceId: CREDIT, Direction: newCC.Direction, Balance: &Balance{Value: amount, DestinationId: newCC.Destination}}) - } - } else { - nts.SplitByIncrement(nIdx) - } - } - } - newTs := ts.SplitByIncrement(incrementIndex) - overlapped := (&cc.Timespans).OverlapWithTimeSpans(paidTs, newTs, tsIndex) - paid, tsWasSplit = overlapped, overlapped - } - } - if !paid { - // FIXME: must debit all from the first monetary balance (go negative) - // no balance was attached to this increment: cut the rest of increments/timespans - if incrementIndex == 0 { - // if we are right at the begining in the ts leave it out - cc.Timespans = cc.Timespans[:tsIndex] - } else { - ts.SplitByIncrement(incrementIndex) - cc.Timespans = cc.Timespans[:tsIndex+1] - } - return errors.New("Not enough credit") - //return nil + if paid, incrementIndex := ts.IsPaid(); !paid { + allPaidWithMinutes = false + newTs := ts.SplitByIncrement(incrementIndex) + if newTs != nil { + idx := tsIndex + 1 + cc.Timespans = append(cc.Timespans, nil) + copy(cc.Timespans[idx+1:], cc.Timespans[idx:]) + cc.Timespans[idx] = newTs } } } + if allPaidWithMinutes { + return nil + } + // debit money + for _, balance := range usefulMoneyBalances { + balance.DebitMoney(cc, count, ub) + } + var returnError error + insuficientCreditError := errors.New("not enough credit") + // get the highest priority money balanance + // and go negative on it with the amount still unpaid + moneyBalance := ub.GetDefaultMoneyBalance(cc.Direction) + for tsIndex := 0; tsIndex < len(cc.Timespans); tsIndex++ { + ts := cc.Timespans[tsIndex] + if ts.Increments == nil { + ts.createIncrementsSlice() + } + if paid, incrementIndex := ts.IsPaid(); !paid { + newTs := ts.SplitByIncrement(incrementIndex) + if newTs != nil { + idx := tsIndex + 1 + cc.Timespans = append(cc.Timespans, nil) + copy(cc.Timespans[idx+1:], cc.Timespans[idx:]) + cc.Timespans[idx] = newTs + continue + } + for _, increment := range ts.Increments { + cost := increment.Cost + moneyBalance.Value -= cost + if count { + ub.countUnits(&Action{BalanceId: CREDIT, Direction: cc.Direction, Balance: &Balance{Value: cost, DestinationId: cc.Destination}}) + } + returnError = insuficientCreditError + } + } + } + return returnError +} - return nil +func (ub *UserBalance) GetDefaultMoneyBalance(direction string) *Balance { + for _, balance := range ub.BalanceMap[CREDIT+direction] { + if balance.IsDefault() { + return balance + } + } + + // create default balance + defaultBalance := &Balance{Weight: 999} + ub.BalanceMap[CREDIT+direction] = append(ub.BalanceMap[CREDIT+direction], defaultBalance) + return defaultBalance } func (ub *UserBalance) refoundIncrements(increments Increments, count bool) { diff --git a/engine/userbalance_test.go b/engine/userbalance_test.go index b52ed48c3..9543cc49f 100644 --- a/engine/userbalance_test.go +++ b/engine/userbalance_test.go @@ -227,6 +227,7 @@ func TestDebitCreditZeroSecond(t *testing.T) { if err != nil { t.Error("Error debiting balance: ", err) } + t.Logf("%+v", cc.Timespans[0]) if cc.Timespans[0].Increments[0].BalanceUuids[0] != "testb" { t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) } @@ -258,6 +259,7 @@ func TestDebitCreditZeroMinute(t *testing.T) { if err != nil { t.Error("Error debiting balance: ", err) } + t.Logf("%+v", cc.Timespans) if cc.Timespans[0].Increments[0].BalanceUuids[0] != "testb" || cc.Timespans[0].Increments[0].Duration != time.Minute { t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) @@ -338,7 +340,7 @@ func TestDebitCreditNoCredit(t *testing.T) { t.Error("Error extracting minutes from balance: ", rifsBalance.BalanceMap[MINUTES+OUTBOUND][0]) } - if len(cc.Timespans) != 1 || cc.Timespans[0].GetDuration() != time.Minute { + if len(cc.Timespans) != 2 || cc.Timespans[0].GetDuration() != time.Minute { t.Error("Error truncating extra timespans: ", cc.Timespans) } } @@ -523,12 +525,12 @@ func TestDebitCreditNoConectFeeCredit(t *testing.T) { MINUTES + OUTBOUND: BalanceChain{b1}, }} err := rifsBalance.debitCreditBalance(cc, false) - if err != nil { - t.Error("Error debiting balance: ", err) + if err == nil { + t.Error("Error showing debiting balance error: ", err) } - if len(cc.Timespans) != 0 || cc.GetDuration() != 0 { - t.Error("Error cutting at no connect fee: ", cc.Timespans) + if len(cc.Timespans) != 2 || rifsBalance.BalanceMap[CREDIT+OUTBOUND].GetTotalValue() != -30 { + t.Error("Error cutting at no connect fee: ", rifsBalance.BalanceMap[CREDIT+OUTBOUND].GetTotalValue()) } } @@ -559,19 +561,20 @@ func TestDebitCreditMoneyOnly(t *testing.T) { if err == nil { t.Error("Missing noy enough credit error ") } - + t.Logf("%+v", cc.Timespans[0].Increments) if cc.Timespans[0].Increments[0].BalanceUuids[0] != "money" || cc.Timespans[0].Increments[0].Duration != 10*time.Second { - t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) + t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0].Duration) } - if rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != 0 { + if rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != -30 { t.Error("Error extracting minutes from balance: ", rifsBalance.BalanceMap[CREDIT+OUTBOUND][0]) } - if len(cc.Timespans) != 2 || + if len(cc.Timespans) != 3 || cc.Timespans[0].GetDuration() != 10*time.Second || - cc.Timespans[1].GetDuration() != 40*time.Second { - t.Error("Error truncating extra timespans: ", cc.Timespans[1].Increments[0]) + cc.Timespans[1].GetDuration() != 40*time.Second || + cc.Timespans[2].GetDuration() != 30*time.Second { + t.Error("Error truncating extra timespans: ", cc.Timespans[2].GetDuration()) } } @@ -679,13 +682,13 @@ func TestDebitCreditSubjectMixed(t *testing.T) { t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) } if rifsBalance.BalanceMap[MINUTES+OUTBOUND][0].Value != 0 || - rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != 20 { + rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != 95 { t.Errorf("Error extracting minutes from balance: %+v, %+v", rifsBalance.BalanceMap[MINUTES+OUTBOUND][0].Value, rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value) } - if len(cc.Timespans) != 9 || cc.Timespans[0].GetDuration() != 40*time.Second { + if len(cc.Timespans) != 2 || cc.Timespans[0].GetDuration() != 40*time.Second { for _, ts := range cc.Timespans { - t.Logf("%+v %+v", ts, ts.Increments[0]) + t.Log(ts) } t.Error("Error truncating extra timespans: ", len(cc.Timespans), cc.Timespans[0].GetDuration()) }