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
39 import gr.grnet.aquarium.logic.events.{WalletEntry, ResourceEvent}
40 import collection.immutable.SortedMap
42 import gr.grnet.aquarium.util.{CryptoUtils, Loggable}
43 import com.ckkloverdos.maybe.{NoVal, Maybe, Failed, Just}
46 * Methods for converting accounting events to wallet entries.
48 * @author Georgios Gousios <gousiosg@gmail.com>
50 trait Accounting extends DSLUtils with Loggable {
52 def chargeEvent2( oldResourceEventM: Maybe[ResourceEvent],
53 newResourceEvent: ResourceEvent,
54 dslAgreement: DSLAgreement,
55 lastSnapshotDate: Date,
56 related: Traversable[WalletEntry]): Maybe[Traversable[WalletEntry]] = {
58 val dslPolicy: DSLPolicy = Policy.policy // TODO: query based on time
59 val resourceEvent = newResourceEvent
60 dslPolicy.findResource(resourceEvent.resource) match {
62 throw new AccountingException("No resource [%s]".format(resourceEvent.resource))
63 case Some(dslResource) ⇒
65 val costPolicy = dslResource.costPolicy
66 val isDiscrete = costPolicy.isDiscrete
67 val oldValueM = oldResourceEventM.map(_.value)
68 val newValue = newResourceEvent.value
70 /* This is a safeguard against the special case where the last
71 * resource state update, as marked by the lastUpdate parameter
72 * is equal to the time of the event occurrence. This means that
73 * this is the first time the resource state has been recorded.
74 * Charging in this case only makes sense for discrete resources.
76 if (lastSnapshotDate.getTime == resourceEvent.occurredMillis && !isDiscrete) {
79 val creditCalculationValueM = dslResource.costPolicy.getValueForCreditCalculation(oldValueM, newValue).forNoVal(Just(0.0))
81 amount <- creditCalculationValueM
83 // We don't do strict checking for all cases for OnOffPolicies as
84 // above, since this point won't be reached in case of error.
85 val isFinal = dslResource.costPolicy match {
86 case OnOffCostPolicy =>
87 OnOffPolicyResourceState(oldValueM) match {
88 case OnResourceState => false
89 case OffResourceState => true
94 val timeslot = dslResource.costPolicy match {
95 case DiscreteCostPolicy => Timeslot(new Date(resourceEvent.occurredMillis),
96 new Date(resourceEvent.occurredMillis + 1))
97 case _ => Timeslot(lastSnapshotDate, new Date(resourceEvent.occurredMillis))
100 val chargeChunks = calcChangeChunks(dslAgreement, amount, dslResource, timeslot)
102 val timeReceived = System.currentTimeMillis
104 val rel = related.map{x => x.sourceEventIDs}.flatten ++ Traversable(resourceEvent.id)
106 val entries = chargeChunks.map {
109 id = CryptoUtils.sha1(chargedChunk.id),
110 occurredMillis = resourceEvent.occurredMillis,
111 receivedMillis = timeReceived,
112 sourceEventIDs = rel.toList,
113 value = chargedChunk.cost,
114 reason = chargedChunk.reason,
115 userId = resourceEvent.userId,
116 resource = resourceEvent.resource,
117 instanceId = resourceEvent.instanceId,
129 * Creates a list of wallet entries by applying the agreement provisions on
130 * the resource state.
132 * @param resourceEvent The resource event to create charges for
133 * @param agreement The agreement applicable to the user mentioned in the event
134 * @param currentValue The current state of the resource
135 * @param currentSnapshotDate The last time the resource state was updated
137 def chargeEvent(resourceEvent: ResourceEvent,
138 agreement: DSLAgreement,
139 currentValue: Double,
140 currentSnapshotDate: Date,
141 related: List[WalletEntry]): Maybe[List[WalletEntry]] = {
143 assert(currentSnapshotDate.getTime <= resourceEvent.occurredMillis)
145 if (!resourceEvent.validate())
146 return Failed(new AccountingException("Event not valid"))
148 val policy = Policy.policy
149 val dslResource = policy.findResource(resourceEvent.resource) match {
151 case None => return Failed(new AccountingException("No resource [%s]".format(resourceEvent.resource)))
154 /* This is a safeguard against the special case where the last
155 * resource state update, as marked by the lastUpdate parameter
156 * is equal to the time of the event occurrence. This means that
157 * this is the first time the resource state has been recorded.
158 * Charging in this case only makes sense for discrete resources.
160 if (currentSnapshotDate.getTime == resourceEvent.occurredMillis) {
161 dslResource.costPolicy match {
162 case DiscreteCostPolicy => //Ok
163 case _ => return Some(List())
167 val creditCalculationValueM = dslResource.costPolicy.getValueForCreditCalculation(Just(currentValue), resourceEvent.value)
168 val amount = creditCalculationValueM match {
169 case failed @ Failed(_, _) ⇒
177 // We don't do strict checking for all cases for OnOffPolicies as
178 // above, since this point won't be reached in case of error.
179 val isFinal = dslResource.costPolicy match {
180 case OnOffCostPolicy =>
181 OnOffPolicyResourceState(currentValue) match {
182 case OnResourceState => false
183 case OffResourceState => true
188 val timeslot = dslResource.costPolicy match {
189 case DiscreteCostPolicy => Timeslot(new Date(resourceEvent.occurredMillis),
190 new Date(resourceEvent.occurredMillis + 1))
191 case _ => Timeslot(currentSnapshotDate, new Date(resourceEvent.occurredMillis))
194 val chargeChunks = calcChangeChunks(agreement, amount, dslResource, timeslot)
196 val timeReceived = System.currentTimeMillis
198 val rel = related.map{x => x.sourceEventIDs}.flatten ++ List(resourceEvent.id)
200 val entries = chargeChunks.map {
203 id = CryptoUtils.sha1(c.id),
204 occurredMillis = resourceEvent.occurredMillis,
205 receivedMillis = timeReceived,
206 sourceEventIDs = rel,
209 userId = resourceEvent.userId,
210 resource = resourceEvent.resource,
211 instanceId = resourceEvent.instanceId,
218 def calcChangeChunks(agr: DSLAgreement, volume: Double,
219 res: DSLResource, t: Timeslot): List[ChargeChunk] = {
221 val alg = resolveEffectiveAlgorithmsForTimeslot(t, agr)
222 val pri = resolveEffectivePricelistsForTimeslot(t, agr)
223 val chunks = splitChargeChunks(alg, pri)
224 val algChunked = chunks._1
225 val priChunked = chunks._2
227 assert(algChunked.size == priChunked.size)
229 res.costPolicy match {
230 case DiscreteCostPolicy => calcChargeChunksDiscrete(algChunked, priChunked, volume, res)
231 case _ => calcChargeChunksContinuous(algChunked, priChunked, volume, res)
236 def calcChargeChunksDiscrete(algChunked: Map[Timeslot, DSLAlgorithm],
237 priChunked: Map[Timeslot, DSLPriceList],
238 volume: Double, res: DSLResource): List[ChargeChunk] = {
239 assert(algChunked.size == 1)
240 assert(priChunked.size == 1)
241 assert(algChunked.keySet.head.compare(priChunked.keySet.head) == 0)
243 List(ChargeChunk(volume,
244 algChunked.valuesIterator.next.algorithms.getOrElse(res, ""),
245 priChunked.valuesIterator.next.prices.getOrElse(res, 0),
246 algChunked.keySet.head, res))
250 def calcChargeChunksContinuous(algChunked: Map[Timeslot, DSLAlgorithm],
251 priChunked: Map[Timeslot, DSLPriceList],
252 volume: Double, res: DSLResource): List[ChargeChunk] = {
253 algChunked.keysIterator.map {
256 algChunked.get(x).get.algorithms.getOrElse(res, ""),
257 priChunked.get(x).get.prices.getOrElse(res, 0), x, res)
262 * Align charge timeslots between algorithms and pricelists. As algorithm
263 * and pricelists can have different effectivity periods, this method
264 * examines them and splits them as necessary.
266 private[logic] def splitChargeChunks(alg: SortedMap[Timeslot, DSLAlgorithm],
267 price: SortedMap[Timeslot, DSLPriceList]) :
268 (Map[Timeslot, DSLAlgorithm], Map[Timeslot, DSLPriceList]) = {
270 val zipped = alg.keySet.zip(price.keySet)
272 zipped.find(p => !p._1.equals(p._2)) match {
273 case None => (alg, price)
275 val algTimeslot = x._1
276 val priTimeslot = x._2
278 assert(algTimeslot.from == priTimeslot.from)
280 if (algTimeslot.endsAfter(priTimeslot)) {
281 val slices = algTimeslot.slice(priTimeslot.to)
282 val algo = alg.get(algTimeslot).get
283 val newalg = alg - algTimeslot ++ Map(slices.apply(0) -> algo) ++ Map(slices.apply(1) -> algo)
284 splitChargeChunks(newalg, price)
287 val slices = priTimeslot.slice(priTimeslot.to)
288 val pl = price.get(priTimeslot).get
289 val newPrice = price - priTimeslot ++ Map(slices.apply(0) -> pl) ++ Map(slices.apply(1) -> pl)
290 splitChargeChunks(alg, newPrice)
296 case class ChargeChunk(value: Double, algorithm: String,
297 price: Double, when: Timeslot,
298 resource: DSLResource) {
300 assert(!algorithm.isEmpty)
301 assert(resource != null)
304 //TODO: Apply the algorithm, when we start parsing it
305 resource.costPolicy match {
306 case DiscreteCostPolicy =>
309 value * price * when.hours
312 def reason(): String =
313 resource.costPolicy match {
314 case DiscreteCostPolicy =>
315 "%f %s at %s @ %f/%s".format(value, resource.unit, when.from, price,
317 case ContinuousCostPolicy =>
318 "%f %s of %s from %s to %s @ %f/%s".format(value, resource.unit,
319 resource.name, when.from, when.to, price, resource.unit)
320 case OnOffCostPolicy =>
321 "%f %s of %s from %s to %s @ %f/%s".format(when.hours, resource.unit,
322 resource.name, when.from, when.to, price, resource.unit)
326 CryptoUtils.sha1("%f%s%f%s%s%d".format(value, algorithm, price, when.toString,
327 resource.name, System.currentTimeMillis()))
330 /** An exception raised when something goes wrong with accounting */
331 class AccountingException(msg: String) extends Exception(msg)