462a750665b79da445ee5c97ef1ddb8a192fdfc8
[aquarium] / src / main / scala / gr / grnet / aquarium / charging / ChargingBehavior.scala
1 /*
2  * Copyright 2011-2012 GRNET S.A. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or
5  * without modification, are permitted provided that the following
6  * conditions are met:
7  *
8  *   1. Redistributions of source code must retain the above
9  *      copyright notice, this list of conditions and the following
10  *      disclaimer.
11  *
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.
16  *
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.
29  *
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.
34  */
35
36 package gr.grnet.aquarium.charging
37
38 import scala.collection.immutable
39 import scala.collection.mutable
40
41 import gr.grnet.aquarium.event.model.resource.ResourceEventModel
42 import gr.grnet.aquarium.{Aquarium, AquariumInternalError, AquariumException}
43 import gr.grnet.aquarium.policy.{UserAgreementModel, ResourceType}
44 import com.ckkloverdos.key.{TypedKey, TypedKeySkeleton}
45 import gr.grnet.aquarium.util._
46 import gr.grnet.aquarium.util.date.TimeHelpers
47 import gr.grnet.aquarium.charging.wallet.WalletEntry
48 import gr.grnet.aquarium.computation.{TimeslotComputations, BillingMonthInfo}
49 import gr.grnet.aquarium.charging.state.AgreementHistory
50 import gr.grnet.aquarium.logic.accounting.dsl.Timeslot
51 import gr.grnet.aquarium.store.PolicyStore
52 import gr.grnet.aquarium.charging.ChargingBehavior.EnvKeys
53
54 /**
55  * A charging behavior indicates how charging for a resource will be done
56  * wrt the various states a resource can be.
57  *
58  * @author Christos KK Loverdos <loverdos@gmail.com>
59  */
60
61 abstract class ChargingBehavior(val alias: String, val inputs: Set[ChargingInput]) extends Loggable {
62
63   final lazy val inputNames = inputs.map(_.name)
64
65   /**
66    *
67    * @param aquarium
68    * @param resourceEvent
69    * @param resourceType
70    * @param billingMonthInfo
71    * @param userAgreements
72    * @param chargingData
73    * @param totalCredits
74    * @param walletEntryRecorder
75    * @param clogOpt
76    * @return The number of wallet entries recorded and the new total credits
77    */
78   def chargeResourceEvent(
79       aquarium: Aquarium,
80       resourceEvent: ResourceEventModel,
81       resourceType: ResourceType,
82       billingMonthInfo: BillingMonthInfo,
83       userAgreements: AgreementHistory,
84       chargingData: mutable.Map[String, Any],
85       totalCredits: Double,
86       walletEntryRecorder: WalletEntry ⇒ Unit,
87       clogOpt: Option[ContextualLogger] = None
88   ): (Int, Double) = {
89
90     genericChargeResourceEvent(
91       aquarium,
92       resourceEvent,
93       resourceType,
94       billingMonthInfo,
95       userAgreements,
96       chargingData,
97       totalCredits,
98       walletEntryRecorder,
99       clogOpt
100     )
101   }
102
103   @inline private[this] def hrs(millis: Double) = {
104     val hours = millis / 1000 / 60 / 60
105     val roundedHours = hours
106     roundedHours
107   }
108
109   protected def computeCreditsToSubtract(
110       oldCredits: Double,
111       oldAccumulatingAmount: Double,
112       newAccumulatingAmount: Double,
113       timeDeltaMillis: Long,
114       previousValue: Double,
115       currentValue: Double,
116       unitPrice: Double,
117       details: Map[String, String]
118   ): Double = {
119     alias match {
120      case ChargingBehaviorAliases.continuous ⇒
121        hrs(timeDeltaMillis) * oldAccumulatingAmount * unitPrice
122
123      case ChargingBehaviorAliases.discrete ⇒
124        currentValue * unitPrice
125
126      case ChargingBehaviorAliases.onoff ⇒
127        hrs(timeDeltaMillis) * unitPrice
128
129      case ChargingBehaviorAliases.once ⇒
130        currentValue
131
132      case name ⇒
133        throw new AquariumInternalError("Cannot compute credit diff for charging behavior %s".format(name))
134     }
135   }
136
137   protected def rcDebugInfo(rcEvent: ResourceEventModel) = {
138     rcEvent.toDebugString
139   }
140
141   /**
142    *
143    * @param chargingData
144    * @param previousResourceEventOpt
145    * @param currentResourceEvent
146    * @param billingMonthInfo
147    * @param referenceTimeslot
148    * @param resourceType
149    * @param agreementByTimeslot
150    * @param previousValue
151    * @param totalCredits
152    * @param policyStore
153    * @param walletEntryRecorder
154    * @return The number of wallet entries recorded and the new total credits
155    */
156   protected def computeChargeslots(
157       chargingData: mutable.Map[String, Any],
158       previousResourceEventOpt: Option[ResourceEventModel],
159       currentResourceEvent: ResourceEventModel,
160       billingMonthInfo: BillingMonthInfo,
161       referenceTimeslot: Timeslot,
162       resourceType: ResourceType,
163       agreementByTimeslot: immutable.SortedMap[Timeslot, UserAgreementModel],
164       previousValue: Double,
165       totalCredits: Double,
166       policyStore: PolicyStore,
167       walletEntryRecorder: WalletEntry ⇒ Unit
168   ): (Int, Double) = {
169
170     val currentValue = currentResourceEvent.value
171     val userID = currentResourceEvent.userID
172     val currentDetails = currentResourceEvent.details
173
174     var _oldAccumulatingAmount = getChargingData(
175       chargingData,
176       EnvKeys.ResourceInstanceAccumulatingAmount
177     ).getOrElse(getResourceInstanceInitialAmount)
178
179     var _oldTotalCredits = totalCredits
180
181     var _newAccumulatingAmount = this.computeNewAccumulatingAmount(_oldAccumulatingAmount, currentValue, currentDetails)
182     setChargingData(chargingData, EnvKeys.ResourceInstanceAccumulatingAmount, _newAccumulatingAmount)
183
184     val policyByTimeslot = policyStore.loadAndSortPoliciesWithin(
185       referenceTimeslot.from.getTime,
186       referenceTimeslot.to.getTime
187     )
188
189     val initialChargeslots = TimeslotComputations.computeInitialChargeslots(
190       referenceTimeslot,
191       resourceType,
192       policyByTimeslot,
193       agreementByTimeslot
194     )
195
196     val fullChargeslots = initialChargeslots.map {
197       case chargeslot@Chargeslot(startMillis, stopMillis, unitPrice, _) ⇒
198         val timeDeltaMillis = stopMillis - startMillis
199
200         val creditsToSubtract = this.computeCreditsToSubtract(
201           _oldTotalCredits,       // FIXME ??? Should recalculate ???
202           _oldAccumulatingAmount, // FIXME ??? Should recalculate ???
203           _newAccumulatingAmount, // FIXME ??? Should recalculate ???
204           timeDeltaMillis,
205           previousValue,
206           currentValue,
207           unitPrice,
208           currentDetails
209         )
210
211         val newChargeslot = chargeslot.copyWithCreditsToSubtract(creditsToSubtract)
212         newChargeslot
213     }
214
215     if(fullChargeslots.length == 0) {
216       throw new AquariumInternalError("No chargeslots computed for resource event %s".format(currentResourceEvent.id))
217     }
218
219     val sumOfCreditsToSubtract = fullChargeslots.map(_.creditsToSubtract).sum
220     val newTotalCredits = _oldTotalCredits - sumOfCreditsToSubtract
221
222     val newWalletEntry = WalletEntry(
223       userID,
224       sumOfCreditsToSubtract,
225       _oldTotalCredits,
226       newTotalCredits,
227       TimeHelpers.nowMillis(),
228       referenceTimeslot,
229       billingMonthInfo.year,
230       billingMonthInfo.month,
231       previousResourceEventOpt.map(List(_, currentResourceEvent)).getOrElse(List(currentResourceEvent)),
232       fullChargeslots,
233       resourceType,
234       currentResourceEvent.isSynthetic
235     )
236
237     walletEntryRecorder.apply(newWalletEntry)
238
239     (1, newTotalCredits)
240   }
241
242   protected def removeChargingData[T: Manifest](
243       chargingData: mutable.Map[String, Any],
244       envKey: TypedKey[T]
245   ) = {
246
247     chargingData.remove(envKey.name).asInstanceOf[Option[T]]
248   }
249
250   protected def getChargingData[T: Manifest](
251       chargingData: mutable.Map[String, Any],
252       envKey: TypedKey[T]
253   ) = {
254
255     chargingData.get(envKey.name).asInstanceOf[Option[T]]
256   }
257
258   protected def setChargingData[T: Manifest](
259       chargingData: mutable.Map[String, Any],
260       envKey: TypedKey[T],
261       value: T
262   ) = {
263
264     chargingData(envKey.name) = value
265   }
266
267   /**
268    *
269    * @param aquarium
270    * @param currentResourceEvent
271    * @param resourceType
272    * @param billingMonthInfo
273    * @param userAgreements
274    * @param chargingData
275    * @param totalCredits
276    * @param walletEntryRecorder
277    * @param clogOpt
278    * @return The number of wallet entries recorded and the new total credits
279    */
280   protected def genericChargeResourceEvent(
281       aquarium: Aquarium,
282       currentResourceEvent: ResourceEventModel,
283       resourceType: ResourceType,
284       billingMonthInfo: BillingMonthInfo,
285       userAgreements: AgreementHistory,
286       chargingData: mutable.Map[String, Any],
287       totalCredits: Double,
288       walletEntryRecorder: WalletEntry ⇒ Unit,
289       clogOpt: Option[ContextualLogger] = None
290   ): (Int, Double) = {
291     import ChargingBehavior.EnvKeys
292
293     val clog = ContextualLogger.fromOther(clogOpt, logger, "chargeResourceEvent(%s)", currentResourceEvent.id)
294     val currentResourceEventDebugInfo = rcDebugInfo(currentResourceEvent)
295
296     val isBillable = this.isBillableEvent(currentResourceEvent)
297     val retval = if(!isBillable) {
298       // The resource event is not billable.
299       clog.debug("Ignoring not billable %s", currentResourceEventDebugInfo)
300       (0, totalCredits)
301     } else {
302       // The resource event is billable.
303       // Find the previous event if needed.
304       // This is (potentially) needed to calculate new credit amount and new resource instance amount
305       if(this.needsPreviousEventForCreditAndAmountCalculation) {
306         val previousResourceEventOpt = removeChargingData(chargingData, EnvKeys.PreviousEvent)
307
308         if(previousResourceEventOpt.isDefined) {
309           val previousResourceEvent = previousResourceEventOpt.get
310           val previousValue = previousResourceEvent.value
311
312           computeChargeslots(
313             chargingData,
314             previousResourceEventOpt,
315             currentResourceEvent,
316             billingMonthInfo,
317             Timeslot(previousResourceEvent.occurredMillis, currentResourceEvent.occurredMillis),
318             resourceType,
319             userAgreements.agreementByTimeslot,
320             previousValue,
321             totalCredits,
322             aquarium.policyStore,
323             walletEntryRecorder
324           )
325         } else {
326           // We do not have the needed previous event, so this must be the first resource event of its kind, ever.
327           // Let's see if we can create a dummy previous event.
328           val actualFirstEvent = currentResourceEvent
329
330           if(this.isBillableFirstEvent(actualFirstEvent) && this.mustGenerateDummyFirstEvent) {
331             clog.debug("First event of its kind %s", currentResourceEventDebugInfo)
332
333             val dummyFirst = this.constructDummyFirstEventFor(currentResourceEvent, billingMonthInfo.monthStartMillis)
334             clog.debug("Dummy first event %s", rcDebugInfo(dummyFirst))
335
336             val previousResourceEvent = dummyFirst
337             val previousValue = previousResourceEvent.value
338
339             computeChargeslots(
340               chargingData,
341               Some(previousResourceEvent),
342               currentResourceEvent,
343               billingMonthInfo,
344               Timeslot(previousResourceEvent.occurredMillis, currentResourceEvent.occurredMillis),
345               resourceType,
346               userAgreements.agreementByTimeslot,
347               previousValue,
348               totalCredits,
349               aquarium.policyStore,
350               walletEntryRecorder
351             )
352           } else {
353             clog.debug("Ignoring first event of its kind %s", currentResourceEventDebugInfo)
354             // userStateWorker.updateIgnored(currentResourceEvent)
355             (0, totalCredits)
356           }
357         }
358       } else {
359         // No need for previous event. One event does it all.
360         computeChargeslots(
361           chargingData,
362           None,
363           currentResourceEvent,
364           billingMonthInfo,
365           Timeslot(currentResourceEvent.occurredMillis, currentResourceEvent.occurredMillis + 1),
366           resourceType,
367           userAgreements.agreementByTimeslot,
368           this.getResourceInstanceUndefinedAmount,
369           totalCredits,
370           aquarium.policyStore,
371           walletEntryRecorder
372         )
373       }
374     }
375
376     // After processing, all events billable or not update the previous state
377     setChargingData(chargingData, EnvKeys.PreviousEvent, currentResourceEvent)
378
379     retval
380   }
381
382   /**
383    * Generate a map where the key is a [[gr.grnet.aquarium.charging.ChargingInput]]
384    * and the value the respective value. This map will be used to do the actual credit charge calculation
385    * by the respective algorithm.
386    *
387    * Values are obtained from a corresponding context, which is provided by the parameters. We assume that this context
388    * has been validated before the call to `makeValueMap` is made.
389    *
390    * @param totalCredits   the value for [[gr.grnet.aquarium.charging.TotalCreditsInput.]]
391    * @param oldTotalAmount the value for [[gr.grnet.aquarium.charging.OldTotalAmountInput]]
392    * @param newTotalAmount the value for [[gr.grnet.aquarium.charging.NewTotalAmountInput]]
393    * @param timeDelta      the value for [[gr.grnet.aquarium.charging.TimeDeltaInput]]
394    * @param previousValue  the value for [[gr.grnet.aquarium.charging.PreviousValueInput]]
395    * @param currentValue   the value for [[gr.grnet.aquarium.charging.CurrentValueInput]]
396    * @param unitPrice      the value for [[gr.grnet.aquarium.charging.UnitPriceInput]]
397    *
398    * @return a map from [[gr.grnet.aquarium.charging.ChargingInput]]s to respective values.
399    */
400   def makeValueMap(
401       totalCredits: Double,
402       oldTotalAmount: Double,
403       newTotalAmount: Double,
404       timeDelta: Double,
405       previousValue: Double,
406       currentValue: Double,
407       unitPrice: Double
408   ): Map[ChargingInput, Any] = {
409
410     ChargingBehavior.makeValueMapFor(
411       this,
412       totalCredits,
413       oldTotalAmount,
414       newTotalAmount,
415       timeDelta,
416       previousValue,
417       currentValue,
418       unitPrice)
419   }
420
421   def needsPreviousEventForCreditAndAmountCalculation: Boolean = {
422     // If we need any variable that is related to the previous event
423     // then we do need a previous event
424     inputs.exists(_.isDirectlyRelatedToPreviousEvent)
425   }
426
427   /**
428    * Given the old amount of a resource instance, the value arriving in a new resource event and the new details,
429    * compute the new instance amount.
430    *
431    * Note that the `oldAmount` does not make sense for all types of [[gr.grnet.aquarium.charging.ChargingBehavior]],
432    * in which case it is ignored.
433    *
434    * @param oldAccumulatingAmount     the old accumulating amount
435    * @param newEventValue the value contained in a newly arrived
436    *                      [[gr.grnet.aquarium.event.model.resource.ResourceEventModel]]
437    * @param newDetails       the `details` of the newly arrived
438    *                      [[gr.grnet.aquarium.event.model.resource.ResourceEventModel]]
439    * @return
440    */
441   def computeNewAccumulatingAmount(
442       oldAccumulatingAmount: Double,
443       newEventValue: Double,
444       newDetails: Map[String, String]
445   ): Double
446
447   /**
448    * The initial amount.
449    */
450   def getResourceInstanceInitialAmount: Double
451
452   /**
453    * The amount used when no amount is meant to be relevant.
454    *
455    * For example, when there is no need for a previous event but an API requires the amount of the previous event.
456    *
457    * Normally, this value will never be used by client code (= charge computation code).
458    */
459   def getResourceInstanceUndefinedAmount: Double = Double.NaN
460
461   /**
462    * An event carries enough info to characterize it as billable or not.
463    *
464    * Typically all events are billable by default and indeed this is the default implementation
465    * provided here.
466    *
467    * The only exception to the rule is ON events for [[gr.grnet.aquarium.charging.OnOffChargingBehavior]].
468    */
469   def isBillableEvent(event: ResourceEventModel): Boolean = false
470
471   /**
472    * This is called when we have the very first event for a particular resource instance, and we want to know
473    * if it is billable or not.
474    */
475   def isBillableFirstEvent(event: ResourceEventModel): Boolean
476
477   def mustGenerateDummyFirstEvent: Boolean
478
479   def dummyFirstEventValue: Double = 0.0 // FIXME read from configuration
480
481   def constructDummyFirstEventFor(actualFirst: ResourceEventModel, newOccurredMillis: Long): ResourceEventModel = {
482     if(!mustGenerateDummyFirstEvent) {
483       throw new AquariumException("constructDummyFirstEventFor() Not compliant with %s".format(this))
484     }
485
486     val newDetails = Map(
487       ResourceEventModel.Names.details_aquarium_is_synthetic   -> "true",
488       ResourceEventModel.Names.details_aquarium_is_dummy_first -> "true",
489       ResourceEventModel.Names.details_aquarium_reference_event_id -> actualFirst.id,
490       ResourceEventModel.Names.details_aquarium_reference_event_id_in_store -> actualFirst.stringIDInStoreOrEmpty
491     )
492
493     actualFirst.withDetailsAndValue(newDetails, dummyFirstEventValue, newOccurredMillis)
494   }
495
496   /**
497    * There are resources (cost policies) for which implicit events must be generated at the end of the billing period
498    * and also at the beginning of the next one. For these cases, this method must return `true`.
499    *
500    * The motivating example comes from the [[gr.grnet.aquarium.charging.OnOffChargingBehavior]] for which we
501    * must implicitly assume `OFF` events at the end of the billing period and `ON` events at the beginning of the next
502    * one.
503    *
504    */
505   def supportsImplicitEvents: Boolean
506
507   def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel): Boolean
508
509   @throws(classOf[Exception])
510   def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, newOccurredMillis: Long): ResourceEventModel
511 }
512
513 object ChargingBehavior {
514   final case class ChargingBehaviorKey[T: Manifest](override val name: String) extends TypedKeySkeleton[T](name)
515
516   /**
517    * Keys used to save information between calls of `chargeResourceEvent`
518    */
519   object EnvKeys {
520     final val PreviousEvent = ChargingBehaviorKey[ResourceEventModel]("previous.event")
521
522     final val ResourceInstanceAccumulatingAmount = ChargingBehaviorKey[Double]("resource.instance.accumulating.amount")
523   }
524
525   def makeValueMapFor(
526       chargingBehavior: ChargingBehavior,
527       totalCredits: Double,
528       oldTotalAmount: Double,
529       newTotalAmount: Double,
530       timeDelta: Double,
531       previousValue: Double,
532       currentValue: Double,
533       unitPrice: Double
534   ): Map[ChargingInput, Any] = {
535     
536     val inputs = chargingBehavior.inputs
537     var map = Map[ChargingInput, Any]()
538
539     if(inputs contains ChargingBehaviorNameInput) map += ChargingBehaviorNameInput -> chargingBehavior.alias
540     if(inputs contains TotalCreditsInput        ) map += TotalCreditsInput   -> totalCredits
541     if(inputs contains OldTotalAmountInput      ) map += OldTotalAmountInput -> oldTotalAmount
542     if(inputs contains NewTotalAmountInput      ) map += NewTotalAmountInput -> newTotalAmount
543     if(inputs contains TimeDeltaInput           ) map += TimeDeltaInput      -> timeDelta
544     if(inputs contains PreviousValueInput       ) map += PreviousValueInput  -> previousValue
545     if(inputs contains CurrentValueInput        ) map += CurrentValueInput   -> currentValue
546     if(inputs contains UnitPriceInput           ) map += UnitPriceInput      -> unitPrice
547
548     map
549   }
550 }