Statistics
| Branch: | Tag: | Revision:

root / src / main / scala / gr / grnet / aquarium / logic / accounting / Accounting.scala @ 28b43673

History | View | Annotate | Download (9.3 kB)

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: Float,
64
                  lastUpdate: Date,
65
                  related: List[WalletEntry]):
66
  Maybe[List[WalletEntry]] = {
67

    
68
    assert(lastUpdate.getTime <= ev.occurredMillis)
69

    
70
    if (!ev.validate())
71
      return Failed(new AccountingException("Event not valid"))
72

    
73
    val resource = Policy.policy.findResource(ev.resource) match {
74
      case Some(x) => x
75
      case None => return Failed(
76
        new AccountingException("No resource [%s]".format(ev.resource)))
77
    }
78

    
79
    val amount = resource.costpolicy match {
80
      case ContinuousCostPolicy => resState.asInstanceOf[Float]
81
      case DiscreteCostPolicy => ev.value
82
      case OnOffCostPolicy =>
83
        OnOffPolicyResourceState(resState) match {
84
          case OnResourceState =>
85
            OnOffPolicyResourceState(ev.value) match {
86
              case OnResourceState =>
87
                return Failed(new AccountingException(("Resource state " +
88
                  "transition error(was:%s, now:%s)").format(OnResourceState,
89
                  OnResourceState)))
90
              case OffResourceState => 0
91
            }
92
          case OffResourceState =>
93
            OnOffPolicyResourceState(ev.value) match {
94
              case OnResourceState => 1
95
              case OffResourceState =>
96
                return Failed(new AccountingException(("Resource state " +
97
                  "transition error(was:%s, now:%s)").format(OffResourceState,
98
                  OffResourceState)))
99
            }
100
        }
101
    }
102

    
103
    // We don't do strict checking for all cases for OnOffPolicies as
104
    // above, since this point won't be reached in case of error.
105
    val isFinal = resource.costpolicy match {
106
      case OnOffCostPolicy =>
107
        OnOffPolicyResourceState(resState) match {
108
          case OnResourceState => false
109
          case OffResourceState => true
110
        }
111
      case _ => true
112
    }
113

    
114
    val ts = resource.costpolicy match {
115
      case DiscreteCostPolicy => Timeslot(new Date(ev.occurredMillis),
116
        new Date(ev.occurredMillis + 1))
117
      case _ => Timeslot(lastUpdate, new Date(ev.occurredMillis))
118
    }
119

    
120
    val chargeChunks = calcChangeChunks(agreement, amount, resource, ts)
121

    
122
    val timeReceived = System.currentTimeMillis
123

    
124
    val rel = related.map{x => x.sourceEventIDs}.flatten ++ List(ev.id)
125

    
126
    val entries = chargeChunks.map {
127
      c =>
128
        WalletEntry(
129
          id = CryptoUtils.sha1(c.id),
130
          occurredMillis = ev.occurredMillis,
131
          receivedMillis = timeReceived,
132
          sourceEventIDs = rel,
133
          value = c.cost,
134
          reason = c.reason,
135
          userId = ev.userId,
136
          resource = ev.resource,
137
          instanceId = ev.getInstanceId,
138
          finalized = isFinal
139
        )
140
    }
141
    Just(entries)
142
  }
143

    
144
  def calcChangeChunks(agr: DSLAgreement, volume: Float,
145
                       res: DSLResource, t: Timeslot): List[ChargeChunk] = {
146

    
147
    val alg = resolveEffectiveAlgorithmsForTimeslot(t, agr)
148
    val pri = resolveEffectivePricelistsForTimeslot(t, agr)
149
    val chunks = splitChargeChunks(alg, pri)
150
    val algChunked = chunks._1
151
    val priChunked = chunks._2
152

    
153
    assert(algChunked.size == priChunked.size)
154

    
155
    res.costpolicy match {
156
      case DiscreteCostPolicy => calcChargeChunksDiscrete(algChunked, priChunked, volume, res)
157
      case _ => calcChargeChunksContinuous(algChunked, priChunked, volume, res)
158
    }
159
  }
160

    
161
  private[logic]
