Rename stuff to capture semantics better
[aquarium] / src / main / scala / gr / grnet / aquarium / logic / accounting / Accounting.scala
1 /*
2  * Copyright 2011 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 gr.grnet.aquarium.logic.events.{WalletEntry, ResourceEvent}
41 import collection.immutable.SortedMap
42 import java.util.Date
43 import gr.grnet.aquarium.util.{CryptoUtils, Loggable}
44 import com.ckkloverdos.maybe.{NoVal, Maybe, Failed, Just}
45 import gr.grnet.aquarium.util.date.MutableDateCalc
46
47 /**
48  * A timeslot together with the algorithm and unit price that apply for this particular timeslot.
49  *
50  * @param startMillis
51  * @param stopMillis
52  * @param algorithmDefinition
53  * @param unitPrice
54  * @param computedCredits The computed credits
55  */
56 case class Chargeslot(startMillis: Long,
57                       stopMillis: Long,
58                       algorithmDefinition: String,
59                       unitPrice: Double,
60                       computedCredits: Option[Double] = None)
61
62 /**
63  * Methods for converting accounting events to wallet entries.
64  *
65  * @author Georgios Gousios <gousiosg@gmail.com>
66  * @author Christos KK Loverdos <loverdos@gmail.com>
67  */
68 trait Accounting extends DSLUtils with Loggable {
69   /**
70    * Breaks a reference timeslot (e.g. billing period) according to policies and agreements.
71    *
72    * @param referenceTimeslot
73    * @param policyTimeslots
74    * @param agreementTimeslots
75    * @return
76    */
77   protected
78   def splitTimeslotByPoliciesAndAgreements(referenceTimeslot: Timeslot,
79                                            policyTimeslots: List[Timeslot],
80                                            agreementTimeslots: List[Timeslot]): List[Timeslot] = {
81     // Align policy and agreement validity timeslots to the referenceTimeslot
82     val alignedPolicyTimeslots    = referenceTimeslot.align(policyTimeslots)
83     val alignedAgreementTimeslots = referenceTimeslot.align(agreementTimeslots)
84
85     alignTimeslots(alignedPolicyTimeslots, alignedAgreementTimeslots)
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    * @param referenceTimeslot
93    * @param policiesByTimeslot
94    * @param agreementNamesByTimeslot
95    * @return
96    */
97   protected
98   def computeInitialChargeslots(referenceTimeslot: Timeslot,
99                                 dslResource: DSLResource,
100                                 policiesByTimeslot: Map[Timeslot, DSLPolicy],
101                                 agreementNamesByTimeslot: Map[Timeslot, String]): Maybe[List[Chargeslot]] = Maybe {
102
103     val policyTimeslots = policiesByTimeslot.keySet
104     val agreementTimeslots = agreementNamesByTimeslot.keySet
105
106     def getPolicy(ts: Timeslot): DSLPolicy = {
107       policiesByTimeslot.find(_._1.contains(ts)).get._2
108     }
109     def getAgreementName(ts: Timeslot): String = {
110       agreementNamesByTimeslot.find(_._1.contains(ts)).get._2
111     }
112
113     // 1. Round ONE: split time according to overlapping policies and agreements.
114     val alignedTimeslots = splitTimeslotByPoliciesAndAgreements(referenceTimeslot, policyTimeslots.toList, agreementTimeslots.toList)
115
116     // 2. Round TWO: Use the aligned timeslots of Round ONE to produce even more
117     //    fine-grained timeslots according to applicable algorithms.
118     //    Then pack the info into charge slots.
119     val allChargeslots = for {
120       alignedTimeslot <- alignedTimeslots
121     } yield {
122       val dslPolicy = getPolicy(alignedTimeslot)
123       val agreementName = getAgreementName(alignedTimeslot)
124       val agreementOpt = dslPolicy.findAgreement(agreementName)
125
126       agreementOpt match {
127         case None ⇒
128           throw new Exception("Unknown agreement %s during %s".format(agreementName, alignedTimeslot))
129
130         case Some(agreement) ⇒
131           // TODO: Factor this out, just like we did with:
132           // TODO:  val alignedTimeslots = splitTimeslotByPoliciesAndAgreements
133           // TODO: Note that most of the code is already taken from calcChangeChunks()
134           val alg = resolveEffectiveAlgorithmsForTimeslot(alignedTimeslot, agreement)
135           val pri = resolveEffectivePricelistsForTimeslot(alignedTimeslot, agreement)
136           val chargeChunks = splitChargeChunks(alg, pri)
137           val algorithmByTimeslot = chargeChunks._1
138           val pricelistByTimeslot = chargeChunks._2
139
140           // Now, the timeslots must be the same
141           val finegrainedTimeslots = algorithmByTimeslot.keySet
142
143           val chargeslots = for {
144             finegrainedTimeslot <- finegrainedTimeslots
145           } yield {
146             val dslAlgorithm = algorithmByTimeslot(finegrainedTimeslot) // TODO: is this correct?
147             val dslPricelist = pricelistByTimeslot(finegrainedTimeslot) // TODO: is this correct?
148             val algorithmDefOpt = dslAlgorithm.algorithms.get(dslResource)
149             val priceUnitOpt = dslPricelist.prices.get(dslResource)
150             (algorithmDefOpt, priceUnitOpt) match {
151               case (None, None) ⇒
152                 throw new Exception(
153                   "Unknown algorithm and price unit for resource %s during %s".
154                     format(dslResource.name, finegrainedTimeslot))
155               case (None, _) ⇒
156                 throw new Exception(
157                   "Unknown algorithm for resource %s during %s".
158                     format(dslResource.name, finegrainedTimeslot))
159               case (_, None) ⇒
160                 throw new Exception(
161                   "Unknown price unit for resource %s during %s".
162                     format(dslResource.name, finegrainedTimeslot))
163               case (Some(algorithmDefinition), Some(priceUnit)) ⇒
164                 Chargeslot(finegrainedTimeslot.from.getTime, finegrainedTimeslot.to.getTime, algorithmDefinition, priceUnit)
165             }
166           }
167
168           chargeslots.toList
169       }
170     }
171
172     allChargeslots.flatten
173   }
174
175   /**
176    * Compute the charge slots generated by a particular resource event.
177    *
178    */
179   def computeFullChargeslots(previousResourceEventM: Maybe[ResourceEvent],
180                              currentResourceEvent: ResourceEvent,
181                              oldCredits: Double,
182                              oldTotalAmount: Double,
183                              newTotalAmount: Double,
184                              dslResource: DSLResource,
185                              defaultResourceMap: DSLResourcesMap,
186                              agreementNamesByTimeslot: Map[Timeslot, String],
187                              algorithmCompiler: CostPolicyAlgorithmCompiler): Maybe[Traversable[Chargeslot]] = Maybe {
188
189     val occurredDate = currentResourceEvent.occurredDate
190     val costPolicy = dslResource.costPolicy
191     
192     val (referenceTimeslot, relevantPolicies, previousValue) = costPolicy.needsPreviousEventForCreditAndAmountCalculation match {
193       // We need a previous event
194       case true ⇒
195         previousResourceEventM match {
196           // We have a previous event
197           case Just(previousResourceEvent) ⇒
198             val referenceTimeslot = Timeslot(previousResourceEvent.occurredDate, occurredDate)
199             // all policies within the interval from previous to current resource event
200             val relevantPolicies = Policy.policies(referenceTimeslot)
201
202             (referenceTimeslot, relevantPolicies, previousResourceEvent.value)
203
204           // We do not have a previous event
205           case NoVal ⇒
206             throw new Exception(
207               "Unable to charge. No previous event given for %s".
208                 format(currentResourceEvent.toDebugString(defaultResourceMap)))
209
210           // We could not obtain a previous event
211           case failed @ Failed(e, m) ⇒
212             throw new Exception(
213               "Unable to charge. Could not obtain previous event for %s".
214                 format(currentResourceEvent.toDebugString(defaultResourceMap)))
215         }
216         
217       // We do not need a previous event
218       case false ⇒
219         // ... so we cannot compute timedelta from a previous event, there is just one chargeslot
220         // referring to (almost) an instant in time
221         val referenceTimeslot = Timeslot(new MutableDateCalc(occurredDate).goPreviousMilli.toDate, occurredDate)
222         val relevantPolicy = Policy.policy(occurredDate)
223         val relevantPolicies = Map(referenceTimeslot -> relevantPolicy)
224
225         (referenceTimeslot, relevantPolicies, costPolicy.getResourceInstanceUndefinedAmount)
226     }
227
228     val initialChargeslotsM = computeInitialChargeslots(
229       referenceTimeslot,
230       dslResource,
231       relevantPolicies,
232       agreementNamesByTimeslot
233     )
234     
235     val fullChargeslotsM = initialChargeslotsM.map { chargeslots ⇒
236       chargeslots.map {
237         case chargeslot @ Chargeslot(startMillis, stopMillis, algorithmDefinition, unitPrice, _) ⇒
238           val execAlgorithmM = algorithmCompiler.compile(algorithmDefinition)
239           execAlgorithmM match {
240             case NoVal ⇒
241               throw new Exception("Could not compile algorithm %s".format(algorithmDefinition))
242
243             case failed @ Failed(e, m) ⇒
244               throw new Exception(m, e)
245
246             case Just(execAlgorithm) ⇒
247               val valueMap = costPolicy.makeValueMap(
248                 costPolicy.name,
249                 oldCredits,
250                 oldTotalAmount,
251                 newTotalAmount,
252                 stopMillis - startMillis,
253                 previousValue,
254                 currentResourceEvent.value,
255                 unitPrice
256               )
257
258               // This is it
259               val creditsM = execAlgorithm.apply(valueMap)
260
261               creditsM match {
262                 case NoVal ⇒
263                   throw new Exception(
264                     "Could not compute credits for resource %s during %s".
265                       format(dslResource.name, Timeslot(new Date(startMillis), new Date(stopMillis))))
266
267                 case failed @ Failed(e, m) ⇒
268                   throw new Exception(m, e)
269
270                 case Just(credits) ⇒
271                   chargeslot.copy(computedCredits = Some(credits))
272               }
273           }
274       }
275     }
276
277     fullChargeslotsM match {
278       case Just(fullChargeslots) ⇒
279         fullChargeslots
280       case NoVal ⇒
281         null
282       case failed @ Failed(e, m) ⇒
283         throw new Exception(m, e)
284     }
285   }
286
287   /**
288    * Creates a list of wallet entries by examining the on the resource state.
289    *
290    * @param currentResourceEvent The resource event to create charges for
291    * @param agreements The user's agreement names, indexed by their
292    *                   applicability timeslot
293    * @param previousAmount The current state of the resource
294    * @param previousOccurred The last time the resource state was updated
295    */
296   def chargeEvent(currentResourceEvent: ResourceEvent,
297                   agreements: SortedMap[Timeslot, String],
298                   previousAmount: Double,
299                   previousOccurred: Date,
300                   related: List[WalletEntry]): Maybe[List[WalletEntry]] = {
301
302     assert(previousOccurred.getTime <= currentResourceEvent.occurredMillis)
303     val occuredDate = new Date(currentResourceEvent.occurredMillis)
304
305     /* The following makes sure that agreements exist between the start
306      * and end days of the processed event. As agreement updates are
307      * guaranteed not to leave gaps, this means that the event can be
308      * processed correctly, as at least one agreement will be valid
309      * throughout the event's life.
310      */
311     assert(
312       agreements.keysIterator.exists {
313         p => p.includes(occuredDate)
314       } && agreements.keysIterator.exists {
315         p => p.includes(previousOccurred)
316       }
317     )
318
319     val t = Timeslot(previousOccurred, occuredDate)
320
321     // Align policy and agreement validity timeslots to the event's boundaries
322     val policyTimeslots = t.align(
323       Policy.policies(previousOccurred, occuredDate).keysIterator.toList)
324     val agreementTimeslots = t.align(agreements.keysIterator.toList)
325
326     /*
327      * Get a set of timeslot slices covering the different durations of
328      * agreements and policies.
329      */
330     val aligned = alignTimeslots(policyTimeslots, agreementTimeslots)
331
332     val walletEntries = aligned.map {
333       x =>
334         // Retrieve agreement from policy valid at time of event
335         val agreementName = agreements.find(y => y._1.contains(x)) match {
336           case Some(x) => x
337           case None => return Failed(new AccountingException(("Cannot find" +
338             " user agreement for period %s").format(x)))
339         }
340
341         // Do the wallet entry calculation
342         val entries = chargeEvent(
343           currentResourceEvent,
344           Policy.policy(x.from).findAgreement(agreementName._2).getOrElse(
345             return Failed(new AccountingException("Cannot get agreement for "))
346           ),
347           previousAmount,
348           previousOccurred,
349           related
350         ) match {
351           case Just(x) => x
352           case Failed(f, e) => return Failed(f,e)
353           case NoVal => List()
354         }
355         entries
356     }.flatten
357
358     Just(walletEntries)
359   }
360
361   /**
362    * Creates a list of wallet entries by applying the agreement provisions on
363    * the resource state.
364    *
365    * @param currentResourceEvent The resource event to create charges for
366    * @param agr The agreement implementation to use
367    * @param previousAmount The current state of the resource
368    * @param previousOccurred
369    * @param related
370    * @return
371    */
372   def chargeEvent(currentResourceEvent: ResourceEvent,
373                   agr: DSLAgreement,
374                   previousAmount: Double,
375                   previousOccurred: Date,
376                   related: List[WalletEntry]): Maybe[List[WalletEntry]] = {
377
378     if (!currentResourceEvent.validate())
379       return Failed(new AccountingException("Event not valid"))
380
381     val policy = Policy.policy
382     val dslResource = policy.findResource(currentResourceEvent.resource) match {
383       case Some(x) => x
384       case None => return Failed(new AccountingException("No resource [%s]".format(currentResourceEvent.resource)))
385     }
386
387     /* This is a safeguard against the special case where the last
388      * resource state update, as marked by the lastUpdate parameter
389      * is equal to the time of the event occurrence. This means that
390      * this is the first time the resource state has been recorded.
391      * Charging in this case only makes sense for discrete resources.
392      */
393     if (previousOccurred.getTime == currentResourceEvent.occurredMillis) {
394       dslResource.costPolicy match {
395         case DiscreteCostPolicy => //Ok
396         case _ => return Some(List())
397       }
398     }
399
400     val creditCalculationValueM = dslResource.costPolicy.getValueForCreditCalculation(Just(previousAmount), currentResourceEvent.value)
401     val amount = creditCalculationValueM match {
402       case failed @ Failed(_, _) ⇒
403         return failed
404       case Just(amount) ⇒
405         amount
406       case NoVal ⇒
407         0.0
408     }
409
410     // We don't do strict checking for all cases for OnOffPolicies as
411     // above, since this point won't be reached in case of error.
412     val isFinal = dslResource.costPolicy match {
413       case OnOffCostPolicy =>
414         OnOffPolicyResourceState(previousAmount) match {
415           case OnResourceState => false
416           case OffResourceState => true
417         }
418       case _ => true
419     }
420
421     val timeslot = dslResource.costPolicy match {
422       case DiscreteCostPolicy => Timeslot(new Date(currentResourceEvent.occurredMillis),
423         new Date(currentResourceEvent.occurredMillis + 1))
424       case _ => Timeslot(previousOccurred, new Date(currentResourceEvent.occurredMillis))
425     }
426
427     val chargeChunks = calcChangeChunks(agr, amount, dslResource, timeslot)
428
429     val timeReceived = System.currentTimeMillis
430
431     val rel = related.map{x => x.sourceEventIDs}.flatten ++ List(currentResourceEvent.id)
432
433     val entries = chargeChunks.map { c=>
434         WalletEntry(
435           id = CryptoUtils.sha1(c.id),
436           occurredMillis = currentResourceEvent.occurredMillis,
437           receivedMillis = timeReceived,
438           sourceEventIDs = rel,
439           value = c.cost,
440           reason = c.reason,
441           userId = currentResourceEvent.userId,
442           resource = currentResourceEvent.resource,
443           instanceId = currentResourceEvent.instanceId,
444           finalized = isFinal
445         )
446     }
447     Just(entries)
448   }
449
450   def calcChangeChunks(agr: DSLAgreement, volume: Double,
451                        res: DSLResource, t: Timeslot): List[ChargeChunk] = {
452
453     val alg = resolveEffectiveAlgorithmsForTimeslot(t, agr)
454     val pri = resolveEffectivePricelistsForTimeslot(t, agr)
455     val chunks = splitChargeChunks(alg, pri)
456     val algChunked = chunks._1
457     val priChunked = chunks._2
458
459     assert(algChunked.size == priChunked.size)
460
461     res.costPolicy match {
462       case DiscreteCostPolicy => calcChargeChunksDiscrete(algChunked, priChunked, volume, res)
463       case _ => calcChargeChunksContinuous(algChunked, priChunked, volume, res)
464     }
465   }
466
467   private[logic]
468   def calcChargeChunksDiscrete(algChunked: Map[Timeslot, DSLAlgorithm],
469                                priChunked: Map[Timeslot, DSLPriceList],
470                                volume: Double, res: DSLResource): List[ChargeChunk] = {
471     assert(algChunked.size == 1)
472     assert(priChunked.size == 1)
473     assert(algChunked.keySet.head.compare(priChunked.keySet.head) == 0)
474
475     List(ChargeChunk(volume,
476       algChunked.valuesIterator.next.algorithms.getOrElse(res, ""),
477       priChunked.valuesIterator.next.prices.getOrElse(res, 0),
478       algChunked.keySet.head, res))
479   }
480
481   private[logic]
482   def calcChargeChunksContinuous(algChunked: Map[Timeslot, DSLAlgorithm],
483                                  priChunked: Map[Timeslot, DSLPriceList],
484                                  volume: Double, res: DSLResource): List[ChargeChunk] = {
485     algChunked.keysIterator.map {
486       x =>
487         ChargeChunk(volume,
488           algChunked.get(x).get.algorithms.getOrElse(res, ""),
489           priChunked.get(x).get.prices.getOrElse(res, 0), x, res)
490     }.toList
491   }
492
493   /**
494    * Align charge timeslots between algorithms and pricelists. As algorithm
495    * and pricelists can have different effectivity periods, this method
496    * examines them and splits them as necessary.
497    */
498   private[logic] def splitChargeChunks(alg: SortedMap[Timeslot, DSLAlgorithm],
499                                        price: SortedMap[Timeslot, DSLPriceList]) :
500     (Map[Timeslot, DSLAlgorithm], Map[Timeslot, DSLPriceList]) = {
501
502     val zipped = alg.keySet.zip(price.keySet)
503
504     zipped.find(p => !p._1.equals(p._2)) match {
505       case None => (alg, price)
506       case Some(x) =>
507         val algTimeslot = x._1
508         val priTimeslot = x._2
509
510         assert(algTimeslot.from == priTimeslot.from)
511
512         if (algTimeslot.endsAfter(priTimeslot)) {
513           val slices = algTimeslot.slice(priTimeslot.to)
514           val algo = alg.get(algTimeslot).get
515           val newalg = alg - algTimeslot ++ Map(slices.apply(0) -> algo) ++ Map(slices.apply(1) -> algo)
516           splitChargeChunks(newalg, price)
517         }
518         else {
519           val slices = priTimeslot.slice(priTimeslot.to)
520           val pl = price.get(priTimeslot).get
521           val newPrice = price - priTimeslot ++ Map(slices.apply(0) -> pl) ++ Map(slices.apply(1) -> pl)
522           splitChargeChunks(alg, newPrice)
523         }
524     }
525   }
526
527   /**
528    * Given two lists of timeslots, produce a list which contains the
529    * set of timeslot slices, as those are defined by
530    * timeslot overlaps.
531    *
532    * For example, given the timeslots a and b below, split them as shown.
533    *
534    * a = |****************|
535    *     ^                ^
536    *   a.from            a.to
537    * b = |*********|
538    *     ^         ^
539    *   b.from     b.to
540    *
541    * result: List(Timeslot(a.from, b.to), Timeslot(b.to, a.to))
542    */
543   private[logic] def alignTimeslots(a: List[Timeslot],
544                                     b: List[Timeslot]): List[Timeslot] = {
545     if (a.isEmpty) return b.tail
546     if (b.isEmpty) return a.tail
547     assert (a.head.from == b.head.from)
548
549     if (a.head.endsAfter(b.head)) {
550       a.head.slice(b.head.to) ::: alignTimeslots(a.tail, b.tail)
551     } else if (b.head.endsAfter(a.head)) {
552       b.head.slice(a.head.to) ::: alignTimeslots(a.tail, b.tail)
553     } else {
554       a.head :: alignTimeslots(a.tail, b.tail)
555     }
556   }
557 }
558
559 /**
560  * Encapsulates a computation for a specific timeslot of
561  * resource usage.
562  */
563 case class ChargeChunk(value: Double, algorithm: String,
564                        price: Double, when: Timeslot,
565                        resource: DSLResource) {
566   assert(value > 0)
567   assert(!algorithm.isEmpty)
568   assert(resource != null)
569
570   def cost(): Double =
571     //TODO: Apply the algorithm, when we start parsing it
572     resource.costPolicy match {
573       case DiscreteCostPolicy =>
574         value * price
575       case _ =>
576         value * price * when.hours
577     }
578
579   def reason(): String =
580     resource.costPolicy match {
581       case DiscreteCostPolicy =>
582         "%f %s at %s @ %f/%s".format(value, resource.unit, when.from, price,
583           resource.unit)
584       case ContinuousCostPolicy =>
585         "%f %s of %s from %s to %s @ %f/%s".format(value, resource.unit,
586           resource.name, when.from, when.to, price, resource.unit)
587       case OnOffCostPolicy =>
588         "%f %s of %s from %s to %s @ %f/%s".format(when.hours, resource.unit,
589           resource.name, when.from, when.to, price, resource.unit)
590     }
591
592   def id(): String =
593     CryptoUtils.sha1("%f%s%f%s%s%d".format(value, algorithm, price, when.toString,
594       resource.name, System.currentTimeMillis()))
595 }
596
597 /** An exception raised when something goes wrong with accounting */
598 class AccountingException(msg: String) extends Exception(msg)