From 0c6586a9b7dbc74fec9e1a90d6e8479fa4efc1fb Mon Sep 17 00:00:00 2001
From: Javier Etxarri <javier.echarri@openbravo.com>
Date: Mon, 22 Nov 2021 13:00:54 +0100
Subject: [PATCH] Fixes issue-48120: Improve Discount endpoint performance
 creating a cache of the rules

---
 src-db/database/sourcedata/AD_PREFERENCE.xml  |  14 +
 src-db/database/sourcedata/AD_REF_LIST.xml    |  14 +
 .../discounts/engine/DiscountJSExecutor.java  |  70 +++
 .../engine/DiscountJSExecutor.java.orig       | 548 ++++++++++++++++++
 .../engine/DiscountJSExecutor.java.rej        |  17 +
 5 files changed, 663 insertions(+)
 create mode 100644 src-db/database/sourcedata/AD_PREFERENCE.xml
 create mode 100644 src-db/database/sourcedata/AD_REF_LIST.xml
 create mode 100644 src/org/openbravo/discounts/engine/DiscountJSExecutor.java.orig
 create mode 100644 src/org/openbravo/discounts/engine/DiscountJSExecutor.java.rej

diff --git a/src-db/database/sourcedata/AD_PREFERENCE.xml b/src-db/database/sourcedata/AD_PREFERENCE.xml
new file mode 100644
index 0000000..c3731b8
--- /dev/null
+++ b/src-db/database/sourcedata/AD_PREFERENCE.xml
@@ -0,0 +1,14 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<data>
+<!--5A73DD8743434202B2116B98D5EC13B3--><AD_PREFERENCE>
+<!--5A73DD8743434202B2116B98D5EC13B3-->  <AD_PREFERENCE_ID><![CDATA[5A73DD8743434202B2116B98D5EC13B3]]></AD_PREFERENCE_ID>
+<!--5A73DD8743434202B2116B98D5EC13B3-->  <AD_CLIENT_ID><![CDATA[0]]></AD_CLIENT_ID>
+<!--5A73DD8743434202B2116B98D5EC13B3-->  <AD_ORG_ID><![CDATA[0]]></AD_ORG_ID>
+<!--5A73DD8743434202B2116B98D5EC13B3-->  <ISACTIVE><![CDATA[Y]]></ISACTIVE>
+<!--5A73DD8743434202B2116B98D5EC13B3-->  <VALUE><![CDATA[0]]></VALUE>
+<!--5A73DD8743434202B2116B98D5EC13B3-->  <PROPERTY><![CDATA[OBDISBE_RulesCacheTime]]></PROPERTY>
+<!--5A73DD8743434202B2116B98D5EC13B3-->  <ISPROPERTYLIST><![CDATA[Y]]></ISPROPERTYLIST>
+<!--5A73DD8743434202B2116B98D5EC13B3-->  <AD_MODULE_ID><![CDATA[B86B05B713C746D89B40EB5E09046E52]]></AD_MODULE_ID>
+<!--5A73DD8743434202B2116B98D5EC13B3--></AD_PREFERENCE>
+
+</data>
diff --git a/src-db/database/sourcedata/AD_REF_LIST.xml b/src-db/database/sourcedata/AD_REF_LIST.xml
new file mode 100644
index 0000000..6b5b016
--- /dev/null
+++ b/src-db/database/sourcedata/AD_REF_LIST.xml
@@ -0,0 +1,14 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<data>
+<!--75209391828342CF869DAAA65EACC1BE--><AD_REF_LIST>
+<!--75209391828342CF869DAAA65EACC1BE-->  <AD_REF_LIST_ID><![CDATA[75209391828342CF869DAAA65EACC1BE]]></AD_REF_LIST_ID>
+<!--75209391828342CF869DAAA65EACC1BE-->  <AD_CLIENT_ID><![CDATA[0]]></AD_CLIENT_ID>
+<!--75209391828342CF869DAAA65EACC1BE-->  <AD_ORG_ID><![CDATA[0]]></AD_ORG_ID>
+<!--75209391828342CF869DAAA65EACC1BE-->  <ISACTIVE><![CDATA[Y]]></ISACTIVE>
+<!--75209391828342CF869DAAA65EACC1BE-->  <VALUE><![CDATA[OBDISBE_RulesCacheTime]]></VALUE>
+<!--75209391828342CF869DAAA65EACC1BE-->  <NAME><![CDATA[Discount Rules Cache Max Duration]]></NAME>
+<!--75209391828342CF869DAAA65EACC1BE-->  <AD_REFERENCE_ID><![CDATA[A26BA480E2014707B47257024C3CBFF7]]></AD_REFERENCE_ID>
+<!--75209391828342CF869DAAA65EACC1BE-->  <AD_MODULE_ID><![CDATA[B86B05B713C746D89B40EB5E09046E52]]></AD_MODULE_ID>
+<!--75209391828342CF869DAAA65EACC1BE--></AD_REF_LIST>
+
+</data>
diff --git a/src/org/openbravo/discounts/engine/DiscountJSExecutor.java b/src/org/openbravo/discounts/engine/DiscountJSExecutor.java
index bf5655a..bd0d50d 100644
--- a/src/org/openbravo/discounts/engine/DiscountJSExecutor.java
+++ b/src/org/openbravo/discounts/engine/DiscountJSExecutor.java
@@ -17,12 +17,14 @@ import java.nio.file.Paths;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Calendar;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -47,6 +49,8 @@ import org.openbravo.dal.core.OBContext;
 import org.openbravo.dal.service.OBDal;
 import org.openbravo.discounts.DiscountsComponent;
 import org.openbravo.discounts.api.DiscountsTicket;
