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.user
38 import gr.grnet.aquarium.store.ResourceEventStore
40 import gr.grnet.aquarium.util.date.DateCalculator
41 import com.ckkloverdos.maybe.{Failed, NoVal, Just, Maybe}
44 * The lowest (billing/charging) period is one week.
45 * By convention, all (billing/charging) periods are multiples of a week.
47 * @param name is the name of the period and it must not contain "-"
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")
54 def isWeek = weeks == 1
55 def isMonth = weeks == 4
56 def isMonthMultiple = (weeks % 4) == 0
58 def representation = "%s-%s".format(name, weeks)
60 def truncateToPeriod(millis: Long): Long = millis
61 def truncateToPeriod(date: Date): Date = new Date(truncateToPeriod(date.getTime))
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
70 def fromRepresentation(bp: String): Maybe[PeriodType] = {
72 case WeeklyBillingPeriodRepr ⇒
73 Just(WeeklyBillingPeriod)
74 case MonthlyBillingPeriodRepr ⇒
75 Just(MonthlyBillingPeriod)
77 val dashIndex = bp.lastIndexOf('-')
78 val name = bp.substring(0, dashIndex)
79 val weeks = bp.substring(dashIndex + 1).toInt
81 Maybe(PeriodType(name, weeks))
86 //sealed trait AnyPeriod {
87 // def startMillis: Long
88 // def periodType: PeriodType
91 //case class BillingPeriod(startMillis: Long, periodType: PeriodType) extends AnyPeriod
92 //case class ChargingPeriod(startMillis: Long, periodType: PeriodType) extends AnyPeriod
94 sealed abstract class CalculationType(_name: String) {
99 * Normal calculations that are part of the bill generation procedure
101 case object PeriodicCalculation extends CalculationType("periodic")
104 * Adhoc calculations, e.g. when computing the state in realtime.
106 case object AdhocCalculation extends CalculationType("adhoc")
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?)
116 * So, the are two kinds of periods:
119 * These are counted in the same way, i.e. a weekly-
123 case class SingleEventChargingEntry(
128 resourceEventId: String,
129 resourceEventOccurredMillis: Long, // caching some resourceEvent-oriented values here
130 resourceEventReceivedMillis: Long,
135 case class MultipleEventsChargingEntry(
140 resourceEventIds: List[String],
141 resourceEventOccurredMillis: List[Long], // caching some resourceEvent-oriented values here
142 resourceEventReceivedMillis: List[Long],
148 * The full stage of a usage period
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
156 singleEventEntries: List[SingleEventChargingEntry],
157 multipleEventEntries: List[MultipleEventsChargingEntry]
161 * The full state of a billing period may contain charges for several usage periods,
162 * due to "out of bounds" events.
164 case class BillingPeriodFullState(
166 periodType: PeriodType,
171 usagePeriods: List[UsagePeriodFullState])
173 case class AccountingStatus(
177 trait UserPolicyFinder {
178 def findUserPolicyAt(userId: String, whenMillis: Long)
181 trait FullStateFinder {
182 def findFullState(userId: String, whenMillis: Long): Any
185 trait UserStateCache {
186 def findUserStateAtEndOfPeriod(userId: String, year: Int, month: Int): Maybe[UserState]
188 def findLatestUserStateForBillingPeriod(userId: String, yearOfBillingMonth: Int, billingMonth: Int): Maybe[UserState]
193 * @author Christos KK Loverdos <loverdos@gmail.com>
195 object UserStateComputations {
196 def createFirstUserState(userId: String, agreementName: String) = {
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)
216 def doBillingForFullMonth( billingYear: Int,
219 policyFinder: UserPolicyFinder,
220 fullStateFinder: FullStateFinder,
221 userStateFinder: UserStateCache,
222 timeUnitInMillis: Long,
223 rcEventStore: ResourceEventStore,
224 otherStuff: Traversable[Any]): UserState = {
226 val startBillingDate = new DateCalculator(billingYear, billingMonth, 1)
227 val endBillingDate = startBillingDate.endOfThisMonth
229 doBillingAtStartOfMonth_(
232 endBillingDate.toMillis,
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.
249 def doBillingAtStartOfMonth_(startBillingYear: Int,
250 startBillingMonth: Int,
251 stopBillingMillis: Long,
253 policyFinder: UserPolicyFinder,
254 fullStateFinder: FullStateFinder,
255 userStateFinder: UserStateCache,
256 timeUnitInMillis: Long,
257 rcEventStore: ResourceEventStore,
258 otherStuff: Traversable[Any]): UserState = {
260 // Start of month for which billing is calculated
261 val startBillingDate = new DateCalculator(startBillingYear, startBillingMonth, 1)
262 val startBillingMillis = startBillingDate.toMillis
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))
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)
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)
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)
290 val previousBillingMonthStartDate = startBillingDate.previousMonth
291 val yearOfPreviousBillingMonthStartDate = previousBillingMonthStartDate.year
292 val monthOfPreviousBillingMonthStartDate = previousBillingMonthStartDate.monthOfYear
293 val previousUserStateM = userStateFinder.findUserStateAtEndOfPeriod(userId, yearOfPreviousBillingMonthStartDate, monthOfPreviousBillingMonthStartDate)
295 previousUserStateM match {
296 case Just(previousUserState) ⇒
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,
316 throw new Exception(m, e)
319 // OK. We have some "out of sync" resource events that will lead to a new state for the previous billing period
322 // previous billing month
323 val previousBillingMonthStartDate = startBillingDate.previousMonth
329 null.asInstanceOf[UserState]
333 * Get the user state as computed up to (and not including) the start of the new billing period.
335 * Always compute, taking into account any "out of sync" resource events
337 def computeUserStateAtStartOfBillingPeriod(billingYear: Int,
339 knownUserState: UserState): UserState = {
341 val billingDate = new DateCalculator(billingYear, billingMonth, 1)
342 val billingDateMillis = billingDate.toMillis
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
350 createFirstUserState(userId, agreementName)
352 // We really need to compute the user state here
354 // get all events that
362 * Do a full month billing.
364 * Takes into account "out of sync events".
367 def computeFullMonthlyBilling(yearOfBillingMonth: Int,
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 {
378 val billingMonthStartDate = new DateCalculator(yearOfBillingMonth, billingMonth, 1)
379 val prevBillingMonthStartDate = billingMonthStartDate.previousMonth
380 val yearOfPrevBillingMonth = prevBillingMonthStartDate.year
381 val prevBillingMonth = prevBillingMonthStartDate.monthOfYear
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)
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
391 // Check how many resource events were used to produce this user state
392 val cachedHowmanyRCEvents = cachedStartUserState.resourceEventsCounter
394 // Ask resource event store to see if we had any "out of sync" events for the particular (== previous)
396 val prevHowmanyOutOfSyncRCEvents = rcEventStore.countOutOfSyncEventsForBillingMonth(
398 yearOfPrevBillingMonth,
401 val recomputedStartUserState = if(prevHowmanyOutOfSyncRCEvents == 0) {
402 // This is good, can return the cached value
405 // "Out of sync" resource events means re-computation
406 computeUserStateAtStartOfBillingPeriod(yearOfPrevBillingMonth, prevBillingMonth, cachedStartUserState)
409 (cachedStartUserState, recomputedStartUserState)
411 // We do not even have a cached value, so perform re-computation
412 val recomputedStartUserState = computeUserStateAtStartOfBillingPeriod(yearOfPrevBillingMonth, prevBillingMonth, currentUserState)
413 (recomputedStartUserState, recomputedStartUserState)
415 throw new Exception(m, e)
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)
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
429 for(currentRCEvent <- allBillingPeriodRelevantRCEvents) {
430 // We need to do these kinds of calculations:
431 // 1. Credit state calculations
432 // 2. Resource state calculations
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
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.
458 def doPartialMonthlyBilling(startBillingYear: Int,
459 startBillingMonth: Int,
460 stopBillingMillis: Long,
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 {
470 // Start of month for which billing is calculated
471 val startBillingDate = new DateCalculator(startBillingYear, startBillingMonth, 1)
472 val startBillingMillis = startBillingDate.toMillis
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))
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,
489 // Get all relevant events.
492 null.asInstanceOf[UserState]