admin-dashboard-backend/routers/router/api/v1/productpipeline/productpipeline.go

414 lines
12 KiB
Go

package productpipeline
import (
"context"
"errors"
"fmt"
"jannex/admin-dashboard-backend/modules/cache"
"jannex/admin-dashboard-backend/modules/config"
"jannex/admin-dashboard-backend/modules/database"
"jannex/admin-dashboard-backend/modules/logger"
"jannex/admin-dashboard-backend/modules/notification"
"jannex/admin-dashboard-backend/modules/structs"
"sort"
"strconv"
"time"
"git.ex.umbach.dev/Alex/roese-utils/rslogger"
"git.ex.umbach.dev/Alex/roese-utils/rsutils"
"github.com/gofiber/fiber/v2"
"google.golang.org/api/option"
"google.golang.org/api/sheets/v4"
"gorm.io/gorm"
)
var (
serviceAccountFile = "./groupTasks/secrets/shinnex-424321-b88b144bc9ef.json"
spreadsheetId = "1gZkjykb55aLWumBxHxj_ZUq_ktjGK4FlJsH_9qg-ThU"
// names of worksheets from google sheet
googleWorksheetPipeline = "Pipeline"
googleWorksheetPipelineParameterValues = "Pipeline Parameterwerte"
)
func GetProducts(c *fiber.Ctx) error {
// fetch from google sheets if cache time expired
if cache.IsPipelineProductsCacheTimeExpired() {
if err := FetchGoogleSheets(); err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
}
return c.JSON(cache.GetPipelineProducts())
}
func getGoogleSheetsService() (*sheets.Service, error) {
ctx := context.Background()
srv, err := sheets.NewService(ctx, option.WithCredentialsFile(serviceAccountFile))
if err != nil {
logger.AddSystemLog(rslogger.LogTypeError, "Unable to retrieve Sheets client: %v", err)
return nil, err
}
return srv, nil
}
func getStateByStatus(status string) uint8 {
for _, productState := range cache.GetProductsStates() {
if productState.Status == status {
return productState.State
}
}
return 0
}
func FetchGoogleSheets() error {
srv, err := getGoogleSheetsService()
if err != nil {
logger.AddSystemLog(rslogger.LogTypeError, "Failed to fetch product states")
return err
}
if err = fetchProductStates(srv); err != nil {
logger.AddSystemLog(rslogger.LogTypeError, "FetchGoogleSheets failed on fetching product states, err: "+err.Error())
return err
}
if err = fetchProducts(srv); err != nil {
logger.AddSystemLog(rslogger.LogTypeError, "FetchGoogleSheets failed on fetching products, err: "+err.Error())
return err
}
cache.SortFutureProducts()
cache.UpdateProductsPipelineLastCacheUpdate()
return nil
}
func fetchProducts(srv *sheets.Service) error {
logger.AddSystemLog(rslogger.LogTypeInfo, "Fetching products from google sheets")
readRange := googleWorksheetPipeline
resp, err := srv.Spreadsheets.Values.Get(spreadsheetId, readRange).Do()
if err != nil {
logger.AddSystemLog(rslogger.LogTypeError, "Unable to retrieve data from sheet: %v", err)
return err
}
// no products found
if len(resp.Values) == 0 {
logger.AddSystemLog(rslogger.LogTypeInfo, "No products found")
return nil
}
// get products from database
var databasePipelineProducts []structs.PipelineProduct
database.DB.Find(&databasePipelineProducts)
// check if there is a new product on google sheets which not exists in database
var newProducts []structs.NewProduct
var inWorkProducts []structs.InWorkProduct
var futureProducts []structs.FutureProduct
var (
rowIndexProductId int = -1
rowIndexStatus int = -1
rowIndexName int = -1
rowIndexProductVariant int = -1
rowIndexProductCharacteristics int = -1
rowIndexShopProductLink int = -1
rowIndexPublishedAt int = -1
)
for i, row := range resp.Values[1] {
switch row {
case "#id":
rowIndexProductId = i
case "Status":
rowIndexStatus = i
case "Name":
rowIndexName = i
case "Produktvariante":
rowIndexProductVariant = i
case "Produktfarbe/Characteristics":
rowIndexProductCharacteristics = i
case "Shop Produkt Link":
rowIndexShopProductLink = i
case "Veröffentlicht am":
rowIndexPublishedAt = i
}
}
if rowIndexProductId == -1 || rowIndexStatus == -1 || rowIndexName == -1 || rowIndexProductVariant == -1 || rowIndexProductCharacteristics == -1 || rowIndexShopProductLink == -1 || rowIndexPublishedAt == -1 {
notification.AddNotification(nil, structs.AddNotificationRequest{
UserIds: config.Cfg.NotificationUserIds,
Type: 3,
Title: "Admin-Dashboard: Failed to get row index. Please check if the row names are equal to the names of the google sheet table header of " + spreadsheetId,
})
return errors.New("Failed to get row index. Please check if the row names are equal to the names of the google sheet table header of " + spreadsheetId)
}
for _, row := range resp.Values[2:] { // skip first two google worksheet row
// maybe some error in google sheets table
if len(row) < 3 {
logger.AddSystemLog(rslogger.LogTypeError, "Skipped row, because row length less than 2")
continue
}
// skip to next if id, status or name is empty
if row[rowIndexProductId] == "" || row[rowIndexStatus] == "" || row[rowIndexName] == "" {
continue
}
productId := fmt.Sprintf("%v", row[rowIndexProductId])
status := fmt.Sprintf("%v", row[rowIndexStatus])
name := fmt.Sprintf("%v", row[rowIndexName])
var productVariant string
if len(row) > rowIndexProductVariant && row[rowIndexProductVariant] != "" {
productVariant = fmt.Sprintf("%v", row[rowIndexProductVariant])
}
var productCharacteristics string
if len(row) > rowIndexProductCharacteristics && row[rowIndexProductCharacteristics] != "" {
productCharacteristics = fmt.Sprintf("%v", row[rowIndexProductCharacteristics])
}
state := getStateByStatus(status)
// add product to database if not exist
if !isPipelineProductInDatabase(databasePipelineProducts, productId) {
database.DB.Create(&structs.PipelineProduct{
Id: productId,
Votes: 0,
})
}
// cache product
if state == 1 {
futureProducts = append(futureProducts, structs.FutureProduct{
Id: productId,
Name: name,
Votes: getVotes(databasePipelineProducts, productId), // votes from database
Variant: productVariant,
Characteristics: productCharacteristics,
})
} else if state == 2 {
inWorkProducts = append(inWorkProducts, structs.InWorkProduct{
Id: productId,
Name: name,
Variant: productVariant,
Characteristics: productCharacteristics,
})
} else if state == 3 {
var url string
var publishedAt string
if len(row) > rowIndexShopProductLink && row[rowIndexShopProductLink] != "" {
url = fmt.Sprintf("%v", row[rowIndexShopProductLink])
} else {
logger.AddSystemLog(rslogger.LogTypeWarning, "Url missing for product id: %s name: %s", productId, name)
notification.AddNotification(nil, structs.AddNotificationRequest{
UserIds: config.Cfg.NotificationUserIds,
Type: 3,
Title: "Missing 'Shop Produkt Link' for product pipeline\n\nproduct id: " + productId + "\nname: " + name + "\n\nGoogle Sheet 'Produkte' -> Worksheet: 'Pipeline' eintragen!",
})
}
if len(row) > rowIndexPublishedAt && row[rowIndexPublishedAt] != "" {
publishedAt = fmt.Sprintf("%v", row[rowIndexPublishedAt])
} else {
logger.AddSystemLog(rslogger.LogTypeWarning, "'Veröffentlicht am' date missing for product id: %s name: %s", productId, name)
notification.AddNotification(nil, structs.AddNotificationRequest{
UserIds: config.Cfg.NotificationUserIds,
Type: 3,
Title: "Missing 'Veröffentlicht am' for product pipeline\n\nproduct id: " + productId + "\nname: " + name + "\n\nGoogle Sheet 'Produkte' -> Worksheet: 'Pipeline' eintragen!",
})
}
newProducts = append(newProducts, structs.NewProduct{
Id: productId,
Name: name,
Url: url,
Variant: productVariant,
Characteristics: productCharacteristics,
PublishedAt: publishedAt,
})
}
}
// sort new products
// compare function
comparePublishedAt := func(a, b structs.NewProduct) int {
if a.PublishedAt == "" && b.PublishedAt == "" {
return 0
}
if a.PublishedAt == "" {
return 1
}
if b.PublishedAt == "" {
return -1
}
dateFormat := "02.01.2006"
dateA, errA := time.Parse(dateFormat, a.PublishedAt)
dateB, errB := time.Parse(dateFormat, b.PublishedAt)
if errA != nil || errB != nil {
return 0 // Handle parsing errors if necessary
}
if dateA.After(dateB) {
return -1
}
if dateA.Before(dateB) {
return 1
}
return 0
}
// sort slice
sort.Slice(newProducts, func(i, j int) bool {
return comparePublishedAt(newProducts[i], newProducts[j]) < 0
})
// limit to max 10 new products
if len(newProducts) > 10 {
newProducts = newProducts[:10]
}
cache.SetPipelineProductsCache(structs.PipelineProductsCache{
NewProducts: newProducts,
InWorkProducts: inWorkProducts,
FutureProducts: futureProducts,
})
return nil
}
func getVotes(pipelineProducts []structs.PipelineProduct, id string) int {
for _, pipelineProduct := range pipelineProducts {
if pipelineProduct.Id == id {
return pipelineProduct.Votes
}
}
return 0
}
func fetchProductStates(srv *sheets.Service) error {
logger.AddSystemLog(rslogger.LogTypeInfo, "Fetching product states from google sheets")
readRange := googleWorksheetPipelineParameterValues
resp, err := srv.Spreadsheets.Values.Get(spreadsheetId, readRange).Do()
if err != nil {
logger.AddSystemLog(rslogger.LogTypeError, "Unable to retrieve data from sheet: %v", err)
return err
}
if len(resp.Values) == 0 {
logger.AddSystemLog(rslogger.LogTypeInfo, "No product states found")
return nil
}
var productStates []structs.ProductState
for index, row := range resp.Values {
// skip first google sheets row
if index == 0 {
continue
}
status := fmt.Sprintf("%v", row[0])
state, err := strconv.ParseUint(fmt.Sprintf("%v", row[1]), 10, 64)
if err != nil {
return errors.New("Failed to parse uint8, err: " + err.Error())
}
productStates = append(productStates, structs.ProductState{
Status: status,
State: uint8(state),
})
}
cache.SetProductStates(productStates)
return nil
}
func isPipelineProductInDatabase(pipelineProducts []structs.PipelineProduct, id string) bool {
for _, pipelineProduct := range pipelineProducts {
if pipelineProduct.Id == id {
return true
}
}
return false
}
func VoteProduct(c *fiber.Ctx) error {
var query structs.VoteProductQuery
if err := rsutils.QueryParserHelper(c, &query); err != nil {
logger.AddSystemLog(rslogger.LogTypeError, "VoteProduct invalid query, err: %s", err.Error())
return c.SendStatus(fiber.StatusBadRequest)
}
if query.T != "u" && query.T != "d" {
return c.SendStatus(fiber.StatusBadRequest)
}
var body structs.VoteProductRequest
if err := rsutils.BodyParserHelper(c, &body); err != nil {
logger.AddSystemLog(rslogger.LogTypeError, "VoteProduct invalid body, err: %s", err.Error())
return c.SendStatus(fiber.StatusBadRequest)
}
if !cache.IsIdInFutureProducts(body.Id) {
logger.AddSystemLog(rslogger.LogTypeWarning, "VoteProduct invalid product id provided, id: %s", body.Id)
return c.SendStatus(fiber.StatusBadRequest)
}
operator := "+"
if query.T == "d" {
operator = "-"
}
database.DB.Model(&structs.PipelineProduct{}).Where("id = ?", body.Id).Update("votes", gorm.Expr(fmt.Sprintf("votes %s ?", operator), 1))
cache.VoteFutureProduct(body.Id, query.T == "u")
cache.SortFutureProducts()
logger.AddSystemLog(rslogger.LogTypeInfo, "Vote name: %v up vote: %v", body.Id, query.T)
return c.SendStatus(fiber.StatusOK)
}
// can be triggered by admin dashboard group task
func GetManualFetchProducts(c *fiber.Ctx) error {
logger.AddSystemLog(rslogger.LogTypeInfo, "Request for manual retrieval of Google Sheet products received")
if err := FetchGoogleSheets(); err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
return c.SendStatus(fiber.StatusOK)
}