Wallets go REST
authorChristos KK Loverdos <loverdos@gmail.com>
Fri, 20 Jul 2012 09:21:42 +0000 (12:21 +0300)
committerChristos KK Loverdos <loverdos@gmail.com>
Fri, 20 Jul 2012 09:21:42 +0000 (12:21 +0300)
There is a bug related to the timeslot computations, so I had to disable
every piece that computes fine-grained timeslots and their corresponing
stuff (agreement).

20 files changed:
.gitignore
src/main/scala/gr/grnet/aquarium/Aquarium.scala
src/main/scala/gr/grnet/aquarium/actor/ActorRole.scala
src/main/scala/gr/grnet/aquarium/actor/message/GetUserWalletRequest.scala [new file with mode: 0644]
src/main/scala/gr/grnet/aquarium/actor/message/GetUserWalletResponse.scala [new file with mode: 0644]
src/main/scala/gr/grnet/aquarium/actor/service/user/UserActor.scala
src/main/scala/gr/grnet/aquarium/charging/Chargeslot.scala
src/main/scala/gr/grnet/aquarium/charging/ChargingBehavior.scala
src/main/scala/gr/grnet/aquarium/charging/ChargingService.scala
src/main/scala/gr/grnet/aquarium/charging/state/StdUserState.scala
src/main/scala/gr/grnet/aquarium/charging/state/UserStateModel.scala
src/main/scala/gr/grnet/aquarium/charging/state/UserStateModelSkeleton.scala [new file with mode: 0644]
src/main/scala/gr/grnet/aquarium/charging/state/WorkingUserState.scala
src/main/scala/gr/grnet/aquarium/charging/wallet/WalletEntry.scala
src/main/scala/gr/grnet/aquarium/computation/TimeslotComputations.scala
src/main/scala/gr/grnet/aquarium/event/model/im/IMEventModel.scala
src/main/scala/gr/grnet/aquarium/service/FinagleRESTService.scala
src/main/scala/gr/grnet/aquarium/service/RESTPaths.scala
src/main/scala/gr/grnet/aquarium/store/memory/MemStoreProvider.scala
src/main/scala/gr/grnet/aquarium/store/mongodb/MongoDBUserState.scala

index 2afaa36..190d553 100644 (file)
@@ -46,3 +46,4 @@ aquarium.home_IS_UNDEFINED
 .gradle/
 logs/
 History.md
+npm-debug.log
index 54d6e39..e61a7ab 100644 (file)
@@ -131,6 +131,8 @@ final class Aquarium(env: Env) extends Lifecycle with Loggable {
       logger.info("{} = {}", EnvKeys.eventsStoreFolder.name, folder)
     }
     this.eventsStoreFolder.throwMe // on error
