diff --git a/engine/balances.go b/engine/balances.go index 214c3cfa8..d51f4e299 100644 --- a/engine/balances.go +++ b/engine/balances.go @@ -642,6 +642,7 @@ func (bc BalanceChain) SaveDirtyBalances(acc *Account) { allowNegative := "" disabled := "" if b.account != nil { // only publish modifications for balances with account set + //utils.LogStack() accountId = b.account.Id allowNegative = strconv.FormatBool(b.account.AllowNegative) disabled = strconv.FormatBool(b.account.Disabled) diff --git a/engine/callcost.go b/engine/callcost.go index 238619491..c692fe963 100644 --- a/engine/callcost.go +++ b/engine/callcost.go @@ -152,3 +152,15 @@ func (cc *CallCost) AsJSON() string { ccJson, _ := json.Marshal(cc) return string(ccJson) } + +func (cc *CallCost) UpdateCost() { + cost := 0.0 + if cc.deductConnectFee { // add back the connectFee + cost += cc.GetConnectFee() + } + for _, ts := range cc.Timespans { + cost += ts.getCost() + cost = utils.Round(cost, globalRoundingDecimals, utils.ROUNDING_MIDDLE) // just get rid of the extra decimals + } + cc.Cost = cost +} diff --git a/engine/calldesc.go b/engine/calldesc.go index f6af50f7e..4b93208cc 100644 --- a/engine/calldesc.go +++ b/engine/calldesc.go @@ -83,7 +83,7 @@ var ( // Exported method to set the storage getter. func SetRatingStorage(sg RatingStorage) { - ratingStorage = sg + ratingStorage = sg } func SetAccountingStorage(ag AccountingStorage) { @@ -636,16 +636,7 @@ func (cd *CallDescriptor) debit(account *Account, dryRun bool, goNegative bool) utils.Logger.Err(fmt.Sprintf(" Error getting cost for account key <%s>: %s", cd.GetAccountKey(), err.Error())) return nil, err } - cost := 0.0 - // calculate call cost after balances - if cc.deductConnectFee { // add back the connectFee - cost += cc.GetConnectFee() - } - for _, ts := range cc.Timespans { - cost += ts.getCost() - cost = utils.Round(cost, globalRoundingDecimals, utils.ROUNDING_MIDDLE) // just get rid of the extra decimals - } - cc.Cost = cost + cc.UpdateCost() cc.Timespans.Compress() //log.Printf("OUT CC: ", cc) return diff --git a/engine/calldesc_test.go b/engine/calldesc_test.go index 21827f091..6e31a701e 100644 --- a/engine/calldesc_test.go +++ b/engine/calldesc_test.go @@ -557,6 +557,28 @@ func TestGetCostWithMaxCost(t *testing.T) { t.Errorf("Expected %v was %v", expected, cc.Cost) } } +func TestGetCostRoundingIssue(t *testing.T) { + ap, _ := ratingStorage.GetActionPlans("TOPUP10_AT") + for _, at := range ap { + at.Execute() + } + cd := &CallDescriptor{ + Direction: "*out", + Category: "call", + Tenant: "cgrates.org", + Subject: "dy", + Account: "dy", + Destination: "0723123113", + TimeStart: time.Date(2015, 10, 26, 13, 29, 27, 0, time.UTC), + TimeEnd: time.Date(2015, 10, 26, 13, 29, 51, 0, time.UTC), + MaxCostSoFar: 0, + } + cc, err := cd.GetCost() + expected := 0.17 + if cc.Cost != expected || err != nil { + t.Errorf("Expected %v was %+v", expected, cc) + } +} func TestMaxSessionTimeWithMaxCostFree(t *testing.T) { ap, _ := ratingStorage.GetActionPlans("TOPUP10_AT") diff --git a/engine/loader_csv_test.go b/engine/loader_csv_test.go index 9d4fe2b1c..8ff6e154d 100644 --- a/engine/loader_csv_test.go +++ b/engine/loader_csv_test.go @@ -70,6 +70,7 @@ RT_UK_Mobile_BIG5_PKG,0.01,0,20s,20s,0s RT_UK_Mobile_BIG5,0.01,0.10,1s,1s,0s R_URG,0,0,1,1,0 MX,0,1,1s,1s,0 +DY,0.15,0.05,60s,1s,0s ` destinationRates = ` RT_STANDARD,GERMANY,R1,*middle,4,0, @@ -91,6 +92,7 @@ DATA_RATE,*any,LANDLINE_OFFPEAK,*middle,4,0, RT_URG,URG,R_URG,*middle,4,0, MX_FREE,RET,MX,*middle,4,10,*free MX_DISC,RET,MX,*middle,4,10,*disconnect +RT_DY,RET,DY,*up,2,0, ` ratingPlans = ` STANDARD,RT_STANDARD,WORKDAYS_00,10 @@ -115,6 +117,7 @@ RP_MX,MX_DISC,WORKDAYS_00,10 RP_MX,MX_FREE,WORKDAYS_18,10 GER_ONLY,GER,*any,10 ANY_PLAN,DATA_RATE,*any,10 +DY_PLAN,RT_DY,*any,10 ` ratingProfiles = ` *out,CUSTOMER_1,0,rif:from:tm,2012-01-01T00:00:00Z,PREMIUM,danb, @@ -141,6 +144,7 @@ ANY_PLAN,DATA_RATE,*any,10 *out,cgrates.org,call,nt,2012-02-28T00:00:00Z,GER_ONLY,, *in,cgrates.org,LCR_STANDARD,max,2013-03-23T00:00:00Z,RP_MX,, *out,cgrates.org,call,money,2015-02-28T00:00:00Z,EVENING,, +*out,cgrates.org,call,dy,2015-02-28T00:00:00Z,DY_PLAN,, ` sharedGroups = ` SG1,*any,*lowest, @@ -391,7 +395,7 @@ func TestLoadTimimgs(t *testing.T) { } func TestLoadRates(t *testing.T) { - if len(csvr.rates) != 13 { + if len(csvr.rates) != 14 { t.Error("Failed to load rates: ", len(csvr.rates)) } rate := csvr.rates["R1"].RateSlots[0] @@ -461,7 +465,7 @@ func TestLoadRates(t *testing.T) { } func TestLoadDestinationRates(t *testing.T) { - if len(csvr.destinationRates) != 14 { + if len(csvr.destinationRates) != 15 { t.Error("Failed to load destinationrates: ", len(csvr.destinationRates)) } drs := csvr.destinationRates["RT_STANDARD"] @@ -609,7 +613,7 @@ func TestLoadDestinationRates(t *testing.T) { } func TestLoadRatingPlans(t *testing.T) { - if len(csvr.ratingPlans) != 13 { + if len(csvr.ratingPlans) != 14 { t.Error("Failed to load rating plans: ", len(csvr.ratingPlans)) } rplan := csvr.ratingPlans["STANDARD"] @@ -781,7 +785,7 @@ func TestLoadRatingPlans(t *testing.T) { } func TestLoadRatingProfiles(t *testing.T) { - if len(csvr.ratingProfiles) != 21 { + if len(csvr.ratingProfiles) != 22 { t.Error("Failed to load rating profiles: ", len(csvr.ratingProfiles), csvr.ratingProfiles) } rp := csvr.ratingProfiles["*out:test:0:trp"] diff --git a/engine/suretax.go b/engine/suretax.go index 20b30d4dd..516e557ad 100644 --- a/engine/suretax.go +++ b/engine/suretax.go @@ -26,7 +26,6 @@ import ( "fmt" "io/ioutil" "net/http" - "net/url" "strconv" "strings" @@ -52,7 +51,7 @@ func NewSureTaxRequest(cdr *StoredCdr, stCfg *config.SureTaxCfg) (*SureTaxReques if len(definedTaxExtempt) != 0 { taxExempt = strings.Split(cdr.FieldsAsString(stCfg.TaxExemptionCodeList), ",") } - stReq := new(SureTaxRequest) + stReq := new(STRequest) stReq.ClientNumber = stCfg.ClientNumber stReq.BusinessUnit = "" // Export it to config stReq.ValidationKey = stCfg.ValidationKey @@ -86,11 +85,15 @@ func NewSureTaxRequest(cdr *StoredCdr, stCfg *config.SureTaxCfg) (*SureTaxReques TaxExemptionCodeList: taxExempt, }, } - return stReq, nil + return &SureTaxRequest{Request: stReq}, nil +} + +type SureTaxRequest struct { + Request *STRequest // SureTax Requires us to encapsulate the content into a request element } // SureTax Request type -type SureTaxRequest struct { +type STRequest struct { ClientNumber string // Client ID Number – provided by SureTax. Required. Max Len: 10 BusinessUnit string // Client’s Business Unit. Value for this field is not required. Max Len: 20 ValidationKey string // Validation Key provided by SureTax. Required for client access to API function. Max Len: 36 @@ -130,19 +133,12 @@ type STRequestItem struct { TaxExemptionCodeList []string // Required. Tax Exemption to be applied to this item only. } -// Converts the request into the format SureTax expects -func (self *SureTaxRequest) AsHttpForm() (url.Values, error) { - jsnContent, err := json.Marshal(self) - if err != nil { - return nil, err - } - v := url.Values{} - v.Set("request", string(jsnContent)) - return v, nil -} - // SureTax Response type type SureTaxResponse struct { + D *STResponse // SureTax requires encapsulating reply into a D object +} + +type STResponse struct { Successful string // Response will be either ‘Y' or ‘N' : Y = Success / Success with Item error N = Failure ResponseCode int64 // ResponseCode: 9999 – Request was successful. 1101-1400 – Range of values for a failed request (no processing occurred) 9001 – Request was successful, but items within the request have errors. The specific items with errors are provided in the ItemMessages field. HeaderMessage string // Response message: For ResponseCode 9999 – “Success”For ResponseCode 9001 – “Success with Item errors”. For ResponseCode 1100-1400 – Unsuccessful / declined web request. @@ -190,38 +186,40 @@ func SureTaxProcessCdr(cdr *StoredCdr) error { if err != nil { return err } - - body, err := json.Marshal(req) + jsnContent, err := json.Marshal(req) if err != nil { return err } - utils.Logger.Debug(fmt.Sprintf("###SureTax NewSureTaxRequest: %+v, ItemList: %+v\n", req, req.ItemList[0])) - resp, err := sureTaxClient.Post(stCfg.Url, "application/json", bytes.NewBuffer(body)) + utils.Logger.Debug(fmt.Sprintf("NewSureTaxRequest: %s\n", string(jsnContent))) + resp, err := sureTaxClient.Post(stCfg.Url, "application/json", bytes.NewBuffer(jsnContent)) if err != nil { return err } defer resp.Body.Close() respBody, err := ioutil.ReadAll(resp.Body) if err != nil { + utils.Logger.Debug(fmt.Sprintf("Unexpected response body received, error: %s\n", err.Error())) return err } if resp.StatusCode > 299 { + utils.Logger.Debug(fmt.Sprintf("Unexpected code received: %d\n", resp.StatusCode)) return fmt.Errorf("Unexpected status code received: %d", resp.StatusCode) } + utils.Logger.Debug(fmt.Sprintf("Received raw answer from SureTax: %s\n", string(respBody))) var stResp SureTaxResponse if err := json.Unmarshal(respBody, &stResp); err != nil { return err } - utils.Logger.Debug(fmt.Sprintf("###SureTax received response: %+v\n", stResp)) - if stResp.ResponseCode != 9999 { - cdr.ExtraInfo = stResp.HeaderMessage + utils.Logger.Debug(fmt.Sprintf("Received answer from SureTax: %+v\n", stResp)) + if stResp.D.ResponseCode != 9999 { + cdr.ExtraInfo = stResp.D.HeaderMessage return nil // No error because the request was processed by SureTax, error will be in the ExtraInfo } // Write cost to CDR if !stCfg.IncludeLocalCost { - cdr.Cost = utils.Round(stResp.TotalTax, config.CgrConfig().RoundingDecimals, utils.ROUNDING_MIDDLE) + cdr.Cost = utils.Round(stResp.D.TotalTax, config.CgrConfig().RoundingDecimals, utils.ROUNDING_MIDDLE) } else { - cdr.Cost = utils.Round(cdr.Cost+stResp.TotalTax, config.CgrConfig().RoundingDecimals, utils.ROUNDING_MIDDLE) + cdr.Cost = utils.Round(cdr.Cost+stResp.D.TotalTax, config.CgrConfig().RoundingDecimals, utils.ROUNDING_MIDDLE) } // Add response into extra fields to be available for later review cdr.ExtraFields[utils.META_SURETAX] = string(respBody) diff --git a/engine/suretax_test.go b/engine/suretax_test.go index a01b203a3..5273d6fa3 100644 --- a/engine/suretax_test.go +++ b/engine/suretax_test.go @@ -40,7 +40,7 @@ func TestNewSureTaxRequest(t *testing.T) { stCfg.ClientNumber = "000000000" stCfg.ValidationKey = "19491161-F004-4F44-BDB3-E976D6739A64" stCfg.Timezone = time.UTC - eSureTaxRequest := &SureTaxRequest{ + eSureTaxRequest := &SureTaxRequest{Request: &STRequest{ ClientNumber: "000000000", ValidationKey: "19491161-F004-4F44-BDB3-E976D6739A64", DataYear: "2013", @@ -69,10 +69,10 @@ func TestNewSureTaxRequest(t *testing.T) { TaxExemptionCodeList: []string{}, }, }, - } + }} if stReq, err := NewSureTaxRequest(cdr, stCfg); err != nil { t.Error(err) } else if !reflect.DeepEqual(eSureTaxRequest, stReq) { - t.Errorf("Expecting: %+v, received: %+v", eSureTaxRequest.ItemList[0], stReq.ItemList[0]) + t.Errorf("Expecting: %+v, received: %+v", eSureTaxRequest.Request.ItemList[0], stReq.Request.ItemList[0]) } } diff --git a/engine/timespans.go b/engine/timespans.go index a497ab4ef..24df295d7 100644 --- a/engine/timespans.go +++ b/engine/timespans.go @@ -263,11 +263,7 @@ func (ts *TimeSpan) getCost() float64 { ts.Cost = utils.Round(cost, ts.RateInterval.Rating.RoundingDecimals, ts.RateInterval.Rating.RoundingMethod) return ts.Cost } else { - cost := 0.0 - // some increments may have 0 cost because of the max cost strategy - for _, inc := range ts.Increments { - cost += inc.Cost - } + cost := ts.Increments.GetTotalCost() if ts.RateInterval != nil && ts.RateInterval.Rating != nil { return utils.Round(cost, ts.RateInterval.Rating.RoundingDecimals, ts.RateInterval.Rating.RoundingMethod) } else { diff --git a/general_tests/suretax_it_test.go b/general_tests/suretax_it_test.go index 5ade9bce0..f45a3c4ce 100644 --- a/general_tests/suretax_it_test.go +++ b/general_tests/suretax_it_test.go @@ -137,7 +137,7 @@ func TestSTIProcessExternalCdr(t *testing.T) { AccId: "teststicdr1", CdrHost: "192.168.1.1", CdrSource: "STI_TEST", ReqType: utils.META_RATED, Direction: utils.OUT, Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "+14082342500", Destination: "+16268412300", Supplier: "SUPPL1", SetupTime: "2015-10-18T13:00:00Z", AnswerTime: "2015-10-18T13:00:00Z", - Usage: "15s", Pdd: "7.0", ExtraFields: map[string]string{"ClientNumber": "000000534", "": "valextr2"}, + Usage: "15s", Pdd: "7.0", ExtraFields: map[string]string{"CustomerNumber": "000000534", "ZipCode": ""}, } var reply string if err := stiRpc.Call("CdrsV2.ProcessExternalCdr", cdr, &reply); err != nil { @@ -145,7 +145,7 @@ func TestSTIProcessExternalCdr(t *testing.T) { } else if reply != utils.OK { t.Error("Unexpected reply received: ", reply) } - time.Sleep(time.Duration(*waitRater) * time.Millisecond) + time.Sleep(time.Duration(2) * time.Second) } func TestSTIGetCdrs(t *testing.T) { @@ -163,6 +163,16 @@ func TestSTIGetCdrs(t *testing.T) { t.Errorf("Unexpected Cost for CDR: %+v", cdrs[0]) } } + req = utils.RpcCdrsFilter{RunIds: []string{utils.META_SURETAX}, Accounts: []string{"1001"}} + if err := stiRpc.Call("ApierV2.GetCdrs", req, &cdrs); err != nil { + t.Error("Unexpected error: ", err.Error()) + } else if len(cdrs) != 1 { + t.Error("Unexpected number of CDRs returned: ", len(cdrs)) + } else { + if cdrs[0].Cost != 0.012 { + t.Errorf("Unexpected Cost for CDR: %+v", cdrs[0]) + } + } } func TestSTIStopCgrEngine(t *testing.T) { diff --git a/general_tests/tutorial_local_test.go b/general_tests/tutorial_local_test.go index 01052bcff..9f5e2dcb5 100644 --- a/general_tests/tutorial_local_test.go +++ b/general_tests/tutorial_local_test.go @@ -663,7 +663,7 @@ func TestTutLocalCostErrors(t *testing.T) { } else if reply != utils.OK { t.Error("Unexpected reply received: ", reply) } - + time.Sleep(time.Duration(*waitRater) * time.Millisecond) // Give time for CDR to be processed req = utils.RpcCdrsFilter{RunIds: []string{utils.META_DEFAULT}, Accounts: []string{cdr2.Account}, DestPrefixes: []string{cdr2.Destination}} if err := tutLocalRpc.Call("ApierV2.GetCdrs", req, &cdrs); err != nil { t.Error("Unexpected error: ", err.Error()) diff --git a/sessionmanager/session.go b/sessionmanager/session.go index 813058f65..faa5c9b7d 100644 --- a/sessionmanager/session.go +++ b/sessionmanager/session.go @@ -233,6 +233,8 @@ func (s *Session) SaveOperations() { firstCC.Merge(cc) //utils.Logger.Debug(fmt.Sprintf("AFTER MERGE: %s", utils.ToJSON(firstCC))) } + // make sure we have rounded timespans final cost + firstCC.UpdateCost() var reply string err := s.sessionManager.CdrSrv().LogCallCost(&engine.CallCostLog{ diff --git a/utils/logger.go b/utils/logger.go index d5300b93b..cabbe068b 100644 --- a/utils/logger.go +++ b/utils/logger.go @@ -22,6 +22,7 @@ import ( "fmt" "log" "log/syslog" + "runtime" ) var Logger LoggerInterface @@ -85,3 +86,9 @@ func (sl *StdLogger) Warning(m string) (err error) { log.Print("[WARNING]" + m) return } + +func LogStack() { + buf := make([]byte, 300) + runtime.Stack(buf, false) + Logger.Debug(string(buf)) +} diff --git a/utils/rsrfield_test.go b/utils/rsrfield_test.go index 02adf10bb..cc03bd21f 100644 --- a/utils/rsrfield_test.go +++ b/utils/rsrfield_test.go @@ -221,3 +221,14 @@ func TestRSRFieldsId(t *testing.T) { t.Errorf("Received id: %s", idRcv) } } + +func TestRSRCostDetails(t *testing.T) { + fieldsStr1 := `{"Direction":"*out","Category":"default_route","Tenant":"demo.cgrates.org","Subject":"voxbeam_premium","Account":"6335820713","Destination":"15143606781","TOR":"*voice","Cost":0.0007,"Timespans":[{"TimeStart":"2015-08-30T21:46:54Z","TimeEnd":"2015-08-30T21:47:06Z","Cost":0.00072,"RateInterval":{"Timing":{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[],"StartTime":"00:00:00","EndTime":""},"Rating":{"ConnectFee":0,"RoundingMethod":"*middle","RoundingDecimals":5,"MaxCost":0,"MaxCostStrategy":"0","Rates":[{"GroupIntervalStart":0,"Value":0.0036,"RateIncrement":6000000000,"RateUnit":60000000000}]},"Weight":10},"DurationIndex":12000000000,"Increments":[{"Duration":6000000000,"Cost":0.00036,"BalanceInfo":{"UnitBalanceUuid":"","MoneyBalanceUuid":"40adda88-25d3-4009-b928-f39d61590439","AccountId":"*out:demo.cgrates.org:6335820713"},"BalanceRateInterval":null,"UnitInfo":null,"CompressFactor":2}],"MatchedSubject":"*out:demo.cgrates.org:default_route:voxbeam_premium","MatchedPrefix":"1514","MatchedDestId":"Canada","RatingPlanId":"RP_VOXBEAM_PREMIUM"}]}` + rsrField, err := NewRSRField(`~cost_details:s/"MatchedDestId":"(\w+)"/${1}/`) + if err != nil { + t.Error(err) + } + if parsedVal := rsrField.ParseValue(fieldsStr1); parsedVal != "Canada" { + t.Errorf("Expecting: Canada, received: %s", parsedVal) + } +}