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 }