commit 0cb03a74a9a6d1352873fc2bbcfeb741488c508a Author: merdan Date: Wed May 3 13:07:34 2023 +0500 init diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ccbb022 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +DB_DSN="usr:pwd@tcp(127.0.0.1:3306)/test_baza?parseTime=true" +UPDATE_PERIOD=30m \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..226ca36 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +vendor diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..0cb8739 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/sarga_updater.iml b/.idea/sarga_updater.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/sarga_updater.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/bagisto_models/brand.go b/bagisto_models/brand.go new file mode 100644 index 0000000..ff5886f --- /dev/null +++ b/bagisto_models/brand.go @@ -0,0 +1,48 @@ +package bagisto_models + +import ( + "log" + "time" + + "github.com/gosimple/slug" + "gorm.io/gorm" +) + +type Brand struct { + ID uint `gorm:"primary_key"` + CreatedAt time.Time + UpdatedAt time.Time + Name string `gorm:"default:TESTBRAND"` + Code string + Status bool + Categories []Category `gorm:"many2many:category_brands;"` +} + +func FindOrCreateBrand(db *gorm.DB, brand string, categories []Category) (Brand, error) { + + var brandObject Brand + + if brand != "" { + + code := slug.Make(brand) + + err := db.FirstOrCreate(&brandObject, Brand{Name: brand, Code: code}).Error + //err := db.Model(Brand{Name: brand, Code: code}).First(&brandObject).Error + //log.Println(Brand{Name: brand, Code: code}) + + if err != nil { + //err := db.Omit("Categories.*").FirstOrCreate(&brandObject, Brand{Name: brand, Code: code, Categories: categories}).Error + log.Println("ERR0000000000000001" + err.Error()) + return brandObject, err + } + + db.Model(&brandObject).Association("Categories").Append(categories) + + return brandObject, nil + + } + + // var brandObject Brand + + return brandObject, nil +} diff --git a/bagisto_models/category.go b/bagisto_models/category.go new file mode 100644 index 0000000..cbcab8d --- /dev/null +++ b/bagisto_models/category.go @@ -0,0 +1,48 @@ +package bagisto_models + +import ( + "log" + + "gorm.io/gorm" +) + +type CategoryTranslation struct { + ID uint `gorm:"primaryKey"` + Slug string + CategoryID uint + MetaDescription string + MetaKeywords string + Locale string +} + +type Category struct { + ID uint `gorm:"primaryKey"` + Status int8 + Position int + ParentId uint + DisplayMode string + Translations []CategoryTranslation +} + +func GetMainCategories(db *gorm.DB) ([]Category, error) { + var categories []Category + err := db.Model(&Category{}).Preload("Translations").Find(&categories, "status=1 AND parent_id=1 AND display_mode!='promotion'").Error + + if err != nil { + log.Println(err.Error()) + return nil, err + } + + return categories, nil +} + +func GetCatKeywords(db *gorm.DB, catIDs []int) ([]Category, error) { + var categories []Category + + if errCat := db.Preload("Translations", "locale=?", "tm").Find(&categories, catIDs).Error; errCat != nil { + log.Println(errCat) + return categories, errCat + } + + return categories, nil +} diff --git a/bagisto_models/family.go b/bagisto_models/family.go new file mode 100644 index 0000000..d1c0cce --- /dev/null +++ b/bagisto_models/family.go @@ -0,0 +1,108 @@ +package bagisto_models + +import ( + "errors" + "log" + + "gorm.io/gorm" +) + +type AttributeOption struct { + ID uint `gorm:"primaryKey"` + AttributeID uint + AdminName string + SortOrder int `sql:"DEFAULT:NULL" gorm:"default:null"` + Translations []AttributeOptionTranslation +} + +type AttributeOptionTranslation struct { + ID uint `gorm:"primaryKey"` + AttributeOptionID uint + Locale string + Label string +} + +type Attribute struct { + ID uint `gorm:"primaryKey"` + Code string + AttributeOptions []AttributeOption +} + +type AttributeFamily struct { + ID uint `gorm:"primaryKey"` + Code string + Groups []AttributeGroup +} + +type AttributeGroup struct { + ID uint `gorm:"primaryKey"` + Name string + AttributeFamilyID uint + Attributes []Attribute `gorm:"many2many:attribute_group_mappings;"` +} + +func GetFamilies(db *gorm.DB) ([]AttributeFamily, error) { + var families []AttributeFamily + + err := db.Model(&AttributeFamily{}).Preload("Groups").Order("id ASC").Find(&families).Error + + return families, err +} + +func GetGroups(db *gorm.DB) []AttributeGroup { + var groups []AttributeGroup + err := db.Model(&AttributeGroup{}).Preload("Family").Find(&groups).Error + + if err != nil { + log.Println(err.Error()) + + } + return groups +} + +func GetAttributes(db *gorm.DB) ([]Attribute, error) { + var attributes []Attribute + + err := db.Model(&Attribute{}).Find(&attributes).Error + + return attributes, err +} + +func GetAttributeOption(db *gorm.DB, attrbuteID uint, value string) AttributeOption { + var option AttributeOption + + err := db.Where("attribute_id=? and admin_name=?", attrbuteID, value).First(&option).Error + + if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { + option = AttributeOption{ + AttributeID: attrbuteID, + AdminName: value, + SortOrder: 1000, + Translations: []AttributeOptionTranslation{ + {Locale: "tm", Label: value}, + {Locale: "ru", Label: value}, + {Locale: "tr", Label: value}, + }, + } + + if err := db.Create(&option).Error; err != nil { + log.Println(err.Error()) + + } + } + + return option +} + +func GetAttrOptions(db *gorm.DB, attrbuteID uint) ([]AttributeOption, error) { + + var options []AttributeOption + + err := db.Find(&options, AttributeOption{AttributeID: attrbuteID}).Error + + if err != nil { + log.Println(err.Error()) + } + + return options, err +} diff --git a/bagisto_models/product.go b/bagisto_models/product.go new file mode 100644 index 0000000..6dcc907 --- /dev/null +++ b/bagisto_models/product.go @@ -0,0 +1,166 @@ +package bagisto_models + +import ( + "context" + + "gorm.io/gorm" + "log" + "time" +) + +type Product struct { + ID uint `gorm:"primary_key"` + CreatedAt time.Time + UpdatedAt time.Time + Sku string + Type string + ParentID uint `sql:"DEFAULT:NULL"` + AttributeFamilyID uint + AttributeFamily AttributeFamily + BrandID uint `sql:"DEFAULT:NULL"` + Brand Brand + Images []ProductImage + Categories []Category `gorm:"many2many:product_categories;"` + AttributeValues []ProductAttributeValue + SuperAttributes []Attribute `gorm:"many2many:product_super_attributes;"` + //Variants []*Product `gorm:"many2many:product_relations;foreignKey:child_id;primaryKey:parent_id;"` +} + +type ProductRelation struct { + ParentID uint + ChildID uint +} + +type ProductFlat struct { + ID uint `gorm:"primary_key"` + Sku string + ProductNumber string `sql:"DEFAULT:NULL" gorm:"default:null"` + Name string `sql:"DEFAULT:''" gorm:"default:''"` + Weight float64 `sql:"DEFAULT:NULL" gorm:"type:decimal(12,4);default:null"` + Status bool `sql:"DEFAULT:NULL" gorm:"default:null"` + VisibleIndividually bool `sql:"DEFAULT:NULL" gorm:"default:null"` + Price float64 `gorm:"type:decimal(12,4)"` + MinPrice float64 `gorm:"type:decimal(12,4)"` + MaxPrice float64 `gorm:"type:decimal(12,4)"` + SpecialPrice float64 `sql:"DEFAULT:NULL" gorm:"type:decimal(12,4);default:null"` + UrlKey string `sql:"DEFAULT:NULL" gorm:"default:null"` + ShortDescription string `sql:"DEFAULT:NULL" gorm:"default:null"` + Description string `sql:"DEFAULT:NULL" gorm:"default:null"` + FavoritesCount uint `sql:"DEFAULT:NULL" gorm:"default:null;column:favoritesCount"` + CreatedAt time.Time `sql:"DEFAULT:NULL" gorm:"default:null"` + UpdatedAt time.Time `sql:"DEFAULT:NULL" gorm:"default:null"` + ProductID uint + Product Product + Channel string `sql:"DEFAULT:default" gorm:"default:default"` + Locale string `sql:"DEFAULT:tm" gorm:"default:tm"` + ParentID uint `sql:"DEFAULT:NULL" gorm:"default:null"` + Variants []ProductFlat `gorm:"foreignKey:ParentID"` + Color int `sql:"DEFAULT:NULL" gorm:"default:null"` + ColorLabel string `sql:"DEFAULT:NULL" gorm:"default:null"` + Size int `sql:"DEFAULT:NULL" gorm:"default:null"` + SizeLabel string `sql:"DEFAULT:NULL" gorm:"default:null"` + Boyut int `sql:"DEFAULT:NULL" gorm:"default:null"` + BoyutLabel string `sql:"DEFAULT:NULL" gorm:"default:null"` + MetaTitle string `sql:"DEFAULT:NULL" gorm:"default:null"` + MetaKeywords string `sql:"DEFAULT:NULL" gorm:"default:null"` + BrandID uint `sql:"DEFAULT:NULL" gorm:"default:null"` + Brand Brand + Cinsiyet int `sql:"DEFAULT:NULL" gorm:"default:null"` + CinsiyetLabel string `sql:"DEFAULT:NULL" gorm:"default:null"` +} + +type ProductImage struct { + ID uint `gorm:"primary_key"` + Type string + Path string + ProductID uint +} + +type ProductAttributeValue struct { + ID uint `gorm:"primary_key"` + Locale string `sql:"DEFAULT:NULL" gorm:"default:null"` + Channel string `sql:"DEFAULT:NULL" gorm:"default:null"` + ProductID uint + AttributeID uint + TextValue string `sql:"DEFAULT:NULL" gorm:"default:null"` + BooleanValue bool `sql:"DEFAULT:NULL" gorm:"default:null"` + IntegerValue int `sql:"DEFAULT:NULL" gorm:"default:null"` + FloatValue float64 `sql:"DEFAULT:NULL" gorm:"type:decimal(12,4);default:null"` +} + +type ProductSuperAttribute struct { + ProductID uint + AttributeID uint +} + +type Tabler interface { + TableName() string +} + +// TableName overrides the table name used by User to `profiles` +func (ProductFlat) TableName() string { + return "product_flat" +} + +func DeleteProducts(db *gorm.DB) error { + //todo delete from elastico + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + //qb := "DELETE FROM products WHERE id NOT IN (select product_id as id from wishlist) AND id NOT IN (select product_id as id from order_items) AND id NOT IN (select parent_id as idfrom order_items);" + qb := "DELETE p FROM products p " + + "LEFT JOIN order_items oi ON p.id = oi.product_id " + + "LEFT JOIN order_items op ON p.id = op.parent_id " + + "LEFT JOIN wishlist wp ON p.id = wp.product_id " + + "LEFT JOIN wishlist wl ON wl.product_id = p.parent_id " + + "JOIN marketplace_products mp ON p.id = mp.product_id " + + "WHERE oi.id IS NULL AND op.id IS NULL AND wp.id IS NULL AND wl.id IS NULL AND mp.marketplace_seller_id=1;" + db.WithContext(ctx).Exec(qb) + //db.WithContext(ctx).Exec("UPDATE product_flat set sku=concat(id,\"-ordered\"), status=0 where status=1 AND product_id IN (SELECT product_id from marketplace_products where marketplace_seller_id=1)" ) + //db.WithContext(ctx).Exec("UPDATE products set sku=concat(id,\"-ordered\") WHERE id IN (SELECT product_id from marketplace_products where marketplace_seller_id=1)") + return db.Error +} + +func GetWishlistProducts(db *gorm.DB) ([]Product, error) { + var products []Product + err := db.Joins("JOIN wishlist wp ON products.id = wp.product_id"). + Joins("JOIN marketplace_products mp ON products.id = mp.product_id where marketplace_seller_id=1"). + Find(&products).Error + + if err != nil { + log.Println(err.Error()) + return nil, err + } + + return products, nil + //qb := "SELECT p.id,p.sku FROM products p " + + // "JOIN wishlist wp ON p.id = wp.product_id " + + // "JOIN marketplace_products mp ON p.id = mp.product_id where marketplace_seller_id=1);" +} + +func GetFlatSources(db *gorm.DB) ([]ProductAttributeValue, error) { + var productSources []ProductAttributeValue + err := db.Joins("JOIN wishlist wp ON product_attribute_values.product_id = wp.product_id"). + Joins("JOIN marketplace_products mp ON product_attribute_values.product_id = mp.product_id where marketplace_seller_id=1"). + Find(&productSources, "text_value IS NOT NULL AND attribute_id=31").Error + + if err != nil { + log.Println(err.Error()) + return nil, err + } + + return productSources, nil +} + +//func DisableProducts (db *gorm.DB) error { +// ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) +// defer cancel() +// db.WithContext(ctx).Exec("UPDATE product_flat set sku=concat(sku,\"-ordered\"), status=0") +// db.WithContext(ctx).Exec("UPDATE products set sku=concat(sku,\"-ordered\")") +// return db.Error +//} + +//func Flush() error { +// _, err := helper.SendRequest("GET", os.Getenv("scout_flash"), nil, "") +// +// return err +//} diff --git a/bagisto_models/vendor.go b/bagisto_models/vendor.go new file mode 100644 index 0000000..81704bc --- /dev/null +++ b/bagisto_models/vendor.go @@ -0,0 +1,37 @@ +package bagisto_models + +import ( + "time" + + "gorm.io/gorm" +) + +type MarketplaceProduct struct { + ID uint `gorm:"primaryKey"` + CreatedAt time.Time `sql:"DEFAULT:NULL" gorm:"default:null"` + UpdatedAt time.Time `sql:"DEFAULT:NULL" gorm:"default:null"` + ProductID uint + //Product Product + ParentID *uint `sql:"DEFAULT:NULL" gorm:"default:null"` + Condition string `sql:"DEFAULT:NULL" gorm:"default:null"` + Price float64 + Description string `sql:"DEFAULT:NULL" gorm:"default:null"` + IsApproved bool `sql:"DEFAULT:NULL" gorm:"default:null"` + IsOwner bool + MarketplaceSellerID uint + MarketplaceSeller MarketplaceSeller + Variants []MarketplaceProduct `gorm:"foreignKey:ParentID"` +} + +type MarketplaceSeller struct { + ID uint `gorm:"primaryKey"` + Url string +} + +func GetSellers(db *gorm.DB) ([]MarketplaceSeller, error) { + var sellers []MarketplaceSeller + + err := db.Model(&MarketplaceSeller{}).Find(&sellers).Error + + return sellers, err +} diff --git a/controllers/UpdateController.go b/controllers/UpdateController.go new file mode 100644 index 0000000..b5097f4 --- /dev/null +++ b/controllers/UpdateController.go @@ -0,0 +1,7 @@ +package controllers + +import "net/http" + +func StartUpdate(w http.ResponseWriter, _ *http.Request) { + +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e0605ec --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module sarga_updater + +go 1.19 + +require ( + github.com/gosimple/slug v1.13.1 + github.com/leesper/couchdb-golang v1.2.1 + gorm.io/driver/mysql v1.5.0 + gorm.io/gorm v1.25.0 +) + +require ( + github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/gosimple/unidecode v1.0.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e8dc9c1 --- /dev/null +++ b/go.sum @@ -0,0 +1,17 @@ +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/gosimple/slug v1.13.1 h1:bQ+kpX9Qa6tHRaK+fZR0A0M2Kd7Pa5eHPPsb1JpHD+Q= +github.com/gosimple/slug v1.13.1/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= +github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= +github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/leesper/couchdb-golang v1.2.1 h1:FqSaTxxT2mVRLbxGQVkZakRRoSzWhPmV8UEKYjA/GWc= +github.com/leesper/couchdb-golang v1.2.1/go.mod h1:OU3FDAM3mazHx15oi8Hm+egTMneBUqepwnh0LuBSH54= +gorm.io/driver/mysql v1.5.0 h1:6hSAT5QcyIaty0jfnff0z0CLDjyRgZ8mlMHLqSt7uXM= +gorm.io/driver/mysql v1.5.0/go.mod h1:FFla/fJuCvyTi7rJQd27qlNX2v3L6deTR1GgTjSOLPo= +gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU= +gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= diff --git a/helpers/global.go b/helpers/global.go new file mode 100644 index 0000000..3819ffb --- /dev/null +++ b/helpers/global.go @@ -0,0 +1,12 @@ +package helpers + +import ( + "github.com/leesper/couchdb-golang" + models "sarga_updater/trendyol_models" +) + +// CdbServer is global couchdb server +var CdbServer *couchdb.Server + +// Categories stores all categories and their weight +var Categories []models.CouchCategory = []models.CouchCategory{} diff --git a/helpers/helper.go b/helpers/helper.go new file mode 100644 index 0000000..545163e --- /dev/null +++ b/helpers/helper.go @@ -0,0 +1,112 @@ +package helpers + +import ( + "context" + "log" + "net" + "net/http" + "os" + models "sarga_updater/trendyol_models" + "time" +) + +func CheckDBExists(endpoint string) bool { + + const ConnectMaxWaitTime = 30 * time.Second + const RequestMaxWaitTime = 60 * time.Second + + client := http.Client{ + Transport: &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: ConnectMaxWaitTime, + }).DialContext, + }, + } + + ctx, cancel := context.WithTimeout(context.Background(), RequestMaxWaitTime) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil) + req.Proto = "HTTP/1.0" + if err != nil { + log.Println(err.Error()) + return false + } + + q := req.URL.Query() + req.URL.RawQuery = q.Encode() + req.Header.Set("Connection", "close") + req.Close = true + client.CloseIdleConnections() + + response, err := client.Do(req) + + // handle timeout + if e, ok := err.(net.Error); ok && e.Timeout() { + if response != nil { + response.Body.Close() + } + log.Println(err.Error()) + return false + } else if err != nil { + if response != nil { + response.Body.Close() + } + log.Println(err.Error()) + return false + } + + if response != nil { + response.Body.Close() + } + + if response.StatusCode == http.StatusOK { + return true + } else { + return false + } +} + +func GetCategoryWeight(slug string) string { + + db, err := CdbServer.Get(os.Getenv("db_ty_categories")) + + if err != nil { + Error("can not get db. Err: " + err.Error()) + return "0.5" + } + + doc, err := db.Get(slug, nil) + + if err != nil { + Error("can not get by slug. Err: " + err.Error()) + return "0.5" + } + + return doc["weight"].(string) +} +func IsLCWSizeVariantsAdded(items []models.Variant, keyItem models.Variant) bool { + for _, option := range items { + if option.ItemNumber == keyItem.ItemNumber { + return true + } + } + return false +} + +func IsImageAdded(images []string, image string) bool { + for _, img := range images { + if img == image { + return true + } + } + return false +} +func IsVariantsAdded(variants []models.Variant, itemNumber int) bool { + for _, variant := range variants { + if variant.ItemNumber == itemNumber { + return true + } + } + return false +} diff --git a/helpers/log.go b/helpers/log.go new file mode 100644 index 0000000..f2f340c --- /dev/null +++ b/helpers/log.go @@ -0,0 +1,86 @@ +package helpers + +import ( + "fmt" + "log" + "os" + "path/filepath" + "runtime" + "time" +) + +type Level int + +var ( + DefaultPrefix = "" + DefaultCallerDepth = 2 + + logger *log.Logger + logPrefix = "" + levelFlags = []string{"DEBUG", "INFO", "WARN", "ERROR", "FATAL"} +) + +const ( + DEBUG Level = iota + INFO + WARNING + ERROR + FATAL +) + +// Setup initialize the log instance +func Setup() { + logger = log.New(os.Stdout, DefaultPrefix, log.Flags()&^(log.Ldate|log.Ltime)) +} + +// Debug output logs at debug level +func Debug(v ...interface{}) { + setPrefix(DEBUG) + logger.Println(v...) +} + +// Info output logs at info level +func Info(v ...interface{}) { + setPrefix(INFO) + logger.Println(v...) +} + +// Warn output logs at warn level +func Warn(v ...interface{}) { + setPrefix(WARNING) + logger.Println(v...) +} + +// Error output logs at error level +func Error(v ...interface{}) { + prefix := setPrefix(ERROR) + logger.Println(v...) + + prefix.msg = v +} + +// Fatal output logs at fatal level +func Fatal(v ...interface{}) { + setPrefix(FATAL) + logger.Fatalln(v...) +} + +// setPrefix set the prefix of the log output +func setPrefix(level Level) prefix { + _, file, line, ok := runtime.Caller(DefaultCallerDepth) + if ok { + logPrefix = fmt.Sprintf("[%s:%d] [%s] ", filepath.Base(file), line, levelFlags[level]) + } else { + logPrefix = fmt.Sprintf("[%s] ", levelFlags[level]) + } + + logPrefix += "| " + time.Now().Format("01.02.2006 15:04:05") + " | " + + logger.SetPrefix(logPrefix) + return prefix{prefix: logPrefix} +} + +type prefix struct { + prefix string + msg interface{} +} diff --git a/helpers/request.go b/helpers/request.go new file mode 100644 index 0000000..c60f969 --- /dev/null +++ b/helpers/request.go @@ -0,0 +1,100 @@ +package helpers + +import ( + "bytes" + "context" + "fmt" + "io/ioutil" + "net" + "net/http" + "time" +) + +func SendRequest(method string, endpoint string, values []byte, authKey string) ([]byte, error) { + + const ConnectMaxWaitTime = time.Minute + const RequestMaxWaitTime = 5 * time.Minute + + client := http.Client{ + Transport: &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: ConnectMaxWaitTime, + }).DialContext, + }, + } + + ctx, cancel := context.WithTimeout(context.Background(), RequestMaxWaitTime) + defer cancel() + + emptyBody := []byte{} + + req, err := http.NewRequestWithContext(ctx, method, endpoint, bytes.NewBuffer(values)) + req.Proto = "HTTP/2.0" + if err != nil { + Error(err) + return emptyBody, err + } + + q := req.URL.Query() + req.URL.RawQuery = q.Encode() + req.Header.Set("Connection", "close") + req.Header.Add("Authorization", authKey) + req.Close = true + client.CloseIdleConnections() + + response, err := client.Do(req) + + if e, ok := err.(net.Error); ok && e.Timeout() { + if response != nil { + response.Body.Close() + } + Error(err) + return emptyBody, err + } else if err != nil { + if response != nil { + response.Body.Close() + } + Error(err) + return emptyBody, err + } + if response.StatusCode != http.StatusOK { + if response != nil { + response.Body.Close() + } + err := fmt.Errorf("response: code: %d, body: %v", response.StatusCode, response.Body) + Error(err) + return emptyBody, err + } + + body, err := ioutil.ReadAll(response.Body) + if err != nil { + Error(err) + if response != nil { + response.Body.Close() + } + return emptyBody, nil + } + + // if you call it in loop, don't use defer + response.Body.Close() + + return body, nil +} +func NewHttpClient() (http.Client, context.Context) { + const ConnectMaxWaitTime = 30 * time.Second + const RequestMaxWaitTime = 60 * time.Second + //proxyUrl, _ := url.Parse("http://79.98.129.183:3128") + client := http.Client{ + Transport: &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: ConnectMaxWaitTime, + }).DialContext, + //Proxy: http.ProxyURL(proxyUrl), + }, + } + + ctx, cancel := context.WithTimeout(context.Background(), RequestMaxWaitTime) + defer cancel() + + return client, ctx +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..b02e032 --- /dev/null +++ b/main.go @@ -0,0 +1,93 @@ +package sarga_updater + +import ( + "gorm.io/driver/mysql" + "gorm.io/gorm" + "log" + "os" + "os/signal" + "sarga_updater/bagisto_models" + "sarga_updater/repositories" + "syscall" + "time" +) + +func main() { + // Retrieve database connection information from environment variables + dsn := os.Getenv("DB_DSN") + + // Connect to the database + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) + if err != nil { + log.Fatalf("Error connecting to database: %s", err) + } + importer, _ := repositories.ParseImporterInstance(db) + + updatePeriodStr := os.Getenv("UPDATE_PERIOD") + updatePeriod, err := time.ParseDuration(updatePeriodStr) + if err != nil { + log.Fatal("Error parsing update period:", err) + } + // Start the worker + stopCh := make(chan struct{}) + go worker(importer, stopCh, updatePeriod) + + // Wait for termination signal + signalCh := make(chan os.Signal, 1) + signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM) + <-signalCh + + // Stop the worker + close(stopCh) + + // Close the database connection + sqlDB, err := db.DB() + if err != nil { + log.Fatalf("Error getting database connection from gorm: %s", err) + } + err = sqlDB.Close() + if err != nil { + log.Fatalf("Error closing database connection: %s", err) + } +} + +func worker(importer *repositories.Importer, stopCh <-chan struct{}, updatePeriod time.Duration) { + ticker := time.NewTicker(updatePeriod) + defer ticker.Stop() + + for { + select { + case <-stopCh: + return + case <-ticker.C: + + result, err := bagisto_models.GetFlatSources(importer.Baza) + if err != nil { + log.Println("Error retrieving products:", err) + continue + } + + // Update products as needed + for _, product := range result { + // Retrieve the latest information from the source URL + parser := repositories.NewLinkParser(product.TextValue) + data, err := parser.ParseLink() + + if err != nil { + log.Println("Error decoding product information:", err) + continue + } + + jsonProduct, err := parser.GetProductDetailWithOptions(data.ID, data.ProductGroupID) + + // Update the product in the database + if err := importer.UpdateOrCreate(jsonProduct).Error; err != nil { + log.Println("Error decoding product information:", err) + continue + } + + log.Printf("Product %d updated: %+v\n", product.ID, product) + } + } + } +} diff --git a/repositories/ImportRepository.go b/repositories/ImportRepository.go new file mode 100644 index 0000000..c2c8555 --- /dev/null +++ b/repositories/ImportRepository.go @@ -0,0 +1,842 @@ +package repositories + +import ( + "encoding/json" + "errors" + "fmt" + "gorm.io/gorm" + "log" + "math" + gm "sarga_updater/bagisto_models" + helper "sarga_updater/helpers" + models "sarga_updater/trendyol_models" + "strconv" + "sync" +) + +type Importer struct { + mainCategories []gm.Category + Baza *gorm.DB + families []gm.AttributeFamily + sellers map[string]gm.MarketplaceSeller + //wishlist map[string]gm.Product + AttributesMap map[string]gm.Attribute + Error error + ImportWGroup sync.WaitGroup + ColorOptions map[string]gm.AttributeOption + SexOptions map[string]gm.AttributeOption + ColorMutex sync.Mutex + SexMutex sync.Mutex +} +type GormErr struct { + Number int `json:"Number"` + Message string `json:"Message"` +} + +func ParseImporterInstance(db *gorm.DB) (instance *Importer, err error) { + + if err != nil { + log.Println(err) + return nil, err + } + + instance = &Importer{Baza: db} + instance.ImportWGroup.Add(3) + //load families to memory + go func() { + defer instance.ImportWGroup.Done() + instance.families, instance.Error = gm.GetFamilies(db) + }() + + //load attributes to memory + go func() { + defer instance.ImportWGroup.Done() + + if attributes, err := gm.GetAttributes(db); err != nil { + instance.Error = err + return + } else { + instance.AttributesMap = make(map[string]gm.Attribute, len(attributes)) + + for _, attribute := range attributes { + instance.AttributesMap[attribute.Code] = attribute + } + } + + if colorOptions, err := gm.GetAttrOptions(db, instance.AttributesMap["color"].ID); err != nil { + instance.Error = err + return + } else { + instance.ColorOptions = make(map[string]gm.AttributeOption, len(colorOptions)) + + for _, option := range colorOptions { + instance.ColorOptions[option.AdminName] = option + } + } + + if sexOPtions, err := gm.GetAttrOptions(db, instance.AttributesMap["cinsiyet"].ID); err != nil { + instance.Error = err + return + } else { + instance.SexOptions = make(map[string]gm.AttributeOption, len(sexOPtions)) + + for _, option := range sexOPtions { + instance.SexOptions[option.AdminName] = option + } + } + + }() + + //load sellers to memory + go func() { + defer instance.ImportWGroup.Done() + + var vendors, err = gm.GetSellers(db) + + if err != nil { + instance.Error = err + return + } + //init sellers map + instance.sellers = make(map[string]gm.MarketplaceSeller, len(vendors)) + + for _, vendor := range vendors { + instance.sellers[vendor.Url] = vendor + } + }() + + if instance.Error != nil { + log.Println(instance.Error) + return nil, instance.Error + } + + return instance, nil +} + +func (importer *Importer) ImportProduct(product models.Product) (instance *Importer) { + + var linkedProducts []gm.Product + var firstProduct *gm.Product + if firstProduct, importer.Error = importer.importVariant(product); importer.Error != nil { + return importer + } else if product.ColorVariants != nil && len(*product.ColorVariants) > 0 { + linkedProducts = append(linkedProducts, *firstProduct) + for _, colorVariant := range *product.ColorVariants { + + if !colorVariant.IsSellable { + continue + } + + if variant, err := importer.importVariant(colorVariant); err == nil { + linkedProducts = append(linkedProducts, *variant) + } + } + } + + if len(linkedProducts) > 1 { + + var relation []gm.ProductRelation + for index, variant := range linkedProducts { + //spoint := "color" + strconv.Itoa(index) + temp := make([]gm.Product, len(linkedProducts)) + + copy(temp, linkedProducts) + + if index+1 <= len(temp) { + temp = append(temp[:index], temp[index+1:]...) + + for _, item := range temp { + relation = append(relation, gm.ProductRelation{ParentID: variant.ID, ChildID: item.ID}) + } + } + + } + + if err := importer.Baza.Create(&relation).Error; err != nil { + log.Println(err) + } + + } + + return importer +} + +func (importer *Importer) importVariant(product models.Product) (*gm.Product, error) { + + // check if wishlisted then update if. + //if _, ok := importer.wishlist[product.ProductNumber]; ok { + // delete(importer.wishlist,product.ProductNumber) + // return importer.updateVariant(product) + //} + + productRepo := InitProductRepo(&product, importer.GetColorOption(product.Color), importer.GetSexOption(product.Cinsiyet)) + + if categories, err := gm.GetCatKeywords(importer.Baza, product.Categories); err != nil { + return nil, err + } else { + productRepo.SetCategories(categories) + } + + if brand, err := gm.FindOrCreateBrand(importer.Baza, product.Brand, productRepo.Categories); err != nil { + return nil, err + } else { + productRepo.Brand = brand + } + + mainPorduct := productRepo.makeProduct(importer) + + //BEGIN TRANSACTION + tx := importer.Baza.Begin() + + if err := tx.Omit("Categories.*", "SuperAttributes.*", "ParentID").Create(&mainPorduct).Error; err != nil { + //todo update categories + byteErr, _ := json.Marshal(err) + var newError GormErr + + if err1 := json.Unmarshal((byteErr), &newError); err1 != nil { + tx.Rollback() + log.Println(err1, "err2") + return nil, err1 + } + + if newError.Number == 1062 { + var barProduct gm.Product + if err2 := tx.First(&barProduct, "sku = ?", mainPorduct.Sku).Error; err2 != nil { + tx.Rollback() + log.Println(err2, "err3") + return nil, err2 + } + + if err3 := tx.Model(&barProduct).Association("Categories").Append(mainPorduct.Categories); err3 != nil { + tx.Rollback() + log.Println(err3, "err4") + return nil, err3 + } + + if err4 := tx.Commit().Error; err4 == nil { + return importer.updateVariant(product) + } + } else { + tx.Rollback() + log.Println(err, "er1") + return nil, err + } + + } + + mainFlat := productRepo.makeProductFlat(mainPorduct.ID) + + if err := tx.Create(&mainFlat).Error; err != nil { + tx.Rollback() + log.Println(err, "er5") + return nil, err + } + + if productRepo.HasSizeVariants() { + var sizeVariants []gm.ProductFlat + for index, variant := range *product.SizeVariants { + if !variant.Sellable { + continue + } + savePoint := "size" + strconv.Itoa(index) + tx.SavePoint(savePoint) + + var sizeOPtion gm.AttributeOption + + if variant.AttributeName == "Beden" { + sizeOPtion = gm.GetAttributeOption(tx, importer.AttributesMap["size"].ID, variant.AttributeValue) + } else { + sizeOPtion = gm.GetAttributeOption(tx, importer.AttributesMap["boyut"].ID, variant.AttributeValue) + } + + sku := fmt.Sprintf("%s-%d", product.ProductNumber, variant.ItemNumber) + + variantProduct := productRepo.makeVariant(mainPorduct.ID, mainPorduct.AttributeFamilyID, sku) + + variantProduct.AttributeValues = productRepo.getVariantAttributes(importer.AttributesMap, &variant, sizeOPtion.ID) + + if err := tx.Omit("Categories.*").Create(&variantProduct).Error; err != nil { + log.Println("Variant Product Create Error: " + err.Error()) + tx.RollbackTo(savePoint) + continue + } + + variantFlat := productRepo.makeVariantFlat(variant, sizeOPtion.ID, mainFlat.ID, variantProduct.ID) + + if err := tx.Create(&variantFlat).Error; err != nil { + log.Println("Variant Flat Create Error: " + err.Error()) + tx.RollbackTo(savePoint) + continue + } + + sizeVariants = append(sizeVariants, variantFlat) + } + + if len(sizeVariants) == 0 { + tx.Rollback() + return nil, errors.New("size variantlary yok bolsa main productam girayenok") + } else { + calcPrice(sizeVariants, &mainFlat) + + err := tx.Omit("ParentID", "CreatedAt", "Variants", "SpecialPrice").Save(&mainFlat).Error + + mainFlat.Variants = sizeVariants + + if err != nil { + tx.Rollback() + log.Println(err, "er6") + return nil, err + } + } + + } + + sProduct := importer.createSellerProduct(&mainFlat, product.Vendor) + + if errSProduct := tx.Create(&sProduct).Error; errSProduct != nil { + tx.Rollback() + return nil, errSProduct + } + + if err := tx.Commit().Error; err != nil { + return nil, err + } + + return &mainPorduct, nil +} + +func (importer *Importer) createSellerProduct(flat *gm.ProductFlat, sellerURL string) gm.MarketplaceProduct { + sellerID := importer.sellers[sellerURL].ID + + if sellerID == 0 { + sellerID = 1 + } + + sellerProduct := gm.MarketplaceProduct{ + MarketplaceSellerID: sellerID, + IsApproved: true, + Condition: "new", + Description: "scraped", + IsOwner: true, + ProductID: flat.ProductID, + } + + for _, variant := range flat.Variants { + sellerProduct.Variants = append(sellerProduct.Variants, gm.MarketplaceProduct{ + ProductID: variant.ProductID, + IsOwner: true, + IsApproved: true, + MarketplaceSellerID: sellerID, + Condition: "new", + }) + } + + return sellerProduct + +} + +func calcPrice(variants []gm.ProductFlat, flat *gm.ProductFlat) { + for _, variant := range variants { + + if !variant.Status { + continue + } + + if flat.MinPrice == 0 || flat.MinPrice > variant.MinPrice { + flat.MinPrice = variant.MinPrice + } + + flat.MaxPrice = math.Max(flat.MaxPrice, variant.MaxPrice) + } +} + +func (importer *Importer) GetColorOption(optionName string) gm.AttributeOption { + if optionName == "" { + return gm.AttributeOption{} + } + + importer.ColorMutex.Lock() + var option gm.AttributeOption + var ok bool + + if option, ok = importer.ColorOptions[optionName]; !ok { + option := gm.GetAttributeOption(importer.Baza, importer.AttributesMap["color"].ID, optionName) + + importer.ColorOptions[optionName] = option + } + importer.ColorMutex.Unlock() + return option +} + +func (importer *Importer) GetSexOption(optionName string) gm.AttributeOption { + if optionName == "" { + return gm.AttributeOption{} + } + importer.SexMutex.Lock() + var option gm.AttributeOption + var ok bool + + if option, ok = importer.ColorOptions[optionName]; !ok { + option = gm.GetAttributeOption(importer.Baza, importer.AttributesMap["cinsiyet"].ID, optionName) + importer.SexOptions[optionName] = option + } + importer.SexMutex.Unlock() + return option +} + +func (importer *Importer) updateVariant(product models.Product) (*gm.Product, error) { + + var flat gm.ProductFlat + err := importer.Baza.Preload("Product").Preload("Variants").First(&flat, "sku = ?", product.ProductNumber).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return importer.importVariant(product) + } + //todo not found bolsa create etmeli + return nil, err + } + + if flat.Product.Type == "configurable" { + + productRepo := InitProductRepo(&product, importer.GetColorOption(product.Color), importer.GetSexOption(product.Cinsiyet)) + if brand, err := gm.FindOrCreateBrand(importer.Baza, product.Brand, productRepo.Categories); err != nil { + return nil, err + } else { + productRepo.Brand = brand + } + + for _, variant := range *product.SizeVariants { + + found := false + for _, flatVariant := range flat.Variants { + + if variant.AttributeValue == flatVariant.BoyutLabel || variant.AttributeValue == flatVariant.SizeLabel { + + if !variant.Sellable { + importer.Baza.Model(&flatVariant).Update("status", false) + } else { + + importer.updatePrice(variant.Price, &flatVariant) + + if !flatVariant.Status { + + importer.Baza.Model(&flatVariant).Update("status", true) + } + } + found = true + break + } + } + + if variant.Sellable && !found { + // insert variant + var sizeOPtion gm.AttributeOption + + if variant.AttributeName == "Beden" { + sizeOPtion = gm.GetAttributeOption(importer.Baza, importer.AttributesMap["size"].ID, variant.AttributeValue) + } else { + sizeOPtion = gm.GetAttributeOption(importer.Baza, importer.AttributesMap["boyut"].ID, variant.AttributeValue) + } + + sku := fmt.Sprintf("%s-%d", product.ProductNumber, variant.ItemNumber) + variantProduct := productRepo.makeVariant(flat.ProductID, flat.Product.AttributeFamilyID, sku) + + variantProduct.AttributeValues = productRepo.getVariantAttributes(importer.AttributesMap, &variant, sizeOPtion.ID) + + if err := importer.Baza.Omit("Categories.*").Create(&variantProduct).Error; err != nil { + log.Println("Variant Product Create Error: " + err.Error()) + } else { + variantFlat := productRepo.makeVariantFlat(variant, sizeOPtion.ID, flat.ID, variantProduct.ID) + + if err := importer.Baza.Create(&variantFlat).Error; err != nil { + log.Println("Variant Flat Create Error: " + err.Error()) + + } + flat.Variants = append(flat.Variants, variantFlat) + } + + } + } + + calcPrice(flat.Variants, &flat) + importer.Baza.Omit("ParentID", "CreatedAt", "Variants", "SpecialPrice").Save(&flat) + + } else { //simple + + importer.updatePrice(product.Price, &flat) + + } + return &flat.Product, nil +} + +func (importer *Importer) updatePrice(price models.Price, flat *gm.ProductFlat) { + if price.OriginalPrice.Value > price.DiscountedPrice.Value { + + importer.Baza.Model(&flat).Updates(map[string]interface{}{ + "price": price.OriginalPrice.Value, + "special_price": price.DiscountedPrice.Value, + "min_price": price.DiscountedPrice.Value, + "max_price": price.OriginalPrice.Value, + }) + + importer.Baza.Model(&gm.ProductAttributeValue{}). + Where("attribute_id = 11 and product_id = ?", flat.ProductID). + Update("float_value", price.OriginalPrice.Value) + + importer.Baza.Model(&gm.ProductAttributeValue{}). + Where("attribute_id = 13 and product_id = ?", flat.ProductID). + Update("float_value", price.DiscountedPrice.Value) + } else { + + importer.Baza.Model(&flat).Updates(map[string]interface{}{ + "price": price.DiscountedPrice.Value, + "special_price": nil, + "min_price": price.DiscountedPrice.Value, + "max_price": price.DiscountedPrice.Value, + }) + + importer.Baza.Model(&gm.ProductAttributeValue{}). + Where("attribute_id = 11 and product_id = ?", flat.ProductID). + Update("float_value", price.DiscountedPrice.Value) + + importer.Baza.Where("attribute_id = 13 and product_id = ?", flat.ProductID). + Delete(&gm.ProductAttributeValue{}) + } +} + +func (importer *Importer) importParsedVariant(product models.Product) (gm.Product, error) { + + //todo search if exists + + productRepo := InitProductRepo(&product, importer.GetColorOption(product.Color), importer.GetSexOption(product.Cinsiyet)) + productRepo.ImageType = "lcw" + if brand, err := gm.FindOrCreateBrand(importer.Baza, product.Brand, productRepo.Categories); err != nil { + return gm.Product{}, err + } else { + productRepo.Brand = brand + } + mainPorduct := productRepo.makeProduct(importer) + + //BEGIN TRANSACTION + tx := importer.Baza.Begin() + + if err := tx.Omit("Categories", "SuperAttributes.*", "ParentID").Create(&mainPorduct).Error; err != nil { + tx.Rollback() + return gm.Product{}, err + + } + + mainFlat := productRepo.makeProductFlat(mainPorduct.ID) + + if err := tx.Create(&mainFlat).Error; err != nil { + tx.Rollback() + return gm.Product{}, err + } + + if productRepo.HasSizeVariants() { + var sizeVariants []gm.ProductFlat + for index, variant := range *product.SizeVariants { + if !variant.Sellable { + continue + } + savePoint := "size" + strconv.Itoa(index) + tx.SavePoint(savePoint) + + var sizeOPtion gm.AttributeOption + + if variant.AttributeName == "Beden" { + sizeOPtion = gm.GetAttributeOption(tx, importer.AttributesMap["size"].ID, variant.AttributeValue) + } else { + sizeOPtion = gm.GetAttributeOption(tx, importer.AttributesMap["boyut"].ID, variant.AttributeValue) + } + + sku := fmt.Sprintf("%s-%d", product.ProductNumber, variant.ItemNumber) + log.Println(sku) + log.Println(variant) + + variantProduct := productRepo.makeVariant(mainPorduct.ID, mainPorduct.AttributeFamilyID, sku) + + variantProduct.AttributeValues = productRepo.getVariantAttributes(importer.AttributesMap, &variant, sizeOPtion.ID) + + if err := tx.Omit("Categories.*").Create(&variantProduct).Error; err != nil { + log.Println("Variant Product Create Error: " + err.Error()) + tx.RollbackTo(savePoint) + continue + } + + variantFlat := productRepo.makeVariantFlat(variant, sizeOPtion.ID, mainFlat.ID, variantProduct.ID) + + if err := tx.Create(&variantFlat).Error; err != nil { + log.Println("Variant Flat Create Error: " + err.Error()) + tx.RollbackTo(savePoint) + continue + } + + sizeVariants = append(sizeVariants, variantFlat) + } + + if len(sizeVariants) == 0 { + tx.Rollback() + return gm.Product{}, errors.New("siz variantlary yok bolsa main productam girayenok") + } else { + calcPrice(sizeVariants, &mainFlat) + + err := tx.Omit("ParentID", "CreatedAt", "Variants", "SpecialPrice").Save(&mainFlat).Error + + mainFlat.Variants = sizeVariants + + if err != nil { + tx.Rollback() + return gm.Product{}, err + } + } + + } + + log.Println(product.Vendor) + + sProduct := importer.createSellerProduct(&mainFlat, product.Vendor) + + if errSProduct := tx.Create(&sProduct).Error; errSProduct != nil { + tx.Rollback() + return gm.Product{}, errSProduct + } + + if err := tx.Commit().Error; err != nil { + return gm.Product{}, err + } + + return mainPorduct, nil + +} + +func (importer *Importer) updateParsedVariant(product models.Product) (gm.Product, error) { + var flat gm.ProductFlat + err := importer.Baza.Preload("Product").Preload("Variants").First(&flat, "sku = ?", product.ProductNumber).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return importer.importParsedVariant(product) + } + //todo not found bolsa create etmeli + return gm.Product{}, err + } + + if flat.Product.Type == "configurable" { + + productRepo := InitProductRepo(&product, importer.GetColorOption(product.Color), importer.GetSexOption(product.Cinsiyet)) + productRepo.ImageType = "lcw" + if brand, err := gm.FindOrCreateBrand(importer.Baza, product.Brand, productRepo.Categories); err != nil { + return gm.Product{}, err + } else { + productRepo.Brand = brand + } + + for _, variant := range *product.SizeVariants { + + found := false + for _, flatVariant := range flat.Variants { + + if variant.AttributeValue == flatVariant.BoyutLabel || variant.AttributeValue == flatVariant.SizeLabel { + + if !variant.Sellable { + importer.Baza.Model(&flatVariant).Update("status", false) + } else { + + importer.updatePrice(variant.Price, &flatVariant) + + if !flatVariant.Status { + + importer.Baza.Model(&flatVariant).Update("status", true) + } + } + found = true + break + } + } + + if variant.Sellable && !found { + // insert variant + var sizeOPtion gm.AttributeOption + + if variant.AttributeName == "Beden" { + sizeOPtion = gm.GetAttributeOption(importer.Baza, importer.AttributesMap["size"].ID, variant.AttributeValue) + } else { + sizeOPtion = gm.GetAttributeOption(importer.Baza, importer.AttributesMap["boyut"].ID, variant.AttributeValue) + } + log.Println(variant) + sku := fmt.Sprintf("%s-%d", product.ProductNumber, variant.ItemNumber) + log.Println(sku) + + variantProduct := productRepo.makeVariant(flat.ProductID, flat.Product.AttributeFamilyID, sku) + + variantProduct.AttributeValues = productRepo.getVariantAttributes(importer.AttributesMap, &variant, sizeOPtion.ID) + + if err := importer.Baza.Omit("Categories").Create(&variantProduct).Error; err != nil { + log.Println("Variant Product Create Error: " + err.Error()) + } + + variantFlat := productRepo.makeVariantFlat(variant, sizeOPtion.ID, flat.ID, variantProduct.ID) + + if err := importer.Baza.Create(&variantFlat).Error; err != nil { + log.Println("Variant Flat Create Error: " + err.Error()) + + } + flat.Variants = append(flat.Variants, variantFlat) + } + } + + calcPrice(flat.Variants, &flat) + importer.Baza.Omit("ParentID", "CreatedAt", "Variants", "SpecialPrice").Save(&flat) + + } else { //simple + + importer.updatePrice(product.Price, &flat) + + } + return flat.Product, nil +} + +func (importer *Importer) UpdateOrCreateLCW(product models.Product) (instance *Importer) { + var firstProduct gm.Product + result := importer.Baza.First(&firstProduct, "sku=?", product.ProductNumber) + + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + firstProduct, importer.Error = importer.importParsedVariant(product) + + } else { + firstProduct, importer.Error = importer.updateParsedVariant(product) + + } + + var newProducts []gm.Product + + if importer.Error != nil { + return importer + } else if &firstProduct != nil { + newProducts = append(newProducts, firstProduct) + } + + if product.ColorVariants != nil && len(*product.ColorVariants) > 0 { + linkedProducts := []gm.Product{firstProduct} + + for _, colorVariant := range *product.ColorVariants { + + var ( + variant gm.Product + err error + ) + if !colorVariant.IsSellable { + + if err = importer.Baza.Model(&gm.ProductFlat{}).Where("sku=?", colorVariant.ProductNumber).Update("status", false).Error; err != nil { + log.Println(err) + } + continue + + } else { + + //todo check parsed import variant + if variant, err = importer.importParsedVariant(colorVariant); err != nil { + + if variant, importer.Error = importer.updateParsedVariant(colorVariant); importer.Error != nil { + return importer + } + + linkedProducts = append(linkedProducts, variant) + + } else { + newProducts = append(newProducts, variant) + } + } + + } + + if len(newProducts) > 0 { + // relation + + var relation []gm.ProductRelation + for _, linkedProduct := range linkedProducts { + + for _, newProd := range newProducts { + relation = append(relation, gm.ProductRelation{ParentID: linkedProduct.ID, ChildID: newProd.ID}) + relation = append(relation, gm.ProductRelation{ParentID: newProd.ID, ChildID: linkedProduct.ID}) + } + } + + if err := importer.Baza.Create(&relation).Error; err != nil { + log.Println(err) + } + } + + } + return importer +} + +func (importer *Importer) UpdateOrCreate(product models.Product) (instance *Importer) { + + firstProduct, err := importer.updateVariant(product) + var newProducts []gm.Product + if err != nil { + helper.Error(err) + + firstProduct, importer.Error = importer.updateVariant(product) + + if importer.Error != nil { + return importer + } + } else if &firstProduct != nil { + newProducts = append(newProducts, *firstProduct) + } + + if product.ColorVariants != nil && len(*product.ColorVariants) > 0 { + linkedProducts := []gm.Product{*firstProduct} + + for _, colorVariant := range *product.ColorVariants { + + var ( + variant *gm.Product + err error + ) + if !colorVariant.IsSellable { + + if err = importer.Baza.Model(&gm.ProductFlat{}).Where("sku=?", colorVariant.ProductNumber).Update("status", false).Error; err != nil { + log.Println(err) + } + continue + + } else { + + if variant, err = importer.importVariant(colorVariant); err != nil { + + if variant, importer.Error = importer.updateVariant(colorVariant); importer.Error != nil { + return importer + } + + linkedProducts = append(linkedProducts, *variant) + + } else { + newProducts = append(newProducts, *variant) + } + } + + } + + if len(newProducts) > 0 { + // relation + + var relation []gm.ProductRelation + for _, linkedProduct := range linkedProducts { + + for _, newProd := range newProducts { + relation = append(relation, gm.ProductRelation{ParentID: linkedProduct.ID, ChildID: newProd.ID}) + relation = append(relation, gm.ProductRelation{ParentID: newProd.ID, ChildID: linkedProduct.ID}) + } + } + + if err := importer.Baza.Create(&relation).Error; err != nil { + log.Println(err) + } + } + + } + return importer +} diff --git a/repositories/ParseRepository.go b/repositories/ParseRepository.go new file mode 100644 index 0000000..99fbb4a --- /dev/null +++ b/repositories/ParseRepository.go @@ -0,0 +1,375 @@ +package repositories + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "reflect" + "regexp" + helper "sarga_updater/helpers" + models "sarga_updater/trendyol_models" + "strconv" + "strings" +) + +type LinkParser struct { + link string +} + +func NewLinkParser(link string) LinkParser { + return LinkParser{link: link} +} + +func (l LinkParser) ParseLink() (models.TrendyolProductDetailModel, error) { + + helper.Info("link: ", l.link) + + productId := "" + + if isShortLink(l.link) { + productId = getProductIdFromShortLink(l.link) + } else { + productId = getProductIdFromLink(l.link) + } + + if len(productId) == 0 { + parseErr := errors.New("can not parse product id") + helper.Error(parseErr) + return models.TrendyolProductDetailModel{}, parseErr + } + + helper.Info("productId: ", productId) + + return GetProductDetails(productId) +} + +func getProductIdFromShortLink(shortLink string) string { + + var productId string + + client, _ := helper.NewHttpClient() + req, err := http.NewRequest("GET", shortLink, nil) + req.Proto = "HTTP/2.0" + if err != nil { + helper.Error(err) + return "" + } + + q := req.URL.Query() + req.URL.RawQuery = q.Encode() + req.Header.Set("Connection", "close") + req = req.WithContext(req.Context()) + req.Close = true + client.CloseIdleConnections() + + response, err := client.Do(req) + if err != nil { + helper.Error(err) + return productId + } + + defer response.Body.Close() + + url := response.Request.URL.Path + + helper.Info("link url: ", url) + + productId = getProductIdFromLink(url) + + helper.Info("productId: ", productId) + + return productId +} + +func getProductIdFromLink(link string) string { + var productId string + + if strings.Contains(link, "?") { + link = strings.Split(link, "?")[0] + } + + strArr := strings.Split(link, "-") + productId = strArr[len(strArr)-1] + + return productId +} + +func isShortLink(link string) bool { + return !strings.Contains(link, "trendyol.com") +} + +// GetProductDetails return JSON object. Merges color option +func GetProductDetails(productId string) (models.TrendyolProductDetailModel, error) { + + var response models.TrendyolProductDetailResponse + productDetailModel := models.TrendyolProductDetailModel{} + + // set linearVariants false to get variants + url := "https://public.trendyol.com/discovery-web-productgw-service/api/productDetail/" + productId + "?storefrontId=1&culture=tr-TR&linearVariants=false" + + body, err := helper.SendRequest("GET", url, nil, "") + + if err != nil { + return productDetailModel, err + } + + err = json.Unmarshal(body, &response) + + if err != nil { + helper.Error(err) + return productDetailModel, err + } + + productDetailModel = response.Result + + return productDetailModel, nil +} + +// getProductDetailWithOptions returns JSON with variants +func (l LinkParser) GetProductDetailWithOptions(productId, productGroupId int) (models.Product, error) { + + primaryProductDetail, err := GetProductDetails(strconv.Itoa(productId)) + + if err != nil { + return models.Product{}, err + } + + productDetailJSON := CreateJSONFromModel(primaryProductDetail) + + if productDetailJSON != nil { + colorVariants, err := GetProductColorVariants(productGroupId) + + var colorVariantsJson []map[string]interface{} + + if err != nil { + return models.Product{}, err + } + + // get the color variant products + for _, slicingAttribute := range colorVariants.Result.SlicingAttributes { + for _, attribute := range slicingAttribute.Attributes { + for _, content := range attribute.Contents { + // Don't fetch primary product again. + if content.ID != primaryProductDetail.ID { + productVariantDetail, errGPD := GetProductDetails(strconv.Itoa(content.ID)) + if errGPD == nil { + productVariantJSON := CreateJSONFromModel(productVariantDetail) + if productVariantJSON != nil { + colorVariantsJson = append(colorVariantsJson, productVariantJSON) + } + } + } + } + } + // Color variant count + if len(slicingAttribute.Attributes) > 0 { + productDetailJSON["color_variant_count"] = len(slicingAttribute.Attributes) + productDetailJSON["color_variants"] = colorVariantsJson + } + } + } + + jsonString, _ := json.Marshal(productDetailJSON) + + // convert json to struct + converted := models.Product{} + json.Unmarshal(jsonString, &converted) + + return converted, nil +} + +// getProductColorVariants returns color options of product +func GetProductColorVariants(productGroupId int) (models.TrendyolProductVariantsResponse, error) { + + url := "https://public.trendyol.com/discovery-web-productgw-service/api/productGroup/" + strconv.Itoa(productGroupId) + "?storefrontId=1&culture=tr-TR" + + var response models.TrendyolProductVariantsResponse + + body, err := helper.SendRequest("GET", url, nil, "") + + if err != nil { + return response, err + } + + err = json.Unmarshal(body, &response) + + if err != nil { + helper.Error(err) + return response, err + } + + return response, nil +} + +// createJSONFromModel creates json from [trendyol.TrendyolProductDetailModel] and returns +func CreateJSONFromModel(model models.TrendyolProductDetailModel) map[string]interface{} { + + json := map[string]interface{}{} + + // get weight from categories (Stored in helper array (RAM)) + weight := helper.GetCategoryWeight(model.Category.BeautifiedName) + productGroupId := strconv.Itoa(model.ProductGroupID) + + json["_id"] = productGroupId + json["product_group_id"] = productGroupId + json["vendor"] = "trendyol" + json["sku"] = "p-" + strconv.Itoa(model.ID) + json["product_number"] = strconv.Itoa(model.ID) + json["product_code"] = model.ProductCode + json["name"] = model.Name + json["sellable"] = model.IsSellable + json["favorite_count"] = model.FavoriteCount + json["weight"] = weight + json["name_with_product_code"] = model.NameWithProductCode + json["url_key"] = "https://www.trendyol.com" + model.URL + json["images"] = model.Images + json["brand"] = model.Brand.Name + json["cinsiyet"] = model.Gender.Name + json["description"] = model.Description + json["descriptions"] = model.ContentDescriptions + json["short_description"] = model.Description + // nested structure + json["price"] = make(map[string]interface{}) + json["price"].(map[string]interface{})["originalPrice"] = model.Price.OriginalPrice + json["price"].(map[string]interface{})["sellingPrice"] = model.Price.SellingPrice + json["price"].(map[string]interface{})["discountedPrice"] = model.Price.DiscountedPrice + + attrLen := len(model.Attributes) + + if attrLen != 0 { + for i := 0; i < attrLen; i++ { + attribute := model.Attributes[i] + if attribute.Key.Name == "Renk" { + json["color"] = attribute.Value.Name + } + } + } + + // set categories with value 1 + json["categories"] = []int{1} + + attributes := make([]map[string]string, 0) + for _, attr := range model.Attributes { + var re = regexp.MustCompile(`/[^A-Z0-9]/ig`) + keyStr := re.ReplaceAllString(attr.Key.Name, `_`) + key := strings.ToLower(keyStr) + attribute := map[string]string{ + key: attr.Value.Name, + } + + attributes = append(attributes, attribute) + } + + json["attributes"] = attributes + + var variants []models.Variant + + // if show variants, then it is configurable product. + if model.ShowVariants { + + for i := 0; i < len(model.Variants); i++ { + variant := model.Variants[i] + + if variant.Sellable { + stockType := reflect.TypeOf(variant.Stock) + + if stockType == nil { + variants = append(variants, variant) + } else { + // scrape is via link parse + // we need to parse other variants + + // convert stock to int + stock, ok := variant.Stock.(float64) + + if ok { + if stock > 0 { + variants = append(variants, variant) + } + } + } + } + } + + for i := 0; i < len(model.AllVariants); i++ { + singleVariant := model.AllVariants[i] + + // get the first variant for attribute info + fv := variants[0] + + variant := models.Variant{ + AttributeID: fv.AttributeID, // + AttributeName: fv.AttributeName, // Sample: "Beden" + AttributeType: fv.AttributeType, // Sample: "Size" + AttributeValue: singleVariant.Value, + Price: models.Price{ + ProfitMargin: 0, + DiscountedPrice: struct { + Text string "json:\"text\"" + Value float64 "json:\"value\"" + }{ + Text: fmt.Sprintf("%f", singleVariant.Price), + Value: singleVariant.Price, + }, + SellingPrice: struct { + Text string "json:\"text\"" + Value float64 "json:\"value\"" + }{ + Text: fmt.Sprintf("%f", singleVariant.Price), + Value: singleVariant.Price, + }, + OriginalPrice: struct { + Text string "json:\"text\"" + Value float64 "json:\"value\"" + }{ + Text: fmt.Sprintf("%f", singleVariant.Price), + Value: singleVariant.Price, + }, + Currency: singleVariant.Currency, + }, + ItemNumber: singleVariant.ItemNumber, + Sellable: singleVariant.InStock, + } + + exists := helper.IsVariantsAdded(variants, variant.ItemNumber) + + if !exists { + variants = append(variants, variant) + } + } + + for i := 0; i < len(model.AlternativeVariants); i++ { + alternativeVariant := model.AlternativeVariants[i] + + if len(variants) > 0 { + + // get the first variant for attribute info + fv := variants[0] + + variant := models.Variant{ + AttributeID: fv.AttributeID, // + AttributeName: fv.AttributeName, // Sample: "Beden" + AttributeType: fv.AttributeType, // Sample: "Size" + AttributeValue: alternativeVariant.AttributeValue, + Price: alternativeVariant.Price, + ItemNumber: alternativeVariant.ItemNumber, + Sellable: alternativeVariant.Quantity > 0, + } + exists := helper.IsVariantsAdded(variants, variant.ItemNumber) + + if !exists { + variants = append(variants, variant) + } + } + } + + json["size_variants"] = variants + } + + if model.ShowVariants && len(variants) == 0 { + return nil + } + + return json +} diff --git a/repositories/ProductRepository.go b/repositories/ProductRepository.go new file mode 100644 index 0000000..d4e6026 --- /dev/null +++ b/repositories/ProductRepository.go @@ -0,0 +1,308 @@ +package repositories + +import ( + "fmt" + "math" + gm "sarga_updater/bagisto_models" + models "sarga_updater/trendyol_models" + "strconv" +) + +type ProductRepo struct { + Categories []gm.Category + Brand gm.Brand + Weight float64 + Description string + Keywords string + Data *models.Product + Error error + ColorOption gm.AttributeOption + SexOption gm.AttributeOption + ImageType string +} + +func InitProductRepo(data *models.Product, color, sex gm.AttributeOption) *ProductRepo { + + weight, wError := strconv.ParseFloat(data.Weight, 64) + + if wError != nil || weight == 0 { + weight = 0.5 + } + + var description string + + for _, desc := range data.Descriptions { + description += "

" + desc.Description + "

" + } + + instance := &ProductRepo{ + Weight: weight, + Description: description, + Data: data, + ColorOption: color, + SexOption: sex, + ImageType: "cdn", + } + + return instance +} + +func (pr *ProductRepo) SetCategories(categories []gm.Category) { + pr.Categories = categories + pr.Keywords = pr.Data.Brand + + for _, cat := range categories { + //log.Println(cat) + if len(cat.Translations) > 0 && cat.Translations[0].MetaKeywords != "" { + translation := cat.Translations[0] + + pr.Keywords += "," + translation.MetaKeywords + } + } +} + +func (pr *ProductRepo) HasSizeVariants() bool { + return pr.Data.SizeVariants != nil && len(*pr.Data.SizeVariants) > 0 +} + +func (pr *ProductRepo) HasColorVariants() bool { + return pr.Data.ColorVariants != nil && len(*pr.Data.ColorVariants) > 0 +} + +func (pr *ProductRepo) makeProduct(imp *Importer) gm.Product { + var famID uint = 1 + + if len(imp.families) > 0 { //todo make real fam function + famID = imp.families[0].ID + } + + product := gm.Product{ + Sku: pr.Data.ProductNumber, + AttributeFamilyID: famID, + Categories: pr.Categories, + AttributeValues: pr.getProductAttributes(imp.AttributesMap, pr.Data), + } + + if pr.Data.Brand != "" { + product.BrandID = pr.Brand.ID + } + + if pr.HasSizeVariants() { + product.Type = "configurable" + if (*pr.Data.SizeVariants)[0].AttributeName == "Beden" { + product.SuperAttributes = []gm.Attribute{imp.AttributesMap["size"]} + } else { + product.SuperAttributes = []gm.Attribute{imp.AttributesMap["boyut"]} + product.AttributeFamilyID = 4 // todo fix hardcode + } + + } else { + product.Type = "simple" + price := pr.Data.Price + + if price.OriginalPrice.Value > price.DiscountedPrice.Value { + product.AttributeValues = append(product.AttributeValues, []gm.ProductAttributeValue{ + {AttributeID: imp.AttributesMap["price"].ID, FloatValue: price.OriginalPrice.Value}, + {AttributeID: imp.AttributesMap["special_price"].ID, FloatValue: price.DiscountedPrice.Value}, + }...) + } else { + product.AttributeValues = append(product.AttributeValues, gm.ProductAttributeValue{ + AttributeID: imp.AttributesMap["price"].ID, FloatValue: price.DiscountedPrice.Value, + }) + } + } + + for _, element := range pr.Data.Images { + product.Images = append(product.Images, gm.ProductImage{Type: pr.ImageType, Path: element}) + } + + return product +} + +func (pr *ProductRepo) makeVariant(parentID, famID uint, sku string) gm.Product { + + product := gm.Product{ + Sku: sku, + Type: "simple", + AttributeFamilyID: famID, + Categories: pr.Categories, + ParentID: parentID, + } + + if pr.Data.Brand != "" { + product.BrandID = pr.Brand.ID + } + + return product +} + +func (pr *ProductRepo) makeProductFlat(productId uint) gm.ProductFlat { + + flat := gm.ProductFlat{ + ProductID: productId, + Status: true, + VisibleIndividually: true, + Name: pr.Data.Name, + Sku: pr.Data.ProductNumber, + //ProductNumber: pr.Data.ProductNumber, + Description: pr.Description, + //UrlKey: pr.Data.ProductGroupID, + Weight: pr.Weight, + FavoritesCount: uint(pr.Data.FavoriteCount), + MetaKeywords: pr.Keywords, + MaxPrice: 0, + MinPrice: 0, + Price: 0, + } + + if pr.Data.Color != "" { + flat.Color = int(pr.ColorOption.ID) + flat.ColorLabel = pr.Data.Color + } + + if pr.Data.Brand != "" { + flat.BrandID = pr.Brand.ID + } + + if pr.Data.Cinsiyet != "" { + flat.Cinsiyet = int(pr.SexOption.ID) + flat.CinsiyetLabel = pr.Data.Cinsiyet + } + + if !pr.HasSizeVariants() { + flat.MinPrice = pr.Data.Price.DiscountedPrice.Value + flat.MaxPrice = math.Max(pr.Data.Price.OriginalPrice.Value, pr.Data.Price.DiscountedPrice.Value) + flat.Price = flat.MaxPrice + + if flat.MinPrice < flat.MaxPrice { + flat.SpecialPrice = flat.MinPrice + } + + } + //else {//todo calculate price after variants inserted + // for _, variant := range *pr.Data.SizeVariants { + // price := variant.Price + // + // if flat.MinPrice == 0 || flat.MinPrice > price.DiscountedPrice.Value { + // flat.MaxPrice = price.DiscountedPrice.Value + // } + // + // maxPrice := math.Max(price.OriginalPrice.Value, price.DiscountedPrice.Value) + // if flat.MaxPrice == 0 || flat.MaxPrice < maxPrice { + // flat.MaxPrice = maxPrice + // } + // } + // + //} + + return flat +} + +func (pr *ProductRepo) makeVariantFlat(variant models.Variant, SizID, parentID, productID uint) gm.ProductFlat { + + maxPRice := math.Max(variant.Price.OriginalPrice.Value, variant.Price.DiscountedPrice.Value) + sku := fmt.Sprintf("%s-%d", pr.Data.ProductNumber, variant.ItemNumber) + + flat := gm.ProductFlat{ + ParentID: parentID, + Status: true, + Name: pr.Data.Name, + Sku: sku, + ProductNumber: fmt.Sprintf("%d", variant.ItemNumber), + Weight: pr.Weight, + FavoritesCount: uint(pr.Data.FavoriteCount), + //SizeLabel: variant.AttributeValue, + //Size: int(SizID), + MaxPrice: maxPRice, + MinPrice: variant.Price.DiscountedPrice.Value, + Price: maxPRice, + ProductID: productID, + } + + if variant.AttributeName == "Beden" { + flat.Size = int(SizID) + flat.SizeLabel = variant.AttributeValue + } else { + flat.Boyut = int(SizID) + flat.BoyutLabel = variant.AttributeValue + } + + if flat.MaxPrice > flat.MinPrice { + flat.SpecialPrice = flat.MinPrice + } + + if pr.Data.Color != "" { + flat.Color = int(pr.ColorOption.ID) + flat.ColorLabel = pr.Data.Color + } + + if pr.Data.Brand != "" { + flat.BrandID = pr.Brand.ID + } + + if pr.Data.Cinsiyet != "" { + flat.Cinsiyet = int(pr.SexOption.ID) + flat.CinsiyetLabel = pr.Data.Cinsiyet + } + + return flat +} + +func (pr *ProductRepo) getProductAttributes(AttributesMap map[string]gm.Attribute, product *models.Product) []gm.ProductAttributeValue { + attributes := []gm.ProductAttributeValue{ + {AttributeID: AttributesMap["source"].ID, TextValue: product.URLKey}, + {AttributeID: AttributesMap["favoritesCount"].ID, IntegerValue: product.FavoriteCount}, + {AttributeID: AttributesMap["sku"].ID, TextValue: product.ProductNumber}, + {AttributeID: AttributesMap["name"].ID, TextValue: product.Name, Channel: "default", Locale: "tm"}, + {AttributeID: AttributesMap["weight"].ID, TextValue: product.Weight}, + {AttributeID: AttributesMap["status"].ID, BooleanValue: true}, + {AttributeID: AttributesMap["visible_individually"].ID, BooleanValue: true}, + {AttributeID: AttributesMap["description"].ID, TextValue: pr.Description, Channel: "default", Locale: "tm"}, + {AttributeID: AttributesMap["meta_keywords"].ID, TextValue: pr.Keywords, Channel: "default", Locale: "tm"}, + } + + if product.Color != "" { + attributes = append(attributes, gm.ProductAttributeValue{AttributeID: AttributesMap["color"].ID, IntegerValue: int(pr.ColorOption.ID)}) + } + + if product.Cinsiyet != "" { + attributes = append(attributes, gm.ProductAttributeValue{AttributeID: AttributesMap["cinsiyet"].ID, IntegerValue: int(pr.SexOption.ID)}) + } + + return attributes +} + +func (pr *ProductRepo) getVariantAttributes(AttributesMap map[string]gm.Attribute, product *models.Variant, SizID uint) []gm.ProductAttributeValue { + + price := math.Max(product.Price.OriginalPrice.Value, product.Price.DiscountedPrice.Value) + sku := fmt.Sprintf("%s-%d", pr.Data.ProductNumber, product.ItemNumber) + attributes := []gm.ProductAttributeValue{ + {AttributeID: AttributesMap["source"].ID, TextValue: pr.Data.URLKey}, + {AttributeID: AttributesMap["sku"].ID, TextValue: sku}, //todo unique + {AttributeID: AttributesMap["product_number"].ID, TextValue: fmt.Sprintf("%d", product.ItemNumber)}, //todo unique + {AttributeID: AttributesMap["name"].ID, TextValue: pr.Data.Name, Channel: "default", Locale: "tm"}, + {AttributeID: AttributesMap["weight"].ID, TextValue: pr.Data.Weight}, + {AttributeID: AttributesMap["status"].ID, BooleanValue: true}, + //{AttributeID: AttributesMap["size"].ID, IntegerValue: int(SizID)}, + {AttributeID: AttributesMap["price"].ID, FloatValue: price}, + } + if product.AttributeName == "Beden" { + attributes = append(attributes, gm.ProductAttributeValue{AttributeID: AttributesMap["size"].ID, IntegerValue: int(SizID)}) + } else { + attributes = append(attributes, gm.ProductAttributeValue{AttributeID: AttributesMap["boyut"].ID, IntegerValue: int(SizID)}) + } + + if pr.Data.Color != "" { + attributes = append(attributes, gm.ProductAttributeValue{AttributeID: AttributesMap["color"].ID, IntegerValue: int(pr.ColorOption.ID)}) + } + + if pr.Data.Cinsiyet != "" { + attributes = append(attributes, gm.ProductAttributeValue{AttributeID: AttributesMap["cinsiyet"].ID, IntegerValue: int(pr.SexOption.ID)}) + } + + if product.Price.OriginalPrice.Value > product.Price.DiscountedPrice.Value { + attributes = append(attributes, gm.ProductAttributeValue{AttributeID: AttributesMap["special_price"].ID, FloatValue: product.Price.DiscountedPrice.Value}) + } + + return attributes +} diff --git a/trendyol_models/category.go b/trendyol_models/category.go new file mode 100644 index 0000000..4bc2558 --- /dev/null +++ b/trendyol_models/category.go @@ -0,0 +1,30 @@ +package trendyol_models + +type Category struct { + ID int `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + TrendyolURL string `json:"trendyol_url"` + LcwURL string `json:"lcw_url"` + DisplayMode string `json:"display_mode"` + ImageURL interface{} `json:"image_url"` + CategoryIconPath interface{} `json:"category_icon_path"` + ProductLimit int `json:"product_limit"` + Children []Category +} + +type BagistoResponse struct { + Data []Category +} +type CouchCategory struct { + Rev string `json:"_rev"` + CreatedAt string `json:"createdAt"` + ID string `json:"id"` + Name string `json:"name"` + Order string `json:"order"` + ParentID string `json:"parent_id"` + SargaID string `json:"sarga_id"` + Slug string `json:"slug"` + UpdatedAt string `json:"updatedAt"` + Weight string `json:"weight"` +} diff --git a/trendyol_models/product.go b/trendyol_models/product.go new file mode 100644 index 0000000..aadf435 --- /dev/null +++ b/trendyol_models/product.go @@ -0,0 +1,309 @@ +package trendyol_models + +type Product struct { + Attributes []map[string]string `json:"attributes"` + Brand string `json:"brand"` + Categories []int `json:"categories"` + Cinsiyet string `json:"cinsiyet"` + Color string `json:"color"` + ColorVariantCount int `json:"color_variant_count"` + ColorVariants *[]Product `json:"color_variants"` + Description string `json:"description"` + IsSellable bool `json:"sellable"` + FavoriteCount int `json:"favorite_count"` + Descriptions []struct { + Description string `json:"description"` + Bold bool `json:"bold"` + } `json:"descriptions"` + Images []string `json:"images"` + Name string `json:"name"` + NameWithProductCode string `json:"name_with_product_code"` + Price Price `json:"price"` + ProductCode string `json:"product_code"` + ProductGroupID string `json:"product_group_id"` + ProductNumber string `json:"product_number"` + ShortDescription string `json:"short_description"` + SizeVariants *[]Variant `json:"size_variants"` + Sku string `json:"sku"` + Stock interface{} `json:"stock"` + URLKey string `json:"url_key"` + Vendor string `json:"vendor"` + Weight string `json:"weight"` +} + +type Price struct { + ProfitMargin int `json:"profitMargin"` + DiscountedPrice PriceValue `json:"discountedPrice"` + SellingPrice PriceValue `json:"sellingPrice"` + OriginalPrice PriceValue `json:"originalPrice"` + Currency string `json:"currency"` +} + +type PriceValue struct { + Text string `json:"text"` + Value float64 `json:"value"` +} + +type Variant struct { + AttributeID int `json:"attributeId"` + AttributeName string `json:"attributeName"` + AttributeType string `json:"attributeType"` + AttributeValue string `json:"attributeValue"` + Stamps []interface{} `json:"stamps"` + Price Price `json:"price"` + FulfilmentType string `json:"fulfilmentType"` + AttributeBeautifiedValue string `json:"attributeBeautifiedValue"` + IsWinner bool `json:"isWinner"` + ListingID string `json:"listingId"` + Stock interface{} `json:"stock"` + Sellable bool `json:"sellable"` + AvailableForClaim bool `json:"availableForClaim"` + Barcode string `json:"barcode"` + ItemNumber int `json:"itemNumber"` + DiscountedPriceInfo string `json:"discountedPriceInfo"` + HasCollectable bool `json:"hasCollectable"` + RushDeliveryMerchantListingExist bool `json:"rushDeliveryMerchantListingExist"` + LowerPriceMerchantListingExist bool `json:"lowerPriceMerchantListingExist"` +} +type Row struct { + Key string `json:"key"` + Value map[string]interface{} `json:"value"` + Doc Product +} +type BagistoModelResponse struct { + Rows []Row `json:"rows"` +} + +type TrendyolProductDetailResponse struct { + IsSuccess bool `json:"isSuccess"` + StatusCode int `json:"statusCode"` + Error interface{} `json:"error"` + Result TrendyolProductDetailModel `json:"result"` + Headers struct { + Tysidecarcachable string `json:"tysidecarcachable"` + } `json:"headers"` +} + +type TrendyolProductDetailModel struct { + AlternativeVariants []AlternativeVariant `json:"alternativeVariants"` + Attributes []struct { + Key struct { + Name string `json:"name"` + ID int `json:"id"` + } `json:"key"` + Value struct { + Name string `json:"name"` + ID int `json:"id"` + } `json:"value"` + Starred bool `json:"starred"` + } `json:"attributes"` + Variants []Variant `json:"variants"` + OtherMerchants []interface{} `json:"otherMerchants"` + Campaign struct { + ID int `json:"id"` + Name string `json:"name"` + StartDate string `json:"startDate"` + EndDate string `json:"endDate"` + IsMultipleSupplied bool `json:"isMultipleSupplied"` + StockTypeID int `json:"stockTypeId"` + URL string `json:"url"` + ShowTimer bool `json:"showTimer"` + } `json:"campaign"` + Category struct { + ID int `json:"id"` + Name string `json:"name"` + Hierarchy string `json:"hierarchy"` + Refundable bool `json:"refundable"` + BeautifiedName string `json:"beautifiedName"` + IsVASEnabled bool `json:"isVASEnabled"` + } `json:"category"` + Brand struct { + IsVirtual bool `json:"isVirtual"` + BeautifiedName string `json:"beautifiedName"` + ID int `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + } `json:"brand"` + Color string `json:"color"` + MetaBrand struct { + ID int `json:"id"` + Name string `json:"name"` + BeautifiedName string `json:"beautifiedName"` + IsVirtual bool `json:"isVirtual"` + Path string `json:"path"` + } `json:"metaBrand"` + ShowVariants bool `json:"showVariants"` + ShowSexualContent bool `json:"showSexualContent"` + BrandCategoryBanners []interface{} `json:"brandCategoryBanners"` + AllVariants []struct { + ItemNumber int `json:"itemNumber"` + Value string `json:"value"` + InStock bool `json:"inStock"` + Currency string `json:"currency"` + Barcode string `json:"barcode"` + Price float64 `json:"price"` + } `json:"allVariants"` + OtherMerchantVariants []interface{} `json:"otherMerchantVariants"` + InstallmentBanner interface{} `json:"installmentBanner"` + IsVasEnabled bool `json:"isVasEnabled"` + OriginalCategory struct { + ID int `json:"id"` + Name string `json:"name"` + Hierarchy string `json:"hierarchy"` + Refundable bool `json:"refundable"` + BeautifiedName string `json:"beautifiedName"` + IsVASEnabled bool `json:"isVASEnabled"` + } `json:"originalCategory"` + Landings []interface{} `json:"landings"` + ID int `json:"id"` + ProductCode string `json:"productCode"` + Name string `json:"name"` + NameWithProductCode string `json:"nameWithProductCode"` + Description string `json:"description"` + ContentDescriptions []struct { + Description string `json:"description"` + Bold bool `json:"bold"` + } `json:"contentDescriptions"` + ProductGroupID int `json:"productGroupId"` + Tax int `json:"tax"` + BusinessUnit string `json:"businessUnit"` + MaxInstallment int `json:"maxInstallment"` + Gender struct { + Name string `json:"name"` + ID int `json:"id"` + } `json:"gender"` + URL string `json:"url"` + Images []string `json:"images"` + IsSellable bool `json:"isSellable"` + IsBasketDiscount bool `json:"isBasketDiscount"` + HasStock bool `json:"hasStock"` + Price Price `json:"price"` + IsFreeCargo bool `json:"isFreeCargo"` + Promotions []struct { + PromotionRemainingTime string `json:"promotionRemainingTime"` + Type int `json:"type"` + Text string `json:"text"` + ID int `json:"id"` + Link string `json:"link"` + } `json:"promotions"` + Merchant struct { + IsSearchableMerchant bool `json:"isSearchableMerchant"` + Stickers []interface{} `json:"stickers"` + ID int `json:"id"` + Name string `json:"name"` + OfficialName string `json:"officialName"` + CityName string `json:"cityName"` + TaxNumber string `json:"taxNumber"` + SellerScore float64 `json:"sellerScore"` + SellerScoreColor string `json:"sellerScoreColor"` + DeliveryProviderName string `json:"deliveryProviderName"` + SellerLink string `json:"sellerLink"` + } `json:"merchant"` + DeliveryInformation struct { + IsRushDelivery bool `json:"isRushDelivery"` + DeliveryDate string `json:"deliveryDate"` + } `json:"deliveryInformation"` + CargoRemainingDays int `json:"cargoRemainingDays"` + IsMarketplace bool `json:"isMarketplace"` + ProductStamps []struct { + Type string `json:"type"` + ImageURL string `json:"imageUrl"` + Position string `json:"position"` + AspectRatio float64 `json:"aspectRatio"` + Priority int `json:"priority"` + PriceTagStamp bool `json:"priceTagStamp,omitempty"` + } `json:"productStamps"` + HasHTMLContent bool `json:"hasHtmlContent"` + FavoriteCount int `json:"favoriteCount"` + UxLayout string `json:"uxLayout"` + IsDigitalGood bool `json:"isDigitalGood"` + IsRunningOut bool `json:"isRunningOut"` + ScheduledDelivery bool `json:"scheduledDelivery"` + RatingScore struct { + AverageRating float64 `json:"averageRating"` + TotalRatingCount int `json:"totalRatingCount"` + TotalCommentCount int `json:"totalCommentCount"` + } `json:"ratingScore"` + ShowStarredAttributes bool `json:"showStarredAttributes"` + ReviewsURL string `json:"reviewsUrl"` + QuestionsURL string `json:"questionsUrl"` + SellerQuestionEnabled bool `json:"sellerQuestionEnabled"` + SizeExpectationAvailable bool `json:"sizeExpectationAvailable"` + CrossPromotionAward struct { + AwardType interface{} `json:"awardType"` + AwardValue interface{} `json:"awardValue"` + ContentID int `json:"contentId"` + MerchantID int `json:"merchantId"` + } `json:"crossPromotionAward"` + RushDeliveryMerchantListingExist bool `json:"rushDeliveryMerchantListingExist"` + LowerPriceMerchantListingExist bool `json:"lowerPriceMerchantListingExist"` + ShowValidFlashSales bool `json:"showValidFlashSales"` + ShowExpiredFlashSales bool `json:"showExpiredFlashSales"` + WalletRebate struct { + MinPrice int `json:"minPrice"` + MaxPrice int `json:"maxPrice"` + RebateRatio float64 `json:"rebateRatio"` + } `json:"walletRebate"` + IsArtWork bool `json:"isArtWork"` +} + +type TrendyolProductVariantsResponse struct { + IsSuccess bool `json:"isSuccess"` + StatusCode int `json:"statusCode"` + Error interface{} `json:"error"` + Result struct { + SlicingAttributes []struct { + Brand struct { + BeautifiedName string `json:"beautifiedName"` + ID int `json:"id"` + Name string `json:"name"` + IsVirtual bool `json:"isVirtual"` + Path string `json:"path"` + } `json:"brand"` + Attributes []struct { + Contents []struct { + URL string `json:"url"` + ID int `json:"id"` + ImageURL string `json:"imageUrl"` + Name string `json:"name"` + Price struct { + DiscountedPrice struct { + Text string `json:"text"` + Value float64 `json:"value"` + } `json:"discountedPrice"` + OriginalPrice struct { + Text string `json:"text"` + Value float64 `json:"value"` + } `json:"originalPrice"` + SellingPrice struct { + Text string `json:"text"` + Value float64 `json:"value"` + } `json:"sellingPrice"` + } `json:"price"` + } `json:"contents"` + Name string `json:"name"` + BeautifiedName string `json:"beautifiedName"` + } `json:"attributes"` + Type string `json:"type"` + DisplayName string `json:"displayName"` + Order int `json:"order"` + DisplayType int `json:"displayType"` + } `json:"slicingAttributes"` + } `json:"result"` + Headers struct { + Tysidecarcachable string `json:"tysidecarcachable"` + } `json:"headers"` +} +type AlternativeVariant struct { + AttributeValue string `json:"attributeValue"` + AttributeBeautifiedValue string `json:"attributeBeautifiedValue"` + CampaignID int `json:"campaignId"` + MerchantID int `json:"merchantId"` + URLQuery string `json:"urlQuery"` + ListingID string `json:"listingId"` + ItemNumber int `json:"itemNumber"` + Barcode string `json:"barcode"` + Stock interface{} `json:"stock"` + Quantity int `json:"quantity"` + Price Price `json:"price"` +} diff --git a/trendyol_models/size_variant_model.go b/trendyol_models/size_variant_model.go new file mode 100644 index 0000000..6ec2177 --- /dev/null +++ b/trendyol_models/size_variant_model.go @@ -0,0 +1,7 @@ +package trendyol_models + +type SizeVariants struct { + Price float64 `json:"price"` + Size string `json:"size"` + Stock int `json:"stock"` +}