Applying the new policy semantics everywhere
[aquarium] / src / main / scala / gr / grnet / aquarium / charging / ChargingBehavior.scala
index 4c252e1..605aa72 100644 (file)
 
 package gr.grnet.aquarium.charging
 
+import scala.collection.immutable
+import scala.collection.mutable
+
 import gr.grnet.aquarium.event.model.resource.ResourceEventModel
-import gr.grnet.aquarium.{AquariumInternalError, AquariumException}
+import gr.grnet.aquarium.{Aquarium, AquariumInternalError, AquariumException}
+import gr.grnet.aquarium.policy.{UserAgreementModel, ResourceType}
+import com.ckkloverdos.key.{TypedKey, TypedKeySkeleton}
+import gr.grnet.aquarium.util._
+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.computation.state.parts.AgreementHistory
+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
@@ -45,11 +58,328 @@ import gr.grnet.aquarium.{AquariumInternalError, AquariumException}
  * @author Christos KK Loverdos <loverdos@gmail.com>
  */
 
-abstract class ChargingBehavior(val alias: String, val inputs: Set[ChargingInput]) {
+abstract class ChargingBehavior(val alias: String, val inputs: Set[ChargingInput]) extends Loggable {
 
   final lazy val inputNames = inputs.map(_.name)
 
   /**
+   *
+   * @param aquarium
+   * @param resourceEvent
+   * @param resourceType
+   * @param billingMonthInfo
+   * @param userAgreements
+   * @param chargingData
+   * @param totalCredits
+   * @param walletEntryRecorder
+   * @param clogOpt
+   * @return The number of wallet entries recorded and the new total credits
+   */
+  def chargeResourceEvent(
+      aquarium: Aquarium,
+      resourceEvent: ResourceEventModel,
+      resourceType: ResourceType,
+      billingMonthInfo: BillingMonthInfo,
+      userAgreements: AgreementHistory,
+      chargingData: mutable.Map[String, Any],
+      totalCredits: Double,
+      walletEntryRecorder: WalletEntry ⇒ Unit,
+      clogOpt: Option[ContextualLogger] = None
+  ): (Int, Double) = {
+
+    genericChargeResourceEvent(
+      aquarium,
+      resourceEvent,
+      resourceType,
+      billingMonthInfo,
+      userAgreements,
+      chargingData,
+      totalCredits,
+      walletEntryRecorder,
+      clogOpt
+    )
+  }
+
+  @inline private[this] 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 = {
+    alias match {
+     case ChargingBehaviorAliases.continuous ⇒
+       hrs(timeDeltaMillis) * oldAccumulatingAmount * unitPrice
+
+     case ChargingBehaviorAliases.discrete ⇒
+       currentValue * unitPrice
+
+     case ChargingBehaviorAliases.onoff ⇒
+       hrs(timeDeltaMillis) * unitPrice
+
+     case ChargingBehaviorAliases.once ⇒
+       currentValue
+
+     case name ⇒
+       throw new AquariumInternalError("Cannot compute credit diff for charging behavior %s".format(name))
+    }
+  }
+
+  protected def rcDebugInfo(rcEvent: ResourceEventModel) = {
+    rcEvent.toDebugString
+  }
+
+  /**
+   *
+   * @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,
+      billingMonthInfo: BillingMonthInfo,
+      referenceTimeslot: Timeslot,
+      resourceType: ResourceType,
+      agreementByTimeslot: immutable.SortedMap[Timeslot, UserAgreementModel],
+      previousValue: Double,
+      totalCredits: Double,
+      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)
+
+    var _oldTotalCredits = totalCredits
+
+    var _newAccumulatingAmount = this.computeNewAccumulatingAmount(_oldAccumulatingAmount, currentValue, currentDetails)
+    setChargingData(chargingData, EnvKeys.ResourceInstanceAccumulatingAmount, _newAccumulatingAmount)
+
+    val policyByTimeslot = policyStore.loadAndSortPoliciesWithin(
+      referenceTimeslot.from.getTime,
+      referenceTimeslot.to.getTime
+    )
+
+    val initialChargeslots = TimeslotComputations.computeInitialChargeslots(
+      referenceTimeslot,
+      resourceType,
+      policyByTimeslot,
+      agreementByTimeslot
+    )
+
+    val fullChargeslots = initialChargeslots.map {
+      case chargeslot@Chargeslot(startMillis, stopMillis, unitPrice, _) ⇒
+        val timeDeltaMillis = stopMillis - startMillis
+
+        val creditsToSubtract = this.computeCreditsToSubtract(
+          _oldTotalCredits,       // FIXME ??? Should recalculate ???
+          _oldAccumulatingAmount, // FIXME ??? Should recalculate ???
+          _newAccumulatingAmount, // FIXME ??? Should recalculate ???
+          timeDeltaMillis,
+          previousValue,
+          currentValue,
+          unitPrice,
+          currentDetails
+        )
+
+        val newChargeslot = chargeslot.copyWithCreditsToSubtract(creditsToSubtract)
+        newChargeslot
+    }
+
+    if(fullChargeslots.length == 0) {
+      throw new AquariumInternalError("No chargeslots computed for resource event %s".format(currentResourceEvent.id))
+    }
+
+    val sumOfCreditsToSubtract = fullChargeslots.map(_.creditsToSubtract).sum
+    val newTotalCredits = _oldTotalCredits - sumOfCreditsToSubtract
+
+    val newWalletEntry = WalletEntry(
+      userID,
+      sumOfCreditsToSubtract,
+      _oldTotalCredits,
+      newTotalCredits,
+      TimeHelpers.nowMillis(),
+      referenceTimeslot,
+      billingMonthInfo.year,
+      billingMonthInfo.month,
+      previousResourceEventOpt.map(List(_, currentResourceEvent)).getOrElse(List(currentResourceEvent)),
+      fullChargeslots,
+      resourceType,
+      currentResourceEvent.isSynthetic
+    )
+
+    walletEntryRecorder.apply(newWalletEntry)
+
+    (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
+  }
+
+  /**
+   *
+   * @param aquarium
+   * @param currentResourceEvent
+   * @param resourceType
+   * @param billingMonthInfo
+   * @param userAgreements
+   * @param chargingData
+   * @param totalCredits
+   * @param walletEntryRecorder
+   * @param clogOpt
+   * @return The number of wallet entries recorded and the new total credits
+   */
+  protected def genericChargeResourceEvent(
+      aquarium: Aquarium,
+      currentResourceEvent: ResourceEventModel,
+      resourceType: ResourceType,
+      billingMonthInfo: BillingMonthInfo,
+      userAgreements: AgreementHistory,
+      chargingData: mutable.Map[String, Any],
+      totalCredits: Double,
+      walletEntryRecorder: WalletEntry ⇒ Unit,
+      clogOpt: Option[ContextualLogger] = None
+  ): (Int, Double) = {
+    import ChargingBehavior.EnvKeys
+
+    val clog = ContextualLogger.fromOther(clogOpt, logger, "chargeResourceEvent(%s)", currentResourceEvent.id)
+    val currentResourceEventDebugInfo = rcDebugInfo(currentResourceEvent)
+
+    val isBillable = this.isBillableEvent(currentResourceEvent)
+    val retval = if(!isBillable) {
+      // The resource event is not billable.
+      clog.debug("Ignoring not billable %s", currentResourceEventDebugInfo)
+      (0, totalCredits)
+    } else {
+      // The resource event is billable.
+      // Find the previous event if needed.
+      // This is (potentially) needed to calculate new credit amount and new resource instance amount
+      if(this.needsPreviousEventForCreditAndAmountCalculation) {
+        val previousResourceEventOpt = removeChargingData(chargingData, EnvKeys.PreviousEvent)
+
+        if(previousResourceEventOpt.isDefined) {
+          val previousResourceEvent = previousResourceEventOpt.get
+          val previousValue = previousResourceEvent.value
+
+          computeChargeslots(
+            chargingData,
+            previousResourceEventOpt,
+            currentResourceEvent,
+            billingMonthInfo,
+            Timeslot(previousResourceEvent.occurredMillis, currentResourceEvent.occurredMillis),
+            resourceType,
+            userAgreements.agreementByTimeslot,
+            previousValue,
+            totalCredits,
+            aquarium.policyStore,
+            walletEntryRecorder
+          )
+        } else {
+          // We do not have the needed previous event, so this must be the first resource event of its kind, ever.
+          // Let's see if we can create a dummy previous event.
+          val actualFirstEvent = currentResourceEvent
+
+          if(this.isBillableFirstEvent(actualFirstEvent) && this.mustGenerateDummyFirstEvent) {
+            clog.debug("First event of its kind %s", currentResourceEventDebugInfo)
+
+            val dummyFirst = this.constructDummyFirstEventFor(currentResourceEvent, billingMonthInfo.monthStartMillis)
+            clog.debug("Dummy first event %s", rcDebugInfo(dummyFirst))
+
+            val previousResourceEvent = dummyFirst
+            val previousValue = previousResourceEvent.value
+
+            computeChargeslots(
+              chargingData,
+              Some(previousResourceEvent),
+              currentResourceEvent,
+              billingMonthInfo,
+              Timeslot(previousResourceEvent.occurredMillis, currentResourceEvent.occurredMillis),
+              resourceType,
+              userAgreements.agreementByTimeslot,
+              previousValue,
+              totalCredits,
+              aquarium.policyStore,
+              walletEntryRecorder
+            )
+          } else {
+            clog.debug("Ignoring first event of its kind %s", currentResourceEventDebugInfo)
+            // userStateWorker.updateIgnored(currentResourceEvent)
+            (0, totalCredits)
+          }
+        }
+      } else {
+        // No need for previous event. One event does it all.
+        computeChargeslots(
+          chargingData,
+          None,
+          currentResourceEvent,
+          billingMonthInfo,
+          Timeslot(currentResourceEvent.occurredMillis, currentResourceEvent.occurredMillis + 1),
+          resourceType,
+          userAgreements.agreementByTimeslot,
+          this.getResourceInstanceUndefinedAmount,
+          totalCredits,
+          aquarium.policyStore,
+          walletEntryRecorder
+        )
+      }
+    }
+
+    // After processing, all events billable or not update the previous state
+    setChargingData(chargingData, EnvKeys.PreviousEvent, currentResourceEvent)
+
+    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.
@@ -88,8 +418,6 @@ abstract class ChargingBehavior(val alias: String, val inputs: Set[ChargingInput
       unitPrice)
   }
 
-  def isNamed(aName: String): Boolean = aName == alias
-
   def needsPreviousEventForCreditAndAmountCalculation: Boolean = {
     // If we need any variable that is related to the previous event
     // then we do need a previous event
@@ -98,20 +426,24 @@ abstract class ChargingBehavior(val alias: String, val inputs: Set[ChargingInput
 
   /**
    * Given the old amount of a resource instance
-   * (see [[gr.grnet.aquarium.computation.state.parts.ResourceInstanceSnapshot]]), the
+   * (see [[gr.grnet.aquarium.computation.state.parts.ResourceInstanceAmount]]), 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 oldAmount     the old accumulating amount
+   * @param oldAccumulatingAmount     the old accumulating amount
    * @param newEventValue the value contained in a newly arrived
    *                      [[gr.grnet.aquarium.event.model.resource.ResourceEventModel]]
-   * @param details       the `details` of the newly arrived
+   * @param newDetails       the `details` of the newly arrived
    *                      [[gr.grnet.aquarium.event.model.resource.ResourceEventModel]]
    * @return
    */
-  def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double, details: Map[String, String]): Double
+  def computeNewAccumulatingAmount(
+      oldAccumulatingAmount: Double,
+      newEventValue: Double,
+      newDetails: Map[String, String]
+  ): Double
 
   /**
    * The initial amount.
@@ -125,7 +457,7 @@ abstract class ChargingBehavior(val alias: String, val inputs: Set[ChargingInput
    *
    * Normally, this value will never be used by client code (= charge computation code).
    */
-  def getResourceInstanceUndefinedAmount: Double = -1.0
+  def getResourceInstanceUndefinedAmount: Double = Double.NaN
 
   /**
    * An event carries enough info to characterize it as billable or not.
@@ -145,7 +477,7 @@ abstract class ChargingBehavior(val alias: String, val inputs: Set[ChargingInput
 
   def mustGenerateDummyFirstEvent: Boolean
 
-  def dummyFirstEventValue: Double = 0.0
+  def dummyFirstEventValue: Double = 0.0 // FIXME read from configuration
 
   def constructDummyFirstEventFor(actualFirst: ResourceEventModel, newOccurredMillis: Long): ResourceEventModel = {
     if(!mustGenerateDummyFirstEvent) {
@@ -180,6 +512,17 @@ abstract class ChargingBehavior(val alias: String, val inputs: Set[ChargingInput
 }
 
 object ChargingBehavior {
+  final case class ChargingBehaviorKey[T: Manifest](override val name: String) extends TypedKeySkeleton[T](name)
+
+  /**
+   * Keys used to save information between calls of `chargeResourceEvent`
+   */
+  object EnvKeys {
+    final val PreviousEvent = ChargingBehaviorKey[ResourceEventModel]("previous.event")
+
+    final val ResourceInstanceAccumulatingAmount = ChargingBehaviorKey[Double]("resource.instance.accumulating.amount")
+  }
+
   def makeValueMapFor(
       chargingBehavior: ChargingBehavior,
       totalCredits: Double,
@@ -195,13 +538,13 @@ object ChargingBehavior {
     var map = Map[ChargingInput, Any]()
 
     if(inputs contains ChargingBehaviorNameInput) map += ChargingBehaviorNameInput -> chargingBehavior.alias
-    if(inputs contains TotalCreditsInput  ) map += TotalCreditsInput   -> totalCredits
-    if(inputs contains OldTotalAmountInput) map += OldTotalAmountInput -> oldTotalAmount
-    if(inputs contains NewTotalAmountInput) map += NewTotalAmountInput -> newTotalAmount
-    if(inputs contains TimeDeltaInput     ) map += TimeDeltaInput      -> timeDelta
-    if(inputs contains PreviousValueInput ) map += PreviousValueInput  -> previousValue
-    if(inputs contains CurrentValueInput  ) map += CurrentValueInput   -> currentValue
-    if(inputs contains UnitPriceInput     ) map += UnitPriceInput      -> unitPrice
+    if(inputs contains TotalCreditsInput        ) map += TotalCreditsInput   -> totalCredits
+    if(inputs contains OldTotalAmountInput      ) map += OldTotalAmountInput -> oldTotalAmount
+    if(inputs contains NewTotalAmountInput      ) map += NewTotalAmountInput -> newTotalAmount
+    if(inputs contains TimeDeltaInput           ) map += TimeDeltaInput      -> timeDelta
+    if(inputs contains PreviousValueInput       ) map += PreviousValueInput  -> previousValue
+    if(inputs contains CurrentValueInput        ) map += CurrentValueInput   -> currentValue
+    if(inputs contains UnitPriceInput           ) map += UnitPriceInput      -> unitPrice
 
     map
   }