flamingo.me/flamingo-commerce/v3@v3.11.0/product/infrastructure/fake/productservice.go (about) 1 package fake 2 3 import ( 4 "context" 5 "fmt" 6 "math/big" 7 "math/rand" 8 "os" 9 "sort" 10 "strconv" 11 "strings" 12 "time" 13 14 "flamingo.me/flamingo/v3/framework/web" 15 16 "flamingo.me/flamingo/v3/framework/flamingo" 17 18 priceDomain "flamingo.me/flamingo-commerce/v3/price/domain" 19 "flamingo.me/flamingo-commerce/v3/product/domain" 20 ) 21 22 var ( 23 brands = []string{ 24 "Apple", 25 "Bose", 26 "Dior", 27 "Hugo Boss", 28 } 29 30 inflightLoyaltyAmount = 500.0 31 hdLoyaltyAmount = 600.0 32 ) 33 34 // ProductService is just mocking stuff 35 type ProductService struct { 36 currencyCode string 37 testDataFiles map[string]string 38 logger flamingo.Logger 39 deliverDefaultProducts bool 40 deliveryCodes []string 41 } 42 43 // Inject dependencies 44 func (ps *ProductService) Inject(logger flamingo.Logger, 45 c *struct { 46 CurrencyCode string `inject:"config:commerce.product.fakeservice.currency,optional"` 47 TestDataFolder string `inject:"config:commerce.product.fakeservice.jsonTestDataFolder,optional"` 48 DeliveryDefaultProducts bool `inject:"config:commerce.product.fakeservice.defaultProducts,optional"` 49 DeliveryCodes []string `inject:"config:commerce.product.fakeservice.deliveryCodes,optional"` 50 }, 51 ) *ProductService { 52 ps.logger = logger 53 if c != nil { 54 ps.currencyCode = c.CurrencyCode 55 if len(c.TestDataFolder) > 0 { 56 ps.testDataFiles = registerTestData(c.TestDataFolder, ps.logger) 57 } 58 59 ps.deliverDefaultProducts = c.DeliveryDefaultProducts 60 ps.deliveryCodes = c.DeliveryCodes 61 } 62 63 return ps 64 } 65 66 // Get returns a product struct 67 func (ps *ProductService) Get(_ context.Context, marketplaceCode string) (domain.BasicProduct, error) { 68 switch marketplaceCode { 69 case "fake_configurable": 70 return ps.getFakeConfigurableWithVariants(marketplaceCode), nil 71 72 case "fake_configurable_with_active_variant": 73 return ps.getFakeConfigurableWithActiveVariant(marketplaceCode), nil 74 75 case "fake_simple": 76 return ps.FakeSimple(marketplaceCode, false, false, false, true, false), nil 77 78 case "fake_simple_with_fixed_price": 79 return ps.FakeSimple(marketplaceCode, false, false, false, true, true), nil 80 81 case "fake_fixed_simple_without_discounts": 82 return ps.FakeSimple(marketplaceCode, false, false, false, false, true), nil 83 84 case "fake_simple_out_of_stock": 85 return ps.FakeSimple(marketplaceCode, false, false, true, true, false), nil 86 case "fake_bundle": 87 return ps.FakeBundle(marketplaceCode, false, false, true, true, false), nil 88 default: 89 jsonProduct, err := ps.getProductFromJSON(marketplaceCode) 90 if err != nil { 91 if _, isProductNotFoundError := err.(domain.ProductNotFound); !isProductNotFoundError { 92 return nil, err 93 } 94 } else { 95 return jsonProduct, nil 96 } 97 } 98 99 marketPlaceCodes := ps.GetMarketPlaceCodes() 100 return nil, domain.ProductNotFound{ 101 MarketplaceCode: "Code " + marketplaceCode + " Not implemented in FAKE: Only following codes should be used" + strings.Join(marketPlaceCodes, ", "), 102 } 103 } 104 105 // FakeSimple generates a simple fake product 106 func (ps *ProductService) FakeSimple(marketplaceCode string, isNew bool, isExclusive bool, isOutOfStock bool, isDiscounted bool, hasFixedPrice bool) domain.SimpleProduct { 107 product := domain.SimpleProduct{} 108 product.Title = "TypeSimple product" 109 ps.addBasicData(&product.BasicProductData) 110 111 product.Saleable = domain.Saleable{ 112 IsSaleable: true, 113 SaleableTo: time.Now().Add(time.Hour * time.Duration(1)), 114 SaleableFrom: time.Now().Add(time.Hour * time.Duration(-1)), 115 ActiveLoyaltyPrice: &domain.LoyaltyPriceInfo{ 116 Type: "AwesomeLoyaltyProgram", 117 Default: priceDomain.NewFromFloat(inflightLoyaltyAmount, "BonusPoints"), 118 }, 119 LoyaltyPrices: []domain.LoyaltyPriceInfo{ 120 { 121 Type: "AwesomeLoyaltyProgram", 122 Default: priceDomain.NewFromFloat(inflightLoyaltyAmount, "BonusPoints"), 123 Context: domain.PriceContext{DeliveryCode: "inflight"}, 124 }, 125 { 126 Type: "AwesomeLoyaltyProgram", 127 Default: priceDomain.NewFromFloat(hdLoyaltyAmount, "BonusPoints"), 128 Context: domain.PriceContext{DeliveryCode: "delivery____domestichome"}, 129 }, 130 }, 131 LoyaltyEarnings: []domain.LoyaltyEarningInfo{ 132 { 133 Type: "AwesomeLoyaltyProgram", 134 Default: priceDomain.NewFromFloat(23.23, "BonusPoints"), 135 }, 136 }, 137 } 138 139 discountedPrice := 0.0 140 if isDiscounted { 141 discountedPrice = 10.49 + float64(rand.Intn(10)) 142 if hasFixedPrice { 143 discountedPrice = 10.49 144 } 145 } 146 147 defaultPrice := 20.99 + float64(rand.Intn(10)) 148 if hasFixedPrice { 149 defaultPrice = 20.99 150 } 151 152 product.ActivePrice = ps.getPrice(defaultPrice, discountedPrice) 153 product.MarketPlaceCode = marketplaceCode 154 155 product.CreatedAt = time.Date(2019, 6, 29, 00, 00, 00, 00, time.UTC) 156 product.UpdatedAt = time.Date(2019, 7, 29, 12, 00, 00, 00, time.UTC) 157 product.VisibleFrom = time.Date(2019, 7, 29, 12, 00, 00, 00, time.UTC) 158 product.VisibleTo = time.Now().Add(time.Hour * time.Duration(10)) 159 160 product.Teaser = domain.TeaserData{ 161 ShortDescription: product.ShortDescription, 162 ShortTitle: product.Title, 163 URLSlug: product.BaseData().Attributes["urlSlug"].Value(), 164 Media: product.Media, 165 MarketPlaceCode: product.MarketPlaceCode, 166 TeaserPrice: domain.PriceInfo{ 167 Default: priceDomain.NewFromFloat(9.99, "SD").GetPayable(), 168 }, 169 TeaserLoyaltyPriceInfo: &domain.LoyaltyPriceInfo{ 170 Type: "AwesomeLoyaltyProgram", 171 Default: priceDomain.NewFromFloat(500, "BonusPoints"), 172 }, 173 TeaserLoyaltyEarningInfo: &domain.LoyaltyEarningInfo{ 174 Type: "AwesomeLoyaltyProgram", 175 Default: priceDomain.NewFromFloat(23.23, "BonusPoints"), 176 }, 177 Badges: product.Badges, 178 } 179 180 if isNew { 181 product.BasicProductData.IsNew = true 182 } 183 184 if isExclusive { 185 product.Attributes["exclusiveProduct"] = domain.Attribute{ 186 RawValue: "30002654_yes", 187 Code: "exclusiveProduct", 188 } 189 } 190 191 product.Stock = ps.getStock(true, domain.StockLevelInStock, 999) 192 193 if isOutOfStock { 194 product.Stock = ps.getStock(false, domain.StockLevelOutOfStock, 0) 195 } 196 197 return product 198 } 199 200 func (ps *ProductService) getStock(inStock bool, level string, amount int) []domain.Stock { 201 stock := make([]domain.Stock, 0) 202 203 for _, code := range ps.deliveryCodes { 204 stock = append(stock, domain.Stock{ 205 Amount: amount, 206 InStock: inStock, 207 Level: level, 208 DeliveryCode: code, 209 }) 210 } 211 212 return stock 213 } 214 215 // GetMarketPlaceCodes returns list of available marketplace codes which are supported by this fakeservice 216 func (ps *ProductService) GetMarketPlaceCodes() []string { 217 if !ps.deliverDefaultProducts { 218 return ps.jsonProductCodes() 219 } 220 221 marketPlaceCodes := []string{ 222 "fake_configurable", 223 "fake_configurable_with_active_variant", 224 "fake_simple", 225 "fake_simple_with_fixed_price", 226 "fake_simple_out_of_stock", 227 "fake_fixed_simple_without_discounts", 228 } 229 230 return append(marketPlaceCodes, ps.jsonProductCodes()...) 231 } 232 233 func (ps *ProductService) getFakeConfigurable(marketplaceCode string) domain.ConfigurableProduct { 234 product := domain.ConfigurableProduct{} 235 product.Title = "TypeConfigurable product" 236 ps.addBasicData(&product.BasicProductData) 237 product.MarketPlaceCode = marketplaceCode 238 product.Identifier = marketplaceCode + "_identifier" 239 product.Teaser.TeaserPrice = ps.getPrice(30.99+float64(rand.Intn(10)), 20.49+float64(rand.Intn(10))) 240 product.Teaser.Badges = product.Badges 241 product.VariantVariationAttributes = []string{"color", "size"} 242 product.VariantVariationAttributesSorting = map[string][]string{ 243 "size": {"M", "L"}, 244 "color": {"red", "white", "black"}, 245 } 246 247 return product 248 } 249 250 func (ps *ProductService) getFakeConfigurableWithVariants(marketplaceCode string) domain.ConfigurableProduct { 251 product := ps.getFakeConfigurable(marketplaceCode) 252 product.RetailerCode = "retailer" 253 254 variants := []struct { 255 marketplaceCode string 256 title string 257 attributes domain.Attributes 258 stock []domain.Stock 259 badges []domain.Badge 260 }{ 261 {"shirt-red-s", "Shirt Red S", domain.Attributes{ 262 "size": domain.Attribute{RawValue: "S", Code: "size", CodeLabel: "Size", Label: "S"}, 263 "manufacturerColor": domain.Attribute{RawValue: "red", Code: "manufacturerColor", CodeLabel: "Manufacturer Color", Label: "Red"}, 264 "manufacturerColorCode": domain.Attribute{RawValue: "#ff0000", Code: "manufacturerColorCode", CodeLabel: "Manufacturer Color Code", Label: "BloodRed"}}, 265 ps.getStock(true, domain.StockLevelInStock, 999), 266 []domain.Badge{{Code: "new", Label: "New"}}, 267 }, 268 {"shirt-white-s", "Shirt White S", domain.Attributes{ 269 "size": domain.Attribute{RawValue: "S", Code: "size", CodeLabel: "Size", Label: "S"}, 270 "manufacturerColor": domain.Attribute{RawValue: "white", Code: "manufacturerColor", CodeLabel: "Manufacturer Color", Label: "White"}, 271 "manufacturerColorCode": domain.Attribute{RawValue: "#ffffff", Code: "manufacturerColorCode", CodeLabel: "Manufacturer Color Code", Label: "SnowWhite"}}, 272 ps.getStock(true, domain.StockLevelInStock, 999), 273 nil, 274 }, 275 {"shirt-white-m", "Shirt White M", domain.Attributes{ 276 "size": domain.Attribute{RawValue: "M", Code: "size", CodeLabel: "Size", Label: "M"}, 277 "color": domain.Attribute{RawValue: "white", Code: "color", CodeLabel: "Color", Label: "White"}}, 278 ps.getStock(true, domain.StockLevelInStock, 999), 279 nil, 280 }, 281 {"shirt-black-m", "Shirt Black M", domain.Attributes{ 282 "size": domain.Attribute{RawValue: "M", Code: "size", CodeLabel: "Size", Label: "M"}, 283 "manufacturerColor": domain.Attribute{RawValue: "blue", Code: "manufacturerColor", CodeLabel: "Manufacturer Color", Label: "Blue"}, 284 "manufacturerColorCode": domain.Attribute{RawValue: "#0000ff", Code: "manufacturerColorCode", CodeLabel: "Manufacturer Color Code", Label: "SkyBlue"}}, 285 ps.getStock(true, domain.StockLevelInStock, 999), 286 nil, 287 }, 288 {"shirt-black-l", "Shirt Black L", domain.Attributes{ 289 "size": domain.Attribute{RawValue: "L", Code: "size", CodeLabel: "Size", Label: "L"}, 290 "color": domain.Attribute{RawValue: "black", Code: "color", CodeLabel: "Color", Label: "Black"}}, 291 ps.getStock(true, domain.StockLevelInStock, 999), 292 nil, 293 }, 294 {"shirt-red-l", "Shirt Red L", domain.Attributes{ 295 "size": domain.Attribute{RawValue: "L", Code: "size", CodeLabel: "Size", Label: "L"}, 296 "color": domain.Attribute{RawValue: "red", Code: "color", CodeLabel: "Color", Label: "Red"}}, 297 ps.getStock(false, domain.StockLevelOutOfStock, 0), 298 nil, 299 }, 300 {"shirt-red-m", "Shirt Red M", domain.Attributes{ 301 "size": domain.Attribute{RawValue: "M", Code: "size", CodeLabel: "Size", Label: "M"}, 302 "color": domain.Attribute{RawValue: "red", Code: "color", CodeLabel: "Color", Label: "Red"}}, 303 ps.getStock(false, domain.StockLevelOutOfStock, 0), 304 nil, 305 }, 306 } 307 308 for _, variant := range variants { 309 simpleVariant := ps.fakeVariant(variant.marketplaceCode) 310 simpleVariant.Title = variant.title 311 312 for key, attr := range variant.attributes { 313 simpleVariant.Attributes[key] = attr 314 simpleVariant.BasicProductData.Attributes[key] = attr 315 } 316 317 simpleVariant.Stock = variant.stock 318 319 // Give new images for variants with custom colors 320 if simpleVariant.Attributes.HasAttribute("manufacturerColorCode") { 321 manufacturerColorCode := simpleVariant.Attributes["manufacturerColorCode"].Value() 322 manufacturerColorCode = strings.TrimPrefix(manufacturerColorCode, "#") 323 image := domain.Media{Type: "image-external", Reference: "http://dummyimage.com/1024x768/000/" + manufacturerColorCode, Usage: "detail"} 324 simpleVariant.Media[0] = image 325 product.Media = []domain.Media{image} 326 product.Teaser.Media = []domain.Media{image} 327 } 328 simpleVariant.Badges = variant.badges 329 330 product.Variants = append(product.Variants, simpleVariant) 331 } 332 333 return product 334 } 335 336 func (ps *ProductService) getFakeConfigurableWithActiveVariant(marketplaceCode string) domain.ConfigurableProductWithActiveVariant { 337 configurable := ps.getFakeConfigurableWithVariants(marketplaceCode) 338 product := domain.ConfigurableProductWithActiveVariant{ 339 Identifier: configurable.Identifier, 340 BasicProductData: configurable.BasicProductData, 341 Teaser: configurable.Teaser, 342 VariantVariationAttributes: configurable.VariantVariationAttributes, 343 VariantVariationAttributesSorting: configurable.VariantVariationAttributesSorting, 344 Variants: configurable.Variants, 345 ActiveVariant: configurable.Variants[4], // shirt-black-l 346 } 347 348 product.Teaser.TeaserPrice = product.ActiveVariant.ActivePrice 349 350 return product 351 } 352 353 func (ps *ProductService) fakeVariant(marketplaceCode string) domain.Variant { 354 var simpleVariant domain.Variant 355 simpleVariant.Attributes = make(map[string]domain.Attribute) 356 357 ps.addBasicData(&simpleVariant.BasicProductData) 358 359 simpleVariant.ActivePrice = ps.getPrice(30.99+float64(rand.Intn(10)), 20.49+float64(rand.Intn(10))) 360 simpleVariant.MarketPlaceCode = marketplaceCode 361 simpleVariant.IsSaleable = true 362 363 return simpleVariant 364 } 365 366 func (ps *ProductService) addBasicData(product *domain.BasicProductData) { 367 product.ShortDescription = "Short Description" 368 product.Description = "Description" 369 product.Keywords = []string{"keywords"} 370 371 product.Media = append(product.Media, domain.Media{Type: "image-external", Reference: "http://dummyimage.com/1024x768/000/fff", Usage: "detail"}) 372 product.Media = append(product.Media, domain.Media{Type: "image-external", Reference: "http://dummyimage.com/200x200/000/fff", Usage: "list"}) 373 374 brand := brands[rand.Intn(len(brands))] 375 brandValue := web.URLTitle(brand) 376 product.Attributes = domain.Attributes{ 377 "brandCode": domain.Attribute{Code: "brandCode", Label: brand, RawValue: brandValue}, 378 "brandName": domain.Attribute{Code: "brandName", Label: brand, RawValue: brandValue}, 379 "collectionOption": domain.Attribute{Code: "collectionOption", Label: "Collection option", RawValue: []interface{}{"departure", "arrival"}}, 380 "urlSlug": domain.Attribute{Code: "urlSlug", Label: "product-slug", RawValue: "product-slug"}, 381 } 382 383 product.RetailerCode = "retailer" 384 product.RetailerName = "Test Retailer" 385 product.RetailerSku = "12345sku" 386 387 categoryTeaser1 := domain.CategoryTeaser{ 388 Path: "Testproducts", 389 Name: "Testproducts", 390 Code: "testproducts", 391 } 392 categoryTeaser2 := domain.CategoryTeaser{ 393 Path: "Testproducts/Fake/Configurable", 394 Name: "Configurable", 395 Code: "configurable", 396 } 397 badges := []domain.Badge{ 398 { 399 Code: "new", 400 Label: "New", 401 }, 402 } 403 product.Categories = append(product.Categories, categoryTeaser1) 404 product.Categories = append(product.Categories, categoryTeaser2) 405 product.MainCategory = categoryTeaser1 406 product.Badges = badges 407 } 408 409 func (ps *ProductService) getPrice(defaultP float64, discounted float64) domain.PriceInfo { 410 defaultP, _ = strconv.ParseFloat(fmt.Sprintf("%.2f", defaultP), 64) 411 discounted, _ = strconv.ParseFloat(fmt.Sprintf("%.2f", discounted), 64) 412 413 var price domain.PriceInfo 414 currency := "EUR" 415 if ps.currencyCode != "" { 416 currency = ps.currencyCode 417 } 418 419 price.Default = priceDomain.NewFromFloat(defaultP, currency).GetPayable() 420 if discounted > 0 { 421 price.Discounted = priceDomain.NewFromFloat(discounted, currency).GetPayable() 422 price.DiscountText = "Super test campaign" 423 price.IsDiscounted = true 424 } 425 price.ActiveBase = *big.NewFloat(1) 426 price.ActiveBaseAmount = *big.NewFloat(10) 427 price.ActiveBaseUnit = "ml" 428 return price 429 } 430 431 func (ps *ProductService) getProductFromJSON(code string) (domain.BasicProduct, error) { 432 file, ok := ps.testDataFiles[code] 433 434 if !ok { 435 return nil, &domain.ProductNotFound{MarketplaceCode: code} 436 } 437 438 jsonBytes, err := os.ReadFile(file) 439 if err != nil { 440 return nil, err 441 } 442 443 return unmarshalJSONProduct(jsonBytes) 444 } 445 446 // jsonProductCodes returns an ordered list of the json product codes 447 func (ps *ProductService) jsonProductCodes() []string { 448 keys := make([]string, 0, len(ps.testDataFiles)) 449 for k := range ps.testDataFiles { 450 keys = append(keys, k) 451 } 452 sort.Strings(keys) 453 return keys 454 } 455 456 // FakeBundle generates a bundle fake product 457 func (ps *ProductService) FakeBundle(marketplaceCode string, isNew bool, isExclusive bool, isOutOfStock bool, isDiscounted bool, hasFixedPrice bool) domain.BundleProduct { 458 product := domain.BundleProduct{} 459 product.Title = "TypeBundle product" 460 ps.addBasicData(&product.BasicProductData) 461 462 product.Saleable = domain.Saleable{ 463 IsSaleable: true, 464 SaleableTo: time.Now().Add(time.Hour * time.Duration(1)), 465 SaleableFrom: time.Now().Add(time.Hour * time.Duration(-1)), 466 ActiveLoyaltyPrice: &domain.LoyaltyPriceInfo{ 467 Type: "AwesomeLoyaltyProgram", 468 Default: priceDomain.NewFromFloat(inflightLoyaltyAmount, "BonusPoints"), 469 }, 470 LoyaltyPrices: []domain.LoyaltyPriceInfo{ 471 { 472 Type: "AwesomeLoyaltyProgram", 473 Default: priceDomain.NewFromFloat(inflightLoyaltyAmount, "BonusPoints"), 474 Context: domain.PriceContext{DeliveryCode: "inflight"}, 475 }, 476 { 477 Type: "AwesomeLoyaltyProgram", 478 Default: priceDomain.NewFromFloat(hdLoyaltyAmount, "BonusPoints"), 479 Context: domain.PriceContext{DeliveryCode: "delivery____domestichome"}, 480 }, 481 }, 482 LoyaltyEarnings: []domain.LoyaltyEarningInfo{ 483 { 484 Type: "AwesomeLoyaltyProgram", 485 Default: priceDomain.NewFromFloat(23.23, "BonusPoints"), 486 }, 487 }, 488 } 489 490 discountedPrice := 0.0 491 if isDiscounted { 492 discountedPrice = 10.49 + float64(rand.Intn(10)) 493 if hasFixedPrice { 494 discountedPrice = 10.49 495 } 496 } 497 498 defaultPrice := 21.37 + float64(rand.Intn(10)) 499 if hasFixedPrice { 500 defaultPrice = 20.99 501 } 502 503 product.ActivePrice = ps.getPrice(defaultPrice, discountedPrice) 504 product.MarketPlaceCode = marketplaceCode 505 506 product.CreatedAt = time.Date(2019, 6, 29, 00, 00, 00, 00, time.UTC) 507 product.UpdatedAt = time.Date(2019, 7, 29, 12, 00, 00, 00, time.UTC) 508 product.VisibleFrom = time.Date(2019, 7, 29, 12, 00, 00, 00, time.UTC) 509 product.VisibleTo = time.Now().Add(time.Hour * time.Duration(10)) 510 511 product.Teaser = domain.TeaserData{ 512 ShortDescription: product.ShortDescription, 513 ShortTitle: product.Title, 514 URLSlug: product.BaseData().Attributes["urlSlug"].Value(), 515 Media: product.Media, 516 MarketPlaceCode: product.MarketPlaceCode, 517 TeaserPrice: domain.PriceInfo{ 518 Default: priceDomain.NewFromFloat(9.99, "SD").GetPayable(), 519 }, 520 TeaserLoyaltyPriceInfo: &domain.LoyaltyPriceInfo{ 521 Type: "AwesomeLoyaltyProgram", 522 Default: priceDomain.NewFromFloat(500, "BonusPoints"), 523 }, 524 TeaserLoyaltyEarningInfo: &domain.LoyaltyEarningInfo{ 525 Type: "AwesomeLoyaltyProgram", 526 Default: priceDomain.NewFromFloat(23.23, "BonusPoints"), 527 }, 528 Badges: product.Badges, 529 } 530 531 if isNew { 532 product.BasicProductData.IsNew = true 533 } 534 535 if isExclusive { 536 product.Attributes["exclusiveProduct"] = domain.Attribute{ 537 RawValue: "30002654_yes", 538 Code: "exclusiveProduct", 539 } 540 } 541 542 product.Stock = ps.getStock(true, domain.StockLevelInStock, 999) 543 544 if isOutOfStock { 545 product.Stock = ps.getStock(false, domain.StockLevelOutOfStock, 0) 546 } 547 548 product.Choices = []domain.Choice{ 549 { 550 Identifier: "identifier1", 551 Required: true, 552 Label: "first choice", 553 Options: []domain.Option{ 554 { 555 MinQty: 1, 556 MaxQty: 1, 557 Product: ps.FakeSimple("simple_option1", false, false, false, true, true), 558 }, 559 { 560 MinQty: 1, 561 MaxQty: 1, 562 Product: ps.FakeSimple("simple_option2", false, false, false, true, true), 563 }, 564 }, 565 }, 566 { 567 Identifier: "identifier2", 568 Required: true, 569 Label: "second choice", 570 Options: []domain.Option{ 571 { 572 MinQty: 1, 573 MaxQty: 1, 574 Product: ps.getFakeConfigurableWithVariants("configurable_option1"), 575 }, 576 { 577 MinQty: 1, 578 MaxQty: 1, 579 Product: ps.getFakeConfigurableWithVariants("configurable_option2"), 580 }, 581 }, 582 }, 583 } 584 585 return product 586 }