+import org.openbravo.erpCommon.businessUtility.Preferences;
+import org.openbravo.erpCommon.utility.PropertyException;
 import org.openbravo.model.pricing.priceadjustment.BusinessPartner;
 import org.openbravo.model.pricing.priceadjustment.BusinessPartnerGroup;
 import org.openbravo.model.pricing.priceadjustment.Characteristic;
@@ -68,6 +72,13 @@ import org.openbravo.service.json.DataToJsonConverter;
 public abstract class DiscountJSExecutor implements DiscountExecutor {
   private static final Logger log = LogManager.getLogger();
 
+  private static HashMap<String, JSONObject> rulesCache = new HashMap<String, JSONObject>();
+  private static Date rulesCacheDate = new Date();
+
+  final static SimpleDateFormat dtFormat = createOrderLoaderDateTimeFormat();
+
+  private static final String OBDISBE_RULESCACHETIME = "OBDISBE_RulesCacheTime";
+
   @Inject
   private ApplicationDictionaryCachedStructures adcs;
 
@@ -146,6 +157,12 @@ public abstract class DiscountJSExecutor implements DiscountExecutor {
       init();
     }
     long t = System.currentTimeMillis();
+    Optional<HashMap<String, JSONObject>> newCache = resetCacheIfItIsNecessary(
+        getDiscountRulesCacheMaxDurationValue(), rulesCacheDate);
+    if (!newCache.isEmpty()) {
+      rulesCache = newCache.get();
+    }
+
     final List<JSONObject> rules = getDiscountRules(ticket);
     final JSONObject bpSets = getBPSets(ticket);
     log.debug("Calculated {} discount rules for ticket {} in {} ms", rules.size(), ticket,
@@ -153,6 +170,33 @@ public abstract class DiscountJSExecutor implements DiscountExecutor {
     return invokeJSDiscountCalculation(ticket, rules, bpSets);
   }
 
+  private Optional<HashMap<String, JSONObject>> resetCacheIfItIsNecessary(long maxCacheDuration,
+      Date currentCacheInitialized) {
+    Date currentDate = new Date();
+
+    if (currentDate.getTime() - currentCacheInitialized.getTime() > maxCacheDuration * 60 * 60
+        * 1000) {
+      return Optional.of(new HashMap<String, JSONObject>());
+    }
+
+    return Optional.empty();
+  }
+
+  private static long getDiscountRulesCacheMaxDurationValue() {
+    String maxCacheTime = "0";
+    try {
+      maxCacheTime = Preferences.getPreferenceValue(OBDISBE_RULESCACHETIME, true,
+          OBContext.getOBContext().getCurrentClient(),
+          OBContext.getOBContext().getCurrentOrganization(), OBContext.getOBContext().getUser(),
+          OBContext.getOBContext().getRole(), null);
+    } catch (PropertyException pe) {
+      // Pref not defined
+      log.debug("Not possible to recover preference Discount Rules Cache Max Duration");
+    }
+    // return 0L;
+    return Long.parseLong(maxCacheTime.trim(), 10);
+  }
+
   /**
    * Returns the discount rules that can be applied for a given ticket.
    *
@@ -285,6 +329,24 @@ public abstract class DiscountJSExecutor implements DiscountExecutor {
           .get(PriceAdjustment.class, discountRule.getString("id"));
       JSONObject jsonObject = null;
 
+      if (rulesCache.containsKey(discountRule.getString("id"))) {
+        Date updated = pa.getUpdated();
+        JSONObject rule = rulesCache.get(discountRule.getString("id"));
+
+        Date cachedRuleUpdated;
+        try {
+          cachedRuleUpdated = dtFormat.parse(rule.getString("updated"));
+        } catch (Exception e) {
+          Calendar cal = Calendar.getInstance();
+          cal.add(Calendar.YEAR, -1);
+          cachedRuleUpdated = cal.getTime();
+        }
+
+        if (updated.getTime() / 1000 <= cachedRuleUpdated.getTime() / 1000) {
+          return rule;
+        }
+      }
+
       JSONArray cbpartners = new JSONArray();
       for (BusinessPartner bp : pa.getPricingAdjustmentBusinessPartnerList()) {
         jsonObject = getBusinessPartnerInfo(bp);
@@ -355,6 +417,8 @@ public abstract class DiscountJSExecutor implements DiscountExecutor {
           .stream()
           .forEach(hook -> hook.addDataToDiscount(pa, discountRule));
 
+      rulesCache.put(discountRule.getString("id"), discountRule);
+
     } catch (JSONException e) {
       log.error("Couldn't calculate discount rules", e);
     }
@@ -527,6 +591,12 @@ public abstract class DiscountJSExecutor implements DiscountExecutor {
     String value();
   }
 
+  private static SimpleDateFormat createOrderLoaderDateTimeFormat() {
+    final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX");
+    dateFormat.setLenient(true);
+    return dateFormat;
+  }
+
   /**
    * A class used to select a DiscountJSExecutor instance.
    */
diff --git a/src/org/openbravo/discounts/engine/DiscountJSExecutor.java.orig b/src/org/openbravo/discounts/engine/DiscountJSExecutor.java.orig
new file mode 100644
index 0000000..bf5655a
--- /dev/null
+++ b/src/org/openbravo/discounts/engine/DiscountJSExecutor.java.orig
@@ -0,0 +1,548 @@
+/*
+ ************************************************************************************
+ * Copyright (C) 2019-2021 Openbravo S.L.U.
+ * Licensed under the Openbravo Commercial License version 1.0
+ * You may obtain a copy of the License at http://www.openbravo.com/legal/obcl.html
+ * or in the legal folder of this module distribution.
+ ************************************************************************************
+ */
+package org.openbravo.discounts.engine;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.annotation.PostConstruct;
+import javax.enterprise.context.ApplicationScoped;
+import javax.enterprise.util.AnnotationLiteral;
+import javax.inject.Inject;
+import javax.servlet.ServletContext;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.codehaus.jettison.json.JSONArray;
+import org.codehaus.jettison.json.JSONException;
+import org.codehaus.jettison.json.JSONObject;
+import org.hibernate.query.Query;
+import org.openbravo.api.ticket.TicketResult;
+import org.openbravo.base.provider.OBProvider;
+import org.openbravo.client.application.window.ApplicationDictionaryCachedStructures;
+import org.openbravo.client.kernel.KernelConstants;
+import org.openbravo.dal.core.DalContextListener;
+import org.openbravo.dal.core.OBContext;
+import org.openbravo.dal.service.OBDal;
+import org.openbravo.discounts.DiscountsComponent;
+import org.openbravo.discounts.api.DiscountsTicket;
+import org.openbravo.model.pricing.priceadjustment.BusinessPartner;
+import org.openbravo.model.pricing.priceadjustment.BusinessPartnerGroup;
+import org.openbravo.model.pricing.priceadjustment.Characteristic;
+import org.openbravo.model.pricing.priceadjustment.DiscountBusinessPartnerSet;
+import org.openbravo.model.pricing.priceadjustment.PriceAdjustment;
+import org.openbravo.model.pricing.priceadjustment.PriceList;
+import org.openbravo.model.pricing.priceadjustment.Product;
+import org.openbravo.model.pricing.priceadjustment.ProductCategory;
+import org.openbravo.retail.discounts.DiscountDataHookSelector;
+import org.openbravo.service.json.DataToJsonConverter;
+
+/**
+ * A DiscountExecutor that implements the discount calculation logic using the Javascript language.
+ * 
+ * Classes extending this one can decide how to invoke the discount calculation logic and the
+ * Javascript engine to be used to execute the code.
+ */
+@ApplicationScoped
+public abstract class DiscountJSExecutor implements DiscountExecutor {
+  private static final Logger log = LogManager.getLogger();
+
+  @Inject
+  private ApplicationDictionaryCachedStructures adcs;
+
+  @Inject
+  private DiscountsComponent discountsComponent;
+
+  @Inject
+  private DiscountDataHookSelector dataHookSelector;
+
+  @PostConstruct
+  public void init() {
+    doInit();
+  }
+
+  /**
+   * Initializes this Javascript DiscountExecutor.
+   */
+  public abstract void doInit();
+
+  /**
+   * Invokes the discounts calculation.
+   * 
+   * @param ticket
+   *          The ticket whose discounts should be calculated.
+   * @param rules
+   *          The list of applicable discount rules.
+   * 
+   * @return a TicketResult with the calculated discounts for the given ticket.
+   */
+  public abstract TicketResult invokeJSDiscountCalculation(DiscountsTicket ticket,
+      List<JSONObject> rules);
+
+  /**
+   * Invokes the discounts calculation.
+   * 
+   * @param ticket
+   *          The ticket whose discounts should be calculated.
+   * @param rules
+   *          The list of applicable discount rules.
+   * @param bpSets
+   *          The list of applicable bpSets.
+   * 
+   * @return a TicketResult with the calculated discounts for the given ticket.
+   */
+  public abstract TicketResult invokeJSDiscountCalculation(DiscountsTicket ticket,
+      List<JSONObject> rules, JSONObject bpSets);
+
+  /**
+   * @return the Path of the file that contains the Javascript code with the discounts calculation
+   *         logic.
+   */
+  public Path getDiscountsJavaScript() {
+    Map<String, Object> p = new HashMap<>(1);
+    ServletContext requestCtx = DalContextListener.getServletContext();
+    p.put(KernelConstants.SERVLET_CONTEXT, requestCtx);
+    discountsComponent.setParameters(p);
+    String jsFileName = discountsComponent.getStaticResourceFileName() + ".js";
+    String jsPath = requestCtx.getRealPath("web/js/gen/" + jsFileName);
+    log.debug("JS contents generated in {}", jsPath);
+    return Paths.get(jsPath);
+  }
+
+  /**
+   * Calculates the discounts for the provided ticket, retrieving the applicable discount rules from
+   * database.
+   *
+   * @param ticket
+   *          The ticket whose discounts should be calculated.
+   *
+   * @return a TicketResult with the calculated discounts for the given ticket.
+   */
+  @Override
+  public TicketResult calculateDiscounts(DiscountsTicket ticket) {
+    if (adcs.isInDevelopment()) {
+      log.info("Re-initiliazing jsExecutor because of in development instance");
+      init();
+    }
+    long t = System.currentTimeMillis();
+    final List<JSONObject> rules = getDiscountRules(ticket);
+    final JSONObject bpSets = getBPSets(ticket);
+    log.debug("Calculated {} discount rules for ticket {} in {} ms", rules.size(), ticket,
+        System.currentTimeMillis() - t);
+    return invokeJSDiscountCalculation(ticket, rules, bpSets);
+  }
+
+  /**
+   * Returns the discount rules that can be applied for a given ticket.
+   *
+   * @param ticket
+   *          The ticket whose applicable discounts will be retrieved.
+   *
+   * @return a list of JSONObjects representing the discount rules that can be applied for a given
+   *         ticket.
+   */
+  private List<JSONObject> getDiscountRules(DiscountsTicket ticket) {
+    String orgId = ticket.getOrganization().getId();
+    Set<String> ticketProducts = getProducts(ticket);
+    Set<String> ticketProductCategories = getProductCategories(ticketProducts);
+    String bpId = ticket.getBusinessPartner().getId();
+    String bpCatId = getBusinessPartnerCategory(bpId);
+    List<String> manualPromotions = getManualPromotions();
+    Date orderDate = getOrderDateWithoutTime(ticket.getOrderDate());
+
+    // @formatter:off
+      String whereClause = "as pa where "
+          + "      ((pa.includedOrganizations = 'N' and exists (select 1 from PricingAdjustmentOrganization po where po.priceAdjustment = pa and po.active = 'Y' and po.organization.id = :orgId))"
+          + "    or (pa.includedOrganizations = 'Y' and not exists (select 1 from PricingAdjustmentOrganization po where po.priceAdjustment = pa and po.active = 'Y' and po.organization.id = :orgId)))"
+
+          + "  and ((pa.includedProducts = 'N' and exists (select 1 from PricingAdjustmentProduct pap where pap.priceAdjustment = pa and pap.active = 'Y' and pap.product.id in :products))"
+          + "    or (pa.includedProducts = 'Y' and not exists (select 1 from PricingAdjustmentProduct pap where pap.priceAdjustment = pa and pap.active = 'Y' and pap.product.id in :products)))"
+
+          + "  and ((pa.includedProductCategories = 'N' and exists (select 1 from PricingAdjustmentProductCategory papc where papc.priceAdjustment = pa and papc.active = 'Y' and papc.productCategory.id in :prodCategories))"
+          + "    or (pa.includedProductCategories = 'Y' and not exists (select 1 from PricingAdjustmentProductCategory papc where papc.priceAdjustment = pa and papc.active = 'Y' and papc.productCategory.id in :prodCategories)))"
+
+          + "  and ((pa.includedBusinessPartners = 'N' and exists (select 1 from PricingAdjustmentBusinessPartner pabp where pabp.priceAdjustment = pa and pabp.active = 'Y' and pabp.businessPartner.id = :bpId))"
+          + "    or (pa.includedBusinessPartners = 'Y' and not exists (select 1 from PricingAdjustmentBusinessPartner pabp where pabp.priceAdjustment = pa and pabp.active = 'Y' and pabp.businessPartner.id = :bpId)))"
+
+          + "  and ((pa.includedBPCategories ='N' and exists (select 1 from PricingAdjustmentBusinessPartnerGroup pabc where pabc.priceAdjustment = pa and pabc.active = 'Y' and pabc.businessPartnerCategory.id = :bpCatId))"
+          + "    or (pa.includedBPCategories ='Y' and not exists (select 1 from PricingAdjustmentBusinessPartnerGroup pabc where pabc.priceAdjustment = pa and pabc.active = 'Y' and pabc.businessPartnerCategory.id = :bpCatId)))"
+
+          + " and pa.active = 'Y'"
+          + " and pa.organization.id in :orgIds "
+          + " and pa.discountType.id not in (:manualPromotions)"
+          + " and pa.startingDate <= :startingDate"
+          + " and (pa.endingDate is null"
+          + " or pa.endingDate >= :endingDate)"
+          + " order by pa.priority nulls first, pa.id";
+      // @formatter:on
+
+    List<PriceAdjustment> discountCandidates = OBDal.getInstance()
+        .createQuery(PriceAdjustment.class, whereClause)
+        .setNamedParameter("orgId", orgId)
+        .setNamedParameter("orgIds", getOrgTree(orgId))
+        .setNamedParameter("products", ticketProducts)
+        .setNamedParameter("prodCategories", ticketProductCategories)
+        .setNamedParameter("bpId", bpId)
+        .setNamedParameter("bpCatId", bpCatId)
+        .setNamedParameter("manualPromotions", manualPromotions)
+        .setNamedParameter("startingDate", orderDate)
+        .setNamedParameter("endingDate", orderDate)
+        .list();
+
+    return transformDiscountRules(discountCandidates);
+  }
+
+  /**
+   * Returns the application bpSets.
+   *
+   * @param ticket
+   *          The ticket whose applicable discounts will be retrieved.
+   *
+   * @return a JSONObject representing the list of bpSets.
+   */
+  private JSONObject getBPSets(DiscountsTicket ticket) {
+    JSONObject jsonBPSet = new JSONObject();
+    try {
+      final String orgId = ticket.getOrganization().getId();
+      final Date orderDate = getOrderDateWithoutTime(ticket.getOrderDate());
+      final Set<String> naturalTreeOrgList = OBContext.getOBContext()
+          .getOrganizationStructureProvider()
+          .getNaturalTree(orgId);
+
+      final String hqlQuery = "select c.id as id, c.bpSet.id as bpSet, c.businessPartner.id as businessPartner, "
+          + "c.startingDate as startingDate, c.endingDate as endingDate from BusinessPartnerSetLine c "
+          + "where c.bpSet.client.id = :clientId and c.bpSet.organization.id in (:orgList) and c.active = true and c.bpSet.active = true and c.businessPartner.active = true "
+          + "and (c.startingDate is null or c.startingDate <= :orderDate) and (c.endingDate is null or c.endingDate >= :orderDate) order by c.bpSet.id";
+      final Query<Object[]> bpSetQuery = OBDal.getInstance()
+          .getSession()
+          .createQuery(hqlQuery, Object[].class);
+      bpSetQuery.setParameter("clientId", OBContext.getOBContext().getCurrentClient().getId());
+      bpSetQuery.setParameter("orgList", naturalTreeOrgList);
+      bpSetQuery.setParameter("orderDate", orderDate);
+      final List<Object[]> bpSetList = bpSetQuery.list();
+      if (bpSetList.size() > 0) {
+        for (Object[] bpSet : bpSetList) {
+          final String bpSetId = (String) bpSet[1];
+          JSONArray bpSetJSONArray = null;
+          if (jsonBPSet.has(bpSetId)) {
+            bpSetJSONArray = jsonBPSet.getJSONArray(bpSetId);
+          } else {
+            bpSetJSONArray = new JSONArray();
+            jsonBPSet.put(bpSetId, bpSetJSONArray);
+          }
+          JSONObject bpSetJSON = new JSONObject();
+          bpSetJSON.put("id", (String) bpSet[0]);
+          bpSetJSON.put("bpSet", (String) bpSet[1]);
+          bpSetJSON.put("businessPartner", (String) bpSet[2]);
+          bpSetJSON.put("startingDate", (Date) bpSet[3]);
+          bpSetJSON.put("endingDate", (Date) bpSet[4]);
+          bpSetJSONArray.put(bpSetJSON);
+        }
+      }
+    } catch (JSONException e) {
+      log.error("Couldn't calculate discount rules", e);
+    }
+    return jsonBPSet;
+  }
+
+  private List<JSONObject> transformDiscountRules(List<PriceAdjustment> rules) {
+    DataToJsonConverter toJsonConverter = OBProvider.getInstance().get(DataToJsonConverter.class);
+    return toJsonConverter.toJsonObjects(rules)
+        .stream()
+        .map(this::transformDiscountRule)
+        .collect(Collectors.toList());
+  }
+
+  /**
+   * Maps some properties and adds required information to the rule so that it can be processed by
+   * the JS discount engine.
+   */
+  private JSONObject transformDiscountRule(JSONObject discountRule) {
+    try {
+      // Add required information
+      PriceAdjustment pa = OBDal.getInstance()
+          .get(PriceAdjustment.class, discountRule.getString("id"));
+      JSONObject jsonObject = null;
+
+      JSONArray cbpartners = new JSONArray();
+      for (BusinessPartner bp : pa.getPricingAdjustmentBusinessPartnerList()) {
+        jsonObject = getBusinessPartnerInfo(bp);
+        if (jsonObject != null) {
+          cbpartners.put(jsonObject);
+        }
+      }
+      discountRule.put("cbpartners", cbpartners);
+
+      JSONArray cbpartnerGroups = new JSONArray();
+      for (BusinessPartnerGroup bpg : pa.getPricingAdjustmentBusinessPartnerGroupList()) {
+        jsonObject = getBPGroupInfo(bpg);
+        if (jsonObject != null) {
+          cbpartnerGroups.put(jsonObject);
+        }
+      }
+      discountRule.put("cbpartnerGroups", cbpartnerGroups);
+
+      JSONArray cbpartnerSets = new JSONArray();
+      for (DiscountBusinessPartnerSet bps : pa.getPricingAdjustmentBusinessPartnerSetList()) {
+        jsonObject = getBPSetInfo(bps);
+        if (jsonObject != null) {
+          cbpartnerSets.put(jsonObject);
+        }
+      }
+      discountRule.put("cbpartnerSets", cbpartnerSets);
+
+      JSONArray products = new JSONArray();
+      for (Product p : pa.getPricingAdjustmentProductList()) {
+        jsonObject = getProductInfo(p);
+        if (jsonObject != null) {
+          products.put(jsonObject);
+        }
+      }
+      discountRule.put("products", products);
+
+      JSONArray productCategories = new JSONArray();
+      for (ProductCategory pc : pa.getPricingAdjustmentProductCategoryList()) {
+        jsonObject = getProductCategoryInfo(pc);
+        if (jsonObject != null) {
+          JSONObject category = new JSONObject();
+          category.put("productCategory", jsonObject);
+          productCategories.put(category);
+        }
+      }
+      discountRule.put("productCategories", productCategories);
+
+      JSONArray characteristics = new JSONArray();
+      for (Characteristic c : pa.getPricingAdjustmentCharacteristicList()) {
+        jsonObject = getCharacteristicInfo(c);
+        if (jsonObject != null) {
+          characteristics.put(jsonObject);
+        }
+      }
+      discountRule.put("productCharacteristics", characteristics);
+
+      JSONArray priceLists = new JSONArray();
+      for (PriceList pl : pa.getPricingAdjustmentPriceListList()) {
+        jsonObject = getPriceListInfo(pl);
+        if (jsonObject != null) {
+          priceLists.put(jsonObject);
+        }
+      }
+      discountRule.put("pricelists", priceLists);
+
+      // Add information from external discount definitions
+      dataHookSelector.getDiscountDataHooks(pa.getDiscountType().getId())
+          .stream()
+          .forEach(hook -> hook.addDataToDiscount(pa, discountRule));
+
+    } catch (JSONException e) {
+      log.error("Couldn't calculate discount rules", e);
+    }
+    return discountRule;
+  }
+
+  private JSONObject getBusinessPartnerInfo(BusinessPartner businessPartner) throws JSONException {
+    if (!businessPartner.isActive() || !businessPartner.getBusinessPartner().isActive()) {
+      return null;
+    }
+    JSONObject info = new JSONObject();
+    info.put("id", businessPartner.getId());
+    info.put("priceAdjustment", businessPartner.getPriceAdjustment().getId());
+    info.put("_identifier", businessPartner.getIdentifier());
+    JSONObject bpInfo = new JSONObject();
+    bpInfo.put("id", businessPartner.getBusinessPartner().getId());
+    info.put("businessPartner", bpInfo);
+    return info;
+  }
+
+  private JSONObject getBPGroupInfo(BusinessPartnerGroup businessPartnerGroup)
+      throws JSONException {
+    if (!businessPartnerGroup.isActive()
+        || !businessPartnerGroup.getBusinessPartnerCategory().isActive()) {
+      return null;
+    }
+    JSONObject info = new JSONObject();
+    info.put("id", businessPartnerGroup.getId());
+    info.put("priceAdjustment", businessPartnerGroup.getPriceAdjustment().getId());
+    info.put("_identifier", businessPartnerGroup.getIdentifier());
+    JSONObject bpCategoryInfo = new JSONObject();
+    bpCategoryInfo.put("id", businessPartnerGroup.getBusinessPartnerCategory().getId());
+    info.put("businessPartnerCategory", bpCategoryInfo);
+    return info;
+  }
+
+  private JSONObject getBPSetInfo(DiscountBusinessPartnerSet businessPartnerSet)
+      throws JSONException {
+    if (!businessPartnerSet.isActive() || !businessPartnerSet.getBpSet().isActive()) {
+      return null;
+    }
+    JSONObject info = new JSONObject();
+    info.put("id", businessPartnerSet.getId());
+    info.put("_identifier", businessPartnerSet.getIdentifier());
+    info.put("bpSet", businessPartnerSet.getBpSet().getId());
+    info.put("priceAdjustment", businessPartnerSet.getPromotionDiscount().getId());
+    return info;
+  }
+
+  private JSONObject getProductInfo(Product discountProduct) throws JSONException {
+    if (!discountProduct.isActive() || !discountProduct.getProduct().isActive()) {
+      return null;
+    }
+    JSONObject prodInfo = new JSONObject();
+    JSONObject product = new JSONObject();
+    product.put("id", discountProduct.getProduct().getId());
+    product.put("_identifier", discountProduct.getProduct().getSearchKey());
+    prodInfo.put("product", product);
+
+    // Discount rule additional properties
+    prodInfo.put("obdiscIsGift", discountProduct.isObdiscIsGift());
+    prodInfo.put("obdiscQty", discountProduct.getObdiscQty());
+    prodInfo.put("obdiscGifqty", discountProduct.getObdiscGifqty());
+
+    return prodInfo;
+  }
+
+  private JSONObject getProductCategoryInfo(ProductCategory discountProductCategory)
+      throws JSONException {
+    if (!discountProductCategory.isActive()
+        || !discountProductCategory.getProductCategory().isActive()) {
+      return null;
+    }
+    JSONObject prodInfo = new JSONObject();
+    prodInfo.put("id", discountProductCategory.getProductCategory().getId());
+    prodInfo.put("_identifier", discountProductCategory.getProductCategory().getSearchKey());
+    return prodInfo;
+  }
+
+  private JSONObject getCharacteristicInfo(Characteristic characteristic) throws JSONException {
+    if (!characteristic.isActive() || !characteristic.getCharacteristic().isActive()) {
+      return null;
+    }
+    JSONObject info = new JSONObject();
+    info.put("id", characteristic.getId());
+    info.put("_identifier", characteristic.getCharacteristic().getIdentifier());
+    info.put("characteristic", characteristic.getCharacteristic().getId());
+    info.put("chValue", characteristic.getChValue().getId());
+    info.put("isincludecharacteristics", characteristic.isIncludecharacteristics());
+    return info;
+  }
+
+  private JSONObject getPriceListInfo(PriceList priceList) throws JSONException {
+    if (!priceList.isActive() || !priceList.getPriceList().isActive()) {
+      return null;
+    }
+    JSONObject priceListInfo = new JSONObject();
+    priceListInfo.put("id", priceList.getId());
+    priceListInfo.put("_identifier", priceList.getIdentifier());
+    priceListInfo.put("priceAdjustment", priceList.getPriceAdjustment().getId());
+    priceListInfo.put("m_pricelist_id", priceList.getPriceList().getId());
+    return priceListInfo;
+  }
+
+  private Set<String> getProducts(DiscountsTicket ticket) {
+    return ticket.getLines()
+        .stream()
+        .map(line -> line.getProduct().getId())
+        .collect(Collectors.toSet());
+  }
+
+  private String getBusinessPartnerCategory(String businessPartnerId) {
+    String hql = "SELECT bp.businessPartnerCategory.id FROM BusinessPartner bp WHERE bp.id = :bpId";
+    return OBDal.getInstance()
+        .getSession()
+        .createQuery(hql, String.class)
+        .setParameter("bpId", businessPartnerId)
+        .uniqueResult();
+  }
+
+  private Set<String> getProductCategories(Set<String> productIds) {
+    if (productIds.isEmpty()) {
+      return Collections.emptySet();
+    }
+    String hql = "SELECT DISTINCT(p.productCategory.id) FROM Product p WHERE p.id IN (:productIds)";
+    List<String> productCategories = OBDal.getInstance()
+        .getSession()
+        .createQuery(hql, String.class)
+        .setParameterList("productIds", new ArrayList<>(productIds))
+        .list();
+    return new HashSet<>(productCategories);
+  }
+
+  private Date getOrderDateWithoutTime(final String orderDate) {
+    try {
+      final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+      return dateFormat.parse(orderDate);
+    } catch (Exception e) {
+      log.error("Couldn't get order date", e);
+    }
+    return null;
+  }
+
+  private List<String> getOrgTree(String organization) {
+    List<String> result = new ArrayList<String>();
+    if (organization != null) {
+      result = OBContext.getOBContext()
+          .getOrganizationStructureProvider()
+          .getParentList(organization, true);
+    }
+    return result;
+  }
+
+  private List<String> getManualPromotions() {
+    return new ArrayList<String>(
+        Arrays.asList("D1D193305A6443B09B299259493B272A", "F3B0FB45297844549D9E6B5F03B23A82",
+            "7B49D8CC4E084A75B7CB4D85A6A3A578", "20E4EC27397344309A2185097392D964",
+            "8338556C0FBF45249512DB343FEFD280", "ADF7E7F91B3A49869FB3F89B8C5A325E",
+            "096984DC2B944C85A9162C66C37EE7A3", "971642418DD24DE5BD860D63EF57D5F6",
+            "00535FB65D9941AE9575546FBAF11B95", "4776954A80E747C5BAA565AD464759BF"));
+  }
+
+  /**
+   * Defines the qualifier used to register a DiscountJSExecutor instance.
+   */
+  @javax.inject.Qualifier
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.TYPE })
+  public @interface JSEngine {
+    String value();
+  }
+
+  /**
+   * A class used to select a DiscountJSExecutor instance.
+   */
+  @SuppressWarnings("all")
+  public static class Selector extends AnnotationLiteral<JSEngine> implements JSEngine {
+    private static final long serialVersionUID = 1L;
+
+    private final String value;
+
+    public Selector(String value) {
+      this.value = value;
+    }
+
+    @Override
+    public String value() {
+      return value;
+    }
+  }
+}
diff --git a/src/org/openbravo/discounts/engine/DiscountJSExecutor.java.rej b/src/org/openbravo/discounts/engine/DiscountJSExecutor.java.rej
new file mode 100644
index 0000000..00fd325
--- /dev/null
+++ b/src/org/openbravo/discounts/engine/DiscountJSExecutor.java.rej
@@ -0,0 +1,17 @@
+***************
+*** 55,60 ****
+  import org.openbravo.discounts.api.DiscountsTicket;
+  import org.openbravo.discounts.engine.hook.DiscountRulesHook;
+  import org.openbravo.discounts.engine.hook.WhereClauseHook;
+  import org.openbravo.model.pricing.priceadjustment.BusinessPartner;
+  import org.openbravo.model.pricing.priceadjustment.BusinessPartnerGroup;
+  import org.openbravo.model.pricing.priceadjustment.Characteristic;
+--- 57,64 ----
+  import org.openbravo.discounts.api.DiscountsTicket;
+  import org.openbravo.discounts.engine.hook.DiscountRulesHook;
+  import org.openbravo.discounts.engine.hook.WhereClauseHook;
++ import org.openbravo.erpCommon.businessUtility.Preferences;
++ import org.openbravo.erpCommon.utility.PropertyException;
+  import org.openbravo.model.pricing.priceadjustment.BusinessPartner;
+  import org.openbravo.model.pricing.priceadjustment.BusinessPartnerGroup;
+  import org.openbravo.model.pricing.priceadjustment.Characteristic;
-- 
2.31.0

