2 * Copyright 2011-2012 GRNET S.A. All rights reserved.
4 * Redistribution and use in source and binary forms, with or
5 * without modification, are permitted provided that the following
8 * 1. Redistributions of source code must retain the above
9 * copyright notice, this list of conditions and the following
12 * 2. Redistributions in binary form must reproduce the above
13 * copyright notice, this list of conditions and the following
14 * disclaimer in the documentation and/or other materials
15 * provided with the distribution.
17 * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
18 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
21 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
24 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
25 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
27 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28 * POSSIBILITY OF SUCH DAMAGE.
30 * The views and conclusions contained in the software and
31 * documentation are those of the authors and should not be
32 * interpreted as representing official policies, either expressed
33 * or implied, of GRNET S.A.
36 package gr.grnet.aquarium.charging
38 import scala.collection.immutable
39 import scala.collection.mutable
41 import gr.grnet.aquarium.event.model.resource.ResourceEventModel
42 import gr.grnet.aquarium.{Aquarium, AquariumInternalError}
43 import gr.grnet.aquarium.policy.{FullPriceTable, EffectivePriceTable, UserAgreementModel, ResourceType}
44 import gr.grnet.aquarium.util._
45 import gr.grnet.aquarium.util.date.TimeHelpers
46 import gr.grnet.aquarium.charging.wallet.WalletEntry
47 import gr.grnet.aquarium.computation.{TimeslotComputations, BillingMonthInfo}
48 import gr.grnet.aquarium.charging.state.{WorkingResourceInstanceChargingState, WorkingResourcesChargingState, AgreementHistoryModel}
49 import gr.grnet.aquarium.logic.accounting.dsl.Timeslot
50 import gr.grnet.aquarium.store.PolicyStore
53 * A charging behavior indicates how charging for a resource will be done
54 * wrt the various states a resource can be.
56 * @author Christos KK Loverdos <loverdos@gmail.com>
59 abstract class ChargingBehaviorSkeleton(
60 final val selectorLabelsHierarchy: List[String]
61 ) extends ChargingBehavior with Loggable {
63 protected def hrs(millis: Double) = {
64 val hours = millis / 1000 / 60 / 60
65 val roundedHours = hours
69 protected def rcDebugInfo(rcEvent: ResourceEventModel) = {
73 protected def newWorkingResourceInstanceChargingState() = {
74 new WorkingResourceInstanceChargingState(
85 final protected def ensureWorkingState(
86 workingResourcesChargingState: WorkingResourcesChargingState,
87 resourceEvent: ResourceEventModel
89 ensureResourcesChargingStateDetails(workingResourcesChargingState.details)
90 ensureResourceInstanceChargingState(workingResourcesChargingState, resourceEvent)
93 protected def ensureResourcesChargingStateDetails(
94 details: mutable.Map[String, Any]
97 protected def ensureResourceInstanceChargingState(
98 workingResourcesChargingState: WorkingResourcesChargingState,
99 resourceEvent: ResourceEventModel
102 val instanceID = resourceEvent.instanceID
103 val stateOfResourceInstance = workingResourcesChargingState.stateOfResourceInstance
105 stateOfResourceInstance.get(instanceID) match {
107 stateOfResourceInstance(instanceID) = newWorkingResourceInstanceChargingState()
113 protected def computeWalletEntriesForNewEvent(
114 resourceEvent: ResourceEventModel,
115 resourceType: ResourceType,
116 billingMonthInfo: BillingMonthInfo,
117 totalCredits: Double,
118 referenceTimeslot: Timeslot,
119 agreementByTimeslot: immutable.SortedMap[Timeslot, UserAgreementModel],
120 workingResourcesChargingStateDetails: mutable.Map[String, Any],
121 workingResourceInstanceChargingState: WorkingResourceInstanceChargingState,
122 policyStore: PolicyStore,
123 walletEntryRecorder: WalletEntry ⇒ Unit
126 val userID = resourceEvent.userID
127 val resourceEventDetails = resourceEvent.details
129 var _oldTotalCredits = totalCredits
131 var _newAccumulatingAmount = computeNewAccumulatingAmount(workingResourceInstanceChargingState, resourceEventDetails)
132 // It will also update the old one inside the data structure.
133 workingResourceInstanceChargingState.setNewAccumulatingAmount(_newAccumulatingAmount)
135 val policyByTimeslot = policyStore.loadAndSortPoliciesWithin(
136 referenceTimeslot.from.getTime,
137 referenceTimeslot.to.getTime
140 val effectivePriceTableSelector: FullPriceTable ⇒ EffectivePriceTable = fullPriceTable ⇒ {
141 this.selectEffectivePriceTable(
143 workingResourcesChargingStateDetails,
144 workingResourceInstanceChargingState,
151 val initialChargeslots = TimeslotComputations.computeInitialChargeslots(
155 effectivePriceTableSelector
158 val fullChargeslots = initialChargeslots.map {
159 case chargeslot@Chargeslot(startMillis, stopMillis, unitPrice, _, _) ⇒
160 val timeDeltaMillis = stopMillis - startMillis
162 val (creditsToSubtract, explanation) = this.computeCreditsToSubtract(
163 workingResourceInstanceChargingState,
164 _oldTotalCredits, // FIXME ??? Should recalculate ???
169 val newChargeslot = chargeslot.copyWithCreditsToSubtract(creditsToSubtract, explanation)
173 if(fullChargeslots.length == 0) {
174 throw new AquariumInternalError("No chargeslots computed for resource event %s".format(resourceEvent.id))
177 val sumOfCreditsToSubtract = fullChargeslots.map(_.creditsToSubtract).sum
178 val newTotalCredits = _oldTotalCredits - sumOfCreditsToSubtract
180 val newWalletEntry = WalletEntry(
182 sumOfCreditsToSubtract,
185 TimeHelpers.nowMillis(),
187 billingMonthInfo.year,
188 billingMonthInfo.month,
190 resourceEvent :: workingResourceInstanceChargingState.previousEvents,
192 resourceEvent.isSynthetic
195 logger.debug("newWalletEntry = {}", newWalletEntry.toJsonString)
197 walletEntryRecorder.apply(newWalletEntry)
203 def selectEffectivePriceTable(
204 fullPriceTable: FullPriceTable,
205 workingChargingBehaviorDetails: mutable.Map[String, Any],
206 workingResourceInstanceChargingState: WorkingResourceInstanceChargingState,
207 currentResourceEvent: ResourceEventModel,
208 referenceTimeslot: Timeslot,
210 ): EffectivePriceTable = {
212 val selectorPath = computeSelectorPath(
213 workingChargingBehaviorDetails,
214 workingResourceInstanceChargingState,
215 currentResourceEvent,
220 fullPriceTable.effectivePriceTableOfSelectorForResource(selectorPath, currentResourceEvent.safeResource)
224 * A generic implementation for charging a resource event.
225 * TODO: Ditch this in favor of completely ahdoc behaviors.
227 * @return The number of wallet entries recorded and the new total credits
229 def processResourceEvent(
231 currentResourceEvent: ResourceEventModel,
232 resourceType: ResourceType,
233 billingMonthInfo: BillingMonthInfo,
234 workingResourcesChargingState: WorkingResourcesChargingState,
235 userAgreements: AgreementHistoryModel,
236 totalCredits: Double,
237 walletEntryRecorder: WalletEntry ⇒ Unit
241 /*val currentResourceEventDebugInfo = rcDebugInfo(currentResourceEvent)
243 val isBillable = this.isBillableEvent(currentResourceEvent)
244 val retval = if(!isBillable) {
245 // The resource event is not billable.
246 Debug(logger, "Ignoring not billable %s", currentResourceEventDebugInfo)
249 // The resource event is billable.
250 // Find the previous event if needed.
251 // This is (potentially) needed to calculate new credit amount and new resource instance amount
252 if(this.needsPreviousEventForCreditAndAmountCalculation) {
253 if(previousResourceEventOpt.isDefined) {
254 val previousResourceEvent = previousResourceEventOpt.get
255 val previousValue = previousResourceEvent.value
257 Debug(logger, "I have previous event %s", previousResourceEvent.toDebugString)
261 previousResourceEventOpt,
262 currentResourceEvent,
264 Timeslot(previousResourceEvent.occurredMillis, currentResourceEvent.occurredMillis),
266 userAgreements.agreementByTimeslot,
269 aquarium.policyStore,
273 // We do not have the needed previous event, so this must be the first resource event of its kind, ever.
274 // Let's see if we can create a dummy previous event.
275 val actualFirstEvent = currentResourceEvent
278 if(this.isBillableFirstEvent(actualFirstEvent) && this.mustGenerateDummyFirstEvent) {
279 Debug(logger, "First event of its kind %s", currentResourceEventDebugInfo)
281 val dummyFirst = this.constructDummyFirstEventFor(currentResourceEvent, billingMonthInfo.monthStartMillis)
282 Debug(logger, "Dummy first event %s", dummyFirst.toDebugString)
284 val previousResourceEvent = dummyFirst
285 val previousValue = previousResourceEvent.value
289 Some(previousResourceEvent),
290 currentResourceEvent,
292 Timeslot(previousResourceEvent.occurredMillis, currentResourceEvent.occurredMillis),
294 userAgreements.agreementByTimeslot,
297 aquarium.policyStore,
301 Debug(logger, "Ignoring first event of its kind %s", currentResourceEventDebugInfo)
302 // userStateWorker.updateIgnored(currentResourceEvent)
307 // No need for previous event. One event does it all.
311 currentResourceEvent,
313 Timeslot(currentResourceEvent.occurredMillis, currentResourceEvent.occurredMillis + 1),
315 userAgreements.agreementByTimeslot,
318 aquarium.policyStore,
329 * Given the charging state of a resource instance and the details of the incoming message, compute the new
330 * accumulating amount.
332 def computeNewAccumulatingAmount(
333 workingResourceInstanceChargingState: WorkingResourceInstanceChargingState,
334 eventDetails: Map[String, String]
338 def constructDummyFirstEventFor(actualFirst: ResourceEventModel, newOccurredMillis: Long): ResourceEventModel = {
340 val newDetails = Map(
341 ResourceEventModel.Names.details_aquarium_is_synthetic -> "true",
342 ResourceEventModel.Names.details_aquarium_is_dummy_first -> "true",
343 ResourceEventModel.Names.details_aquarium_reference_event_id -> actualFirst.id,
344 ResourceEventModel.Names.details_aquarium_reference_event_id_in_store -> actualFirst.stringIDInStoreOrEmpty
347 actualFirst.withDetailsAndValue(newDetails, 0.0, newOccurredMillis)