162
  def calcChargeChunksDiscrete(algChunked: Map[Timeslot, DSLAlgorithm],
163
                               priChunked: Map[Timeslot, DSLPriceList],
164
                               volume: Float, res: DSLResource): List[ChargeChunk] = {
165
    assert(algChunked.size == 1)
166
    assert(priChunked.size == 1)
167
    assert(algChunked.keySet.head.compare(priChunked.keySet.head) == 0)
168

    
169
    List(ChargeChunk(volume,
170
      algChunked.valuesIterator.next.algorithms.getOrElse(res, ""),
171
      priChunked.valuesIterator.next.prices.getOrElse(res, 0F),
172
      algChunked.keySet.head, res))
173
  }
174

    
175
  private[logic]
176
  def calcChargeChunksContinuous(algChunked: Map[Timeslot, DSLAlgorithm],
177
                                 priChunked: Map[Timeslot, DSLPriceList],
178
                                 volume: Float, res: DSLResource): List[ChargeChunk] = {
179
    algChunked.keySet.map {
180
      x =>
181
        ChargeChunk(volume,
182
          algChunked.get(x).get.algorithms.getOrElse(res, ""),
183
          priChunked.get(x).get.prices.getOrElse(res, 0F), x, res)
184
    }.toList
185
  }
186

    
187
  /**
188
   * Align charge timeslots between algorithms and pricelists. As algorithm
189
   * and pricelists can have different effectivity periods, this method
190
   * examines them and splits them as necessary.
191
   */
192
  private[logic] def splitChargeChunks(alg: SortedMap[Timeslot, DSLAlgorithm],
193
                        price: SortedMap[Timeslot, DSLPriceList]) :
194
    (Map[Timeslot, DSLAlgorithm], Map[Timeslot, DSLPriceList]) = {
195

    
196
    val zipped = alg.keySet.zip(price.keySet)
197

    
198
    zipped.find(p => !p._1.equals(p._2)) match {
199
      case None => (alg, price)
200
      case Some(x) =>
201
        val algTimeslot = x._1
202
        val priTimeslot = x._2
203

    
204
        assert(algTimeslot.from == priTimeslot.from)
205

    
206
        if (algTimeslot.endsAfter(priTimeslot)) {
207
          val slices = algTimeslot.slice(priTimeslot.to)
208
          val algo = alg.get(algTimeslot).get
209
          val newalg = alg - algTimeslot ++ Map(slices.apply(0) -> algo) ++ Map(slices.apply(1) -> algo)
210
          splitChargeChunks(newalg, price)
211
        }
212
        else {
213
          val slices = priTimeslot.slice(priTimeslot.to)
214
          val pl = price.get(priTimeslot).get
215
          val newPrice = price - priTimeslot ++ Map(slices.apply(0) -> pl) ++ Map(slices.apply(1) -> pl)
216
          splitChargeChunks(alg, newPrice)
217
        }
218
    }
219
  }
220
}
221

    
222
case class ChargeChunk(value: Float, algorithm: String,
223
                       price: Float, when: Timeslot,
224
                       resource: DSLResource) {
225
  assert(value > 0)
226
  assert(!algorithm.isEmpty)
227
  assert(resource != null)
228

    
229
  def cost(): Float =
230
    //TODO: Apply the algorithm, when we start parsing it
231
    resource.costpolicy match {
232
      case DiscreteCostPolicy =>
233
        value * price
234
      case _ =>
235
        value * price * when.hours
236
    }
237

    
238
  def reason(): String =
239
    resource.costpolicy match {
240
      case DiscreteCostPolicy =>
241
        "%f %s at %s @ %f/%s".format(value, resource.unit, when.from, price,
242
          resource.unit)
243
      case ContinuousCostPolicy =>
244
        "%f %s of %s from %s to %s @ %f/%s".format(value, resource.unit,
245
          resource.name, when.from, when.to, price, resource.unit)
246
      case OnOffCostPolicy =>
247
        "%f %s of %s from %s to %s @ %f/%s".format(when.hours, resource.unit,
248
          resource.name, when.from, when.to, price, resource.unit)
249
    }
250

    
251
  def id(): String =
252
    CryptoUtils.sha1("%f%s%f%s%s%d".format(value, algorithm, price, when.toString,
253
      resource.name, System.currentTimeMillis()))
254
}
255

    
256
/** An exception raised when something goes wrong with accounting */
257
class AccountingException(msg: String) extends Exception(msg)