flamingo.me/flamingo-commerce/v3@v3.11.0/product/domain/productBasics.go (about)

     1  package domain
     2  
     3  import (
     4  	"fmt"
     5  	"math/big"
     6  	"strings"
     7  	"time"
     8  
     9  	priceDomain "flamingo.me/flamingo-commerce/v3/price/domain"
    10  )
    11  
    12  // Media usage constants
    13  const (
    14  	MediaUsageList      = "list"
    15  	MediaUsageDetail    = "detail"
    16  	MediaUsageThumbnail = "thumbnail"
    17  )
    18  
    19  type (
    20  	// BasicProduct interface need to be implemented by all Product Types!
    21  	BasicProduct interface {
    22  		// BaseData gives you basic product information
    23  		// like attributes, title, media, marketplacecode etc
    24  		BaseData() BasicProductData
    25  		// TeaserData gives you basic information to typically show in list views - including a teaser prices
    26  		TeaserData() TeaserData
    27  		// GetSpecifications return grouped specifications - typically used for comparisons
    28  		GetSpecifications() Specifications
    29  		// IsSaleable indicates if that product type can be purchased in general
    30  		IsSaleable() bool
    31  		// SaleableData returns information required to purchase - like the definite price information
    32  		// makes only sense to call if IsSaleable() returns true
    33  		SaleableData() Saleable
    34  		// Type returns product type
    35  		Type() string
    36  		// GetIdentifier returns an identifier for the product instance
    37  		GetIdentifier() string
    38  		HasMedia(group string, usage string) bool
    39  		GetMedia(group string, usage string) Media
    40  	}
    41  
    42  	// BasicProductData is the basic product model
    43  	BasicProductData struct {
    44  		Title            string
    45  		Attributes       Attributes
    46  		ShortDescription string
    47  		Description      string
    48  		Media            []Media
    49  		Badges           Badges
    50  
    51  		MarketPlaceCode string
    52  		RetailerCode    string
    53  		RetailerSku     string
    54  		RetailerName    string
    55  
    56  		CreatedAt   time.Time
    57  		UpdatedAt   time.Time
    58  		VisibleFrom time.Time
    59  		VisibleTo   time.Time
    60  
    61  		Categories   []CategoryTeaser
    62  		MainCategory CategoryTeaser
    63  
    64  		CategoryToCodeMapping []string
    65  
    66  		// Deprecated: use Stock[x].Level instead
    67  		StockLevel string
    68  		Stock      []Stock
    69  
    70  		Keywords []string
    71  		IsNew    bool
    72  	}
    73  
    74  	// CategoryTeaser represents some Teaser infos for Category
    75  	CategoryTeaser struct {
    76  		// Code the identifier of the Category
    77  		Code string
    78  		// The Path (root to leaf) for this Category - separated by "/"
    79  		Path string
    80  		// Name is the speaking name of the category
    81  		Name string
    82  		// Parent is an optional link to parent teaser
    83  		Parent *CategoryTeaser `swaggerignore:"true"`
    84  	}
    85  
    86  	// Saleable are properties required for being selled
    87  	Saleable struct {
    88  		IsSaleable         bool
    89  		SaleableFrom       time.Time
    90  		SaleableTo         time.Time
    91  		ActivePrice        PriceInfo
    92  		AvailablePrices    []PriceInfo
    93  		ActiveLoyaltyPrice *LoyaltyPriceInfo
    94  		// LoyaltyPrices holds optional infos for products that can be paid in a loyalty program
    95  		LoyaltyPrices []LoyaltyPriceInfo
    96  		// LoyaltyEarnings holds optional infos about potential loyalty earnings
    97  		LoyaltyEarnings []LoyaltyEarningInfo
    98  	}
    99  
   100  	// PriceInfo holds product price information
   101  	PriceInfo struct {
   102  		Default           priceDomain.Price
   103  		Discounted        priceDomain.Price
   104  		DiscountText      string
   105  		ActiveBase        big.Float `swaggertype:"string"`
   106  		ActiveBaseAmount  big.Float `swaggertype:"string"`
   107  		ActiveBaseUnit    string
   108  		IsDiscounted      bool
   109  		CampaignRules     []string
   110  		DenyMoreDiscounts bool
   111  		Context           PriceContext
   112  		TaxClass          string
   113  	}
   114  
   115  	// LoyaltyPriceInfo contains info used for product with
   116  	LoyaltyPriceInfo struct {
   117  		// Type or Name of the Loyalty program
   118  		Type             string
   119  		Default          priceDomain.Price
   120  		IsDiscounted     bool
   121  		Discounted       priceDomain.Price
   122  		DiscountText     string
   123  		MinPointsToSpent big.Float  `swaggertype:"string"`
   124  		MaxPointsToSpent *big.Float `swaggertype:"string"`
   125  		Context          PriceContext
   126  	}
   127  
   128  	// LoyaltyEarningInfo contains earning infos
   129  	LoyaltyEarningInfo struct {
   130  		Type    string
   131  		Default priceDomain.Price
   132  	}
   133  
   134  	// PriceContext defines the scope in which the price was calculated
   135  	PriceContext struct {
   136  		DeliveryCode  string
   137  		CustomerGroup string
   138  		ChannelCode   string
   139  		Locale        string
   140  	}
   141  
   142  	// TeaserData is the teaser-information for product previews
   143  	TeaserData struct {
   144  		ShortTitle       string
   145  		ShortDescription string
   146  		URLSlug          string
   147  		// TeaserPrice is the price that should be shown in teasers (listview)
   148  		TeaserPrice PriceInfo
   149  		// TeaserPriceIsFromPrice is set to true in cases where a product might have different prices (e.g. configurable)
   150  		TeaserPriceIsFromPrice bool
   151  		// PreSelectedVariantSku might be set for configurables to give a hint to link to a variant of a configurable (That might be the case if a user filters for an attribute and in the teaser the variant with that attribute is shown)
   152  		PreSelectedVariantSku string
   153  		// Media
   154  		Media []Media
   155  		// The sku that should be used to link from Teasers
   156  		MarketPlaceCode       string
   157  		TeaserAvailablePrices []PriceInfo
   158  		// TeaserLoyaltyPriceInfo is the loyalty price that can be used for teaser (e.g. on listing views)
   159  		TeaserLoyaltyPriceInfo *LoyaltyPriceInfo
   160  		// TeaserLoyaltyEarning is the teaser for the loyalty earning used in grid / list view
   161  		TeaserLoyaltyEarningInfo *LoyaltyEarningInfo
   162  		// Badges optional slice of badges to teaser a product
   163  		Badges Badges
   164  	}
   165  
   166  	// Media holds product media information
   167  	Media struct {
   168  		Type      string
   169  		MimeType  string
   170  		Usage     string
   171  		Title     string
   172  		Reference string
   173  	} // @name ProductMedia
   174  
   175  	// Attributes describe a product attributes map
   176  	Attributes map[string]Attribute // @name ProductAttributes
   177  
   178  	// Attribute for product attributes
   179  	// Example:
   180  	// Attribute{
   181  	//		Code:      "my-attribute",
   182  	//		CodeLabel: "My Attribute",
   183  	//		Label:     "My attribute value",
   184  	//		RawValue:  2,
   185  	//		UnitCode:  "PCS",
   186  	//	}
   187  	Attribute struct {
   188  		// Code is the internal attribute identifier
   189  		Code string
   190  		// CodeLabel is the human-readable (perhaps localized) attribute name
   191  		CodeLabel string
   192  		// Label is the human-readable (perhaps localized) attribute value
   193  		Label string
   194  		// RawValue is the untouched original value of the attribute
   195  		RawValue interface{}
   196  		// UnitCode is the internal code of the attribute values measuring unit
   197  		UnitCode string
   198  	} // @name ProductAttribute
   199  
   200  	// Specifications of a product
   201  	Specifications struct {
   202  		Groups []SpecificationGroup
   203  	}
   204  
   205  	// SpecificationGroup groups specifications
   206  	SpecificationGroup struct {
   207  		Title   string
   208  		Entries []SpecificationEntry
   209  	}
   210  
   211  	// SpecificationEntry data
   212  	SpecificationEntry struct {
   213  		Label  string
   214  		Values []string
   215  	}
   216  
   217  	// WishedToPay is the list of prices by type
   218  	WishedToPay struct {
   219  		priceByType map[string]priceDomain.Price
   220  	}
   221  
   222  	// Badge for a product
   223  	// Example:
   224  	// Badge {
   225  	//   Code: "new",
   226  	//   Label: "New Product",
   227  	// }
   228  	Badge struct {
   229  		Code  string
   230  		Label string
   231  	}
   232  
   233  	// Badges slice of Badge
   234  	Badges []Badge
   235  
   236  	// Stock holds data with product availability info
   237  	Stock struct {
   238  		InStock      bool
   239  		Level        string
   240  		Amount       int
   241  		DeliveryCode string
   242  	}
   243  )
   244  
   245  // Stock Level values
   246  const (
   247  	StockLevelOutOfStock = "out"
   248  	StockLevelInStock    = "in"
   249  	StockLevelLowStock   = "low"
   250  )
   251  
   252  // Value returns the raw value
   253  func (at Attribute) Value() string {
   254  	return strings.Trim(fmt.Sprintf("%v", at.RawValue), " ")
   255  }
   256  
   257  // IsEnabledValue returns true if the value can be seen as a toggle and is enabled
   258  func (at Attribute) IsEnabledValue() bool {
   259  	switch at.RawValue {
   260  	case "Yes", "yes":
   261  		return true
   262  	case "true", true:
   263  		return true
   264  	case "1", 1:
   265  		return true
   266  	default:
   267  		return false
   268  	}
   269  }
   270  
   271  // IsDisabledValue returns true if the value can be seen as a disable toggle/switch value
   272  func (at Attribute) IsDisabledValue() bool {
   273  	switch at.RawValue {
   274  	case "No", "no":
   275  		return true
   276  	case "false", false:
   277  		return true
   278  	case "0", 0:
   279  		return true
   280  	default:
   281  		return false
   282  	}
   283  }
   284  
   285  // HasMultipleValues checks for multiple raw values
   286  func (at Attribute) HasMultipleValues() bool {
   287  	_, ok := at.RawValue.([]Attribute)
   288  	if ok {
   289  		return true
   290  	}
   291  
   292  	_, ok = at.RawValue.([]interface{})
   293  	return ok
   294  }
   295  
   296  // Values builds a list of product attribute values in case the raw value is a slice
   297  func (at Attribute) Values() []string {
   298  	var result []string
   299  
   300  	list, ok := at.RawValue.([]Attribute)
   301  	if ok {
   302  		for _, entry := range list {
   303  			result = append(result, entry.Value())
   304  		}
   305  		return result
   306  	}
   307  
   308  	listFallback, ok := at.RawValue.([]interface{})
   309  	if ok {
   310  		for _, entry := range listFallback {
   311  			result = append(result, strings.Trim(fmt.Sprintf("%v", entry), " "))
   312  		}
   313  	}
   314  	return result
   315  }
   316  
   317  // Labels builds a list of human-readable product attribute values in case the raw value is a slice of Attribute, uses Values() as fallback
   318  func (at Attribute) Labels() []string {
   319  	var result []string
   320  	list, ok := at.RawValue.([]Attribute)
   321  	if ok {
   322  		for _, entry := range list {
   323  			result = append(result, entry.Label)
   324  		}
   325  		return result
   326  	}
   327  
   328  	return at.Values()
   329  }
   330  
   331  // HasUnitCode checks if a unit code is set on the attribute
   332  func (at Attribute) HasUnitCode() bool {
   333  	return len(at.UnitCode) > 0
   334  }
   335  
   336  // GetUnit returns the unit on an attribute
   337  func (at Attribute) GetUnit() Unit {
   338  	unit, ok := Units[at.UnitCode]
   339  	if !ok {
   340  		return Unit{
   341  			Code:   at.UnitCode,
   342  			Symbol: "",
   343  		}
   344  	}
   345  	return unit
   346  }
   347  
   348  // HasAttribute check
   349  func (bpd BasicProductData) HasAttribute(key string) bool {
   350  	if _, ok := bpd.Attributes[key]; ok {
   351  		return true
   352  	}
   353  	return false
   354  }
   355  
   356  // HasAllAttributes returns true, if all attributes are set
   357  func (bpd BasicProductData) HasAllAttributes(keys []string) bool {
   358  	for _, key := range keys {
   359  		if !bpd.HasAttribute(key) {
   360  			return false
   361  		}
   362  	}
   363  	return true
   364  }
   365  
   366  // Attribute get Attribute by key
   367  func (bpd BasicProductData) Attribute(key string) Attribute {
   368  	return bpd.Attributes[key]
   369  }
   370  
   371  // GetFinalPrice getter for price that should be used in calculations (either discounted or default)
   372  func (p PriceInfo) GetFinalPrice() priceDomain.Price {
   373  	if p.IsDiscounted {
   374  		return p.Discounted
   375  	}
   376  	return p.Default
   377  }
   378  
   379  // GetListMedia returns the product media for listing
   380  func (bpd BasicProductData) GetListMedia() Media {
   381  	return bpd.GetMedia(MediaUsageList)
   382  }
   383  
   384  // GetSpecifications getter
   385  func (bpd BasicProductData) GetSpecifications() Specifications {
   386  	if specs, ok := bpd.Attributes["specifications"].RawValue.(Specifications); ok {
   387  		return specs
   388  	}
   389  
   390  	return Specifications{}
   391  }
   392  
   393  // GetMedia returns the FIRST found product media by usage
   394  func (bpd BasicProductData) GetMedia(usage string) Media {
   395  	var emptyMedia Media
   396  	for _, media := range bpd.Media {
   397  		if media.Usage == usage {
   398  			return media
   399  		}
   400  	}
   401  	return emptyMedia
   402  }
   403  
   404  // IsSaleableNow checks flag and time
   405  func (p Saleable) IsSaleableNow() bool {
   406  	if !p.IsSaleable {
   407  		return false
   408  	}
   409  
   410  	// For some reasons IsZero does not always work - thats why we check for 1970
   411  	if (p.SaleableFrom.IsZero() || p.SaleableFrom.Year() == 1970 || p.SaleableFrom.Before(time.Now())) &&
   412  		(p.SaleableTo.IsZero() || p.SaleableTo.Year() == 1970 || p.SaleableTo.After(time.Now())) {
   413  		return true
   414  	}
   415  
   416  	return false
   417  }
   418  
   419  // GetLoyaltyPriceByType returns first encountered loyalty price with this type
   420  func (p Saleable) GetLoyaltyPriceByType(ltype string) (*LoyaltyPriceInfo, bool) {
   421  	if p.ActiveLoyaltyPrice != nil && p.ActiveLoyaltyPrice.Type == ltype {
   422  		return p.ActiveLoyaltyPrice, true
   423  	}
   424  
   425  	for _, lp := range p.LoyaltyPrices {
   426  		if lp.Type == ltype {
   427  			return &lp, true
   428  		}
   429  	}
   430  
   431  	return nil, false
   432  }
   433  
   434  // GetLoyaltyEarningByType returns the loyalty earning infos for a specific loyalty type
   435  func (p Saleable) GetLoyaltyEarningByType(ltype string) (*LoyaltyEarningInfo, bool) {
   436  	for _, le := range p.LoyaltyEarnings {
   437  		if le.Type == ltype {
   438  			return &le, true
   439  		}
   440  	}
   441  	return nil, false
   442  }
   443  
   444  func (p Saleable) generateLoyaltyChargeSplit(valuedPriceToPay *priceDomain.Price, loyaltyPointsWishedToPay *WishedToPay, qty int, ignoreMin bool) priceDomain.Charges {
   445  	if valuedPriceToPay == nil {
   446  		finalPrice := p.ActivePrice.GetFinalPrice()
   447  		valuedPriceToPay = &finalPrice
   448  	}
   449  	requiredCharges := make(map[string]priceDomain.Charge)
   450  	remainingMainChargeValue := valuedPriceToPay.Amount()
   451  
   452  	if p.ActiveLoyaltyPrice == nil || !p.ActiveLoyaltyPrice.GetFinalPrice().IsPositive() {
   453  		return buildCharges(requiredCharges, *remainingMainChargeValue, *valuedPriceToPay)
   454  	}
   455  
   456  	// loyaltyAmountToSpent - set as default without potential wish the minimum
   457  	loyaltyAmountToSpent := p.ActiveLoyaltyPrice.getMin(qty)
   458  
   459  	// check if the minimum points should be ignored, if so minimum will be set to 0
   460  	if ignoreMin {
   461  		loyaltyAmountToSpent = *big.NewFloat(0.0)
   462  	}
   463  
   464  	//nolint:nestif // to be refactored some other day
   465  	if loyaltyPointsWishedToPay != nil {
   466  		// if a loyaltyPointsWishedToPay is passed evaluate it within min and max and update loyaltyAmountToSpent:
   467  		wishedPrice := loyaltyPointsWishedToPay.GetByType(p.ActiveLoyaltyPrice.Type)
   468  
   469  		if wishedPrice != nil && wishedPrice.Currency() == p.ActiveLoyaltyPrice.GetFinalPrice().Currency() {
   470  			wishedPriceRounded := wishedPrice.GetPayable()
   471  
   472  			// if wish is bigger than min we using the wish
   473  			if loyaltyAmountToSpent.Cmp(wishedPriceRounded.Amount()) <= 0 {
   474  				loyaltyAmountToSpent = *wishedPriceRounded.Amount()
   475  			}
   476  			// evaluate max
   477  			max := p.ActiveLoyaltyPrice.getMax(qty)
   478  			if max != nil {
   479  				// more then max - return max
   480  				if max.Cmp(wishedPrice.Amount()) == -1 {
   481  					loyaltyAmountToSpent = *max
   482  				}
   483  			}
   484  		}
   485  	}
   486  
   487  	loyaltyCharge := getValidLoyaltyCharge(loyaltyAmountToSpent, *p.ActiveLoyaltyPrice, p.ActivePrice, *valuedPriceToPay)
   488  
   489  	if !loyaltyCharge.Value.IsPositive() {
   490  		return buildCharges(requiredCharges, *remainingMainChargeValue, *valuedPriceToPay)
   491  	}
   492  
   493  	// Add the loyalty charge and at the same time reduce the remainingValue
   494  	remainingMainChargeValue = new(big.Float).Sub(remainingMainChargeValue, loyaltyCharge.Value.Amount())
   495  	requiredCharges[p.ActiveLoyaltyPrice.Type] = loyaltyCharge
   496  
   497  	return buildCharges(requiredCharges, *remainingMainChargeValue, *valuedPriceToPay)
   498  }
   499  
   500  func buildCharges(requiredCharges map[string]priceDomain.Charge, remainingMainChargeValue big.Float, valuedPriceToPay priceDomain.Price) priceDomain.Charges {
   501  	remainingMainChargePrice := priceDomain.NewFromBigFloat(remainingMainChargeValue, valuedPriceToPay.Currency()).GetPayable()
   502  
   503  	requiredCharges[priceDomain.ChargeTypeMain] = priceDomain.Charge{
   504  		Price: remainingMainChargePrice,
   505  		Type:  priceDomain.ChargeTypeMain,
   506  		Value: remainingMainChargePrice,
   507  	}
   508  
   509  	return *priceDomain.NewCharges(requiredCharges)
   510  }
   511  
   512  // getValidLoyaltyCharge returns the loyaltyCharge of the given type, making sure the currentlyRemainingMainChargeValue is not exceeded
   513  func getValidLoyaltyCharge(loyaltyAmountWishedToSpent big.Float, activeLoyaltyPrice LoyaltyPriceInfo, activePrice PriceInfo, valuedPriceToPay priceDomain.Price) priceDomain.Charge {
   514  	currentlyRemainingMainChargeValue := valuedPriceToPay.Amount()
   515  
   516  	loyaltyCurrency := activeLoyaltyPrice.GetFinalPrice().Currency()
   517  	rateLoyaltyFinalPriceToRealFinalPrice := activeLoyaltyPrice.GetRate(activePrice.GetFinalPrice())
   518  	maximumPossibleLoyaltyValue := big.NewFloat(0.0)
   519  
   520  	if currentlyRemainingMainChargeValue.Cmp(big.NewFloat(0.0)) != 0 {
   521  		remainingPrice := valuedPriceToPay.GetPayable()
   522  		maximumPossibleLoyaltyValue = new(big.Float).Quo(remainingPrice.Amount(), &rateLoyaltyFinalPriceToRealFinalPrice)
   523  		maximumPossibleLoyaltyValue = priceDomain.NewFromBigFloat(*maximumPossibleLoyaltyValue, "").GetPayableByRoundingMode(priceDomain.RoundingModeHalfUp, 1).Amount()
   524  	}
   525  
   526  	maximumPossibleLoyaltyPrice := priceDomain.NewFromBigFloat(*maximumPossibleLoyaltyValue, loyaltyCurrency).GetPayable()
   527  
   528  	if loyaltyAmountWishedToSpent.Cmp(maximumPossibleLoyaltyValue) > 0 {
   529  		loyaltyAmountWishedToSpent = *maximumPossibleLoyaltyValue
   530  	}
   531  
   532  	valuedLoyaltyPrice := priceDomain.NewFromBigFloat(*new(big.Float).Mul(&rateLoyaltyFinalPriceToRealFinalPrice, &loyaltyAmountWishedToSpent), valuedPriceToPay.Currency()).GetPayable()
   533  	if maximumPossibleLoyaltyPrice.Amount().Cmp(&loyaltyAmountWishedToSpent) == 0 {
   534  		// If the wish equals the rounded maximum - we need to use the complete remaining value
   535  		valuedLoyaltyPrice = priceDomain.NewFromBigFloat(*currentlyRemainingMainChargeValue, valuedPriceToPay.Currency())
   536  	}
   537  
   538  	return priceDomain.Charge{
   539  		Price: priceDomain.NewFromBigFloat(loyaltyAmountWishedToSpent, loyaltyCurrency).GetPayable(),
   540  		Type:  activeLoyaltyPrice.Type,
   541  		Value: valuedLoyaltyPrice,
   542  	}
   543  }
   544  
   545  // GetLoyaltyChargeSplit gets the Charges that need to be paid by type:
   546  // Type "main" is the remaining charge in the main currency and the other charges returned are the loyalty price charges that need to be paid.
   547  // The method takes the min, max and the calculated loyalty conversion rate into account
   548  //
   549  // * valuedPriceToPay  Optional the price that need to be paid - if not given the products final price will be used
   550  // * loyaltyPointsWishedToPay   Optional a list of loyaltyPrices that the (customer) wants to spend. Its used as a wish and may not be fulfilled because of min, max properties on the products loyaltyPrices
   551  // * qty the quantity of the current item affects min max loyalty charge
   552  func (p Saleable) GetLoyaltyChargeSplit(valuedPriceToPay *priceDomain.Price, loyaltyPointsWishedToPay *WishedToPay, qty int) priceDomain.Charges {
   553  	return p.generateLoyaltyChargeSplit(valuedPriceToPay, loyaltyPointsWishedToPay, qty, false)
   554  }
   555  
   556  // GetLoyaltyChargeSplitIgnoreMin same as GetLoyaltyChargeSplit but ignoring the min points to spend
   557  func (p Saleable) GetLoyaltyChargeSplitIgnoreMin(valuedPriceToPay *priceDomain.Price, loyaltyPointsWishedToPay *WishedToPay, qty int) priceDomain.Charges {
   558  	return p.generateLoyaltyChargeSplit(valuedPriceToPay, loyaltyPointsWishedToPay, qty, true)
   559  }
   560  
   561  // getMin returns minimum points to spend - scaled by qty
   562  func (l LoyaltyPriceInfo) getMin(qty int) big.Float {
   563  	return *new(big.Float).Mul(&l.MinPointsToSpent, big.NewFloat(float64(qty)))
   564  }
   565  
   566  // getMax returns max points to spend - scaled by qty. If no max set returns nil
   567  func (l LoyaltyPriceInfo) getMax(qty int) *big.Float {
   568  	if !l.HasMax() {
   569  		return nil
   570  	}
   571  	return new(big.Float).Mul(l.MaxPointsToSpent, big.NewFloat(float64(qty)))
   572  }
   573  
   574  func findMediaInProduct(p BasicProduct, group string, usage string) *Media {
   575  	var mediaList []Media
   576  	if group == "teaser" {
   577  		mediaList = p.TeaserData().Media
   578  		for _, media := range mediaList {
   579  			if media.Usage == usage {
   580  				return &media
   581  			}
   582  		}
   583  	}
   584  
   585  	mediaList = p.BaseData().Media
   586  	for _, media := range mediaList {
   587  		if media.Usage == usage {
   588  			return &media
   589  		}
   590  	}
   591  	return nil
   592  }
   593  
   594  // IsInStock returns information if current product whether in stock or not
   595  func (bpd BasicProductData) IsInStock() bool {
   596  	for _, stock := range bpd.Stock {
   597  		if stock.InStock {
   598  			return true
   599  		}
   600  	}
   601  
   602  	if bpd.StockLevel == "" || bpd.StockLevel == StockLevelOutOfStock {
   603  		return false
   604  	}
   605  
   606  	return true
   607  }
   608  
   609  // IsInStockForDeliveryCode returns information if current product whether in stock or not for provided delivery code
   610  func (bpd BasicProductData) IsInStockForDeliveryCode(deliveryCode string) bool {
   611  	for _, stock := range bpd.Stock {
   612  		if stock.DeliveryCode == deliveryCode {
   613  			return stock.InStock
   614  		}
   615  	}
   616  
   617  	return false
   618  }
   619  
   620  // NewWishedToPay returns a new WishedToPay struct
   621  func NewWishedToPay() WishedToPay {
   622  	return WishedToPay{
   623  		priceByType: make(map[string]priceDomain.Price),
   624  	}
   625  }
   626  
   627  // Add returns new WishedToPay instance with the given wish added
   628  func (w WishedToPay) Add(ctype string, price priceDomain.Price) WishedToPay {
   629  	if w.priceByType == nil {
   630  		w.priceByType = make(map[string]priceDomain.Price)
   631  	}
   632  	w.priceByType[ctype] = price
   633  	return w
   634  }
   635  
   636  // GetByType returns the wished price for the given type or nil
   637  func (w WishedToPay) GetByType(ctype string) *priceDomain.Price {
   638  	if price, ok := w.priceByType[ctype]; ok {
   639  		return &price
   640  	}
   641  	return nil
   642  }
   643  
   644  // GetRate returns the currency conversion rate of the current loyaltyprice final price in relation to the passed value
   645  func (l LoyaltyPriceInfo) GetRate(valuedPrice priceDomain.Price) big.Float {
   646  	if !l.GetFinalPrice().IsPositive() {
   647  		return *big.NewFloat(0)
   648  	}
   649  
   650  	return *new(big.Float).Quo(valuedPrice.GetPayable().Amount(), l.GetFinalPrice().Amount())
   651  }
   652  
   653  // HasMax checks if product has a maximum (points to spend) restriction
   654  func (l LoyaltyPriceInfo) HasMax() bool {
   655  	return l.MaxPointsToSpent != nil
   656  }
   657  
   658  // GetFinalPrice gets either the Default or the Discounted Loyaltyprice
   659  func (l LoyaltyPriceInfo) GetFinalPrice() priceDomain.Price {
   660  	if l.IsDiscounted && l.Discounted.IsLessThen(l.Default) {
   661  		return l.Discounted
   662  	}
   663  	return l.Default
   664  }
   665  
   666  // Split splits the given WishedToPay in payable childs
   667  func (w WishedToPay) Split(count int) []WishedToPay {
   668  	// init slice
   669  	result := make([]WishedToPay, count)
   670  	for k := range result {
   671  		result[k] = NewWishedToPay()
   672  	}
   673  	// fill slice with splitted
   674  	for chargeType, v := range w.priceByType {
   675  		valuesSplitted, _ := v.SplitInPayables(count)
   676  		for i, splittedValue := range valuesSplitted {
   677  			result[i] = result[i].Add(chargeType, splittedValue)
   678  		}
   679  	}
   680  	return result
   681  }
   682  
   683  // AttributeKeys lists all available keys
   684  func (a Attributes) AttributeKeys() []string {
   685  	res := make([]string, len(a))
   686  	i := 0
   687  	for k := range a {
   688  		res[i] = k
   689  		i++
   690  	}
   691  	return res
   692  }
   693  
   694  // Attributes lists all attributes
   695  func (a Attributes) Attributes() []Attribute {
   696  	res := make([]Attribute, len(a))
   697  	i := 0
   698  	for _, v := range a {
   699  		res[i] = v
   700  		i++
   701  	}
   702  	return res
   703  }
   704  
   705  // HasAttribute checks if an attribute is available
   706  func (a Attributes) HasAttribute(key string) bool {
   707  	_, exist := a[key]
   708  	return exist
   709  }
   710  
   711  // Attribute returns a specified attribute
   712  func (a Attributes) Attribute(key string) Attribute {
   713  	attribute := a[key]
   714  	return attribute
   715  }
   716  
   717  // AttributesByKey returns slice of attributes by given attribute keys
   718  func (a Attributes) AttributesByKey(keys []string) []Attribute {
   719  	res := make([]Attribute, 0)
   720  	for _, key := range keys {
   721  		if a.HasAttribute(key) {
   722  			res = append(res, a.Attribute(key))
   723  		}
   724  	}
   725  
   726  	return res
   727  }
   728  
   729  // CPath returns the constructed Path from this category to the root - splitted by /
   730  func (c *CategoryTeaser) CPath() string {
   731  	if c.Parent == nil || c.Parent == c {
   732  		return c.Code
   733  	}
   734  	return c.Parent.CPath() + "/" + c.Code
   735  }
   736  
   737  // First of the badges, returns nil if there is no first badge
   738  func (b Badges) First() *Badge {
   739  	if len(b) == 0 {
   740  		return nil
   741  	}
   742  
   743  	result := b[0]
   744  	return &result
   745  }