Preparing the move to master
[aquarium] / src / main / scala / gr / grnet / aquarium / user / UserStateComputations.scala
index fdace1a..e344c05 100644 (file)
@@ -37,19 +37,22 @@ package gr.grnet.aquarium.user
 
 
 import com.ckkloverdos.maybe.{Failed, NoVal, Just, Maybe}
-import gr.grnet.aquarium.util.date.MutableDateCalc
-import gr.grnet.aquarium.logic.accounting.dsl.{DSLResourcesMap, DSLPolicy}
-import gr.grnet.aquarium.logic.events.ResourceEvent
 import gr.grnet.aquarium.store.{PolicyStore, UserStateStore, ResourceEventStore}
 import gr.grnet.aquarium.util.{ContextualLogger, Loggable}
 import gr.grnet.aquarium.logic.accounting.Accounting
+import gr.grnet.aquarium.logic.accounting.algorithm.SimpleCostPolicyAlgorithmCompiler
+import gr.grnet.aquarium.logic.events.{NewWalletEntry, ResourceEvent}
+import gr.grnet.aquarium.util.date.{TimeHelpers, MutableDateCalc}
+import gr.grnet.aquarium.logic.accounting.dsl.{DSLAgreement, DSLCostPolicy, DSLResourcesMap, DSLPolicy}
 
 /**
  *
  * @author Christos KK Loverdos <loverdos@gmail.com>
  */
 class UserStateComputations extends Loggable {
-  def createFirstUserState(userId: String, agreementName: String = "default") = {
+  def createFirstUserState(userId: String,
+                           millis: Long = TimeHelpers.nowMillis,
+                           agreementName: String = DSLAgreement.DefaultAgreementName) = {
     val now = 0L
     UserState(
       userId,
@@ -57,9 +60,9 @@ class UserStateComputations extends Loggable {
       0L,
       false,
       null,
-      ImplicitlyIssuedResourceEventsSnapshot(Map(), now),
+      ImplicitlyIssuedResourceEventsSnapshot(List(), now),
       Nil, Nil,
-      LatestResourceEventsSnapshot(Map(), now),
+      LatestResourceEventsSnapshot(List(), now),
       0L, 0L,
       ActiveStateSnapshot(false, now),
       CreditSnapshot(0, now),
@@ -77,9 +80,9 @@ class UserStateComputations extends Loggable {
         0L,
         false,
         null,
-        ImplicitlyIssuedResourceEventsSnapshot(Map(), now),
+        ImplicitlyIssuedResourceEventsSnapshot(List(), now),
         Nil, Nil,
-        LatestResourceEventsSnapshot(Map(), now),
+        LatestResourceEventsSnapshot(List(), now),
         0L, 0L,
         ActiveStateSnapshot(false, now),
         CreditSnapshot(0, now),
@@ -131,7 +134,8 @@ class UserStateComputations extends Loggable {
 
     if(billingMonthStopMillis < userCreationMillis) {
       // If the user did not exist for this billing month, piece of cake
-      clog.debug("User did not exist before %s. Returning %s", userCreationDateCalc, zeroUserState)
+      clog.debug("User did not exist before %s", userCreationDateCalc)
+      clog.debug("Returning ZERO state %s".format(zeroUserState))
       clog.endWith(Just(zeroUserState))
     } else {
       // Ask DB cache for the latest known user state for this billing period
@@ -206,6 +210,10 @@ class UserStateComputations extends Loggable {
                            accounting: Accounting,
                            contextualLogger: Maybe[ContextualLogger] = NoVal): Maybe[UserState] = Maybe {
 
+    def rcDebugInfo(rcEvent: ResourceEvent) = {
+      rcEvent.toDebugString(defaultResourcesMap, false)
+    }
+
     val clog = ContextualLogger.fromOther(
       contextualLogger,
       logger,
@@ -249,13 +257,22 @@ class UserStateComputations extends Loggable {
         // specified in the parameters.
         var _workingUserState = startingUserState
 
-        // Prepare the implicit OFF resource events
-        val theImplicitOFFs = _workingUserState.implicitlyTerminatedSnapshot.toMutableWorker
-        clog.debug("theImplicitOFFs = %s", theImplicitOFFs)
+        // Prepare the implicitly terminated resource events from previous billing period
+        val implicitlyTerminatedResourceEvents = _workingUserState.implicitlyTerminatedSnapshot.toMutableWorker
+        if(implicitlyTerminatedResourceEvents.size > 0) {
+          clog.debug("%s implicitlyTerminatedResourceEvents", implicitlyTerminatedResourceEvents.size)
+          clog.withIndent {
+            implicitlyTerminatedResourceEvents.foreach(ev ⇒ clog.debug("%s", rcDebugInfo(ev)))
+          }
+        }
+
+        // Keep the resource events from this period that were first (and unused) of their kind
+        val ignoredFirstResourceEvents = IgnoredFirstResourceEventsWorker.Empty
 
         /**
-         * Finds the previous resource event by checking two possible sources: a) The implicit OFF resource events and
-         * b) the explicit previous resource events. If the event is found, it is removed from the respective source.
+         * Finds the previous resource event by checking two possible sources: a) The implicitly terminated resource
+         * events and b) the explicit previous resource events. If the event is found, it is removed from the
+         * respective source.
          *
          * If the event is not found, then this must be for a new resource instance.
          * (and probably then some `zero` resource event must be implied as the previous one)
@@ -265,12 +282,12 @@ class UserStateComputations extends Loggable {
          * @return
          */
         def findAndRemovePreviousResourceEvent(resource: String, instanceId: String): Maybe[ResourceEvent] = {
-          // implicit OFFs are checked first
-          theImplicitOFFs.findAndRemoveResourceEvent(resource, instanceId) match {
+          // implicitly terminated events are checked first
+          implicitlyTerminatedResourceEvents.findAndRemoveResourceEvent(resource, instanceId) match {
             case just @ Just(_) ⇒
               just
             case NoVal ⇒
-              // explicit previous are checked second
+              // explicit previous resource events are checked second
               previousResourceEvents.findAndRemoveResourceEvent(resource, instanceId) match {
                 case just @ Just(_) ⇒
                   just
@@ -282,10 +299,6 @@ class UserStateComputations extends Loggable {
           }
         }
 
-        def rcDebugInfo(rcEvent: ResourceEvent) = {
-          rcEvent.toDebugString(defaultResourcesMap, false)
-        }
-
         // Find the actual resource events from DB
         val allResourceEventsForMonth = resourceEventStore.findAllRelevantResourceEventsForBillingPeriod(
           userId,
@@ -294,43 +307,55 @@ class UserStateComputations extends Loggable {
         var _eventCounter = 0
 
         clog.debug("resourceEventStore = %s".format(resourceEventStore))
-        clog.debug("Found %s resource events, starting processing...", allResourceEventsForMonth.size)
-        
+        if(allResourceEventsForMonth.size > 0) {
+          clog.debug("Found %s resource events, starting processing...", allResourceEventsForMonth.size)
+        } else {
+          clog.debug("Not found any resource events")
+        }
+
         for {
           currentResourceEvent <- allResourceEventsForMonth
         } {
           _eventCounter = _eventCounter + 1
-          val theResource = currentResourceEvent.resource
-          val theInstanceId = currentResourceEvent.instanceId
+          val theResource = currentResourceEvent.safeResource
+          val theInstanceId = currentResourceEvent.safeInstanceId
           val theValue = currentResourceEvent.value
 
           clog.indent()
+          clog.debug("")
           clog.debug("Processing %s", currentResourceEvent)
-          clog.debug("Friendlier %s", rcDebugInfo(currentResourceEvent))
+          clog.debug("+========= %s", rcDebugInfo(currentResourceEvent))
+
           clog.indent()
 
           if(previousResourceEvents.size > 0) {
             clog.debug("%s previousResourceEvents", previousResourceEvents.size)
-            clog.indent()
-            previousResourceEvents.foreach(ev ⇒ clog.debug("%s", rcDebugInfo(ev)))
-            clog.unindent()
+            clog.withIndent {
+              previousResourceEvents.foreach(ev ⇒ clog.debug("%s", rcDebugInfo(ev)))
+            }
           }
-          if(theImplicitOFFs.size > 0) {
-            clog.debug("%s theImplicitOFFs", theImplicitOFFs.size)
-            clog.indent()
-            theImplicitOFFs.foreach(ev ⇒ clog.debug("%s", rcDebugInfo(ev)))
-            clog.unindent()
+          if(implicitlyTerminatedResourceEvents.size > 0) {
+            clog.debug("%s implicitlyTerminatedResourceEvents", implicitlyTerminatedResourceEvents.size)
+            clog.withIndent {
+              implicitlyTerminatedResourceEvents.foreach(ev ⇒ clog.debug("%s", rcDebugInfo(ev)))
+            }
+          }
+          if(ignoredFirstResourceEvents.size > 0) {
+            clog.debug("%s ignoredFirstResourceEvents", ignoredFirstResourceEvents.size)
+            clog.withIndent {
+              ignoredFirstResourceEvents.foreach(ev ⇒ clog.debug("%s", rcDebugInfo(ev)))
+            }
           }
 
           // Ignore the event if it is not billable (but still record it in the "previous" stuff).
-          // But to make this decision, first we need the resource definiton (and its cost policy).
-          val resourceDefM = defaultResourcesMap.findResourceM(currentResourceEvent.safeResource)
+          // But to make this decision, first we need the resource definition (and its cost policy).
+          val resourceDefM = defaultResourcesMap.findResourceM(theResource)
           resourceDefM match {
             // We have a resource (and thus a cost policy)
             case Just(resourceDef) ⇒
               val costPolicy = resourceDef.costPolicy
               clog.debug("Cost policy: %s", costPolicy)
-              val isBillable = costPolicy.isBillableEventBasedOnValue(currentResourceEvent.value)
+              val isBillable = costPolicy.isBillableEventBasedOnValue(theValue)
               isBillable match {
                 // The resource event is not billable
                 case false ⇒
@@ -342,30 +367,85 @@ class UserStateComputations extends Loggable {
                   // This is (potentially) needed to calculate new credit amount and new resource instance amount
                   val previousResourceEventM = findAndRemovePreviousResourceEvent(theResource, theInstanceId)
                   clog.debug("PreviousM %s", previousResourceEventM.map(rcDebugInfo(_)))
-                  val defaultInitialAmount = costPolicy.getResourceInstanceInitialAmount
-                  val oldAmount = _workingUserState.getResourceInstanceAmount(theResource, theInstanceId, defaultInitialAmount)
-                  val oldCredits = _workingUserState.creditsSnapshot.creditAmount
-
-                  // A. Compute new resource instance accumulating amount
-                  val newAmount = costPolicy.computeNewAccumulatingAmount(oldAmount, theValue)
-
-                  // B. Compute new wallet entries
-                  val alltimeAgreements = _workingUserState.agreementsSnapshot.agreements
-//                  val chargeChunksM = accounting.computeChargeChunks(
-//                    previousResourceEventM,
-//                    currentResourceEvent,
-//                    oldCredits,
-//                    oldAmount,
-//                    newAmount,
-//                    resourceDef,
-//                    defaultResourcesMap,
-//                    alltimeAgreements)
-
-                  // C. Compute new credit amount (based on the wallet entries)
-                  // Maybe this can be consolidated inthe previous step (B)
-
-
-                  ()
+
+                  val havePreviousResourceEvent = previousResourceEventM.isJust
+                  val needPreviousResourceEvent = costPolicy.needsPreviousEventForCreditAndAmountCalculation
+                  if(needPreviousResourceEvent && !havePreviousResourceEvent) {
+                    // This must be the first resource event of its kind, ever.
+                    // TODO: We should normally check the DB to verify the claim (?)
+                    clog.info("Ignoring first event of its kind %s", rcDebugInfo(currentResourceEvent))
+                    ignoredFirstResourceEvents.updateResourceEvent(currentResourceEvent)
+                  } else {
+                    val defaultInitialAmount = costPolicy.getResourceInstanceInitialAmount
+                    val oldAmount = _workingUserState.getResourceInstanceAmount(theResource, theInstanceId, defaultInitialAmount)
+                    val oldCredits = _workingUserState.creditsSnapshot.creditAmount
+
+                    // A. Compute new resource instance accumulating amount
+                    val newAmount = costPolicy.computeNewAccumulatingAmount(oldAmount, theValue)
+                    
+                    clog.debug("theValue = %s, oldAmount = %s, newAmount = %s, oldCredits = %s", theValue, oldAmount, newAmount, oldCredits)
+
+                    // B. Compute new wallet entries
+                    val alltimeAgreements = _workingUserState.agreementsSnapshot.agreementsByTimeslot
+
+                    val fullChargeslotsM = accounting.computeFullChargeslots(
+                      previousResourceEventM,
+                      currentResourceEvent,
+                      oldCredits,
+                      oldAmount,
+                      newAmount,
+                      resourceDef,
+                      defaultResourcesMap,
+                      alltimeAgreements,
+                      SimpleCostPolicyAlgorithmCompiler,
+                      policyStore,
+                      Just(clog)
+                    )
+
+                    // We have the chargeslots, let's associate them with the current event
+                    fullChargeslotsM match {
+                      case Just(fullChargeslots) ⇒
+                        if(fullChargeslots.length == 0) {
+                          // At least one chargeslot is required.
+                          throw new Exception("No chargeslots computed")
+                        }
+                        clog.debug("chargeslots:")
+                        clog.withIndent {
+                          for(fullChargeslot <- fullChargeslots) {
+                            clog.debug("%s", fullChargeslot)
+                          }
+                        }
+                        
+                        // C. Compute new credit amount (based on the charge slots)
+                        val newCreditsDiff = fullChargeslots.map(_.computedCredits.get).sum
+                        val newCredits = oldCredits + newCreditsDiff
+                        clog.debug("newCreditsDiff = %s, newCredits = %s", newCreditsDiff, newCredits)
+
+                        val newWalletEntry = NewWalletEntry(
+                          userId,
+                          newCreditsDiff,
+                          oldCredits,
+                          newCredits,
+                          TimeHelpers.nowMillis,
+                          billingMonthInfo.year,
+                          billingMonthInfo.month,
+                          currentResourceEvent,
+                          previousResourceEventM.toOption,
+                          fullChargeslots,
+                          resourceDef
+                        )
+
+                        clog.debug("New %s", newWalletEntry)
+
+                      case NoVal ⇒
+                        // At least one chargeslot is required.
+                        throw new Exception("No chargeslots computed")
+
+                      case failed @ Failed(e, m) ⇒
+                        throw new Exception(m, e)
+                    }
+                  }
+
               }
 
               // After processing, all event, billable or not update the previous state
@@ -383,6 +463,7 @@ class UserStateComputations extends Loggable {
           }
 
           clog.unindent()
+          clog.debug("-========= %s", rcDebugInfo(currentResourceEvent))
           clog.unindent()
         }