/* * #%L * BroadleafCommerce Framework * %% * Copyright (C) 2009 - 2013 Broadleaf Commerce * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ package org.broadleafcommerce.core.offer.service.processor; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.broadleafcommerce.common.money.Money; import org.broadleafcommerce.core.offer.dao.OfferDao; import org.broadleafcommerce.core.offer.domain.FulfillmentGroupAdjustment; import org.broadleafcommerce.core.offer.domain.Offer; import org.broadleafcommerce.core.offer.domain.OfferOfferRuleXref; import org.broadleafcommerce.core.offer.domain.OrderAdjustment; import org.broadleafcommerce.core.offer.domain.OrderItemPriceDetailAdjustment; import org.broadleafcommerce.core.offer.service.OfferServiceUtilities; import org.broadleafcommerce.core.offer.service.discount.CandidatePromotionItems; import org.broadleafcommerce.core.offer.service.discount.PromotionQualifier; import org.broadleafcommerce.core.offer.service.discount.domain.PromotableCandidateOrderOffer; import org.broadleafcommerce.core.offer.service.discount.domain.PromotableFulfillmentGroup; import org.broadleafcommerce.core.offer.service.discount.domain.PromotableFulfillmentGroupAdjustment; import org.broadleafcommerce.core.offer.service.discount.domain.PromotableItemFactory; import org.broadleafcommerce.core.offer.service.discount.domain.PromotableOrder; import org.broadleafcommerce.core.offer.service.discount.domain.PromotableOrderAdjustment; import org.broadleafcommerce.core.offer.service.discount.domain.PromotableOrderItem; import org.broadleafcommerce.core.offer.service.discount.domain.PromotableOrderItemPriceDetail; import org.broadleafcommerce.core.offer.service.discount.domain.PromotableOrderItemPriceDetailAdjustment; import org.broadleafcommerce.core.offer.service.type.OfferDiscountType; import org.broadleafcommerce.core.offer.service.type.OfferRuleType; import org.broadleafcommerce.core.order.dao.OrderItemDao; import org.broadleafcommerce.core.order.domain.FulfillmentGroup; import org.broadleafcommerce.core.order.domain.Order; import org.broadleafcommerce.core.order.domain.OrderItem; import org.broadleafcommerce.core.order.domain.OrderItemPriceDetail; import org.broadleafcommerce.core.order.domain.OrderItemQualifier; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.annotation.Resource; /** * @author jfischer, bpolster */ @Service("blOrderOfferProcessor") public class OrderOfferProcessorImpl extends AbstractBaseProcessor implements OrderOfferProcessor { private static final Log LOG = LogFactory.getLog(OrderOfferProcessorImpl.class); @Resource(name = "blPromotableItemFactory") protected PromotableItemFactory promotableItemFactory; @Resource(name = "blOrderItemDao") protected OrderItemDao orderItemDao; @Resource(name = "blOfferDao") protected OfferDao offerDao; @Resource(name = "blOfferServiceUtilities") protected OfferServiceUtilities offerServiceUtilities; @Override public void filterOrderLevelOffer(PromotableOrder promotableOrder, List<PromotableCandidateOrderOffer> qualifiedOrderOffers, Offer offer) { if (offer.getDiscountType().getType().equals(OfferDiscountType.FIX_PRICE.getType())) { LOG.warn("Offers of type ORDER may not have a discount type of FIX_PRICE. Ignoring order offer (name=" + offer.getName() + ")"); return; } boolean orderLevelQualification = false; //Order Qualification orderQualification: { if (couldOfferApplyToOrder(offer, promotableOrder)) { orderLevelQualification = true; break orderQualification; } for (PromotableOrderItem orderItem : promotableOrder.getDiscountableOrderItems(offer.getApplyDiscountToSalePrice())) { if (couldOfferApplyToOrder(offer, promotableOrder, orderItem)) { orderLevelQualification = true; break orderQualification; } } for (PromotableFulfillmentGroup fulfillmentGroup : promotableOrder.getFulfillmentGroups()) { if (couldOfferApplyToOrder(offer, promotableOrder, fulfillmentGroup)) { orderLevelQualification = true; break orderQualification; } } } //Item Qualification - new for 1.5! if (orderLevelQualification) { CandidatePromotionItems candidates = couldOfferApplyToOrderItems(offer, promotableOrder.getDiscountableOrderItems(offer.getApplyDiscountToSalePrice())); if (candidates.isMatchedQualifier()) { PromotableCandidateOrderOffer candidateOffer = createCandidateOrderOffer(promotableOrder, qualifiedOrderOffers, offer); candidateOffer.getCandidateQualifiersMap().putAll(candidates.getCandidateQualifiersMap()); } } } @Override public boolean couldOfferApplyToOrder(Offer offer, PromotableOrder promotableOrder) { return couldOfferApplyToOrder(offer, promotableOrder, null, null); } /** * Private method which executes the appliesToOrderRules in the Offer to determine if this offer * can be applied to the Order, OrderItem, or FulfillmentGroup. * * @param offer * @param order * @param orderItem * @return true if offer can be applied, otherwise false */ protected boolean couldOfferApplyToOrder(Offer offer, PromotableOrder promotableOrder, PromotableOrderItem orderItem) { return couldOfferApplyToOrder(offer, promotableOrder, orderItem, null); } /** * Private method which executes the appliesToOrderRules in the Offer to determine if this offer * can be applied to the Order, OrderItem, or FulfillmentGroup. * * @param offer * @param order * @param fulfillmentGroup * @return true if offer can be applied, otherwise false */ protected boolean couldOfferApplyToOrder(Offer offer, PromotableOrder promotableOrder, PromotableFulfillmentGroup fulfillmentGroup) { return couldOfferApplyToOrder(offer, promotableOrder, null, fulfillmentGroup); } /** * Private method which executes the appliesToOrderRules in the Offer to determine if this offer * can be applied to the Order, OrderItem, or FulfillmentGroup. * * @param offer * @param order * @param promotableOrderItem * @param promotableFulfillmentGroup * @return true if offer can be applied, otherwise false */ protected boolean couldOfferApplyToOrder(Offer offer, PromotableOrder promotableOrder, PromotableOrderItem promotableOrderItem, PromotableFulfillmentGroup promotableFulfillmentGroup) { boolean appliesToItem = false; String rule = null; if (offer.getAppliesToOrderRules() != null && offer.getAppliesToOrderRules().trim().length() != 0) { rule = offer.getAppliesToOrderRules(); } else { OfferOfferRuleXref orderRule = offer.getOfferMatchRulesXref().get(OfferRuleType.ORDER.getType()); if (orderRule != null && orderRule.getOfferRule() != null) { rule = orderRule.getOfferRule().getMatchRule(); } } if (rule != null) { HashMap<String, Object> vars = new HashMap<String, Object>(); promotableOrder.updateRuleVariables(vars); vars.put("offer", offer); if (promotableFulfillmentGroup != null) { promotableFulfillmentGroup.updateRuleVariables(vars); } if (promotableOrderItem != null) { promotableOrderItem.updateRuleVariables(vars); } Boolean expressionOutcome = executeExpression(rule, vars); if (expressionOutcome != null && expressionOutcome) { appliesToItem = true; } } else { appliesToItem = true; } return appliesToItem; } protected PromotableCandidateOrderOffer createCandidateOrderOffer(PromotableOrder promotableOrder, List<PromotableCandidateOrderOffer> qualifiedOrderOffers, Offer offer) { PromotableCandidateOrderOffer promotableCandidateOrderOffer = promotableItemFactory.createPromotableCandidateOrderOffer(promotableOrder, offer); qualifiedOrderOffers.add(promotableCandidateOrderOffer); return promotableCandidateOrderOffer; } @Override public List<PromotableCandidateOrderOffer> removeTrailingNotCombinableOrderOffers(List<PromotableCandidateOrderOffer> candidateOffers) { List<PromotableCandidateOrderOffer> remainingCandidateOffers = new ArrayList<PromotableCandidateOrderOffer>(); int offerCount = 0; for (PromotableCandidateOrderOffer candidateOffer : candidateOffers) { if (offerCount == 0) { remainingCandidateOffers.add(candidateOffer); } else { boolean treatAsNewFormat = false; if (candidateOffer.getOffer().getTreatAsNewFormat() != null && candidateOffer.getOffer().getTreatAsNewFormat()) { treatAsNewFormat = true; } if ((!treatAsNewFormat && candidateOffer.getOffer().isCombinableWithOtherOffers()) || (treatAsNewFormat && (candidateOffer.getOffer().isTotalitarianOffer() == null || !candidateOffer.getOffer().isTotalitarianOffer()))) { remainingCandidateOffers.add(candidateOffer); } } offerCount++; } return remainingCandidateOffers; } @Override public void applyAllOrderOffers(List<PromotableCandidateOrderOffer> orderOffers, PromotableOrder promotableOrder) { // If order offer is not combinable, first verify order adjustment is zero, if zero, compare item discount total vs this offer's total Iterator<PromotableCandidateOrderOffer> orderOfferIterator = orderOffers.iterator(); while (orderOfferIterator.hasNext()) { PromotableCandidateOrderOffer orderOffer = orderOfferIterator.next(); if (promotableOrder.canApplyOrderOffer(orderOffer)) { applyOrderOffer(promotableOrder, orderOffer); if (orderOffer.isTotalitarian() || promotableOrder.isTotalitarianItemOfferApplied()) { if (LOG.isTraceEnabled()) { LOG.trace("Totalitarian Order Offer Applied. Comparing order and item offers for best outcome."); } compareAndAdjustOrderAndItemOffers(promotableOrder); // We continue because this could be the first offer and marked as totalitarian, but not as good as an // item offer. There could be other order offers that are not totalitarian that also qualify. continue; } if (!orderOffer.isCombinable()) { if (LOG.isTraceEnabled()) { LOG.trace("Non-Combinable Order Offer Applied with id=[" + orderOffer.getOffer().getId() +"]. No other order offers can be applied"); } break; } } } promotableOrder.getOrder().setSubTotal(promotableOrder.calculateSubtotalWithAdjustments()); } /** * Called when the system must determine whether to apply order or item adjustments. * @param promotableOrder * @param orderOffersApplied */ protected void compareAndAdjustOrderAndItemOffers(PromotableOrder promotableOrder) { Money orderAdjustmentTotal = promotableOrder.calculateOrderAdjustmentTotal(); Money itemAdjustmentTotal = promotableOrder.calculateItemAdjustmentTotal(); if (orderAdjustmentTotal.greaterThanOrEqual(itemAdjustmentTotal)) { promotableOrder.removeAllCandidateItemOfferAdjustments(); } else { promotableOrder.removeAllCandidateOrderOfferAdjustments(); } } /** * Private method used by applyAllOrderOffers to create an OrderAdjustment from a CandidateOrderOffer * and associates the OrderAdjustment to the Order. * * @param orderOffer a CandidateOrderOffer to apply to an Order */ protected void applyOrderOffer(PromotableOrder promotableOrder, PromotableCandidateOrderOffer orderOffer) { PromotableOrderAdjustment promotableOrderAdjustment = promotableItemFactory.createPromotableOrderAdjustment(orderOffer, promotableOrder); promotableOrder.addCandidateOrderAdjustment(promotableOrderAdjustment); } @Override public PromotableItemFactory getPromotableItemFactory() { return promotableItemFactory; } @Override public void setPromotableItemFactory(PromotableItemFactory promotableItemFactory) { this.promotableItemFactory = promotableItemFactory; } protected Map<Long, PromotableOrderAdjustment> buildPromotableOrderAdjustmentsMap(PromotableOrder promotableOrder) { Map<Long, PromotableOrderAdjustment> adjustmentsMap = new HashMap<Long, PromotableOrderAdjustment>(); for (PromotableOrderAdjustment adjustment : promotableOrder.getCandidateOrderAdjustments()) { adjustmentsMap.put(adjustment.getOffer().getId(), adjustment); } return adjustmentsMap; } protected void synchronizeOrderAdjustments(PromotableOrder promotableOrder) { Order order = promotableOrder.getOrder(); if (order.getOrderAdjustments().isEmpty() && promotableOrder.getCandidateOrderAdjustments().isEmpty()) { return; } Map<Long, PromotableOrderAdjustment> newAdjustmentsMap = buildPromotableOrderAdjustmentsMap(promotableOrder); Iterator<OrderAdjustment> orderAdjIterator = order.getOrderAdjustments().iterator(); while (orderAdjIterator.hasNext()) { OrderAdjustment adjustment = orderAdjIterator.next(); if (adjustment.getOffer() != null) { Long offerId = adjustment.getOffer().getId(); PromotableOrderAdjustment promotableAdjustment = newAdjustmentsMap.remove(offerId); if (promotableAdjustment != null) { if (!adjustment.getValue().equals(promotableAdjustment.getAdjustmentValue())) { if (LOG.isDebugEnabled()) { LOG.debug("Updating value for order adjustment with offer Id " + offerId + " to " + promotableAdjustment.getAdjustmentValue()); } adjustment.setValue(promotableAdjustment.getAdjustmentValue()); } } else { // No longer using this order adjustment, remove it. orderAdjIterator.remove(); } } } for (PromotableOrderAdjustment promotableOrderAdjustment : newAdjustmentsMap.values()) { // Add the newly introduced adjustments. Offer offer = promotableOrderAdjustment.getOffer(); OrderAdjustment orderAdjustment = offerDao.createOrderAdjustment(); orderAdjustment.init(order, offer, offer.getName()); orderAdjustment.setValue(promotableOrderAdjustment.getAdjustmentValue()); order.getOrderAdjustments().add(orderAdjustment); } } protected void synchronizeOrderItems(PromotableOrder promotableOrder) { Order order = promotableOrder.getOrder(); Map<OrderItem, PromotableOrderItem> promotableItemMap = offerServiceUtilities.buildPromotableItemMap(promotableOrder); List<OrderItem> orderItemList = offerServiceUtilities.buildOrderItemList(order); for (OrderItem orderItem : orderItemList) { PromotableOrderItem promotableItem = promotableItemMap.get(orderItem); if (promotableItem == null) { continue; } synchronizeItemPriceDetails(orderItem, promotableItem); synchronizeItemQualifiers(orderItem, promotableItem); } } protected void synchronizeItemPriceDetails(OrderItem orderItem, PromotableOrderItem promotableOrderItem) { Map<String, PromotableOrderItemPriceDetail> promotableDetailsMap = buildPromotableDetailsMap(promotableOrderItem); Map<Long, OrderItemPriceDetail> unmatchedDetailsMap = new HashMap<Long, OrderItemPriceDetail>(); for (OrderItemPriceDetail orderItemPriceDetail : orderItem.getOrderItemPriceDetails()) { String detailKey = buildItemPriceDetailKey(orderItemPriceDetail); PromotableOrderItemPriceDetail promotableDetail = promotableDetailsMap.remove(detailKey); if (promotableDetail != null) { processMatchingDetails(orderItemPriceDetail, promotableDetail); } else { unmatchedDetailsMap.put(orderItemPriceDetail.getId(), orderItemPriceDetail); } } Iterator<OrderItemPriceDetail> unmatchedDetailsIterator = unmatchedDetailsMap.values().iterator(); for (PromotableOrderItemPriceDetail priceDetail : promotableDetailsMap.values()) { if (unmatchedDetailsIterator.hasNext()) { // Reuse an existing priceDetail OrderItemPriceDetail existingDetail = unmatchedDetailsIterator.next(); // Reset use Sale flag to true existingDetail.setUseSalePrice(true); offerServiceUtilities.updatePriceDetail(existingDetail, priceDetail); unmatchedDetailsIterator.remove(); } else { // Create a new priceDetail OrderItemPriceDetail newPriceDetail = orderItemDao.createOrderItemPriceDetail(); newPriceDetail.setOrderItem(orderItem); offerServiceUtilities.updatePriceDetail(newPriceDetail, priceDetail); orderItem.getOrderItemPriceDetails().add(newPriceDetail); } } // Remove any unmatched details Iterator<OrderItemPriceDetail> pdIterator = orderItem.getOrderItemPriceDetails().iterator(); offerServiceUtilities.removeUnmatchedPriceDetails(unmatchedDetailsMap, pdIterator); } protected void synchronizeItemQualifiers(OrderItem orderItem, PromotableOrderItem promotableOrderItem) { Map<Long, PromotionQualifier> qualifiersMap = buildPromotableQualifiersMap(promotableOrderItem); Map<Long, OrderItemQualifier> unmatchedQualifiersMap = new HashMap<Long, OrderItemQualifier>(); for (OrderItemQualifier orderItemQualifier : orderItem.getOrderItemQualifiers()) { PromotionQualifier promotableQualifier = qualifiersMap.remove(orderItemQualifier.getOffer().getId()); if (promotableQualifier != null) { // Offer was used as a qualifier on previous run. Update quantity if needed. if (orderItemQualifier.getQuantity() != promotableQualifier.getQuantity()) { orderItemQualifier.setQuantity(new Long(promotableQualifier.getQuantity())); } } else { unmatchedQualifiersMap.put(orderItemQualifier.getId(), orderItemQualifier); } } Iterator<OrderItemQualifier> unmatchedQualifiersIterator = unmatchedQualifiersMap.values().iterator(); for (PromotionQualifier qualifier : qualifiersMap.values()) { if (unmatchedQualifiersIterator.hasNext()) { // Reuse an existing qualifier OrderItemQualifier existingQualifier = unmatchedQualifiersIterator.next(); existingQualifier.setOffer(qualifier.getPromotion()); existingQualifier.setQuantity(Long.valueOf(qualifier.getQuantity())); unmatchedQualifiersIterator.remove(); } else { // Create a new qualifier OrderItemQualifier newQualifier = orderItemDao.createOrderItemQualifier(); newQualifier.setOrderItem(orderItem); newQualifier.setOffer(qualifier.getPromotion()); newQualifier.setQuantity(Long.valueOf(qualifier.getQuantity())); orderItem.getOrderItemQualifiers().add(newQualifier); } } // Remove any unmatched qualifiers Iterator<OrderItemQualifier> qIterator = orderItem.getOrderItemQualifiers().iterator(); offerServiceUtilities.removeUnmatchedQualifiers(unmatchedQualifiersMap, qIterator); } protected void processMatchingDetails(OrderItemPriceDetail itemDetail, PromotableOrderItemPriceDetail promotableItemDetail) { Map<Long, OrderItemPriceDetailAdjustment> itemAdjustmentMap = offerServiceUtilities.buildItemDetailAdjustmentMap(itemDetail); if (itemDetail.getQuantity() != promotableItemDetail.getQuantity()) { itemDetail.setQuantity(promotableItemDetail.getQuantity()); } for (PromotableOrderItemPriceDetailAdjustment adjustment : promotableItemDetail.getCandidateItemAdjustments()) { OrderItemPriceDetailAdjustment itemAdjustment = itemAdjustmentMap.get(adjustment.getOfferId()); if (!itemAdjustment.getValue().equals(adjustment.getAdjustmentValue())) { itemAdjustment.setValue(adjustment.getAdjustmentValue()); itemAdjustment.setAppliedToSalePrice(adjustment.isAppliedToSalePrice()); } } } protected String buildItemPriceDetailKey(OrderItemPriceDetail itemDetail) { List<Long> offerIds = new ArrayList<Long>(); for (OrderItemPriceDetailAdjustment adjustment : itemDetail.getOrderItemPriceDetailAdjustments()) { Long offerId = adjustment.getOffer().getId(); offerIds.add(offerId); } Collections.sort(offerIds); return itemDetail.getOrderItem().toString() + offerIds.toString() + itemDetail.getUseSalePrice(); } protected Map<String, PromotableOrderItemPriceDetail> buildPromotableDetailsMap(PromotableOrderItem item) { Map<String, PromotableOrderItemPriceDetail> detailsMap = new HashMap<String, PromotableOrderItemPriceDetail>(); for (PromotableOrderItemPriceDetail detail : item.getPromotableOrderItemPriceDetails()) { detailsMap.put(detail.buildDetailKey(), detail); } return detailsMap; } protected Map<Long, PromotionQualifier> buildPromotableQualifiersMap(PromotableOrderItem item) { Map<Long, PromotionQualifier> qualifiersMap = new HashMap<Long, PromotionQualifier>(); for (PromotableOrderItemPriceDetail detail : item.getPromotableOrderItemPriceDetails()) { for (PromotionQualifier qualifier : detail.getPromotionQualifiers()) { PromotionQualifier existingQualifier = qualifiersMap.get(qualifier.getPromotion().getId()); if (existingQualifier != null) { existingQualifier.setQuantity(existingQualifier.getQuantity() + qualifier.getQuantity()); } else { qualifiersMap.put(qualifier.getPromotion().getId(), qualifier); } } } return qualifiersMap; } protected void synchronizeFulfillmentGroups(PromotableOrder promotableOrder) { Order order = promotableOrder.getOrder(); Map<Long, PromotableFulfillmentGroup> fgMap = buildPromotableFulfillmentGroupMap(promotableOrder); for (FulfillmentGroup fg : order.getFulfillmentGroups()) { synchronizeFulfillmentGroupAdjustments(fg, fgMap.get(fg.getId())); } } protected Map<Long, PromotableFulfillmentGroup> buildPromotableFulfillmentGroupMap(PromotableOrder order) { Map<Long, PromotableFulfillmentGroup> fgMap = new HashMap<Long, PromotableFulfillmentGroup>(); for (PromotableFulfillmentGroup fg : order.getFulfillmentGroups()) { fgMap.put(fg.getFulfillmentGroup().getId(), fg); } return fgMap; } protected Map<Long, PromotableFulfillmentGroupAdjustment> buildPromFulfillmentAdjMap(PromotableFulfillmentGroup fg) { Map<Long, PromotableFulfillmentGroupAdjustment> fgMap = new HashMap<Long, PromotableFulfillmentGroupAdjustment>(); for (PromotableFulfillmentGroupAdjustment adjustment : fg.getCandidateFulfillmentGroupAdjustments()) { fgMap.put(adjustment.getPromotableCandidateFulfillmentGroupOffer().getOffer().getId(), adjustment); } return fgMap; } protected void synchronizeFulfillmentGroupAdjustments(FulfillmentGroup fg, PromotableFulfillmentGroup promotableFG) { Iterator<FulfillmentGroupAdjustment> adjustmentIterator = fg.getFulfillmentGroupAdjustments().iterator(); Map<Long, PromotableFulfillmentGroupAdjustment> promotableAdjMap = buildPromFulfillmentAdjMap(promotableFG); // First try and update existing adjustment records while (adjustmentIterator.hasNext()) { FulfillmentGroupAdjustment currentAdj = adjustmentIterator.next(); PromotableFulfillmentGroupAdjustment newAdj = promotableAdjMap.remove(currentAdj.getOffer().getId()); if (newAdj != null) { if (!currentAdj.getValue().equals(newAdj.getAdjustmentValue())) { // Update the currentAdj. currentAdj.setValue(newAdj.getAdjustmentValue()); } } else { // Removing no longer valid adjustment adjustmentIterator.remove(); } } // Now add missing adjustments for (PromotableFulfillmentGroupAdjustment newAdj : promotableAdjMap.values()) { FulfillmentGroupAdjustment fa = offerDao.createFulfillmentGroupAdjustment(); fa.setFulfillmentGroup(fg); fa.init(fg, newAdj.getPromotableCandidateFulfillmentGroupOffer().getOffer(), null); fa.setValue(newAdj.getAdjustmentValue()); fg.getFulfillmentGroupAdjustments().add(fa); } } @Override public void synchronizeAdjustmentsAndPrices(PromotableOrder promotableOrder) { synchronizeOrderAdjustments(promotableOrder); synchronizeOrderItems(promotableOrder); if (extensionManager != null) { extensionManager.getProxy().synchronizeAdjustmentsAndPrices(promotableOrder); } synchronizeFulfillmentGroups(promotableOrder); } @Override public void setOfferDao(OfferDao offerDao) { this.offerDao = offerDao; } @Override public void setOrderItemDao(OrderItemDao orderItemDao) { this.orderItemDao = orderItemDao; } public OfferServiceUtilities getOfferServiceUtilities() { return offerServiceUtilities; } public void setOfferServiceUtilities(OfferServiceUtilities offerServiceUtilities) { this.offerServiceUtilities = offerServiceUtilities; } }