c628015fa542c72c3ce18ec507395c7c73182638
[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[List[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             println("referenceTimeslot = %s".format(referenceTimeslot.toISODateString))
200             // all policies within the interval from previous to current resource event
201             val relevantPolicies = Policy.policies(referenceTimeslot)
202
203             (referenceTimeslot, relevantPolicies, previousResourceEvent.value)
204
205           // We do not have a previous event
206           case NoVal ⇒
207             throw new Exception(
208               "Unable to charge. No previous event given for %s".
209                 format(currentResourceEvent.toDebugString(defaultResourceMap)))
210
211           // We could not obtain a previous event
212           case failed @ Failed(e, m) ⇒
213             throw new Exception(
214               "Unable to charge. Could not obtain previous event for %s".
215                 format(currentResourceEvent.toDebugString(defaultResourceMap)))
216         }
217         
218       // We do not need a previous event
219       case false ⇒
220         // ... so we cannot compute timedelta from a previous event, there is just one chargeslot
221         // referring to (almost) an instant in time
222         val referenceTimeslot = Timeslot(new MutableDateCalc(occurredDate).goPreviousMilli.toDate, occurredDate)
223         val relevantPolicy = Policy.policy(occurredDate)
224         val relevantPolicies = Map(referenceTimeslot -> relevantPolicy)
225
226         (referenceTimeslot, relevantPolicies, costPolicy.getResourceInstanceUndefinedAmount)
227     }
228
229     val initialChargeslotsM = computeInitialChargeslots(
230       referenceTimeslot,
231       dslResource,
232       relevantPolicies,
233       agreementNamesByTimeslot
234     )
235     
236     val fullChargeslotsM = initialChargeslotsM.map { chargeslots ⇒
237       chargeslots.map {
238         case chargeslot @ Chargeslot(startMillis, stopMillis, algorithmDefinition, unitPrice, _) ⇒
239           val execAlgorithmM = algorithmCompiler.compile(algorithmDefinition)
240           execAlgorithmM match {
241             case NoVal ⇒
242               throw new Exception("Could not compile algorithm %s".format(algorithmDefinition))
243
244             case failed @ Failed(e, m) ⇒
245               throw new Exception(m, e)
246
247             case Just(execAlgorithm) ⇒
248               val valueMap = costPolicy.makeValueMap(
249                 costPolicy.name,
250                 oldCredits,
251                 oldTotalAmount,
252                 newTotalAmount,
253                 stopMillis - startMillis,
254                 previousValue,
255                 currentResourceEvent.value,
256                 unitPrice
257               )
258
259               // This is it
260               val creditsM = execAlgorithm.apply(valueMap)
261
262               creditsM match {
263                 case NoVal ⇒
264                   throw new Exception(
265                     "Could not compute credits for resource %s during %s".
266                       format(dslResource.name, Timeslot(new Date(startMillis), new Date(stopMillis))))
267
268                 case failed @ Failed(e, m) ⇒
269                   throw new Exception(m, e)
270
271                 case Just(credits) ⇒
272                   chargeslot.copy(computedCredits = Some(credits))
273               }
274           }
275       }
276     }
277
278     fullChargeslotsM match {
279       case Just(fullChargeslots) ⇒
280         fullChargeslots
281       case NoVal ⇒
282         null
283       case failed @ Failed(e, m) ⇒
284         throw new Exception(m, e)
285     }
286   }
287
288   /**
289    * Create a list of wallet entries by charging for a resource event.
290    *
291    * @param currentResourceEvent The resource event to create charges for
292    * @param agreements The user's agreement names, indexed by their
293    *                   applicability timeslot
294    * @param previousAmount The current state of the resource
295    * @param previousOccurred The last time the resource state was updated
296    */
297   def chargeEvent(currentResourceEvent: ResourceEvent,
298                   agreements: SortedMap[Timeslot, String],
299                   previousAmount: Double,
300                   previousOccurred: Date,
301                   related: List[WalletEntry]): Maybe[List[WalletEntry]] = {
302
303     assert(previousOccurred.getTime <= currentResourceEvent.occurredMillis)
304     val occuredDate = new Date(currentResourceEvent.occurredMillis)
305
306     /* The following makes sure that agreements exist between the start
307      * and end days of the processed event. As agreement updates are
308      * guaranteed not to leave gaps, this means that the event can be
309      * processed correctly, as at least one agreement will be valid
310      * throughout the event's life.
311      */
312     assert(
313       agreements.keysIterator.exists {
314         p => p.includes(occuredDate)
315       } && agreements.keysIterator.exists {
316         p => p.includes(previousOccurred)
317       }
318     )
319
320     val t = Timeslot(previousOccurred, occuredDate)
321
322     // Align policy and agreement validity timeslots to the event's boundaries
323     val policyTimeslots = t.align(
324       Policy.policies(previousOccurred, occuredDate).keysIterator.toList)
325     val agreementTimeslots = t.align(agreements.keysIterator.toList)
326
327     /*
328      * Get a set of timeslot slices covering the different durations of
329      * agreements and policies.
330      */
331     val aligned = alignTimeslots(policyTimeslots, agreementTimeslots)
332
333     val walletEntries = aligned.map {
334       x =>
335         // Retrieve agreement from the policy valid at time of event
336         val agreementName = agreements.find(y => y._1.contains(x)) match {
337           case Some(x) => x
338           case None => return Failed(new AccountingException(("Cannot find" +
339             " user agreement for period %s").format(x)))
340         }
341
342         // Do the wallet entry calculation
343         val entries = chargeEvent(
344           currentResourceEvent,
345           Policy.policy(x.from).findAgreement(agreementName._2).getOrElse(
346             return Failed(new AccountingException("Cannot get agreement for %s".format()))
347           ),
348           previousAmount,
349           previousOccurred,
350           related,
351           Some(x)
352         ) match {
353           case Just(x) => x
354           case Failed(f, e) => return Failed(f,e)
355           case NoVal => List()
356         }
357         entries
358     }.flatten
359
360     Just(walletEntries)
361   }
362
363   /**
364    * Creates a list of wallet entries by applying the agreement provisions on
365    * the resource state.
366    *
367    * @param event The resource event to create charges for
368    * @param agr The agreement implementation to use
369    * @param previousAmount The current state of the resource
370    * @param previousOccurred The timestamp of the previous event
371    * @param related Related wallet entries (TODO: should remove)
372    * @param chargeFor The duration for which the charge should be done.
373    *                  Should fall between the previous and current
374    *                  resource event boundaries
375    * @return A list of wallet entries, one for each
376    */
377   def chargeEvent(event: ResourceEvent,
378                   agr: DSLAgreement,
379                   previousAmount: Double,
380                   previousOccurred: Date,
381                   related: List[WalletEntry],
382                   chargeFor: Option[Timeslot]): Maybe[List[WalletEntry]] = {
383
384     // If chargeFor is not null, make sure it falls within
385     // event time boundaries
386     chargeFor.map{x => assert(true,
387       Timeslot(previousOccurred, new Date(event.occurredMillis)))}
388
389     if (!event.validate())
390       return Failed(new AccountingException("Event not valid"))
391
392     val policy = Policy.policy
393     val dslResource = policy.findResource(event.resource) match {
394       case Some(x) => x
395       case None => return Failed(
396         new AccountingException("No resource [%s]".format(event.resource)))
397     }
398
399     /* This is a safeguard against the special case where the last
400      * resource state update, as marked by the lastUpdate parameter
401      * is equal to the time of the event occurrence. This means that
402      * this is the first time the resource state has been recorded.
403      * Charging in this case only makes sense for discrete resources.
404      */
405     if (previousOccurred.getTime == event.occurredMillis) {
406       dslResource.costPolicy match {
407         case DiscreteCostPolicy => //Ok
408         case _ => return Some(List())
409       }
410     }
411
412     val creditCalculationValueM = dslResource.costPolicy.getValueForCreditCalculation(Just(previousAmount), event.value)
413     val amount = creditCalculationValueM match {
414       case failed @ Failed(_, _) ⇒
415         return failed
416       case Just(amount) ⇒
417         amount
418       case NoVal ⇒
419         0.0
420     }
421
422     // We don't do strict checking for all cases for OnOffPolicies as
423     // above, since this point won't be reached in case of error.
424     val isFinal = dslResource.costPolicy match {
425       case OnOffCostPolicy =>
426         OnOffPolicyResourceState(previousAmount) match {
427           case OnResourceState => false
428           case OffResourceState => true
429         }
430       case _ => true
431     }
432
433     /*
434      * Get the timeslot for which this event will be charged. In case we
435      * have a discrete resource, we do not really care for the time duration
436      * of an event. To process all events in a uniform way, we create an
437      * artificial timeslot lasting the minimum amount of time. In all other
438      * cases, we first check whether a desired charge period passed as
439      * an argument.
440      */
441     val timeslot = dslResource.costPolicy match {
442       case DiscreteCostPolicy => Timeslot(new Date(event.occurredMillis - 1),
443         new Date(event.occurredMillis))
444       case _ => chargeFor match {
445         case Some(x) => x
446         case None => Timeslot(previousOccurred, new Date(event.occurredMillis))
447       }
448     }
449
450     /*
451      * The following splits the chargable timeslot into smaller timeslots to
452      * comply with different applicability periods for algorithms and
453      * pricelists defined by the provided agreement.
454      */
455     val chargeChunks = calcChangeChunks(agr, amount, dslResource, timeslot)
456
457     val timeReceived = System.currentTimeMillis
458
459     val rel = event.id :: related.map{x => x.sourceEventIDs}.flatten
460
461     val entries = chargeChunks.map { c=>
462         WalletEntry(
463           id = CryptoUtils.sha1(c.id),
464           occurredMillis = event.occurredMillis,
465           receivedMillis = timeReceived,
466           sourceEventIDs = rel,
467           value = c.cost,
468           reason = c.reason,
469           userId = event.userId,
470           resource = event.resource,
471           instanceId = event.instanceId,
472           finalized = isFinal
473         )
474     }
475     Just(entries)
476   }
477
478   /**
479    * Create a
480    */
481   def calcChangeChunks(agr: DSLAgreement, volume: Double,
482                        res: DSLResource, t: Timeslot): List[ChargeChunk] = {
483
484     val alg = resolveEffectiveAlgorithmsForTimeslot(t, agr)
485     val pri = resolveEffectivePricelistsForTimeslot(t, agr)
486     val chunks = splitChargeChunks(alg, pri)
487     val algChunked = chunks._1
488     val priChunked = chunks._2
489
490     assert(algChunked.size == priChunked.size)
491
492     res.costPolicy match {
493       case DiscreteCostPolicy => calcChargeChunksDiscrete(algChunked, priChunked, volume, res)
494       case _ => calcChargeChunksContinuous(algChunked, priChunked, volume, res)
495     }
496   }
497
498   /**
499    * Get a list of charge chunks for discrete resources.
500    */
501   private[logic]
502   def calcChargeChunksDiscrete(algChunked: Map[Timeslot, DSLAlgorithm],
503                                priChunked: Map[Timeslot, DSLPriceList],
504                                volume: Double, res: DSLResource): List[ChargeChunk] = {
505     // In case of descrete resources, we only a expect a
506     assert(algChunked.size == 1)
507     assert(priChunked.size == 1)
508     assert(algChunked.keySet.head.compare(priChunked.keySet.head) == 0)
509
510     List(ChargeChunk(volume,
511       algChunked.valuesIterator.next.algorithms.getOrElse(res, ""),
512       priChunked.valuesIterator.next.prices.getOrElse(res, 0),
513       algChunked.keySet.head, res))
514   }
515
516   /**
517    * Get a list of charge chunks for continuous resources.
518    */
519   private[logic]
520   def calcChargeChunksContinuous(algChunked: Map[Timeslot, DSLAlgorithm],
521                                  priChunked: Map[Timeslot, DSLPriceList],
522                                  volume: Double, res: DSLResource): List[ChargeChunk] = {
523     algChunked.keysIterator.map {
524       x =>
525         ChargeChunk(volume,
526           algChunked.get(x).get.algorithms.getOrElse(res, ""),
527           priChunked.get(x).get.prices.getOrElse(res, 0), x, res)
528     }.toList
529   }
530
531   /**
532    * Align charge timeslots between algorithms and pricelists. As algorithm
533    * and pricelists can have different effectivity periods, this method
534    * examines them and splits them as necessary.
535    */
536   private[logic] def splitChargeChunks(alg: SortedMap[Timeslot, DSLAlgorithm],
537                                        price: SortedMap[Timeslot, DSLPriceList]) :
538     (Map[Timeslot, DSLAlgorithm], Map[Timeslot, DSLPriceList]) = {
539
540     val zipped = alg.keySet.zip(price.keySet)
541
542     zipped.find(p => !p._1.equals(p._2)) match {
543       case None => (alg, price)
544       case Some(x) =>
545         val algTimeslot = x._1
546         val priTimeslot = x._2
547
548         assert(algTimeslot.from == priTimeslot.from)
549
550         if (algTimeslot.endsAfter(priTimeslot)) {
551           val slices = algTimeslot.slice(priTimeslot.to)
552           val algo = alg.get(algTimeslot).get
553           val newalg = alg - algTimeslot ++ Map(slices.apply(0) -> algo) ++ Map(slices.apply(1) -> algo)
554           splitChargeChunks(newalg, price)
555         }
556         else {
557           val slices = priTimeslot.slice(priTimeslot.to)
558           val pl = price.get(priTimeslot).get
559           val newPrice = price - priTimeslot ++ Map(slices.apply(0) -> pl) ++ Map(slices.apply(1) -> pl)
560           splitChargeChunks(alg, newPrice)
561         }
562     }
563   }
564
565   /**
566    * Given two lists of timeslots, produce a list which contains the
567    * set of timeslot slices, as those are defined by
568    * timeslot overlaps.
569    *
570    * For example, given the timeslots a and b below, split them as shown.
571    *
572    * a = |****************|
573    *     ^                ^
574    *   a.from            a.to
575    * b = |*********|
576    *     ^         ^
577    *   b.from     b.to
578    *
579    * result: List(Timeslot(a.from, b.to), Timeslot(b.to, a.to))
580    */
581   private[logic] def alignTimeslots(a: List[Timeslot],
582                                     b: List[Timeslot]): List[Timeslot] = {
583     if (a.isEmpty) return b.tail
584     if (b.isEmpty) return a.tail
585     assert (a.head.from == b.head.from)
586
587     if (a.head.endsAfter(b.head)) {
588       a.head.slice(b.head.to) ::: alignTimeslots(a.tail, b.tail)
589     } else if (b.head.endsAfter(a.head)) {
590       b.head.slice(a.head.to) ::: alignTimeslots(a.tail, b.tail)
591     } else {
592       a.head :: alignTimeslots(a.tail, b.tail)
593     }
594   }
595 }
596
597 /**
598  * Encapsulates a computation for a specific timeslot of
599  * resource usage.
600  */
601 case class ChargeChunk(value: Double, algorithm: String,
602                        price: Double, when: Timeslot,
603                        resource: DSLResource) {
604   assert(value > 0)
605   assert(!algorithm.isEmpty)
606   assert(resource != null)
607
608   def cost(): Double =
609     //TODO: Apply the algorithm, when we start parsing it
610     resource.costPolicy match {
611       case DiscreteCostPolicy =>
612         value * price
613       case _ =>
614         value * price * when.hours
615     }
616
617   def reason(): String =
618     resource.costPolicy match {
619       case DiscreteCostPolicy =>
620         "%f %s at %s @ %f/%s".format(value, resource.unit, when.from, price,
621           resource.unit)
622       case ContinuousCostPolicy =>
623         "%f %s of %s from %s to %s @ %f/%s".format(value, resource.unit,
624           resource.name, when.from, when.to, price, resource.unit)
625       case OnOffCostPolicy =>
626         "%f %s of %s from %s to %s @ %f/%s".format(when.hours, resource.unit,
627           resource.name, when.from, when.to, price, resource.unit)
628     }
629
630   def id(): String =
631     CryptoUtils.sha1("%f%s%f%s%s%d".format(value, algorithm, price, when.toString,
632       resource.name, System.currentTimeMillis()))
633 }
634
635 /** An exception raised when something goes wrong with accounting */
636 class AccountingException(msg: String) extends Exception(msg)