Custom promotion: Buy products, get gift
A custom promotion for giving a gift item when buying products.
Read my blog here
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 :)