2 * Copyright 2011-2012 GRNET S.A. All rights reserved.
4 * Redistribution and use in source and binary forms, with or
5 * without modification, are permitted provided that the following
8 * 1. Redistributions of source code must retain the above
9 * copyright notice, this list of conditions and the following
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.
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.
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.
36 package gr.grnet.aquarium.logic.accounting
38 import gr.grnet.aquarium.util.shortClassNameOf
39 import algorithm.CostPolicyAlgorithmCompiler
41 import collection.immutable.SortedMap
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 import gr.grnet.aquarium.AquariumException
48 import gr.grnet.aquarium.events.{WalletEntry, ResourceEvent}
51 * A timeslot together with the algorithm and unit price that apply for this particular timeslot.
55 * @param algorithmDefinition
57 * @param computedCredits The computed credits
59 case class Chargeslot(startMillis: Long,
61 algorithmDefinition: String,
63 computedCredits: Option[Double] = None) {
65 override def toString = "%s(%s, %s, %s, %s, %s)".format(
66 shortClassNameOf(this),
67 new MutableDateCalc(startMillis).toYYYYMMDDHHMMSSSSS,
68 new MutableDateCalc(stopMillis).toYYYYMMDDHHMMSSSSS,
76 * Methods for converting accounting events to wallet entries.
78 * @author Georgios Gousios <gousiosg@gmail.com>
79 * @author Christos KK Loverdos <loverdos@gmail.com>
81 trait Accounting extends DSLUtils with Loggable {
83 * Breaks a reference timeslot (e.g. billing period) according to policies and agreements.
85 * @param referenceTimeslot
86 * @param policyTimeslots
87 * @param agreementTimeslots
91 def splitTimeslotByPoliciesAndAgreements(referenceTimeslot: Timeslot,
92 policyTimeslots: List[Timeslot],
93 agreementTimeslots: List[Timeslot],
94 clogM: Maybe[ContextualLogger] = NoVal): List[Timeslot] = {
96 // val clog = ContextualLogger.fromOther(clogM, logger, "splitTimeslotByPoliciesAndAgreements()")
99 // Align policy and agreement validity timeslots to the referenceTimeslot
100 val alignedPolicyTimeslots = referenceTimeslot.align(policyTimeslots)
101 val alignedAgreementTimeslots = referenceTimeslot.align(agreementTimeslots)
103 // clog.debug("referenceTimeslot = %s", referenceTimeslot)
104 // clog.debugSeq("alignedPolicyTimeslots", alignedPolicyTimeslots, 0)
105 // clog.debugSeq("alignedAgreementTimeslots", alignedAgreementTimeslots, 0)
107 val result = alignTimeslots(alignedPolicyTimeslots, alignedAgreementTimeslots)
108 // clog.debugSeq("result", result, 1)
114 * Given a reference timeslot, we have to break it up to a series of timeslots where a particular
115 * algorithm and price unit is in effect.
119 def resolveEffectiveAlgorithmsAndPriceLists(alignedTimeslot: Timeslot,
120 agreement: DSLAgreement,
121 clogM: Maybe[ContextualLogger] = NoVal): (Map[Timeslot, DSLAlgorithm], Map[Timeslot, DSLPriceList]) = {
123 val clog = ContextualLogger.fromOther(clogM, logger, "resolveEffectiveAlgorithmsAndPriceLists()")
125 // Note that most of the code is taken from calcChangeChunks()
126 val alg = resolveEffectiveAlgorithmsForTimeslot(alignedTimeslot, agreement)
127 val pri = resolveEffectivePricelistsForTimeslot(alignedTimeslot, agreement)
128 val chargeChunks = splitChargeChunks(alg, pri)
129 val algorithmByTimeslot = chargeChunks._1
130 val pricelistByTimeslot = chargeChunks._2
132 assert(algorithmByTimeslot.size == pricelistByTimeslot.size)
134 (algorithmByTimeslot, pricelistByTimeslot)
138 def computeInitialChargeslots(referenceTimeslot: Timeslot,
139 dslResource: DSLResource,
140 policiesByTimeslot: Map[Timeslot, DSLPolicy],
141 agreementNamesByTimeslot: Map[Timeslot, String],
142 contextualLogger: Maybe[ContextualLogger] = NoVal): Maybe[List[Chargeslot]] = Maybe {
144 val clog = ContextualLogger.fromOther(contextualLogger, logger, "computeInitialChargeslots()")
147 val policyTimeslots = policiesByTimeslot.keySet
148 val agreementTimeslots = agreementNamesByTimeslot.keySet
150 // clog.debugMap("policiesByTimeslot", policiesByTimeslot, 1)
151 // clog.debugMap("agreementNamesByTimeslot", agreementNamesByTimeslot, 1)
153 def getPolicy(ts: Timeslot): DSLPolicy = {
154 policiesByTimeslot.find(_._1.contains(ts)).get._2
156 def getAgreementName(ts: Timeslot): String = {
157 agreementNamesByTimeslot.find(_._1.contains(ts)).get._2
160 // 1. Round ONE: split time according to overlapping policies and agreements.
161 // clog.begin("ROUND 1")
162 val alignedTimeslots = splitTimeslotByPoliciesAndAgreements(referenceTimeslot, policyTimeslots.toList, agreementTimeslots.toList, Just(clog))
163 // clog.debugSeq("alignedTimeslots", alignedTimeslots, 1)
164 // clog.end("ROUND 1")
166 // 2. Round TWO: Use the aligned timeslots of Round ONE to produce even more
167 // fine-grained timeslots according to applicable algorithms.
168 // Then pack the info into charge slots.
169 // clog.begin("ROUND 2")
170 val allChargeslots = for {
171 alignedTimeslot <- alignedTimeslots
173 // val alignedTimeslotMsg = "alignedTimeslot = %s".format(alignedTimeslot)
174 // clog.begin(alignedTimeslotMsg)
176 val dslPolicy = getPolicy(alignedTimeslot)
177 // clog.debug("dslPolicy = %s", dslPolicy)
178 val agreementName = getAgreementName(alignedTimeslot)
179 // clog.debug("agreementName = %s", agreementName)
180 val agreementOpt = dslPolicy.findAgreement(agreementName)
181 // clog.debug("agreementOpt = %s", agreementOpt)
185 val errMsg = "Unknown agreement %s during %s".format(agreementName, alignedTimeslot)
186 clog.error("%s", errMsg)
187 throw new Exception(errMsg)
189 case Some(agreement) ⇒
190 // TODO: Factor this out, just like we did with:
191 // TODO: val alignedTimeslots = splitTimeslotByPoliciesAndAgreements
192 // Note that most of the code is already taken from calcChangeChunks()
193 val r = resolveEffectiveAlgorithmsAndPriceLists(alignedTimeslot, agreement, Just(clog))
194 val algorithmByTimeslot: Map[Timeslot, DSLAlgorithm] = r._1
195 val pricelistByTimeslot: Map[Timeslot, DSLPriceList] = r._2
197 // Now, the timeslots must be the same
198 val finegrainedTimeslots = algorithmByTimeslot.keySet
200 val chargeslots = for {
201 finegrainedTimeslot <- finegrainedTimeslots
203 // val finegrainedTimeslotMsg = "finegrainedTimeslot = %s".format(finegrainedTimeslot)
204 // clog.begin(finegrainedTimeslotMsg)
206 val dslAlgorithm = algorithmByTimeslot(finegrainedTimeslot) // TODO: is this correct?
207 // clog.debug("dslAlgorithm = %s", dslAlgorithm)
208 // clog.debugMap("dslAlgorithm.algorithms", dslAlgorithm.algorithms, 1)
209 val dslPricelist = pricelistByTimeslot(finegrainedTimeslot) // TODO: is this correct?
210 // clog.debug("dslPricelist = %s", dslPricelist)
211 // clog.debug("dslResource = %s", dslResource)
212 val algorithmDefOpt = dslAlgorithm.algorithms.get(dslResource)
213 // clog.debug("algorithmDefOpt = %s", algorithmDefOpt)
214 val priceUnitOpt = dslPricelist.prices.get(dslResource)
215 // clog.debug("priceUnitOpt = %s", priceUnitOpt)
217 val chargeslot = (algorithmDefOpt, priceUnitOpt) match {
220 "Unknown algorithm and price unit for resource %s during %s".
221 format(dslResource, finegrainedTimeslot))
224 "Unknown algorithm for resource %s during %s".
225 format(dslResource, finegrainedTimeslot))
228 "Unknown price unit for resource %s during %s".
229 format(dslResource, finegrainedTimeslot))
230 case (Some(algorithmDefinition), Some(priceUnit)) ⇒
231 Chargeslot(finegrainedTimeslot.from.getTime, finegrainedTimeslot.to.getTime, algorithmDefinition, priceUnit)
234 // clog.end(finegrainedTimeslotMsg)
238 // clog.end(alignedTimeslotMsg)
242 // clog.end("ROUND 2")
245 val result = allChargeslots.flatten
246 // clog.debugSeq("result", allChargeslots, 1)
252 * Compute the charge slots generated by a particular resource event.
255 def computeFullChargeslots(previousResourceEventM: Maybe[ResourceEvent],
256 currentResourceEvent: ResourceEvent,
258 oldTotalAmount: Double,
259 newTotalAmount: Double,
260 dslResource: DSLResource,
261 defaultResourceMap: DSLResourcesMap,
262 agreementNamesByTimeslot: Map[Timeslot, String],
263 algorithmCompiler: CostPolicyAlgorithmCompiler,
264 policyStore: PolicyStore,
265 contextualLogger: Maybe[ContextualLogger] = NoVal): Maybe[(Timeslot, List[Chargeslot])] = Maybe {
267 val clog = ContextualLogger.fromOther(contextualLogger, logger, "computeFullChargeslots()")
270 val occurredDate = currentResourceEvent.occurredDate
271 val occurredMillis = currentResourceEvent.occurredMillis
272 val costPolicy = dslResource.costPolicy
275 val (referenceTimeslot, relevantPolicies, previousValue) = costPolicy.needsPreviousEventForCreditAndAmountCalculation match {
276 // We need a previous event
278 previousResourceEventM match {
279 // We have a previous event
280 case Just(previousResourceEvent) ⇒
281 // clog.debug("Have previous event")
282 // clog.debug("previousValue = %s", previousResourceEvent.value)
284 val referenceTimeslot = Timeslot(previousResourceEvent.occurredDate, occurredDate)
285 // clog.debug("referenceTimeslot = %s".format(referenceTimeslot))
287 // all policies within the interval from previous to current resource event
288 // clog.debug("Calling policyStore.loadAndSortPoliciesWithin(%s)", referenceTimeslot)
289 val relevantPolicies = policyStore.loadAndSortPoliciesWithin(referenceTimeslot.from.getTime, referenceTimeslot.to.getTime, dsl)
290 // clog.debugMap("==> relevantPolicies", relevantPolicies, 0)
292 (referenceTimeslot, relevantPolicies, previousResourceEvent.value)
294 // We do not have a previous event
296 throw new AquariumException(
297 "Unable to charge. No previous event given for %s".
298 format(currentResourceEvent.toDebugString()))
300 // We could not obtain a previous event
301 case failed @ Failed(e) ⇒
302 throw new AquariumException(
303 "Unable to charge. Could not obtain previous event for %s".
304 format(currentResourceEvent.toDebugString()), e)
307 // We do not need a previous event
309 // ... so we cannot compute timedelta from a previous event, there is just one chargeslot
310 // referring to (almost) an instant in time
311 // clog.debug("DO NOT have previous event")
312 val previousValue = costPolicy.getResourceInstanceUndefinedAmount
313 // clog.debug("previousValue = costPolicy.getResourceInstanceUndefinedAmount = %s", previousValue)
315 val referenceTimeslot = Timeslot(new MutableDateCalc(occurredDate).goPreviousMilli.toDate, occurredDate)
316 // clog.debug("referenceTimeslot = %s".format(referenceTimeslot))
318 // clog.debug("Calling policyStore.loadValidPolicyEntryAt(%s)", new MutableDateCalc(occurredMillis))
319 val relevantPolicyM = policyStore.loadValidPolicyAt(occurredMillis, dsl)
320 // clog.debug(" ==> relevantPolicyM = %s", relevantPolicyM)
322 val relevantPolicies = relevantPolicyM match {
323 case Just(relevantPolicy) ⇒
324 Map(referenceTimeslot -> relevantPolicy)
326 throw new AquariumException("No relevant policy found for %s".format(referenceTimeslot))
327 case failed @ Failed(e) ⇒
328 throw new AquariumException("No relevant policy found for %s".format(referenceTimeslot), e)
332 (referenceTimeslot, relevantPolicies, previousValue)
335 val initialChargeslotsM = computeInitialChargeslots(
339 agreementNamesByTimeslot,
343 val fullChargeslotsM = initialChargeslotsM.map { chargeslots ⇒
345 case chargeslot @ Chargeslot(startMillis, stopMillis, algorithmDefinition, unitPrice, _) ⇒
346 val execAlgorithmM = algorithmCompiler.compile(algorithmDefinition)
347 execAlgorithmM match {
349 throw new AquariumException("Could not compile algorithm %s".format(algorithmDefinition))
351 case failed @ Failed(e) ⇒
354 case Just(execAlgorithm) ⇒
355 val valueMap = costPolicy.makeValueMap(
359 stopMillis - startMillis,
361 currentResourceEvent.value,
365 // clog.debug("execAlgorithm = %s", execAlgorithm)
366 clog.debugMap("valueMap", valueMap, 1)
369 val creditsM = execAlgorithm.apply(valueMap)
373 throw new AquariumException(
374 "Could not compute credits for resource %s during %s".
375 format(dslResource.name, Timeslot(new Date(startMillis), new Date(stopMillis))))
377 case failed @ Failed(e) ⇒
381 chargeslot.copy(computedCredits = Some(credits))
387 val result = fullChargeslotsM match {
388 case Just(fullChargeslots) ⇒
389 referenceTimeslot -> fullChargeslots
392 case failed @ Failed(e) ⇒
402 * Create a list of wallet entries by charging for a resource event.
404 * @param currentResourceEvent The resource event to create charges for
405 * @param agreements The user's agreement names, indexed by their
406 * applicability timeslot
407 * @param previousAmount The current state of the resource
408 * @param previousOccurred The last time the resource state was updated
410 def chargeEvent(currentResourceEvent: ResourceEvent,
411 agreements: SortedMap[Timeslot, String],
412 previousAmount: Double,
413 previousOccurred: Date,
414 related: List[WalletEntry]): Maybe[List[WalletEntry]] = {
416 assert(previousOccurred.getTime <= currentResourceEvent.occurredMillis)
417 val occuredDate = new Date(currentResourceEvent.occurredMillis)
419 /* The following makes sure that agreements exist between the start
420 * and end days of the processed event. As agreement updates are
421 * guaranteed not to leave gaps, this means that the event can be
422 * processed correctly, as at least one agreement will be valid
423 * throughout the event's life.
426 agreements.keysIterator.exists {
427 p => p.includes(occuredDate)
428 } && agreements.keysIterator.exists {
429 p => p.includes(previousOccurred)
433 val t = Timeslot(previousOccurred, occuredDate)
435 // Align policy and agreement validity timeslots to the event's boundaries
436 val policyTimeslots = t.align(
437 Policy.policies(previousOccurred, occuredDate).keysIterator.toList)
438 val agreementTimeslots = t.align(agreements.keysIterator.toList)
441 * Get a set of timeslot slices covering the different durations of
442 * agreements and policies.
444 val aligned = alignTimeslots(policyTimeslots, agreementTimeslots)
446 val walletEntries = aligned.map {
448 // Retrieve agreement from the policy valid at time of event
449 val agreementName = agreements.find(y => y._1.contains(x)) match {
451 case None => return Failed(new AccountingException(("Cannot find" +
452 " user agreement for period %s").format(x)))
455 // Do the wallet entry calculation
456 val entries = chargeEvent(
457 currentResourceEvent,
458 Policy.policy(x.from).findAgreement(agreementName._2).getOrElse(
459 return Failed(new AccountingException("Cannot get agreement for %s".format()))
467 case Failed(f) => return Failed(f)
477 * Creates a list of wallet entries by applying the agreement provisions on
478 * the resource state.
480 * @param event The resource event to create charges for
481 * @param agr The agreement implementation to use
482 * @param previousAmount The current state of the resource
483 * @param previousOccurred The timestamp of the previous event
484 * @param related Related wallet entries (TODO: should remove)
485 * @param chargeFor The duration for which the charge should be done.
486 * Should fall between the previous and current
487 * resource event boundaries
488 * @return A list of wallet entries, one for each
490 def chargeEvent(event: ResourceEvent,
492 previousAmount: Double,
493 previousOccurred: Date,
494 related: List[WalletEntry],
495 chargeFor: Option[Timeslot]): Maybe[List[WalletEntry]] = {
497 // If chargeFor is not null, make sure it falls within
498 // event time boundaries
499 chargeFor.map{x => assert(true,
500 Timeslot(previousOccurred, new Date(event.occurredMillis)))}
502 if (!event.validate())
503 return Failed(new AccountingException("Event not valid"))
505 val policy = Policy.policy
506 val dslResource = policy.findResource(event.resource) match {
508 case None => return Failed(
509 new AccountingException("No resource [%s]".format(event.resource)))
512 /* This is a safeguard against the special case where the last
513 * resource state update, as marked by the lastUpdate parameter
514 * is equal to the time of the event occurrence. This means that
515 * this is the first time the resource state has been recorded.
516 * Charging in this case only makes sense for discrete resources.
518 if (previousOccurred.getTime == event.occurredMillis) {
519 dslResource.costPolicy match {
520 case DiscreteCostPolicy => //Ok
521 case _ => return Some(List())
525 val creditCalculationValueM = dslResource.costPolicy.getValueForCreditCalculation(Just(previousAmount), event.value)
526 val amount = creditCalculationValueM match {
527 case failed @ Failed(_) ⇒
535 // We don't do strict checking for all cases for OnOffPolicies as
536 // above, since this point won't be reached in case of error.
537 val isFinal = dslResource.costPolicy match {
538 case OnOffCostPolicy =>
539 OnOffPolicyResourceState(previousAmount) match {
540 case OnResourceState => false
541 case OffResourceState => true
547 * Get the timeslot for which this event will be charged. In case we
548 * have a discrete resource, we do not really care for the time duration
549 * of an event. To process all events in a uniform way, we create an
550 * artificial timeslot lasting the minimum amount of time. In all other
551 * cases, we first check whether a desired charge period passed as
554 val timeslot = dslResource.costPolicy match {
555 case DiscreteCostPolicy => Timeslot(new Date(event.occurredMillis - 1),
556 new Date(event.occurredMillis))
557 case _ => chargeFor match {
559 case None => Timeslot(previousOccurred, new Date(event.occurredMillis))
564 * The following splits the chargable timeslot into smaller timeslots to
565 * comply with different applicability periods for algorithms and
566 * pricelists defined by the provided agreement.
568 val chargeChunks = calcChangeChunks(agr, amount, dslResource, timeslot)
570 val timeReceived = System.currentTimeMillis
572 val rel = event.id :: related.map{x => x.sourceEventIDs}.flatten
574 val entries = chargeChunks.map { c=>
576 id = CryptoUtils.sha1(c.id),
577 occurredMillis = event.occurredMillis,
578 receivedMillis = timeReceived,
579 sourceEventIDs = rel,
582 userId = event.userID,
583 resource = event.resource,
584 instanceId = event.instanceID,
594 def calcChangeChunks(agr: DSLAgreement, volume: Double,
595 res: DSLResource, t: Timeslot): List[ChargeChunk] = {
597 val alg = resolveEffectiveAlgorithmsForTimeslot(t, agr)
598 val pri = resolveEffectivePricelistsForTimeslot(t, agr)
599 val chunks = splitChargeChunks(alg, pri)
600 val algChunked = chunks._1
601 val priChunked = chunks._2
603 assert(algChunked.size == priChunked.size)
605 res.costPolicy match {
606 case DiscreteCostPolicy => calcChargeChunksDiscrete(algChunked, priChunked, volume, res)
607 case _ => calcChargeChunksContinuous(algChunked, priChunked, volume, res)
612 * Get a list of charge chunks for discrete resources.
615 def calcChargeChunksDiscrete(algChunked: Map[Timeslot, DSLAlgorithm],
616 priChunked: Map[Timeslot, DSLPriceList],
617 volume: Double, res: DSLResource): List[ChargeChunk] = {
618 // In case of descrete resources, we only a expect a
619 assert(algChunked.size == 1)
620 assert(priChunked.size == 1)
621 assert(algChunked.keySet.head.compare(priChunked.keySet.head) == 0)
623 List(ChargeChunk(volume,
624 algChunked.valuesIterator.next.algorithms.getOrElse(res, ""),
625 priChunked.valuesIterator.next.prices.getOrElse(res, 0),
626 algChunked.keySet.head, res))
630 * Get a list of charge chunks for continuous resources.
633 def calcChargeChunksContinuous(algChunked: Map[Timeslot, DSLAlgorithm],
634 priChunked: Map[Timeslot, DSLPriceList],
635 volume: Double, res: DSLResource): List[ChargeChunk] = {
636 algChunked.keysIterator.map {
639 algChunked.get(x).get.algorithms.getOrElse(res, ""),
640 priChunked.get(x).get.prices.getOrElse(res, 0), x, res)
645 * Align charge timeslots between algorithms and pricelists. As algorithm
646 * and pricelists can have different effectivity periods, this method
647 * examines them and splits them as necessary.
649 private[logic] def splitChargeChunks(alg: SortedMap[Timeslot, DSLAlgorithm],
650 price: SortedMap[Timeslot, DSLPriceList]) :
651 (Map[Timeslot, DSLAlgorithm], Map[Timeslot, DSLPriceList]) = {
653 val zipped = alg.keySet.zip(price.keySet)
655 zipped.find(p => !p._1.equals(p._2)) match {
656 case None => (alg, price)
658 val algTimeslot = x._1
659 val priTimeslot = x._2
661 assert(algTimeslot.from == priTimeslot.from)
663 if (algTimeslot.endsAfter(priTimeslot)) {
664 val slices = algTimeslot.slice(priTimeslot.to)
665 val algo = alg.get(algTimeslot).get
666 val newalg = alg - algTimeslot ++ Map(slices.apply(0) -> algo) ++ Map(slices.apply(1) -> algo)
667 splitChargeChunks(newalg, price)
670 val slices = priTimeslot.slice(priTimeslot.to)
671 val pl = price.get(priTimeslot).get
672 val newPrice = price - priTimeslot ++ Map(slices.apply(0) -> pl) ++ Map(slices.apply(1) -> pl)
673 splitChargeChunks(alg, newPrice)
679 * Given two lists of timeslots, produce a list which contains the
680 * set of timeslot slices, as those are defined by
683 * For example, given the timeslots a and b below, split them as shown.
685 * a = |****************|
692 * result: List(Timeslot(a.from, b.to), Timeslot(b.to, a.to))
694 private[logic] def alignTimeslots(a: List[Timeslot],
695 b: List[Timeslot]): List[Timeslot] = {
697 def safeTail(foo: List[Timeslot]) = foo match {
699 case x :: Nil => List()
700 case x :: rest => rest
703 if (a.isEmpty) return b
704 if (b.isEmpty) return a
706 assert (a.head.from == b.head.from)
708 if (a.head.endsAfter(b.head)) {
709 val slice = a.head.slice(b.head.to)
710 slice.head :: alignTimeslots(slice.last :: a.tail, safeTail(b))
711 } else if (b.head.endsAfter(a.head)) {
712 val slice = b.head.slice(a.head.to)
713 slice.head :: alignTimeslots(safeTail(a), slice.last :: b.tail)
715 a.head :: alignTimeslots(safeTail(a), safeTail(b))
721 * Encapsulates a computation for a specific timeslot of
724 case class ChargeChunk(value: Double, algorithm: String,
725 price: Double, when: Timeslot,
726 resource: DSLResource) {
728 assert(!algorithm.isEmpty)
729 assert(resource != null)
732 //TODO: Apply the algorithm, when we start parsing it
733 resource.costPolicy match {
734 case DiscreteCostPolicy =>
737 value * price * when.hours
740 def reason(): String =
741 resource.costPolicy match {
742 case DiscreteCostPolicy =>
743 "%f %s at %s @ %f/%s".format(value, resource.unit, when.from, price,
745 case ContinuousCostPolicy =>
746 "%f %s of %s from %s to %s @ %f/%s".format(value, resource.unit,
747 resource.name, when.from, when.to, price, resource.unit)
748 case OnOffCostPolicy =>
749 "%f %s of %s from %s to %s @ %f/%s".format(when.hours, resource.unit,
750 resource.name, when.from, when.to, price, resource.unit)
754 CryptoUtils.sha1("%f%s%f%s%s%d".format(value, algorithm, price, when.toString,
755 resource.name, System.currentTimeMillis()))
758 /** An exception raised when something goes wrong with accounting */
759 class AccountingException(msg: String) extends AquariumException(msg)