Hook selectors into the price computation path
[aquarium] / src / main / scala / gr / grnet / aquarium / computation / TimeslotComputations.scala
index 5475793..c0da20d 100644 (file)
 package gr.grnet.aquarium.computation
 
 import collection.immutable.SortedMap
-import com.ckkloverdos.maybe.{NoVal, Maybe, Just}
+import com.ckkloverdos.maybe.{NoVal, Maybe}
 import gr.grnet.aquarium.util.{ContextualLogger, Loggable}
-import gr.grnet.aquarium.store.PolicyStore
-import gr.grnet.aquarium.util.date.MutableDateCalc
-import gr.grnet.aquarium.{AquariumInternalError, AquariumException}
-import gr.grnet.aquarium.event.model.resource.ResourceEventModel
-import gr.grnet.aquarium.logic.accounting.algorithm.CostPolicyAlgorithmCompiler
-import gr.grnet.aquarium.logic.accounting.dsl.{DSL, DSLResourcesMap, DSLPolicy, DSLResource, DSLPriceList, DSLAlgorithm, DSLAgreement, Timeslot, DSLUtils}
+import gr.grnet.aquarium.AquariumInternalError
+import gr.grnet.aquarium.logic.accounting.dsl.Timeslot
+import gr.grnet.aquarium.policy._
+import collection.immutable
+import com.ckkloverdos.maybe.Just
+import gr.grnet.aquarium.policy.ResourceType
+import gr.grnet.aquarium.policy.EffectiveUnitPrice
+import gr.grnet.aquarium.charging.{ChargingBehavior, Chargeslot}
 
 /**
  * Methods for converting accounting events to wallet entries.
@@ -51,9 +53,7 @@ import gr.grnet.aquarium.logic.accounting.dsl.{DSL, DSLResourcesMap, DSLPolicy,
  * @author Georgios Gousios <gousiosg@gmail.com>
  * @author Christos KK Loverdos <loverdos@gmail.com>
  */