+
+    logger.info("default policy = {}", defaultPolicyModel.toJsonString)
   }
 
   private[this] def addShutdownHooks(): Unit = {
index 43e32f6..4f05fef 100644 (file)
@@ -36,7 +36,7 @@ package gr.grnet.aquarium.actor
 
 import service.user.UserActor
 import gr.grnet.aquarium.actor.message.event.{ProcessIMEvent, ProcessResourceEvent}
-import gr.grnet.aquarium.actor.message.{GetUserStateRequest, GetUserBalanceRequest}
+import gr.grnet.aquarium.actor.message.{GetUserWalletRequest, GetUserStateRequest, GetUserBalanceRequest}
 import gr.grnet.aquarium.actor.message.config.{InitializeUserActorState, AquariumPropertiesLoaded, ActorConfigurationMessage}
 
 /**
@@ -88,6 +88,7 @@ case object UserActorRole
                       classOf[UserActor],
                       Set(classOf[ProcessResourceEvent],
                           classOf[ProcessIMEvent],
+                          classOf[GetUserWalletRequest],
                           classOf[GetUserBalanceRequest],
                           classOf[GetUserStateRequest]),
                       Set(classOf[InitializeUserActorState],
diff --git a/src/main/scala/gr/grnet/aquarium/actor/message/GetUserWalletRequest.scala b/src/main/scala/gr/grnet/aquarium/actor/message/GetUserWalletRequest.scala
new file mode 100644 (file)
index 0000000..48f6d9c
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2011-2012 GRNET S.A. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above
+ *      copyright notice, this list of conditions and the following
+ *      disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following
+ *      disclaimer in the documentation and/or other materials
+ *      provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+ * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * The views and conclusions contained in the software and
+ * documentation are those of the authors and should not be
+ * interpreted as representing official policies, either expressed
+ * or implied, of GRNET S.A.
+ */
+
+package gr.grnet.aquarium.actor.message
+
+/**
+ *
+ * @author Christos KK Loverdos <loverdos@gmail.com>
+ */
+
+case class GetUserWalletRequest(userID: String, timestamp: Long) extends ActorMessage with UserActorRequestMessage {
+  def referenceTimeMillis = timestamp
+}
diff --git a/src/main/scala/gr/grnet/aquarium/actor/message/GetUserWalletResponse.scala b/src/main/scala/gr/grnet/aquarium/actor/message/GetUserWalletResponse.scala
new file mode 100644 (file)
index 0000000..d707bf2
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2011-2012 GRNET S.A. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above
+ *      copyright notice, this list of conditions and the following
+ *      disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following
+ *      disclaimer in the documentation and/or other materials
+ *      provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+ * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * The views and conclusions contained in the software and
+ * documentation are those of the authors and should not be
+ * interpreted as representing official policies, either expressed
+ * or implied, of GRNET S.A.
+ */
+
+package gr.grnet.aquarium.actor.message
+
+import gr.grnet.aquarium.AquariumInternalError
+import gr.grnet.aquarium.charging.wallet.WalletEntry
+
+/**
+ *
+ * @author Christos KK Loverdos <loverdos@gmail.com>
+ */
+
+case class GetUserWalletResponse(
+    state: Either[String, GetUserWalletResponseData],
+    override val suggestedHTTPStatus: Int = 200)
+extends UserActorResponseMessage(state, suggestedHTTPStatus) {
+  def userID = state match {
+    case Left(error) ⇒
+      throw new AquariumInternalError("Could not obtain userID. %s".format(error))
+
+    case Right(data) ⇒
+      data.userID
+  }
+}
+
+case class GetUserWalletResponseData(userID: String, credits: Double, walletEntries: List[WalletEntry])
\ No newline at end of file
index 46f5824..6093340 100644 (file)
@@ -43,7 +43,7 @@ import gr.grnet.aquarium.actor.message.event.{ProcessResourceEvent, ProcessIMEve
 import gr.grnet.aquarium.actor.message.config.{InitializeUserActorState, AquariumPropertiesLoaded}
 import gr.grnet.aquarium.util.date.TimeHelpers
 import gr.grnet.aquarium.event.model.im.IMEventModel
-import gr.grnet.aquarium.actor.message.{GetUserStateResponse, GetUserBalanceResponseData, GetUserBalanceResponse, GetUserStateRequest, GetUserBalanceRequest}
+import gr.grnet.aquarium.actor.message.{GetUserWalletResponseData, GetUserWalletResponse, GetUserWalletRequest, GetUserStateResponse, GetUserBalanceResponseData, GetUserBalanceResponse, GetUserStateRequest, GetUserBalanceRequest}
 import gr.grnet.aquarium.util.{LogHelpers, shortClassNameOf}
 import gr.grnet.aquarium.AquariumInternalError
 import gr.grnet.aquarium.computation.BillingMonthInfo
@@ -51,6 +51,7 @@ import gr.grnet.aquarium.charging.state.UserStateBootstrap
 import gr.grnet.aquarium.charging.state.{WorkingAgreementHistory, WorkingUserState, UserStateModel}
 import gr.grnet.aquarium.charging.reason.{InitialUserActorSetup, RealtimeChargingReason}
 import gr.grnet.aquarium.policy.{PolicyDefinedFullPriceTableRef, StdUserAgreement}
+import gr.grnet.aquarium.event.model.resource.ResourceEventModel
 
 /**
  *
@@ -157,6 +158,10 @@ class UserActor extends ReflectiveRoleableActor {
     this._latestIMEventID = imEvent.id
   }
 
+  private[this] def updateLatestResourceEventIDFrom(rcEvent: ResourceEventModel): Unit = {
+    this._latestResourceEventID = rcEvent.id
+  }
+
   /**
    * Creates the initial state that is related to IMEvents.
    */
@@ -306,35 +311,69 @@ class UserActor extends ReflectiveRoleableActor {
     }
 
     val now = TimeHelpers.nowMillis()
-    val billingMonthInfo = BillingMonthInfo.fromMillis(now)
     val currentResourcesMap = aquarium.currentResourceTypesMap
-    val calculationReason = RealtimeChargingReason(None, now)
+    val chargingReason = RealtimeChargingReason(None, now)
+
+    val nowBillingMonthInfo = BillingMonthInfo.fromMillis(now)
+    val nowYear = nowBillingMonthInfo.year
+    val nowMonth = nowBillingMonthInfo.month
+
     val eventOccurredMillis = rcEvent.occurredMillis
+    val eventBillingMonthInfo = BillingMonthInfo.fromMillis(eventOccurredMillis)
+    val eventYear = eventBillingMonthInfo.year
+    val eventMonth = eventBillingMonthInfo.month
 
-//    DEBUG("Using %s", currentResourceTypesMap.toJsonString)
-    if(rcEvent.occurredMillis >= _workingUserState.occurredMillis) {
-      chargingService.processResourceEvent(
-        rcEvent,
-        this._workingUserState,
-        calculationReason,
-        billingMonthInfo,
-        None
-      )
-    }
-    else {
-      // Oops. Event is OUT OF SYNC
-      DEBUG("OUT OF SYNC %s", rcEvent.toDebugString)
+    def computeBatch(): Unit = {
+      DEBUG("Going for out of sync charging")
       this._workingUserState = chargingService.replayMonthChargingUpTo(
-        billingMonthInfo,
+        nowBillingMonthInfo,
         // Take into account that the event may be out-of-sync.
         // TODO: Should we use this._latestResourceEventOccurredMillis instead of now?
         now max eventOccurredMillis,
         this._userStateBootstrap,
         currentResourcesMap,
-        calculationReason,
+        chargingReason,
         stdUserStateStoreFunc,
         None
       )
+
+      updateLatestResourceEventIDFrom(rcEvent)
+    }
+
+    def computeRealtime(): Unit = {
+      DEBUG("Going for in sync charging")
+      chargingService.processResourceEvent(
+        rcEvent,
+        this._workingUserState,
+        chargingReason,
+        nowBillingMonthInfo,
+        None,
+        true
+      )
+
+      updateLatestResourceEventIDFrom(rcEvent)
+    }
+
+    // FIXME check these
+    if(nowYear != eventYear || nowMonth != eventMonth) {
+      DEBUG(
+        "nowYear(%s) != eventYear(%s) || nowMonth(%s) != eventMonth(%s)",
+        nowYear, eventYear,
+        nowMonth, eventMonth
+      )
+      computeBatch()
+    }
+    else if(this._workingUserState.latestResourceEventOccurredMillis < rcEvent.occurredMillis) {
+      DEBUG("this._workingUserState.latestResourceEventOccurredMillis < rcEvent.occurredMillis")
+      DEBUG(
+        "%s < %s",
+        TimeHelpers.toYYYYMMDDHHMMSSSSS(this._workingUserState.latestResourceEventOccurredMillis),
+        TimeHelpers.toYYYYMMDDHHMMSSSSS(rcEvent.occurredMillis)
+      )
+      computeRealtime()
+    }
+    else {
+      computeBatch()
     }
 
     DEBUG("Updated %s", this._workingUserState)
@@ -381,6 +420,38 @@ class UserActor extends ReflectiveRoleableActor {
     }
   }
 
+  def onGetUserWalletRequest(event: GetUserWalletRequest): Unit = {
+    haveWorkingUserState match {
+      case true ⇒
+        DEBUG("haveWorkingUserState: %s", event)
+        sender ! GetUserWalletResponse(
+          Right(
+            GetUserWalletResponseData(
+              this._userID,
+              this._workingUserState.totalCredits,
+              this._workingUserState.walletEntries.toList
+        )))
+
+      case false ⇒
+        DEBUG("!haveWorkingUserState: %s", event)
+        haveUserCreationIMEvent match {
+          case true ⇒
+            DEBUG("haveUserCreationIMEvent: %s", event)
+            sender ! GetUserWalletResponse(
+              Right(
+                GetUserWalletResponseData(
+                  this._userID,
+                  aquarium.initialUserBalance(this._userCreationIMEvent.role, this._userCreationIMEvent.occurredMillis),
+                  Nil
+            )))
+
+          case false ⇒
+            DEBUG("!haveUserCreationIMEvent: %s", event)
+            sender ! GetUserWalletResponse(Left("No wallet for user %s [AQU-WAL-00 8]".format(event.userID)), 404)
+        }
+    }
+  }
+
   private[this] def D_userID = {
     this._userID
   }
index bd668b1..d8902e4 100644 (file)
@@ -54,6 +54,7 @@ case class Chargeslot(
     startMillis: Long,
     stopMillis: Long,
     unitPrice: Double,
+    explanation: String = "",
     creditsToSubtract: Double = Double.NaN
 ) {
 
@@ -61,8 +62,8 @@ case class Chargeslot(
     !creditsToSubtract.isInfinite
   }
 
-  def copyWithCreditsToSubtract(credits: Double) = {
-    copy(creditsToSubtract = credits)
+  def copyWithCreditsToSubtract(credits: Double, _explanation: String) = {
+    copy(creditsToSubtract = credits, explanation = _explanation)
   }
 
   override def toString = "%s(%s, %s, %s, %s, %s)".format(
@@ -70,6 +71,7 @@ case class Chargeslot(
     toYYYYMMDDHHMMSSSSS(startMillis),
     toYYYYMMDDHHMMSSSSS(stopMillis),
     unitPrice,
+    explanation,
     creditsToSubtract
   )
 }
index 462a750..70cb330 100644 (file)
@@ -62,44 +62,6 @@ abstract class ChargingBehavior(val alias: String, val inputs: Set[ChargingInput
 
   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
@@ -115,19 +77,35 @@ abstract class ChargingBehavior(val alias: String, val inputs: Set[ChargingInput
       currentValue: Double,
       unitPrice: Double,
       details: Map[String, String]
-  ): Double = {
+  ): (Double, String) = {
     alias match {
      case ChargingBehaviorAliases.continuous ⇒
-       hrs(timeDeltaMillis) * oldAccumulatingAmount * unitPrice
+       val credits = hrs(timeDeltaMillis) * oldAccumulatingAmount * unitPrice
+       val explanation = "Time(%s) * OldTotal(%s) * Unit(%s)".format(
+         hrs(timeDeltaMillis),
+         oldAccumulatingAmount,
+         unitPrice
+       )
+
+       (credits, explanation)
 
      case ChargingBehaviorAliases.discrete ⇒
-       currentValue * unitPrice
+       val credits = currentValue * unitPrice
+       val explanation = "Value(%s) * Unit(%s)".format(currentValue, unitPrice)
+
+       (credits, explanation)
 
      case ChargingBehaviorAliases.onoff ⇒
-       hrs(timeDeltaMillis) * unitPrice
+       val credits = hrs(timeDeltaMillis) * unitPrice
+       val explanation = "Time(%s) * Unit(%s)".format(hrs(timeDeltaMillis), unitPrice)
+
+       (credits, explanation)
 
      case ChargingBehaviorAliases.once ⇒
-       currentValue
+       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))
@@ -194,10 +172,10 @@ abstract class ChargingBehavior(val alias: String, val inputs: Set[ChargingInput
     )
 
     val fullChargeslots = initialChargeslots.map {
-      case chargeslot@Chargeslot(startMillis, stopMillis, unitPrice, _) ⇒
+      case chargeslot@Chargeslot(startMillis, stopMillis, unitPrice, _, _) ⇒
         val timeDeltaMillis = stopMillis - startMillis
 
-        val creditsToSubtract = this.computeCreditsToSubtract(
+        val (creditsToSubtract, explanation) = this.computeCreditsToSubtract(
           _oldTotalCredits,       // FIXME ??? Should recalculate ???
           _oldAccumulatingAmount, // FIXME ??? Should recalculate ???
           _newAccumulatingAmount, // FIXME ??? Should recalculate ???
@@ -208,7 +186,7 @@ abstract class ChargingBehavior(val alias: String, val inputs: Set[ChargingInput
           currentDetails
         )
 
-        val newChargeslot = chargeslot.copyWithCreditsToSubtract(creditsToSubtract)
+        val newChargeslot = chargeslot.copyWithCreditsToSubtract(creditsToSubtract, explanation)
         newChargeslot
     }
 
@@ -228,12 +206,14 @@ abstract class ChargingBehavior(val alias: String, val inputs: Set[ChargingInput
       referenceTimeslot,
       billingMonthInfo.year,
       billingMonthInfo.month,
-      previousResourceEventOpt.map(List(_, currentResourceEvent)).getOrElse(List(currentResourceEvent)),
       fullChargeslots,
+      previousResourceEventOpt.map(List(_, currentResourceEvent)).getOrElse(List(currentResourceEvent)),
       resourceType,
       currentResourceEvent.isSynthetic
     )
 
+    logger.debug("newWalletEntry = {}", newWalletEntry.toJsonString)
+
     walletEntryRecorder.apply(newWalletEntry)
 
     (1, newTotalCredits)
@@ -270,6 +250,7 @@ abstract class ChargingBehavior(val alias: String, val inputs: Set[ChargingInput
    * @param currentResourceEvent
    * @param resourceType
    * @param billingMonthInfo
+   * @param previousResourceEventOpt
    * @param userAgreements
    * @param chargingData
    * @param totalCredits
@@ -277,18 +258,18 @@ abstract class ChargingBehavior(val alias: String, val inputs: Set[ChargingInput
    * @param clogOpt
    * @return The number of wallet entries recorded and the new total credits
    */
-  protected def genericChargeResourceEvent(
+  def chargeResourceEvent(
       aquarium: Aquarium,
       currentResourceEvent: ResourceEventModel,
       resourceType: ResourceType,
       billingMonthInfo: BillingMonthInfo,
+      previousResourceEventOpt: Option[ResourceEventModel],
       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)
@@ -303,12 +284,12 @@ abstract class ChargingBehavior(val alias: String, val inputs: Set[ChargingInput
       // 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
 
+          clog.debug("I have previous event %s", previousResourceEvent.toDebugString)
+
           computeChargeslots(
             chargingData,
             previousResourceEventOpt,
@@ -327,11 +308,12 @@ abstract class ChargingBehavior(val alias: String, val inputs: Set[ChargingInput
           // Let's see if we can create a dummy previous event.
           val actualFirstEvent = currentResourceEvent
 
+          // FIXME: Why && ?
           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))
+            clog.debug("Dummy first event %s", dummyFirst.toDebugString)
 
             val previousResourceEvent = dummyFirst
             val previousValue = previousResourceEvent.value
@@ -373,9 +355,6 @@ abstract class ChargingBehavior(val alias: String, val inputs: Set[ChargingInput
       }
     }
 
-    // After processing, all events billable or not update the previous state
-    setChargingData(chargingData, EnvKeys.PreviousEvent, currentResourceEvent)
-
     retval
   }
 
@@ -466,7 +445,7 @@ abstract class ChargingBehavior(val alias: String, val inputs: Set[ChargingInput
    *
    * The only exception to the rule is ON events for [[gr.grnet.aquarium.charging.OnOffChargingBehavior]].
    */
-  def isBillableEvent(event: ResourceEventModel): Boolean = false
+  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
@@ -480,7 +459,7 @@ abstract class ChargingBehavior(val alias: String, val inputs: Set[ChargingInput
 
   def constructDummyFirstEventFor(actualFirst: ResourceEventModel, newOccurredMillis: Long): ResourceEventModel = {
     if(!mustGenerateDummyFirstEvent) {
-      throw new AquariumException("constructDummyFirstEventFor() Not compliant with %s".format(this))
+      throw new AquariumInternalError("constructDummyFirstEventFor() Not compliant with %s", this)
     }
 
     val newDetails = Map(
@@ -517,8 +496,6 @@ object ChargingBehavior {
    * 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")
   }
 
index 396a0fe..91042b5 100644 (file)
@@ -202,7 +202,8 @@ final class ChargingService extends AquariumAwareSkeleton with Lifecycle with Lo
       workingUserState: WorkingUserState,
       chargingReason: ChargingReason,
       billingMonthInfo: BillingMonthInfo,
-      clogOpt: Option[ContextualLogger]
+      clogOpt: Option[ContextualLogger],
+      updateLatestMiilis: Boolean
   ): Unit = {
 
     val resourceTypeName = resourceEvent.resource
@@ -220,6 +221,7 @@ final class ChargingService extends AquariumAwareSkeleton with Lifecycle with Lo
       resourceEvent,
       resourceType,
       billingMonthInfo,
+      workingUserState.previousEventOfResourceInstance.get(resourceAndInstanceInfo),
       workingUserState.workingAgreementHistory.toAgreementHistory,
       workingUserState.getChargingDataForResourceEvent(resourceAndInstanceInfo),
       workingUserState.totalCredits,
@@ -227,6 +229,12 @@ final class ChargingService extends AquariumAwareSkeleton with Lifecycle with Lo
       clogOpt
     )
 
+    if(updateLatestMiilis) {
+      workingUserState.latestUpdateMillis = TimeHelpers.nowMillis()
+    }
+
+    workingUserState.updateLatestResourceEventOccurredMillis(resourceEvent.occurredMillis)
+    workingUserState.previousEventOfResourceInstance(resourceAndInstanceInfo) = resourceEvent
     workingUserState.totalCredits = newTotalCredits
   }
 
@@ -238,14 +246,22 @@ final class ChargingService extends AquariumAwareSkeleton with Lifecycle with Lo
       clogOpt: Option[ContextualLogger] = None
   ): Unit = {
 
+    var _counter = 0
     for(currentResourceEvent ← resourceEvents) {
       processResourceEvent(
         currentResourceEvent,
         workingUserState,
         chargingReason,
         billingMonthInfo,
-        clogOpt
+        clogOpt,
+        false
       )
+
+      _counter += 1
+    }
+
+    if(_counter > 0) {
+      workingUserState.latestUpdateMillis = TimeHelpers.nowMillis()
     }
   }
 
@@ -341,12 +357,17 @@ final class ChargingService extends AquariumAwareSkeleton with Lifecycle with Lo
         workingUserState,
         chargingReason,
         billingMonthInfo,
-        clogSome
+        clogSome,
+        false
       )
 
       _rcEventsCounter += 1
     }
 
+    if(_rcEventsCounter > 0) {
+      workingUserState.latestUpdateMillis = TimeHelpers.nowMillis()
+    }
+
     clog.debug("Found %s resource events for month %s".format(_rcEventsCounter, billingMonthInfo.toShortDebugString))
 
     if(isFullMonthBilling) {
index 4137ec0..e1e594f 100644 (file)
@@ -52,6 +52,7 @@ final case class StdUserState(
     parentIDInStore: Option[String],
     userID: String,
     occurredMillis: Long,
+    latestResourceEventOccurredMillis: Long,
     totalCredits: Double,
     isFullBillingMonth: Boolean,
     billingYear: Int,
@@ -115,6 +116,7 @@ final object StdUserState {
       None,
       userID,
       userCreationMillis,
+      0L, // FIXME is this correct?
       totalCredits,
       false,
       bmi.year,
index e563407..44ad509 100644 (file)
@@ -57,6 +57,8 @@ trait UserStateModel extends JsonSupport {
 
   def occurredMillis: Long // When this user state was computed
 
+  def latestResourceEventOccurredMillis: Long
+
   def totalCredits: Double
 
   /**
diff --git a/src/main/scala/gr/grnet/aquarium/charging/state/UserStateModelSkeleton.scala b/src/main/scala/gr/grnet/aquarium/charging/state/UserStateModelSkeleton.scala
new file mode 100644 (file)
index 0000000..2bdf896
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2011-2012 GRNET S.A. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above
+ *      copyright notice, this list of conditions and the following
+ *      disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following
+ *      disclaimer in the documentation and/or other materials
+ *      provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+ * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * The views and conclusions contained in the software and
+ * documentation are those of the authors and should not be
+ * interpreted as representing official policies, either expressed
+ * or implied, of GRNET S.A.
+ */
+
+package gr.grnet.aquarium.charging.state
+
+import scala.collection.mutable
+import gr.grnet.aquarium.event.model.resource.ResourceEventModel
+import gr.grnet.aquarium.charging.wallet.WalletEntry
+import gr.grnet.aquarium.policy.ResourceType
+
+/**
+ * Provides common, helper operations for a [[gr.grnet.aquarium.charging.state.UserStateModel]].
+ *
+ * @author Christos KK Loverdos <loverdos@gmail.com>
+ */
+
+abstract class UserStateModelSkeleton extends UserStateModel {
+  protected def mutableMap[Vin, Vout](
+      inputMap: Map[String, Vin],
+      vInOut: Vin ⇒ Vout
+  ): mutable.Map[(String, String), Vout] = {
+    val items = for {
+      (resourceAndInstanceID, vIn) ← inputMap.toSeq
+    } yield {
+      StdUserState.resourceAndInstanceIDOfString(resourceAndInstanceID) -> vInOut(vIn)
+    }
+
+    mutable.Map(items: _*)
+  }
+
+  protected def mutableAccumulatingAmountMap: mutable.Map[(String, String), Double] = {
+    mutableMap(accumulatingAmountOfResourceInstance, identity[Double])
+  }
+
+  protected def mutableChargingDataMap: mutable.Map[(String, String), mutable.Map[String, Any]] = {
+    mutableMap(chargingDataOfResourceInstance, (vIn: Map[String, Any]) ⇒ mutable.Map(vIn.toSeq: _*))
+  }
+
+  protected def mutableImplicitlyIssuedStartMap: mutable.Map[(String, String), ResourceEventModel] = {
+    mutable.Map(implicitlyIssuedStartEvents.map(rem ⇒ (rem.safeResource, rem.safeInstanceID) -> rem): _*)
+  }
+
+  protected def mutablePreviousEventsMap: mutable.Map[(String, String), ResourceEventModel] = {
+    mutable.Map(previousResourceEvents.map(rem ⇒ (rem.safeResource, rem.safeInstanceID) -> rem): _*)
+  }
+
+  protected def mutableWalletEntries = {
+    val buffer = new mutable.ListBuffer[WalletEntry]
+    buffer ++= this.walletEntries
+    buffer
+  }
+
+  protected def mutableAgreementHistory = {
+    this.agreementHistory.toWorkingAgreementHistory
+  }
+
+  def toWorkingUserState(resourceTypesMap: Map[String, ResourceType]): WorkingUserState = {
+    new WorkingUserState(
+      this.userID,
+      this.parentIDInStore,
+      this.chargingReason,
+      resourceTypesMap,
+      mutablePreviousEventsMap,
+      mutableImplicitlyIssuedStartMap,
+      mutableAccumulatingAmountMap,
+      mutableChargingDataMap,
+      this.totalCredits,
+      mutableAgreementHistory,
+      this.occurredMillis,
+      this.latestResourceEventOccurredMillis,
+      this.billingPeriodOutOfSyncResourceEventsCounter,
+      mutableWalletEntries
+    )
+  }
+}
index 9c8745f..0bff90e 100644 (file)
@@ -71,11 +71,18 @@ final class WorkingUserState(
     val chargingDataOfResourceInstance: mutable.Map[(String, String), mutable.Map[String, Any]],
     var totalCredits: Double,
     val workingAgreementHistory: WorkingAgreementHistory,
-    var occurredMillis: Long,
+    var latestUpdateMillis: Long, // last update of this working user state
+    var latestResourceEventOccurredMillis: Long,
     var billingPeriodOutOfSyncResourceEventsCounter: Long,
     val walletEntries: mutable.ListBuffer[WalletEntry]
 ) extends JsonSupport {
 
+  def updateLatestResourceEventOccurredMillis(millis: Long): Unit = {
+    if(millis > this.latestResourceEventOccurredMillis) {
+      this.latestResourceEventOccurredMillis = millis
+    }
+  }
+
   private[this] def immutablePreviousResourceEvents: List[ResourceEventModel] = {
     previousEventOfResourceInstance.valuesIterator.toList
   }
@@ -118,7 +125,8 @@ final class WorkingUserState(
       idOpt.getOrElse(""),
       this.parentUserStateIDInStore,
       this.userID,
-      this.occurredMillis,
+      this.latestUpdateMillis,
+      this.latestResourceEventOccurredMillis,
       this.totalCredits,
       isFullBillingMonth,
       billingYear,
@@ -149,7 +157,8 @@ final class WorkingUserState(
       this.chargingDataOfResourceInstance,
       this.totalCredits,
       this.workingAgreementHistory,
-      this.occurredMillis,
+      this.latestUpdateMillis,
+      this.latestResourceEventOccurredMillis,
       this.billingPeriodOutOfSyncResourceEventsCounter,
       this.walletEntries
     )
index a51144f..84b331f 100644 (file)
@@ -41,6 +41,7 @@ import gr.grnet.aquarium.logic.accounting.dsl.{Timeslot}
 import gr.grnet.aquarium.converter.{JsonTextFormat, StdConverters}
 import gr.grnet.aquarium.policy.ResourceType
 import gr.grnet.aquarium.event.model.resource.ResourceEventModel
+import gr.grnet.aquarium.util.json.JsonSupport
 
 /**
  * The following equation must hold: `newTotalCredits = oldTotalCredits - sumOfCreditsToSubtract`.
@@ -50,7 +51,7 @@ import gr.grnet.aquarium.event.model.resource.ResourceEventModel
  * @param oldTotalCredits
  * @param newTotalCredits
  * @param whenComputedMillis When the computation took place
- * @param yearOfBillingMonth
+ * @param billingYear
  * @param billingMonth
  * @param resourceEvents
  * @param chargeslots The details of the credit computation
@@ -65,13 +66,13 @@ case class WalletEntry(
     newTotalCredits: Double,
     whenComputedMillis: Long,
     referenceTimeslot: Timeslot,
-    yearOfBillingMonth: Int,
+    billingYear: Int,
     billingMonth: Int,
-    resourceEvents: List[ResourceEventModel], // current is the last one
     chargeslots: List[Chargeslot],
+    resourceEvents: List[ResourceEventModel], // current is the last one
     resourceType: ResourceType,
     isSynthetic: Boolean
-) {
+) extends JsonSupport {
 
   def currentResourceEvent = resourceEvents match {
     case previous :: current :: Nil ⇒
@@ -87,7 +88,7 @@ case class WalletEntry(
 
   def chargslotCount = chargeslots.length
 
-  def isOutOfSync = currentResourceEvent.isOutOfSyncForBillingMonth(yearOfBillingMonth, billingMonth)
+  def isOutOfSync = currentResourceEvent.isOutOfSyncForBillingMonth(billingYear, billingMonth)
 
   def toDebugString = "%s%s(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)".format(
     if(isSynthetic) "*" else "",
@@ -98,7 +99,7 @@ case class WalletEntry(
     oldTotalCredits,
     newTotalCredits,
     new MutableDateCalc(whenComputedMillis).toYYYYMMDDHHMMSSSSS,
-    yearOfBillingMonth,
+    billingYear,
     billingMonth,
     resourceEvents,
     chargeslots,
index 77a676d..85d90ae 100644 (file)
@@ -122,7 +122,7 @@ object TimeslotComputations extends Loggable {
     }
 
     // 1. Round ONE: split time according to overlapping policies and agreements.
-    val alignedTimeslots = splitTimeslotByPoliciesAndAgreements(referenceTimeslot, policyTimeslots.toList, agreementTimeslots.toList, Just(clog))
+    val alignedTimeslots = List(referenceTimeslot) //splitTimeslotByPoliciesAndAgreements(referenceTimeslot, policyTimeslots.toList, agreementTimeslots.toList, Just(clog))
 
     // 2. Round TWO: Use the aligned timeslots of Round ONE to produce even more
     //    fine-grained timeslots according to applicable algorithms.
@@ -131,9 +131,9 @@ object TimeslotComputations extends Loggable {
     val allChargeslots = for {
       alignedTimeslot <- alignedTimeslots
     } yield {
-      val policy = getPolicyWithin(alignedTimeslot)
+      val policy = policyByTimeslot.valuesIterator.next()//getPolicyWithin(alignedTimeslot)
       //      clog.debug("dslPolicy = %s", dslPolicy)
-      val userAgreement = getAgreementWithin(alignedTimeslot)
+      val userAgreement = agreementByTimeslot.valuesIterator.next()//getAgreementWithin(alignedTimeslot)
 
       // TODO: Factor this out, just like we did with:
       // TODO:  val alignedTimeslots = splitTimeslotByPoliciesAndAgreements
@@ -238,7 +238,8 @@ object TimeslotComputations extends Loggable {
           effectivePriceTable
       }
 
-      resolveEffective(alignedTimeslot, effectivePriceTable.priceOverrides)
+      //resolveEffective(alignedTimeslot, effectivePriceTable.priceOverrides)
+      immutable.SortedMap(alignedTimeslot -> effectivePriceTable.priceOverrides.head)
     }
 
     private def printPriceList(p: PriceList) : Unit = {
index 7139cd5..20ac755 100644 (file)
@@ -67,13 +67,15 @@ trait IMEventModel extends ExternalEventModel {
   def userCreationMillisOption = if(isCreateUser) Some(this.occurredMillis) else None
 
   override def toDebugString = {
-    "%s(userID=%s, id=%s, isActive=%s, role='%s', occurred=%s)".format(
+    "%s(userID=%s, id=%s, isActive=%s, role='%s', occurred=%s, type=%s)".format(
       shortClassNameOf(this),
       userID,
       id,
       isActive,
       role,
-      new MutableDateCalc(occurredMillis).toString)
+      new MutableDateCalc(occurredMillis).toString,
+      eventType
+    )
   }
 }
 
index 553e96f..2f376a7 100644 (file)
@@ -51,7 +51,7 @@ import java.net.InetSocketAddress
 import java.util.concurrent.{Executors, TimeUnit}
 import gr.grnet.aquarium.util.date.TimeHelpers
 import org.joda.time.format.ISODateTimeFormat
-import gr.grnet.aquarium.actor.message.{UserActorRequestMessage, GetUserStateRequest, GetUserBalanceRequest, UserActorResponseMessage}
+import gr.grnet.aquarium.actor.message.{GetUserWalletRequest, UserActorRequestMessage, GetUserStateRequest, GetUserBalanceRequest, UserActorResponseMessage}
 import com.ckkloverdos.resource.StreamResource
 import com.ckkloverdos.maybe.{Just, Failed}
 import gr.grnet.aquarium.event.model.ExternalEventModel
@@ -194,6 +194,8 @@ class FinagleRESTService extends Lifecycle with AquariumAwareSkeleton with Confi
       actorRouterService(requestMessage).transform { tryResponse ⇒
         tryResponse match {
           case TReturn(responseMessage: UserActorResponseMessage[_]) ⇒
+            logger.debug("{}", responseMessage)
+            logger.debug("{}", responseMessage.responseToJsonString)
             val statusCode = responseMessage.suggestedHTTPStatus
             val status = THttpResponseStatus.valueOf(statusCode)
 
@@ -208,7 +210,7 @@ class FinagleRESTService extends Lifecycle with AquariumAwareSkeleton with Confi
                 stringResponse(status, errorMessage, TEXT_PLAIN)
 
               case Right(_) ⇒
-                stringResponse(status, responseMessage.toJsonString, APPLICATION_JSON)
+                stringResponse(status, responseMessage.responseToJsonString, APPLICATION_JSON)
             }
 
           case TThrow(throwable) ⇒
@@ -301,9 +303,13 @@ class FinagleRESTService extends Lifecycle with AquariumAwareSkeleton with Confi
           // /user/(.+)/balance/?
           callUserActor(GetUserBalanceRequest(userID, millis))
 
-        case RESTPaths.UserStatePath(userId) ⇒
+        case RESTPaths.UserStatePath(userID) ⇒
           // /user/(.+)/state/?
-          callUserActor(GetUserStateRequest(userId, millis))
+          callUserActor(GetUserStateRequest(userID, millis))
+
+        case RESTPaths.UserWalletPath(userID) ⇒
+          // /user/(.+)/wallet/?
+          callUserActor(GetUserWalletRequest(userID, millis))
       }
 
       val DefaultHandler: URIPF = {
index 3717583..e8c588b 100644 (file)
@@ -74,6 +74,8 @@ object RESTPaths {
    */
   final val UserStatePath = "/user/([^/]+)/state/?".r
 
+  final val UserWalletPath = "/user/([^/]+)/wallet/?".r
+
   final val UserActorCacheContentsPath = (AdminPrefix + "/cache/actor/user/contents").r
   final val UserActorCacheCountPath    = (AdminPrefix + "/cache/actor/user/size").r
   final val UserActorCacheStatsPath    = (AdminPrefix + "/cache/actor/user/stats").r
index 5dac55e..41a5ca8 100644 (file)
@@ -123,6 +123,7 @@ extends StoreProvider
         model.parentIDInStore,
         model.userID,
         model.occurredMillis,
+        model.latestResourceEventOccurredMillis,
         model.totalCredits,
         model.isFullBillingMonth,
         model.billingYear,
index 1e097cf..a0ee96b 100644 (file)
@@ -51,6 +51,7 @@ case class MongoDBUserState(
     parentIDInStore: Option[String],
     userID: String,
     occurredMillis: Long,
+    latestResourceEventOccurredMillis: Long,
     totalCredits: Double,
     isFullBillingMonth: Boolean,
     billingYear: Int,
@@ -83,6 +84,7 @@ object MongoDBUserState {
       model.parentIDInStore,
       model.userID,
       model.occurredMillis,
+      model.latestResourceEventOccurredMillis,
       model.totalCredits,
       model.isFullBillingMonth,
       model.billingYear,