diff --git a/services/external/weni/service.go b/services/external/weni/service.go index f68d294fd..02e93c06c 100644 --- a/services/external/weni/service.go +++ b/services/external/weni/service.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "fmt" - "io" "net/http" "net/url" "strconv" @@ -114,7 +113,7 @@ func (s *service) Call(session flows.Session, params assets.MsgCatalogParam, log var trace *httpx.Trace var traces []*httpx.Trace var sellerID string - var allProducts []string + existingProductsIds := []string{} qttProducts := 5 postalCode_ := strings.TrimSpace(params.PostalCode) @@ -143,9 +142,8 @@ func (s *service) Call(session flows.Session, params assets.MsgCatalogParam, log searchResult, trace, err = GetProductListFromSentenX(product, catalog.FacebookCatalogID(), searchThreshold, s.rtConfig) callResult.Traces = append(callResult.Traces, trace) } else if params.SearchType == "vtex" { - searchResult, searchResultSponsored, traces, err = GetProductListFromVtex(product, params.SearchUrl, params.ApiType, catalog.FacebookCatalogID(), s.rtConfig, hasVtexAds, hideUnavailableItems) + searchResult, searchResultSponsored, traces, err = GetProductListFromVtex(product, params.SearchUrl, params.ApiType, catalog.FacebookCatalogID(), s.rtConfig, hasVtexAds, hideUnavailableItems, sellerID) callResult.Traces = append(callResult.Traces, traces...) - allProducts = append(allProducts, searchResult...) if searchResult == nil { continue } @@ -174,25 +172,47 @@ func (s *service) Call(session flows.Session, params assets.MsgCatalogParam, log if len(searchResultSponsored) > 0 { hasSponsored = true - allProductsSponsored[0].ProductRetailerIDs = append(allProductsSponsored[0].ProductRetailerIDs, searchResultSponsored+"#"+sellerID) + allProductsSponsored[0].ProductRetailerIDs = append(allProductsSponsored[0].ProductRetailerIDs, searchResultSponsored) } - } callResult.ProductRetailerIDS = productEntries - existingProductsIds := []string{} - + // simulates cart in VTEX with all products hasSimulation := false if postalCode_ != "" && sellerID != "1" { + var tracesSimulation []*httpx.Trace hasSimulation = true - existingProductsIds, trace, err = CartSimulation(allProducts, sellerID, params.SearchUrl, postalCode_) - callResult.Traces = append(callResult.Traces, trace) + existingProductsIds, tracesSimulation, err = CartSimulation(callResult.ProductRetailerIDS, sellerID, params.SearchUrl, postalCode_) + callResult.Traces = append(callResult.Traces, tracesSimulation...) if err != nil { return callResult, err } } + // adds '#sellerID' formatting to the end of all retailer IDs + for _, productEntry := range callResult.ProductRetailerIDS { + for i, retailerID := range productEntry.ProductRetailerIDs { + productEntry.ProductRetailerIDs[i] = retailerID + "#" + sellerID + } + } + + // search for products in Meta + retries := 2 + var productSections []flows.ProductEntry + var tracesMeta []*httpx.Trace + for i := 0; i < retries; i++ { + productSections, tracesMeta, err = ProductsSearchMeta(callResult.ProductRetailerIDS, fmt.Sprint(catalog.FacebookCatalogID()), s.rtConfig.WhatsappSystemUserToken) + callResult.Traces = append(callResult.Traces, tracesMeta...) + if err != nil { + continue + } + break + } + if err != nil { + return callResult, err + } + finalResult := &flows.MsgCatalogCall{} finalResult.Traces = callResult.Traces finalResult.ResponseJSON = callResult.ResponseJSON @@ -200,21 +220,26 @@ func (s *service) Call(session flows.Session, params assets.MsgCatalogParam, log finalResult.ProductRetailerIDS = allProductsSponsored } - for _, productEntry := range callResult.ProductRetailerIDS { + // checks available products and limits to 5 per section + for _, productEntry := range productSections { newEntry := productEntry newEntry.ProductRetailerIDs = []string{} for _, productRetailerID := range productEntry.ProductRetailerIDs { if hasSimulation { for _, existingProductId := range existingProductsIds { - if productRetailerID == existingProductId { - if len(newEntry.ProductRetailerIDs) < qttProducts { - newEntry.ProductRetailerIDs = append(newEntry.ProductRetailerIDs, productRetailerID+"#"+sellerID) + if productRetailerID == existingProductId+"#"+sellerID { + _, exists := productRetailerIDMap[productRetailerID] + if !exists { + if len(newEntry.ProductRetailerIDs) < qttProducts { + newEntry.ProductRetailerIDs = append(newEntry.ProductRetailerIDs, productRetailerID) + } + productRetailerIDMap[productRetailerID] = struct{}{} } } } } else { if len(newEntry.ProductRetailerIDs) < qttProducts { - newEntry.ProductRetailerIDs = append(newEntry.ProductRetailerIDs, productRetailerID+"#"+sellerID) + newEntry.ProductRetailerIDs = append(newEntry.ProductRetailerIDs, productRetailerID) } } } @@ -224,22 +249,6 @@ func (s *service) Call(session flows.Session, params assets.MsgCatalogParam, log } } - retries := 2 - var newProductRetailerIDS []flows.ProductEntry - var tracesMeta []*httpx.Trace - for i := 0; i < retries; i++ { - newProductRetailerIDS, tracesMeta, err = ProductsSearchMeta(finalResult.ProductRetailerIDS, fmt.Sprint(catalog.FacebookCatalogID()), s.rtConfig.WhatsappSystemUserToken) - finalResult.Traces = append(finalResult.Traces, tracesMeta...) - if err != nil { - continue - } - break - } - if err != nil { - return finalResult, err - } - finalResult.ProductRetailerIDS = newProductRetailerIDS - return finalResult, nil } @@ -343,7 +352,7 @@ func GetProductListFromChatGPT(ctx context.Context, rtConfig *runtime.Config, co return products["products"], trace, nil } -func GetProductListFromVtex(productSearch string, searchUrl string, apiType string, catalog string, rt *runtime.Config, hasVtexAds bool, hideUnavailableItems bool) ([]string, string, []*httpx.Trace, error) { +func GetProductListFromVtex(productSearch string, searchUrl string, apiType string, catalog string, rt *runtime.Config, hasVtexAds bool, hideUnavailableItems bool, sellerID string) ([]string, string, []*httpx.Trace, error) { var result []string var traces []*httpx.Trace var err error @@ -368,13 +377,11 @@ func GetProductListFromVtex(productSearch string, searchUrl string, apiType stri } productRetailerIDS := []string{} - productRetailerIDMap := make(map[string]struct{}) var productEntries []flows.ProductEntry var productEntry flows.ProductEntry for _, productRetailerID := range resultSponsored { - productRetailerIDS = append(productRetailerIDS, productRetailerID) - productRetailerIDMap[productRetailerID] = struct{}{} + productRetailerIDS = append(productRetailerIDS, productRetailerID+"#"+sellerID) } if len(productRetailerIDS) > 0 { @@ -383,7 +390,6 @@ func GetProductListFromVtex(productSearch string, searchUrl string, apiType stri ProductRetailerIDs: productRetailerIDS, } productEntries = append(productEntries, productEntry) - productRetailerIDS = nil retries := 2 var newProductRetailerIDS []flows.ProductEntry @@ -407,16 +413,18 @@ func GetProductListFromVtex(productSearch string, searchUrl string, apiType stri } type SearchSeller struct { - Items []struct { - ID string `json:"id"` - Quantity int `json:"quantity"` - Seller string `json:"seller"` - Availability string `json:"availability,omitempty"` - } `json:"items"` + Items []Item `json:"items"` PostalCode string `json:"postalCode"` Country string `json:"country"` } +type Item struct { + ID string `json:"id"` + Quantity int `json:"quantity"` + Seller string `json:"seller"` + Availability string `json:"availability,omitempty"` +} + type VtexProduct struct { ItemId string `json:"itemId"` } @@ -601,75 +609,90 @@ func VtexSponsoredSearch(searchUrl string, productSearch string, hideUnavailable } -func CartSimulation(products []string, sellerID string, url string, postalCode string) ([]string, *httpx.Trace, error) { - var trace *httpx.Trace - var body SearchSeller - result := []string{} +func CartSimulation(ProductRetailerIDS []flows.ProductEntry, sellerID string, url string, postalCode string) ([]string, []*httpx.Trace, error) { + const batchSize = 300 + var traces []*httpx.Trace + var availableProducts []string + var products []string - for _, product := range products { - product_retailer_id := product + urlSplit := strings.Split(url, "api") + urlSimulation := urlSplit[0] + "api/checkout/pub/orderForms/simulation" - body.Items = append(body.Items, struct { - ID string "json:\"id\"" - Quantity int "json:\"quantity\"" - Seller string "json:\"seller\"" - Availability string "json:\"availability,omitempty\"" - }{ID: product_retailer_id, Quantity: 1, Seller: sellerID}) + for _, p := range ProductRetailerIDS { + products = append(products, p.ProductRetailerIDs...) } - if len(body.Items) > 0 { - - if postalCode != "" { - body.PostalCode = postalCode - body.Country = "BRA" + for i := 0; i < len(products); i += batchSize { + end := i + batchSize + if end > len(products) { + end = len(products) } + batchProducts := products[i:end] - urlSplit := strings.Split(url, "api") - - urlSimulation := urlSplit[0] + "api/checkout/pub/orderForms/simulation" - - headers := map[string]string{ - "Accept": "application/json", + searchSeller := SearchSeller{ + Items: []Item{}, } - var b io.Reader - data, err := jsonx.Marshal(body) - if err != nil { - return nil, trace, err + if postalCode != "" { + searchSeller.PostalCode = postalCode + searchSeller.Country = "BRA" } - b = bytes.NewReader(data) - headers["Content-Type"] = "application/json" - req, err := httpx.NewRequest("POST", urlSimulation, b, headers) - if err != nil { - return nil, trace, err + + for _, product := range batchProducts { + searchSeller.Items = append(searchSeller.Items, Item{ID: product, Quantity: 1, Seller: sellerID}) } - client := &http.Client{} - trace, err := httpx.DoTrace(client, req, nil, nil, -1) + batchAvailableProducts, trace, err := sendBatchRequest(searchSeller, urlSimulation) + traces = append(traces, trace) if err != nil { - return nil, trace, err + return nil, traces, err } - if trace.Response.StatusCode >= 400 { - return nil, trace, fmt.Errorf("error when searching with seller: status code %d", trace.Response.StatusCode) - } + availableProducts = append(availableProducts, batchAvailableProducts...) + } - response := &SearchSeller{} + return availableProducts, traces, nil +} - err = json.Unmarshal(trace.ResponseBody, response) - if err != nil { - return nil, trace, err - } +func sendBatchRequest(body SearchSeller, url string) ([]string, *httpx.Trace, error) { + client := &http.Client{} - availableProducts := []string{} - for _, item := range response.Items { - if item.Availability == "available" { - availableProducts = append(availableProducts, item.ID) - } + headers := map[string]string{ + "Accept": "application/json", + } + + data, err := jsonx.Marshal(body) + if err != nil { + return nil, nil, err + } + + req, err := httpx.NewRequest("POST", url, bytes.NewReader(data), headers) + if err != nil { + return nil, nil, err + } + + trace, err := httpx.DoTrace(client, req, nil, nil, -1) + if err != nil { + return nil, trace, err + } + + if trace.Response.StatusCode >= 400 { + return nil, trace, fmt.Errorf("error when searching with seller: status code %d", trace.Response.StatusCode) + } + + response := &SearchSeller{} + err = json.Unmarshal(trace.ResponseBody, response) + if err != nil { + return nil, trace, err + } + + availableProducts := []string{} + for _, item := range response.Items { + if item.Availability == "available" { + availableProducts = append(availableProducts, item.ID) } - return availableProducts, trace, nil } - return result, trace, nil + return availableProducts, trace, nil } // Filter represents the structure of the filter for the API request @@ -733,6 +756,7 @@ func fetchProducts(url string) (*Response, *httpx.Trace, error) { } t, err := httpx.DoTrace(client, req, nil, nil, -1) + t.Request.URL = truncateURL(t.Request.URL) if err != nil { return nil, t, err } @@ -752,9 +776,24 @@ func fetchProducts(url string) (*Response, *httpx.Trace, error) { } func ProductsSearchMeta(productEntryList []flows.ProductEntry, catalog string, whatsappSystemUserToken string) ([]flows.ProductEntry, []*httpx.Trace, error) { + const batchSize = 15 + validProductIds := []string{} traces := []*httpx.Trace{} - for i, productEntry := range productEntryList { - filter, err := createFilter(productEntry.ProductRetailerIDs) + allIds := []string{} + for _, productEntry := range productEntryList { + allIds = append(allIds, productEntry.ProductRetailerIDs...) + } + + newProductEntryList := []flows.ProductEntry{} + + for i := 0; i < len(allIds); i += batchSize { + end := i + batchSize + if end > len(allIds) { + end = len(allIds) + } + + batchIds := allIds[i:end] + filter, err := createFilter(batchIds) if err != nil { return nil, nil, err } @@ -764,6 +803,7 @@ func ProductsSearchMeta(productEntryList []flows.ProductEntry, catalog string, w params.Add("summary", "true") params.Add("access_token", whatsappSystemUserToken) params.Add("filter", filter) + url_ := fmt.Sprintf("https://graph.facebook.com/v14.0/%s/products?%s", catalog, params.Encode()) response, trace, err := fetchProducts(url_) @@ -772,22 +812,24 @@ func ProductsSearchMeta(productEntryList []flows.ProductEntry, catalog string, w return nil, traces, err } - var productRetailerIDs []string - - // Process the data - for _, product := range response.Data { - productRetailerIDs = append(productRetailerIDs, product.RetailerID) + for _, id := range response.Data { + validProductIds = append(validProductIds, id.RetailerID) } - productEntryList[i].ProductRetailerIDs = productRetailerIDs } - newProductEntryList := []flows.ProductEntry{} - for _, productEntry := range productEntryList { - if len(productEntry.ProductRetailerIDs) > 0 { - newProductEntryList = append(newProductEntryList, productEntry) + + for i, productEntry := range productEntryList { + newProductEntryList = append(newProductEntryList, flows.ProductEntry{Product: productEntry.Product, ProductRetailerIDs: []string{}}) + for _, retailerId := range productEntry.ProductRetailerIDs { + for _, id := range validProductIds { + if retailerId == id { + newProductEntryList[i].ProductRetailerIDs = append(newProductEntryList[i].ProductRetailerIDs, id) + break + } + } } } - return newProductEntryList, traces, nil + return newProductEntryList, traces, nil } var languages = map[string]string{ @@ -795,3 +837,16 @@ var languages = map[string]string{ "por": "Você também pode gostar:", "spa": "También te puede interesar:", } + +func truncateURL(u *url.URL) *url.URL { + const maxLength = 2048 + if len(u.String()) > maxLength { + excessLength := len(u.String()) - maxLength + if excessLength < len(u.RawQuery) { + u.RawQuery = u.RawQuery[:len(u.RawQuery)-excessLength] + } else { + u.RawQuery = "" + } + } + return u +}