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