Implement Once behavior with the new scheme. Refactor in the process
[aquarium] / src / main / scala / gr / grnet / aquarium / charging / ChargingBehaviorSkeleton.scala
index 993fe62..9a5551e 100644 (file)
@@ -41,16 +41,13 @@ import scala.collection.mutable
 import gr.grnet.aquarium.event.model.resource.ResourceEventModel
 import gr.grnet.aquarium.{Aquarium, AquariumInternalError}
 import gr.grnet.aquarium.policy.{FullPriceTable, EffectivePriceTable, UserAgreementModel, ResourceType}
-import com.ckkloverdos.key.TypedKey
 import gr.grnet.aquarium.util._
-import gr.grnet.aquarium.util.LogHelpers.Debug
 import gr.grnet.aquarium.util.date.TimeHelpers
 import gr.grnet.aquarium.charging.wallet.WalletEntry
 import gr.grnet.aquarium.computation.{TimeslotComputations, BillingMonthInfo}
-import gr.grnet.aquarium.charging.state.AgreementHistory
+import gr.grnet.aquarium.charging.state.{WorkingResourceInstanceChargingState, WorkingResourcesChargingState, AgreementHistoryModel}
 import gr.grnet.aquarium.logic.accounting.dsl.Timeslot
 import gr.grnet.aquarium.store.PolicyStore
