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

     1  package domain
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  )
     8  
     9  const (
    10  	// TypeBundle denotes bundle products
    11  	TypeBundle                  = "bundle"
    12  	TypeBundleWithActiveChoices = "bundle_with_active_choices"
    13  )
    14  
    15  type (
    16  	Option struct {
    17  		Product BasicProduct
    18  		MinQty  int
    19  		MaxQty  int
    20  	}
    21  
    22  	Choice struct {
    23  		Identifier string
    24  		Required   bool
    25  		Label      string
    26  		Options    []Option
    27  	}
    28  
    29  	BundleProduct struct {
    30  		Identifier string
    31  		Choices    []Choice
    32  		BasicProductData
    33  		Teaser TeaserData
    34  		Saleable
    35  	}
    36  
    37  	ActiveChoice struct {
    38  		Identifier string
    39  		Required   bool
    40  		Label      string
    41  		Product    BasicProduct
    42  		Qty        int
    43  	}
    44  
    45  	// BundleProductWithActiveChoices - A product with preselected choices
    46  	BundleProductWithActiveChoices struct {
    47  		BundleProduct
    48  		ActiveChoices map[Identifier]ActiveChoice
    49  	}
    50  
    51  	Identifier string
    52  
    53  	BundleConfiguration map[Identifier]ChoiceConfiguration
    54  
    55  	ChoiceConfiguration struct {
    56  		MarketplaceCode        string
    57  		VariantMarketplaceCode string
    58  		Qty                    int
    59  	}
    60  )
    61  
    62  var (
    63  	_                                BasicProduct = BundleProduct{}
    64  	ErrRequiredChoicesAreNotSelected              = errors.New("required choices are not selected")
    65  	ErrSelectedQuantityOutOfRange                 = errors.New("selected quantity is out of range")
    66  	ErrMarketplaceCodeDoNotExists                 = errors.New("selected marketplace code does not exist")
    67  )
    68  
    69  func (b BundleProduct) BaseData() BasicProductData {
    70  	return b.BasicProductData
    71  }
    72  
    73  func (b BundleProduct) TeaserData() TeaserData {
    74  	return b.Teaser
    75  }
    76  
    77  func (b BundleProduct) IsSaleable() bool {
    78  	return true
    79  }
    80  
    81  func (b BundleProduct) SaleableData() Saleable {
    82  	return b.Saleable
    83  }
    84  
    85  func (b BundleProduct) Type() string {
    86  	return TypeBundle
    87  }
    88  
    89  func (b BundleProductWithActiveChoices) Type() string {
    90  	return TypeBundleWithActiveChoices
    91  }
    92  
    93  func (b BundleProduct) GetIdentifier() string {
    94  	return b.Identifier
    95  }
    96  
    97  func (b BundleProduct) HasMedia(group string, usage string) bool {
    98  	media := findMediaInProduct(BasicProduct(b), group, usage)
    99  	return media != nil
   100  }
   101  
   102  func (b BundleProduct) GetMedia(group string, usage string) Media {
   103  	return *findMediaInProduct(BasicProduct(b), group, usage)
   104  }
   105  
   106  func (b BundleProduct) GetBundleProductWithActiveChoices(bundleConfiguration BundleConfiguration) (BundleProductWithActiveChoices, error) {
   107  	bundleProductWithActiveChoices := BundleProductWithActiveChoices{
   108  		BundleProduct: b,
   109  		ActiveChoices: make(map[Identifier]ActiveChoice, 0),
   110  	}
   111  
   112  	for _, choice := range b.Choices {
   113  		choiceConfig, ok := bundleConfiguration[Identifier(choice.Identifier)]
   114  		if !ok && choice.Required {
   115  			return BundleProductWithActiveChoices{}, ErrRequiredChoicesAreNotSelected
   116  		}
   117  
   118  		for _, option := range choice.Options {
   119  			if choiceConfig.MarketplaceCode != option.Product.BaseData().MarketPlaceCode {
   120  				continue
   121  			}
   122  
   123  			activeChoice, err := mapChoiceToActiveProduct(option, choice, choiceConfig)
   124  			if err != nil {
   125  				return BundleProductWithActiveChoices{}, fmt.Errorf("bundle product: %w", err)
   126  			}
   127  
   128  			bundleProductWithActiveChoices.ActiveChoices[Identifier(choice.Identifier)] = activeChoice
   129  		}
   130  
   131  		if _, ok := bundleProductWithActiveChoices.ActiveChoices[Identifier(choice.Identifier)]; !ok && choice.Required {
   132  			return BundleProductWithActiveChoices{}, ErrMarketplaceCodeDoNotExists
   133  		}
   134  	}
   135  
   136  	return bundleProductWithActiveChoices, nil
   137  }
   138  
   139  func mapChoiceToActiveProduct(option Option, possibleChoice Choice, selectedChoice ChoiceConfiguration) (ActiveChoice, error) {
   140  	activeChoice := ActiveChoice{}
   141  
   142  	quantity, err := getQuantity(option.MinQty, option.MaxQty, selectedChoice.Qty)
   143  
   144  	if err != nil {
   145  		return ActiveChoice{}, err
   146  	}
   147  
   148  	switch option.Product.Type() {
   149  	case TypeConfigurable:
   150  		activeChoice = ActiveChoice{
   151  			Identifier: possibleChoice.Identifier,
   152  			Product:    option.Product,
   153  			Qty:        quantity,
   154  			Label:      possibleChoice.Label,
   155  			Required:   possibleChoice.Required,
   156  		}
   157  
   158  		if configurable, ok := option.Product.(ConfigurableProduct); ok {
   159  			configurableWithActiveVariant, err := configurable.GetConfigurableWithActiveVariant(selectedChoice.VariantMarketplaceCode)
   160  			if err != nil {
   161  				return ActiveChoice{}, fmt.Errorf("error getting configurable with active variant: %w", err)
   162  			}
   163  
   164  			activeChoice.Product = configurableWithActiveVariant
   165  		}
   166  	case TypeSimple:
   167  		activeChoice = ActiveChoice{
   168  			Identifier: possibleChoice.Identifier,
   169  			Product:    option.Product,
   170  			Qty:        quantity,
   171  			Label:      possibleChoice.Label,
   172  			Required:   possibleChoice.Required,
   173  		}
   174  	}
   175  
   176  	return activeChoice, nil
   177  }
   178  
   179  func getQuantity(min, max, selected int) (int, error) {
   180  	if selected >= min && selected <= max {
   181  		return selected, nil
   182  	}
   183  
   184  	return 0, ErrSelectedQuantityOutOfRange
   185  }
   186  
   187  func (o *Option) UnmarshalJSON(optionData []byte) error {
   188  	option := &struct {
   189  		Product json.RawMessage
   190  		MinQty  int
   191  		MaxQty  int
   192  	}{}
   193  
   194  	err := json.Unmarshal(optionData, option)
   195  	if err != nil {
   196  		return errors.New("option product: " + err.Error())
   197  	}
   198  
   199  	product := &map[string]interface{}{}
   200  	err = json.Unmarshal(option.Product, product)
   201  	if err != nil {
   202  		return errors.New("option product: " + err.Error())
   203  	}
   204  
   205  	productType, ok := (*product)["Type"]
   206  
   207  	if !ok {
   208  		return errors.New("option product: type is not specified")
   209  	}
   210  
   211  	o.MinQty = option.MinQty
   212  	o.MaxQty = option.MaxQty
   213  
   214  	switch productType {
   215  	case TypeConfigurable:
   216  		configurableProduct := &ConfigurableProduct{}
   217  		err = json.Unmarshal(option.Product, configurableProduct)
   218  		if err != nil {
   219  			return errors.New("option product: " + err.Error())
   220  		}
   221  		o.Product = *configurableProduct
   222  	default:
   223  		simpleProduct := &SimpleProduct{}
   224  		err = json.Unmarshal(option.Product, simpleProduct)
   225  		if err != nil {
   226  			return errors.New("option product: " + err.Error())
   227  		}
   228  		o.Product = *simpleProduct
   229  	}
   230  	return nil
   231  }
   232  
   233  func (b BundleProductWithActiveChoices) ExtractBundleConfig() BundleConfiguration {
   234  	if len(b.ActiveChoices) == 0 {
   235  		return nil
   236  	}
   237  
   238  	config := make(BundleConfiguration)
   239  
   240  	for identifier, choice := range b.ActiveChoices {
   241  		if choice.Product.Type() == TypeSimple {
   242  			config[identifier] = ChoiceConfiguration{
   243  				MarketplaceCode: choice.Product.BaseData().MarketPlaceCode,
   244  				Qty:             choice.Qty,
   245  			}
   246  		}
   247  		if choice.Product.Type() == TypeConfigurableWithActiveVariant {
   248  			configurableWithActiveVariant, ok := choice.Product.(ConfigurableProductWithActiveVariant)
   249  			if ok {
   250  				config[identifier] = ChoiceConfiguration{
   251  					MarketplaceCode:        configurableWithActiveVariant.ConfigurableBaseData().MarketPlaceCode,
   252  					VariantMarketplaceCode: choice.Product.BaseData().MarketPlaceCode,
   253  					Qty:                    choice.Qty,
   254  				}
   255  			}
   256  		}
   257  	}
   258  
   259  	return config
   260  }
   261  
   262  // Equals compares the marketplace codes of all choices
   263  func (bc BundleConfiguration) Equals(other BundleConfiguration) bool {
   264  	if len(bc) != len(other) {
   265  		return false
   266  	}
   267  
   268  	for choiceID, cartChoiceConfig := range bc {
   269  		otherChoiceConfig, ok := other[choiceID]
   270  		if !ok {
   271  			return false
   272  		}
   273  
   274  		if cartChoiceConfig.MarketplaceCode != otherChoiceConfig.MarketplaceCode {
   275  			return false
   276  		}
   277  
   278  		if cartChoiceConfig.VariantMarketplaceCode != otherChoiceConfig.VariantMarketplaceCode {
   279  			return false
   280  		}
   281  
   282  		if cartChoiceConfig.Qty != otherChoiceConfig.Qty {
   283  			return false
   284  		}
   285  	}
   286  
   287  	return true
   288  }