.gradle/
logs/
History.md
+npm-debug.log
logger.info("{} = {}", EnvKeys.eventsStoreFolder.name, folder)
}
this.eventsStoreFolder.throwMe // on error
+
+ logger.info("default policy = {}", defaultPolicyModel.toJsonString)
}
private[this] def addShutdownHooks(): Unit = {
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}
/**
classOf[UserActor],
Set(classOf[ProcessResourceEvent],
classOf[ProcessIMEvent],
+ classOf[GetUserWalletRequest],
classOf[GetUserBalanceRequest],
classOf[GetUserStateRequest]),
Set(classOf[InitializeUserActorState],
--- /dev/null
+/*
+ * 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
+}
--- /dev/null
+/*
+ * 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
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
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
/**
*
this._latestIMEventID = imEvent.id
}
+ private[this] def updateLatestResourceEventIDFrom(rcEvent: ResourceEventModel): Unit = {
+ this._latestResourceEventID = rcEvent.id
+ }
+
/**
* Creates the initial state that is related to IMEvents.
*/
}
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)
}
}
+ 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
}
startMillis: Long,
stopMillis: Long,
unitPrice: Double,
+ explanation: String = "",
creditsToSubtract: Double = Double.NaN
) {
!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(
toYYYYMMDDHHMMSSSSS(startMillis),
toYYYYMMDDHHMMSSSSS(stopMillis),
unitPrice,
+ explanation,
creditsToSubtract
)
}
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
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))
)
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 ???
currentDetails
)
- val newChargeslot = chargeslot.copyWithCreditsToSubtract(creditsToSubtract)
+ val newChargeslot = chargeslot.copyWithCreditsToSubtract(creditsToSubtract, explanation)
newChargeslot
}
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)
* @param currentResourceEvent
* @param resourceType
* @param billingMonthInfo
+ * @param previousResourceEventOpt
* @param userAgreements
* @param chargingData
* @param totalCredits
* @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)
// 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,
// 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
}
}
- // After processing, all events billable or not update the previous state
- setChargingData(chargingData, EnvKeys.PreviousEvent, currentResourceEvent)
-
retval
}
*
* 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
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(
* 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")
}
workingUserState: WorkingUserState,
chargingReason: ChargingReason,
billingMonthInfo: BillingMonthInfo,
- clogOpt: Option[ContextualLogger]
+ clogOpt: Option[ContextualLogger],
+ updateLatestMiilis: Boolean
): Unit = {
val resourceTypeName = resourceEvent.resource
resourceEvent,
resourceType,
billingMonthInfo,
+ workingUserState.previousEventOfResourceInstance.get(resourceAndInstanceInfo),
workingUserState.workingAgreementHistory.toAgreementHistory,
workingUserState.getChargingDataForResourceEvent(resourceAndInstanceInfo),
workingUserState.totalCredits,
clogOpt
)
+ if(updateLatestMiilis) {
+ workingUserState.latestUpdateMillis = TimeHelpers.nowMillis()
+ }
+
+ workingUserState.updateLatestResourceEventOccurredMillis(resourceEvent.occurredMillis)
+ workingUserState.previousEventOfResourceInstance(resourceAndInstanceInfo) = resourceEvent
workingUserState.totalCredits = newTotalCredits
}
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()
}
}
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) {
parentIDInStore: Option[String],
userID: String,
occurredMillis: Long,
+ latestResourceEventOccurredMillis: Long,
totalCredits: Double,
isFullBillingMonth: Boolean,
billingYear: Int,
None,
userID,
userCreationMillis,
+ 0L, // FIXME is this correct?
totalCredits,
false,
bmi.year,
def occurredMillis: Long // When this user state was computed
+ def latestResourceEventOccurredMillis: Long
+
def totalCredits: Double
/**
--- /dev/null
+/*
+ * 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
+ )
+ }
+}
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
}
idOpt.getOrElse(""),
this.parentUserStateIDInStore,
this.userID,
- this.occurredMillis,
+ this.latestUpdateMillis,
+ this.latestResourceEventOccurredMillis,
this.totalCredits,
isFullBillingMonth,
billingYear,
this.chargingDataOfResourceInstance,
this.totalCredits,
this.workingAgreementHistory,
- this.occurredMillis,
+ this.latestUpdateMillis,
+ this.latestResourceEventOccurredMillis,
this.billingPeriodOutOfSyncResourceEventsCounter,
this.walletEntries
)
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`.
* @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
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 ⇒
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 "",
oldTotalCredits,
newTotalCredits,
new MutableDateCalc(whenComputedMillis).toYYYYMMDDHHMMSSSSS,
- yearOfBillingMonth,
+ billingYear,
billingMonth,
resourceEvents,
chargeslots,
}
// 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.
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
effectivePriceTable
}
- resolveEffective(alignedTimeslot, effectivePriceTable.priceOverrides)
+ //resolveEffective(alignedTimeslot, effectivePriceTable.priceOverrides)
+ immutable.SortedMap(alignedTimeslot -> effectivePriceTable.priceOverrides.head)
}
private def printPriceList(p: PriceList) : Unit = {
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
+ )
}
}
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
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)
stringResponse(status, errorMessage, TEXT_PLAIN)
case Right(_) ⇒
- stringResponse(status, responseMessage.toJsonString, APPLICATION_JSON)
+ stringResponse(status, responseMessage.responseToJsonString, APPLICATION_JSON)
}
case TThrow(throwable) ⇒
// /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 = {
*/
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
model.parentIDInStore,
model.userID,
model.occurredMillis,
+ model.latestResourceEventOccurredMillis,
model.totalCredits,
model.isFullBillingMonth,
model.billingYear,
parentIDInStore: Option[String],
userID: String,
occurredMillis: Long,
+ latestResourceEventOccurredMillis: Long,
totalCredits: Double,
isFullBillingMonth: Boolean,
billingYear: Int,
model.parentIDInStore,
model.userID,
model.occurredMillis,
+ model.latestResourceEventOccurredMillis,
model.totalCredits,
model.isFullBillingMonth,
model.billingYear,