-import gr.grnet.aquarium.charging.ChargingBehavior.EnvKeys
 
 /**
  * A charging behavior indicates how charging for a resource will be done
@@ -60,113 +57,80 @@ import gr.grnet.aquarium.charging.ChargingBehavior.EnvKeys
  */
 
 abstract class ChargingBehaviorSkeleton(
-    final val alias: String,
-    final val inputs: Set[ChargingInput],
-    final val selectorHierarchy: List[List[String]] = Nil
+    final val selectorLabelsHierarchy: List[String]
 ) extends ChargingBehavior with Loggable {
 
-  final val inputNames = inputs.map(_.name)
-
-  @inline private[this] def hrs(millis: Double) = {
+  protected def hrs(millis: Double) = {
     val hours = millis / 1000 / 60 / 60
     val roundedHours = hours
     roundedHours
   }
 
-  protected def computeCreditsToSubtract(
-      oldCredits: Double,
-      oldAccumulatingAmount: Double,
-      newAccumulatingAmount: Double,
-      timeDeltaMillis: Long,
-      previousValue: Double,
-      currentValue: Double,
-      unitPrice: Double,
-      details: Map[String, String]
-  ): (Double, String) = {
-    alias match {
-     case ChargingBehaviorAliases.continuous ⇒
-       val credits = hrs(timeDeltaMillis) * oldAccumulatingAmount * unitPrice
-       val explanation = "Time(%s) * OldTotal(%s) * Unit(%s)".format(
-         hrs(timeDeltaMillis),
-         oldAccumulatingAmount,
-         unitPrice
-       )
-
-       (credits, explanation)
-
-     case ChargingBehaviorAliases.vmtime ⇒
-       val credits = hrs(timeDeltaMillis) * unitPrice
-       val explanation = "Time(%s) * Unit(%s)".format(hrs(timeDeltaMillis), unitPrice)
-
-       (credits, explanation)
-
-     case ChargingBehaviorAliases.once ⇒
-       val credits = currentValue
-       val explanation = "Value(%s)".format(currentValue)
-
-       (credits, explanation)
-
-     case name ⇒
-       throw new AquariumInternalError("Cannot compute credit diff for charging behavior %s".format(name))
-    }
-  }
-
   protected def rcDebugInfo(rcEvent: ResourceEventModel) = {
     rcEvent.toDebugString
   }
 
-  protected def computeSelectorPath(
-      chargingData: mutable.Map[String, Any],
-      currentResourceEvent: ResourceEventModel,
-      referenceTimeslot: Timeslot,
-      previousValue: Double,
-      totalCredits: Double,
-      oldAccumulatingAmount: Double,
-      newAccumulatingAmount: Double
-  ): List[String]
+  protected def newWorkingResourceInstanceChargingState() = {
+    new WorkingResourceInstanceChargingState(
+      mutable.Map(),
+      Nil,
+      Nil,
+      0.0,
+      0.0,
+      0.0,
+      0.0
+    )
+  }
 
-  /**
-   *
-   * @param chargingData
-   * @param previousResourceEventOpt
-   * @param currentResourceEvent
-   * @param billingMonthInfo
-   * @param referenceTimeslot
-   * @param resourceType
-   * @param agreementByTimeslot
-   * @param previousValue
-   * @param totalCredits
-   * @param policyStore
-   * @param walletEntryRecorder
-   * @return The number of wallet entries recorded and the new total credits
-   */
-  protected def computeChargeslots(
-      chargingData: mutable.Map[String, Any],
-      previousResourceEventOpt: Option[ResourceEventModel],
-      currentResourceEvent: ResourceEventModel,
+  final protected def ensureWorkingState(
+      workingResourcesChargingState: WorkingResourcesChargingState,
+      resourceEvent: ResourceEventModel
+  ) {
+    ensureResourcesChargingStateDetails(workingResourcesChargingState.details)
+    ensureResourceInstanceChargingState(workingResourcesChargingState, resourceEvent)
+  }
+
+  protected def ensureResourcesChargingStateDetails(
+    details: mutable.Map[String, Any]
+  ) {}
+
+  protected def ensureResourceInstanceChargingState(
+      workingResourcesChargingState: WorkingResourcesChargingState,
+      resourceEvent: ResourceEventModel
+  ) {
+
+    val instanceID = resourceEvent.instanceID
+    val stateOfResourceInstance = workingResourcesChargingState.stateOfResourceInstance
+
+    stateOfResourceInstance.get(instanceID) match {
+      case None ⇒
+        stateOfResourceInstance(instanceID) = newWorkingResourceInstanceChargingState()
+
+      case _ ⇒
+    }
+  }
+
+  protected def computeWalletEntriesForNewEvent(
+      resourceEvent: ResourceEventModel,
+      resourceType: ResourceType,
       billingMonthInfo: BillingMonthInfo,
+      totalCredits: Double,
       referenceTimeslot: Timeslot,
-      resourceType: ResourceType,
       agreementByTimeslot: immutable.SortedMap[Timeslot, UserAgreementModel],
-      previousValue: Double,
-      totalCredits: Double,
+      workingResourcesChargingStateDetails: mutable.Map[String, Any],
+      workingResourceInstanceChargingState: WorkingResourceInstanceChargingState,
       policyStore: PolicyStore,
       walletEntryRecorder: WalletEntry ⇒ Unit
   ): (Int, Double) = {
 
-    val currentValue = currentResourceEvent.value
-    val userID = currentResourceEvent.userID
-    val currentDetails = currentResourceEvent.details
-
-    var _oldAccumulatingAmount = getChargingData(
-      chargingData,
-      EnvKeys.ResourceInstanceAccumulatingAmount
-    ).getOrElse(getResourceInstanceInitialAmount)
+    val userID = resourceEvent.userID
+    val resourceEventDetails = resourceEvent.details
 
     var _oldTotalCredits = totalCredits
 
-    var _newAccumulatingAmount = this.computeNewAccumulatingAmount(_oldAccumulatingAmount, currentValue, currentDetails)
-    setChargingData(chargingData, EnvKeys.ResourceInstanceAccumulatingAmount, _newAccumulatingAmount)
+    var _newAccumulatingAmount = computeNewAccumulatingAmount(workingResourceInstanceChargingState, resourceEventDetails)
+    // It will also update the old one inside the data structure.
+    workingResourceInstanceChargingState.setNewAccumulatingAmount(_newAccumulatingAmount)
 
     val policyByTimeslot = policyStore.loadAndSortPoliciesWithin(
       referenceTimeslot.from.getTime,
@@ -176,19 +140,16 @@ abstract class ChargingBehaviorSkeleton(
     val effectivePriceTableSelector: FullPriceTable ⇒ EffectivePriceTable = fullPriceTable ⇒ {
       this.selectEffectivePriceTable(
         fullPriceTable,
-        chargingData,
-        currentResourceEvent,
+        workingResourcesChargingStateDetails,
+        workingResourceInstanceChargingState,
+        resourceEvent,
         referenceTimeslot,
-        previousValue,
-        totalCredits,
-        _oldAccumulatingAmount,
-        _newAccumulatingAmount
+        totalCredits
       )
     }
 
     val initialChargeslots = TimeslotComputations.computeInitialChargeslots(
       referenceTimeslot,
-      resourceType,
       policyByTimeslot,
       agreementByTimeslot,
       effectivePriceTableSelector
@@ -199,14 +160,10 @@ abstract class ChargingBehaviorSkeleton(
         val timeDeltaMillis = stopMillis - startMillis
 
         val (creditsToSubtract, explanation) = this.computeCreditsToSubtract(
-          _oldTotalCredits,       // FIXME ??? Should recalculate ???
-          _oldAccumulatingAmount, // FIXME ??? Should recalculate ???
-          _newAccumulatingAmount, // FIXME ??? Should recalculate ???
+          workingResourceInstanceChargingState,
+          _oldTotalCredits, // FIXME ??? Should recalculate ???
           timeDeltaMillis,
-          previousValue,
-          currentValue,
-          unitPrice,
-          currentDetails
+          unitPrice
         )
 
         val newChargeslot = chargeslot.copyWithCreditsToSubtract(creditsToSubtract, explanation)
@@ -214,7 +171,7 @@ abstract class ChargingBehaviorSkeleton(
     }
 
     if(fullChargeslots.length == 0) {
-      throw new AquariumInternalError("No chargeslots computed for resource event %s".format(currentResourceEvent.id))
+      throw new AquariumInternalError("No chargeslots computed for resource event %s".format(resourceEvent.id))
     }
 
     val sumOfCreditsToSubtract = fullChargeslots.map(_.creditsToSubtract).sum
@@ -230,9 +187,9 @@ abstract class ChargingBehaviorSkeleton(
       billingMonthInfo.year,
       billingMonthInfo.month,
       fullChargeslots,
-      previousResourceEventOpt.map(List(_, currentResourceEvent)).getOrElse(List(currentResourceEvent)),
+      resourceEvent :: workingResourceInstanceChargingState.previousEvents,
       resourceType,
-      currentResourceEvent.isSynthetic
+      resourceEvent.isSynthetic
     )
 
     logger.debug("newWalletEntry = {}", newWalletEntry.toJsonString)
@@ -242,50 +199,22 @@ abstract class ChargingBehaviorSkeleton(
     (1, newTotalCredits)
   }
 
-  protected def removeChargingData[T: Manifest](
-      chargingData: mutable.Map[String, Any],
-      envKey: TypedKey[T]
-  ) = {
-
-    chargingData.remove(envKey.name).asInstanceOf[Option[T]]
-  }
-
-  protected def getChargingData[T: Manifest](
-      chargingData: mutable.Map[String, Any],
-      envKey: TypedKey[T]
-  ) = {
-
-    chargingData.get(envKey.name).asInstanceOf[Option[T]]
-  }
-
-  protected def setChargingData[T: Manifest](
-      chargingData: mutable.Map[String, Any],
-      envKey: TypedKey[T],
-      value: T
-  ) = {
-
-    chargingData(envKey.name) = value
-  }
 
   def selectEffectivePriceTable(
       fullPriceTable: FullPriceTable,
-      chargingData: mutable.Map[String, Any],
+      workingChargingBehaviorDetails: mutable.Map[String, Any],
+      workingResourceInstanceChargingState: WorkingResourceInstanceChargingState,
       currentResourceEvent: ResourceEventModel,
       referenceTimeslot: Timeslot,
-      previousValue: Double,
-      totalCredits: Double,
-      oldAccumulatingAmount: Double,
-      newAccumulatingAmount: Double
+      totalCredits: Double
   ): EffectivePriceTable = {
 
     val selectorPath = computeSelectorPath(
-      chargingData,
+      workingChargingBehaviorDetails,
+      workingResourceInstanceChargingState,
       currentResourceEvent,
       referenceTimeslot,
-      previousValue,
-      totalCredits,
-      oldAccumulatingAmount,
-      newAccumulatingAmount
+      totalCredits
     )
 
     fullPriceTable.effectivePriceTableOfSelectorForResource(selectorPath, currentResourceEvent.safeResource)
@@ -295,30 +224,21 @@ abstract class ChargingBehaviorSkeleton(
    * A generic implementation for charging a resource event.
    * TODO: Ditch this in favor of completely ahdoc behaviors.
    *
-   * @param aquarium
-   * @param currentResourceEvent
-   * @param resourceType
-   * @param billingMonthInfo
-   * @param previousResourceEventOpt
-   * @param userAgreements
-   * @param chargingData
-   * @param totalCredits
-   * @param walletEntryRecorder
    * @return The number of wallet entries recorded and the new total credits
    */
-  def chargeResourceEvent(
+  def processResourceEvent(
       aquarium: Aquarium,
       currentResourceEvent: ResourceEventModel,
       resourceType: ResourceType,
       billingMonthInfo: BillingMonthInfo,
-      previousResourceEventOpt: Option[ResourceEventModel],
-      userAgreements: AgreementHistory,
-      chargingData: mutable.Map[String, Any],
+      workingResourcesChargingState: WorkingResourcesChargingState,
+      userAgreements: AgreementHistoryModel,
       totalCredits: Double,
       walletEntryRecorder: WalletEntry ⇒ Unit
   ): (Int, Double) = {
+    (0,0)
 
-    val currentResourceEventDebugInfo = rcDebugInfo(currentResourceEvent)
+    /*val currentResourceEventDebugInfo = rcDebugInfo(currentResourceEvent)
 
     val isBillable = this.isBillableEvent(currentResourceEvent)
     val retval = if(!isBillable) {
@@ -393,7 +313,7 @@ abstract class ChargingBehaviorSkeleton(
           Timeslot(currentResourceEvent.occurredMillis, currentResourceEvent.occurredMillis + 1),
           resourceType,
           userAgreements.agreementByTimeslot,
-          this.getResourceInstanceUndefinedAmount,
+          0.0,
           totalCredits,
           aquarium.policyStore,
           walletEntryRecorder
@@ -401,112 +321,21 @@ abstract class ChargingBehaviorSkeleton(
       }
     }
 
-    retval
-  }
-
-  /**
-   * Generate a map where the key is a [[gr.grnet.aquarium.charging.ChargingInput]]
-   * and the value the respective value. This map will be used to do the actual credit charge calculation
-   * by the respective algorithm.
-   *
-   * Values are obtained from a corresponding context, which is provided by the parameters. We assume that this context
-   * has been validated before the call to `makeValueMap` is made.
-   *
-   * @param totalCredits   the value for [[gr.grnet.aquarium.charging.TotalCreditsInput.]]
-   * @param oldTotalAmount the value for [[gr.grnet.aquarium.charging.OldTotalAmountInput]]
-   * @param newTotalAmount the value for [[gr.grnet.aquarium.charging.NewTotalAmountInput]]
-   * @param timeDelta      the value for [[gr.grnet.aquarium.charging.TimeDeltaInput]]
-   * @param previousValue  the value for [[gr.grnet.aquarium.charging.PreviousValueInput]]
-   * @param currentValue   the value for [[gr.grnet.aquarium.charging.CurrentValueInput]]
-   * @param unitPrice      the value for [[gr.grnet.aquarium.charging.UnitPriceInput]]
-   *
-   * @return a map from [[gr.grnet.aquarium.charging.ChargingInput]]s to respective values.
-   */
-  def makeValueMap(
-      totalCredits: Double,
-      oldTotalAmount: Double,
-      newTotalAmount: Double,
-      timeDelta: Double,
-      previousValue: Double,
-      currentValue: Double,
-      unitPrice: Double
-  ): Map[ChargingInput, Any] = {
-
-    ChargingBehavior.makeValueMapFor(
-      this,
-      totalCredits,
-      oldTotalAmount,
-      newTotalAmount,
-      timeDelta,
-      previousValue,
-      currentValue,
-      unitPrice)
+    retval*/
   }
 
-  def needsPreviousEventForCreditAndAmountCalculation: Boolean = {
-    // If we need any variable that is related to the previous event
-    // then we do need a previous event
-    inputs.exists(_.isDirectlyRelatedToPreviousEvent)
-  }
 
   /**
-   * Given the old amount of a resource instance, the value arriving in a new resource event and the new details,
-   * compute the new instance amount.
-   *
-   * Note that the `oldAmount` does not make sense for all types of [[gr.grnet.aquarium.charging.ChargingBehavior]],
-   * in which case it is ignored.
-   *
-   * @param oldAccumulatingAmount     the old accumulating amount
-   * @param newEventValue the value contained in a newly arrived
-   *                      [[gr.grnet.aquarium.event.model.resource.ResourceEventModel]]
-   * @param newDetails       the `details` of the newly arrived
-   *                      [[gr.grnet.aquarium.event.model.resource.ResourceEventModel]]
-   * @return
+   * Given the charging state of a resource instance and the details of the incoming message, compute the new
+   * accumulating amount.
    */
   def computeNewAccumulatingAmount(
-      oldAccumulatingAmount: Double,
-      newEventValue: Double,
-      newDetails: Map[String, String]
+      workingResourceInstanceChargingState: WorkingResourceInstanceChargingState,
+      eventDetails: Map[String, String]
   ): Double
 
-  /**
-   * The initial amount.
-   */
-  def getResourceInstanceInitialAmount: Double
-
-  /**
-   * The amount used when no amount is meant to be relevant.
-   *
-   * For example, when there is no need for a previous event but an API requires the amount of the previous event.
-   *
-   * Normally, this value will never be used by client code (= charge computation code).
-   */
-  def getResourceInstanceUndefinedAmount: Double = Double.NaN
-
-  /**
-   * An event carries enough info to characterize it as billable or not.
-   *
-   * Typically all events are billable by default and indeed this is the default implementation
-   * provided here.
-   *
-   * The only exception to the rule is ON events for [[gr.grnet.aquarium.charging.VMChargingBehavior]].
-   */
-  def isBillableEvent(event: ResourceEventModel): Boolean = true
-
-  /**
-   * This is called when we have the very first event for a particular resource instance, and we want to know
-   * if it is billable or not.
-   */
-  def isBillableFirstEvent(event: ResourceEventModel): Boolean
-
-  def mustGenerateDummyFirstEvent: Boolean
-
-  def dummyFirstEventValue: Double = 0.0 // FIXME read from configuration
 
   def constructDummyFirstEventFor(actualFirst: ResourceEventModel, newOccurredMillis: Long): ResourceEventModel = {
-    if(!mustGenerateDummyFirstEvent) {
-      throw new AquariumInternalError("constructDummyFirstEventFor() Not compliant with %s", this)
-    }
 
     val newDetails = Map(
       ResourceEventModel.Names.details_aquarium_is_synthetic   -> "true",
@@ -515,22 +344,6 @@ abstract class ChargingBehaviorSkeleton(
       ResourceEventModel.Names.details_aquarium_reference_event_id_in_store -> actualFirst.stringIDInStoreOrEmpty
     )
 
-    actualFirst.withDetailsAndValue(newDetails, dummyFirstEventValue, newOccurredMillis)
+    actualFirst.withDetailsAndValue(newDetails, 0.0, newOccurredMillis)
   }
-
-  /**
-   * There are resources (cost policies) for which implicit events must be generated at the end of the billing period
-   * and also at the beginning of the next one. For these cases, this method must return `true`.
-   *
-   * The motivating example comes from the [[gr.grnet.aquarium.charging.VMChargingBehavior]] for which we
-   * must implicitly assume `OFF` events at the end of the billing period and `ON` events at the beginning of the next
-   * one.
-   *
-   */
-  def supportsImplicitEvents: Boolean
-
-  def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel): Boolean
-
-  @throws(classOf[Exception])
-  def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, newOccurredMillis: Long): ResourceEventModel
 }