-trait TimeslotComputations extends Loggable {
-  // TODO: favour composition over inheritance until we decide what to do with DSLUtils (and TimeslotComputations).
-  protected val dslUtils = new DSLUtils {}
+object TimeslotComputations extends Loggable {
 
   /**
    * Breaks a reference timeslot (e.g. billing period) according to policies and agreements.
@@ -64,25 +64,19 @@ trait TimeslotComputations extends Loggable {
    * @return
    */
   protected
-  def splitTimeslotByPoliciesAndAgreements(referenceTimeslot: Timeslot,
-                                           policyTimeslots: List[Timeslot],
-                                           agreementTimeslots: List[Timeslot],
-                                           clogM: Maybe[ContextualLogger] = NoVal): List[Timeslot] = {
-
-    //    val clog = ContextualLogger.fromOther(clogM, logger, "splitTimeslotByPoliciesAndAgreements()")
-    //    clog.begin()
+  def splitTimeslotByPoliciesAndAgreements(
+      referenceTimeslot: Timeslot,
+      policyTimeslots: List[Timeslot],
+      agreementTimeslots: List[Timeslot],
+      clogM: Maybe[ContextualLogger] = NoVal
+  ): List[Timeslot] = {
 
     // Align policy and agreement validity timeslots to the referenceTimeslot
     val alignedPolicyTimeslots = referenceTimeslot.align(policyTimeslots)
     val alignedAgreementTimeslots = referenceTimeslot.align(agreementTimeslots)
 
-    //    clog.debug("referenceTimeslot = %s", referenceTimeslot)
-    //    clog.debugSeq("alignedPolicyTimeslots", alignedPolicyTimeslots, 0)
-    //    clog.debugSeq("alignedAgreementTimeslots", alignedAgreementTimeslots, 0)
-
     val result = alignTimeslots(alignedPolicyTimeslots, alignedAgreementTimeslots)
-    //    clog.debugSeq("result", result, 1)
-    //    clog.end()
+
     result
   }
 
@@ -92,53 +86,46 @@ trait TimeslotComputations extends Loggable {
    *
    */
   protected
-  def resolveEffectiveAlgorithmsAndPriceLists(alignedTimeslot: Timeslot,
-                                              agreement: DSLAgreement,
-                                              clogOpt: Option[ContextualLogger] = None):
-  (Map[Timeslot, DSLAlgorithm], Map[Timeslot, DSLPriceList]) = {
+  def resolveEffectiveUnitPrices(
+      alignedTimeslot: Timeslot,
+      policy: PolicyModel,
+      agreement: UserAgreementModel,
+      resourceType: ResourceType,
+      effectivePriceTableSelector: FullPriceTable ⇒ EffectivePriceTable,
+      clogOpt: Option[ContextualLogger] = None
+  ): SortedMap[Timeslot, Double] = {
 
-    val clog = ContextualLogger.fromOther(clogOpt, logger, "resolveEffectiveAlgorithmsAndPriceLists()")
+    val clog = ContextualLogger.fromOther(clogOpt, logger, "resolveEffectiveUnitPrices()")
 
     // Note that most of the code is taken from calcChangeChunks()
-    val alg = dslUtils.resolveEffectiveAlgorithmsForTimeslot(alignedTimeslot, agreement)
-    val pri = dslUtils.resolveEffectivePricelistsForTimeslot(alignedTimeslot, agreement)
-    val chargeChunks = splitChargeChunks(alg, pri)
-    val algorithmByTimeslot = chargeChunks._1
-    val pricelistByTimeslot = chargeChunks._2
-
-    assert(algorithmByTimeslot.size == pricelistByTimeslot.size)
-
-    (algorithmByTimeslot, pricelistByTimeslot)
+    val ret = resolveEffectiveUnitPricesForTimeslot(alignedTimeslot, policy, agreement, resourceType, effectivePriceTableSelector)
+    ret map {case (t,p) => (t,p.unitPrice)}
   }
 
-  protected
-  def computeInitialChargeslots(referenceTimeslot: Timeslot,
-                                dslResource: DSLResource,
-                                policiesByTimeslot: Map[Timeslot, DSLPolicy],
-                                agreementNamesByTimeslot: Map[Timeslot, String],
-                                clogOpt: Option[ContextualLogger] = None): List[Chargeslot] = {
+  def computeInitialChargeslots(
+      referenceTimeslot: Timeslot,
+      resourceType: ResourceType,
+      policyByTimeslot: SortedMap[Timeslot, PolicyModel],
+      agreementByTimeslot: SortedMap[Timeslot, UserAgreementModel],
+      effectivePriceTableSelector: FullPriceTable ⇒ EffectivePriceTable,
+      clogOpt: Option[ContextualLogger] = None
+  ): List[Chargeslot] = {
 
     val clog = ContextualLogger.fromOther(clogOpt, logger, "computeInitialChargeslots()")
-    //    clog.begin()
 
-    val policyTimeslots = policiesByTimeslot.keySet
-    val agreementTimeslots = agreementNamesByTimeslot.keySet
+    val policyTimeslots = policyByTimeslot.keySet
+    val agreementTimeslots = agreementByTimeslot.keySet
 
-    //    clog.debugMap("policiesByTimeslot", policiesByTimeslot, 1)
-    //    clog.debugMap("agreementNamesByTimeslot", agreementNamesByTimeslot, 1)
-
-    def getPolicy(ts: Timeslot): DSLPolicy = {
-      policiesByTimeslot.find(_._1.contains(ts)).get._2
+    def getPolicyWithin(ts: Timeslot): PolicyModel = {
+      policyByTimeslot.find(_._1.contains(ts)).get._2
     }
-    def getAgreementName(ts: Timeslot): String = {
-      agreementNamesByTimeslot.find(_._1.contains(ts)).get._2
+    def getAgreementWithin(ts: Timeslot): UserAgreementModel = {
+      agreementByTimeslot.find(_._1.contains(ts)).get._2
     }
 
     // 1. Round ONE: split time according to overlapping policies and agreements.
-    //    clog.begin("ROUND 1")
+    //val alignedTimeslots = List(referenceTimeslot) //splitTimeslotByPoliciesAndAgreements(referenceTimeslot, policyTimeslots.toList, agreementTimeslots.toList, Just(clog))
     val alignedTimeslots = splitTimeslotByPoliciesAndAgreements(referenceTimeslot, policyTimeslots.toList, agreementTimeslots.toList, Just(clog))
-    //    clog.debugSeq("alignedTimeslots", alignedTimeslots, 1)
-    //    clog.end("ROUND 1")
 
     // 2. Round TWO: Use the aligned timeslots of Round ONE to produce even more
     //    fine-grained timeslots according to applicable algorithms.
@@ -147,229 +134,41 @@ trait TimeslotComputations extends Loggable {
     val allChargeslots = for {
       alignedTimeslot <- alignedTimeslots
     } yield {
-      //      val alignedTimeslotMsg = "alignedTimeslot = %s".format(alignedTimeslot)
-      //      clog.begin(alignedTimeslotMsg)
-
-      val dslPolicy = getPolicy(alignedTimeslot)
+      //val policy = policyByTimeslot.valuesIterator.next()//getPolicyWithin(alignedTimeslot)
+      val policy = getPolicyWithin(alignedTimeslot)
       //      clog.debug("dslPolicy = %s", dslPolicy)
-      val agreementName = getAgreementName(alignedTimeslot)
-      //      clog.debug("agreementName = %s", agreementName)
-      val agreementOpt = dslPolicy.findAgreement(agreementName)
-      //      clog.debug("agreementOpt = %s", agreementOpt)
-
-      agreementOpt match {
-        case None ⇒
-          val errMsg = "Unknown agreement %s during %s".format(agreementName, alignedTimeslot)
-          clog.error("%s", errMsg)
-          throw new AquariumException(errMsg)
-
-        case Some(agreement) ⇒
-          // TODO: Factor this out, just like we did with:
-          // TODO:  val alignedTimeslots = splitTimeslotByPoliciesAndAgreements
-          // Note that most of the code is already taken from calcChangeChunks()
-          val r = resolveEffectiveAlgorithmsAndPriceLists(alignedTimeslot, agreement, Some(clog))
-          val algorithmByTimeslot: Map[Timeslot, DSLAlgorithm] = r._1
-          val pricelistByTimeslot: Map[Timeslot, DSLPriceList] = r._2
-
-          // Now, the timeslots must be the same
-          val finegrainedTimeslots = algorithmByTimeslot.keySet
-
-          val chargeslots = for {
-            finegrainedTimeslot <- finegrainedTimeslots
-          } yield {
-            //            val finegrainedTimeslotMsg = "finegrainedTimeslot = %s".format(finegrainedTimeslot)
-            //            clog.begin(finegrainedTimeslotMsg)
-
-            val dslAlgorithm = algorithmByTimeslot(finegrainedTimeslot) // TODO: is this correct?
-            //            clog.debug("dslAlgorithm = %s", dslAlgorithm)
-            //            clog.debugMap("dslAlgorithm.algorithms", dslAlgorithm.algorithms, 1)
-            val dslPricelist = pricelistByTimeslot(finegrainedTimeslot) // TODO: is this correct?
-            //            clog.debug("dslPricelist = %s", dslPricelist)
-            //            clog.debug("dslResource = %s", dslResource)
-            val algorithmDefOpt = dslAlgorithm.algorithms.get(dslResource)
-            //            clog.debug("algorithmDefOpt = %s", algorithmDefOpt)
-            val priceUnitOpt = dslPricelist.prices.get(dslResource)
-            //            clog.debug("priceUnitOpt = %s", priceUnitOpt)
-
-            val chargeslot = (algorithmDefOpt, priceUnitOpt) match {
-              case (None, None) ⇒
-                throw new AquariumException(
-                  "Unknown algorithm and price unit for resource %s during %s".
-                    format(dslResource, finegrainedTimeslot))
-              case (None, _) ⇒
-                throw new AquariumException(
-                  "Unknown algorithm for resource %s during %s".
-                    format(dslResource, finegrainedTimeslot))
-              case (_, None) ⇒
-                throw new AquariumException(
-                  "Unknown price unit for resource %s during %s".
-                    format(dslResource, finegrainedTimeslot))
-              case (Some(algorithmDefinition), Some(priceUnit)) ⇒
-                Chargeslot(finegrainedTimeslot.from.getTime, finegrainedTimeslot.to.getTime, algorithmDefinition, priceUnit)
-            }
-
-            //            clog.end(finegrainedTimeslotMsg)
-            chargeslot
-          }
-
-          //          clog.end(alignedTimeslotMsg)
-          chargeslots.toList
-      }
-    }
-    //    clog.end("ROUND 2")
-
-
-    val result = allChargeslots.flatten
-    //    clog.debugSeq("result", allChargeslots, 1)
-    //    clog.end()
-    result
-  }
-
-  /**
-   * Compute the charge slots generated by a particular resource event.
-   *
-   */
-  def computeFullChargeslots(previousResourceEventOpt: Option[ResourceEventModel],
-                             currentResourceEvent: ResourceEventModel,
-                             oldCredits: Double,
-                             oldTotalAmount: Double,
-                             newTotalAmount: Double,
-                             dslResource: DSLResource,
-                             defaultResourceMap: DSLResourcesMap,
-                             agreementNamesByTimeslot: SortedMap[Timeslot, String],
-                             algorithmCompiler: CostPolicyAlgorithmCompiler,
-                             policyStore: PolicyStore,
-                             clogOpt: Option[ContextualLogger] = None): (Timeslot, List[Chargeslot]) = {
-
-    val clog = ContextualLogger.fromOther(clogOpt, logger, "computeFullChargeslots()")
-    //    clog.begin()
-
-    val occurredDate = currentResourceEvent.occurredDate
-    val occurredMillis = currentResourceEvent.occurredMillis
-    val costPolicy = dslResource.costPolicy
-
-    val dsl = new DSL {}
-    val (referenceTimeslot, relevantPolicies, previousValue) = costPolicy.needsPreviousEventForCreditAndAmountCalculation match {
-      // We need a previous event
-      case true ⇒
-        previousResourceEventOpt match {
-          // We have a previous event
-          case Some(previousResourceEvent) ⇒
-            //            clog.debug("Have previous event")
-            //            clog.debug("previousValue = %s", previousResourceEvent.value)
-
-            val referenceTimeslot = Timeslot(previousResourceEvent.occurredDate, occurredDate)
-            //            clog.debug("referenceTimeslot = %s".format(referenceTimeslot))
-
-            // all policies within the interval from previous to current resource event
-            //            clog.debug("Calling policyStore.loadAndSortPoliciesWithin(%s)", referenceTimeslot)
-            val relevantPolicies = policyStore.loadAndSortPoliciesWithin(referenceTimeslot.from.getTime, referenceTimeslot.to.getTime, dsl)
-            //            clog.debugMap("==> relevantPolicies", relevantPolicies, 0)
-
-            (referenceTimeslot, relevantPolicies, previousResourceEvent.value)
-
-          // We do not have a previous event
-          case None ⇒
-            throw new AquariumException(
-              "Unable to charge. No previous event given for %s".
-                format(currentResourceEvent.toDebugString))
-        }
-
-      // We do not need a previous event
-      case false ⇒
-        // ... so we cannot compute timedelta from a previous event, there is just one chargeslot
-        // referring to (almost) an instant in time
-        //        clog.debug("DO NOT have previous event")
-        val previousValue = costPolicy.getResourceInstanceUndefinedAmount
-        //        clog.debug("previousValue = costPolicy.getResourceInstanceUndefinedAmount = %s", previousValue)
-
-        val referenceTimeslot = Timeslot(new MutableDateCalc(occurredDate).goPreviousMilli.toDate, occurredDate)
-        //        clog.debug("referenceTimeslot = %s".format(referenceTimeslot))
-
-        //        clog.debug("Calling policyStore.loadValidPolicyEntryAt(%s)", new MutableDateCalc(occurredMillis))
-        val relevantPolicyOpt = policyStore.loadValidPolicyAt(occurredMillis, dsl)
-        //        clog.debug("  ==> relevantPolicyM = %s", relevantPolicyM)
-
-        val relevantPolicies = relevantPolicyOpt match {
-          case Some(relevantPolicy) ⇒
-            Map(referenceTimeslot -> relevantPolicy)
-
-          case None ⇒
-            throw new AquariumInternalError("No relevant policy found for %s".format(referenceTimeslot))
-        }
-
-        (referenceTimeslot, relevantPolicies, previousValue)
-    }
-
-    val initialChargeslots = computeInitialChargeslots(
-      referenceTimeslot,
-      dslResource,
-      relevantPolicies,
-      agreementNamesByTimeslot,
-      Some(clog)
-    )
-
-    val fullChargeslots = initialChargeslots.map {
-      case chargeslot@Chargeslot(startMillis, stopMillis, algorithmDefinition, unitPrice, _) ⇒
-        val execAlgorithm = algorithmCompiler.compile(algorithmDefinition)
-        val valueMap = costPolicy.makeValueMap(
-          oldCredits,
-          oldTotalAmount,
-          newTotalAmount,
-          stopMillis - startMillis,
-          previousValue,
-          currentResourceEvent.value,
-          unitPrice
+      //val userAgreement = agreementByTimeslot.valuesIterator.next()//getAgreementWithin(alignedTimeslot)
+      val userAgreement = getAgreementWithin(alignedTimeslot)
+
+      val unitPriceByTimeslot = resolveEffectiveUnitPrices(
+        alignedTimeslot,
+        policy,
+        userAgreement,
+        resourceType,
+        effectivePriceTableSelector,
+        Some(clog)
+      )
+
+      // Now, the timeslots must be the same
+      val finegrainedTimeslots = unitPriceByTimeslot.keySet
+
+      val chargeslots = for (finegrainedTimeslot ← finegrainedTimeslots) yield {
+        Chargeslot(
+          finegrainedTimeslot.from.getTime,
+          finegrainedTimeslot.to.getTime,
+          unitPriceByTimeslot(finegrainedTimeslot)
         )
+      }
 
-        //              clog.debug("execAlgorithm = %s", execAlgorithm)
-        clog.debugMap("valueMap", valueMap, 1)
-
-        // This is it
-        val credits = execAlgorithm.apply(valueMap)
-        chargeslot.copyWithCredits(credits)
+      chargeslots.toList
     }
 
-    val result = referenceTimeslot -> fullChargeslots
+    val result = allChargeslots.flatten
 
     result
   }
 
   /**
-   * Align charge timeslots between algorithms and pricelists. As algorithm
-   * and pricelists can have different effectivity periods, this method
-   * examines them and splits them as necessary.
-   */
-  private[computation] def splitChargeChunks(alg: SortedMap[Timeslot, DSLAlgorithm],
-                                       price: SortedMap[Timeslot, DSLPriceList]):
-  (Map[Timeslot, DSLAlgorithm], Map[Timeslot, DSLPriceList]) = {
-
-    val zipped = alg.keySet.zip(price.keySet)
-
-    zipped.find(p => !p._1.equals(p._2)) match {
-      case None => (alg, price)
-      case Some(x) =>
-        val algTimeslot = x._1
-        val priTimeslot = x._2
-
-        assert(algTimeslot.from == priTimeslot.from)
-
-        if(algTimeslot.endsAfter(priTimeslot)) {
-          val slices = algTimeslot.slice(priTimeslot.to)
-          val algo = alg.get(algTimeslot).get
-          val newalg = alg - algTimeslot ++ Map(slices.apply(0) -> algo) ++ Map(slices.apply(1) -> algo)
-          splitChargeChunks(newalg, price)
-        }
-        else {
-          val slices = priTimeslot.slice(priTimeslot.to)
-          val pl = price.get(priTimeslot).get
-          val newPrice = price - priTimeslot ++ Map(slices.apply(0) -> pl) ++ Map(slices.apply(1) -> pl)
-          splitChargeChunks(alg, newPrice)
-        }
-    }
-  }
-
-  /**
    * Given two lists of timeslots, produce a list which contains the
    * set of timeslot slices, as those are defined by
    * timeslot overlaps.
@@ -409,4 +208,69 @@ trait TimeslotComputations extends Loggable {
       a.head :: alignTimeslots(safeTail(a), safeTail(b))
     }
   }
+
+    type PriceMap =  immutable.SortedMap[Timeslot, EffectiveUnitPrice]
+    private type PriceList = List[EffectiveUnitPrice]
+    private def emptyMap = immutable.SortedMap[Timeslot,EffectiveUnitPrice]()
+
+    /**
+     * Resolves the effective price list for each chunk of the
+     * provided timeslot and returns it as a Map
+     */
+    private[this] def resolveEffectiveUnitPricesForTimeslot(
+        alignedTimeslot: Timeslot,
+        policy: PolicyModel,
+        agreement: UserAgreementModel,
+        resourceType: ResourceType,
+        effectivePriceTableSelector: FullPriceTable ⇒ EffectivePriceTable
+    ): PriceMap = {
+
+      val fullPriceTable = agreement.computeFullPriceTable(policy)
+      val effectivePriceTable = effectivePriceTableSelector(fullPriceTable)
+
+      resolveEffective(alignedTimeslot, effectivePriceTable.priceOverrides)
+      //immutable.SortedMap(alignedTimeslot -> effectivePriceTable.priceOverrides.head)
+    }
+
+    private def printPriceList(p: PriceList) : Unit = {
+      Console.err.println("BEGIN PRICE LIST")
+      for { p1 <- p } Console.err.println(p1)
+      Console.err.println("END PRICE LIST")
+    }
+
+    private def printPriceMap(m: PriceMap) = {
+      Console.err.println("BEGIN PRICE MAP")
+      for { (t,p) <- m.toList } Console.err.println("Timeslot " + t + "\t\t" + p)
+      Console.err.println("END PRICE MAP")
+    }
+
+    private def resolveEffective(alignedTimeslot: Timeslot,p:PriceList): PriceMap = {
+      //Console.err.println("\n\nInput timeslot: " + alignedTimeslot + "\n\n")
+      //printPriceList(p)
+      val ret =  resolveEffective3(alignedTimeslot,p) //HERE
+      //printPriceMap(ret)
+      ret
+    }
+
+
+    private def resolveEffective3(alignedTimeslot: Timeslot, effectiveUnitPrices: PriceList): PriceMap =
+      effectiveUnitPrices match {
+        case Nil =>
+          emptyMap
+        case hd::tl =>
+          val (satisfied,notSatisfied) = hd splitTimeslot alignedTimeslot
+          val satisfiedMap = satisfied.foldLeft (emptyMap)  {(map,t) =>
+          //Console.err.println("Adding timeslot" + t +
+          // " for policy " + policy.name)
+            map + ((t,hd))
+          }
+          val notSatisfiedMap = notSatisfied.foldLeft (emptyMap) {(map,t) =>
+            val otherMap = resolveEffective3(t,tl)
+            //Console.err.println("Residual timeslot: " + t)
+            val ret = map ++ otherMap
+            ret
+          }
+          val ret = satisfiedMap ++ notSatisfiedMap
+          ret
+      }
 }