diff --git a/config/config.go b/config/config.go index 422906a6f..9920290e4 100644 --- a/config/config.go +++ b/config/config.go @@ -116,6 +116,7 @@ type CGRConfig struct { SMRater string // address where to access rater. Can be internal, direct rater address or the address of a balancer SMRaterReconnects int // Number of reconnect attempts to rater SMDebitInterval int // the period to be debited in advanced during a call (in seconds) + SMMaxCallDuration time.Duration // The maximum duration of a call MediatorEnabled bool // Starts Mediator service: . MediatorListen string // Mediator's listening interface: . MediatorRater string // Address where to reach the Rater: @@ -219,6 +220,7 @@ func (self *CGRConfig) setDefaults() error { self.SMRater = "127.0.0.1:2012" self.SMRaterReconnects = 3 self.SMDebitInterval = 10 + self.SMMaxCallDuration = time.Duration(3) * time.Hour self.FreeswitchServer = "127.0.0.1:8021" self.FreeswitchPass = "ClueCon" self.FreeswitchReconnects = 5 @@ -519,6 +521,12 @@ func loadConfig(c *conf.ConfigFile) (*CGRConfig, error) { if hasOpt = c.HasOption("session_manager", "debit_interval"); hasOpt { cfg.SMDebitInterval, _ = c.GetInt("session_manager", "debit_interval") } + if hasOpt = c.HasOption("session_manager", "max_call_duration"); hasOpt { + maxCallDurStr,_ := c.GetString("session_manager", "max_call_duration") + if cfg.SMMaxCallDuration, errParse = utils.ParseDurationWithSecs(maxCallDurStr); errParse != nil { + return nil, errParse + } + } if hasOpt = c.HasOption("freeswitch", "server"); hasOpt { cfg.FreeswitchServer, _ = c.GetString("freeswitch", "server") } diff --git a/config/config_test.go b/config/config_test.go index 99ddbf056..5dd1d6f38 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -123,6 +123,7 @@ func TestDefaults(t *testing.T) { eCfg.SMRater = "127.0.0.1:2012" eCfg.SMRaterReconnects = 3 eCfg.SMDebitInterval = 10 + eCfg.SMMaxCallDuration = time.Time(3) * time.Hour eCfg.FreeswitchServer = "127.0.0.1:8021" eCfg.FreeswitchPass = "ClueCon" eCfg.FreeswitchReconnects = 5 @@ -245,6 +246,7 @@ func TestConfigFromFile(t *testing.T) { eCfg.SMRater = "test" eCfg.SMRaterReconnects = 99 eCfg.SMDebitInterval = 99 + eCfg.SMMaxCallDuration = "test" eCfg.FreeswitchServer = "test" eCfg.FreeswitchPass = "test" eCfg.FreeswitchReconnects = 99 diff --git a/config/test_data.txt b/config/test_data.txt index 2636b4474..f3372761a 100644 --- a/config/test_data.txt +++ b/config/test_data.txt @@ -94,8 +94,9 @@ duration_fields = test # Name of duration fields to be used during mediation. enabled = true # Starts SessionManager service: . switch_type = test # Defines the type of switch behind: . rater = test # Address where to reach the Rater. -rater_reconnects = 99 # Number of reconnects to rater before giving up. -debit_interval = 99 # Interval to perform debits on. +rater_reconnects = 99 # Number of reconnects to rater before giving up. +debit_interval = 99 # Interval to perform debits on. +max_call_duration = test # Maximum call duration a prepaid call can last [freeswitch] server = test # Adress where to connect to FreeSWITCH socket. diff --git a/data/conf/cgrates.cfg b/data/conf/cgrates.cfg index d2724292f..1af1f445f 100644 --- a/data/conf/cgrates.cfg +++ b/data/conf/cgrates.cfg @@ -97,7 +97,8 @@ # switch_type = freeswitch # Defines the type of switch behind: . # rater = 127.0.0.1:2012 # Address where to reach the Rater. # rater_reconnects = 3 # Number of reconnects to rater before giving up. -# debit_interval = 5 # Interval to perform debits on. +# debit_interval = 10 # Interval to perform debits on. +# max_call_duration = 3h # Maximum call duration a prepaid call can last [freeswitch] # server = 127.0.0.1:8021 # Adress where to connect to FreeSWITCH socket. diff --git a/data/storage/mysql/create_costdetails_tables.sql b/data/storage/mysql/create_costdetails_tables.sql index d55cb7a34..774bd27cc 100644 --- a/data/storage/mysql/create_costdetails_tables.sql +++ b/data/storage/mysql/create_costdetails_tables.sql @@ -14,8 +14,8 @@ CREATE TABLE `cost_details` ( `account` varchar(128) NOT NULL, `subject` varchar(128) NOT NULL, `destination` varchar(128) NOT NULL, - `cost` DECIMAL(20,4) NOT NULL, `connect_fee` DECIMAL(5,4) NOT NULL, + `cost` DECIMAL(20,4) NOT NULL, `timespans` text, `source` varchar(64) NOT NULL, `runid` varchar(64) NOT NULL, diff --git a/data/tutorials/fs_csv/freeswitch/etc/freeswitch/dialplan/default.xml b/data/tutorials/fs_csv/freeswitch/etc/freeswitch/dialplan/default.xml index 8bc0ea41b..cfdc0a9e4 100644 --- a/data/tutorials/fs_csv/freeswitch/etc/freeswitch/dialplan/default.xml +++ b/data/tutorials/fs_csv/freeswitch/etc/freeswitch/dialplan/default.xml @@ -254,7 +254,7 @@ --> - + @@ -262,7 +262,7 @@ - + @@ -272,7 +272,7 @@ - + diff --git a/docs/tut_freeswitch_csv.rst b/docs/tut_freeswitch_csv.rst index 70b57caed..654e6e4a0 100644 --- a/docs/tut_freeswitch_csv.rst +++ b/docs/tut_freeswitch_csv.rst @@ -13,10 +13,10 @@ Scenario - **CGRateS** with following components: - CGR-SM started as prepaid controller, with debits taking place at 5s intervals. - - CGR-CDRC component importing FreeSWITCH_ generated *.csv* CDRs into CGR (moving the processed *.csv* files to */tmp* folder). - - CGR-Mediator compoenent attaching costs to the raw CDRs from CGR-CDRC. - - CGR-CDRE exporting mediated CDRs (export path: */tmp*). - - CGR-History component keeping the archive of the rates modifications (path: */tmp/cgr_history*). + - CGR-CDRC component importing FreeSWITCH_ generated *.csv* CDRs into CGR and moving the processed *.csv* files to */tmp* folder. + - CGR-Mediator compoenent attaching costs to the raw CDRs from CGR-CDRC inside CGR StorDB. + - CGR-CDRE exporting mediated CDRs from CGR StorDB (export path: */tmp*). + - CGR-History component keeping the archive of the rates modifications (path browsable with git client at */tmp/cgr_history*). Starting FreeSWITCH_ with custom configuration @@ -103,14 +103,34 @@ To verify that all actions successfully performed, we use following *cgr-console Test calls ---------- -Calling between 1001 and 1003 should generate prepaid debits which can be checked with *get_balance* command integrated within *cgr-console* tool. The difference between calling from 1001 or 1003 should be reflected in fact that 1001 will generate real-time debits as opposite to 1003 which will only generate debits when CDRs will be processed. + +1001 -> 1002 +~~~~~~~~~~~~ + +Since the user 1001 is marked as prepaid inside FreeSWITCH_ directory configuration, calling between 1001 and 1002 should generate prepaid debits which can be checked with *get_balance* command integrated within *cgr-console* tool. As per our tariff plans, we should get first 60s charged as a whole, then in intervals of 1s (configured SessionManager debit interval of 10s). + +*Note*: An important particularity to note here is the ability of **CGRateS** SessionManager to refund units booked in advance (eg: if debit occurs every 10s and rate increments are set to 1s, the SessionManager will be smart enough to refund pre-booked credits for calls stoped in the middle of debit interval). + +Check that 1001 balance is properly debitted, during the call: :: cgr-console get_balance cgrates.org 1001 + + +1002 -> 1001 +~~~~~~~~~~~~ + +The user 1002 is marked as postpaid inside FreeSWITCH_ hence his calls will be debited at the end of the call instead of during a call and his balance will be able to go on negative without influencing his calls. + +To check that we had debits we use again console command, this time not during the call but at the end of it: + +:: + cgr-console get_balance cgrates.org 1002 + CDR processing -------------- diff --git a/engine/storage_sql.go b/engine/storage_sql.go index 539c41a29..0f4e34b78 100644 --- a/engine/storage_sql.go +++ b/engine/storage_sql.go @@ -646,7 +646,7 @@ func (self *SQLStorage) LogCallCost(uuid, source, runid string, cc *CallCost) (e if err != nil { Logger.Err(fmt.Sprintf("Error marshalling timespans to json: %v", err)) } - _, err = self.Db.Exec(fmt.Sprintf("INSERT INTO %s (cgrid, accid, direction, tenant, tor, account, subject, destination, cost, connect_fee, timespans, source, runid)VALUES ('%s', '%s','%s', '%s', '%s', '%s', '%s', '%s', %f, %f, '%s','%s','%s')", + _, err = self.Db.Exec(fmt.Sprintf("INSERT INTO %s (cgrid, accid, direction, tenant, tor, account, subject, destination, connect_fee, cost, timespans, source, runid)VALUES ('%s', '%s','%s', '%s', '%s', '%s', '%s', '%s', %f, %f, '%s','%s','%s')", utils.TBL_COST_DETAILS, utils.FSCgrId(uuid), uuid, @@ -656,8 +656,8 @@ func (self *SQLStorage) LogCallCost(uuid, source, runid string, cc *CallCost) (e cc.Account, cc.Subject, cc.Destination, - cc.Cost, cc.ConnectFee, + cc.Cost, tss, source, runid)) @@ -668,12 +668,12 @@ func (self *SQLStorage) LogCallCost(uuid, source, runid string, cc *CallCost) (e } func (self *SQLStorage) GetCallCostLog(cgrid, source, runid string) (cc *CallCost, err error) { - row := self.Db.QueryRow(fmt.Sprintf("SELECT cgrid, accid, direction, tenant, tor, account, subject, destination, cost, connect_fee, timespans, source FROM %s WHERE cgrid='%s' AND source='%s'", utils.TBL_COST_DETAILS, cgrid, source, runid)) + row := self.Db.QueryRow(fmt.Sprintf("SELECT cgrid, accid, direction, tenant, tor, account, subject, destination, connect_fee, cost, timespans, source FROM %s WHERE cgrid='%s' AND source='%s'", utils.TBL_COST_DETAILS, cgrid, source, runid)) var accid, src string var timespansJson string cc = &CallCost{Cost: -1} err = row.Scan(&cgrid, &accid, &cc.Direction, &cc.Tenant, &cc.TOR, &cc.Account, &cc.Subject, - &cc.Destination, &cc.Cost, &cc.ConnectFee, ×pansJson, &src) + &cc.Destination, &cc.ConnectFee, &cc.Cost, ×pansJson, &src) if err = json.Unmarshal([]byte(timespansJson), &cc.Timespans); err != nil { return nil, err } diff --git a/sessionmanager/fssessionmanager.go b/sessionmanager/fssessionmanager.go index e6c3cfc70..fcc10e50d 100644 --- a/sessionmanager/fssessionmanager.go +++ b/sessionmanager/fssessionmanager.go @@ -101,7 +101,7 @@ func (sm *FSSessionManager) GetSession(uuid string) *Session { // Disconnects a session by sending hangup command to freeswitch func (sm *FSSessionManager) DisconnectSession(s *Session, notify string) { - engine.Logger.Debug(fmt.Sprintf("Session: %+v", s.uuid)) + // engine.Logger.Debug(fmt.Sprintf("Session: %+v", s.uuid)) err := fsock.FS.SendApiCmd(fmt.Sprintf("uuid_setvar %s cgr_notify %s\n\n", s.uuid, notify)) if err != nil { engine.Logger.Err(fmt.Sprintf("could not send disconect api notification to freeswitch: %v", err)) @@ -123,6 +123,17 @@ func (sm *FSSessionManager) RemoveSession(s *Session) { } } +// Sets the call timeout valid of starting of the call +func (sm *FSSessionManager) setMaxCallDuration(uuid string, maxDur time.Duration) error { + err := fsock.FS.SendApiCmd(fmt.Sprintf("sched_hangup +%d %s\n\n", int(maxDur.Seconds()), uuid)) + if err != nil { + engine.Logger.Err("could not send sched_hangup command to freeswitch") + return err + } + return nil +} + + // Sends the transfer command to unpark the call to freeswitch func (sm *FSSessionManager) unparkCall(uuid, call_dest_nb, notify string) { err := fsock.FS.SendApiCmd(fmt.Sprintf("uuid_setvar %s cgr_notify %s\n\n", uuid, notify)) @@ -140,7 +151,7 @@ func (sm *FSSessionManager) OnHeartBeat(ev Event) { } func (sm *FSSessionManager) OnChannelPark(ev Event) { - engine.Logger.Info("freeswitch park") + //engine.Logger.Info("freeswitch park") startTime, err := ev.GetStartTime(PARK_TIME) if err != nil { engine.Logger.Err("Error parsing answer event start time, using time.Now!") @@ -162,8 +173,9 @@ func (sm *FSSessionManager) OnChannelPark(ev Event) { Subject: ev.GetSubject(), Account: ev.GetAccount(), Destination: ev.GetDestination(), - Amount: sm.debitPeriod.Seconds(), - TimeStart: startTime} + TimeStart: startTime, + TimeEnd: startTime.Add(cfg.SMMaxCallDuration), + } var remainingDurationFloat float64 err = sm.connector.GetMaxSessionTime(cd, &remainingDurationFloat) if err != nil { @@ -172,17 +184,18 @@ func (sm *FSSessionManager) OnChannelPark(ev Event) { return } remainingDuration := time.Duration(remainingDurationFloat) - engine.Logger.Info(fmt.Sprintf("Remaining seconds: %v", remainingDuration)) + //engine.Logger.Info(fmt.Sprintf("Remaining duration: %v", remainingDuration)) if remainingDuration == 0 { - engine.Logger.Info(fmt.Sprintf("Not enough credit for trasferring the call %s for %s.", ev.GetUUID(), cd.GetKey(cd.Subject))) + //engine.Logger.Info(fmt.Sprintf("Not enough credit for trasferring the call %s for %s.", ev.GetUUID(), cd.GetKey(cd.Subject))) sm.unparkCall(ev.GetUUID(), ev.GetCallDestNr(), INSUFFICIENT_FUNDS) return } + sm.setMaxCallDuration(ev.GetUUID(), remainingDuration) sm.unparkCall(ev.GetUUID(), ev.GetCallDestNr(), AUTH_OK) } func (sm *FSSessionManager) OnChannelAnswer(ev Event) { - engine.Logger.Info(" FreeSWITCH answer.") + //engine.Logger.Info(" FreeSWITCH answer.") // Make sure cgr_type is enforced even if not set by FreeSWITCH if err := fsock.FS.SendApiCmd(fmt.Sprintf("uuid_setvar %s cgr_reqtype %s\n\n", ev.GetUUID(), ev.GetReqType())); err != nil { engine.Logger.Err(fmt.Sprintf("Error on attempting to overwrite cgr_type in chan variables: %v", err)) @@ -194,7 +207,7 @@ func (sm *FSSessionManager) OnChannelAnswer(ev Event) { } func (sm *FSSessionManager) OnChannelHangupComplete(ev Event) { - engine.Logger.Info(" FreeSWITCH hangup.") + //engine.Logger.Info(" FreeSWITCH hangup.") s := sm.GetSession(ev.GetUUID()) if s == nil { // Not handled by us return @@ -309,7 +322,7 @@ func (sm *FSSessionManager) LoopAction(s *Session, cd *engine.CallDescriptor, in } engine.Logger.Debug(fmt.Sprintf("Result of MaxDebit call: %v", cc)) if cc.GetDuration() == 0 || err != nil { - engine.Logger.Info(fmt.Sprintf("No credit left: Disconnect %v", s)) + // engine.Logger.Info(fmt.Sprintf("No credit left: Disconnect %v", s)) sm.DisconnectSession(s, INSUFFICIENT_FUNDS) return }