A custom promotion for giving a gift item when buying products.
Read my blog here
| using System; | |
| using System.Collections.Generic; | |
| using System.ComponentModel.DataAnnotations; | |
| using EPiServer.Commerce.Catalog.ContentTypes; | |
| using EPiServer.Commerce.Marketing; | |
| using EPiServer.Commerce.Marketing.DataAnnotations; | |
| using EPiServer.Commerce.Marketing.Promotions; | |
| using EPiServer.Core; | |
| using EPiServer.DataAnnotations; | |
| /// <summary> | |
| /// Class BuyProductGetGiftItems. | |
| /// </summary> | |
| /// <seealso cref="EPiServer.Commerce.Marketing.EntryPromotion" /> | |
| [ContentType(GUID = "8a820143-0b0e-46f4-a177-815c482e8510", GroupName = "entrypromotion", Order = 10500, DisplayName = "Buy product, get gift", Description = "Buy at least X items from categories/entries and get a gift.")] | |
| [ImageUrl("Images/SpendAmountGetGiftItems.png")] | |
| public class BuyProductGetGiftItems : EntryPromotion | |
| { | |
| /// <summary> | |
| /// Gets or sets the condition for the promotion that needs to be fulfilled before the discount is applied.. | |
| /// </summary> | |
| /// <value>The condition.</value> | |
| [Display(Order = 10)] | |
| [PromotionRegion("Condition")] | |
| public virtual PurchaseQuantity Condition { get; set; } | |
| /// <summary> | |
| /// Gets or sets the gift items list that will be applied. | |
| /// </summary> | |
| /// <value>The gift items.</value> | |
| [Display(Order = 20)] | |
| [PromotionRegion("Reward")] | |
| [AllowedTypes(typeof(VariationContent), typeof(PackageContent))] | |
| public virtual IList<ContentReference> GiftItems { get; set; } | |
| } |
| using System; | |
| using System.Collections.Generic; | |
| using System.Linq; | |
| using EPiServer.Commerce.Extensions; | |
| using EPiServer.Commerce.Marketing; | |
| using EPiServer.Commerce.Marketing.Extensions; | |
| using EPiServer.Commerce.Order; | |
| using EPiServer.Commerce.Validation; | |
| using EPiServer.Framework.Localization; | |
| using EPiServer.ServiceLocation; | |
| /// <summary> | |
| /// The processor responsible for evaluating if a promotion of type <see cref="T:BuyProductGetGiftItems" /> should | |
| /// apply a reward to an order group. | |
| /// </summary> | |
| [ServiceConfiguration(Lifecycle = ServiceInstanceScope.Singleton)] | |
| public class BuyProductGetGiftItemsProcessor : EntryPromotionProcessorBase<BuyProductGetGiftItems> | |
| { | |
| /// <summary> | |
| /// The fulfillment evaluator | |
| /// </summary> | |
| private readonly FulfillmentEvaluator fulfillmentEvaluator; | |
| /// <summary> | |
| /// The gift item factory | |
| /// </summary> | |
| private readonly GiftItemFactory giftItemFactory; | |
| /// <summary> | |
| /// The localization service | |
| /// </summary> | |
| private readonly LocalizationService localizationService; | |
| /// <summary> | |
| /// The target evaluator | |
| /// </summary> | |
| private readonly CollectionTargetEvaluator targetEvaluator; | |
| /// <summary> | |
| /// Initializes a new instance of the <see cref="BuyProductGetGiftItemsProcessor" /> class. | |
| /// </summary> | |
| /// <param name="targetEvaluator">The target evaluator.</param> | |
| /// <param name="fulfillmentEvaluator">The service that is used to evaluate the fulfillment status of the promotion.</param> | |
| /// <param name="giftItemFactory">The service that is used to get applicable gift items.</param> | |
| /// <param name="localizationService">Service to handle localization of text strings.</param> | |
| public BuyProductGetGiftItemsProcessor( | |
| CollectionTargetEvaluator targetEvaluator, | |
| FulfillmentEvaluator fulfillmentEvaluator, | |
| GiftItemFactory giftItemFactory, | |
| LocalizationService localizationService) | |
| { | |
| ParameterValidator.ThrowIfNull(() => targetEvaluator, targetEvaluator); | |
| ParameterValidator.ThrowIfNull(() => fulfillmentEvaluator, fulfillmentEvaluator); | |
| ParameterValidator.ThrowIfNull(() => giftItemFactory, giftItemFactory); | |
| ParameterValidator.ThrowIfNull(() => localizationService, localizationService); | |
| this.targetEvaluator = targetEvaluator; | |
| this.fulfillmentEvaluator = fulfillmentEvaluator; | |
| this.giftItemFactory = giftItemFactory; | |
| this.localizationService = localizationService; | |
| } | |
| /// <summary> | |
| /// Verify that the current promotion can potentially be fulfilled | |
| /// </summary> | |
| /// <param name="promotionData">The promotion to evaluate.</param> | |
| /// <param name="context">The context for the promotion processor evaluation.</param> | |
| /// <returns><c>true</c> if the current promotion can potentially be fulfilled; otherwise, <c>false</c>.</returns> | |
| /// <remarks>This method is intended to be a very quick pre-check to avoid doing more expensive operations. | |
| /// Used to verify basic things, for example a Buy-3-pay-for-2 promotion needs at least three items in the cart. | |
| /// If we have less than three we can skip further processing.</remarks> | |
| /// <exception cref="ArgumentNullException">Line or discount items is null.</exception> | |
| protected override bool CanBeFulfilled(BuyProductGetGiftItems promotionData, PromotionProcessorContext context) | |
| { | |
| IEnumerable<ILineItem> lineItems = this.GetLineItems(context.OrderForm); | |
| if (lineItems.Any() | |
| && (promotionData?.GiftItems != null && promotionData.GiftItems.Any())) | |
| { | |
| return promotionData.GiftItems.Any(); | |
| } | |
| return false; | |
| } | |
| /// <summary> | |
| /// Evaluates a promotion against an order form. Implementations should use context.OrderForm for evaluations. | |
| /// </summary> | |
| /// <param name="promotionData">The promotion to evaluate.</param> | |
| /// <param name="context">The context for the promotion processor evaluation.</param> | |
| /// <returns>A <see cref="T:EPiServer.Commerce.Marketing.RewardDescription" /> telling whether the promotion was fulfilled, | |
| /// which items the promotion was applied to and to which amount.</returns> | |
| /// <exception cref="ArgumentNullException">Applicable codes is null.</exception> | |
| /// <exception cref="OverflowException">The sum for the quantities is larger than <see cref="F:System.Decimal.MaxValue" />.</exception> | |
| protected override RewardDescription Evaluate( | |
| BuyProductGetGiftItems promotionData, | |
| PromotionProcessorContext context) | |
| { | |
| FulfillmentStatus fulfillmentStatus = promotionData.Condition.GetFulfillmentStatus( | |
| context.OrderForm, | |
| this.targetEvaluator, | |
| this.fulfillmentEvaluator); | |
| if (!fulfillmentStatus.HasFlag(FulfillmentStatus.Fulfilled)) | |
| { | |
| return this.NotFulfilledRewardDescription(promotionData, context, fulfillmentStatus); | |
| } | |
| IEnumerable<ILineItem> lineItems = this.GetLineItems(context.OrderForm); | |
| IList<string> applicableCodes = this.targetEvaluator.GetApplicableCodes( | |
| lineItems, | |
| promotionData.Condition.Items, | |
| true); | |
| if (!applicableCodes.Any()) | |
| { | |
| return this.NotFulfilledRewardDescription(promotionData, context, FulfillmentStatus.NotFulfilled); | |
| } | |
| IEnumerable<RedemptionDescription> redemptions = this.GetRedemptions( | |
| promotionData, | |
| context, | |
| applicableCodes); | |
| return RewardDescription.CreateGiftItemsReward( | |
| fulfillmentStatus, | |
| redemptions, | |
| promotionData, | |
| fulfillmentStatus.GetRewardDescriptionText(this.localizationService)); | |
| } | |
| /// <summary> | |
| /// Gets the items for a promotion. | |
| /// </summary> | |
| /// <param name="promotionData">The promotion data to get items for.</param> | |
| /// <returns>The promotion condition and reward items.</returns> | |
| protected override PromotionItems GetPromotionItems(BuyProductGetGiftItems promotionData) | |
| { | |
| return new PromotionItems( | |
| promotionData, | |
| new CatalogItemSelection(null, CatalogItemSelectionType.All, true), | |
| new CatalogItemSelection(promotionData.GiftItems, CatalogItemSelectionType.Specific, false)); | |
| } | |
| /// <summary> | |
| /// Gets the redemptions. | |
| /// </summary> | |
| /// <param name="promotionData">The promotion data.</param> | |
| /// <param name="context">The context.</param> | |
| /// <param name="applicableCodes">The applicable codes.</param> | |
| /// <returns>A list of <see cref="RedemptionDescription"/>.</returns> | |
| /// <exception cref="ArgumentNullException">Line items or applicable codes is null.</exception> | |
| /// <exception cref="OverflowException">The sum for the quantities is larger than <see cref="F:System.Decimal.MaxValue" />.</exception> | |
| protected IEnumerable<RedemptionDescription> GetRedemptions( | |
| BuyProductGetGiftItems promotionData, | |
| PromotionProcessorContext context, | |
| IEnumerable<string> applicableCodes) | |
| { | |
| decimal quantity = | |
| this.GetLineItems(context.OrderForm) | |
| .Where(li => applicableCodes.Contains(li.Code)) | |
| .Sum(li => li.Quantity); | |
| if (quantity < promotionData.Condition.RequiredQuantity) | |
| { | |
| return Enumerable.Empty<RedemptionDescription>(); | |
| } | |
| AffectedEntries giftItems = this.giftItemFactory.CreateGiftItems(promotionData.GiftItems, context); | |
| return giftItems == null ? Enumerable.Empty<RedemptionDescription>() : new[] { this.CreateRedemptionDescription(giftItems) }; | |
| } | |
| /// <summary> | |
| /// Not fulfilled reward description. Will be returned when CanBeFulfilled is false. | |
| /// </summary> | |
| /// <param name="promotionData">The promotion that was evaluated.</param> | |
| /// <param name="context">The context for the promotion processor evaluation.</param> | |
| /// <param name="fulfillmentStatus">The fulfillment level of the promotion.</param> | |
| /// <returns>A <see cref="T:EPiServer.Commerce.Marketing.RewardDescription" /> for the not fulfilled promotion.</returns> | |
| protected override RewardDescription NotFulfilledRewardDescription( | |
| BuyProductGetGiftItems promotionData, | |
| PromotionProcessorContext context, | |
| FulfillmentStatus fulfillmentStatus) | |
| { | |
| return RewardDescription.CreateGiftItemsReward( | |
| fulfillmentStatus, | |
| Enumerable.Empty<RedemptionDescription>(), | |
| promotionData, | |
| FulfillmentStatus.NotFulfilled.GetRewardDescriptionText(this.localizationService)); | |
| } |
| using System.Collections.Generic; | |
| using System.Linq; | |
| using EPiServer.Commerce.Validation; | |
| using EPiServer.Framework.Localization; | |
| using EPiServer.Validation; | |
| /// <summary> | |
| /// Class BuyProductGetGiftItemsValidator. | |
| /// </summary> | |
| /// <seealso cref="T:EPiServer.Validation.IValidate{Valtech.BeterBed.Web.Business.Marketing.BuyProductGetGiftItems}" /> | |
| public class BuyProductGetGiftItemsValidator : IValidate<BuyProductGetGiftItems> | |
| { | |
| /// <summary> | |
| /// The localization service | |
| /// </summary> | |
| private readonly LocalizationService localizationService; | |
| /// <summary> | |
| /// Initializes a new instance of the <see cref="T:Valtech.BeterBed.Web.Business.Marketing.BuyProductGetGiftItemsValidator" /> class. | |
| /// </summary> | |
| /// <param name="localizationService">The localization service.</param> | |
| public BuyProductGetGiftItemsValidator(LocalizationService localizationService) | |
| { | |
| ParameterValidator.ThrowIfNull(() => localizationService, localizationService); | |
| this.localizationService = localizationService; | |
| } | |
| /// <summary>Validates the specified promotion.</summary> | |
| /// <param name="promotion">The promotion that will be validated.</param> | |
| /// <returns>Validation errors for any empty collection property.</returns> | |
| public IEnumerable<ValidationError> Validate(BuyProductGetGiftItems promotion) | |
| { | |
| ParameterValidator.ThrowIfNull(() => promotion, promotion); | |
| List<ValidationError> validationErrors = new List<ValidationError>(); | |
| this.AddErrorIfNoGiftItem(promotion, validationErrors); | |
| return validationErrors; | |
| } | |
| /// <summary> | |
| /// Adds the error if no gift item. | |
| /// </summary> | |
| /// <param name="promotion">The promotion.</param> | |
| /// <param name="validationErrors">The validation errors.</param> | |
| private void AddErrorIfNoGiftItem(BuyProductGetGiftItems promotion, List<ValidationError> validationErrors) | |
| { | |
| if (promotion.GiftItems != null && promotion.GiftItems.Any()) | |
| { | |
| return; | |
| } | |
| List<ValidationError> validationErrorList = validationErrors; | |
| ValidationError validationError = new ValidationError(); | |
| validationError.Severity = ValidationErrorSeverity.Error; | |
| validationError.ValidationType = ValidationErrorType.StorageValidation; | |
| validationError.PropertyName = "GiftItems"; | |
| string errorMessage = this.localizationService.GetString("/commerce/validation/nogiftitem"); | |
| validationError.ErrorMessage = errorMessage; | |
| validationErrorList.Add(validationError); | |
| } | |
| } |
@JoelYourstone commented on Jun 29, 2017, 8:42 AM GMT+2:
@noshitsherlock then you need to add a property to BuyProductGetGiftItems.cs, another condition. Reflect a built in discount that has an amount condition to see if there are any help classes instead of just
int(can't recall which one it was in my head :)). Then you'd perhaps want to check that amount in CanBeFulfilled (and/or Evaluate, depending on how often you think it'll succeed) and then use that amount to determin how many times you want the discount to "fire", in GetRedemptions :)
Exactly, I added a new property to my GiftWithPurchasePromotion of type IList<Money> (renders amount fields per market connected to the campaign automatically). Then just checked for it in my Evaluate and update the fulfillmentstatus.
@noshitsherlock then you need to add a property to BuyProductGetGiftItems.cs, another condition. Reflect a built in discount that has an amount condition to see if there are any help classes instead of just
int(can't recall which one it was in my head :)). Then you'd perhaps want to check that amount in CanBeFulfilled (and/or Evaluate, depending on how often you think it'll succeed) and then use that amount to determin how many times you want the discount to "fire", in GetRedemptions :)