From a0dcb56fda8d3e054b53863eb2e6a731d78de78d Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Tue, 17 Mar 2026 10:55:17 +0100 Subject: [PATCH] code refactor --- .../web/api/restricted/productDescription.go | 105 ++++++------- app/delivery/web/api/settings.go | 2 +- app/service/langsService/service.go | 17 +- .../productDescriptionService.go | 146 ++++++------------ app/utils/response/response.go | 11 +- .../{response_errors.go => responseErrors.go} | 6 +- .../20260302163122_create_tables.sql | 3 +- .../productDescriptionRepo.go | 74 +++++++++ 8 files changed, 187 insertions(+), 177 deletions(-) rename app/utils/responseErrors/{response_errors.go => responseErrors.go} (97%) create mode 100644 repository/productDescriptionRepo/productDescriptionRepo.go diff --git a/app/delivery/web/api/restricted/productDescription.go b/app/delivery/web/api/restricted/productDescription.go index 8d2f7c7..9488e21 100644 --- a/app/delivery/web/api/restricted/productDescription.go +++ b/app/delivery/web/api/restricted/productDescription.go @@ -6,6 +6,8 @@ import ( "git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/service/productDescriptionService" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" + "git.ma-al.com/goc_daniel/b2b/app/utils/nullable" + "git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "github.com/gofiber/fiber/v3" @@ -41,151 +43,132 @@ func ProductDescriptionHandlerRoutes(r fiber.Router) fiber.Router { func (h *ProductDescriptionHandler) GetProductDescription(c fiber.Ctx) error { userID, ok := c.Locals("userID").(uint) if !ok { - return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).JSON(fiber.Map{ - "error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody), // possibly could return a different error - }) + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) } productID_attribute := c.Query("productID") productID, err := strconv.Atoi(productID_attribute) if err != nil { - return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).JSON(fiber.Map{ - "error": responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute), - }) + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } productShopID_attribute := c.Query("productShopID") productShopID, err := strconv.Atoi(productShopID_attribute) if err != nil { - return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).JSON(fiber.Map{ - "error": responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute), - }) + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } productLangID_attribute := c.Query("productLangID") productLangID, err := strconv.Atoi(productLangID_attribute) if err != nil { - return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).JSON(fiber.Map{ - "error": responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute), - }) + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } - response, err := h.productDescriptionService.GetProductDescription(userID, uint(productID), uint(productShopID), uint(productLangID)) + description, err := h.productDescriptionService.GetProductDescription(userID, uint(productID), uint(productShopID), uint(productLangID)) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{ "error": responseErrors.GetErrorCode(c, err), }) } - return c.JSON(response) + return c.JSON(response.Make(description, 1, i18n.T_(c, response.Message_OK))) } // SaveProductDescription saves the description for a given product ID, in given shop and language func (h *ProductDescriptionHandler) SaveProductDescription(c fiber.Ctx) error { userID, ok := c.Locals("userID").(uint) if !ok { - return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).JSON(fiber.Map{ - "error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody), // possibly could return a different error - }) + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) } productID_attribute := c.Query("productID") productID, err := strconv.Atoi(productID_attribute) if err != nil { - return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).JSON(fiber.Map{ - "error": responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute), - }) + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } productShopID_attribute := c.Query("productShopID") productShopID, err := strconv.Atoi(productShopID_attribute) if err != nil { - return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).JSON(fiber.Map{ - "error": responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute), - }) + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } productLangID_attribute := c.Query("productLangID") productLangID, err := strconv.Atoi(productLangID_attribute) if err != nil { - return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).JSON(fiber.Map{ - "error": responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute), - }) + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } updates := make(map[string]string) if err := c.Bind().Body(&updates); err != nil { - return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).JSON(fiber.Map{ - "error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody), - }) + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) } err = h.productDescriptionService.SaveProductDescription(userID, uint(productID), uint(productShopID), uint(productLangID), updates) if err != nil { - return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{ - "error": responseErrors.GetErrorCode(c, err), - }) + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) } - return c.JSON(fiber.Map{ - "message": i18n.T_(c, "product_description.successfully_updated_fields"), - }) + return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) } -// GetProductDescription returns the product description for a given product ID +// TranslateProductDescription returns translated product description func (h *ProductDescriptionHandler) TranslateProductDescription(c fiber.Ctx) error { userID, ok := c.Locals("userID").(uint) if !ok { - return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).JSON(fiber.Map{ - "error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody), // possibly could return a different error - }) + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) } productID_attribute := c.Query("productID") productID, err := strconv.Atoi(productID_attribute) if err != nil { - return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).JSON(fiber.Map{ - "error": responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute), - }) + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } productShopID_attribute := c.Query("productShopID") productShopID, err := strconv.Atoi(productShopID_attribute) if err != nil { - return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).JSON(fiber.Map{ - "error": responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute), - }) + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } productFromLangID_attribute := c.Query("productFromLangID") productFromLangID, err := strconv.Atoi(productFromLangID_attribute) if err != nil { - return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).JSON(fiber.Map{ - "error": responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute), - }) + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } productToLangID_attribute := c.Query("productToLangID") productToLangID, err := strconv.Atoi(productToLangID_attribute) if err != nil { - return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).JSON(fiber.Map{ - "error": responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute), - }) + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } - model := c.Query("model") - if model != "OpenAI" && model != "Google" { - return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).JSON(fiber.Map{ - "error": responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute), - }) + aiModel := c.Query("model") + if aiModel != "OpenAI" && aiModel != "Google" { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } - response, err := h.productDescriptionService.TranslateProductDescription(userID, uint(productID), uint(productShopID), uint(productFromLangID), uint(productToLangID), model) + description, err := h.productDescriptionService.TranslateProductDescription(userID, uint(productID), uint(productShopID), uint(productFromLangID), uint(productToLangID), aiModel) if err != nil { - return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{ - "error": responseErrors.GetErrorCode(c, err), - }) + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) } - return c.JSON(response) + return c.JSON(response.Make(description, 1, i18n.T_(c, response.Message_OK))) } diff --git a/app/delivery/web/api/settings.go b/app/delivery/web/api/settings.go index ef29c08..eaa4acc 100644 --- a/app/delivery/web/api/settings.go +++ b/app/delivery/web/api/settings.go @@ -86,6 +86,6 @@ func (h *SettingsHandler) GetSettings(cfg *config.Config) fiber.Handler { Version: version.GetInfo(), } - return c.JSON(response.Make(c, fiber.StatusOK, nullable.GetNil(settings), nullable.GetNil(0), i18n.T_(c, response.Message_OK))) + return c.JSON(response.Make(nullable.GetNil(settings), 0, i18n.T_(c, response.Message_OK))) } } diff --git a/app/service/langsService/service.go b/app/service/langsService/service.go index 5d8ca88..14efec7 100644 --- a/app/service/langsService/service.go +++ b/app/service/langsService/service.go @@ -27,9 +27,10 @@ var LangSrv *LangService func (s *LangService) GetActive(c fiber.Ctx) response.Response[[]view.Language] { res, err := s.repo.GetActive() if err != nil { - return response.Make[[]view.Language](c, fiber.StatusBadRequest, nil, nil, i18n.T_(c, response.Message_NOK)) + c.Status(fiber.StatusBadRequest) + return response.Make[[]view.Language](nil, 0, i18n.T_(c, response.Message_NOK)) } - return response.Make(c, fiber.StatusOK, nullable.GetNil(res), nullable.GetNil(len(res)), i18n.T_(c, response.Message_OK)) + return response.Make(nullable.GetNil(res), 0, i18n.T_(c, response.Message_OK)) } // LoadTranslations loads all translations from the database into the cache @@ -54,25 +55,27 @@ func (s *LangService) ReloadTranslations() error { func (s *LangService) GetTranslations(c fiber.Ctx, langID uint, scope string, components []string) response.Response[*i18n.TranslationResponse] { translations, err := i18n.TransStore.GetTranslations(langID, scope, components) if err != nil { - return response.Make[*i18n.TranslationResponse](c, fiber.StatusBadRequest, nil, nil, i18n.T_(c, Message_TranslationsNOK)) + c.Status(fiber.StatusBadRequest) + return response.Make[*i18n.TranslationResponse](nil, 0, i18n.T_(c, Message_TranslationsNOK)) } - return response.Make(c, fiber.StatusOK, nullable.GetNil(translations), nil, i18n.T_(c, Message_TranslationsOK)) + return response.Make(nullable.GetNil(translations), 0, i18n.T_(c, Message_TranslationsOK)) } // GetAllTranslations returns all translations from the cache func (s *LangService) GetAllTranslationsResponse(c fiber.Ctx) response.Response[*i18n.TranslationResponse] { translations := i18n.TransStore.GetAllTranslations() - return response.Make(c, fiber.StatusOK, nullable.GetNil(translations), nil, i18n.T_(c, Message_TranslationsOK)) + return response.Make(nullable.GetNil(translations), 0, i18n.T_(c, Message_TranslationsOK)) } // ReloadTranslationsResponse returns response after reloading translations func (s *LangService) ReloadTranslationsResponse(c fiber.Ctx) response.Response[map[string]string] { err := s.ReloadTranslations() if err != nil { - return response.Make[map[string]string](c, fiber.StatusInternalServerError, nil, nil, i18n.T_(c, Message_LangsNotLoaded)) + c.Status(fiber.StatusInternalServerError) + return response.Make[map[string]string](nil, 0, i18n.T_(c, Message_LangsNotLoaded)) } result := map[string]string{"status": "success"} - return response.Make(c, fiber.StatusOK, nullable.GetNil(result), nil, i18n.T_(c, Message_LangsLoaded)) + return response.Make(nullable.GetNil(result), 0, i18n.T_(c, Message_LangsLoaded)) } // GetDefaultLanguage returns the default language diff --git a/app/service/productDescriptionService/productDescriptionService.go b/app/service/productDescriptionService/productDescriptionService.go index 810bd46..a8c31d6 100644 --- a/app/service/productDescriptionService/productDescriptionService.go +++ b/app/service/productDescriptionService/productDescriptionService.go @@ -12,32 +12,26 @@ import ( "strings" "time" + "cloud.google.com/go/auth/credentials" + translate "cloud.google.com/go/translate/apiv3" + "cloud.google.com/go/translate/apiv3/translatepb" "git.ma-al.com/goc_daniel/b2b/app/config" - "git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/service/langsService" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" + ProductDescriptionRepo "git.ma-al.com/goc_daniel/b2b/repository/productDescriptionRepo" "github.com/openai/openai-go/v3" "github.com/openai/openai-go/v3/option" "github.com/openai/openai-go/v3/responses" googleopt "google.golang.org/api/option" - "gorm.io/gorm" - - // [START translate_v3_import_client_library] - "cloud.google.com/go/auth/credentials" - translate "cloud.google.com/go/translate/apiv3" - "cloud.google.com/go/translate/apiv3/translatepb" - // [END translate_v3_import_client_library] ) type ProductDescriptionService struct { - db *gorm.DB - ctx context.Context - googleCli translate.TranslationClient - client openai.Client - // projectID is the Google Cloud project ID used as the "parent" in API calls, - // e.g. "projects/my-project-123/locations/global" - projectID string + productDescriptionRepo ProductDescriptionRepo.ProductDescriptionRepo + ctx context.Context + googleCli translate.TranslationClient + projectID string + openAIClient openai.Client } // New creates a ProductDescriptionService and authenticates against the @@ -76,31 +70,19 @@ func New() *ProductDescriptionService { log.Fatalf("productDescriptionService: cannot create Translation client: %v", err) } - client := openai.NewClient(option.WithAPIKey("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"), + openAIClient := openai.NewClient(option.WithAPIKey("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"), option.WithHTTPClient(&http.Client{Timeout: 300 * time.Second})) // five minutes timeout return &ProductDescriptionService{ - db: db.Get(), - ctx: ctx, - client: client, - googleCli: *googleCli, - projectID: cfg.GoogleTranslate.ProjectID, + ctx: ctx, + openAIClient: openAIClient, + googleCli: *googleCli, + projectID: cfg.GoogleTranslate.ProjectID, } } -// We assume that any user has access to all product descriptions func (s *ProductDescriptionService) GetProductDescription(userID uint, productID uint, productShopID uint, productLangID uint) (*model.ProductDescription, error) { - var ProductDescription model.ProductDescription - - err := s.db. - Table("ps_product_lang"). - Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID). - First(&ProductDescription).Error - if err != nil { - return nil, fmt.Errorf("database error: %w", err) - } - - return &ProductDescription, nil + return s.productDescriptionRepo.GetProductDescription(productID, productShopID, productLangID) } // Updates relevant fields with the "updates" map @@ -123,37 +105,12 @@ func (s *ProductDescriptionService) SaveProductDescription(userID uint, productI } } - record := model.ProductDescription{ - ProductID: productID, - ShopID: productShopID, - LangID: productLangID, - } - - err := s.db. - Table("ps_product_lang"). - Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID). - FirstOrCreate(&record).Error + err := s.productDescriptionRepo.CreateIfDoesNotExist(productID, productShopID, productLangID) if err != nil { - return fmt.Errorf("database error: %w", err) + return err } - if len(updates) == 0 { - return nil - } - updatesIface := make(map[string]interface{}, len(updates)) - for k, v := range updates { - updatesIface[k] = v - } - - err = s.db. - Table("ps_product_lang"). - Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID). - Updates(updatesIface).Error - if err != nil { - return fmt.Errorf("database error: %w", err) - } - - return nil + return s.productDescriptionRepo.UpdateFields(productID, productShopID, productLangID, updates) } // TranslateProductDescription fetches the product description for productFromLangID, @@ -163,16 +120,12 @@ func (s *ProductDescriptionService) SaveProductDescription(userID uint, productI // The Google Cloud project must have the Cloud Translation API enabled and the // service account must hold the "Cloud Translation API User" role. func (s *ProductDescriptionService) TranslateProductDescription(userID uint, productID uint, productShopID uint, productFromLangID uint, productToLangID uint, aiModel string) (*model.ProductDescription, error) { - var ProductDescription model.ProductDescription - err := s.db. - Table("ps_product_lang"). - Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productFromLangID). - First(&ProductDescription).Error + productDescription, err := s.productDescriptionRepo.GetProductDescription(productID, productShopID, productFromLangID) if err != nil { - return nil, fmt.Errorf("database error: %w", err) + return nil, err } - ProductDescription.LangID = productToLangID + productDescription.LangID = productToLangID // we translate all changeable fields, and we keep the exact same HTML structure in relevant fields. lang, err := langsService.LangSrv.GetLanguageById(productToLangID) @@ -180,14 +133,14 @@ func (s *ProductDescriptionService) TranslateProductDescription(userID uint, pro return nil, err } - fields := []*string{&ProductDescription.Description, - &ProductDescription.DescriptionShort, - &ProductDescription.MetaDescription, - &ProductDescription.MetaTitle, - &ProductDescription.Name, - &ProductDescription.AvailableNow, - &ProductDescription.AvailableLater, - &ProductDescription.Usage, + fields := []*string{&productDescription.Description, + &productDescription.DescriptionShort, + &productDescription.MetaDescription, + &productDescription.MetaTitle, + &productDescription.Name, + &productDescription.AvailableNow, + &productDescription.AvailableLater, + &productDescription.Usage, } keys := []string{"translation_of_product_description", "translation_of_product_short_description", @@ -213,24 +166,23 @@ func (s *ProductDescriptionService) TranslateProductDescription(userID uint, pro } if aiModel == "OpenAI" { - openai_response, _ := s.client.Responses.New(context.Background(), responses.ResponseNewParams{ + response, _ := s.openAIClient.Responses.New(context.Background(), responses.ResponseNewParams{ Input: responses.ResponseNewParamsInputUnion{OfString: openai.String(request)}, Model: openai.ChatModelGPT4_1Mini, // Model: openai.ChatModelGPT4_1Nano, }) - if openai_response.Status != "completed" { + if response.Status != "completed" { return nil, responseErrors.ErrAIResponseFail } - response := openai_response.OutputText() for i := 0; i < len(keys); i++ { - success, resolution := resolveResponse(*fields[i], response, keys[i]) + success, resolution := resolveResponse(*fields[i], response.OutputText(), keys[i]) if !success { return nil, responseErrors.ErrAIBadOutput } *fields[i] = resolution - fmt.Println(resolution) + // fmt.Println(resolution) } } else if aiModel == "Google" { @@ -253,17 +205,17 @@ func (s *ProductDescriptionService) TranslateProductDescription(userID uint, pro response := responseGoogle.GetTranslations()[0].GetTranslatedText() for i := 0; i < len(keys); i++ { - success, match := GetStringInBetween(response, "<"+keys[i]+">", "") + success, match := getStringInBetween(response, "<"+keys[i]+">", "") if !success || !isValidXHTML(match) { return nil, responseErrors.ErrAIBadOutput } *fields[i] = match - fmt.Println(match) + // fmt.Println(match) } } - return &ProductDescription, nil + return productDescription, nil } func cleanForPrompt(s string) string { @@ -284,17 +236,17 @@ func cleanForPrompt(s string) string { switch v := token.(type) { case xml.StartElement: - prompt += "<" + AttrName(v.Name) + prompt += "<" + attrName(v.Name) for _, attr := range v.Attr { if v.Name.Local == "img" && attr.Name.Local == "alt" { - prompt += fmt.Sprintf(` %s="%s"`, AttrName(attr.Name), attr.Value) + prompt += fmt.Sprintf(` %s="%s"`, attrName(attr.Name), attr.Value) } } prompt += ">" case xml.EndElement: - prompt += "" + prompt += "" case xml.CharData: prompt += string(v) case xml.Comment: @@ -307,12 +259,12 @@ func cleanForPrompt(s string) string { } func resolveResponse(original string, response string, key string) (bool, string) { - success, match := GetStringInBetween(response, "<"+key+">", "") + success, match := getStringInBetween(response, "<"+key+">", "") if !success || !isValidXHTML(match) { return false, "" } - success, resolution := RebuildFromResponse("<"+key+">"+original+"", "<"+key+">"+match+"") + success, resolution := rebuildFromResponse("<"+key+">"+original+"", "<"+key+">"+match+"") if !success { return false, "" } @@ -320,8 +272,8 @@ func resolveResponse(original string, response string, key string) (bool, string return true, resolution[2+len(key) : len(resolution)-3-len(key)] } -// GetStringInBetween returns empty string if no start or end string found -func GetStringInBetween(str string, start string, end string) (success bool, result string) { +// getStringInBetween returns empty string if no start or end string found +func getStringInBetween(str string, start string, end string) (success bool, result string) { s := strings.Index(str, start) if s == -1 { return false, "" @@ -358,7 +310,7 @@ func isValidXHTML(s string) bool { // Rebuilds HTML using the original HTML as a template and the response as a source // Assumes that both original and response have the exact same XML structure -func RebuildFromResponse(s_original string, s_response string) (bool, string) { +func rebuildFromResponse(s_original string, s_response string) (bool, string) { r_original := strings.NewReader(s_original) d_original := xml.NewDecoder(r_original) @@ -397,17 +349,17 @@ func RebuildFromResponse(s_original string, s_response string) (bool, string) { return false, "" } - result += "<" + AttrName(v_original.Name) + result += "<" + attrName(v_original.Name) for _, attr := range v_original.Attr { if v_original.Name.Local != "img" || attr.Name.Local != "alt" { - result += fmt.Sprintf(` %s="%s"`, AttrName(attr.Name), attr.Value) + result += fmt.Sprintf(` %s="%s"`, attrName(attr.Name), attr.Value) } } for _, attr := range v_response.Attr { if v_response.Name.Local == "img" && attr.Name.Local == "alt" { - result += fmt.Sprintf(` %s="%s"`, AttrName(attr.Name), attr.Value) + result += fmt.Sprintf(` %s="%s"`, attrName(attr.Name), attr.Value) } } result += ">" @@ -429,7 +381,7 @@ func RebuildFromResponse(s_original string, s_response string) (bool, string) { } if v_original.Name.Local != "img" { - result += "" + result += "" } case xml.CharData: @@ -485,7 +437,7 @@ func RebuildFromResponse(s_original string, s_response string) (bool, string) { } } -func AttrName(name xml.Name) string { +func attrName(name xml.Name) string { if name.Space == "" { return name.Local } else { diff --git a/app/utils/response/response.go b/app/utils/response/response.go index f8cf4dd..d1fc6bc 100644 --- a/app/utils/response/response.go +++ b/app/utils/response/response.go @@ -1,15 +1,12 @@ package response -import "github.com/gofiber/fiber/v3" - type Response[T any] struct { - Message string `json:"message,omitempty"` - Items *T `json:"items,omitempty"` - Count *int `json:"count,omitempty"` + Message string `json:"message"` + Items *T `json:"items"` + Count int `json:"count"` } -func Make[T any](c fiber.Ctx, status int, items *T, count *int, message string) Response[T] { - c.Status(status) +func Make[T any](items *T, count int, message string) Response[T] { return Response[T]{ Message: message, Items: items, diff --git a/app/utils/responseErrors/response_errors.go b/app/utils/responseErrors/responseErrors.go similarity index 97% rename from app/utils/responseErrors/response_errors.go rename to app/utils/responseErrors/responseErrors.go index 797cdc3..1b31433 100644 --- a/app/utils/responseErrors/response_errors.go +++ b/app/utils/responseErrors/responseErrors.go @@ -38,7 +38,7 @@ var ( ErrVerificationTokenExpired = errors.New("verification token has expired") // Typed errors for product description handler - ErrBadAttribute = errors.New("bad attribute") + ErrBadAttribute = errors.New("bad or missing attribute value in header") ErrBadField = errors.New("this field can not be updated") ErrInvalidXHTML = errors.New("text is not in xhtml format") ErrAIResponseFail = errors.New("AI responded with failure") @@ -119,9 +119,9 @@ func GetErrorCode(c fiber.Ctx, err error) string { case errors.Is(err, ErrInvalidXHTML): return i18n.T_(c, "error.err_invalid_html") case errors.Is(err, ErrAIResponseFail): - return i18n.T_(c, "error.err_openai_response_fail") + return i18n.T_(c, "error.err_ai_response_fail") case errors.Is(err, ErrAIBadOutput): - return i18n.T_(c, "error.err_openai_bad_output") + return i18n.T_(c, "error.err_ai_bad_output") default: return i18n.T_(c, "error.err_internal_server_error") diff --git a/i18n/migrations/20260302163122_create_tables.sql b/i18n/migrations/20260302163122_create_tables.sql index 2452f48..e335d0f 100644 --- a/i18n/migrations/20260302163122_create_tables.sql +++ b/i18n/migrations/20260302163122_create_tables.sql @@ -24,7 +24,8 @@ INSERT IGNORE INTO b2b_language VALUES (1, '2022-09-16 17:10:02.837', '2026-03-02 21:24:36.779730', NULL, 'Polski', 'pl', 'pl', '__-__-____', '__-__', 0, 0, 1, '🇵🇱'), (2, '2022-09-16 17:10:02.852', '2026-03-02 21:24:36.779730', NULL, 'English', 'en', 'en', '__-__-____', '__-__', 0, 1, 1, '🇬🇧'), - (3, '2022-09-16 17:10:02.865', '2026-03-02 21:24:36.779730', NULL, 'Čeština', 'cs', 'cs', '__-__-____', '__-__', 0, 0, 1, '🇨🇿'); + (3, '2022-09-16 17:10:02.865', '2026-03-02 21:24:36.779730', NULL, 'Čeština', 'cs', 'cs', '__-__-____', '__-__', 0, 0, 1, '🇨🇿'), + (4, '2022-09-16 17:10:02.852', '2026-03-02 21:24:36.779730', NULL, 'Deutsch', 'de', 'de', '__-__-____', '__-__', 0, 0, 1, '🇩🇪'); CREATE TABLE IF NOT EXISTS b2b_components ( id INT AUTO_INCREMENT PRIMARY KEY, diff --git a/repository/productDescriptionRepo/productDescriptionRepo.go b/repository/productDescriptionRepo/productDescriptionRepo.go new file mode 100644 index 0000000..9a08047 --- /dev/null +++ b/repository/productDescriptionRepo/productDescriptionRepo.go @@ -0,0 +1,74 @@ +package ProductDescriptionRepo + +import ( + "fmt" + + "git.ma-al.com/goc_daniel/b2b/app/db" + "git.ma-al.com/goc_daniel/b2b/app/model" +) + +type UIProductDescriptionRepo interface { + GetProductDescription(productID uint, productShopID uint, productLangID uint) (*model.ProductDescription, error) + CreateIfDoesNotExist(productID uint, productShopID uint, productLangID uint) error + UpdateFields(productID uint, productShopID uint, productLangID uint, updates map[string]string) error +} + +type ProductDescriptionRepo struct{} + +func New() UIProductDescriptionRepo { + return &ProductDescriptionRepo{} +} + +// We assume that any user has access to all product descriptions +func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productShopID uint, productLangID uint) (*model.ProductDescription, error) { + var ProductDescription model.ProductDescription + + err := db.DB. + Table("ps_product_lang"). + Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID). + First(&ProductDescription).Error + if err != nil { + return nil, fmt.Errorf("database error: %w", err) + } + + return &ProductDescription, nil +} + +// If it doesn't exist, returns an error. +func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productShopID uint, productLangID uint) error { + record := model.ProductDescription{ + ProductID: productID, + ShopID: productShopID, + LangID: productLangID, + } + + err := db.DB. + Table("ps_product_lang"). + Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID). + FirstOrCreate(&record).Error + if err != nil { + return fmt.Errorf("database error: %w", err) + } + + return nil +} + +func (r *ProductDescriptionRepo) UpdateFields(productID uint, productShopID uint, productLangID uint, updates map[string]string) error { + if len(updates) == 0 { + return nil + } + updatesIface := make(map[string]interface{}, len(updates)) + for k, v := range updates { + updatesIface[k] = v + } + + err := db.DB. + Table("ps_product_lang"). + Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID). + Updates(updatesIface).Error + if err != nil { + return fmt.Errorf("database error: %w", err) + } + + return nil +}