2 * Copyright 2011 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 algorithm.CostPolicyAlgorithmCompiler
40 import gr.grnet.aquarium.logic.events.{WalletEntry, ResourceEvent}
41 import collection.immutable.SortedMap
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
48 * A timeslot together with the algorithm and unit price that apply for this particular timeslot.
52 * @param algorithmDefinition
54 * @param computedCredits The computed credits
56 case class Chargeslot(startMillis: Long,
58 algorithmDefinition: String,
60 computedCredits: Option[Double] = None)
63 * Methods for converting accounting events to wallet entries.
65 * @author Georgios Gousios <gousiosg@gmail.com>
66 * @author Christos KK Loverdos <loverdos@gmail.com>
68 trait Accounting extends DSLUtils with Loggable {
70 * Breaks a reference timeslot (e.g. billing period) according to policies and agreements.
72 * @param referenceTimeslot
73 * @param policyTimeslots
74 * @param agreementTimeslots
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)
85 alignTimeslots(alignedPolicyTimeslots, alignedAgreementTimeslots)
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.
92 * @param referenceTimeslot
93 * @param policiesByTimeslot
94 * @param agreementNamesByTimeslot
98 def computeInitialChargeslots(referenceTimeslot: Timeslot,
99 dslResource: DSLResource,
100 policiesByTimeslot: Map[Timeslot, DSLPolicy],
101 agreementNamesByTimeslot: Map[Timeslot, String]): Maybe[List[Chargeslot]] = Maybe {
103 val policyTimeslots = policiesByTimeslot.keySet
104 val agreementTimeslots = agreementNamesByTimeslot.keySet
106 def getPolicy(ts: Timeslot): DSLPolicy = {
107 policiesByTimeslot.find(_._1.contains(ts)).get._2
109 def getAgreementName(ts: Timeslot): String = {
110 agreementNamesByTimeslot.find(_._1.contains(ts)).get._2
113 // 1. Round ONE: split time according to overlapping policies and agreements.
114 val alignedTimeslots = splitTimeslotByPoliciesAndAgreements(referenceTimeslot, policyTimeslots.toList, agreementTimeslots.toList)
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
122 val dslPolicy = getPolicy(alignedTimeslot)
123 val agreementName = getAgreementName(alignedTimeslot)
124 val agreementOpt = dslPolicy.findAgreement(agreementName)
128 throw new Exception("Unknown agreement %s during %s".format(agreementName, alignedTimeslot))
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
140 // Now, the timeslots must be the same
141 val finegrainedTimeslots = algorithmByTimeslot.keySet
143 val chargeslots = for {
144 finegrainedTimeslot <- finegrainedTimeslots
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 {
153 "Unknown algorithm and price unit for resource %s during %s".
154 format(dslResource.name, finegrainedTimeslot))
157 "Unknown algorithm for resource %s during %s".
158 format(dslResource.name, finegrainedTimeslot))
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)
172 allChargeslots.flatten
176 * Compute the charge slots generated by a particular resource event.
179 def computeFullChargeslots(previousResourceEventM: Maybe[ResourceEvent],
180 currentResourceEvent: ResourceEvent,
182 oldTotalAmount: Double,
183 newTotalAmount: Double,
184 dslResource: DSLResource,
185 defaultResourceMap: DSLResourcesMap,
186 agreementNamesByTimeslot: Map[Timeslot, String],
187 algorithmCompiler: CostPolicyAlgorithmCompiler): Maybe[List[Chargeslot]] = Maybe {
189 val occurredDate = currentResourceEvent.occurredDate
190 val costPolicy = dslResource.costPolicy
192 val (referenceTimeslot, relevantPolicies, previousValue) = costPolicy.needsPreviousEventForCreditAndAmountCalculation match {
193 // We need a previous event
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)
203 (referenceTimeslot, relevantPolicies, previousResourceEvent.value)
205 // We do not have a previous event
208 "Unable to charge. No previous event given for %s".
209 format(currentResourceEvent.toDebugString(defaultResourceMap)))
211 // We could not obtain a previous event
212 case failed @ Failed(e, m) ⇒
214 "Unable to charge. Could not obtain previous event for %s".
215 format(currentResourceEvent.toDebugString(defaultResourceMap)))
218 // We do not need a previous event
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)
226 (referenceTimeslot, relevantPolicies, costPolicy.getResourceInstanceUndefinedAmount)
229 val initialChargeslotsM = computeInitialChargeslots(
233 agreementNamesByTimeslot
236 val fullChargeslotsM = initialChargeslotsM.map { chargeslots ⇒
238 case chargeslot @ Chargeslot(startMillis, stopMillis, algorithmDefinition, unitPrice, _) ⇒
239 val execAlgorithmM = algorithmCompiler.compile(algorithmDefinition)
240 execAlgorithmM match {
242 throw new Exception("Could not compile algorithm %s".format(algorithmDefinition))
244 case failed @ Failed(e, m) ⇒
245 throw new Exception(m, e)
247 case Just(execAlgorithm) ⇒
248 val valueMap = costPolicy.makeValueMap(
253 stopMillis - startMillis,
255 currentResourceEvent.value,
260 val creditsM = execAlgorithm.apply(valueMap)
265 "Could not compute credits for resource %s during %s".
266 format(dslResource.name, Timeslot(new Date(startMillis), new Date(stopMillis))))
268 case failed @ Failed(e, m) ⇒
269 throw new Exception(m, e)
272 chargeslot.copy(computedCredits = Some(credits))
278 fullChargeslotsM match {
279 case Just(fullChargeslots) ⇒
283 case failed @ Failed(e, m) ⇒
284 throw new Exception(m, e)
289 * Create a list of wallet entries by charging for a resource event.
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
297 def chargeEvent(currentResourceEvent: ResourceEvent,
298 agreements: SortedMap[Timeslot, String],
299 previousAmount: Double,
300 previousOccurred: Date,
301 related: List[WalletEntry]): Maybe[List[WalletEntry]] = {
303 assert(previousOccurred.getTime <= currentResourceEvent.occurredMillis)
304 val occuredDate = new Date(currentResourceEvent.occurredMillis)
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.
313 agreements.keysIterator.exists {
314 p => p.includes(occuredDate)
315 } && agreements.keysIterator.exists {
316 p => p.includes(previousOccurred)
320 val t = Timeslot(previousOccurred, occuredDate)
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)
328 * Get a set of timeslot slices covering the different durations of
329 * agreements and policies.
331 val aligned = alignTimeslots(policyTimeslots, agreementTimeslots)
333 val walletEntries = aligned.map {
335 // Retrieve agreement from the policy valid at time of event
336 val agreementName = agreements.find(y => y._1.contains(x)) match {
338 case None => return Failed(new AccountingException(("Cannot find" +
339 " user agreement for period %s").format(x)))
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()))
354 case Failed(f, e) => return Failed(f,e)
364 * Creates a list of wallet entries by applying the agreement provisions on
365 * the resource state.
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
377 def chargeEvent(event: ResourceEvent,
379 previousAmount: Double,
380 previousOccurred: Date,
381 related: List[WalletEntry],
382 chargeFor: Option[Timeslot]): Maybe[List[WalletEntry]] = {
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)))}
389 if (!event.validate())
390 return Failed(new AccountingException("Event not valid"))
392 val policy = Policy.policy
393 val dslResource = policy.findResource(event.resource) match {
395 case None => return Failed(
396 new AccountingException("No resource [%s]".format(event.resource)))
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.
405 if (previousOccurred.getTime == event.occurredMillis) {
406 dslResource.costPolicy match {
407 case DiscreteCostPolicy => //Ok
408 case _ => return Some(List())
412 val creditCalculationValueM = dslResource.costPolicy.getValueForCreditCalculation(Just(previousAmount), event.value)
413 val amount = creditCalculationValueM match {
414 case failed @ Failed(_, _) ⇒
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
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
441 val timeslot = dslResource.costPolicy match {
442 case DiscreteCostPolicy => Timeslot(new Date(event.occurredMillis - 1),
443 new Date(event.occurredMillis))
444 case _ => chargeFor match {
446 case None => Timeslot(previousOccurred, new Date(event.occurredMillis))
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.
455 val chargeChunks = calcChangeChunks(agr, amount, dslResource, timeslot)
457 val timeReceived = System.currentTimeMillis
459 val rel = event.id :: related.map{x => x.sourceEventIDs}.flatten
461 val entries = chargeChunks.map { c=>
463 id = CryptoUtils.sha1(c.id),
464 occurredMillis = event.occurredMillis,
465 receivedMillis = timeReceived,
466 sourceEventIDs = rel,
469 userId = event.userId,
470 resource = event.resource,
471 instanceId = event.instanceId,
481 def calcChangeChunks(agr: DSLAgreement, volume: Double,
482 res: DSLResource, t: Timeslot): List[ChargeChunk] = {
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
490 assert(algChunked.size == priChunked.size)
492 res.costPolicy match {
493 case DiscreteCostPolicy => calcChargeChunksDiscrete(algChunked, priChunked, volume, res)
494 case _ => calcChargeChunksContinuous(algChunked, priChunked, volume, res)
499 * Get a list of charge chunks for discrete resources.
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)
510 List(ChargeChunk(volume,
511 algChunked.valuesIterator.next.algorithms.getOrElse(res, ""),
512 priChunked.valuesIterator.next.prices.getOrElse(res, 0),
513 algChunked.keySet.head, res))
517 * Get a list of charge chunks for continuous resources.
520 def calcChargeChunksContinuous(algChunked: Map[Timeslot, DSLAlgorithm],
521 priChunked: Map[Timeslot, DSLPriceList],
522 volume: Double, res: DSLResource): List[ChargeChunk] = {
523 algChunked.keysIterator.map {
526 algChunked.get(x).get.algorithms.getOrElse(res, ""),
527 priChunked.get(x).get.prices.getOrElse(res, 0), x, res)
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.
536 private[logic] def splitChargeChunks(alg: SortedMap[Timeslot, DSLAlgorithm],
537 price: SortedMap[Timeslot, DSLPriceList]) :
538 (Map[Timeslot, DSLAlgorithm], Map[Timeslot, DSLPriceList]) = {
540 val zipped = alg.keySet.zip(price.keySet)
542 zipped.find(p => !p._1.equals(p._2)) match {
543 case None => (alg, price)
545 val algTimeslot = x._1
546 val priTimeslot = x._2
548 assert(algTimeslot.from == priTimeslot.from)
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)
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)
566 * Given two lists of timeslots, produce a list which contains the
567 * set of timeslot slices, as those are defined by
570 * For example, given the timeslots a and b below, split them as shown.
572 * a = |****************|
579 * result: List(Timeslot(a.from, b.to), Timeslot(b.to, a.to))
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)
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)
592 a.head :: alignTimeslots(a.tail, b.tail)
598 * Encapsulates a computation for a specific timeslot of
601 case class ChargeChunk(value: Double, algorithm: String,
602 price: Double, when: Timeslot,
603 resource: DSLResource) {
605 assert(!algorithm.isEmpty)
606 assert(resource != null)
609 //TODO: Apply the algorithm, when we start parsing it
610 resource.costPolicy match {
611 case DiscreteCostPolicy =>
614 value * price * when.hours
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,
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)
631 CryptoUtils.sha1("%f%s%f%s%s%d".format(value, algorithm, price, when.toString,
632 resource.name, System.currentTimeMillis()))
635 /** An exception raised when something goes wrong with accounting */
636 class AccountingException(msg: String) extends Exception(msg)