Merge branch 'master' into hapool

This commit is contained in:
Radu Ioan Fericean
2016-01-21 17:06:29 +02:00
56 changed files with 881 additions and 201 deletions

View File

@@ -26,6 +26,7 @@ import (
"github.com/cgrates/cgrates/cache2go"
"github.com/cgrates/cgrates/utils"
"github.com/cgrates/structmatcher"
"strings"
)
@@ -176,9 +177,11 @@ func (ub *Account) enableDisableBalanceAction(a *Action) error {
}
found := false
id := a.BalanceType
disabled := a.Balance.Disabled
a.Balance.Disabled = !disabled // match for the opposite
for _, b := range ub.BalanceMap[id] {
if b.MatchFilter(a.Balance, false) {
b.Disabled = a.Balance.Disabled
b.Disabled = disabled
b.dirty = true
found = true
}
@@ -316,9 +319,13 @@ func (ub *Account) debitCreditBalance(cd *CallDescriptor, count bool, dryRun boo
return
}
}
// check for blocker
if dryRun && balance.Blocker {
//log.Print("BLOCKER!")
return // don't go to next balances
}
}
}
// debit money
moneyBalanceChecker := true
for moneyBalanceChecker {
@@ -332,14 +339,13 @@ func (ub *Account) debitCreditBalance(cd *CallDescriptor, count bool, dryRun boo
return nil, debitErr
}
//utils.Logger.Info(fmt.Sprintf("CD AFTER MONEY: %+v", cd))
//log.Printf("partCC: %+v", partCC)
if partCC != nil {
cc.Timespans = append(cc.Timespans, partCC.Timespans...)
cc.negativeConnectFee = partCC.negativeConnectFee
//for i, ts := range cc.Timespans {
//log.Printf("cc.times[an[%d]: %+v\n", i, ts)
//}
/*for i, ts := range cc.Timespans {
log.Printf("cc.times[an[%d]: %+v\n", i, ts)
}*/
cd.TimeStart = cc.GetEndTime()
//log.Printf("CD: %+v", cd)
//log.Printf("CD: %+v - %+v", cd.TimeStart, cd.TimeEnd)
@@ -354,6 +360,11 @@ func (ub *Account) debitCreditBalance(cd *CallDescriptor, count bool, dryRun boo
return
}
}
// check for blocker
if dryRun && balance.Blocker {
//log.Print("BLOCKER!")
return // don't go to next balances
}
}
}
//log.Printf("END CD: %+v", cd)
@@ -687,6 +698,31 @@ func (acc *Account) DebitConnectionFee(cc *CallCost, usefulMoneyBalances Balance
}
}
func (acc *Account) matchActionFilter(condition string) (bool, error) {
sm, err := structmatcher.NewStructMatcher(condition)
if err != nil {
return false, err
}
for balanceType, balanceChain := range acc.BalanceMap {
for _, b := range balanceChain {
check, err := sm.Match(&struct {
Type string
*Balance
}{
Type: balanceType,
Balance: b,
})
if err != nil {
return false, err
}
if check {
return true, nil
}
}
}
return false, nil
}
// used in some api for transition
func (acc *Account) AsOldStructure() interface{} {
type Balance struct {

View File

@@ -42,7 +42,8 @@ type Action struct {
ActionType string
BalanceType string
ExtraParameters string
ExpirationString string
Filter string
ExpirationString string // must stay as string because it can have relative values like 1month
Weight float64
Balance *Balance
}
@@ -131,9 +132,9 @@ func getActionFunc(typ string) (actionTypeFunc, bool) {
case SET_DDESTINATIONS:
return setddestinations, true
case REMOVE_ACCOUNT:
return removeAccount, true
return removeAccountAction, true
case REMOVE_BALANCE:
return removeBalance, true
return removeBalanceAction, true
case TRANSFER_MONETARY_DEFAULT:
return transferMonetaryDefault, true
}
@@ -522,7 +523,7 @@ func setddestinations(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actio
return nil
}
func removeAccount(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error {
func removeAccountAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error {
var accID string
if ub != nil {
accID = ub.Id
@@ -568,7 +569,7 @@ func removeAccount(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions)
return nil
}
func removeBalance(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error {
func removeBalanceAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error {
if _, exists := ub.BalanceMap[a.BalanceType]; !exists {
return utils.ErrNotFound
}
@@ -592,6 +593,10 @@ func removeBalance(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions)
}
func transferMonetaryDefault(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error {
if acc == nil {
utils.Logger.Err("*transfer_monetary_default called without account")
return utils.ErrAccountNotFound
}
if _, exists := acc.BalanceMap[utils.MONETARY]; !exists {
return utils.ErrNotFound
}
@@ -599,7 +604,8 @@ func transferMonetaryDefault(acc *Account, sq *StatsQueueTriggered, a *Action, a
bChain := acc.BalanceMap[utils.MONETARY]
for _, balance := range bChain {
if balance.Uuid != defaultBalance.Uuid &&
balance.Id != defaultBalance.Id { // extra caution
balance.Id != defaultBalance.Id && // extra caution
balance.MatchFilter(a.Balance, false) {
if balance.Value > 0 {
defaultBalance.Value += balance.Value
balance.Value = 0

View File

@@ -291,6 +291,16 @@ func (at *ActionTiming) Execute() (err error) {
transactionFailed := false
removeAccountActionFound := false
for _, a := range aac {
// check action filter
if len(a.Filter) > 0 {
matched, err := ub.matchActionFilter(a.Filter)
if err != nil {
return 0, err
}
if !matched {
continue
}
}
if ub.Disabled && a.ActionType != ENABLE_ACCOUNT {
continue // disabled acocunts are not removed from action plan
//return 0, fmt.Errorf("Account %s is disabled", accID)

View File

@@ -46,7 +46,8 @@ type ActionTrigger struct {
BalanceRatingSubject string // filter for balance
BalanceCategories utils.StringMap // filter for balance
BalanceSharedGroups utils.StringMap // filter for balance
BalanceDisabled bool // filter for balance
BalanceBlocker bool
BalanceDisabled bool // filter for balance
Weight float64
ActionsId string
MinQueuedItems int // Trigger actions only if this number is hit (stats only)
@@ -75,6 +76,17 @@ func (at *ActionTrigger) Execute(ub *Account, sq *StatsQueueTriggered) (err erro
transactionFailed := false
removeAccountActionFound := false
for _, a := range aac {
// check action filter
if len(a.Filter) > 0 {
matched, err := ub.matchActionFilter(a.Filter)
if err != nil {
return err
}
if !matched {
continue
}
}
if a.Balance == nil {
a.Balance = &Balance{}
}
@@ -117,7 +129,7 @@ func (at *ActionTrigger) Match(a *Action) bool {
return match
}
id := a.BalanceType == "" || at.BalanceType == a.BalanceType
thresholdType, thresholdValue, direction, destinationId, weight, ratingSubject, categories, sharedGroup, timings, disabled := true, true, true, true, true, true, true, true, true, true
thresholdType, thresholdValue, direction, destinationId, weight, ratingSubject, categories, sharedGroup, timings, blocker, disabled := true, true, true, true, true, true, true, true, true, true, true
if a.ExtraParameters != "" {
t := struct {
ThresholdType string
@@ -129,6 +141,7 @@ func (at *ActionTrigger) Match(a *Action) bool {
BalanceCategories string
BalanceSharedGroups string
BalanceTimingTags string
BalanceBlocker bool
BalanceDisabled bool
}{}
json.Unmarshal([]byte(a.ExtraParameters), &t)
@@ -141,9 +154,10 @@ func (at *ActionTrigger) Match(a *Action) bool {
sharedGroup = len(t.BalanceSharedGroups) == 0 || at.BalanceSharedGroups.Equal(utils.ParseStringMap(t.BalanceSharedGroups))
weight = t.BalanceWeight == 0 || at.BalanceWeight == t.BalanceWeight
ratingSubject = t.BalanceRatingSubject == "" || at.BalanceRatingSubject == t.BalanceRatingSubject
blocker = at.BalanceBlocker == t.BalanceBlocker
disabled = at.BalanceDisabled == t.BalanceDisabled
}
return id && direction && thresholdType && thresholdValue && destinationId && weight && ratingSubject && categories && sharedGroup && timings && disabled
return id && direction && thresholdType && thresholdValue && destinationId && weight && ratingSubject && categories && sharedGroup && timings && blocker && disabled
}
// makes a shallow copy of the receiver
@@ -163,6 +177,7 @@ func (at *ActionTrigger) CreateBalance() *Balance {
Categories: at.BalanceCategories,
SharedGroups: at.BalanceSharedGroups,
TimingIDs: at.BalanceTimingTags,
Blocker: at.BalanceBlocker,
Disabled: at.BalanceDisabled,
Weight: at.BalanceWeight,
}

View File

@@ -1507,6 +1507,258 @@ func TestActionTransferMonetaryDefault(t *testing.T) {
}
}
func TestActionTransferMonetaryDefaultFilter(t *testing.T) {
err := accountingStorage.SetAccount(
&Account{
Id: "cgrates.org:trans",
BalanceMap: map[string]BalanceChain{
utils.MONETARY: BalanceChain{
&Balance{
Uuid: utils.GenUUID(),
Id: utils.META_DEFAULT,
Value: 10,
Weight: 20,
},
&Balance{
Uuid: utils.GenUUID(),
Value: 3,
Weight: 20,
},
&Balance{
Uuid: utils.GenUUID(),
Value: 1,
Weight: 10,
},
&Balance{
Uuid: utils.GenUUID(),
Value: 6,
Weight: 20,
},
},
},
})
if err != nil {
t.Errorf("error setting account: %v", err)
}
a := &Action{
ActionType: TRANSFER_MONETARY_DEFAULT,
Balance: &Balance{Weight: 20},
}
at := &ActionTiming{
accountIDs: map[string]struct{}{"cgrates.org:trans": struct{}{}},
actions: Actions{a},
}
at.Execute()
afterUb, err := accountingStorage.GetAccount("cgrates.org:trans")
if err != nil {
t.Error("account not found: ", err, afterUb)
}
if afterUb.BalanceMap[utils.MONETARY].GetTotalValue() != 20 ||
afterUb.BalanceMap[utils.MONETARY][0].Value != 19 ||
afterUb.BalanceMap[utils.MONETARY][1].Value != 0 ||
afterUb.BalanceMap[utils.MONETARY][2].Value != 1 ||
afterUb.BalanceMap[utils.MONETARY][3].Value != 0 {
for _, b := range afterUb.BalanceMap[utils.MONETARY] {
t.Logf("B: %+v", b)
}
t.Error("ransfer balance value: ", afterUb.BalanceMap[utils.MONETARY].GetTotalValue())
}
}
func TestActionConditionalTopup(t *testing.T) {
err := accountingStorage.SetAccount(
&Account{
Id: "cgrates.org:cond",
BalanceMap: map[string]BalanceChain{
utils.MONETARY: BalanceChain{
&Balance{
Uuid: utils.GenUUID(),
Id: utils.META_DEFAULT,
Value: 10,
Weight: 20,
},
&Balance{
Uuid: utils.GenUUID(),
Value: 3,
Weight: 20,
},
&Balance{
Uuid: utils.GenUUID(),
Value: 1,
Weight: 10,
},
&Balance{
Uuid: utils.GenUUID(),
Value: 6,
Weight: 20,
},
},
},
})
if err != nil {
t.Errorf("error setting account: %v", err)
}
a := &Action{
ActionType: TOPUP,
BalanceType: utils.MONETARY,
Filter: `{"Type":"*monetary","Value":1,"Weight":10}`,
Balance: &Balance{
Value: 11,
Weight: 30,
},
}
at := &ActionTiming{
accountIDs: map[string]struct{}{"cgrates.org:cond": struct{}{}},
actions: Actions{a},
}
at.Execute()
afterUb, err := accountingStorage.GetAccount("cgrates.org:cond")
if err != nil {
t.Error("account not found: ", err, afterUb)
}
if len(afterUb.BalanceMap[utils.MONETARY]) != 5 ||
afterUb.BalanceMap[utils.MONETARY].GetTotalValue() != 31 ||
afterUb.BalanceMap[utils.MONETARY][4].Value != 11 {
for _, b := range afterUb.BalanceMap[utils.MONETARY] {
t.Logf("B: %+v", b)
}
t.Error("ransfer balance value: ", afterUb.BalanceMap[utils.MONETARY].GetTotalValue())
}
}
func TestActionConditionalTopupNoMatch(t *testing.T) {
err := accountingStorage.SetAccount(
&Account{
Id: "cgrates.org:cond",
BalanceMap: map[string]BalanceChain{
utils.MONETARY: BalanceChain{
&Balance{
Uuid: utils.GenUUID(),
Id: utils.META_DEFAULT,
Value: 10,
Weight: 20,
},
&Balance{
Uuid: utils.GenUUID(),
Value: 3,
Weight: 20,
},
&Balance{
Uuid: utils.GenUUID(),
Value: 1,
Weight: 10,
},
&Balance{
Uuid: utils.GenUUID(),
Value: 6,
Weight: 20,
},
},
},
})
if err != nil {
t.Errorf("error setting account: %v", err)
}
a := &Action{
ActionType: TOPUP,
BalanceType: utils.MONETARY,
Filter: `{"Type":"*monetary","Value":2,"Weight":10}`,
Balance: &Balance{
Value: 11,
Weight: 30,
},
}
at := &ActionTiming{
accountIDs: map[string]struct{}{"cgrates.org:cond": struct{}{}},
actions: Actions{a},
}
at.Execute()
afterUb, err := accountingStorage.GetAccount("cgrates.org:cond")
if err != nil {
t.Error("account not found: ", err, afterUb)
}
if len(afterUb.BalanceMap[utils.MONETARY]) != 4 ||
afterUb.BalanceMap[utils.MONETARY].GetTotalValue() != 20 {
for _, b := range afterUb.BalanceMap[utils.MONETARY] {
t.Logf("B: %+v", b)
}
t.Error("ransfer balance value: ", afterUb.BalanceMap[utils.MONETARY].GetTotalValue())
}
}
func TestActionConditionalTopupExistingBalance(t *testing.T) {
err := accountingStorage.SetAccount(
&Account{
Id: "cgrates.org:cond",
BalanceMap: map[string]BalanceChain{
utils.MONETARY: BalanceChain{
&Balance{
Uuid: utils.GenUUID(),
Value: 1,
Weight: 10,
},
&Balance{
Uuid: utils.GenUUID(),
Value: 6,
Weight: 20,
},
},
utils.VOICE: BalanceChain{
&Balance{
Uuid: utils.GenUUID(),
Value: 10,
Weight: 10,
},
&Balance{
Uuid: utils.GenUUID(),
Value: 100,
Weight: 20,
},
},
},
})
if err != nil {
t.Errorf("error setting account: %v", err)
}
a := &Action{
ActionType: TOPUP,
BalanceType: utils.MONETARY,
Filter: `{"Type":"*voice","Value":{"*gte":100}}`,
Balance: &Balance{
Value: 11,
Weight: 10,
},
}
at := &ActionTiming{
accountIDs: map[string]struct{}{"cgrates.org:cond": struct{}{}},
actions: Actions{a},
}
at.Execute()
afterUb, err := accountingStorage.GetAccount("cgrates.org:cond")
if err != nil {
t.Error("account not found: ", err, afterUb)
}
if len(afterUb.BalanceMap[utils.MONETARY]) != 2 ||
afterUb.BalanceMap[utils.MONETARY].GetTotalValue() != 18 {
for _, b := range afterUb.BalanceMap[utils.MONETARY] {
t.Logf("B: %+v", b)
}
t.Error("ransfer balance value: ", afterUb.BalanceMap[utils.MONETARY].GetTotalValue())
}
}
/**************** Benchmarks ********************************/
func BenchmarkUUID(b *testing.B) {

View File

@@ -46,6 +46,7 @@ type Balance struct {
TimingIDs utils.StringMap
Disabled bool
Factor ValueFactor
Blocker bool
precision int
account *Account // used to store ub reference for shared balances
dirty bool
@@ -67,10 +68,14 @@ func (b *Balance) Equal(o *Balance) bool {
b.RatingSubject == o.RatingSubject &&
b.Categories.Equal(o.Categories) &&
b.SharedGroups.Equal(o.SharedGroups) &&
b.Disabled == o.Disabled
b.Disabled == o.Disabled &&
b.Blocker == o.Blocker
}
func (b *Balance) MatchFilter(o *Balance, skipIds bool) bool {
if o == nil {
return true
}
if !skipIds && o.Uuid != "" {
return b.Uuid == o.Uuid
}
@@ -85,6 +90,8 @@ func (b *Balance) MatchFilter(o *Balance, skipIds bool) bool {
}
return (o.ExpirationDate.IsZero() || b.ExpirationDate.Equal(o.ExpirationDate)) &&
(o.Weight == 0 || b.Weight == o.Weight) &&
(b.Blocker == o.Blocker) &&
(b.Disabled == o.Disabled) &&
(len(o.DestinationIds) == 0 || b.DestinationIds.Includes(o.DestinationIds)) &&
(len(o.Directions) == 0 || b.Directions.Includes(o.Directions)) &&
(len(o.Categories) == 0 || b.Categories.Includes(o.Categories)) &&
@@ -231,6 +238,7 @@ func (b *Balance) Clone() *Balance {
SharedGroups: b.SharedGroups,
TimingIDs: b.TimingIDs,
Timings: b.Timings, // should not be a problem with aliasing
Blocker: b.Blocker,
Disabled: b.Disabled,
dirty: b.dirty,
}

View File

@@ -29,6 +29,7 @@ type CallCost struct {
Direction, Category, Tenant, Subject, Account, Destination, TOR string
Cost float64
Timespans TimeSpans
RatedUsage float64
deductConnectFee bool
negativeConnectFee bool // the connect fee went negative on default balance
maxCostDisconect bool
@@ -61,6 +62,15 @@ func (cc *CallCost) GetDuration() (td time.Duration) {
return
}
func (cc *CallCost) UpdateRatedUsage() time.Duration {
if cc == nil {
return 0
}
totalDuration := cc.GetDuration()
cc.RatedUsage = totalDuration.Seconds()
return totalDuration
}
func (cc *CallCost) GetConnectFee() float64 {
if len(cc.Timespans) == 0 ||
cc.Timespans[0].RateInterval == nil ||

View File

@@ -50,6 +50,10 @@ func TestSingleResultMerge(t *testing.T) {
if cc1.Cost != 122 {
t.Errorf("Exdpected 120 was %v", cc1.Cost)
}
d := cc1.UpdateRatedUsage()
if d != 2*time.Minute || cc1.RatedUsage != 120.0 {
t.Errorf("error updating rating usage: %v, %v", d, cc1.RatedUsage)
}
}
func TestMultipleResultMerge(t *testing.T) {

View File

@@ -195,7 +195,11 @@ func (cd *CallDescriptor) LoadRatingPlans() (err error) {
err, _ = cd.getRatingPlansForPrefix(cd.GetKey(FALLBACK_SUBJECT), 1)
}
//load the rating plans
if err != nil || !cd.continousRatingInfos() {
if err != nil {
utils.Logger.Err(fmt.Sprintf("Rating plan not found for destination %s and account: %s, subject: %s", cd.Destination, cd.GetAccountKey(), cd.GetKey(cd.Subject)))
err = utils.ErrRatingPlanNotFound
}
if !cd.continousRatingInfos() {
utils.Logger.Err(fmt.Sprintf("Destination %s not authorized for account: %s, subject: %s", cd.Destination, cd.GetAccountKey(), cd.GetKey(cd.Subject)))
err = utils.ErrUnauthorizedDestination
}
@@ -522,6 +526,7 @@ func (cd *CallDescriptor) getCost() (*CallCost, error) {
cc.Cost = utils.Round(cc.Cost, roundingDecimals, roundingMethod)
//utils.Logger.Info(fmt.Sprintf("<Rater> Get Cost: %s => %v", cd.GetKey(), cc))
cc.Timespans.Compress()
cc.UpdateRatedUsage()
return cc, err
}
@@ -555,7 +560,6 @@ func (origCD *CallDescriptor) getMaxSessionDuration(origAcc *Account) (time.Dura
//utils.Logger.Debug("ACCOUNT: " + utils.ToJSON(account))
//utils.Logger.Debug("DEFAULT_BALANCE: " + utils.ToJSON(defaultBalance))
//
cc, err := cd.debit(account, true, false)
//utils.Logger.Debug("CC: " + utils.ToJSON(cc))
//log.Print("CC: ", utils.ToIJSON(cc))
@@ -665,6 +669,7 @@ func (cd *CallDescriptor) debit(account *Account, dryRun bool, goNegative bool)
return nil, err
}
cc.updateCost()
cc.UpdateRatedUsage()
cc.Timespans.Compress()
//log.Printf("OUT CC: ", cc)
return

View File

@@ -537,6 +537,57 @@ func TestMaxSessionTimeWithMaxCost(t *testing.T) {
}
}
func TestGetMaxSessiontWithBlocker(t *testing.T) {
ap, _ := ratingStorage.GetActionPlan("BLOCK_AT", false)
for _, at := range ap.ActionTimings {
at.accountIDs = ap.AccountIDs
at.Execute()
}
acc, err := accountingStorage.GetAccount("cgrates.org:block")
if err != nil {
t.Error("error getting account: ", err)
}
if len(acc.BalanceMap[utils.MONETARY]) != 2 ||
acc.BalanceMap[utils.MONETARY][0].Blocker != true {
for _, b := range acc.BalanceMap[utils.MONETARY] {
t.Logf("B: %+v", b)
}
t.Error("Error executing action plan on account: ", acc.BalanceMap[utils.MONETARY])
}
cd := &CallDescriptor{
Direction: "*out",
Category: "call",
Tenant: "cgrates.org",
Subject: "block",
Account: "block",
Destination: "0723",
TimeStart: time.Date(2016, 1, 13, 14, 0, 0, 0, time.UTC),
TimeEnd: time.Date(2016, 1, 13, 14, 30, 0, 0, time.UTC),
MaxCostSoFar: 0,
}
result, err := cd.GetMaxSessionDuration()
expected := 985 * time.Second
if result != expected || err != nil {
t.Errorf("Expected %v was %v (%v)", expected, result, err)
}
cd = &CallDescriptor{
Direction: "*out",
Category: "call",
Tenant: "cgrates.org",
Subject: "block",
Account: "block",
Destination: "444",
TimeStart: time.Date(2016, 1, 13, 14, 0, 0, 0, time.UTC),
TimeEnd: time.Date(2016, 1, 13, 14, 30, 0, 0, time.UTC),
MaxCostSoFar: 0,
}
result, err = cd.GetMaxSessionDuration()
expected = 30 * time.Minute
if result != expected || err != nil {
t.Errorf("Expected %v was %v (%v)", expected, result, err)
}
}
func TestGetCostWithMaxCost(t *testing.T) {
ap, _ := ratingStorage.GetActionPlan("TOPUP10_AT", false)
for _, at := range ap.ActionTimings {
@@ -560,6 +611,7 @@ func TestGetCostWithMaxCost(t *testing.T) {
t.Errorf("Expected %v was %v", expected, cc.Cost)
}
}
func TestGetCostRoundingIssue(t *testing.T) {
ap, _ := ratingStorage.GetActionPlan("TOPUP10_AT", false)
for _, at := range ap.ActionTimings {

View File

@@ -126,7 +126,7 @@ func (cdr *CDR) FormatCost(shiftDecimals, roundDecimals int) string {
// Formats usage on export
func (cdr *CDR) FormatUsage(layout string) string {
if utils.IsSliceMember([]string{utils.DATA, utils.SMS, utils.GENERIC}, cdr.ToR) {
if utils.IsSliceMember([]string{utils.DATA, utils.SMS, utils.MMS, utils.GENERIC}, cdr.ToR) {
return strconv.FormatFloat(utils.Round(cdr.Usage.Seconds(), 0, utils.ROUNDING_MIDDLE), 'f', -1, 64)
}
switch layout {

View File

@@ -153,6 +153,7 @@ func (self *CdrServer) LogCallCost(ccl *CallCostLog, reply *string) error {
// RPC method, used to log callcosts to db
func (self *CdrServer) LocalLogCallCost(ccl *CallCostLog) error {
ccl.CallCost.UpdateRatedUsage() // make sure rated usage is updated
if ccl.CheckDuplicate {
_, err := self.guard.Guard(func() (interface{}, error) {
cc, err := self.cdrDb.GetCallCostLog(ccl.CgrId, ccl.RunId)
@@ -220,6 +221,9 @@ func (self *CdrServer) processCdr(cdr *CDR) (err error) {
cdr.RunID = utils.MetaRaw
}
if self.cgrCfg.CDRSStoreCdrs { // Store RawCDRs, this we do sync so we can reply with the status
if cdr.CostDetails != nil {
cdr.CostDetails.UpdateRatedUsage()
}
if err := self.cdrDb.SetCDR(cdr, false); err != nil { // Only original CDR stored in primary table, no derived
utils.Logger.Err(fmt.Sprintf("<CDRS> Storing primary CDR %+v, got error: %s", cdr, err.Error()))
return err // Error is propagated back and we don't continue processing the CDR if we cannot store it
@@ -276,6 +280,9 @@ func (self *CdrServer) rateStoreStatsReplicate(cdr *CDR, sendToStats bool) error
}
if self.cgrCfg.CDRSStoreCdrs { // Store CDRs
// Store RatedCDR
if cdr.CostDetails != nil {
cdr.CostDetails.UpdateRatedUsage()
}
if err := self.cdrDb.SetCDR(cdr, true); err != nil {
utils.Logger.Err(fmt.Sprintf("<CDRS> Storing rated CDR %+v, got error: %s", cdr, err.Error()))
}

View File

@@ -147,6 +147,7 @@ DY_PLAN,RT_DY,*any,10
*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,,
*out,cgrates.org,call,block,2015-02-28T00:00:00Z,DY_PLAN,,
`
sharedGroups = `
SG1,*any,*lowest,
@@ -159,18 +160,20 @@ SG3,*any,*lowest,
*in,cgrates.org,call,*any,*any,*any,LCR_STANDARD,*lowest_cost,,2012-01-01T00:00:00Z,20
`
actions = `
MINI,*topup_reset,,,*monetary,*out,,,,,*unlimited,,10,10,false,10
MINI,*topup,,,*voice,*out,,NAT,test,,*unlimited,,100,10,false,10
SHARED,*topup,,,*monetary,*out,,,,SG1,*unlimited,,100,10,false,10
TOPUP10_AC,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,1,10,false,10
TOPUP10_AC1,*topup_reset,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,10
SE0,*topup_reset,,,*monetary,*out,,,,SG2,*unlimited,,0,10,false,10
SE10,*topup_reset,,,*monetary,*out,,,,SG2,*unlimited,,10,5,false,10
SE10,*topup,,,*monetary,*out,,,,,*unlimited,,10,10,false,10
EE0,*topup_reset,,,*monetary,*out,,,,SG3,*unlimited,,0,10,false,10
EE0,*allow_negative,,,*monetary,*out,,,,,*unlimited,,0,10,false,10
DEFEE,*cdrlog,"{""Category"":""^ddi"",""MediationRunId"":""^did_run""}",,,,,,,,,,,,false,10
NEG,*allow_negative,,,*monetary,*out,,,,,*unlimited,,0,10,false,10
MINI,*topup_reset,,,,*monetary,*out,,,,,*unlimited,,10,10,false,false,10
MINI,*topup,,,,*voice,*out,,NAT,test,,*unlimited,,100,10,false,false,10
SHARED,*topup,,,,*monetary,*out,,,,SG1,*unlimited,,100,10,false,false,10
TOPUP10_AC,*topup_reset,,,,*monetary,*out,,*any,,,*unlimited,,1,10,false,false,10
TOPUP10_AC1,*topup_reset,,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,false,10
SE0,*topup_reset,,,,*monetary,*out,,,,SG2,*unlimited,,0,10,false,false,10
SE10,*topup_reset,,,,*monetary,*out,,,,SG2,*unlimited,,10,5,false,false,10
SE10,*topup,,,,*monetary,*out,,,,,*unlimited,,10,10,false,false,10
EE0,*topup_reset,,,,*monetary,*out,,,,SG3,*unlimited,,0,10,false,false,10
EE0,*allow_negative,,,,*monetary,*out,,,,,*unlimited,,0,10,false,false,10
DEFEE,*cdrlog,"{""Category"":""^ddi"",""MediationRunId"":""^did_run""}",,,,,,,,,,,,,false,false,10
NEG,*allow_negative,,,,*monetary,*out,,,,,*unlimited,,0,10,false,false,10
BLOCK,*topup,,,bblocker,*monetary,*out,,NAT,,,*unlimited,,10,20,true,false,20
BLOCK,*topup,,,bfree,*monetary,*out,,,,,*unlimited,,20,10,false,false,10
`
actionPlans = `
MORE_MINUTES,MINI,ONE_TIME_RUN,10
@@ -181,19 +184,20 @@ TOPUP_SHARED0_AT,SE0,*asap,10
TOPUP_SHARED10_AT,SE10,*asap,10
TOPUP_EMPTY_AT,EE0,*asap,10
POST_AT,NEG,*asap,10
BLOCK_AT,BLOCK,*asap,10
`
actionTriggers = `
STANDARD_TRIGGER,st0,*min_event_counter,10,false,0,,*voice,*out,,GERMANY_O2,,,,,,,,SOME_1,10
STANDARD_TRIGGER,st1,*max_balance,200,false,0,,*voice,*out,,GERMANY,,,,,,,,SOME_2,10
STANDARD_TRIGGERS,,*min_balance,2,false,0,,*monetary,*out,,,,,,,,,,LOG_WARNING,10
STANDARD_TRIGGERS,,*max_balance,20,false,0,,*monetary,*out,,,,,,,,,,LOG_WARNING,10
STANDARD_TRIGGERS,,*max_event_counter,5,false,0,,*monetary,*out,,FS_USERS,,,,,,,,LOG_WARNING,10
CDRST1_WARN_ASR,,*min_asr,45,true,1h,,,,,,,,,,,,3,CDRST_WARN_HTTP,10
CDRST1_WARN_ACD,,*min_acd,10,true,1h,,,,,,,,,,,,5,CDRST_WARN_HTTP,10
CDRST1_WARN_ACC,,*max_acc,10,true,10m,,,,,,,,,,,,5,CDRST_WARN_HTTP,10
CDRST2_WARN_ASR,,*min_asr,30,true,0,,,,,,,,,,,,5,CDRST_WARN_HTTP,10
CDRST2_WARN_ACD,,*min_acd,3,true,0,,,,,,,,,,,,5,CDRST_WARN_HTTP,10
STANDARD_TRIGGER,st0,*min_event_counter,10,false,0,,*voice,*out,,GERMANY_O2,,,,,,,,,SOME_1,10
STANDARD_TRIGGER,st1,*max_balance,200,false,0,,*voice,*out,,GERMANY,,,,,,,,,SOME_2,10
STANDARD_TRIGGERS,,*min_balance,2,false,0,,*monetary,*out,,,,,,,,,,,LOG_WARNING,10
STANDARD_TRIGGERS,,*max_balance,20,false,0,,*monetary,*out,,,,,,,,,,,LOG_WARNING,10
STANDARD_TRIGGERS,,*max_event_counter,5,false,0,,*monetary,*out,,FS_USERS,,,,,,,,,LOG_WARNING,10
CDRST1_WARN_ASR,,*min_asr,45,true,1h,,,,,,,,,,,,,3,CDRST_WARN_HTTP,10
CDRST1_WARN_ACD,,*min_acd,10,true,1h,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10
CDRST1_WARN_ACC,,*max_acc,10,true,10m,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10
CDRST2_WARN_ASR,,*min_asr,30,true,0,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10
CDRST2_WARN_ACD,,*min_acd,3,true,0,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10
`
accountActions = `
vdf,minitsboy,MORE_MINUTES,STANDARD_TRIGGER,,
@@ -207,6 +211,7 @@ vdf,emptyX,TOPUP_EMPTY_AT,,,
vdf,emptyY,TOPUP_EMPTY_AT,,,
vdf,post,POST_AT,,,
cgrates.org,alodis,TOPUP_EMPTY_AT,,true,true
cgrates.org,block,BLOCK_AT,,false,false
`
derivedCharges = `
@@ -790,7 +795,7 @@ func TestLoadRatingPlans(t *testing.T) {
}
func TestLoadRatingProfiles(t *testing.T) {
if len(csvr.ratingProfiles) != 22 {
if len(csvr.ratingProfiles) != 23 {
t.Error("Failed to load rating profiles: ", len(csvr.ratingProfiles), csvr.ratingProfiles)
}
rp := csvr.ratingProfiles["*out:test:0:trp"]
@@ -809,7 +814,7 @@ func TestLoadRatingProfiles(t *testing.T) {
}
func TestLoadActions(t *testing.T) {
if len(csvr.actions) != 9 {
if len(csvr.actions) != 10 {
t.Error("Failed to load actions: ", len(csvr.actions))
}
as1 := csvr.actions["MINI"]
@@ -822,12 +827,14 @@ func TestLoadActions(t *testing.T) {
ExtraParameters: "",
Weight: 10,
Balance: &Balance{
Uuid: as1[0].Balance.Uuid,
Directions: utils.NewStringMap(utils.OUT),
Value: 10,
Weight: 10,
TimingIDs: utils.StringMap{},
SharedGroups: utils.StringMap{},
Uuid: as1[0].Balance.Uuid,
Directions: utils.NewStringMap(utils.OUT),
Value: 10,
Weight: 10,
DestinationIds: utils.StringMap{},
TimingIDs: utils.StringMap{},
SharedGroups: utils.StringMap{},
Categories: utils.StringMap{},
},
},
&Action{
@@ -846,10 +853,11 @@ func TestLoadActions(t *testing.T) {
DestinationIds: utils.NewStringMap("NAT"),
TimingIDs: utils.StringMap{},
SharedGroups: utils.StringMap{},
Categories: utils.StringMap{},
},
},
}
if !reflect.DeepEqual(as1[1], expected[1]) {
if !reflect.DeepEqual(as1, expected) {
t.Errorf("Error loading action1: %+v", as1[0].Balance)
}
as2 := csvr.actions["SHARED"]
@@ -868,10 +876,11 @@ func TestLoadActions(t *testing.T) {
Weight: 10,
SharedGroups: utils.NewStringMap("SG1"),
TimingIDs: utils.StringMap{},
Categories: utils.StringMap{},
},
},
}
if !reflect.DeepEqual(as2[0], expected[0]) {
if !reflect.DeepEqual(as2, expected) {
t.Errorf("Error loading action: %+v", as2[0].Balance)
}
as3 := csvr.actions["DEFEE"]
@@ -886,7 +895,9 @@ func TestLoadActions(t *testing.T) {
Directions: utils.StringMap{},
DestinationIds: utils.StringMap{},
TimingIDs: utils.StringMap{},
Categories: utils.StringMap{},
SharedGroups: utils.StringMap{},
Blocker: false,
},
},
}
@@ -982,7 +993,7 @@ func TestLoadLCRs(t *testing.T) {
}
func TestLoadActionTimings(t *testing.T) {
if len(csvr.actionPlans) != 6 {
if len(csvr.actionPlans) != 7 {
t.Error("Failed to load action timings: ", len(csvr.actionPlans))
}
atm := csvr.actionPlans["MORE_MINUTES"]
@@ -1070,7 +1081,7 @@ func TestLoadActionTriggers(t *testing.T) {
}
func TestLoadAccountActions(t *testing.T) {
if len(csvr.accountActions) != 11 {
if len(csvr.accountActions) != 12 {
t.Error("Failed to load account actions: ", len(csvr.accountActions))
}
aa := csvr.accountActions["vdf:minitsboy"]

View File

@@ -173,12 +173,15 @@ func APItoModelAction(as *utils.TPActions) (result []TpAction) {
Directions: a.Directions,
Units: a.Units,
ExpiryTime: a.ExpiryTime,
Filter: a.Filter,
TimingTags: a.TimingTags,
DestinationTags: a.DestinationIds,
RatingSubject: a.RatingSubject,
Categories: a.Categories,
SharedGroups: a.SharedGroups,
BalanceWeight: a.BalanceWeight,
BalanceBlocker: a.BalanceBlocker,
BalanceDisabled: a.BalanceDisabled,
ExtraParameters: a.ExtraParameters,
Weight: a.Weight,
})
@@ -231,6 +234,7 @@ func APItoModelActionTrigger(ats *utils.TPActionTriggers) (result []TpActionTrig
BalanceRatingSubject: at.BalanceRatingSubject,
BalanceCategories: at.BalanceCategories,
BalanceSharedGroups: at.BalanceSharedGroups,
BalanceBlocker: at.BalanceBlocker,
BalanceDisabled: at.BalanceDisabled,
MinQueuedItems: at.MinQueuedItems,
ActionsTag: at.ActionsId,

View File

@@ -390,12 +390,15 @@ func (tps TpActions) GetActions() (map[string][]*utils.TPAction, error) {
Directions: tpAc.Directions,
Units: tpAc.Units,
ExpiryTime: tpAc.ExpiryTime,
Filter: tpAc.Filter,
TimingTags: tpAc.TimingTags,
DestinationIds: tpAc.DestinationTags,
RatingSubject: tpAc.RatingSubject,
Categories: tpAc.Categories,
SharedGroups: tpAc.SharedGroups,
BalanceWeight: tpAc.BalanceWeight,
BalanceBlocker: tpAc.BalanceBlocker,
BalanceDisabled: tpAc.BalanceDisabled,
ExtraParameters: tpAc.ExtraParameters,
Weight: tpAc.Weight,
}
@@ -437,6 +440,7 @@ func (tps TpActionTriggers) GetActionTriggers() (map[string][]*utils.TPActionTri
BalanceRatingSubject: tpAt.BalanceRatingSubject,
BalanceCategories: tpAt.BalanceCategories,
BalanceSharedGroups: tpAt.BalanceSharedGroups,
BalanceBlocker: tpAt.BalanceBlocker,
BalanceDisabled: tpAt.BalanceDisabled,
Weight: tpAt.Weight,
ActionsId: tpAt.ActionsTag,

View File

@@ -265,8 +265,8 @@ func TestTPActionsAsExportSlice(t *testing.T) {
},
}
expectedSlc := [][]string{
[]string{"TEST_ACTIONS", "*topup_reset", "", "", "*monetary", utils.OUT, "call", "*any", "special1", "GROUP1", "*never", "", "5", "10", "false", "10"},
[]string{"TEST_ACTIONS", "*http_post", "http://localhost/&param1=value1", "", "", "", "", "", "", "", "", "", "0", "0", "false", "20"},
[]string{"TEST_ACTIONS", "*topup_reset", "", "", "", "*monetary", utils.OUT, "call", "*any", "special1", "GROUP1", "*never", "", "5", "10", "false", "false", "10"},
[]string{"TEST_ACTIONS", "*http_post", "http://localhost/&param1=value1", "", "", "", "", "", "", "", "", "", "", "0", "0", "false", "false", "20"},
}
ms := APItoModelAction(tpActs)
@@ -567,6 +567,7 @@ func TestTPActionPlanAsExportSlice(t *testing.T) {
BalanceRatingSubject: "special1",
BalanceCategories: "call",
BalanceSharedGroups: "SHARED_1",
BalanceBlocker: false,
BalanceDisabled: false,
MinQueuedItems: 0,
ActionsId: "LOG_WARNING",
@@ -588,6 +589,7 @@ func TestTPActionPlanAsExportSlice(t *testing.T) {
BalanceRatingSubject: "special1",
BalanceCategories: "call",
BalanceSharedGroups: "SHARED_1",
BalanceBlocker: false,
BalanceDisabled: false,
MinQueuedItems: 0,
ActionsId: "LOG_WARNING",
@@ -595,8 +597,8 @@ func TestTPActionPlanAsExportSlice(t *testing.T) {
},
}
expectedSlc := [][]string{
[]string{"STANDARD_TRIGGERS", "1", "*min_balance", "2", "false", "0", "b1", "*monetary", "*out", "call", "", "special1", "SHARED_1", "*never", "T1", "0", "false", "0", "LOG_WARNING", "10"},
[]string{"STANDARD_TRIGGERS", "2", "*max_event_counter", "5", "false", "0", "b2", "*monetary", "*out", "call", "FS_USERS", "special1", "SHARED_1", "*never", "T1", "0", "false", "0", "LOG_WARNING", "10"},
[]string{"STANDARD_TRIGGERS", "1", "*min_balance", "2", "false", "0", "b1", "*monetary", "*out", "call", "", "special1", "SHARED_1", "*never", "T1", "0", "false", "false", "0", "LOG_WARNING", "10"},
[]string{"STANDARD_TRIGGERS", "2", "*max_event_counter", "5", "false", "0", "b2", "*monetary", "*out", "call", "FS_USERS", "special1", "SHARED_1", "*never", "T1", "0", "false", "false", "0", "LOG_WARNING", "10"},
}
ms := APItoModelActionTrigger(at)
var slc [][]string

View File

@@ -156,19 +156,21 @@ type TpAction struct {
Tag string `index:"0" re:"\w+\s*"`
Action string `index:"1" re:"\*\w+\s*"`
ExtraParameters string `index:"2" re:"\S+\s*"`
BalanceTag string `index:"3" re:"\w+\s*"`
BalanceType string `index:"4" re:"\*\w+\s*"`
Directions string `index:"5" re:""`
Categories string `index:"6" re:""`
DestinationTags string `index:"7" re:"\*any|\w+\s*"`
RatingSubject string `index:"8" re:"\w+\s*"`
SharedGroups string `index:"9" re:"[0-9A-Za-z_;]*"`
ExpiryTime string `index:"10" re:"\*\w+\s*|\+\d+[smh]\s*|\d+\s*"`
TimingTags string `index:"11" re:"[0-9A-Za-z_;]*|\*any"`
Units float64 `index:"12" re:"\d+\s*"`
BalanceWeight float64 `index:"13" re:"\d+\.?\d*\s*"`
BalanceDisabled bool `index:"14" re:""`
Weight float64 `index:"15" re:"\d+\.?\d*\s*"`
Filter string `index:"3" re:"\S+\s*"`
BalanceTag string `index:"4" re:"\w+\s*"`
BalanceType string `index:"5" re:"\*\w+\s*"`
Directions string `index:"6" re:""`
Categories string `index:"7" re:""`
DestinationTags string `index:"8" re:"\*any|\w+\s*"`
RatingSubject string `index:"9" re:"\w+\s*"`
SharedGroups string `index:"10" re:"[0-9A-Za-z_;]*"`
ExpiryTime string `index:"11" re:"\*\w+\s*|\+\d+[smh]\s*|\d+\s*"`
TimingTags string `index:"12" re:"[0-9A-Za-z_;]*|\*any"`
Units float64 `index:"13" re:"\d+\s*"`
BalanceWeight float64 `index:"14" re:"\d+\.?\d*\s*"`
BalanceBlocker bool `index:"15" re:""`
BalanceDisabled bool `index:"16" re:""`
Weight float64 `index:"17" re:"\d+\.?\d*\s*"`
CreatedAt time.Time
}
@@ -201,10 +203,11 @@ type TpActionTrigger struct {
BalanceExpiryTime string `index:"13" re:"\*\w+\s*|\+\d+[smh]\s*|\d+\s*"`
BalanceTimingTags string `index:"14" re:"[0-9A-Za-z_;]*|\*any"`
BalanceWeight float64 `index:"15" re:"\d+\.?\d*"`
BalanceDisabled bool `index:"16" re:""`
MinQueuedItems int `index:"17" re:"\d+"`
ActionsTag string `index:"18" re:"\w+"`
Weight float64 `index:"19" re:"\d+\.?\d*"`
BalanceBlocker bool `index:"16" re:""`
BalanceDisabled bool `index:"17" re:""`
MinQueuedItems int `index:"18" re:"\d+"`
ActionsTag string `index:"19" re:"\w+"`
Weight float64 `index:"20" re:"\d+\.?\d*"`
CreatedAt time.Time
}

View File

@@ -274,7 +274,7 @@ func TestDifferentUuid(t *testing.T) {
func TestStorageTask(t *testing.T) {
// clean previous unused tasks
for i := 0; i < 16; i++ {
for i := 0; i < 18; i++ {
ratingStorage.PopTask()
}

View File

@@ -513,6 +513,7 @@ func (tpr *TpReader) LoadActions() (err error) {
Weight: tpact.Weight,
ExtraParameters: tpact.ExtraParameters,
ExpirationString: tpact.ExpiryTime,
Filter: tpact.Filter,
Balance: &Balance{
Id: tpact.BalanceId,
Value: tpact.Units,
@@ -523,6 +524,8 @@ func (tpr *TpReader) LoadActions() (err error) {
DestinationIds: utils.ParseStringMap(tpact.DestinationIds),
SharedGroups: utils.ParseStringMap(tpact.SharedGroups),
TimingIDs: utils.ParseStringMap(tpact.TimingTags),
Blocker: tpact.BalanceBlocker,
Disabled: tpact.BalanceDisabled,
},
}
// load action timings from tags
@@ -640,6 +643,8 @@ func (tpr *TpReader) LoadActionTriggers() (err error) {
BalanceRatingSubject: atr.BalanceRatingSubject,
BalanceCategories: utils.ParseStringMap(atr.BalanceCategories),
BalanceSharedGroups: utils.ParseStringMap(atr.BalanceSharedGroups),
BalanceBlocker: atr.BalanceBlocker,
BalanceDisabled: atr.BalanceDisabled,
Weight: atr.Weight,
ActionsId: atr.ActionsId,
MinQueuedItems: atr.MinQueuedItems,
@@ -788,9 +793,12 @@ func (tpr *TpReader) LoadAccountActionsFiltered(qriedAA *TpAccountAction) error
BalanceDestinationIds: utils.ParseStringMap(apiAtr.BalanceDestinationIds),
BalanceWeight: apiAtr.BalanceWeight,
BalanceExpirationDate: expTime,
BalanceTimingTags: utils.ParseStringMap(apiAtr.BalanceTimingTags),
BalanceRatingSubject: apiAtr.BalanceRatingSubject,
BalanceCategories: utils.ParseStringMap(apiAtr.BalanceCategories),
BalanceSharedGroups: utils.ParseStringMap(apiAtr.BalanceSharedGroups),
BalanceBlocker: apiAtr.BalanceBlocker,
BalanceDisabled: apiAtr.BalanceDisabled,
Weight: apiAtr.Weight,
ActionsId: apiAtr.ActionsId,
}
@@ -830,14 +838,19 @@ func (tpr *TpReader) LoadAccountActionsFiltered(qriedAA *TpAccountAction) error
Weight: tpact.Weight,
ExtraParameters: tpact.ExtraParameters,
ExpirationString: tpact.ExpiryTime,
Filter: tpact.Filter,
Balance: &Balance{
Id: tpact.BalanceId,
Value: tpact.Units,
Weight: tpact.BalanceWeight,
RatingSubject: tpact.RatingSubject,
Categories: utils.ParseStringMap(tpact.Categories),
Directions: utils.ParseStringMap(tpact.Directions),
DestinationIds: utils.ParseStringMap(tpact.DestinationIds),
SharedGroups: utils.ParseStringMap(tpact.SharedGroups),
TimingIDs: utils.ParseStringMap(tpact.TimingTags),
Blocker: tpact.BalanceBlocker,
Disabled: tpact.BalanceDisabled,
},
}
}
@@ -1055,14 +1068,19 @@ func (tpr *TpReader) LoadCdrStatsFiltered(tag string, save bool) (err error) {
Weight: tpact.Weight,
ExtraParameters: tpact.ExtraParameters,
ExpirationString: tpact.ExpiryTime,
Filter: tpact.Filter,
Balance: &Balance{
Id: tpact.BalanceId,
Value: tpact.Units,
Weight: tpact.BalanceWeight,
RatingSubject: tpact.RatingSubject,
Categories: utils.ParseStringMap(tpact.Categories),
Directions: utils.ParseStringMap(tpact.Directions),
DestinationIds: utils.ParseStringMap(tpact.DestinationIds),
SharedGroups: utils.ParseStringMap(tpact.SharedGroups),
TimingIDs: utils.ParseStringMap(tpact.TimingTags),
Blocker: tpact.BalanceBlocker,
Disabled: tpact.BalanceDisabled,
},
}
}