Skip to content

Instantly share code, notes, and snippets.

@jstemerdink
Created February 28, 2017 13:10
Show Gist options
  • Save jstemerdink/67cb557d2e6767f182a958940daff2b2 to your computer and use it in GitHub Desktop.
Save jstemerdink/67cb557d2e6767f182a958940daff2b2 to your computer and use it in GitHub Desktop.

Custom promotion: Buy products, get gift

A custom promotion for giving a gift item when buying products.

Read my blog here

Powered by ReSharper image

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
Copy link

@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 :)

@noshitsherlock
Copy link

noshitsherlock commented Jun 29, 2017

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment