Purify the accounting code
[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 dsl._
39 import gr.grnet.aquarium.logic.events.{WalletEntry, ResourceEvent}
40 import collection.immutable.SortedMap
41 import com.ckkloverdos.maybe.{Maybe, Failed, Just}
42 import java.util.Date
43 import gr.grnet.aquarium.util.{CryptoUtils, Loggable}
44
45 /**
46  * Methods for converting accounting events to wallet entries.
47  *
48  * @author Georgios Gousios <gousiosg@gmail.com>
49  */
50 trait Accounting extends DSLUtils with Loggable {
51
52   /**
53    * Creates a list of wallet entries by applying the agreement provisions on
54    * the resource state
55    *
56    * @param ev The resource event to create charges for
57    * @param agreement The agreement applicable to the user mentioned in the event
58    * @param resState The current state of the resource
59    * @param lastUpdate The last time the resource state was updated
60    */
61   def chargeEvent(ev: ResourceEvent,
62                   agreement: DSLAgreement,
63                   resState: Any,
64                   lastUpdate: Date):
65   Maybe[List[WalletEntry]] = {
66
67     if (!ev.validate())
68       Failed(new AccountingException("Event not valid"))
69
70     val resource = Policy.policy.findResource(ev.resource) match {
71       case Some(x) => x
72       case None => return Failed(
73         new AccountingException("No resource [%s]".format(ev.resource)))
74     }
75
76     val amount = resource.isComplex match {
77       case true => 0
78       case false => 1
79     }
80
81     val chargeChunks = calcChangeChunks(agreement, amount, resource,
82       Timeslot(lastUpdate, new Date(ev.occurredMillis)))
83
84     val entries = chargeChunks.map {
85       c =>
86         WalletEntry(
87           id = CryptoUtils.sha1(c.id),
88           occurredMillis = lastUpdate.getTime,
89           receivedMillis = System.currentTimeMillis(),
90           sourceEventIDs = List(ev.id),
91           value = c.cost,
92           reason = c.reason,
93           userId = ev.userId,
94           finalized = true
95         )
96     }
97     Just(entries)
98   }
99
100  def calcChangeChunks(agr: DSLAgreement, volume: Float,
101                       res: DSLResource, t: Timeslot): List[ChargeChunk] = {
102
103     val alg = resolveEffectiveAlgorithmsForTimeslot(t, agr)
104     val pri = resolveEffectivePricelistsForTimeslot(t, agr)
105     val chunks = splitChargeChunks(alg, pri)
106
107     val algChunked = chunks._1
108     val priChunked = chunks._2
109
110     assert(algChunked.size == priChunked.size)
111     val totalTime = t.from.getTime - t.to.getTime
112     algChunked.keySet.map{
113       x =>
114         val amount = volume * (totalTime / (x.to.getTime - x.from.getTime))
115         ChargeChunk(amount,
116           algChunked.get(x).get.algorithms.getOrElse(res, ""),
117           priChunked.get(x).get.prices.getOrElse(res, 0F), x, res)
118     }.toList
119   }
120
121   /**
122    * Align charge timeslots between algorithms and pricelists. As algorithm
123    * and pricelists can have different effectivity periods, this method
124    * examines them and splits them as necessary.
125    */
126   private[logic] def splitChargeChunks(alg: SortedMap[Timeslot, DSLAlgorithm],
127                         price: SortedMap[Timeslot, DSLPriceList]) :
128     (Map[Timeslot, DSLAlgorithm], Map[Timeslot, DSLPriceList]) = {
129
130     val zipped = alg.keySet.zip(price.keySet)
131
132     zipped.find(p => !p._1.equals(p._2)) match {
133       case None => (alg, price)
134       case Some(x) =>
135         val algTimeslot = x._1
136         val priTimeslot = x._2
137
138         assert(algTimeslot.from == priTimeslot.from)
139
140         if (algTimeslot.endsAfter(priTimeslot)) {
141           val slices = algTimeslot.slice(priTimeslot.to)
142           val algo = alg.get(algTimeslot).get
143           val newalg = alg - algTimeslot ++ Map(slices.apply(0) -> algo) ++ Map(slices.apply(1) -> algo)
144           splitChargeChunks(newalg, price)
145         }
146         else {
147           val slices = priTimeslot.slice(priTimeslot.to)
148           val pl = price.get(priTimeslot).get
149           val newPrice = price - priTimeslot ++ Map(slices.apply(0) -> pl) ++ Map(slices.apply(1) -> pl)
150           splitChargeChunks(alg, newPrice)
151         }
152     }
153   }
154 }
155
156 case class ChargeChunk(value: Float, algorithm: String,
157                        price: Float, when: Timeslot,
158                        resource: DSLResource) {
159   assert(value > 0)
160   assert(!algorithm.isEmpty)
161   assert(resource != null)
162
163   def cost(): Float = {
164     //TODO: Apply the algorithm when we start parsing it
165     value * price
166   }
167
168   def reason(): String =
169     "%d %s of %s from %s to %s @ %d/%s".format(value, resource.unit,
170       resource.name, when.from, when.to, price, resource.unit)
171
172   def id(): String =
173     "%d%s%d%s%s%d".format(value, algorithm, price, when.toString,
174       resource.name, System.currentTimeMillis())
175 }
176
177 /** An exception raised when something goes wrong with accounting */
178 class AccountingException(msg: String) extends Exception(msg)