Files
shop/pricerule/checkout.go
florianschlegel 45c0e91631 Feature/promo cumulation (#6)
* feat: adjust cumulation so that multiple vouchers are allowed on cart but only the single best one is being applied on each item

* clean up debug logs

* minor cleanup on tests

* revert debug changes made to existing tests

* feat: add option to skip voucher for already discounted articles

* minor renamings

* feat: add flag to exclude employees from  vouchers

* feat: add test for inclusion/exclusion of customer type

* add flag ExcludesEmployeesForVoucher
2019-09-18 10:34:53 +02:00

281 lines
12 KiB
Go

package pricerule
import "sort"
import "time"
//------------------------------------------------------------------
// ~ PUBLIC TYPES
//------------------------------------------------------------------
const (
ValidationPriceRuleMinimumAmount TypeRuleValidationMsg = "minimum_order_amount_not_reached" // if sum of ALL applicable prices*qtys < threshold
ValidationPriceRuleMinimumApplicableAmount TypeRuleValidationMsg = "minimum_order_applicable_amount_not_reached" // if sum of applicable prices*qtys < threshold
ValidationPriceRuleIncludeProductGroupsNotMatching TypeRuleValidationMsg = "include_product_groups_not_matching"
ValidationPriceRuleExcludeProductGroupsNotMatching TypeRuleValidationMsg = "exclude_product_groups_not_matching"
ValidationPriceRuleIncludeCustomerGroupsNotMatching TypeRuleValidationMsg = "include_customer_groups_not_matching"
ValidationPriceRuleExcludeCustomerGroupsNotMatching TypeRuleValidationMsg = "exclude_customer_groups_not_matching"
ValidationPriceRuleMaxUsages TypeRuleValidationMsg = "max_usages_reached"
ValidationPriceRuleMaxUsagesPerCustomer TypeRuleValidationMsg = "max_usages_per_customer_reached"
ValidationPriceRuleExpired TypeRuleValidationMsg = "pricerule_expired"
ValidationPriceRuleNotValidYet TypeRuleValidationMsg = "pricerule_not_valid_yet"
ValidationVoucherUnknown TypeRuleValidationMsg = "voucher_unknown"
ValidationVoucherPersonalized TypeRuleValidationMsg = "voucher_only_for_customer"
ValidationVoucherAlreadyUsed TypeRuleValidationMsg = "voucher_already_used"
ValidationVoucherNotApplicableToAlreadyDiscountedItemsByWebshop TypeRuleValidationMsg = "voucher_not_applicable_to_already_discounted_items_by_webshop"
ValidationVoucherNotApplicableToAlreadyDiscountedItemsBySap TypeRuleValidationMsg = "voucher_not_applicable_to_already_discounted_items_by_sap"
ValidationPreviouslyAppliedRuleBlock TypeRuleValidationMsg = "rule_blocked_by_previously_applied_rule" //rule application blocked
ValidationPriceRuleOK TypeRuleValidationMsg = "price_rule_ok"
ValidationPriceRuleCheckoutAttributesMismatch TypeRuleValidationMsg = "pricerule_checkout_attributes_missmatch"
ValidationPriceRuleQuantityThresholdNotMet TypeRuleValidationMsg = "qty_below_pricerule_quantity_threshold"
ValidationPriceRuleBonusValueTooHigh TypeRuleValidationMsg = "bonus_value_too_high"
ValidationPriceRuleBlacklist TypeRuleValidationMsg = "products_blacklisted"
ValidationPriceRuleNotForCatalogueCalculation TypeRuleValidationMsg = "not_for_catalogue_calculation"
ValidationArticleAlreadyDiscountedOnSAP TypeRuleValidationMsg = "article_already_discounted"
)
//------------------------------------------------------------------
// ~ PUBLIC FUNCTIONS
//------------------------------------------------------------------
// PickApplicableVouchers -
func PickApplicableVouchers(candidateVoucherCodes []string, articleCollection *ArticleCollection, checkoutAttributes []string, customProvider PriceRuleCustomProvider) (applicablePriceRules map[string]*PriceRule, err error) {
applicablePriceRules = make(map[string]*PriceRule)
for _, code := range candidateVoucherCodes {
ok, _ := ValidateVoucher(code, articleCollection, checkoutAttributes)
if ok == true {
voucher, voucherPriceRule, getErr := GetVoucherAndPriceRule(code, customProvider)
//check if exists
if getErr != nil || voucher.VoucherCode != code {
err = getErr
return
}
applicablePriceRules[code] = voucherPriceRule
}
}
return
}
// ValidateVoucher - validates a voucher code and returns o = true or a validation message when ok = false
// if articleCollection is not provided it will only check non-articleCollection related conditions
// if customerID == "" we assume not-logged in or guest
//
// validationMessage is
//
// - ValidationPriceRuleOK - validation passed
//
// - ValidationVoucherUnknown - can not find voucher by code or voucher object corrupted
//
// - ValidationVoucherPersonalized - voucher personalized, but customer unknown (guest) or customer ids not matching
//
// - ValidationVoucherAlreadyUsed - PERSONALIZED voucher redeemed - (used on a placed/finalized articleCollection)
//
// - ValidationPriceRuleMaxUsages - priceRule.MaxUses <= priceRule.UsageHistory.TotalUsages
//
// - ValidationPriceRuleMaxUsagesPerCustomer - priceRule.MaxUsesPerCustomer <= customerUsages
//
// - ValidationPriceRuleMinimumAmount - minimum articleCollection amount not met
//
// - ValidationPriceRuleIncludeProductGroupsNotMatching - if no matching products on articleCollection - articleCollection can only be applied to certain product groups ...
//
// - ValidationPriceRuleExcludeProductGroupsNotMatching - if all the articleCollection items are included in the excluded product groups list - for example rule only for non-salwe items, but all items are sale ones
//
// - ValidationPriceRuleIncludeCustomerGroupsNotMatching - not the right customer group - for example rule only for employees
//
// - ValidationPriceRuleExcludeCustomerGroupsNotMatching - can not be applied for customers in the group ... for example rule not for employees
//
// - ValidationPreviouslyAppliedRuleBlock - a previously applied rule (with priority number higher) has a property set to true ... no further rules can be applied
func ValidateVoucher(voucherCode string, articleCollection *ArticleCollection, checkoutAttributes []string) (ok bool, validationMessage TypeRuleValidationMsg) {
//check if voucher is for customer or generic/guest
//get voucher
calculationParameters := &CalculationParameters{}
calculationParameters.articleCollection = articleCollection
calculationParameters.isCatalogCalculation = false
calculationParameters.checkoutAttributes = checkoutAttributes
customerID := articleCollection.CustomerID
voucher, voucherPriceRule, err := GetVoucherAndPriceRule(voucherCode, nil)
//check if exists
if err != nil || voucher.VoucherCode != voucherCode {
return false, ValidationVoucherUnknown
}
//check if voucher personalized
if voucher.VoucherType == VoucherTypePersonalized {
//no customer info atm, guest or not logged in yet ...
if len(customerID) == 0 && len(voucher.CustomerID) != 0 {
return false, ValidationVoucherPersonalized
}
}
//check if voucher was redeemed already
if !voucher.TimeRedeemed.IsZero() && voucher.VoucherType == VoucherTypePersonalized {
return false, ValidationVoucherAlreadyUsed
}
if time.Now().After(voucherPriceRule.ValidTo) {
return false, ValidationPriceRuleExpired
}
if time.Now().Before(voucherPriceRule.ValidFrom) {
return false, ValidationPriceRuleNotValidYet
}
//--------------------------------------------------------------
// PriceRule
//--------------------------------------------------------------
//find groups for customer
groupIDsForCustomer := GetGroupsIDSForItem(articleCollection.CustomerID, CustomerGroup)
if len(groupIDsForCustomer) == 0 {
groupIDsForCustomer = []string{}
}
calculationParameters.groupIDsForCustomer = groupIDsForCustomer
//find the groupIds for articleCollection items
productGroupIDsPerPosition := getProductGroupIDsPerPosition(articleCollection, false)
calculationParameters.productGroupIDsPerPosition = productGroupIDsPerPosition
//remove all group limitations if bonus voucher
if voucherPriceRule.Type == TypeBonusVoucher {
//no products should be blacklisted
calculationParameters.blacklistedItemIDs = []string{}
// the bonus voucher should be applicable on shipping costs as well
calculationParameters.shippingGroupIDs = []string{}
}
ok, priceRuleFailReason := validatePriceRuleForOrder(*voucherPriceRule, calculationParameters, OrderDiscounts{})
if !ok {
return false, priceRuleFailReason
}
ok, priceRuleFailReason = checkPreviouslyAppliedRules(voucherPriceRule, voucher, calculationParameters)
if !ok {
return false, priceRuleFailReason
}
return true, ValidationPriceRuleOK
}
// CommitDiscounts is called when and articleCollection is finalized - it redeems all personalized vouchers
// and updates the pricerule/voucher usage history
// IT IS IRREVERSIBLE!!!
//
// alternatively use CommitOrderDiscounts
func CommitDiscounts(orderDiscounts *OrderDiscounts, customerID string) error {
var appliedRuleIDs []string
var appliedVoucherRuleIDs []string
var appliedVoucherCodes []string
for _, orderDiscount := range *orderDiscounts {
for _, appliedDiscount := range orderDiscount.AppliedDiscounts {
//if voucher - we need to redeem vouchers so we keep the separately
if len(appliedDiscount.VoucherCode) > 0 {
appliedVoucherCodes = append(appliedVoucherCodes, appliedDiscount.VoucherCode)
appliedVoucherRuleIDs = append(appliedVoucherRuleIDs, appliedDiscount.PriceRuleID)
} else {
//else normal rule
appliedRuleIDs = append(appliedRuleIDs, appliedDiscount.PriceRuleID)
}
}
}
appliedRuleIDs = RemoveDuplicates(appliedRuleIDs)
appliedVoucherRuleIDs = RemoveDuplicates(appliedVoucherRuleIDs)
appliedVoucherCodes = RemoveDuplicates(appliedVoucherCodes)
//redeem vouchers first
//NOTE: redeem internaly manipulates the associated pricerule as well
for _, voucherCode := range appliedVoucherCodes {
err := redeemVoucherByCode(voucherCode, customerID)
if err != nil {
return err
}
}
for _, ruleID := range appliedRuleIDs {
err := UpdatePriceRuleUsageHistoryAtomic(ruleID, customerID)
if err != nil {
return err
}
}
return nil
}
// CommitOrderDiscounts is called when and articleCollection is finalized - it redeems all personalized vouchers
// and updates the pricerule/voucher usage history
// IT IS IRREVERSIBLE!!!
//
// alternatively use CommitDiscounts
func CommitOrderDiscounts(customerID string, articleCollection *ArticleCollection, voucherCodes []string, checkoutAttributes []string, roundTo float64) error {
orderDiscounts, _, err := ApplyDiscounts(articleCollection, nil, voucherCodes, checkoutAttributes, roundTo, nil)
if err != nil {
return err
}
return CommitDiscounts(&orderDiscounts, customerID)
}
//------------------------------------------------------------------
// ~ PRIVATE FUNCTIONS
//------------------------------------------------------------------
// redeemVoucherByCode -
func redeemVoucherByCode(voucherCode string, customerID string) error {
voucher, err := GetVoucherByCode(voucherCode, nil)
if err != nil {
return err
}
return voucher.Redeem(customerID)
}
// Returns false, ValidationPreviouslyAppliedRuleBlock if a previous rule blocks application
func checkPreviouslyAppliedRules(voucherPriceRule *PriceRule, voucher *Voucher, calculationParameters *CalculationParameters) (ok bool, reason TypeRuleValidationMsg) {
// find applicable pricerules - auto promotions
promotionPriceRules, err := GetValidPriceRulesForPromotions([]Type{TypePromotionOrder, TypePromotionCustomer, TypePromotionProduct}, nil)
if err != nil {
panic(err)
}
var ruleVoucherPairs []RuleVoucherPair
previousMutualExclusionRule := false
for _, promotionRule := range promotionPriceRules {
//only add the customer and product promotion if an excluding one has not been applied yet
if promotionRule.Type == TypePromotionCustomer || promotionRule.Type == TypePromotionProduct {
if previousMutualExclusionRule == false {
ruleVoucherPairs = append(ruleVoucherPairs, RuleVoucherPair{Rule: &promotionRule, Voucher: nil})
previousMutualExclusionRule = true
}
} else {
ruleVoucherPairs = append(ruleVoucherPairs, RuleVoucherPair{Rule: &promotionRule, Voucher: nil})
}
}
ruleVoucherPairs = append(ruleVoucherPairs, RuleVoucherPair{Rule: voucherPriceRule, Voucher: voucher})
sort.Sort(ByPriority(ruleVoucherPairs))
found := false
for _, ruleVoucherPair := range ruleVoucherPairs {
if voucher != nil && ruleVoucherPair.Voucher != nil {
if ruleVoucherPair.Voucher.VoucherCode == voucher.VoucherCode {
found = true
}
for _, article := range calculationParameters.articleCollection.Articles {
applicable, _ := validatePriceRuleForPosition(*ruleVoucherPair.Rule, article, calculationParameters, nil)
if applicable && ruleVoucherPair.Rule.Exclusive == true {
if found == false {
return false, ValidationPreviouslyAppliedRuleBlock
}
return true, ValidationPreviouslyAppliedRuleBlock
}
}
}
}
return true, ValidationPreviouslyAppliedRuleBlock
}