From 5a614711f2cade94565303e9679ca1ad5b2a8437 Mon Sep 17 00:00:00 2001 From: DanB Date: Sun, 4 Oct 2015 18:23:32 +0200 Subject: [PATCH] SureTax data structures with tests for NewSureTaxRequest --- config/config.go | 28 +++++-- config/config_defaults.go | 4 + config/config_json_test.go | 10 ++- config/libconfig.go | 12 ++- config/libconfig_json.go | 10 ++- engine/storedcdr.go | 12 +++ engine/storedcdr_test.go | 14 ++++ engine/suretax.go | 154 +++++++++++++++++++++++++++++++++++++ engine/suretax_test.go | 76 ++++++++++++++++++ 9 files changed, 306 insertions(+), 14 deletions(-) create mode 100644 engine/suretax.go create mode 100644 engine/suretax_test.go diff --git a/config/config.go b/config/config.go index 015a6e8c8..f9e87cfa6 100644 --- a/config/config.go +++ b/config/config.go @@ -238,7 +238,7 @@ type CGRConfig struct { MailerAuthUser string // Authenticate to email server using this user MailerAuthPass string // Authenticate to email server with this password MailerFromAddr string // From address used when sending emails out - SureTax *SureTaxCfg // Load here SureTax configuration, as pointer so we can have runtime reloads in the future + SureTaxCfg *SureTaxCfg // Load here SureTax configuration, as pointer so we can have runtime reloads in the future DataFolderPath string // Path towards data folder, for tests internal usage, not loading out of .json options ConfigReloads map[string]chan struct{} // Signals to specific entities that a config reload should occur // Cache defaults loaded from json and needing clones @@ -804,15 +804,33 @@ func (self *CGRConfig) loadFromJsonCfg(jsnCfg *CgrJsonCfg) error { } if jsnSureTaxCfg != nil { - self.SureTax = new(SureTaxCfg) // Reset previous values + self.SureTaxCfg = new(SureTaxCfg) // Reset previous values if jsnSureTaxCfg.Url != nil { - self.SureTax.Url = *jsnSureTaxCfg.Url + self.SureTaxCfg.Url = *jsnSureTaxCfg.Url } if jsnSureTaxCfg.Client_number != nil { - self.SureTax.ClientNumber = *jsnSureTaxCfg.Client_number + self.SureTaxCfg.ClientNumber = *jsnSureTaxCfg.Client_number } if jsnSureTaxCfg.Validation_key != nil { - self.SureTax.ValidationKey = *jsnSureTaxCfg.Validation_key + self.SureTaxCfg.ValidationKey = *jsnSureTaxCfg.Validation_key + } + if jsnSureTaxCfg.Timezone != nil { + if self.SureTaxCfg.Timezone, err = time.LoadLocation(*jsnSureTaxCfg.Timezone); err != nil { + return err + } + } + if jsnSureTaxCfg.Include_local_cost != nil { + self.SureTaxCfg.IncludeLocalCost = *jsnSureTaxCfg.Include_local_cost + } + if jsnSureTaxCfg.Origination_number != nil { + if self.SureTaxCfg.OriginationNumber, err = utils.ParseRSRFields(*jsnSureTaxCfg.Origination_number, utils.INFIELD_SEP); err != nil { + return err + } + } + if jsnSureTaxCfg.Termination_number != nil { + if self.SureTaxCfg.TerminationNumber, err = utils.ParseRSRFields(*jsnSureTaxCfg.Termination_number, utils.INFIELD_SEP); err != nil { + return err + } } } return nil diff --git a/config/config_defaults.go b/config/config_defaults.go index ab5cdc961..a604eb4a7 100644 --- a/config/config_defaults.go +++ b/config/config_defaults.go @@ -283,6 +283,10 @@ const CGRATES_CFG_JSON = ` "url": "", // API url "client_number": "", // client number, provided by SureTax "validation_key": "", // validation key provided by SureTax + "timezone": "Local", // convert the time of the events to this timezone before sending request out + "include_local_cost": false, // sum local calculated cost with tax one in final cost + "origination_number": "Subject", // template extracting origination number out of StoredCdr + "termination_number": "Destination", // template extracting termination number out of StoredCdr }, }` diff --git a/config/config_json_test.go b/config/config_json_test.go index 036da9e96..3b9580792 100644 --- a/config/config_json_test.go +++ b/config/config_json_test.go @@ -459,9 +459,13 @@ func TestDfMailerJsonCfg(t *testing.T) { func TestDfSureTaxJsonCfg(t *testing.T) { eCfg := &SureTaxJsonCfg{ - Url: utils.StringPointer(""), - Client_number: utils.StringPointer(""), - Validation_key: utils.StringPointer(""), + Url: utils.StringPointer(""), + Client_number: utils.StringPointer(""), + Validation_key: utils.StringPointer(""), + Timezone: utils.StringPointer("Local"), + Include_local_cost: utils.BoolPointer(false), + Origination_number: utils.StringPointer("Subject"), + Termination_number: utils.StringPointer("Destination"), } if cfg, err := dfCgrJsonCfg.SureTaxJsonCfg(); err != nil { t.Error(err) diff --git a/config/libconfig.go b/config/libconfig.go index 723cd79cf..38c5d0400 100644 --- a/config/libconfig.go +++ b/config/libconfig.go @@ -19,6 +19,8 @@ along with this program. If not, see package config import ( + "time" + "github.com/cgrates/cgrates/utils" ) @@ -31,7 +33,11 @@ type CdrReplicationCfg struct { } type SureTaxCfg struct { - Url string - ClientNumber string - ValidationKey string + Url string + ClientNumber string + ValidationKey string + Timezone *time.Location // Convert the time of the events to this timezone before sending request out + IncludeLocalCost bool + OriginationNumber utils.RSRFields // Concatenate all of them to get value + TerminationNumber utils.RSRFields // Concatenate all of them to get value } diff --git a/config/libconfig_json.go b/config/libconfig_json.go index 8d16356ed..9367ce538 100644 --- a/config/libconfig_json.go +++ b/config/libconfig_json.go @@ -256,7 +256,11 @@ type MailerJsonCfg struct { // SureTax config section type SureTaxJsonCfg struct { - Url *string // API url - Client_number *string // client number provided by SureTax - Validation_key *string // validation key provided by SureTax + Url *string // API url + Client_number *string // client number provided by SureTax + Validation_key *string // validation key provided by SureTax + Timezone *string // convert the time of the events to this timezone before sending request out + Include_local_cost *bool // sum local calculated cost with tax one + Origination_number *string // template extracting origination number out of StoredCdr + Termination_number *string // template extracting origination number out of StoredCdr } diff --git a/engine/storedcdr.go b/engine/storedcdr.go index 0b2bcd672..a398ad714 100644 --- a/engine/storedcdr.go +++ b/engine/storedcdr.go @@ -135,6 +135,9 @@ func (storedCdr *StoredCdr) FormatUsage(layout string) string { // Used to retrieve fields as string, primary fields are const labeled func (storedCdr *StoredCdr) FieldAsString(rsrFld *utils.RSRField) string { + if rsrFld.IsStatic() { // Static values do not care about headers + return rsrFld.ParseValue("") + } switch rsrFld.Id { case utils.CGRID: return rsrFld.ParseValue(storedCdr.CgrId) @@ -189,6 +192,15 @@ func (storedCdr *StoredCdr) FieldAsString(rsrFld *utils.RSRField) string { } } +// concatenates values of multiple fields defined in template, used eg in CDR templates +func (storedCdr *StoredCdr) FieldsAsString(rsrFlds utils.RSRFields) string { + var fldVal string + for _, rsrFld := range rsrFlds { + fldVal += storedCdr.FieldAsString(rsrFld) + } + return fldVal +} + func (storedCdr *StoredCdr) PassesFieldFilter(fieldFilter *utils.RSRField) (bool, string) { if fieldFilter == nil { return true, "" diff --git a/engine/storedcdr_test.go b/engine/storedcdr_test.go index 304d0a061..47b4f3cd6 100644 --- a/engine/storedcdr_test.go +++ b/engine/storedcdr_test.go @@ -124,6 +124,20 @@ func TestFieldAsString(t *testing.T) { } } +func TestFieldsAsString(t *testing.T) { + cdr := StoredCdr{CgrId: utils.Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC).String()), OrderId: 123, TOR: utils.VOICE, AccId: "dsafdsaf", + CdrHost: "192.168.1.1", CdrSource: "test", ReqType: utils.META_RATED, Direction: "*out", Tenant: "cgrates.org", + Category: "call", Account: "1001", Subject: "1001", Destination: "1002", SetupTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), + AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), MediationRunId: utils.DEFAULT_RUNID, + Usage: time.Duration(10) * time.Second, Pdd: time.Duration(5) * time.Second, Supplier: "SUPPL1", ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}, + Cost: 1.01, RatedAccount: "dan", RatedSubject: "dans", + } + eVal := "call_from_1001" + if val := cdr.FieldsAsString(utils.ParseRSRFieldsMustCompile("Category;^_from_;Account", utils.INFIELD_SEP)); val != eVal { + t.Errorf("Expecting : %s, received: %s", eVal, val) + } +} + func TestPassesFieldFilter(t *testing.T) { cdr := &StoredCdr{CgrId: utils.Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC).String()), OrderId: 123, TOR: utils.VOICE, AccId: "dsafdsaf", CdrHost: "192.168.1.1", CdrSource: "test", ReqType: utils.META_RATED, Direction: "*out", Tenant: "cgrates.org", diff --git a/engine/suretax.go b/engine/suretax.go new file mode 100644 index 000000000..42364ac9d --- /dev/null +++ b/engine/suretax.go @@ -0,0 +1,154 @@ +/* +Rating system designed to be used in VoIP Carriers World +Copyright (C) 2012-2015 ITsysCOM + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +package engine + +import ( + "encoding/json" + "net/url" + "strconv" + "time" + + "github.com/cgrates/cgrates/utils" +) + +// Part of SureTax Request +type STRequestItem struct { + LineNumber string // Used to identify an item within the request. If no value is provided, requests are numbered sequentially. Max Len: 40 + InvoiceNumber string // Used for tax aggregation by Invoice. Must be alphanumeric. Max Len: 40 + CustomerNumber string // Used for tax aggregation by Customer. Must be alphanumeric. Max Len: 40 + OrigNumber string // Required when using Tax Situs Rule 01 or 03. Format: NPANXXNNNN + TermNumber string // Required when using Tax Situs Rule 01. Format: NPANXXNNNN + BillToNumber string // Required when using Tax Situs Rule 01 or 02. Format: NPANXXNNNN + Zipcode string // Required when using Tax Situs Rule 04, 05, or 14. + Plus4 string // Zip code extension in format: 9999 (not applicable for Tax Situs Rule 14) + P2PZipcode string // Secondary zip code in format: 99999 (US or US territory) or X9X9X9 (Canadian) + P2PPlus4 string // Secondary zip code extension in format: 99999 (US or US territory) or X9X9X9 (Canadian) + TransDate string // Required. Date of transaction. Valid date formats include: MM/DD/YYYY, MM-DD-YYYY, YYYY-MM-DDTHH:MM:SS + Revenue float64 // Required. Format: $$$$$$$$$.CCCC. For Negative charges, the first position should have a minus ‘-‘indicator. + Units int64 // Required. Units representing number of “lines” or unique charges contained within the revenue. This value is essentially a multiplier on unit-based fees (e.g. E911 fees). Format: 99999. Default should be 1 (one unit). + UnitType string // Required. 00 – Default / Number of unique access lines. + Seconds int64 // Required. Duration of call in seconds. Format 99999. Default should be 1. + TaxIncludedCode string // Required. Values: 0 – Default (No Tax Included) 1 – Tax Included in Revenue + TaxSitusRule string // Required. + TransTypeCode string // Required. Transaction Type Indicator. + SalesTypeCode string // Required. Values: R – Residential customer (default) B – Business customer I – Industrial customer L – Lifeline customer + RegulatoryCode string // Required. Provider Type. + TaxExemptionCodeList []string // Required. Tax Exemption to be applied to this item only. +} + +// Init a new request to be sent out to SureTax +func NewSureTaxRequest(clientNumber, validationKey string, timezone *time.Location, originationNrTpl, terminationNrTpl utils.RSRFields, cdr *StoredCdr) (*SureTaxRequest, error) { + if clientNumber == "" { + return nil, utils.NewErrMandatoryIeMissing("ClientNumber") + } + if validationKey == "" { + return nil, utils.NewErrMandatoryIeMissing("ValidationKey") + } + aTime := cdr.AnswerTime.In(timezone) + stReq := &SureTaxRequest{ClientNumber: clientNumber, + ValidationKey: validationKey, + DataYear: strconv.Itoa(aTime.Year()), + DataMonth: strconv.Itoa(int(aTime.Month())), + TotalRevenue: utils.Round(cdr.Cost, 4, utils.ROUNDING_MIDDLE), + ReturnFileCode: "0", + ClientTracking: cdr.CgrId, + ResponseGroup: "03", + ResponseType: "", + ItemList: []*STRequestItem{ + &STRequestItem{ + OrigNumber: cdr.FieldsAsString(originationNrTpl), + TermNumber: cdr.FieldsAsString(terminationNrTpl), + BillToNumber: cdr.FieldsAsString(originationNrTpl), + TransDate: aTime.Format("2006-01-02T15:04:05"), + Revenue: utils.Round(cdr.Cost, 4, utils.ROUNDING_MIDDLE), + Units: 1, + UnitType: "00", + Seconds: int64(utils.Round(cdr.Usage.Seconds(), 0, utils.ROUNDING_MIDDLE)), + TaxIncludedCode: "0", + TaxSitusRule: "1", + TransTypeCode: "010101", + SalesTypeCode: "R", + RegulatoryCode: "01", + TaxExemptionCodeList: []string{"00"}, + }, + }, + } + return stReq, nil +} + +// SureTax Request type +type SureTaxRequest 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 + DataYear string // Required. YYYY – Year to use for tax calculation purposes + DataMonth string // Required. MM – Month to use for tax calculation purposes. Leading zero is preferred. + TotalRevenue float64 // Required. Format: $$$$$$$$$.CCCC. For Negative charges, the first position should have a minus ‘-‘ indicator. + ReturnFileCode string // Required. 0 – Default.Q – Quote purposes – taxes are computed and returned in the response message for generating quotes. + ClientTracking string // Field for client transaction tracking. This value will be provided in the response data. Value for this field is not required, but preferred. Max Len: 100 + IndustryExemption string // Reserved for future use. + ResponseGroup string // Required. Determines how taxes are grouped for the response. + ResponseType string // Required. Determines the granularity of taxes and (optionally) the decimal precision for the tax calculations and amounts in the response. + ItemList []*STRequestItem // List of Item records +} + +// 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 { + Successful string // Response will be either ‘Y' or ‘N' : Y = Success / Success with Item error N = Failure + ResponseCode string // 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. + ItemMessages []*STItemMessage // This field contains a list of items that were not able to be processed due to bad or invalid data (see Response Code of “9001”). + ClientTracking string // Client transaction tracking provided in web request. + TotalTax float64 // Total Tax – a total of all taxes included in the TaxList + TransId int // Transaction ID – provided by SureTax + GroupList []*STGroup // contains one-to-many Groups +} + +// Part of the SureTax Response +type STItemMessage struct { + LineNumber string // value corresponding to the line number in the web request + ResponseCode string // a value in the range 9100-9400 + Message string // the error message corresponding to the ResponseCode +} + +// Part of the SureTax Response +type STGroup struct { + StateCode string // Tax State + InvoiceNumber string // Invoice Number + CustomerNumber string // Customer number + TaxList []*STTaxItem // contains one-to-many Tax Items +} + +// Part of the SureTax Response +type STTaxItem struct { + TaxTypeCode string // Tax Type Code + TaxTypeDesc string // Tax Type Description + TaxAmount float64 // Tax Amount +} diff --git a/engine/suretax_test.go b/engine/suretax_test.go new file mode 100644 index 000000000..6bdb6d586 --- /dev/null +++ b/engine/suretax_test.go @@ -0,0 +1,76 @@ +/* +Real-time Charging System for Telecom & ISP environments +Copyright (C) 2012-2015 ITsysCOM GmbH + +This program is free software: you can Storagetribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITH*out ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +package engine + +import ( + "reflect" + "testing" + "time" + + "github.com/cgrates/cgrates/config" + "github.com/cgrates/cgrates/utils" +) + +func TestNewSureTaxRequest(t *testing.T) { + storedCdr := &StoredCdr{CgrId: utils.Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC).String()), OrderId: 123, TOR: utils.VOICE, + AccId: "dsafdsaf", CdrHost: "192.168.1.1", CdrSource: utils.UNIT_TEST, ReqType: utils.META_RATED, Direction: "*out", + Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1002", Supplier: "SUPPL1", + SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC), AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), MediationRunId: utils.DEFAULT_RUNID, + Usage: time.Duration(12) * time.Second, Pdd: time.Duration(7) * time.Second, ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}, Cost: 1.01, RatedAccount: "dan", RatedSubject: "dans", Rated: true, + } + cfg, _ := config.NewDefaultCGRConfig() + cfg.SureTaxCfg.ClientNumber = "000000000" + cfg.SureTaxCfg.ValidationKey = "19491161-F004-4F44-BDB3-E976D6739A64" + cfg.SureTaxCfg.Timezone = time.UTC + eSureTaxRequest := &SureTaxRequest{ + ClientNumber: cfg.SureTaxCfg.ClientNumber, + ValidationKey: cfg.SureTaxCfg.ValidationKey, + DataYear: "2013", + DataMonth: "11", + TotalRevenue: 1.01, + ReturnFileCode: "0", + ClientTracking: storedCdr.CgrId, + ResponseGroup: "03", + ResponseType: "", + ItemList: []*STRequestItem{ + &STRequestItem{ + OrigNumber: "1001", + TermNumber: "1002", + BillToNumber: "1001", + TransDate: "2013-11-07T08:42:26", + Revenue: 1.01, + Units: 1, + UnitType: "00", + Seconds: 12, + TaxIncludedCode: "0", + TaxSitusRule: "1", + TransTypeCode: "010101", + SalesTypeCode: "R", + RegulatoryCode: "01", + TaxExemptionCodeList: []string{"00"}, + }, + }, + } + if stReq, err := NewSureTaxRequest(cfg.SureTaxCfg.ClientNumber, cfg.SureTaxCfg.ValidationKey, cfg.SureTaxCfg.Timezone, cfg.SureTaxCfg.OriginationNumber, + cfg.SureTaxCfg.TerminationNumber, storedCdr); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(eSureTaxRequest, stReq) { + t.Errorf("Expecting: %+v, received: %+v", eSureTaxRequest.ItemList[0], stReq.ItemList[0]) + } +}