WIP Resource event handling
[aquarium] / src / main / scala / gr / grnet / aquarium / logic / accounting / Accounting.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.logic.accounting
37
38 import algorithm.CostPolicyAlgorithmCompiler
39 import dsl._
40 import collection.immutable.SortedMap
41 import com.ckkloverdos.maybe.{NoVal, Maybe, Just}
42 import gr.grnet.aquarium.util.{ContextualLogger, Loggable}
43 import gr.grnet.aquarium.store.PolicyStore
44 import gr.grnet.aquarium.util.date.MutableDateCalc
45 import gr.grnet.aquarium.{AquariumInternalError, AquariumException}
46 import gr.grnet.aquarium.event.model.resource.ResourceEventModel
47
48 /**
49  * Methods for converting accounting events to wallet entries.
50  *
51  * @author Georgios Gousios <gousiosg@gmail.com>
52  * @author Christos KK Loverdos <loverdos@gmail.com>
53  */
54 trait Accounting extends Loggable {
55   // TODO: favour composition over inheritance until we decide what to do with DSLUtils (and Accounting).
56   protected val dslUtils = new DSLUtils {}
57   /**
58    * Breaks a reference timeslot (e.g. billing period) according to policies and agreements.
59    *
60    * @param referenceTimeslot
61    * @param policyTimeslots
62    * @param agreementTimeslots
63    * @return
64    */
65   protected
66   def splitTimeslotByPoliciesAndAgreements(referenceTimeslot: Timeslot,
67                                            policyTimeslots: List[Timeslot],
68                                            agreementTimeslots: List[Timeslot],
69                                            clogM: Maybe[ContextualLogger] = NoVal): List[Timeslot] = {
70
71 //    val clog = ContextualLogger.fromOther(clogM, logger, "splitTimeslotByPoliciesAndAgreements()")
72 //    clog.begin()
73
74     // Align policy and agreement validity timeslots to the referenceTimeslot
75     val alignedPolicyTimeslots    = referenceTimeslot.align(policyTimeslots)
76     val alignedAgreementTimeslots = referenceTimeslot.align(agreementTimeslots)
77
78 //    clog.debug("referenceTimeslot = %s", referenceTimeslot)
79 //    clog.debugSeq("alignedPolicyTimeslots", alignedPolicyTimeslots, 0)
80 //    clog.debugSeq("alignedAgreementTimeslots", alignedAgreementTimeslots, 0)
81
82     val result = alignTimeslots(alignedPolicyTimeslots, alignedAgreementTimeslots)
83 //    clog.debugSeq("result", result, 1)
84 //    clog.end()
85     result
86   }
87
88   /**
89    * Given a reference timeslot, we have to break it up to a series of timeslots where a particular
90    * algorithm and price unit is in effect.
91    *
92    */
93   protected
94   def resolveEffectiveAlgorithmsAndPriceLists(alignedTimeslot: Timeslot,
95                                               agreement: DSLAgreement,
96                                               clogOpt: Option[ContextualLogger] = None):
97           (Map[Timeslot, DSLAlgorithm], Map[Timeslot, DSLPriceList]) = {
98
99     val clog = ContextualLogger.fromOther(clogOpt, logger, "resolveEffectiveAlgorithmsAndPriceLists()")
100
101     // Note that most of the code is taken from calcChangeChunks()
102     val alg = dslUtils.resolveEffectiveAlgorithmsForTimeslot(alignedTimeslot, agreement)
103     val pri = dslUtils.resolveEffectivePricelistsForTimeslot(alignedTimeslot, agreement)
104     val chargeChunks = splitChargeChunks(alg, pri)
105     val algorithmByTimeslot = chargeChunks._1
106     val pricelistByTimeslot = chargeChunks._2
107
108     assert(algorithmByTimeslot.size == pricelistByTimeslot.size)
109
110     (algorithmByTimeslot, pricelistByTimeslot)
111   }
112
113   protected
114   def computeInitialChargeslots(referenceTimeslot: Timeslot,
115                                 dslResource: DSLResource,
116                                 policiesByTimeslot: Map[Timeslot, DSLPolicy],
117                                 agreementNamesByTimeslot: Map[Timeslot, String],
118                                 clogOpt: Option[ContextualLogger] = None): List[Chargeslot] = {
119
120     val clog = ContextualLogger.fromOther(clogOpt, logger, "computeInitialChargeslots()")
121 //    clog.begin()
122
123     val policyTimeslots = policiesByTimeslot.keySet
124     val agreementTimeslots = agreementNamesByTimeslot.keySet
125
126 //    clog.debugMap("policiesByTimeslot", policiesByTimeslot, 1)
127 //    clog.debugMap("agreementNamesByTimeslot", agreementNamesByTimeslot, 1)
128
129     def getPolicy(ts: Timeslot): DSLPolicy = {
130       policiesByTimeslot.find(_._1.contains(ts)).get._2
131     }
132     def getAgreementName(ts: Timeslot): String = {
133       agreementNamesByTimeslot.find(_._1.contains(ts)).get._2
134     }
135
136     // 1. Round ONE: split time according to overlapping policies and agreements.
137 //    clog.begin("ROUND 1")
138     val alignedTimeslots = splitTimeslotByPoliciesAndAgreements(referenceTimeslot, policyTimeslots.toList, agreementTimeslots.toList, Just(clog))
139 //    clog.debugSeq("alignedTimeslots", alignedTimeslots, 1)
140 //    clog.end("ROUND 1")
141
142     // 2. Round TWO: Use the aligned timeslots of Round ONE to produce even more
143     //    fine-grained timeslots according to applicable algorithms.
144     //    Then pack the info into charge slots.
145 //    clog.begin("ROUND 2")
146     val allChargeslots = for {
147       alignedTimeslot <- alignedTimeslots
148     } yield {
149 //      val alignedTimeslotMsg = "alignedTimeslot = %s".format(alignedTimeslot)
150 //      clog.begin(alignedTimeslotMsg)
151
152       val dslPolicy = getPolicy(alignedTimeslot)
153 //      clog.debug("dslPolicy = %s", dslPolicy)
154       val agreementName = getAgreementName(alignedTimeslot)
155 //      clog.debug("agreementName = %s", agreementName)
156       val agreementOpt = dslPolicy.findAgreement(agreementName)
157 //      clog.debug("agreementOpt = %s", agreementOpt)
158
159       agreementOpt match {
160         case None ⇒
161           val errMsg = "Unknown agreement %s during %s".format(agreementName, alignedTimeslot)
162           clog.error("%s", errMsg)
163           throw new AquariumException(errMsg)
164
165         case Some(agreement) ⇒
166           // TODO: Factor this out, just like we did with:
167           // TODO:  val alignedTimeslots = splitTimeslotByPoliciesAndAgreements
168           // Note that most of the code is already taken from calcChangeChunks()
169           val r = resolveEffectiveAlgorithmsAndPriceLists(alignedTimeslot, agreement, Some(clog))
170           val algorithmByTimeslot: Map[Timeslot, DSLAlgorithm] = r._1
171           val pricelistByTimeslot: Map[Timeslot, DSLPriceList] = r._2
172
173           // Now, the timeslots must be the same
174           val finegrainedTimeslots = algorithmByTimeslot.keySet
175
176           val chargeslots = for {
177             finegrainedTimeslot <- finegrainedTimeslots
178           } yield {
179 //            val finegrainedTimeslotMsg = "finegrainedTimeslot = %s".format(finegrainedTimeslot)
180 //            clog.begin(finegrainedTimeslotMsg)
181
182             val dslAlgorithm = algorithmByTimeslot(finegrainedTimeslot) // TODO: is this correct?
183 //            clog.debug("dslAlgorithm = %s", dslAlgorithm)
184 //            clog.debugMap("dslAlgorithm.algorithms", dslAlgorithm.algorithms, 1)
185             val dslPricelist = pricelistByTimeslot(finegrainedTimeslot) // TODO: is this correct?
186 //            clog.debug("dslPricelist = %s", dslPricelist)
187 //            clog.debug("dslResource = %s", dslResource)
188             val algorithmDefOpt = dslAlgorithm.algorithms.get(dslResource)
189 //            clog.debug("algorithmDefOpt = %s", algorithmDefOpt)
190             val priceUnitOpt = dslPricelist.prices.get(dslResource)
191 //            clog.debug("priceUnitOpt = %s", priceUnitOpt)
192
193             val chargeslot = (algorithmDefOpt, priceUnitOpt) match {
194               case (None, None) ⇒
195                 throw new AquariumException(
196                   "Unknown algorithm and price unit for resource %s during %s".
197                     format(dslResource, finegrainedTimeslot))
198               case (None, _) ⇒
199                 throw new AquariumException(
200                   "Unknown algorithm for resource %s during %s".
201                     format(dslResource, finegrainedTimeslot))
202               case (_, None) ⇒
203                 throw new AquariumException(
204                   "Unknown price unit for resource %s during %s".
205                     format(dslResource, finegrainedTimeslot))
206               case (Some(algorithmDefinition), Some(priceUnit)) ⇒
207                 Chargeslot(finegrainedTimeslot.from.getTime, finegrainedTimeslot.to.getTime, algorithmDefinition, priceUnit)
208             }
209
210 //            clog.end(finegrainedTimeslotMsg)
211             chargeslot
212           }
213
214 //          clog.end(alignedTimeslotMsg)
215           chargeslots.toList
216       }
217     }
218 //    clog.end("ROUND 2")
219
220
221     val result = allChargeslots.flatten
222 //    clog.debugSeq("result", allChargeslots, 1)
223 //    clog.end()
224     result
225   }
226
227   /**
228    * Compute the charge slots generated by a particular resource event.
229    *
230    */
231   def computeFullChargeslots(previousResourceEventOpt: Option[ResourceEventModel],
232                              currentResourceEvent: ResourceEventModel,
233                              oldCredits: Double,
234                              oldTotalAmount: Double,
235                              newTotalAmount: Double,
236                              dslResource: DSLResource,
237                              defaultResourceMap: DSLResourcesMap,
238                              agreementNamesByTimeslot: SortedMap[Timeslot, String],
239                              algorithmCompiler: CostPolicyAlgorithmCompiler,
240                              policyStore: PolicyStore,
241                              clogOpt: Option[ContextualLogger] = None): (Timeslot, List[Chargeslot]) = {
242
243     val clog = ContextualLogger.fromOther(clogOpt, logger, "computeFullChargeslots()")
244 //    clog.begin()
245
246     val occurredDate = currentResourceEvent.occurredDate
247     val occurredMillis = currentResourceEvent.occurredMillis
248     val costPolicy = dslResource.costPolicy
249
250     val dsl = new DSL{}
251     val (referenceTimeslot, relevantPolicies, previousValue) = costPolicy.needsPreviousEventForCreditAndAmountCalculation match {
252       // We need a previous event
253       case true ⇒
254         previousResourceEventOpt match {
255           // We have a previous event
256           case Some(previousResourceEvent) ⇒
257 //            clog.debug("Have previous event")
258 //            clog.debug("previousValue = %s", previousResourceEvent.value)
259
260             val referenceTimeslot = Timeslot(previousResourceEvent.occurredDate, occurredDate)
261 //            clog.debug("referenceTimeslot = %s".format(referenceTimeslot))
262
263             // all policies within the interval from previous to current resource event
264 //            clog.debug("Calling policyStore.loadAndSortPoliciesWithin(%s)", referenceTimeslot)
265             val relevantPolicies = policyStore.loadAndSortPoliciesWithin(referenceTimeslot.from.getTime, referenceTimeslot.to.getTime, dsl)
266 //            clog.debugMap("==> relevantPolicies", relevantPolicies, 0)
267
268             (referenceTimeslot, relevantPolicies, previousResourceEvent.value)
269
270           // We do not have a previous event
271           case None ⇒
272             throw new AquariumException(
273               "Unable to charge. No previous event given for %s".
274                 format(currentResourceEvent.toDebugString()))
275         }
276
277       // We do not need a previous event
278       case false ⇒
279         // ... so we cannot compute timedelta from a previous event, there is just one chargeslot
280         // referring to (almost) an instant in time
281 //        clog.debug("DO NOT have previous event")
282         val previousValue = costPolicy.getResourceInstanceUndefinedAmount
283 //        clog.debug("previousValue = costPolicy.getResourceInstanceUndefinedAmount = %s", previousValue)
284
285         val referenceTimeslot = Timeslot(new MutableDateCalc(occurredDate).goPreviousMilli.toDate, occurredDate)
286 //        clog.debug("referenceTimeslot = %s".format(referenceTimeslot))
287
288 //        clog.debug("Calling policyStore.loadValidPolicyEntryAt(%s)", new MutableDateCalc(occurredMillis))
289         val relevantPolicyOpt = policyStore.loadValidPolicyAt(occurredMillis, dsl)
290 //        clog.debug("  ==> relevantPolicyM = %s", relevantPolicyM)
291
292         val relevantPolicies = relevantPolicyOpt match {
293           case Some(relevantPolicy) ⇒
294             Map(referenceTimeslot -> relevantPolicy)
295
296           case None ⇒
297             throw new AquariumInternalError("No relevant policy found for %s".format(referenceTimeslot))
298         }
299
300         (referenceTimeslot, relevantPolicies, previousValue)
301     }
302
303     val initialChargeslots = computeInitialChargeslots(
304       referenceTimeslot,
305       dslResource,
306       relevantPolicies,
307       agreementNamesByTimeslot,
308       Some(clog)
309     )
310
311     val fullChargeslots = initialChargeslots.map {
312       case chargeslot @ Chargeslot(startMillis, stopMillis, algorithmDefinition, unitPrice, _) ⇒
313         val execAlgorithm = algorithmCompiler.compile(algorithmDefinition)
314         val valueMap = costPolicy.makeValueMap(
315           oldCredits,
316           oldTotalAmount,
317           newTotalAmount,
318           stopMillis - startMillis,
319           previousValue,
320           currentResourceEvent.value,
321           unitPrice
322         )
323
324 //              clog.debug("execAlgorithm = %s", execAlgorithm)
325         clog.debugMap("valueMap", valueMap, 1)
326
327         // This is it
328         val credits = execAlgorithm.apply(valueMap)
329         chargeslot.copyWithCredits(credits)
330     }
331
332     val result = referenceTimeslot -> fullChargeslots
333
334     result
335   }
336
337   /**
338    * Align charge timeslots between algorithms and pricelists. As algorithm
339    * and pricelists can have different effectivity periods, this method
340    * examines them and splits them as necessary.
341    */
342   private[logic] def splitChargeChunks(alg: SortedMap[Timeslot, DSLAlgorithm],
343                                        price: SortedMap[Timeslot, DSLPriceList]) :
344     (Map[Timeslot, DSLAlgorithm], Map[Timeslot, DSLPriceList]) = {
345
346     val zipped = alg.keySet.zip(price.keySet)
347
348     zipped.find(p => !p._1.equals(p._2)) match {
349       case None => (alg, price)
350       case Some(x) =>
351         val algTimeslot = x._1
352         val priTimeslot = x._2
353
354         assert(algTimeslot.from == priTimeslot.from)
355
356         if (algTimeslot.endsAfter(priTimeslot)) {
357           val slices = algTimeslot.slice(priTimeslot.to)
358           val algo = alg.get(algTimeslot).get
359           val newalg = alg - algTimeslot ++ Map(slices.apply(0) -> algo) ++ Map(slices.apply(1) -> algo)
360           splitChargeChunks(newalg, price)
361         }
362         else {
363           val slices = priTimeslot.slice(priTimeslot.to)
364           val pl = price.get(priTimeslot).get
365           val newPrice = price - priTimeslot ++ Map(slices.apply(0) -> pl) ++ Map(slices.apply(1) -> pl)
366           splitChargeChunks(alg, newPrice)
367         }
368     }
369   }
370
371   /**
372    * Given two lists of timeslots, produce a list which contains the
373    * set of timeslot slices, as those are defined by
374    * timeslot overlaps.
375    *
376    * For example, given the timeslots a and b below, split them as shown.
377    *
378    * a = |****************|
379    *     ^                ^
380    *   a.from            a.to
381    * b = |*********|
382    *     ^         ^
383    *   b.from     b.to
384    *
385    * result: List(Timeslot(a.from, b.to), Timeslot(b.to, a.to))
386    */
387   private[logic] def alignTimeslots(a: List[Timeslot],
388                                     b: List[Timeslot]): List[Timeslot] = {
389
390     def safeTail(foo: List[Timeslot]) = foo match {
391       case Nil       => List()
392       case x :: Nil  => List()
393       case x :: rest => rest
394     }
395
396     if (a.isEmpty) return b
397     if (b.isEmpty) return a
398
399     assert (a.head.from == b.head.from)
400
401     if (a.head.endsAfter(b.head)) {
402       val slice = a.head.slice(b.head.to)
403       slice.head :: alignTimeslots(slice.last :: a.tail, safeTail(b))
404     } else if (b.head.endsAfter(a.head)) {
405       val slice = b.head.slice(a.head.to)
406       slice.head :: alignTimeslots(safeTail(a), slice.last :: b.tail)
407     } else {
408       a.head :: alignTimeslots(safeTail(a), safeTail(b))
409     }
410   }
411 }