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