[Does not compile] Clearing up some misunderstanding with amounts
[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 java.util.Date
42 import gr.grnet.aquarium.util.{CryptoUtils, Loggable}
43 import com.ckkloverdos.maybe.{NoVal, Maybe, Failed, Just}
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   def chargeEvent2( oldResourceEventM: Maybe[ResourceEvent],
53                    newResourceEvent: ResourceEvent,
54                    dslAgreement: DSLAgreement,
55                    lastSnapshotDate: Date,
56                    related: Traversable[WalletEntry]): Maybe[Traversable[WalletEntry]] = {
57     Maybe {
58       val dslPolicy: DSLPolicy = Policy.policy // TODO: query based on time
59       val resourceEvent = newResourceEvent
60       dslPolicy.findResource(resourceEvent.resource) match {
61         case None ⇒
62           throw new AccountingException("No resource [%s]".format(resourceEvent.resource))
63         case Some(dslResource) ⇒
64
65           val costPolicy = dslResource.costPolicy
66           val isDiscrete = costPolicy.isDiscrete
67           val oldValueM = oldResourceEventM.map(_.value)
68           val newValue = newResourceEvent.value
69
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.
75           */
76           if (lastSnapshotDate.getTime == resourceEvent.occurredMillis && !isDiscrete) {
77             Just(List())
78           } else {
79             val creditCalculationValueM = dslResource.costPolicy.getValueForCreditCalculation(oldValueM, newValue).forNoVal(Just(0.0))
80             for {
81               amount <- creditCalculationValueM
82             } yield {
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
90                   }
91                 case _ => true
92               }
93
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))
98               }
99
100               val chargeChunks = calcChangeChunks(dslAgreement, amount, dslResource, timeslot)
101
102               val timeReceived = System.currentTimeMillis
103
104               val rel = related.map{x => x.sourceEventIDs}.flatten ++ Traversable(resourceEvent.id)
105
106               val entries = chargeChunks.map {
107                 chargedChunk ⇒
108                   WalletEntry(
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,
118                     finalized = isFinal
119                   )
120               } // entries
121
122               entries
123             } // yield
124           } // else
125       }
126     }.flatten1
127   }
128   /**
129    * Creates a list of wallet entries by applying the agreement provisions on
130    * the resource state.
131    *
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
136    */
137   def chargeEvent(resourceEvent: ResourceEvent,
138                   agreement: DSLAgreement,
139                   currentValue: Double,
140                   currentSnapshotDate: Date,
141                   related: List[WalletEntry]): Maybe[List[WalletEntry]] = {
142
143     assert(currentSnapshotDate.getTime <= resourceEvent.occurredMillis)
144
145     if (!resourceEvent.validate())
146       return Failed(new AccountingException("Event not valid"))
147
148     val policy = Policy.policy
149     val dslResource = policy.findResource(resourceEvent.resource) match {
150       case Some(x) => x
151       case None => return Failed(new AccountingException("No resource [%s]".format(resourceEvent.resource)))
152     }
153
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.
159      */
160     if (currentSnapshotDate.getTime == resourceEvent.occurredMillis) {
161       dslResource.costPolicy match {
162         case DiscreteCostPolicy => //Ok
163         case _ => return Some(List())
164       }
165     }
166
167     val creditCalculationValueM = dslResource.costPolicy.getValueForCreditCalculation(Just(currentValue), resourceEvent.value)
168     val amount = creditCalculationValueM match {
169       case failed @ Failed(_, _) ⇒
170         return failed
171       case Just(amount) ⇒
172         amount
173       case NoVal ⇒
174         0.0
175     }
176
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
184         }
185       case _ => true
186     }
187
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))
192     }
193
194     val chargeChunks = calcChangeChunks(agreement, amount, dslResource, timeslot)
195
196     val timeReceived = System.currentTimeMillis
197
198     val rel = related.map{x => x.sourceEventIDs}.flatten ++ List(resourceEvent.id)
199
200     val entries = chargeChunks.map {
201       c =>
202         WalletEntry(
203           id = CryptoUtils.sha1(c.id),
204           occurredMillis = resourceEvent.occurredMillis,
205           receivedMillis = timeReceived,
206           sourceEventIDs = rel,
207           value = c.cost,
208           reason = c.reason,
209           userId = resourceEvent.userId,
210           resource = resourceEvent.resource,
211           instanceId = resourceEvent.instanceId,
212           finalized = isFinal
213         )
214     }
215     Just(entries)
216   }
217
218   def calcChangeChunks(agr: DSLAgreement, volume: Double,
219                        res: DSLResource, t: Timeslot): List[ChargeChunk] = {
220
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
226
227     assert(algChunked.size == priChunked.size)
228
229     res.costPolicy match {
230       case DiscreteCostPolicy => calcChargeChunksDiscrete(algChunked, priChunked, volume, res)
231       case _ => calcChargeChunksContinuous(algChunked, priChunked, volume, res)
232     }
233   }
234
235   private[logic]
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)
242
243     List(ChargeChunk(volume,
244       algChunked.valuesIterator.next.algorithms.getOrElse(res, ""),
245       priChunked.valuesIterator.next.prices.getOrElse(res, 0),
246       algChunked.keySet.head, res))
247   }
248
249   private[logic]
250   def calcChargeChunksContinuous(algChunked: Map[Timeslot, DSLAlgorithm],
251                                  priChunked: Map[Timeslot, DSLPriceList],
252                                  volume: Double, res: DSLResource): List[ChargeChunk] = {
253     algChunked.keysIterator.map {
254       x =>
255         ChargeChunk(volume,
256           algChunked.get(x).get.algorithms.getOrElse(res, ""),
257           priChunked.get(x).get.prices.getOrElse(res, 0), x, res)
258     }.toList
259   }
260
261   /**
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.
265    */
266   private[logic] def splitChargeChunks(alg: SortedMap[Timeslot, DSLAlgorithm],
267                         price: SortedMap[Timeslot, DSLPriceList]) :
268     (Map[Timeslot, DSLAlgorithm], Map[Timeslot, DSLPriceList]) = {
269
270     val zipped = alg.keySet.zip(price.keySet)
271
272     zipped.find(p => !p._1.equals(p._2)) match {
273       case None => (alg, price)
274       case Some(x) =>
275         val algTimeslot = x._1
276         val priTimeslot = x._2
277
278         assert(algTimeslot.from == priTimeslot.from)
279
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)
285         }
286         else {
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)
291         }
292     }
293   }
294 }
295
296 case class ChargeChunk(value: Double, algorithm: String,
297                        price: Double, when: Timeslot,
298                        resource: DSLResource) {
299   assert(value > 0)
300   assert(!algorithm.isEmpty)
301   assert(resource != null)
302
303   def cost(): Double =
304     //TODO: Apply the algorithm, when we start parsing it
305     resource.costPolicy match {
306       case DiscreteCostPolicy =>
307         value * price
308       case _ =>
309         value * price * when.hours
310     }
311
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,
316           resource.unit)
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)
323     }
324
325   def id(): String =
326     CryptoUtils.sha1("%f%s%f%s%s%d".format(value, algorithm, price, when.toString,
327       resource.name, System.currentTimeMillis()))
328 }
329
330 /** An exception raised when something goes wrong with accounting */
331 class AccountingException(msg: String) extends Exception(msg)