From 5d56411b5b08093a9efc748ec1b6b36b2f27bd60 Mon Sep 17 00:00:00 2001 From: DanB Date: Sun, 18 Oct 2015 14:24:15 +0200 Subject: [PATCH] Improved SureTax configuration with defaults, modified NewSureTaxRequest method --- config/config.go | 62 +++++-------- config/config_defaults.go | 32 +++++-- config/config_json_test.go | 32 +++++-- config/libconfig.go | 11 --- config/libconfig_json.go | 32 +++++-- config/suretaxconfig.go | 186 +++++++++++++++++++++++++++++++++++++ engine/suretax.go | 185 ++++++++++++++++++++++++------------ engine/suretax_test.go | 29 +++--- utils/consts.go | 2 + 9 files changed, 430 insertions(+), 141 deletions(-) create mode 100644 config/suretaxconfig.go diff --git a/config/config.go b/config/config.go index 2a9ed741a..fab72222e 100644 --- a/config/config.go +++ b/config/config.go @@ -31,15 +31,14 @@ import ( ) const ( - DISABLED = "disabled" - JSON = "json" - GOB = "gob" - POSTGRES = "postgres" - MONGO = "mongo" - REDIS = "redis" - SAME = "same" - FS = "freeswitch" - CFG_RELOADS = "cfg_reloads_" + DISABLED = "disabled" + JSON = "json" + GOB = "gob" + POSTGRES = "postgres" + MONGO = "mongo" + REDIS = "redis" + SAME = "same" + FS = "freeswitch" ) var ( @@ -70,6 +69,8 @@ func NewDefaultCGRConfig() (*CGRConfig, error) { cfg.ConfigReloads[utils.CDRC] <- struct{}{} // Unlock the channel cfg.ConfigReloads[utils.CDRE] = make(chan struct{}, 1) cfg.ConfigReloads[utils.CDRE] <- struct{}{} // Unlock the channel + cfg.ConfigReloads[utils.SURETAX] = make(chan struct{}, 1) + cfg.ConfigReloads[utils.SURETAX] <- struct{}{} // Unlock the channel cgrJsonCfg, err := NewCgrJsonCfgFromReader(strings.NewReader(CGRATES_CFG_JSON)) if err != nil { return nil, err @@ -238,8 +239,8 @@ 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 - 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 + sureTaxCfg *SureTaxCfg // Load here SureTax configuration, as pointer so we can have runtime reloads in the future ConfigReloads map[string]chan struct{} // Signals to specific entities that a config reload should occur // Cache defaults loaded from json and needing clones dfltCdreProfile *CdreConfig // Default cdreConfig profile @@ -804,35 +805,22 @@ func (self *CGRConfig) loadFromJsonCfg(jsnCfg *CgrJsonCfg) error { } } - if jsnSureTaxCfg != nil { - self.SureTaxCfg = new(SureTaxCfg) // Reset previous values - if jsnSureTaxCfg.Url != nil { - self.SureTaxCfg.Url = *jsnSureTaxCfg.Url + if jsnSureTaxCfg != nil { // New config for SureTax + if self.sureTaxCfg, err = NewSureTaxCfgWithDefaults(); err != nil { + return err } - if jsnSureTaxCfg.Client_number != nil { - self.SureTaxCfg.ClientNumber = *jsnSureTaxCfg.Client_number - } - if jsnSureTaxCfg.Validation_key != nil { - 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 - } + if err := self.sureTaxCfg.loadFromJsonCfg(jsnSureTaxCfg); err != nil { + return err } } + return nil } + +// Use locking to retrieve the configuration, possibility later for runtime reload +func (self *CGRConfig) SureTaxCfg() *SureTaxCfg { + cfgChan := <-self.ConfigReloads[utils.SURETAX] // Lock config for read or reloads + stCfg := self.sureTaxCfg + self.ConfigReloads[utils.SURETAX] <- cfgChan // unlock config for reloads or read + return stCfg +} diff --git a/config/config_defaults.go b/config/config_defaults.go index a604eb4a7..f59ec3f98 100644 --- a/config/config_defaults.go +++ b/config/config_defaults.go @@ -280,13 +280,31 @@ const CGRATES_CFG_JSON = ` "suretax": { - "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 + "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 + "return_file_code": "0", // default or Quote purposes <0|Q> + "response_group": "03", // determines how taxes are grouped for the response <03|13> + "response_type": "D4", // determines the granularity of taxes and (optionally) the decimal precision for the tax calculations and amounts in the response + "regulatory_code": "03", // provider type + "client_tracking": "CgrId", // template extracting client information out of StoredCdr; <$RSRFields> + "customer_number": "CustomerNumber", // template extracting customer number out of StoredCdr; <$RSRFields> + "orig_number": "Subject", // template extracting origination number out of StoredCdr; <$RSRFields> + "term_number": "Destination", // template extracting termination number out of StoredCdr; <$RSRFields> + "bill_to_number": "", // template extracting billed to number out of StoredCdr; <$RSRFields> + "zipcode": "", // template extracting billing zip code out of StoredCdr; <$RSRFields> + "plus4": "", // template extracting billing zip code extension out of StoredCdr; <$RSRFields> + "p2pzipcode": "", // template extracting secondary zip code out of StoredCdr; <$RSRFields> + "p2pplus4": "", // template extracting secondary zip code extension out of StoredCdr; <$RSRFields> + "units": "^1", // template extracting number of “lines” or unique charges contained within the revenue out of StoredCdr; <$RSRFields> + "unit_type": "^00", // template extracting number of unique access lines out of StoredCdr; <$RSRFields> + "tax_included": "^0", // template extracting tax included in revenue out of StoredCdr; <$RSRFields> + "tax_situs_rule": "^04", // template extracting tax situs rule out of StoredCdr; <$RSRFields> + "trans_type_code": "^010101", // template extracting transaction type indicator out of StoredCdr; <$RSRFields> + "sales_type_code": "^R", // template extracting sales type code out of StoredCdr; <$RSRFields> + "tax_exemption_code_list": "", // template extracting tax exemption code list out of StoredCdr; <$RSRFields> }, }` diff --git a/config/config_json_test.go b/config/config_json_test.go index 3b9580792..13a52de2b 100644 --- a/config/config_json_test.go +++ b/config/config_json_test.go @@ -459,13 +459,31 @@ func TestDfMailerJsonCfg(t *testing.T) { func TestDfSureTaxJsonCfg(t *testing.T) { eCfg := &SureTaxJsonCfg{ - 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"), + Url: utils.StringPointer(""), + Client_number: utils.StringPointer(""), + Validation_key: utils.StringPointer(""), + Timezone: utils.StringPointer("Local"), + Include_local_cost: utils.BoolPointer(false), + Return_file_code: utils.StringPointer("0"), + Response_group: utils.StringPointer("03"), + Response_type: utils.StringPointer("D4"), + Regulatory_code: utils.StringPointer("03"), + Client_tracking: utils.StringPointer("CgrId"), + Customer_number: utils.StringPointer("CustomerNumber"), + Orig_number: utils.StringPointer("Subject"), + Term_number: utils.StringPointer("Destination"), + Bill_to_number: utils.StringPointer(""), + Zipcode: utils.StringPointer(""), + Plus4: utils.StringPointer(""), + P2PZipcode: utils.StringPointer(""), + P2PPlus4: utils.StringPointer(""), + Units: utils.StringPointer("^1"), + Unit_type: utils.StringPointer("^00"), + Tax_included: utils.StringPointer("^0"), + Tax_situs_rule: utils.StringPointer("^04"), + Trans_type_code: utils.StringPointer("^010101"), + Sales_type_code: utils.StringPointer("^R"), + Tax_exemption_code_list: utils.StringPointer(""), } if cfg, err := dfCgrJsonCfg.SureTaxJsonCfg(); err != nil { t.Error(err) diff --git a/config/libconfig.go b/config/libconfig.go index 09744ee33..af5dfb6c4 100644 --- a/config/libconfig.go +++ b/config/libconfig.go @@ -22,7 +22,6 @@ import ( "fmt" "github.com/cgrates/cgrates/utils" "net/url" - "time" ) type CdrReplicationCfg struct { @@ -36,13 +35,3 @@ type CdrReplicationCfg struct { func (rplCfg CdrReplicationCfg) FallbackFileName() string { return fmt.Sprintf("cdr_%s_%s_%s.form", rplCfg.Transport, url.QueryEscape(rplCfg.Server), utils.GenUUID()) } - -type SureTaxCfg struct { - 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 9367ce538..d4b3f282e 100644 --- a/config/libconfig_json.go +++ b/config/libconfig_json.go @@ -256,11 +256,29 @@ 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 - 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 + Url *string + Client_number *string + Validation_key *string + Timezone *string + Include_local_cost *bool + Return_file_code *string + Response_group *string + Response_type *string + Regulatory_code *string + Client_tracking *string + Customer_number *string + Orig_number *string + Term_number *string + Bill_to_number *string + Zipcode *string + Plus4 *string + P2PZipcode *string + P2PPlus4 *string + Units *string + Unit_type *string + Tax_included *string + Tax_situs_rule *string + Trans_type_code *string + Sales_type_code *string + Tax_exemption_code_list *string } diff --git a/config/suretaxconfig.go b/config/suretaxconfig.go new file mode 100644 index 000000000..5ab6d23f7 --- /dev/null +++ b/config/suretaxconfig.go @@ -0,0 +1,186 @@ +/* +Real-time Charging System for Telecom & ISP environments +Copyright (C) ITsysCOM GmbH + +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 config + +import ( + "github.com/cgrates/cgrates/utils" + "strings" + "time" +) + +// Creates a new SureTaxCfg with defaults pre-populated out of config_defaults.json +func NewSureTaxCfgWithDefaults() (*SureTaxCfg, error) { + jsnCfg, err := NewCgrJsonCfgFromReader(strings.NewReader(CGRATES_CFG_JSON)) + if err != nil { + return nil, err + } + jsnSureTaxCfg, err := jsnCfg.SureTaxJsonCfg() + if err != nil { + return nil, err + } + st := new(SureTaxCfg) + if err := st.loadFromJsonCfg(jsnSureTaxCfg); err != nil { + return nil, err + } + return st, nil +} + +// SureTax configuration object +type SureTaxCfg struct { + Url string + ClientNumber string + ValidationKey string + Timezone *time.Location // Convert the time of the events to this timezone before sending request out + IncludeLocalCost bool + ReturnFileCode string + ResponseGroup string + ResponseType string + RegulatoryCode string + ClientTracking utils.RSRFields // Concatenate all of them to get value + CustomerNumber utils.RSRFields + OrigNumber utils.RSRFields + TermNumber utils.RSRFields + BillToNumber utils.RSRFields + Zipcode utils.RSRFields + Plus4 utils.RSRFields + P2PZipcode utils.RSRFields + P2PPlus4 utils.RSRFields + Units utils.RSRFields + UnitType utils.RSRFields + TaxIncluded utils.RSRFields + TaxSitusRule utils.RSRFields + TransTypeCode utils.RSRFields + SalesTypeCode utils.RSRFields + TaxExemptionCodeList utils.RSRFields +} + +// Loads/re-loads data from json config object +func (self *SureTaxCfg) loadFromJsonCfg(jsnCfg *SureTaxJsonCfg) error { + var err error + if jsnCfg.Url != nil { + self.Url = *jsnCfg.Url + } + if jsnCfg.Client_number != nil { + self.ClientNumber = *jsnCfg.Client_number + } + if jsnCfg.Validation_key != nil { + self.ValidationKey = *jsnCfg.Validation_key + } + if jsnCfg.Timezone != nil { + if self.Timezone, err = time.LoadLocation(*jsnCfg.Timezone); err != nil { + return err + } + } + if jsnCfg.Include_local_cost != nil { + self.IncludeLocalCost = *jsnCfg.Include_local_cost + } + if jsnCfg.Return_file_code != nil { + self.ReturnFileCode = *jsnCfg.Return_file_code + } + if jsnCfg.Response_group != nil { + self.ResponseGroup = *jsnCfg.Response_group + } + if jsnCfg.Response_type != nil { + self.ResponseType = *jsnCfg.Response_type + } + if jsnCfg.Regulatory_code != nil { + self.RegulatoryCode = *jsnCfg.Regulatory_code + } + if jsnCfg.Client_tracking != nil { + if self.ClientTracking, err = utils.ParseRSRFields(*jsnCfg.Client_tracking, utils.INFIELD_SEP); err != nil { + return err + } + } + if jsnCfg.Customer_number != nil { + if self.CustomerNumber, err = utils.ParseRSRFields(*jsnCfg.Customer_number, utils.INFIELD_SEP); err != nil { + return err + } + } + if jsnCfg.Orig_number != nil { + if self.OrigNumber, err = utils.ParseRSRFields(*jsnCfg.Orig_number, utils.INFIELD_SEP); err != nil { + return err + } + } + if jsnCfg.Term_number != nil { + if self.TermNumber, err = utils.ParseRSRFields(*jsnCfg.Term_number, utils.INFIELD_SEP); err != nil { + return err + } + } + if jsnCfg.Bill_to_number != nil { + if self.BillToNumber, err = utils.ParseRSRFields(*jsnCfg.Bill_to_number, utils.INFIELD_SEP); err != nil { + return err + } + } + if jsnCfg.Zipcode != nil { + if self.Zipcode, err = utils.ParseRSRFields(*jsnCfg.Zipcode, utils.INFIELD_SEP); err != nil { + return err + } + } + if jsnCfg.Plus4 != nil { + if self.Plus4, err = utils.ParseRSRFields(*jsnCfg.Plus4, utils.INFIELD_SEP); err != nil { + return err + } + } + if jsnCfg.P2PZipcode != nil { + if self.P2PZipcode, err = utils.ParseRSRFields(*jsnCfg.P2PZipcode, utils.INFIELD_SEP); err != nil { + return err + } + } + if jsnCfg.P2PPlus4 != nil { + if self.P2PPlus4, err = utils.ParseRSRFields(*jsnCfg.P2PPlus4, utils.INFIELD_SEP); err != nil { + return err + } + } + if jsnCfg.Units != nil { + if self.Units, err = utils.ParseRSRFields(*jsnCfg.Units, utils.INFIELD_SEP); err != nil { + return err + } + } + if jsnCfg.Unit_type != nil { + if self.UnitType, err = utils.ParseRSRFields(*jsnCfg.Unit_type, utils.INFIELD_SEP); err != nil { + return err + } + } + if jsnCfg.Tax_included != nil { + if self.TaxIncluded, err = utils.ParseRSRFields(*jsnCfg.Tax_included, utils.INFIELD_SEP); err != nil { + return err + } + } + if jsnCfg.Tax_situs_rule != nil { + if self.TaxSitusRule, err = utils.ParseRSRFields(*jsnCfg.Tax_situs_rule, utils.INFIELD_SEP); err != nil { + return err + } + } + if jsnCfg.Trans_type_code != nil { + if self.TransTypeCode, err = utils.ParseRSRFields(*jsnCfg.Trans_type_code, utils.INFIELD_SEP); err != nil { + return err + } + } + if jsnCfg.Sales_type_code != nil { + if self.SalesTypeCode, err = utils.ParseRSRFields(*jsnCfg.Sales_type_code, utils.INFIELD_SEP); err != nil { + return err + } + } + if jsnCfg.Tax_exemption_code_list != nil { + if self.TaxExemptionCodeList, err = utils.ParseRSRFields(*jsnCfg.Tax_exemption_code_list, utils.INFIELD_SEP); err != nil { + return err + } + } + return nil +} diff --git a/engine/suretax.go b/engine/suretax.go index 42364ac9d..9de873b6c 100644 --- a/engine/suretax.go +++ b/engine/suretax.go @@ -19,14 +19,89 @@ along with this program. If not, see package engine import ( + "bytes" + "crypto/tls" "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" "net/url" "strconv" - "time" + "strings" + "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/utils" ) +var sureTaxClient *http.Client // Cache the client here if in use + +// Init a new request to be sent out to SureTax +func NewSureTaxRequest(cdr *StoredCdr, stCfg *config.SureTaxCfg) (*SureTaxRequest, error) { + aTimeLoc := cdr.AnswerTime.In(stCfg.Timezone) + revenue := utils.Round(cdr.Cost, 4, utils.ROUNDING_MIDDLE) + unts, err := strconv.ParseInt(cdr.FieldsAsString(stCfg.Units), 10, 64) + if err != nil { + return nil, err + } + taxExempt := []string{} + definedTaxExtempt := cdr.FieldsAsString(stCfg.TaxExemptionCodeList) + if len(definedTaxExtempt) != 0 { + taxExempt = strings.Split(cdr.FieldsAsString(stCfg.TaxExemptionCodeList), ",") + } + stReq := new(SureTaxRequest) + stReq.ClientNumber = stCfg.ClientNumber + stReq.BusinessUnit = "" // Export it to config + stReq.ValidationKey = stCfg.ValidationKey + stReq.DataYear = strconv.Itoa(aTimeLoc.Year()) + stReq.DataMonth = strconv.Itoa(int(aTimeLoc.Month())) + stReq.TotalRevenue = revenue + stReq.ReturnFileCode = stCfg.ReturnFileCode + stReq.ClientTracking = cdr.FieldsAsString(stCfg.ClientTracking) + stReq.ResponseGroup = stCfg.ResponseGroup + stReq.ResponseType = stCfg.ResponseType + stReq.ItemList = []*STRequestItem{ + &STRequestItem{ + CustomerNumber: cdr.FieldsAsString(stCfg.CustomerNumber), + OrigNumber: cdr.FieldsAsString(stCfg.OrigNumber), + TermNumber: cdr.FieldsAsString(stCfg.TermNumber), + BillToNumber: cdr.FieldsAsString(stCfg.BillToNumber), + Zipcode: cdr.FieldsAsString(stCfg.Zipcode), + Plus4: cdr.FieldsAsString(stCfg.Plus4), + P2PZipcode: cdr.FieldsAsString(stCfg.P2PZipcode), + P2PPlus4: cdr.FieldsAsString(stCfg.P2PPlus4), + TransDate: aTimeLoc.Format("2006-01-02T15:04:05"), + Revenue: revenue, + Units: unts, + UnitType: cdr.FieldsAsString(stCfg.UnitType), + Seconds: int64(cdr.Usage.Seconds()), + TaxIncludedCode: cdr.FieldsAsString(stCfg.TaxIncluded), + TaxSitusRule: cdr.FieldsAsString(stCfg.TaxSitusRule), + TransTypeCode: cdr.FieldsAsString(stCfg.TransTypeCode), + SalesTypeCode: cdr.FieldsAsString(stCfg.SalesTypeCode), + RegulatoryCode: stCfg.RegulatoryCode, + TaxExemptionCodeList: taxExempt, + }, + } + 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 +} + // 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 @@ -52,62 +127,6 @@ type STRequestItem struct { 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) @@ -122,7 +141,7 @@ func (self *SureTaxRequest) AsHttpForm() (url.Values, error) { // 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. + 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. 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. @@ -152,3 +171,53 @@ type STTaxItem struct { TaxTypeDesc string // Tax Type Description TaxAmount float64 // Tax Amount } + +func SureTaxProcessCdr(cdr *StoredCdr) error { + stCfg := config.CgrConfig().SureTaxCfg() + if stCfg == nil { + return errors.New("SureTax configuration missing") + } + if sureTaxClient == nil { // First time used, init the client here + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: config.CgrConfig().HttpSkipTlsVerify}, + } + sureTaxClient = &http.Client{Transport: tr} + } + req, err := NewSureTaxRequest(cdr, stCfg) + if err != nil { + return err + } + body, err := json.Marshal(req) + if err != nil { + return err + } + resp, err := sureTaxClient.Post(stCfg.Url, "application/json", bytes.NewBuffer(body)) + if err != nil { + return err + } + defer resp.Body.Close() + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + if resp.StatusCode > 299 { + return fmt.Errorf("Unexpected status code received: %d", resp.StatusCode) + } + var stResp SureTaxResponse + if err := json.Unmarshal(respBody, &stResp); err != nil { + return err + } + if stResp.ResponseCode != 9999 { + cdr.ExtraInfo = stResp.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) + } else { + cdr.Cost = utils.Round(cdr.Cost+stResp.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) + return nil +} diff --git a/engine/suretax_test.go b/engine/suretax_test.go index 6bdb6d586..4482f4bf1 100644 --- a/engine/suretax_test.go +++ b/engine/suretax_test.go @@ -28,47 +28,48 @@ import ( ) 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, + cgrId := utils.Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC).String()) + cdr := &StoredCdr{CgrId: cgrId, 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 + stCfg := cfg.SureTaxCfg() + stCfg.ClientNumber = "000000000" + stCfg.ValidationKey = "19491161-F004-4F44-BDB3-E976D6739A64" + stCfg.Timezone = time.UTC eSureTaxRequest := &SureTaxRequest{ - ClientNumber: cfg.SureTaxCfg.ClientNumber, - ValidationKey: cfg.SureTaxCfg.ValidationKey, + ClientNumber: "000000000", + ValidationKey: "19491161-F004-4F44-BDB3-E976D6739A64", DataYear: "2013", DataMonth: "11", TotalRevenue: 1.01, ReturnFileCode: "0", - ClientTracking: storedCdr.CgrId, + ClientTracking: cgrId, ResponseGroup: "03", - ResponseType: "", + ResponseType: "D4", ItemList: []*STRequestItem{ &STRequestItem{ OrigNumber: "1001", TermNumber: "1002", - BillToNumber: "1001", + BillToNumber: "", TransDate: "2013-11-07T08:42:26", Revenue: 1.01, Units: 1, UnitType: "00", Seconds: 12, TaxIncludedCode: "0", - TaxSitusRule: "1", + TaxSitusRule: "04", TransTypeCode: "010101", SalesTypeCode: "R", - RegulatoryCode: "01", - TaxExemptionCodeList: []string{"00"}, + RegulatoryCode: "03", + TaxExemptionCodeList: []string{}, }, }, } - if stReq, err := NewSureTaxRequest(cfg.SureTaxCfg.ClientNumber, cfg.SureTaxCfg.ValidationKey, cfg.SureTaxCfg.Timezone, cfg.SureTaxCfg.OriginationNumber, - cfg.SureTaxCfg.TerminationNumber, storedCdr); err != nil { + 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]) diff --git a/utils/consts.go b/utils/consts.go index 1ed4e1c22..fe44cb630 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -235,6 +235,8 @@ const ( NOT_AVAILABLE = "N/A" CALL = "call" EXTRA_FIELDS = "ExtraFields" + META_SURETAX = "*sure_tax" + SURETAX = "suretax" ) var (