package data import ( "encoding/json" "fmt" "log" "os" "path" "recipe-manager/helpers" "recipe-manager/models" "recipe-manager/services/logger" "slices" "sort" "strconv" "strings" "time" "reflect" "go.uber.org/zap" "golang.org/x/text/collate" "golang.org/x/text/language" ) type RecipeWithTimeStamps struct { Recipe map[string]*models.Recipe TimeStamps int64 } type Data struct { CurrentFile map[string]string CurrentCountryID map[string]string DefaultCountryMap []DefaultByCountry AllRecipeFiles map[string][]helpers.RecipePath CurrentRecipe map[string]*models.Recipe recipeMap map[string]RecipeWithTimeStamps Countries []helpers.CountryName taoLogger *logger.TaoLogger redisClient *RedisCli } type DefaultByCountry struct { CountryShortName string CountryLongName string DefaultFileVersion int } // sorting type recipe01Sorter struct { recipe01s []models.Recipe01 byKey func(r1, r2 *models.Recipe01) bool } // Len implements sort.Interface. func (r *recipe01Sorter) Len() int { return len(r.recipe01s) } // Less implements sort.Interface. func (r *recipe01Sorter) Less(i int, j int) bool { return r.byKey(&r.recipe01s[i], &r.recipe01s[j]) } // Swap implements sort.Interface. func (r *recipe01Sorter) Swap(i int, j int) { r.recipe01s[i], r.recipe01s[j] = r.recipe01s[j], r.recipe01s[i] } type ByRecipe01Field func(r1, r2 *models.Recipe01) bool func (by ByRecipe01Field) Sort(recipe01s []models.Recipe01) { sorter_inst := &recipe01Sorter{ recipe01s: recipe01s, byKey: by, } sort.Sort(sorter_inst) // set back to cache } func SortRecipe01ByFieldName(field string, ascending bool) func(r1, r2 *models.Recipe01) bool { return func(r1, r2 *models.Recipe01) bool { // get value by reflect v1 := reflect.Indirect(reflect.ValueOf(r1)).FieldByName(field) v2 := reflect.Indirect(reflect.ValueOf(r2)).FieldByName(field) asAscending := false // ensure type is string if v1.Kind() == reflect.String { // check field name if field == "LastChange" { // special case, do date parser // parse date layout := "02-Jan-2006 15:04:05" // noLeftLastChange := false // noRightLastChange := false time1, err := time.Parse(layout, v1.String()) if err != nil { // noLeftLastChange = true } time2, err := time.Parse(layout, v2.String()) if err != nil { // noRightLastChange = true } asAscending = time1.Before(time2) } else { asAscending = string(v1.String()) < string(v2.String()) } } if ascending { return asAscending } // descending return !asAscending } } func partition(recipe01s []models.Recipe01, field string, low int, high int, isDate bool) ([]models.Recipe01, int) { pivot := recipe01s[high] pivotField := reflect.Indirect(reflect.ValueOf(pivot)).FieldByName(field) init := low for iter := low; iter < high; iter++ { currentFieldIter := reflect.Indirect(reflect.ValueOf(recipe01s[iter])).FieldByName(field) if !isDate { if string(currentFieldIter.String()) < string(pivotField.String()) { recipe01s[init], recipe01s[iter] = recipe01s[iter], recipe01s[init] init++ } } else { // parse date layout := "02-Jan-2006 15:04:05" timePivot, _ := time.Parse(layout, pivotField.String()) timeIter, _ := time.Parse(layout, currentFieldIter.String()) if timeIter.Before(timePivot) { recipe01s[init], recipe01s[iter] = recipe01s[iter], recipe01s[init] init++ } } } recipe01s[init], recipe01s[high] = recipe01s[high], recipe01s[init] return recipe01s, init } func quickSortRecipe01(recipe01s []models.Recipe01, field string, low int, high int) []models.Recipe01 { isDate := false if field == "LastChange" { isDate = true } if low < high { var pivotIdx int recipe01s, pivotIdx = partition(recipe01s, field, low, high, isDate) recipe01s = quickSortRecipe01(recipe01s, field, low, pivotIdx-1) recipe01s = quickSortRecipe01(recipe01s, field, pivotIdx+1, high) } return recipe01s } var ( countries = []helpers.CountryName{{ CountryID: "tha", CountryName: "Thailand", }, { CountryID: "mys", CountryName: "Malaysia", }, { CountryID: "aus", CountryName: "Australia", }, { CountryID: "dubai", CountryName: "UAE Dubai", }, { CountryID: "counter01", CountryName: "Counter Cafe", }, { CountryID: "sgp", CountryName: "Singapore", }, { CountryID: "cocktail_tha", CountryName: "Cocktail", }, } ) func NewData(taoLogger *logger.TaoLogger, redisClient *RedisCli) *Data { allRecipeFiles := helpers.ScanRecipeFiles(helpers.LoadCountrySettings()) // fmt.Println(allRecipeFiles) defaultFile := "coffeethai02_600.json" // read 'version' file by country // versionPath := path.Join("cofffeemachineConfig", defaultCountry, "version") // taoLogger.Log.Debug("version", zap.Any("version path", versionPath)) // // versionFile, err := os.Open(versionPath) // content, err := os.ReadFile(versionPath) // if err != nil { // taoLogger.Log.Debug("Error when open version file", zap.Error(err)) // } // initVersion := string(content) // // read latest version // // set latest to default version // latest_version, err := strconv.Atoi(initVersion) // if err != nil { // latest_version = 600 // } defaultForEachCountry := []DefaultByCountry{} for _, elem := range countries { // generate default of all countries currentVersionPath := path.Join("cofffeemachineConfig", elem.CountryID, "version") // this is default version for each country content, err := os.ReadFile(currentVersionPath) if err != nil { taoLogger.Log.Debug("Error when open version file", zap.Error(err)) } initVersion := string(content) // read latest version latest_version, _ := strconv.Atoi(initVersion) defaultForEachCountry = append(defaultForEachCountry, DefaultByCountry{CountryShortName: elem.CountryID, CountryLongName: elem.CountryName, DefaultFileVersion: latest_version}) // emit out latest version taoLogger.Log.Info("Latest version", zap.Any("country", elem.CountryID), zap.Any("version", latest_version)) } currentFileMap := make(map[string]string) CurrentCountryIDMap := make(map[string]string) currentDefaultFileForEachCountry := make(map[string]*models.Recipe) // all default versions as string versionsString := "" // loop default for each country for _, v := range defaultForEachCountry { for _, v2 := range allRecipeFiles[v.CountryShortName] { // extract filename as version current_version_iter, err := strconv.Atoi(strings.Split(strings.Split(v2.Name, "_")[1], ".")[0]) if err != nil { continue } if current_version_iter == v.DefaultFileVersion { currentFileMap[v.CountryShortName] = v2.Name CurrentCountryIDMap[v.CountryShortName] = v.CountryLongName versionsString = versionsString + v.CountryShortName + ":" + strconv.Itoa(current_version_iter) + "," // do read default defaultRecipe, err := helpers.ReadRecipeFile(v.CountryShortName, v2.Name) if err != nil { log.Panic("Error when read default recipe file for each country:", v.CountryShortName, err) } redisClient.SetToKey(v2.Name, defaultRecipe) currentDefaultFileForEachCountry[v.CountryShortName] = defaultRecipe break } } } // for _, v := range allRecipeFiles[defaultCountry] { // // extract filename as version // current_version_iter, err := strconv.Atoi(strings.Split(strings.Split(v.Name, "_")[1], ".")[0]) // if err != nil { // continue // } // if current_version_iter == latest_version { // // taoLogger.Log.Debug("current_version_iter", zap.Any("current_version_iter", current_version_iter)) // // set latest // latest_version = current_version_iter // defaultFile = v.Name // break // } // } // FIXME: default file bug. do assign each default recipe model to each country // taoLogger.Log.Debug("defaultFile", zap.Any("defaultFile", defaultFile), zap.Any("latest_version", versionsString)) // defaultRecipe, err := helpers.ReadRecipeFile(defaultCountry, defaultFile) // if err != nil { // log.Panic("Error when read default recipe file:", err) // } fmt.Println(CurrentCountryIDMap) return &Data{ CurrentFile: currentFileMap, CurrentCountryID: CurrentCountryIDMap, AllRecipeFiles: allRecipeFiles, CurrentRecipe: currentDefaultFileForEachCountry, recipeMap: map[string]RecipeWithTimeStamps{ defaultFile: { Recipe: currentDefaultFileForEachCountry, TimeStamps: time.Now().Unix(), }, }, Countries: countries, taoLogger: taoLogger, DefaultCountryMap: defaultForEachCountry, redisClient: redisClient, } } func (d *Data) GetRecipe(countryID, filename string) *models.Recipe { d.taoLogger.Log.Debug("invoke GetRecipe", zap.String("countryID", countryID), zap.String("filename", filename)) // concat submenu into recipe if countryID == "" { d.taoLogger.Log.Debug("GetRecipe", zap.Any("EmptyCountryId", "return default country = tha")) return d.CurrentRecipe["tha"] } if filename == "" { d.taoLogger.Log.Debug("GetRecipe", zap.Any("EmptyFilename", filename)) return d.CurrentRecipe[countryID] } // add validate flag needValidate := false // do check if match the current pointer if d.CurrentFile[countryID] == filename { d.taoLogger.Log.Debug("GetRecipe", zap.Any("FileMatchCurrent", "return from stored "+filename), zap.Any("CurrentFile", d.CurrentFile), zap.Any("countryID", countryID)) d.taoLogger.Log.Debug("CurrentRecipeOK?", zap.Any("CurrentRecipe", d.CurrentRecipe[countryID] != nil)) // make sure recipe vesion is equal currentConfig := d.CurrentRecipe[countryID].MachineSetting.ConfigNumber // get requested version requestedConfig, _ := strconv.Atoi(strings.Split(strings.Split(filename, "_")[1], ".")[0]) if currentConfig != requestedConfig { // TODO: from `/dashboard`, this already checked the version if matched, d.taoLogger.Log.Debug("GetRecipe", zap.Any("ActualFileNotMatch", "Skip this!")) needValidate = true } else { // detect patches, return original var cached_original_recipe *models.Recipe = &models.Recipe{} err := d.redisClient.GetKeyTo(filename, &cached_original_recipe) // for index, v := range cached_original_recipe.Recipe01 { // fmt.Println(index, " ", v.ProductCode) // } if err == nil && d.redisClient.HealthCheck() == nil { d.taoLogger.Log.Debug("GetRecipe.NoReturnUpdated", zap.Any("target", filename)) return cached_original_recipe } // if equal, OK d.taoLogger.Log.Debug("GetRecipe.NoReturnUpdatedRedisOffline", zap.Any("target", filename)) return d.CurrentRecipe[countryID] } } if recipe, ok := d.recipeMap[filename]; ok && d.redisClient.HealthCheck() != nil { d.taoLogger.Log.Debug("GetRecipe", zap.Any("ValidOnStored", "return from stored "+filename)) // d.CurrentFile[countryID] = filename // d.CurrentCountryID[countryID] = countryID // make sure recipe vesion is equal currentConfig := d.CurrentRecipe[countryID].MachineSetting.ConfigNumber // get requested version requestedConfig, _ := strconv.Atoi(strings.Split(strings.Split(filename, "_")[1], ".")[0]) if currentConfig != requestedConfig { d.taoLogger.Log.Debug("GetRecipe", zap.Any("InvalidOnStored", "Skip this!")) } else { // detect patches, return original var cached_original_recipe *models.Recipe = &models.Recipe{} err := d.redisClient.GetKeyTo(filename, &cached_original_recipe) if err == nil && d.redisClient.HealthCheck() == nil { d.taoLogger.Log.Debug("GetRecipe.NoReturnUpdated", zap.Any("target", filename)) return cached_original_recipe } d.taoLogger.Log.Debug("GetRecipe.ReturnDefault", zap.Any("error_cache_recipe", err)) // if equal, OK return recipe.Recipe[countryID] } } // change current version and read new recipe if filename == "default" { // use redis cache if d.redisClient.HealthCheck() == nil { d.taoLogger.Log.Debug("GetRecipe", zap.Any("RedisOffline", "return default recipe")) // var defaultFileName string d.taoLogger.Log.Debug("GetRcipe.TryGetDefaultFromRedis", zap.Any("ByCountry", countryID)) var cached_default_recipe *models.Recipe = &models.Recipe{} err := d.redisClient.GetKeyTo(countryID, cached_default_recipe) if err != nil { filename = d.CurrentFile[countryID] d.taoLogger.Log.Debug("GetRecipe.ReplaceDefaultCase2", zap.Any("RedisGetKeyFail+getFromServerMem", filename), zap.Any("Error", err)) } else { // filename = defaultFileName d.taoLogger.Log.Debug("GetRecipe.ReplaceDefaultCase3", zap.Any("Case3+RedisOK,do check if matched", filename)) return cached_default_recipe } } else { filename = d.CurrentFile[countryID] d.taoLogger.Log.Debug("GetRecipe.ReplaceDefaultCase1", zap.Any("RedisOffline+getFromServerMem", filename)) } } d.taoLogger.Log.Debug("GetRecipe.ReplaceDefault", zap.Any("Filename", filename), zap.Any("Conflict", needValidate)) // var recipe *models.Recipe = &models.Recipe{} // do check if redis contains the recipe var cached_recipe *models.Recipe = &models.Recipe{} if err := d.redisClient.GetKeyTo(filename, cached_recipe); err != nil { d.taoLogger.Log.Debug("GetRecipe.Cached", zap.Any("GetCacheRecipeError", err)) d.taoLogger.Log.Debug("GetRecipe", zap.String("filename", filename), zap.String("countryID", countryID)) // d.CurrentCountryID[countryID] = countryID cached_recipe = nil } if cached_recipe != nil { d.taoLogger.Log.Debug("GetRecipe", zap.Any("Check on cached recipe invalid", cached_recipe == nil), zap.Any("test config number", cached_recipe.MachineSetting.ConfigNumber)) // set to current // d.CurrentRecipe[countryID] = cached_recipe return cached_recipe } recipe, err := helpers.ReadRecipeFile(countryID, filename) if err != nil { d.taoLogger.Log.Debug("GetRecipe", zap.Any("ReadRecipeError -> return default", err)) return d.CurrentRecipe[countryID] } // cache to redis err = d.redisClient.SetToKey(filename, recipe) if err != nil { d.taoLogger.Log.Error("GetRecipe: Error when read recipe file, Return default recipe", zap.Error(err)) return d.CurrentRecipe[countryID] } //. service is connected. Use from cache // check healthcheck redis var return_recipe *models.Recipe = &models.Recipe{} err = d.redisClient.HealthCheck() d.taoLogger.Log.Info("GetRecipe: HealthCheck", zap.Any("result", err)) if d.redisClient.HealthCheck() == nil && cached_recipe != nil { d.taoLogger.Log.Debug("GetRecipeCached", zap.Any("cached_recipe", "yes")) // d.CurrentRecipe[countryID] = cached_recipe return_recipe = cached_recipe } else { d.taoLogger.Log.Debug("GetRecipeCached", zap.Any("cached_recipe", "no")) // TODO: handle country and recipe not match isFound, index := d.ValidateUpdateCurrentRecipePointer(countryID, filename) if isFound && index != -1 { d.CurrentRecipe[countryID] = recipe } return_recipe = recipe } // save to map if len(d.recipeMap) > 5 { // limit keep in memory 5 version // remove oldest version var oldestVersion string var oldestTime int64 for k, v := range d.recipeMap { if oldestTime == 0 || v.TimeStamps < oldestTime { oldestTime = v.TimeStamps oldestVersion = k } } delete(d.recipeMap, oldestVersion) } d.recipeMap[filename] = RecipeWithTimeStamps{ Recipe: d.CurrentRecipe, TimeStamps: time.Now().Unix(), } return return_recipe } // func (d *Data) GetRecipe01() []models.Recipe01 { // return d.currentRecipe.Recipe01 // } // func (d *Data) GetCurrentRecipe() *models.Recipe { // return d.currentRecipe // } func loopMatchProductCode(recipe01s []models.Recipe01, productCode string) (models.Recipe01, error) { for _, v := range recipe01s { if v.ProductCode == productCode { return v, nil } else if len(v.SubMenu) > 0 { for _, subMenu := range v.SubMenu { if subMenu.ProductCode == productCode { return subMenu, nil } } } } return models.Recipe01{}, fmt.Errorf("NotFound") } func (d *Data) GetRecipe01ByProductCode(filename, countryID, productCode string) (models.Recipe01, error) { // try convert if len(countryID) != 3 { for k, v := range d.CurrentCountryID { // //fmt.Println("GetRecipe01ByProductCode.Iterate", k, v, v == countryID) if v == countryID { countryID = k break } } } // //fmt.Println("GetRecipe01ByProductCode", filename, countryID, productCode) if !strings.Contains(filename, "tmp") { if filename == "" || filename == d.CurrentFile[countryID] { // , d.CurrentFile, countryID, "result by country id", len(d.currentRecipe[countryID].Recipe01) // //fmt.Println("GetRecipe01ByProductCode.ReadCurrent::filename", filename) // //fmt.Println("GetRecipe01ByProductCode.ReadCurrent::countryID", countryID) // //fmt.Println("GetRecipe01ByProductCode.ReadCurrent::CurrentFile", d.CurrentFile) // //fmt.Println("GetRecipe01ByProductCode.ReadCurrent::CurrentCountryID", d.CurrentCountryID) // if redis online if d.redisClient.HealthCheck() == nil { d.taoLogger.Log.Debug("GetRecipe01ByProductCode", zap.Any("useRedis", true)) recipeFromRedis := d.GetRecipe(countryID, filename) // find productCode from this // for _, v := range recipeFromRedis.Recipe01 { // if v.ProductCode == productCode { // return v, nil // } else if len(v.SubMenu) > 0 { // for _, subMenu := range v.SubMenu { // if subMenu.ProductCode == productCode { // return subMenu, nil // } // } // } // } recipe01, err := loopMatchProductCode(recipeFromRedis.Recipe01, productCode) if err == nil { return recipe01, nil } } else { d.taoLogger.Log.Debug("GetRecipe01ByProductCode", zap.Any("useRedis", false)) // for _, v := range d.CurrentRecipe[countryID].Recipe01 { // if v.ProductCode == productCode { // return v, nil // } else if len(v.SubMenu) > 0 { // for _, subMenu := range v.SubMenu { // if subMenu.ProductCode == productCode { // return subMenu, nil // } // } // } // } recipe01, err := loopMatchProductCode(d.CurrentRecipe[countryID].Recipe01, productCode) if err == nil { return recipe01, nil } } // //fmt.Println("No result in current recipe", countryID) } else if recipe, ok := d.recipeMap[filename]; ok { // //fmt.Println("GetRecipe01ByProductCode.ReadMap", filename, d.CurrentFile, recipe.Recipe[countryID], "countryID=", countryID) // for _, v := range recipe.Recipe[countryID].Recipe01 { // if v.ProductCode == productCode { // // d.taoLogger.Log.Debug("GetRecipe01ByProductCode.getSuccess", zap.Any("fromFile", filename), zap.Any("whereSource", d.recipeMap)) // return v, nil // } else if len(v.SubMenu) > 0 { // for _, subMenu := range v.SubMenu { // if subMenu.ProductCode == productCode { // // d.taoLogger.Log.Debug("GetRecipe01ByProductCode.getSuccess", zap.Any("fromFile", filename), zap.Any("whereSource", d.recipeMap)) // return subMenu, nil // } // } // } // } // if redis online if d.redisClient.HealthCheck() == nil { d.taoLogger.Log.Debug("GetRecipe01ByProductCode", zap.Any("useRedis", true)) recipeFromRedis := d.GetRecipe(countryID, filename) // find productCode from this // for _, v := range recipeFromRedis.Recipe01 { // if v.ProductCode == productCode { // return v, nil // } else if len(v.SubMenu) > 0 { // for _, subMenu := range v.SubMenu { // if subMenu.ProductCode == productCode { // return subMenu, nil // } // } // } // } recipe01, err := loopMatchProductCode(recipeFromRedis.Recipe01, productCode) if err == nil { return recipe01, nil } } else { d.taoLogger.Log.Debug("GetRecipe01ByProductCode", zap.Any("useRedis", false)) recipe01, err := loopMatchProductCode(recipe.Recipe[countryID].Recipe01, productCode) if err == nil { return recipe01, nil } } d.taoLogger.Log.Debug("GetRecipe01ByProductCode.getFail", zap.Any("fromFile", filename), zap.Any("whereSource", d.recipeMap)) } } d.taoLogger.Log.Debug("GetRecipe01ByProductCode", zap.Any("filename", filename), zap.Any("countryID", countryID), zap.Any("productCode", productCode)) if filename == "default" { filename = d.CurrentFile[countryID] } // d.CurrentFile[countryID] = filename // d.CurrentCountryID[countryID] = countryID for _, v := range countries { if v.CountryName == countryID { // d.CurrentCountryID[countryID] = v.CountryID countryID = v.CountryID break } } recipe := d.GetRecipe(countryID, filename) // if err != nil { // d.taoLogger.Log.Error("GetRecipe01ByProductCode: Error when read recipe file, Return default recipe", zap.Error(err)) // for _, v := range d.CurrentRecipe[countryID].Recipe01 { // if v.ProductCode == productCode { // return v, fmt.Errorf("[DEFAULT]-ERR") // } else if len(v.SubMenu) > 0 { // for _, subMenu := range v.SubMenu { // if subMenu.ProductCode == productCode { // return subMenu, fmt.Errorf("[DEFAULT]-ERR") // } // } // } // } // } d.taoLogger.Log.Debug("GetRecipe01ByProductCode", zap.Any("productCode", productCode), zap.Any("version", recipe.MachineSetting.ConfigNumber)) // TODO: handle country and recipe not match isFound, index := d.ValidateUpdateCurrentRecipePointer(countryID, filename) if isFound && index != -1 { d.CurrentRecipe[countryID] = recipe } // save to map if len(d.recipeMap) > 5 { // limit keep in memory 5 version // remove oldest version var oldestVersion string var oldestTime int64 for k, v := range d.recipeMap { if oldestTime == 0 || v.TimeStamps < oldestTime { oldestTime = v.TimeStamps oldestVersion = k } } delete(d.recipeMap, oldestVersion) } d.recipeMap[filename] = RecipeWithTimeStamps{ Recipe: d.CurrentRecipe, TimeStamps: time.Now().Unix(), } // for _, v := range d.CurrentRecipe[countryID].Recipe01 { // if v.ProductCode == productCode { // // d.taoLogger.Log.Debug("GetRecipe01ByProductCode", zap.Any("productCode", productCode), zap.Any("result", v)) // return v, nil // } else if len(v.SubMenu) > 0 { // for _, subMenu := range v.SubMenu { // if subMenu.ProductCode == productCode { // // d.taoLogger.Log.Debug("GetRecipe01ByProductCode", zap.Any("productCode", productCode), zap.Any("result", subMenu)) // return subMenu, nil // } // } // } // } recipe01, err := loopMatchProductCode(d.CurrentRecipe[countryID].Recipe01, productCode) if err == nil { return recipe01, nil } return models.Recipe01{}, fmt.Errorf("product code: %s not found", productCode) } func (d *Data) SetValuesToRecipe(base_recipe []models.Recipe01, recipe models.Recipe01) { not_found := false // global_idx := 0 for index, v := range base_recipe { if v.ProductCode == recipe.ProductCode { // Log.Debug("SetValuesToRecipe", zap.Any("old", v), zap.Any("new", recipe)) // v = recipe // change only changed values // transform to map base_recipe01_Map := v.ToMap() recipe01_Map := recipe.ToMap() for k, v := range recipe01_Map { if !reflect.DeepEqual(base_recipe01_Map[k], v) { d.taoLogger.Log.Debug("SetValuesToRecipe", zap.Any("key", k), zap.Any("old", base_recipe01_Map[k]), zap.Any("new", v)) base_recipe01_Map[k] = v } } base_recipe[index] = base_recipe[index].FromMap(base_recipe01_Map) not_found = false break } else if len(v.SubMenu) > 0 { for _, sub := range v.SubMenu { if sub.ProductCode == recipe.ProductCode { // Log.Debug("SetValuesToRecipe.SubMenu", zap.Any("old", sub), zap.Any("new", recipe)) // sub = recipe // change only changed values // transform to map base_recipe01_Map := sub.ToMap() recipe01_Map := recipe.ToMap() for k, v := range recipe01_Map { if !reflect.DeepEqual(base_recipe01_Map[k], v) { d.taoLogger.Log.Debug("SetValuesToRecipe.SubMenu", zap.Any("key", k), zap.Any("old", base_recipe01_Map[k]), zap.Any("new", v)) base_recipe01_Map[k] = v } } } } } else { not_found = true // global_idx = index } } if not_found { // base_recipe[global_idx+1] = recipe base_recipe = append(base_recipe, recipe) } } func (d *Data) SetValuesToMaterialSetting(base_mat_setting []models.MaterialSetting, updated_mat_setting models.MaterialSetting) { not_found := false global_idx := 0 for index, v := range base_mat_setting { // find matched id if v.ID == updated_mat_setting.ID { // change only changed values for k, v := range updated_mat_setting.ToMap() { if !reflect.DeepEqual(base_mat_setting[index].ToMap()[k], v) { d.taoLogger.Log.Debug("SetValuesToMaterialSetting", zap.Any("key", k), zap.Any("old", base_mat_setting[index].ToMap()[k]), zap.Any("new", v)) base_mat_setting[index].ToMap()[k] = v } } } else { not_found = true global_idx = index } } // is new value if not_found { base_mat_setting[global_idx+1] = updated_mat_setting } } func (d *Data) SetValuesToToppingList(base_topping_list []models.ToppingList, updated_topping_list models.ToppingList) { not_found := false global_idx := 0 for index, v := range base_topping_list { // find matched id if v.ID == updated_topping_list.ID { // change only changed values for k, v := range updated_topping_list.ToMap() { if !reflect.DeepEqual(base_topping_list[index].ToMap()[k], v) { d.taoLogger.Log.Debug("SetValuesToToppingList", zap.Any("key", k), zap.Any("old", base_topping_list[index].ToMap()[k]), zap.Any("new", v)) base_topping_list[index].ToMap()[k] = v } } } else { not_found = true global_idx = index } } // is new value if not_found { base_topping_list[global_idx+1] = updated_topping_list } } func (d *Data) SetValuesToToppingGroupList(base_topping_group_list []models.ToppingGroup, updated_topping_group_list models.ToppingGroup) { not_found := false global_idx := 0 for index, v := range base_topping_group_list { // find matched id if v.GroupID == updated_topping_group_list.GroupID { // change only changed values for k, v := range updated_topping_group_list.ToMap() { if !reflect.DeepEqual(base_topping_group_list[index].ToMap()[k], v) { d.taoLogger.Log.Debug("SetValuesToToppingGroup", zap.Any("key", k), zap.Any("old", base_topping_group_list[index].ToMap()[k]), zap.Any("new", v)) base_topping_group_list[index].ToMap()[k] = v } } } else { not_found = true global_idx = index } } // is new value if not_found { base_topping_group_list[global_idx+1] = updated_topping_group_list } } func (d *Data) GetMaterialSetting(countryID, filename string) []models.MaterialSetting { // result := make([]models.MaterialSetting, 0) if countryID == "" { // copy(result, d.currentRecipe[countryID].MaterialSetting) return d.CurrentRecipe[countryID].MaterialSetting } if !strings.Contains(filename, "tmp") { if filename == "" || filename == d.CurrentFile[countryID] { // copy(result, d.currentRecipe[countryID].MaterialSetting) // d.taoLogger.Log.Debug("GetMaterialSetting", zap.Any("result", result)) return d.CurrentRecipe[countryID].MaterialSetting } // if recipe, ok := d.recipeMap[filename]; ok { // copy(result, recipe.Recipe[countryID].MaterialSetting) // d.CurrentFile[countryID] = filename // // d.CurrentCountryID[countryID] = countryID // return d.CurrentRecipe[countryID].MaterialSetting // } } if filename == "default" { filename = d.CurrentFile[countryID] } // d.taoLogger.Log.Debug("GetMaterialSetting", zap.Any("filename", filename), zap.Any("countryID", countryID)) // d.CurrentFile[countryID] = filename // d.CurrentCountryID[countryID] = countryID recipe := d.GetRecipe(countryID, filename) // if err != nil { // d.taoLogger.Log.Error("GetMaterialSetting: Error when read recipe file, Return default recipe", zap.Error(err)) // copy(result, d.CurrentRecipe[countryID].MaterialSetting) // return d.CurrentRecipe[countryID].MaterialSetting // } // d.taoLogger.Log.Debug("GetMaterialSetting", zap.Any("recipe", recipe.MaterialSetting)) // TODO: handle country and recipe not match isFound, index := d.ValidateUpdateCurrentRecipePointer(countryID, filename) if isFound && index != -1 { d.CurrentRecipe[countryID] = recipe } // save to map if len(d.recipeMap) > 5 { // limit keep in memory 5 version // remove oldest version var oldestVersion string var oldestTime int64 for k, v := range d.recipeMap { if oldestTime == 0 || v.TimeStamps < oldestTime { oldestTime = v.TimeStamps oldestVersion = k } } delete(d.recipeMap, oldestVersion) } d.recipeMap[filename] = RecipeWithTimeStamps{ Recipe: d.CurrentRecipe, TimeStamps: time.Now().Unix(), } // copy(result, recipe.MaterialSetting) return recipe.MaterialSetting } func (d *Data) GetAllToppingGroups(countryID, filename string) []models.ToppingGroup { if countryID == "" { return d.CurrentRecipe[countryID].Topping.ToppingGroup } if !strings.Contains(filename, ".tmp") { if filename == "" || filename == d.CurrentFile[countryID] { return d.CurrentRecipe[countryID].Topping.ToppingGroup } // if _, ok := d.recipeMap[countryID]; ok { // d.CurrentFile[countryID] = filename // return d.CurrentRecipe[countryID].Topping.ToppingGroup // } } if filename == "default" { filename = d.CurrentFile[countryID] } recipe := d.GetRecipe(countryID, filename) // TODO: handle country and recipe not match isFound, index := d.ValidateUpdateCurrentRecipePointer(countryID, filename) if isFound && index != -1 { d.CurrentRecipe[countryID] = recipe } if len(d.recipeMap) > 5 { // limit keep in memory 5 version // remove oldest version var oldestVersion string var oldestTime int64 for k, v := range d.recipeMap { if oldestTime == 0 || v.TimeStamps < oldestTime { oldestTime = v.TimeStamps oldestVersion = k } } delete(d.recipeMap, oldestVersion) } d.recipeMap[filename] = RecipeWithTimeStamps{ Recipe: d.CurrentRecipe, TimeStamps: time.Now().Unix(), } return recipe.Topping.ToppingGroup } func (d *Data) GetToppingsList(countryID, filename string) []models.ToppingList { // do return default if countryID == "" { return d.CurrentRecipe[countryID].Topping.ToppingList } // handle temporary file if !strings.Contains(filename, ".tmp") { if filename == "" || filename == d.CurrentFile[countryID] { return d.CurrentRecipe[countryID].Topping.ToppingList } // if _, ok := d.recipeMap[countryID]; ok { // d.CurrentFile[countryID] = filename // return d.CurrentRecipe[countryID].Topping.ToppingList // } } if filename == "default" { filename = d.CurrentFile[countryID] } recipe := d.GetRecipe(countryID, filename) if len(d.recipeMap) > 5 { // limit keep in memory 5 version // remove oldest version var oldestVersion string var oldestTime int64 for k, v := range d.recipeMap { if oldestTime == 0 || v.TimeStamps < oldestTime { oldestTime = v.TimeStamps oldestVersion = k } } delete(d.recipeMap, oldestVersion) } d.recipeMap[filename] = RecipeWithTimeStamps{ Recipe: d.CurrentRecipe, TimeStamps: time.Now().Unix(), } return recipe.Topping.ToppingList } func (d *Data) GetMaterialCode(ids []uint64, countryID, filename string) []models.MaterialCode { var result []models.MaterialCode if filename == "" || filename == d.CurrentFile[countryID] { result = d.CurrentRecipe[countryID].MaterialCode } else { // else if recipe, ok := d.recipeMap[filename]; ok { // d.CurrentFile[countryID] = filename // return recipe.Recipe[countryID].MaterialCode // } // else { if filename == "default" { filename = d.CurrentFile[countryID] } // d.CurrentFile[countryID] = filename // d.CurrentCountryID[countryID] = countryID recipe := d.GetRecipe(countryID, filename) // if err != nil { // d.taoLogger.Log.Error("GetMaterialCode: Error when read recipe file, Return default recipe", zap.Error(err)) // return d.CurrentRecipe[countryID].MaterialCode // } // TODO: handle country and recipe not match isFound, index := d.ValidateUpdateCurrentRecipePointer(countryID, filename) if isFound && index != -1 { d.CurrentRecipe[countryID] = recipe } // save to map if len(d.recipeMap) > 5 { // limit keep in memory 5 version // remove oldest version var oldestVersion string var oldestTime int64 for k, v := range d.recipeMap { if oldestTime == 0 || v.TimeStamps < oldestTime { oldestTime = v.TimeStamps oldestVersion = k } } delete(d.recipeMap, oldestVersion) } d.recipeMap[filename] = RecipeWithTimeStamps{ Recipe: d.CurrentRecipe, TimeStamps: time.Now().Unix(), } result = d.CurrentRecipe[countryID].MaterialCode } if len(ids) == 0 { return result } resultFilter := make([]models.MaterialCode, len(ids)) for _, id := range ids { if id == 0 { continue } for _, m := range result { if m.MaterialID == id { resultFilter = append(resultFilter, m) break } } } return resultFilter } func (d *Data) GetToppings(countryID, filename string) models.Topping { if filename == "" || filename == d.CurrentFile[countryID] { return d.CurrentRecipe[countryID].Topping } // else if recipe, ok := d.recipeMap[filename]; ok { // d.CurrentFile[countryID] = filename // return recipe.Recipe[countryID].Topping // } // if filename == "default" { // filename = d.CurrentFile[countryID] // } // d.CurrentFile[countryID] = filename // d.CurrentCountryID[countryID] = countryID recipe := d.GetRecipe(countryID, filename) // TODO: handle country and recipe not match isFound, index := d.ValidateUpdateCurrentRecipePointer(countryID, filename) if isFound && index != -1 { d.CurrentRecipe[countryID] = recipe } return recipe.Topping } func (d *Data) GetToppingsOfRecipe(countryID, filename string, productCode string) ([]models.ToppingSet, error) { if filename == "default" { filename = d.CurrentFile[countryID] } recipe, err := d.GetRecipe01ByProductCode(filename, countryID, productCode) if err != nil { d.taoLogger.Log.Error("GetToppingOfRecipe: Error when read recipe file, Return default recipe", zap.Error(err)) return []models.ToppingSet{}, err } return recipe.ToppingSet, nil } func (d *Data) GetSubmenusOfRecipe(countryID, filename, productCode string) ([]models.Recipe01, error) { if filename == "default" { filename = d.CurrentFile[countryID] } recipe, err := d.GetRecipe01ByProductCode(filename, countryID, productCode) if err != nil { d.taoLogger.Log.Error("GetSubmenusOfRecipe: Error when read recipe file, Return default recipe", zap.Error(err)) return []models.Recipe01{}, err } submenu := recipe.SubMenu if submenu == nil { return []models.Recipe01{}, fmt.Errorf("no submenu") } return submenu, nil } func (d *Data) GetCountryNameByID(countryID string) (string, error) { for _, country := range d.Countries { if country.CountryID == countryID { fmt.Println("Found " + countryID) return country.CountryName, nil } } fmt.Println("Not found " + countryID) return "", fmt.Errorf("country ID: %s not found", countryID) } func (d *Data) GetCountryIDByName(countryName string) (string, error) { for _, country := range d.Countries { if country.CountryName == countryName { return country.CountryID, nil } } return "", fmt.Errorf("country name: %s not found", countryName) } // ------ sorting ------ // FIXME: sorting not working func (d *Data) SortRecipe(countryID, filename string, sort_by string, ascending bool) { // Get recipe recipe := d.GetRecipe(countryID, filename) // define default language priority collator := collate.New(language.Thai) slices.SortStableFunc(recipe.Recipe01, func(a, b models.Recipe01) int { switch sort_by { case "Name": av := reflect.Indirect(reflect.ValueOf(a)).FieldByName("Name") bv := reflect.Indirect(reflect.ValueOf(b)).FieldByName("Name") // add collator if !ascending { return collator.CompareString(bv.String(), av.String()) } return collator.CompareString(av.String(), bv.String()) case "Other Name": av := reflect.Indirect(reflect.ValueOf(a)).FieldByName("OtherName") bv := reflect.Indirect(reflect.ValueOf(b)).FieldByName("OtherName") // add collator if !ascending { return collator.CompareString(bv.String(), av.String()) } return collator.CompareString(av.String(), bv.String()) case "Description": av := reflect.Indirect(reflect.ValueOf(a)).FieldByName("Description") bv := reflect.Indirect(reflect.ValueOf(b)).FieldByName("Description") // add collator if !ascending { return collator.CompareString(bv.String(), av.String()) } return collator.CompareString(av.String(), bv.String()) case "Other Description": av := reflect.Indirect(reflect.ValueOf(a)).FieldByName("OtherDescription") bv := reflect.Indirect(reflect.ValueOf(b)).FieldByName("OtherDescription") // add collator if !ascending { return collator.CompareString(bv.String(), av.String()) } return collator.CompareString(av.String(), bv.String()) case "Product Code": av := reflect.Indirect(reflect.ValueOf(a)).FieldByName("ProductCode") bv := reflect.Indirect(reflect.ValueOf(b)).FieldByName("ProductCode") // // split by '-' // codes_a := strings.Split(av.String(), "-") // codes_b := strings.Split(bv.String(), "-") // // ensure productCode len // if len(codes_a) == len(codes_b) { // for iter := 0; iter > len(codes_a)-1; iter++ { // num_a, _ := strconv.Atoi(codes_a[iter]) // num_b, _ := strconv.Atoi(codes_b[iter]) // if num_a < num_b { // if !ascending { // return 1 // } // return -1 // } else if num_b < num_a { // if !ascending { // return -1 // } // return 1 // } else { // continue // } // } // } prea := strings.ReplaceAll(av.String(), "-", "") preb := strings.ReplaceAll(bv.String(), "-", "") inta, _ := strconv.Atoi(prea) intb, _ := strconv.Atoi(preb) if inta < intb { if !ascending { return 1 } return -1 } else if inta > intb { if !ascending { return -1 } return 1 } else { return 0 } case "Last Updated": av := reflect.Indirect(reflect.ValueOf(a)).FieldByName("LastChange") bv := reflect.Indirect(reflect.ValueOf(b)).FieldByName("LastChange") layout := "02-Jan-2006 15:04:05" timeA, _ := time.Parse(layout, av.String()) timeB, _ := time.Parse(layout, bv.String()) compare_result := 0 if !ascending { compare_result = timeB.Compare(timeA) } else { compare_result = timeA.Compare(timeB) } if compare_result == 0 { // has same time, compare product code av2 := reflect.Indirect(reflect.ValueOf(a)).FieldByName("ProductCode") bv2 := reflect.Indirect(reflect.ValueOf(b)).FieldByName("ProductCode") prea := strings.ReplaceAll(av2.String(), "-", "") preb := strings.ReplaceAll(bv2.String(), "-", "") inta, _ := strconv.Atoi(prea) intb, _ := strconv.Atoi(preb) if inta < intb { if !ascending { return 1 } return -1 } else if inta > intb { if !ascending { return -1 } return 1 } else { return 0 } } else { return compare_result } } // no switching order return 0 }) // Clean or re-sort switch sort_by { case "Product Code": // ByRecipe01Field(SortRecipe01ByFieldName("ProductCode", ascending)).Sort(recipe.Recipe01) // recipe.Recipe01 = quickSortRecipe01(recipe.Recipe01, "ProductCode", 0, len(recipe.Recipe01)-1) // collect product code and print // for _, r := range recipe.Recipe01 { // fmt.Println(r.ProductCode) // } // test sample batch size 10 result := quickSortRecipe01(recipe.Recipe01, "ProductCode", 0, 10) // for index, r := range recipe.Recipe01 { // fmt.Println(index, " ", r.ProductCode) // } // reassign if len(result) == len(recipe.Recipe01) { copy(recipe.Recipe01, result) } case "Name": // ByRecipe01Field(SortRecipe01ByFieldName("Name", ascending)).Sort(recipe.Recipe01) // recipe.Recipe01 = quickSortRecipe01(recipe.Recipe01, "Name", 0, len(recipe.Recipe01)-1) case "Other Name": // ByRecipe01Field(SortRecipe01ByFieldName("OtherName", ascending)).Sort(recipe.Recipe01) // recipe.Recipe01 = quickSortRecipe01(recipe.Recipe01, "OtherName", 0, len(recipe.Recipe01)-1) case "Description": // ByRecipe01Field(SortRecipe01ByFieldName("Description", ascending)).Sort(recipe.Recipe01) // recipe.Recipe01 = quickSortRecipe01(recipe.Recipe01, "Description", 0, len(recipe.Recipe01)-1) case "Other Description": // ByRecipe01Field(SortRecipe01ByFieldName("OtherDecsription", ascending)).Sort(recipe.Recipe01) // recipe.Recipe01 = quickSortRecipe01(recipe.Recipe01, "OtherDecsription", 0, len(recipe.Recipe01)-1) case "Last Updated": // cleaning up empty last change // for index, v := range recipe.Recipe01 { // // save value temp // fmt.Println("checking lastchange ", v.ProductCode, " ='", v.LastChange, "'") // if v.LastChange == "" || len(v.LastChange) == 0 { // fmt.Println("Shift @ ", index, " = ", v.ProductCode) // temp := recipe.Recipe01[index] // // copy(recipe.Recipe01[index:], recipe.Recipe01[index+1:]) // recipe.Recipe01 = append(recipe.Recipe01[:index], recipe.Recipe01[index+1:]...) // recipe.Recipe01 = append(recipe.Recipe01, temp) // } // } no_zero_idx := 0 for i := 0; i < len(recipe.Recipe01); i++ { if recipe.Recipe01[i].LastChange != "" { recipe.Recipe01[no_zero_idx], recipe.Recipe01[i] = recipe.Recipe01[i], recipe.Recipe01[no_zero_idx] no_zero_idx++ } } } // set to cache after sorted // case redis active if d.redisClient.HealthCheck() == nil { fmt.Println("set to redis", filename) d.redisClient.SetToKey(filename, recipe) } fmt.Println("set to server mem") // TODO: handle country and recipe not match isFound, index := d.ValidateUpdateCurrentRecipePointer(countryID, filename) if isFound && index != -1 { d.CurrentRecipe[countryID] = recipe } } // merge func (d *Data) Merge(country string, filename string, attr string, changeKey string, updated interface{}) (string, error) { d.taoLogger.Log.Debug("check on merge request", zap.Any("args", []string{ country, filename, attr, changeKey, })) // change this to switch case if attr == "Recipe" { return d.MergeRecipe(country, filename, changeKey) } else if attr == "NoCache" { if updated == nil { return "UpdatedValueError", fmt.Errorf("updated value is nil") } var updateRecipe01 models.Recipe01 updatedRecord, _ := json.Marshal(updated) json.Unmarshal(updatedRecord, &updateRecipe01) return d.MergeRecipeNoCache(country, filename, updateRecipe01) // // updatedModel, ok := updateRecipe01.(models.Recipe01) // // d.taoLogger.Log.Debug("check on update model", zap.Any("ok?", ok), zap.Any("appliedFromClient", updatedModel)) // if ok { // return d.MergeRecipeNoCache(country, filename, updatedModel) // } else { // d.taoLogger.Log.Debug("Merge", zap.Any("targetAssertionFail", updated)) // return "Fail to upgrade: NotMatchedByType", nil // } } return "NotKnownAttr", nil } func (d *Data) MergeRecipe(country, filename, changeKey string) (string, error) { // get source prefix := "Recipe" // read keys keys, err := d.redisClient.KeyList() if err != nil { return "RedisKeysError", fmt.Errorf("error when read keys from redis: %v", err) } // fullKeyString := "" isLegit := false // // find key that contains commitId, patchSource for _, key := range keys { // Recipe___ if strings.Contains(key, prefix) && key == changeKey { isLegit = true } } if !isLegit { return "NotLegitKey", fmt.Errorf("key not found") } // get actual value patchValue := models.Recipe01{} err = d.redisClient.GetKeyTo(changeKey, &patchValue) if err != nil { return "PatchValueError", fmt.Errorf("error while getting value from source: %v", err) } // get recipe // var sourceRecipe models.Recipe sourceRecipe := d.GetRecipe(country, filename) // copy(d.GetRecipe(country, filename), &sourceRecipe) // check address fmt.Println("[Source] source === recipe? ", sourceRecipe == d.GetRecipe(country, filename)) copyOfSourceRecipe := sourceRecipe // apply value to its source d.SetValuesToRecipe(copyOfSourceRecipe.Recipe01, patchValue) return d.finalizedVersion(country, copyOfSourceRecipe) } func (d *Data) MergeRecipeNoCache(country string, filename string, updatedRecipe models.Recipe01) (string, error) { // get recipe sourceRecipe := d.GetRecipe(country, filename) copyOfSourceRecipe := sourceRecipe // apply value to its source d.SetValuesToRecipe(copyOfSourceRecipe.Recipe01, updatedRecipe) return d.finalizedVersion(country, copyOfSourceRecipe) } func (d *Data) finalizedVersion(country string, sourceRecipe *models.Recipe) (string, error) { // updating version sourceRecipe.MachineSetting.ConfigNumber += 1 newVersionStr := strconv.Itoa(sourceRecipe.MachineSetting.ConfigNumber) // create new file name updatedFilename := "" prefixLocalFile := "coffeethai02_" if country != "tha" { updatedFilename = prefixLocalFile + newVersionStr + "_" + country + ".json" } else { updatedFilename = prefixLocalFile + newVersionStr + ".json" } fullUpdatedFilename := path.Join("./cofffeemachineConfig", country, updatedFilename) // create new file // handle case if file already exists, add version by 1 then search new filename in loop // list all files in dir directory := path.Join("./cofffeemachineConfig", country) files, err := os.ReadDir(directory) if err != nil { d.taoLogger.Log.Error("MergeRecipeNoCache: Error when read dir", zap.Error(err)) return "ReadDirError", fmt.Errorf("error when read dir: %v", err) } for _, file := range files { if file.Name() == updatedFilename { // add version by 1 sourceRecipe.MachineSetting.ConfigNumber += 1 newVersionStr = strconv.Itoa(sourceRecipe.MachineSetting.ConfigNumber) if country != "tha" { updatedFilename = prefixLocalFile + newVersionStr + "_" + country + ".json" } else { updatedFilename = prefixLocalFile + newVersionStr + ".json" } fullUpdatedFilename = path.Join("./cofffeemachineConfig", country, updatedFilename) } } file, err := os.Create(fullUpdatedFilename) if err != nil { d.taoLogger.Log.Error("MergeRecipeNoCache: Error when create new file", zap.Error(err)) return "CreateFileError", fmt.Errorf("error when create new file: %v", err) } // write file encoder := json.NewEncoder(file) encoder.SetIndent("", " ") err = encoder.Encode(sourceRecipe) if err != nil { d.taoLogger.Log.Error("MergeRecipeNoCache: Error when write file", zap.Error(err)) return "WriteFileError", fmt.Errorf("error when write file: %v", err) } // set cache err = d.redisClient.SetToKey(updatedFilename, sourceRecipe) if err != nil { d.taoLogger.Log.Error("MergeRecipeNoCache: Error when set cache", zap.Error(err)) return "SetCacheError", fmt.Errorf("error when set cache: %v", err) } return updatedFilename, nil } func (d *Data) ValidateUpdateCurrentRecipePointer(countryID, filename string) (bool, int) { found := false idx := -1 for index, file := range d.AllRecipeFiles[countryID] { if file.Name == filename { found = true idx = index } } d.taoLogger.Log.Debug("ValidateUpdateCurrentRecipePointer", zap.Any("args", []string{countryID, filename}), zap.Any("pass", found)) return found, idx }