Files
b2b/app/service/productDescriptionService/productDescriptionService.go
2026-03-12 17:46:59 +01:00

266 lines
9.3 KiB
Go

package productDescriptionService
import (
"context"
"encoding/xml"
"fmt"
"io"
"slices"
"strings"
"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"
"gorm.io/gorm"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/option"
"github.com/openai/openai-go/v3/responses"
)
type ProductDescriptionService struct {
db *gorm.DB
client openai.Client
}
func New() *ProductDescriptionService {
return &ProductDescriptionService{
db: db.Get(),
client: openai.NewClient(option.WithAPIKey("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")),
}
}
func isValidXHTML(s string) bool {
decoder := xml.NewDecoder(strings.NewReader(s))
hasStartTag := false
for {
tok, err := decoder.Token()
if err != nil {
if err == io.EOF {
return hasStartTag
}
return false
}
if _, ok := tok.(xml.StartElement); ok {
hasStartTag = true
}
}
}
// 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
}
// Updates relevant fields with the "updates" map
func (s *ProductDescriptionService) SaveProductDescription(userID uint, productID uint, productShopID uint, productLangID uint, updates map[string]string) error {
// only some fields can be affected
allowedFields := []string{"description", "description_short", "meta_description", "meta_title", "name", "available_now", "available_later", "usage"}
for key := range updates {
if !slices.Contains(allowedFields, key) {
return responseErrors.ErrBadField
}
}
// check that fields description, description_short and usage, if they exist, have a valid html format
mustBeHTML := []string{"description", "description_short", "usage"}
for i := 0; i < len(mustBeHTML); i++ {
if text, exists := updates[mustBeHTML[i]]; exists {
if !isValidXHTML(text) {
return responseErrors.ErrInvalidHTML
}
}
}
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
if err != nil {
return fmt.Errorf("database error: %w", 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
}
// Updates relevant fields with the "updates" map
func (s *ProductDescriptionService) TranslateProductDescription(userID uint, productID uint, productShopID uint, productFromLangID uint, productToLangID 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, productFromLangID).
First(&ProductDescription).Error
if err != nil {
return nil, fmt.Errorf("database error: %w", err)
}
// we translate all changeable fields, and we keep the exact same HTML structure in relevant fields.
lang, err := langsService.LangSrv.GetLanguageById(productToLangID)
if err != nil {
return nil, err
}
request := "Translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website.\n\n"
request += "\n"
request += "<translation_of_product_description>"
request += ProductDescription.Description
request += "</translation_of_product_description>"
request += "\n"
request += "Remember: translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website."
request += "\n"
request += "<translation_of_product_short_description>"
request += ProductDescription.DescriptionShort
request += "</translation_of_product_short_description>"
request += "\n"
request += "Remember: translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website."
request += "\n"
request += "<translation_of_product_meta_description>"
request += ProductDescription.MetaDescription
request += "</translation_of_product_meta_description>"
request += "\n"
request += "Remember: translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website."
request += "\n"
request += "<translation_of_product_meta_title>"
request += ProductDescription.MetaTitle
request += "</translation_of_product_meta_title>"
request += "\n"
request += "Remember: translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website."
request += "\n"
request += "<translation_of_product_name>"
request += ProductDescription.Name
request += "</translation_of_product_name>"
request += "\n"
request += "Remember: translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website."
request += "\n"
request += "<translation_of_display_text_available_now>"
request += ProductDescription.AvailableNow
request += "</translation_of_display_text_available_now>"
request += "\n"
request += "Remember: translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website."
request += "\n"
request += "<translation_of_display_text_available_later>"
request += ProductDescription.AvailableLater
request += "</translation_of_display_text_available_later>"
request += "\n"
request += "Remember: translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website."
request += "\n"
request += "<translation_of_product_usage>"
request += ProductDescription.Usage
request += "</translation_of_product_usage>"
openai_response, err := s.client.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" {
return nil, responseErrors.ErrOpenAIResponseFail
}
output := openai_response.OutputText()
// for debugging purposes
// fi, err := os.ReadFile("/home/daniel/coding/work/b2b/app/service/productDescriptionService/test.txt") // just pass the file name
// output := string(fi)
success, match := GetStringInBetween(output, "<translation_of_product_description>", "</translation_of_product_description>")
if !success {
return nil, responseErrors.ErrOpenAIBadOutput
}
ProductDescription.Description = match
success, match = GetStringInBetween(output, "<translation_of_product_short_description>", "</translation_of_product_short_description>")
if !success {
return nil, responseErrors.ErrOpenAIBadOutput
}
ProductDescription.DescriptionShort = match
success, match = GetStringInBetween(output, "<translation_of_product_meta_description>", "</translation_of_product_meta_description>")
if !success {
return nil, responseErrors.ErrOpenAIBadOutput
}
ProductDescription.MetaDescription = match
success, match = GetStringInBetween(output, "<translation_of_product_meta_title>", "</translation_of_product_meta_title>")
if !success {
return nil, responseErrors.ErrOpenAIBadOutput
}
ProductDescription.MetaTitle = match
success, match = GetStringInBetween(output, "<translation_of_product_name>", "</translation_of_product_name>")
if !success {
return nil, responseErrors.ErrOpenAIBadOutput
}
ProductDescription.Name = match
success, match = GetStringInBetween(output, "<translation_of_display_text_available_now>", "</translation_of_display_text_available_now>")
if !success {
return nil, responseErrors.ErrOpenAIBadOutput
}
ProductDescription.AvailableNow = match
success, match = GetStringInBetween(output, "<translation_of_display_text_available_later>", "</translation_of_display_text_available_later>")
if !success {
return nil, responseErrors.ErrOpenAIBadOutput
}
ProductDescription.AvailableLater = match
success, match = GetStringInBetween(output, "<translation_of_product_usage>", "</translation_of_product_usage>")
if !success {
return nil, responseErrors.ErrOpenAIBadOutput
}
ProductDescription.Usage = match
return &ProductDescription, nil
}
// 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, ""
}
s += len(start)
e := strings.Index(str[s:], end)
if e == -1 {
return false, ""
}
return true, str[s : s+e]
}