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 }