Some new billing stuff.
[aquarium] / src / main / scala / gr / grnet / aquarium / user / UserStateComputations.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.user
37
38 import gr.grnet.aquarium.store.ResourceEventStore
39 import java.util.Date
40 import gr.grnet.aquarium.util.date.DateCalculator
41 import com.ckkloverdos.maybe.{Failed, NoVal, Just, Maybe}
42
43 /**
44  * The lowest (billing/charging) period is one week.
45  * By convention, all (billing/charging) periods are multiples of a week.
46  *
47  * @param name is the name of the period and it must not contain "-"
48  */
49 case class PeriodType(name: String, weeks: Int) {
50   require(name ne null, "null name")
51   require(name.indexOf('-') == -1, "name must not contain '-'")
52   require(weeks > 0, "weeks must be positive")
53   
54   def isWeek  = weeks == 1
55   def isMonth = weeks == 4
56   def isMonthMultiple = (weeks % 4) == 0
57
58   def representation = "%s-%s".format(name, weeks)
59
60   def truncateToPeriod(millis: Long): Long = millis
61   def truncateToPeriod(date: Date): Date   = new Date(truncateToPeriod(date.getTime))
62 }
63
64 object PeriodType {
65   final val WeeklyBillingPeriod  = PeriodType("week",  1)
66   final val WeeklyBillingPeriodRepr  = WeeklyBillingPeriod.representation
67   final val MonthlyBillingPeriod = PeriodType("month", 4)
68   final val MonthlyBillingPeriodRepr = MonthlyBillingPeriod.representation
69
70   def fromRepresentation(bp: String): Maybe[PeriodType] = {
71     bp match {
72       case WeeklyBillingPeriodRepr ⇒
73         Just(WeeklyBillingPeriod)
74       case MonthlyBillingPeriodRepr ⇒
75         Just(MonthlyBillingPeriod)
76       case _ ⇒
77         val dashIndex = bp.lastIndexOf('-')
78         val name  = bp.substring(0, dashIndex)
79         val weeks = bp.substring(dashIndex + 1).toInt
80
81         Maybe(PeriodType(name, weeks))
82     }
83   }
84 }
85
86 //sealed trait AnyPeriod {
87 //  def startMillis: Long
88 //  def periodType: PeriodType
89 //}
90 //
91 //case class BillingPeriod(startMillis: Long, periodType: PeriodType) extends AnyPeriod
92 //case class ChargingPeriod(startMillis: Long, periodType: PeriodType) extends AnyPeriod
93
94 sealed abstract class CalculationType(_name: String) {
95   def name = _name
96 }
97
98 /**
99  * Normal calculations that are part of the bill generation procedure
100  */
101 case object PeriodicCalculation extends CalculationType("periodic")
102
103 /**
104  * Adhoc calculations, e.g. when computing the state in realtime.
105  */
106 case object AdhocCalculation extends CalculationType("adhoc")
107
108 /**
109  * - A billing run for a period is executed.
110  * - The outcome of the billing run is a bill.
111  * - A bill is made of charging statements for resources that have been used in several usage periods.
112  *   For example, the bill of this month can contain
113  *   - Charges for this month
114  *   - Charges for previous months (which pricelist applies here, the past or the current one?)
115  *
116  * So, the are two kinds of periods:
117  * - Billing periods
118  * - Usage periods
119  * These are counted in the same way, i.e. a weekly-
120  */
121 case class BigDocs()
122
123 case class SingleEventChargingEntry(
124     u: Double,
125     du: Double,
126     t: Long,
127     dt: Long,
128     resourceEventId: String,
129     resourceEventOccurredMillis: Long, // caching some resourceEvent-oriented values here
130     resourceEventReceivedMillis: Long,
131     costPolicy: String,
132     resource: String,
133     instanceId: String)
134
135 case class MultipleEventsChargingEntry(
136     u: Double,
137     du: Double,
138     t: Long,
139     dt: Long,
140     resourceEventIds: List[String],
141     resourceEventOccurredMillis: List[Long], // caching some resourceEvent-oriented values here
142     resourceEventReceivedMillis: List[Long],
143     costPolicy: String,
144     resource: String,
145     instanceId: String)
146
147 /**
148  * The full stage of a usage period
149  */
150 case class UsagePeriodFullState(
151     periodType: PeriodType,
152     periodStart: Long, // (inclusive) truncated at start of period, this is not the time of the oldest event/charge
153     periodStop: Long,  // (exclusive) truncated at end of period, this is not the time of the newest event/charge
154     oldestMillis: Long,
155     newestMillis: Long,
156     singleEventEntries: List[SingleEventChargingEntry],
157     multipleEventEntries: List[MultipleEventsChargingEntry]
158     )
159
160 /**
161  * The full state of a billing period may contain charges for several usage periods,
162  * due to "out of bounds" events.
163  */
164 case class BillingPeriodFullState(
165     userId: String,
166     periodType: PeriodType,
167     periodStart: Long,
168     periodEnd: Long,
169     oldestMillis: Long,
170     newestMillis: Long,
171     usagePeriods: List[UsagePeriodFullState])
172
173 case class AccountingStatus(
174     userId: String,
175     foo: String)
176
177 trait UserPolicyFinder {
178   def findUserPolicyAt(userId: String, whenMillis: Long)
179 }
180
181 trait FullStateFinder {
182   def findFullState(userId: String, whenMillis: Long): Any
183 }
184
185 trait UserStateCache {
186   def findUserStateAtEndOfPeriod(userId: String, year: Int, month: Int): Maybe[UserState]
187   
188   def findLatestUserStateForBillingPeriod(userId: String, yearOfBillingMonth: Int, billingMonth: Int): Maybe[UserState]
189 }
190
191 /**
192  *
193  * @author Christos KK Loverdos <loverdos@gmail.com>
194  */
195 object UserStateComputations {
196   def createFirstUserState(userId: String, agreementName: String) = {
197     val now = 0L
198     UserState(
199       userId,
200       now,
201       0L,
202       false,
203       null,
204       0L,
205       ActiveSuspendedSnapshot(false, now),
206       CreditSnapshot(0, now),
207       AgreementSnapshot(agreementName, now),
208       RolesSnapshot(List(), now),
209       PaymentOrdersSnapshot(Nil, now),
210       OwnedGroupsSnapshot(Nil, now),
211       GroupMembershipsSnapshot(Nil, now),
212       OwnedResourcesSnapshot(List(), now)
213     )
214   }
215
216   def doBillingForFullMonth( billingYear: Int,
217                              billingMonth: Int,
218                              userId: String,
219                              policyFinder: UserPolicyFinder,
220                              fullStateFinder: FullStateFinder,
221                              userStateFinder: UserStateCache,
222                              timeUnitInMillis: Long,
223                              rcEventStore: ResourceEventStore,
224                              otherStuff: Traversable[Any]): UserState = {
225
226     val startBillingDate = new DateCalculator(billingYear, billingMonth, 1)
227     val endBillingDate = startBillingDate.endOfThisMonth
228
229     doBillingAtStartOfMonth_(
230       billingYear,
231       billingMonth,
232       endBillingDate.toMillis,
233       userId,
234       policyFinder,
235       fullStateFinder,
236       userStateFinder,
237       timeUnitInMillis,
238       rcEventStore,
239       otherStuff
240     )
241   }
242
243   /**
244    * Runs the billing algorithm on the specified period.
245    * By default, a billing period is monthly.
246    * The start of the billing period is midnight of the first day of the month we compute the bill for.
247    *
248    */
249   def doBillingAtStartOfMonth_(startBillingYear: Int,
250                               startBillingMonth: Int,
251                               stopBillingMillis: Long,
252                               userId: String,
253                               policyFinder: UserPolicyFinder,
254                               fullStateFinder: FullStateFinder,
255                               userStateFinder: UserStateCache,
256                               timeUnitInMillis: Long,
257                               rcEventStore: ResourceEventStore,
258                               otherStuff: Traversable[Any]): UserState = {
259
260     // Start of month for which billing is calculated
261     val startBillingDate = new DateCalculator(startBillingYear, startBillingMonth, 1)
262     val startBillingMillis = startBillingDate.toMillis
263
264     // Make sure the end date is within the month
265     val stopBillingDate = new DateCalculator(stopBillingMillis)
266     if(!startBillingDate.isSameYearAndMonthAs(stopBillingDate)) {
267       throw new Exception("Cannot calc billing for dates (%s, %s) that are not in the same month".format(startBillingDate, stopBillingDate))
268     }
269
270     // We bill based on events *received* within the billing period.
271     // These are all the events that were *received* (and not occurred) within the billing period.
272     // Some of them, though, may refer to previous billing period(s)
273     val allRCEvents = rcEventStore.findResourceEventsForReceivedPeriod(userId, startBillingMillis, stopBillingMillis)
274
275     // Get:
276     // a) Those resource events that have arrived within our billing period but refer to previous billing
277     // periods (via their occurredMillis). We call these "out of sync" events.
278     // b) Those events that have arrived within our billing period and refer to it as well
279     val (prevBillingPeriodsOutOfSyncRCEvents, thisBillingPeriodRCEvents) = allRCEvents.partition(_.occurredMillis < startBillingMillis)
280
281     // In order to start the billing for this period, we need a reference point from the previous billing period,
282     // so as to get the initial values for the resources.
283     // If we have no "out of sync" resource events, then we are set.
284     // Otherwise, we need to do some recalculation for the previous billing periods
285     val startUserState: UserState = if(prevBillingPeriodsOutOfSyncRCEvents.size == 0) {
286       // No "out-of-sync" resource events.
287       // We just need to query for the calculated user state at the end of the previous billing period
288       // (or calculate it now if it has not been cached)
289
290       val previousBillingMonthStartDate = startBillingDate.previousMonth
291       val yearOfPreviousBillingMonthStartDate = previousBillingMonthStartDate.year
292       val monthOfPreviousBillingMonthStartDate = previousBillingMonthStartDate.monthOfYear
293       val previousUserStateM = userStateFinder.findUserStateAtEndOfPeriod(userId, yearOfPreviousBillingMonthStartDate, monthOfPreviousBillingMonthStartDate)
294
295       previousUserStateM match {
296         case Just(previousUserState) ⇒
297           previousUserState
298
299         case NoVal ⇒
300           // We need to compute the user state for previous billing period.
301           // This could go on recursively until end of time
302           // ==> FIXME What is the recursion end condition????
303           //     FIXME : Probably the date the user entered the system
304           doBillingForFullMonth(
305             yearOfPreviousBillingMonthStartDate,
306             monthOfPreviousBillingMonthStartDate,
307             userId,
308             policyFinder,
309             fullStateFinder,
310             userStateFinder,
311             timeUnitInMillis,
312             rcEventStore,
313             Traversable())
314
315         case Failed(e, m) ⇒
316           throw new Exception(m, e)
317       }
318     } else {
319       // OK. We have some "out of sync" resource events that will lead to a new state for the previous billing period
320       // calculation.
321
322       // previous billing month
323       val previousBillingMonthStartDate = startBillingDate.previousMonth
324
325       null
326     }
327
328
329     null.asInstanceOf[UserState]
330   }
331
332   /**
333    * Get the user state as computed up to (and not including) the start of the new billing period.
334    *
335    * Always compute, taking into account any "out of sync" resource events
336    */
337   def computeUserStateAtStartOfBillingPeriod(billingYear: Int,
338                                              billingMonth: Int,
339                                              knownUserState: UserState): UserState = {
340
341     val billingDate = new DateCalculator(billingYear, billingMonth, 1)
342     val billingDateMillis = billingDate.toMillis
343
344     if(billingDateMillis < knownUserState.startDateMillis) {
345       val userId = knownUserState.userId
346       val agreementName = knownUserState.agreement match {
347         case null      ⇒ "default"
348         case agreement ⇒ agreement.data
349       }
350       createFirstUserState(userId, agreementName)
351     } else {
352       // We really need to compute the user state here
353
354       // get all events that
355       // FIXME: Implement
356       knownUserState
357     }
358   }
359
360
361   /**
362    * Do a full month billing.
363    *
364    * Takes into account "out of sync events".
365    * 
366    */
367   def computeFullMonthlyBilling(yearOfBillingMonth: Int,
368                                 billingMonth: Int,
369                                 userId: String,
370                                 policyFinder: UserPolicyFinder,
371                                 fullStateFinder: FullStateFinder,
372                                 userStateCache: UserStateCache,
373                                 timeUnitInMillis: Long,
374                                 rcEventStore: ResourceEventStore,
375                                 currentUserState: UserState,
376                                 otherStuff: Traversable[Any]): Maybe[UserState] = Maybe {
377
378     val billingMonthStartDate = new DateCalculator(yearOfBillingMonth, billingMonth, 1)
379     val prevBillingMonthStartDate = billingMonthStartDate.previousMonth
380     val yearOfPrevBillingMonth = prevBillingMonthStartDate.year
381     val prevBillingMonth = prevBillingMonthStartDate.monthOfYear
382
383     // Check if this value is already cached and valid, otherwise compute the value
384     // TODO : cache it in case of new computation
385     val cachedStartUserStateM = userStateCache.findLatestUserStateForBillingPeriod(userId, yearOfPrevBillingMonth, prevBillingMonth)
386
387     val (previousStartUserState, newStartUserState) = cachedStartUserStateM match {
388       case Just(cachedStartUserState) ⇒
389         // So, we do have a cached user state but must check if this is still valid
390
391         // Check how many resource events were used to produce this user state
392         val cachedHowmanyRCEvents = cachedStartUserState.resourceEventsCounter
393
394         // Ask resource event store to see if we had any "out of sync" events for the particular (== previous)
395         // billing period.
396         val prevHowmanyOutOfSyncRCEvents = rcEventStore.countOutOfSyncEventsForBillingMonth(
397           userId,
398           yearOfPrevBillingMonth,
399           prevBillingMonth)
400         
401         val recomputedStartUserState = if(prevHowmanyOutOfSyncRCEvents == 0) {
402           // This is good, can return the cached value
403           cachedStartUserState
404         } else {
405           // "Out of sync" resource events means re-computation
406           computeUserStateAtStartOfBillingPeriod(yearOfPrevBillingMonth, prevBillingMonth, cachedStartUserState)
407         }
408
409         (cachedStartUserState, recomputedStartUserState)
410       case NoVal ⇒
411         // We do not even have a cached value, so perform re-computation
412         val recomputedStartUserState = computeUserStateAtStartOfBillingPeriod(yearOfPrevBillingMonth, prevBillingMonth, currentUserState)
413         (recomputedStartUserState, recomputedStartUserState)
414       case Failed(e, m) ⇒
415         throw new Exception(m, e)
416     }
417
418     // OK. Now that we have a user state to start with (= start of billing period reference point),
419     // let us deal with the events themselves.
420     val billingStartMillis = billingMonthStartDate.toMillis
421     val billingStopMillis = billingMonthStartDate.endOfThisMonth.toMillis
422     val allBillingPeriodRelevantRCEvents = rcEventStore.findAllRelevantResourceEventsForBillingPeriod(userId, billingStartMillis, billingStopMillis)
423
424     type ResourceType = String
425     type ResourceInstanceType = String
426     val prevRCEventMap: scala.collection.mutable.Map[(ResourceType, ResourceInstanceType), Double] = scala.collection.mutable.Map()
427     var workingUserState = newStartUserState
428
429     for(currentRCEvent <- allBillingPeriodRelevantRCEvents) {
430       // We need to do these kinds of calculations:
431       // 1. Credit state calculations
432       // 2. Resource state calculations
433
434       // How credits are computed:
435       // - "onoff" events (think "vmtime"):
436       //   - need to be considered in on/off pairs
437       //   - just use the time difference of this event to the previous one for the credit computation
438       // - "discrete" events (think "bandwidth"):
439       //   - just use their value, which is a difference already for the credit computation
440       // - "continuous" events (think "bandwidth"):
441       //   - need the previous absolute value
442       //   - need the time difference of this event to the previous one
443       //   - use both the above (previous absolute value, time difference) for the credit computation
444
445     }
446
447
448     null
449   }
450
451
452   /**
453   * Runs the billing algorithm on the specified period.
454   * By default, a billing period is monthly.
455   * The start of the billing period is midnight of the first day of the month we compute the bill for.
456   *
457   */
458    def doPartialMonthlyBilling(startBillingYear: Int,
459                                startBillingMonth: Int,
460                                stopBillingMillis: Long,
461                                userId: String,
462                                policyFinder: UserPolicyFinder,
463                                fullStateFinder: FullStateFinder,
464                                userStateFinder: UserStateCache,
465                                timeUnitInMillis: Long,
466                                rcEventStore: ResourceEventStore,
467                                currentUserState: UserState,
468                                otherStuff: Traversable[Any]): Maybe[UserState] = Maybe {
469   
470      // Start of month for which billing is calculated
471      val startBillingDate = new DateCalculator(startBillingYear, startBillingMonth, 1)
472      val startBillingMillis = startBillingDate.toMillis
473
474      // Make sure the end date is within the month
475      val stopBillingDate = new DateCalculator(stopBillingMillis)
476      if(!startBillingDate.isSameYearAndMonthAs(stopBillingDate)) {
477        throw new Exception("Cannot calc billing for dates (%s, %s) that are not in the same month".format(startBillingDate, stopBillingDate))
478      }
479
480      // Get the user state at the end of the previous billing period
481      val previousBillingMonthStartDate = startBillingDate.previousMonth
482      val yearOfPreviousBillingMonthStartDate = previousBillingMonthStartDate.year
483      val monthOfPreviousBillingMonthStartDate = previousBillingMonthStartDate.monthOfYear
484      val previousUserState = computeUserStateAtStartOfBillingPeriod(
485        yearOfPreviousBillingMonthStartDate,
486        monthOfPreviousBillingMonthStartDate,
487        currentUserState)
488
489      // Get all relevant events.
490      
491
492      null.asInstanceOf[UserState]
493    }
494 }