package dbManager import ( "encoding/json" "errors" "math" "reflect" "strings" "time" "github.com/go-openapi/strfmt" "github.com/prometheus/client_golang/prometheus" cdrModels "github.com/Cyclops-Labs/cyclops-4-hpc.git/services/cdr/models" "github.com/Cyclops-Labs/cyclops-4-hpc.git/services/credit-system/models" "github.com/Cyclops-Labs/cyclops-4-hpc.git/services/credit-system/server/cacheManager" cusModels "github.com/Cyclops-Labs/cyclops-4-hpc.git/services/customerdb/models" pmModels "github.com/Cyclops-Labs/cyclops-4-hpc.git/services/plan-manager/models" l "gitlab.com/cyclops-utilities/logging" "gorm.io/driver/postgres" "gorm.io/gorm" ) const ( scaler = 1e7 statusDuplicated = iota statusFail statusMissing statusOK AC_CREDIT = "CREDIT" AC_CASH = "CASH" AC_BOTH = "BOTH" AC_NONE = "NONE" MED_CASH = "CASH" MED_CREDIT = "CREDIT" ) var ( //states = []string{"active", "error", "inactive", "suspended", "terminated"} states = []string{"active", "error", "inactive", "used"} totalTime float64 totalCount int64 ) // DbParameter is the struct defined to group and contain all the methods // that interact with the database. // On it there is the following parameters: // - Cache: CacheManager pointer for the cache mechanism. // - connStr: strings with the connection information to the database // - Db: a gorm.DB pointer to the db to invoke all the db methods type DbParameter struct { Cache *cacheManager.CacheManager connStr string Db *gorm.DB Metrics map[string]*prometheus.GaugeVec } // New is the function to create the struct DbParameter. // Parameters: // - dbConn: strings with the connection information to the database // - tables: array of interfaces that will contains the models to migrate // to the database on initialization // Returns: // - DbParameter: struct to interact with dbManager functionalities¬ func New(dbConn string, tables ...interface{}) *DbParameter { l.Trace.Printf("[DB] Gerenating new DBParameter.\n") var ( dp DbParameter err error ) dp.connStr = dbConn dp.Db, err = gorm.Open(postgres.Open(dbConn), &gorm.Config{}) if err != nil { l.Error.Printf("[DB] Error opening connection. Error: %v\n", err) } l.Trace.Printf("[DB] Migrating tables.\n") //Database migration, it handles everything dp.Db.AutoMigrate(tables...) //l.Trace.Printf("[DB] Generating hypertables.\n") // Hypertables creation for timescaledb in case of needed //dp.Db.Exec("SELECT create_hypertable('" + dp.Db.NewScope(&models.TABLE).TableName() + "', 'TIMESCALE-ROW-INDEX');") return &dp } // AddConsumption job is to decrease the credit in the system linked to the // provided account by a certain amount of credit. // This function is the one that produces a "cosumed" decrease of credit without // human intervention. // Parameters: // - id: string containing the id of the account requested. // - amount: float containing the amount to be -- in the system // Returns: // - reference to creditStatus containing the credit balance after the operation. // - error raised in case of problems. func (d *DbParameter) AddConsumption(id string, amount float64, medium string) (*models.CreditStatus, error) { l.Trace.Printf("[DB] Attempting to add a comsuption of %v credit, in the account with id: %v", amount, id) var c, c0, cs models.CreditStatus var ce models.CreditEvents var e error if r := d.Db.Where(&models.CreditStatus{AccountID: id}).First(&c).Error; errors.Is(r, gorm.ErrRecordNotFound) { l.Trace.Printf("[DB] Account with id: %v doesn't exist in the system, check with administrator.", id) e = errors.New("account doesn't exist in the system") } else { med := strings.ToUpper(medium) if med == models.CreditEventsMediumCREDIT { c0.AvailableCredit = c.AvailableCredit - amount } if med == models.CreditEventsMediumCASH { c0.AvailableCash = c.AvailableCash - amount } c0.LastUpdate = strfmt.DateTime(time.Now()) ce.AccountID = id ce.Delta = -amount ce.EventType = func(s string) *string { return &s }(models.EventEventTypeConsumption) ce.Timestamp = strfmt.DateTime(time.Now()) ce.Medium = &med if e = d.Db.Model(&c).Updates(c0).Error; e == nil { if e = d.Db.Create(&ce).Error; e == nil { d.Db.Where(&models.CreditStatus{AccountID: id}).First(&cs) } else { l.Trace.Printf("[DB] Attempting to add the comsuption of credit event for account in the system failed.") } } else { l.Trace.Printf("[DB] Attempting to update the account in the system failed.") } } return &cs, e } // CreateAccount job is to register a new account in the system with the provided // ID. // Parameters: // - id: string containing the id of the account to be created. // Returns: // - reference to AccountStatus containing the state of the account after the // operation. // - error raised in case of problems. func (d *DbParameter) CreateAccount(id string) (*models.AccountStatus, error) { l.Trace.Printf("[DB] Attempting to create a new account with id: %v", id) var a, a0, as models.AccountStatus var c models.CreditStatus var e error if r := d.Db.Where(&models.AccountStatus{AccountID: id}).First(&a).Error; errors.Is(r, gorm.ErrRecordNotFound) { a0.AccountID = id a0.CreatedAt = strfmt.DateTime(time.Now()) c.AccountID = id c.LastUpdate = strfmt.DateTime(time.Now()) if e = d.Db.Create(&a0).Error; e == nil { if e = d.Db.Create(&c).Error; e == nil { d.Db.Where(&models.AccountStatus{AccountID: id}).First(&as) d.Metrics["count"].With(prometheus.Labels{"type": "Accounts created"}).Inc() } else { l.Trace.Printf("[DB] Attempting to create the credit account in the system failed.") } } else { l.Trace.Printf("[DB] Attempting to create the account in the system failed.") } } else { l.Trace.Printf("[DB] Account with id: %v already in the system, check with administrator.", id) e = errors.New("account already exist in the system") } return &as, e } // DecreaseCredit job is to decrease the credit in the system linked to the // provided account by a certain amount of credit. // Parameters: // - id: string containing the id of the account requested. // - amount: float containing the amount to be decreased in the system. // Returns: // - reference to CreditStatus containing the credit balance after the operation. // - error raised in case of problems. func (d *DbParameter) DecreaseCredit(id string, amount float64, medium string) (*models.CreditStatus, error) { l.Trace.Printf("[DB] Attempting to decrease credit by: %v, in the account with id: %v", amount, id) var c, c0, cs models.CreditStatus var ce models.CreditEvents var e error if r := d.Db.Where(&models.CreditStatus{AccountID: id}).First(&c).Error; errors.Is(r, gorm.ErrRecordNotFound) { l.Trace.Printf("[DB] Account with id: %v doesn't exist in the system, check with administrator.", id) e = errors.New("account doesn't exist in the system") } else { med := strings.ToUpper(medium) if med == models.CreditEventsMediumCREDIT { c0.AvailableCredit = c.AvailableCredit - amount } if med == models.CreditEventsMediumCASH { c0.AvailableCash = c.AvailableCash - amount } c0.LastUpdate = strfmt.DateTime(time.Now()) ce.AccountID = id ce.Delta = -amount ce.EventType = func(s string) *string { return &s }(models.EventEventTypeAuthorizedDecrease) ce.Timestamp = strfmt.DateTime(time.Now()) ce.Medium = &med if e = d.Db.Model(&c).Updates(c0).Error; e == nil { if e = d.Db.Create(&ce).Error; e == nil { d.Db.Where(&models.CreditStatus{AccountID: id}).First(&cs) } else { l.Trace.Printf("[DB] Attempting to add the decrease of credit event for account in the system failed.") } } else { l.Trace.Printf("[DB] Attempting to update the account in the system failed.") } } return &cs, e } // DisableAccount job is to mark as disabled in the system the account provided. // Parameters: // - id: string containing the id of the account to be disabled. // Returns: // - reference to AccountStatus containing the state of the account after the // operation. // - error raised in case of problems. func (d *DbParameter) DisableAccount(id string) (*models.AccountStatus, error) { l.Trace.Printf("[DB] Attempting to disable account with id: %v", id) var a, as models.AccountStatus var e error if r := d.Db.Where(&models.AccountStatus{AccountID: id}).First(&a).Error; errors.Is(r, gorm.ErrRecordNotFound) { l.Trace.Printf("[DB] Account with id: %v doesn't exist in the system, check with administrator.", id) e = errors.New("account doesn't exist in the system") } else { enabled := false if e = d.Db.Model(&a).Updates(&models.AccountStatus{Enabled: enabled}).Error; e == nil { d.Db.Where(&models.AccountStatus{AccountID: id}).First(&as) } else { l.Trace.Printf("[DB] Attempting to update the account in the system failed.") } } return &as, e } // EnableAccount job is to mark as enabled in the system the account provided. // Parameters: // - id: string containing the id of the account to be enabled. // Returns: // - reference to AccountStatus containing the state of the account after the // operation. // - error raised in case of problems. func (d *DbParameter) EnableAccount(id string) (*models.AccountStatus, error) { l.Trace.Printf("[DB] Attempting to enable account with id: %v", id) var a, as models.AccountStatus var e error if r := d.Db.Where(&models.AccountStatus{AccountID: id}).First(&a).Error; errors.Is(r, gorm.ErrRecordNotFound) { l.Trace.Printf("[DB] Account with id: %v doesn't exist in the system, check with administrator.", id) e = errors.New("account doesn't exist in the system") } else { if e = d.Db.Model(&a).Update("Enabled", true).Error; e == nil { d.Db.Where(&models.AccountStatus{AccountID: id}).First(&as) } else { l.Trace.Printf("[DB] Attempting to update the account in the system failed.") } } return &as, e } // GetAccountStatus job is to retrieve the actual state of the provided account. // Parameters: // - id: string containing the id of the account to be retrieved. // Returns: // - reference to AccountStatus containing the state of the account. // - error raised in case of problems. func (d *DbParameter) GetAccountStatus(id string) (*models.AccountStatus, error) { l.Trace.Printf("[DB] Attempting to get the status of the account with id: %v", id) var as models.AccountStatus var e error if r := d.Db.Where(&models.AccountStatus{AccountID: id}).First(&as).Error; errors.Is(r, gorm.ErrRecordNotFound) { l.Trace.Printf("[DB] Account with id: %v doesn't exist in the system, check with administrator.", id) e = errors.New("account doesn't exist in the system") } return &as, e } // GetCredit job is to retrieve the actual state of the credit balance of the // provided account. // Parameters: // - id: string containing the id of the account requested. // Returns: // - reference to CreditStatus containing the credit balance. // - error raised in case of problems. func (d *DbParameter) GetCredit(id string) (*models.CreditStatus, error) { l.Trace.Printf("[DB] Attempting to get the credit balance in the account with id: %v", id) var cs models.CreditStatus var e error if r := d.Db.Where(&models.CreditStatus{AccountID: id}).First(&cs).Error; errors.Is(r, gorm.ErrRecordNotFound) { l.Trace.Printf("[DB] Account with id: %v doesn't exist in the system, check with administrator.", id) e = errors.New("account doesn't exist in the system") } return &cs, e } // GetHistory job is to retrieve the history of the credit balance of the account // provided with the posibility of filter the actions. // Parameters: // - id: string containing the id of the account requested. // - filtered: string specifying the criteria for filtering. // Returns: // - reference to CreditHistory containing the credit balance history contained // in the system. // - error raised in case of problems. func (d *DbParameter) GetHistory(id string, filtered bool, medium string) (*models.CreditHistory, error) { l.Trace.Printf("[DB] Attempting to get the credit history in the account with id: %v", id) var ch models.CreditHistory var ce []*models.CreditEvents var e0 []*models.Event var e error ch.AccountID = id med := strings.ToUpper(medium) if filtered { evType := models.EventEventTypeConsumption e = d.Db.Where(&models.CreditEvents{AccountID: id, Medium: &med}).Not(&models.CreditEvents{EventType: &evType}).Find(&ce).Error } else { e = d.Db.Where(&models.CreditEvents{AccountID: id, Medium: &med}).Find(&ce).Error } if e != nil { l.Trace.Printf("[DB] Account with id: %v doesn't have events in the system, check with administrator.", id) } for _, event := range ce { var ev models.Event ev.Delta = event.Delta ev.EventType = event.EventType ev.Timestamp = event.Timestamp ev.AuthorizedBy = event.AuthorizedBy e0 = append(e0, &ev) } l.Trace.Printf("[DB] Amount of events for the account with id: %v, is: %v.", id, len(e0)) ch.Events = e0 return &ch, e } // IncreaseCredit job is to increase the credit in the system linked to the // provided account by a certain amount of credit. // Parameters: // - id: string containing the id of the account requested. // - amount: float containing the amount to be -- in the system // Returns: // - reference to CreditStatus containing the credit balance after the operation. // - error raised in case of problems. func (d *DbParameter) IncreaseCredit(id string, amount float64, medium string) (*models.CreditStatus, error) { l.Trace.Printf("[DB] Attempting to increase credit by: %v, in the account with id: %v", amount, id) var c, c0, cs models.CreditStatus var ce models.CreditEvents var e error if r := d.Db.Where(&models.CreditStatus{AccountID: id}).First(&c).Error; errors.Is(r, gorm.ErrRecordNotFound) { l.Trace.Printf("[DB] Account with id: %v doesn't exist in the system, check with administrator.", id) e = errors.New("account doesn't exist in the system") } else { med := strings.ToUpper(medium) if med == models.CreditEventsMediumCREDIT { c0.AvailableCredit = c.AvailableCredit + amount } if med == models.CreditEventsMediumCASH { c0.AvailableCash = c.AvailableCash + amount } c0.LastUpdate = strfmt.DateTime(time.Now()) ce.AccountID = id ce.Delta = amount ce.EventType = func(s string) *string { return &s }(models.EventEventTypeAuthorizedIncrease) ce.Timestamp = strfmt.DateTime(time.Now()) ce.Medium = &med if e = d.Db.Model(&c).Updates(c0).Error; e == nil { if e = d.Db.Create(&ce).Error; e == nil { d.Db.Where(&models.CreditStatus{AccountID: id}).First(&cs) } else { l.Trace.Printf("[DB] Attempting to add the increase of credit event for account in the system failed.") } } else { l.Trace.Printf("[DB] Attempting to update the account in the system failed.") } } return &cs, e } // ListAccounts job is to retrieve the list of all the accounts safe in the system. // Returns: // - slice of references to AccountStatus containing the status of every account // in the system func (d *DbParameter) ListAccounts() ([]*models.AccountStatus, error) { l.Trace.Printf("[DB] Attempting to list accounts in the system.") var as []*models.AccountStatus var e error if e = d.Db.Find(&as).Error; e != nil { l.Trace.Printf("[DB] There is not accounts in the system") } return as, e } // ProcessCDR job is to check every CDR report arriving to the system to add the // consumptions of each account to the corresponding // Parameters: // - report: CDR Report model reference to be processed. // - usage: a boolean discriminant to set the accounting to usage instead of cost // Returns: // - e: error in case of problem while doing a conversion, a cache.get, or adding the comsumption. func (d *DbParameter) ProcessCDR(report cdrModels.CReport, token string) error { l.Trace.Printf("[DB] Starting the processing of the a CDR.\n") var planID string now := time.Now().UnixNano() listAccounts, e := d.ListAccounts() if e != nil { l.Warning.Printf("[DB] There's was an error retrieving the accounts in the system. Error: %v\n", e) return e } if len(listAccounts) < 1 { l.Trace.Printf("[DB] There's no accounts in the system right now, skipping the processing...\n") return nil } p, e := d.Cache.Get(report.AccountID, "product", token) if e != nil { l.Warning.Printf("[DB] Something went wrong while retrieving the product info for id [ %v ]. Error: %v\n", report.AccountID, e) return e } product := p.(cusModels.Product) c, e := d.Cache.Get(product.CustomerID, "customer", token) if e != nil { l.Warning.Printf("[DB] Something went wrong while retrieving the customer info for id [ %v ]. Error: %v\n", product.CustomerID, e) return e } customer := c.(cusModels.Customer) account, e := d.GetAccountStatus(customer.CustomerID) if e != nil { l.Trace.Printf("[DB] There's no account associated in the customer level ID: [ %v ], moving to the next level...\n", customer.CustomerID) acc, e := d.GetAccountStatus(customer.ResellerID) if e != nil { l.Trace.Printf("[DB] There's no account associated in the reseller level. ID: [ %v ], skipping this product [ %v ].\n", customer.ResellerID, product.ProductID) return nil } account = acc } if !(account.Enabled) { l.Trace.Printf("[DB] Account associated [ %v ] is disabled! Skipping...\n", account.AccountID) return nil } if product.PlanID != "" { planID = product.PlanID } else { if customer.PlanID != "" { planID = customer.PlanID } else { r, e := d.Cache.Get(customer.ResellerID, "reseller", token) if e != nil { l.Warning.Printf("[DB] Something went wrong while retrieving the reseller info for id [ %v ]. Error: %v\n", customer.ResellerID, e) return e } reseller := r.(cusModels.Reseller) planID = reseller.PlanID } } PlanDefault: p, e = d.Cache.Get(planID, "plan", token) if e != nil { if planID == "DEFAULT" { l.Warning.Printf("[DB] Something went wrong while retrieving the default plan id [ %v ]. Error: %v\n", planID, e) return e } l.Warning.Printf("[DB] Something went wrong while retrieving the plan id [ %v ]. Re-trying with default plan. Error: %v\n", planID, e) planID = "DEFAULT" goto PlanDefault } plan := p.(pmModels.Plan) // In case the plan is not valid we return to the deault plan (id 0) which is valid ad vitam if (time.Now()).After((time.Time)(*plan.OfferedEndDate)) || (time.Now()).Before((time.Time)(*plan.OfferedStartDate)) { l.Warning.Printf("[DB] The plan [ %v ] is only valid between [ %v ] and [ %v ]. Falling back to default plan.\n", plan.ID, *plan.OfferedStartDate, *plan.OfferedEndDate) planID = "DEFAULT" goto PlanDefault } skus := make(map[string]*pmModels.SkuPrice) for _, k := range plan.SkuPrices { skus[k.SkuName] = k } var cashDelta, creditDelta float64 for i := range report.Usage { var value interface{} mode := *skus[report.Usage[i].ResourceType].AccountingMode if mode == AC_CREDIT || mode == AC_BOTH { for _, j := range states { value = report.Usage[i].UsageBreakup[j] if value == nil { l.Debug.Printf("[DB] The state [ %v ] seem to not be part of the usage UsageBreakup [ %+v ], skipping...", j, report.Usage[i].UsageBreakup) continue } if k := reflect.ValueOf(value); k.Kind() == reflect.Float64 { creditDelta += value.(float64) * skus[report.Usage[i].ResourceType].UnitCreditPrice continue } v, e := value.(json.Number).Float64() if e != nil { l.Warning.Printf("[DB] There was a problem retrieving the float value from the usagebreakup interface. Error: %v.\n", e) return e } creditDelta += v * skus[report.Usage[i].ResourceType].UnitCreditPrice } } if mode == AC_CASH || mode == AC_BOTH { value = report.Usage[i].Cost["totalFromSku"] if k := reflect.ValueOf(value); k.Kind() == reflect.Float64 { cashDelta += value.(float64) continue } v, e := value.(json.Number).Float64() if e != nil { l.Warning.Printf("[DB] There was a problem retrieving the float value from the cost interface. Error: %v.\n", e) return e } cashDelta += v } } state, e := d.AddConsumption(account.AccountID, d.getNiceFloat(creditDelta), MED_CREDIT) if e != nil { l.Warning.Printf("[DB] There was a problem while adding the consumption to the account [ %v ]. Error: %v.\n", account.AccountID, e) return e } if state.AvailableCredit < float64(0) { l.Warning.Printf("[DB] Account [ %v ] credit is not positive. Credit: [ %v ].\n", account.AccountID, state.AvailableCredit) } l.Trace.Printf("[DB] Account [ %v ] has been updated with a [ %v ] consumption.\n", account.AccountID, creditDelta) state, e = d.AddConsumption(account.AccountID, d.getNiceFloat(cashDelta), MED_CASH) if e != nil { l.Warning.Printf("[DB] There was a problem while adding the consumption to the account [ %v ]. Error: %v.\n", account.AccountID, e) return e } if state.AvailableCash < float64(0) { l.Warning.Printf("[DB] Account [ %v ] cash is not positive. Cash: [ %v ].\n", account.AccountID, state.AvailableCash) } l.Trace.Printf("[DB] Account [ %v ] has been updated with a [ %v ] consumption.\n", account.AccountID, cashDelta) totalTime += float64(time.Now().UnixNano()-now) / float64(time.Millisecond) totalCount++ d.Metrics["count"].With(prometheus.Labels{"type": "CDRs procesed"}).Inc() d.Metrics["time"].With(prometheus.Labels{"type": "CDRs average processing time"}).Set(totalTime / float64(totalCount)) return nil } func (d *DbParameter) getNiceFloat(i float64) (o float64) { return float64(math.Round(i*scaler) / scaler) }