flamingo.me/flamingo-commerce/v3@v3.11.0/w3cdatalayer/application/factory.go (about)

     1  package application
     2  
     3  import (
     4  	"context"
     5  	"crypto/sha512"
     6  	"encoding/base64"
     7  	"encoding/hex"
     8  	"net/url"
     9  	"regexp"
    10  	"strconv"
    11  	"strings"
    12  
    13  	customerDomain "flamingo.me/flamingo-commerce/v3/customer/domain"
    14  	"flamingo.me/flamingo/v3/core/auth"
    15  	"flamingo.me/flamingo/v3/framework/flamingo"
    16  	"flamingo.me/flamingo/v3/framework/web"
    17  	"github.com/pkg/errors"
    18  	"go.opencensus.io/tag"
    19  
    20  	"flamingo.me/flamingo-commerce/v3/cart/domain/decorator"
    21  	productDomain "flamingo.me/flamingo-commerce/v3/product/domain"
    22  	"flamingo.me/flamingo-commerce/v3/w3cdatalayer/domain"
    23  )
    24  
    25  type (
    26  	// Factory is used to build new datalayers
    27  	Factory struct {
    28  		router                  *web.Router
    29  		logger                  flamingo.Logger
    30  		datalayerProvider       domain.DatalayerProvider
    31  		webIdentityService      *auth.WebIdentityService
    32  		customerIdentityService customerDomain.CustomerIdentityService
    33  		hashEncoder             encoder
    34  
    35  		pageInstanceIDPrefix           string
    36  		pageInstanceIDStage            string
    37  		productMediaBaseURL            string
    38  		productMediaURLPrefix          string
    39  		productMediaThumbnailURLPrefix string
    40  		pageNamePrefix                 string
    41  		siteName                       string
    42  		locale                         string
    43  		defaultCurrency                string
    44  		hashUserValues                 bool
    45  		regex                          *regexp.Regexp
    46  	}
    47  
    48  	// hexEncoder is a wrapper for hex.EncodeToString
    49  	hexEncoder struct{}
    50  
    51  	encoder interface {
    52  		EncodeToString(src []byte) string
    53  	}
    54  )
    55  
    56  func (h *hexEncoder) EncodeToString(src []byte) string {
    57  	return hex.EncodeToString(src)
    58  }
    59  
    60  // Inject factory dependencies
    61  func (s *Factory) Inject(
    62  	router2 *web.Router,
    63  	logger flamingo.Logger,
    64  	provider domain.DatalayerProvider,
    65  	webIdentityService *auth.WebIdentityService,
    66  	customerIdentityService customerDomain.CustomerIdentityService,
    67  	config *struct {
    68  		PageInstanceIDPrefix           string `inject:"config:commerce.w3cDatalayer.pageInstanceIDPrefix,optional"`
    69  		PageInstanceIDStage            string `inject:"config:commerce.w3cDatalayer.pageInstanceIDStage,optional"`
    70  		ProductMediaBaseURL            string `inject:"config:commerce.w3cDatalayer.productMediaBaseUrl,optional"`
    71  		ProductMediaURLPrefix          string `inject:"config:commerce.w3cDatalayer.productMediaUrlPrefix,optional"`
    72  		ProductMediaThumbnailURLPrefix string `inject:"config:commerce.w3cDatalayer.productMediaThumbnailUrlPrefix,optional"`
    73  		PageNamePrefix                 string `inject:"config:commerce.w3cDatalayer.pageNamePrefix,optional"`
    74  		SiteName                       string `inject:"config:commerce.w3cDatalayer.siteName,optional"`
    75  		Locale                         string `inject:"config:locale.locale,optional"`
    76  		DefaultCurrency                string `inject:"config:commerce.w3cDatalayer.defaultCurrency,optional"`
    77  		HashUserValues                 bool   `inject:"config:commerce.w3cDatalayer.hashUserValues,optional"`
    78  		HashEncoding                   string `inject:"config:commerce.w3cDatalayer.hashEncoding,optional"`
    79  	},
    80  ) {
    81  	s.router = router2
    82  	s.logger = logger.WithField(flamingo.LogKeyModule, "w3cdatalayer")
    83  	s.datalayerProvider = provider
    84  	s.webIdentityService = webIdentityService
    85  	s.customerIdentityService = customerIdentityService
    86  
    87  	s.pageInstanceIDPrefix = config.PageInstanceIDPrefix
    88  	s.pageInstanceIDStage = config.PageInstanceIDStage
    89  	s.productMediaBaseURL = config.ProductMediaBaseURL
    90  	s.productMediaURLPrefix = config.ProductMediaURLPrefix
    91  	s.productMediaThumbnailURLPrefix = config.ProductMediaThumbnailURLPrefix
    92  	s.pageNamePrefix = config.PageNamePrefix
    93  	s.siteName = config.SiteName
    94  	s.locale = config.Locale
    95  	s.defaultCurrency = config.DefaultCurrency
    96  	s.hashUserValues = config.HashUserValues
    97  
    98  	if config.HashUserValues {
    99  		switch config.HashEncoding {
   100  		case "base64url":
   101  			s.hashEncoder = base64.URLEncoding
   102  		case "hex":
   103  			s.hashEncoder = &hexEncoder{}
   104  		default:
   105  			s.logger.Warn("invalid configuration for commerce.w3cDatalayer.hashEncoding, using base64url encoding as fallback")
   106  			s.hashEncoder = base64.URLEncoding
   107  		}
   108  	}
   109  
   110  	regexString := "[,|;|\\|]"
   111  	r, err := regexp.Compile(regexString)
   112  	if err != nil {
   113  		panic(err)
   114  	}
   115  	s.regex = r
   116  }
   117  
   118  // BuildForCurrentRequest builds the datalayer for the current request
   119  func (s Factory) BuildForCurrentRequest(ctx context.Context, request *web.Request) domain.Datalayer {
   120  	layer := s.datalayerProvider()
   121  
   122  	// get language from locale code configuration
   123  	language := ""
   124  	localeParts := strings.Split(s.locale, "-")
   125  	if len(localeParts) > 0 {
   126  		language = localeParts[0]
   127  	}
   128  
   129  	baseURL, err := s.router.Absolute(request, request.Request().URL.Path, nil)
   130  	if err != nil {
   131  		s.logger.Warn(errors.Wrap(err, "cannot build absolute url"))
   132  		baseURL = new(url.URL)
   133  	}
   134  	layer.Page = &domain.Page{
   135  		PageInfo: domain.PageInfo{
   136  			PageID:         request.Request().URL.Path,
   137  			PageName:       s.pageNamePrefix + request.Request().URL.Path,
   138  			DestinationURL: baseURL.String(),
   139  			Language:       language,
   140  		},
   141  		Attributes: make(map[string]interface{}),
   142  	}
   143  
   144  	layer.Page.Attributes["currency"] = s.defaultCurrency
   145  
   146  	// Use the handler name as PageId if available
   147  	if controllerHandler, ok := tag.FromContext(ctx).Value(web.ControllerKey); ok {
   148  		layer.Page.PageInfo.PageID = controllerHandler
   149  	}
   150  
   151  	layer.SiteInfo = &domain.SiteInfo{
   152  		SiteName: s.siteName,
   153  	}
   154  
   155  	layer.PageInstanceID = s.pageInstanceIDPrefix + s.pageInstanceIDStage
   156  
   157  	// Handle User
   158  	layer.Page.Attributes["loggedIn"] = false
   159  	layer.Page.Attributes["logintype"] = "guest"
   160  
   161  	identity := s.webIdentityService.Identify(ctx, request)
   162  	if identity != nil {
   163  		// logged in
   164  		layer.Page.Attributes["loggedIn"] = true
   165  		layer.Page.Attributes["logintype"] = "external"
   166  		userData := s.getUserFromIdentity(ctx, identity)
   167  		if userData != nil {
   168  			layer.User = append(layer.User, *userData)
   169  		}
   170  	}
   171  
   172  	return *layer
   173  }
   174  
   175  func (s Factory) getUserFromIdentity(ctx context.Context, identity auth.Identity) *domain.User {
   176  	if identity == nil {
   177  		return nil
   178  	}
   179  
   180  	customer, err := s.customerIdentityService.GetByIdentity(ctx, identity)
   181  	if err != nil {
   182  		return nil
   183  	}
   184  
   185  	dataLayerProfile := s.getUserProfile(customer.GetPersonalData().MainEmail, identity.Subject())
   186  	if dataLayerProfile == nil {
   187  		return nil
   188  	}
   189  
   190  	dataLayerUser := domain.User{}
   191  	dataLayerUser.Profile = append(dataLayerUser.Profile, *dataLayerProfile)
   192  	return &dataLayerUser
   193  }
   194  
   195  func (s Factory) getUserProfile(email string, sub string) *domain.UserProfile {
   196  	dataLayerProfile := domain.UserProfile{
   197  		ProfileInfo: domain.UserProfileInfo{
   198  			EmailID:   s.HashValueIfConfigured(email),
   199  			ProfileID: s.HashValueIfConfigured(sub),
   200  		},
   201  	}
   202  	return &dataLayerProfile
   203  }
   204  
   205  // HashValueIfConfigured returns the hashed `value` if hashing is configured
   206  func (s Factory) HashValueIfConfigured(value string) string {
   207  	if s.hashUserValues && value != "" {
   208  		return s.hashWithSHA512(value)
   209  	}
   210  	return value
   211  }
   212  
   213  // BuildCartData builds the domain cart data
   214  func (s Factory) BuildCartData(cart decorator.DecoratedCart) *domain.Cart {
   215  	cartData := domain.Cart{
   216  		CartID: cart.Cart.ID,
   217  		Price: &domain.CartPrice{
   218  			Currency:       cart.Cart.GrandTotal.Currency(),
   219  			BasePrice:      cart.Cart.SubTotalNet.FloatAmount(),
   220  			CartTotal:      cart.Cart.GrandTotal.FloatAmount(),
   221  			Shipping:       cart.Cart.ShippingNet.FloatAmount(),
   222  			ShippingMethod: strings.Join(cart.Cart.AllShippingTitles(), "/"),
   223  			PriceWithTax:   cart.Cart.GrandTotal.FloatAmount(),
   224  		},
   225  		Attributes: make(map[string]interface{}),
   226  	}
   227  	for _, item := range cart.GetAllDecoratedItems() {
   228  		itemData := s.buildCartItem(item, cart.Cart.GrandTotal.Currency())
   229  		cartData.Item = append(cartData.Item, itemData)
   230  	}
   231  	return &cartData
   232  }
   233  
   234  // BuildTransactionData builds the domain transaction data
   235  func (s Factory) BuildTransactionData(ctx context.Context, cart decorator.DecoratedCart, decoratedItems []decorator.DecoratedCartItem, orderID string, email string) *domain.Transaction {
   236  	identity := s.webIdentityService.Identify(ctx, web.RequestFromContext(ctx))
   237  	profile := s.getUserProfile(email, "")
   238  
   239  	if identity != nil {
   240  		user := s.getUserFromIdentity(ctx, identity)
   241  		if user != nil {
   242  			profile = &user.Profile[0]
   243  		}
   244  	}
   245  
   246  	transactionData := domain.Transaction{
   247  		TransactionID: orderID,
   248  		Price: &domain.TransactionPrice{
   249  			Currency:         cart.Cart.GrandTotal.Currency(),
   250  			BasePrice:        cart.Cart.GrandTotal.FloatAmount(),
   251  			TransactionTotal: cart.Cart.GrandTotal.FloatAmount(),
   252  			Shipping:         cart.Cart.ShippingNet.FloatAmount(),
   253  			ShippingMethod:   strings.Join(cart.Cart.AllShippingTitles(), "/"),
   254  		},
   255  		Profile:    profile,
   256  		Attributes: make(map[string]interface{}),
   257  	}
   258  	for _, item := range decoratedItems {
   259  		itemData := s.buildCartItem(item, cart.Cart.GrandTotal.Currency())
   260  		transactionData.Item = append(transactionData.Item, itemData)
   261  	}
   262  	return &transactionData
   263  }
   264  
   265  func (s Factory) buildCartItem(item decorator.DecoratedCartItem, currencyCode string) domain.CartItem {
   266  	cartItem := domain.CartItem{
   267  		Category:    s.getProductCategory(item.Product),
   268  		Quantity:    item.Item.Qty,
   269  		ProductInfo: s.getProductInfo(item.Product),
   270  		Price: domain.CartItemPrice{
   271  			BasePrice:    item.Item.SinglePriceNet.FloatAmount(),
   272  			PriceWithTax: item.Item.SinglePriceGross.FloatAmount(),
   273  			TaxRate:      item.Item.TotalTaxAmount().FloatAmount(),
   274  			Currency:     currencyCode,
   275  		},
   276  		Attributes: make(map[string]interface{}),
   277  	}
   278  	cartItem.Attributes["sourceId"] = item.Item.SourceID
   279  	cartItem.Attributes["terminal"] = ""
   280  	cartItem.Attributes["leadtime"] = ""
   281  	return cartItem
   282  }
   283  
   284  // BuildProductData builds the domain product data
   285  func (s Factory) BuildProductData(product productDomain.BasicProduct) domain.Product {
   286  	productData := domain.Product{
   287  		ProductInfo: s.getProductInfo(product),
   288  		Category:    s.getProductCategory(product),
   289  		Attributes:  make(map[string]interface{}),
   290  	}
   291  
   292  	// check for defaultVariant
   293  	baseData := product.BaseData()
   294  	saleableData := product.SaleableData()
   295  	if product.Type() == productDomain.TypeConfigurable {
   296  		if configurable, ok := product.(productDomain.ConfigurableProduct); ok {
   297  			defaultVariant, _ := configurable.GetDefaultVariant()
   298  			baseData = defaultVariant.BaseData()
   299  			saleableData = defaultVariant.SaleableData()
   300  		}
   301  	}
   302  
   303  	// set prices
   304  	productData.Attributes["productPrice"] = strconv.FormatFloat(saleableData.ActivePrice.GetFinalPrice().FloatAmount(), 'f', 2, 64)
   305  
   306  	// check for highstreet price
   307  	if baseData.HasAttribute("rrp") {
   308  		productData.Attributes["highstreetPrice"] = baseData.Attributes["rrp"].Value()
   309  	}
   310  
   311  	// if FinalPrice is discounted, add it to specialPrice
   312  	if saleableData.ActivePrice.IsDiscounted {
   313  		productData.Attributes["specialPrice"] = strconv.FormatFloat(saleableData.ActivePrice.Discounted.FloatAmount(), 'f', 2, 64)
   314  		productData.Attributes["productPrice"] = strconv.FormatFloat(saleableData.ActivePrice.Default.FloatAmount(), 'f', 2, 64)
   315  	}
   316  
   317  	// set badge
   318  	productData.Attributes["badge"] = s.EvaluateBadgeHierarchy(product)
   319  
   320  	if product.BaseData().HasAttribute("ispuLimitedToAreas") {
   321  		replacer := strings.NewReplacer("[", "", "]", "")
   322  		productData.Attributes["ispuLimitedToAreas"] = strings.Split(replacer.Replace(product.BaseData().Attributes["ispuLimitedToAreas"].Value()), " ")
   323  	}
   324  	return productData
   325  }
   326  
   327  func (s Factory) getProductCategory(product productDomain.BasicProduct) *domain.ProductCategory {
   328  	level0 := ""
   329  	level1 := ""
   330  	level2 := ""
   331  
   332  	categoryPath := product.BaseData().MainCategory.Path
   333  	baseData := product.BaseData()
   334  
   335  	firstPathLevels := strings.Split(categoryPath, "/")
   336  	if len(firstPathLevels) > 0 {
   337  		level0 = firstPathLevels[0]
   338  	}
   339  	if len(firstPathLevels) > 1 {
   340  		level1 = firstPathLevels[1]
   341  	}
   342  	if len(firstPathLevels) > 2 {
   343  		level2 = firstPathLevels[2]
   344  	}
   345  
   346  	productFamily := ""
   347  	if baseData.HasAttribute("gs1Family") {
   348  		productFamily = baseData.Attributes["gs1Family"].Value()
   349  	}
   350  	return &domain.ProductCategory{
   351  		PrimaryCategory: level0,
   352  		SubCategory1:    level1,
   353  		SubCategory2:    level2,
   354  		SubCategory:     level1,
   355  		ProductType:     productFamily,
   356  	}
   357  }
   358  
   359  func (s Factory) getProductInfo(product productDomain.BasicProduct) domain.ProductInfo {
   360  	baseData := product.BaseData()
   361  	retailerCode := baseData.RetailerCode
   362  
   363  	productName := s.regex.ReplaceAllString(baseData.Title, "-")
   364  
   365  	// Handle Variants if it is a Configurable
   366  	var parentIDRef *string
   367  	var variantSelectedAttributeRef *string
   368  	if product.Type() == productDomain.TypeConfigurableWithActiveVariant {
   369  		if configurableWithActiveVariant, ok := product.(productDomain.ConfigurableProductWithActiveVariant); ok {
   370  			parentID := configurableWithActiveVariant.ConfigurableBaseData().MarketPlaceCode
   371  			parentIDRef = &parentID
   372  			variantSelectedAttribute := configurableWithActiveVariant.ActiveVariant.BaseData().Attributes[configurableWithActiveVariant.VariantVariationAttributes[0]].Value()
   373  			variantSelectedAttributeRef = &variantSelectedAttribute
   374  		}
   375  	}
   376  	if product.Type() == productDomain.TypeConfigurable {
   377  		if configurable, ok := product.(productDomain.ConfigurableProduct); ok {
   378  			parentID := configurable.BaseData().MarketPlaceCode
   379  			parentIDRef = &parentID
   380  
   381  			defaultVariant, err := configurable.GetDefaultVariant()
   382  			if err == nil {
   383  				retailerCode = defaultVariant.RetailerCode
   384  			}
   385  		}
   386  	}
   387  	// Search for some common product attributes to fill the productInfos (This maybe better to be configurable later)
   388  	color := ""
   389  	if baseData.HasAttribute("manufacturerColor") {
   390  		color = baseData.Attributes["manufacturerColor"].Value()
   391  	}
   392  	if baseData.HasAttribute("baseColor") {
   393  		color = baseData.Attributes["baseColor"].Value()
   394  	}
   395  	size := ""
   396  	if baseData.HasAttribute("shoeSize") {
   397  		size = baseData.Attributes["shoeSize"].Value()
   398  	}
   399  	if baseData.HasAttribute("clothingSize") {
   400  		size = baseData.Attributes["clothingSize"].Value()
   401  	}
   402  	brand := ""
   403  	if baseData.HasAttribute("brandCode") {
   404  		brand = baseData.Attributes["brandCode"].Value()
   405  	}
   406  	gtin := ""
   407  	if baseData.HasAttribute("gtin") {
   408  		if baseData.Attributes["gtin"].HasMultipleValues() {
   409  			gtins := baseData.Attributes["gtin"].Values()
   410  			gtin = strings.Join(gtins, ",")
   411  		} else {
   412  			gtin = baseData.Attributes["gtin"].Value()
   413  		}
   414  	}
   415  
   416  	return domain.ProductInfo{
   417  		ProductID:                baseData.MarketPlaceCode,
   418  		ProductName:              productName,
   419  		ProductThumbnail:         s.getProductThumbnailURL(baseData),
   420  		ProductImage:             s.getProductImageURL(baseData),
   421  		ProductType:              product.Type(),
   422  		ParentID:                 parentIDRef,
   423  		VariantSelectedAttribute: variantSelectedAttributeRef,
   424  		Retailer:                 retailerCode,
   425  		Brand:                    brand,
   426  		SKU:                      gtin,
   427  		Manufacturer:             brand,
   428  		Color:                    color,
   429  		Size:                     size,
   430  		InStock:                  strconv.FormatBool(baseData.IsInStock()),
   431  	}
   432  }
   433  
   434  func (s Factory) getProductThumbnailURL(baseData productDomain.BasicProductData) string {
   435  	media := baseData.GetMedia("thumbnail")
   436  	if media.Reference != "" {
   437  		return s.productMediaBaseURL + s.productMediaThumbnailURLPrefix + media.Reference
   438  	}
   439  	media = baseData.GetMedia("list")
   440  	if media.Reference != "" {
   441  		return s.productMediaBaseURL + s.productMediaThumbnailURLPrefix + media.Reference
   442  	}
   443  	return ""
   444  }
   445  
   446  func (s Factory) getProductImageURL(baseData productDomain.BasicProductData) string {
   447  	media := baseData.GetMedia("detail")
   448  	if media.Reference != "" {
   449  		return s.productMediaBaseURL + s.productMediaURLPrefix + media.Reference
   450  	}
   451  	return ""
   452  }
   453  
   454  func (s Factory) hashWithSHA512(value string) string {
   455  	newHash := sha512.New()
   456  	newHash.Write([]byte(value))
   457  	// the hash is a byte array
   458  	result := newHash.Sum(nil)
   459  	// since we want to use it in a variable we base64 encode it (other alternative would be Hexadecimal representation "% x", h.Sum(nil)
   460  	return s.hashEncoder.EncodeToString(result)
   461  }
   462  
   463  // BuildChangeQtyEvent builds the change quantity domain event
   464  func (s Factory) BuildChangeQtyEvent(productIdentifier string, productName string, qty int, qtyBefore int, cartID string) domain.Event {
   465  	event := domain.Event{EventInfo: make(map[string]interface{})}
   466  	event.EventInfo["productId"] = productIdentifier
   467  	event.EventInfo["productName"] = productName
   468  	event.EventInfo["cartId"] = cartID
   469  
   470  	if qty == 0 {
   471  		event.EventInfo["eventName"] = "Remove Product"
   472  	} else {
   473  		event.EventInfo["eventName"] = "Update Quantity"
   474  		event.EventInfo["quantity"] = qty
   475  	}
   476  	return event
   477  }
   478  
   479  // BuildAddToBagEvent builds the add to bag domain event
   480  func (s Factory) BuildAddToBagEvent(productIdentifier string, productName string, qty int) domain.Event {
   481  	event := domain.Event{EventInfo: make(map[string]interface{})}
   482  	event.EventInfo["eventName"] = "Add To Bag"
   483  	event.EventInfo["productId"] = productIdentifier
   484  	event.EventInfo["productName"] = productName
   485  	event.EventInfo["quantity"] = qty
   486  
   487  	return event
   488  }
   489  
   490  // EvaluateBadgeHierarchy get the active badge by product
   491  func (s *Factory) EvaluateBadgeHierarchy(product productDomain.BasicProduct) string {
   492  	badge := ""
   493  
   494  	if product.BaseData().HasAttribute("airportBadge") {
   495  		badge = "airportBadge"
   496  	} else if product.BaseData().HasAttribute("retailerBadge") {
   497  		badge = "retailerBadge"
   498  	} else if product.BaseData().HasAttribute("exclusiveProduct") && product.BaseData().Attributes["exclusiveProduct"].Value() == "true" {
   499  		badge = "travellerExclusive"
   500  	} else if product.BaseData().IsNew {
   501  		badge = "new"
   502  	}
   503  
   504  	return badge
   505  }