Trivial fixes, toString implementation
[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 scala.collection.mutable
39
40 import com.ckkloverdos.maybe.{Failed, NoVal, Just, Maybe}
41 import gr.grnet.aquarium.logic.accounting.Accounting
42 import gr.grnet.aquarium.util.date.DateCalculator
43 import gr.grnet.aquarium.logic.accounting.dsl.{DSLResourcesMap, DSLCostPolicy, DSLPolicy}
44 import gr.grnet.aquarium.logic.events.ResourceEvent
45 import gr.grnet.aquarium.store.{PolicyStore, UserStateStore, ResourceEventStore}
46 import gr.grnet.aquarium.util.{ContextualLogger, Loggable}
47
48 /**
49  *
50  * @author Christos KK Loverdos <loverdos@gmail.com>
51  */
52 class UserStateComputations extends Loggable {
53   def createFirstUserState(userId: String, agreementName: String = "default") = {
54     val now = 0L
55     UserState(
56       userId,
57       now,
58       0L,
59       false,
60       null,
61       ImplicitOFFResourceEventsSnapshot(Map(), now),
62       Nil, Nil,
63       LatestResourceEventsSnapshot(Map(), now),
64       0L,
65       ActiveStateSnapshot(false, now),
66       CreditSnapshot(0, now),
67       AgreementSnapshot(Agreement(agreementName, now, -1) :: Nil, now),
68       RolesSnapshot(List(), now),
69       OwnedResourcesSnapshot(List(), now)
70     )
71   }
72
73   def createFirstUserState(userId: String, agreementName: String, resourcesMap: DSLResourcesMap) = {
74       val now = 0L
75       UserState(
76         userId,
77         now,
78         0L,
79         false,
80         null,
81         ImplicitOFFResourceEventsSnapshot(Map(), now),
82         Nil, Nil,
83         LatestResourceEventsSnapshot(Map(), now),
84         0L,
85         ActiveStateSnapshot(false, now),
86         CreditSnapshot(0, now),
87         AgreementSnapshot(Agreement(agreementName, now, - 1) :: Nil, now),
88         RolesSnapshot(List(), now),
89         OwnedResourcesSnapshot(List(), now)
90       )
91     }
92
93   def findUserStateAtEndOfBillingMonth(userId: String,
94                                        yearOfBillingMonth: Int,
95                                        billingMonth: Int,
96                                        userStateStore: UserStateStore,
97                                        resourceEventStore: ResourceEventStore,
98                                        policyStore: PolicyStore,
99                                        userCreationMillis: Long,
100                                        currentUserState: UserState,
101                                        zeroUserState: UserState, 
102                                        defaultPolicy: DSLPolicy,
103                                        defaultResourcesMap: DSLResourcesMap,
104                                        accounting: Accounting,
105                                        contextualLogger: Maybe[ContextualLogger] = NoVal): Maybe[UserState] = {
106
107     val clog = ContextualLogger.fromOther(
108       contextualLogger,
109       logger,
110       "findUserStateAtEndOfBillingMonth(%s-%02d)", yearOfBillingMonth, billingMonth)
111 //    val clog = new ContextualLogger(logger, "findUserStateAtEndOfBillingMonth(%s-%02d)", yearOfBillingMonth, billingMonth)
112     clog.begin()
113
114     def doCompute: Maybe[UserState] = {
115       clog.debug("Computing full month billing")
116       doFullMonthlyBilling(
117         userId,
118         yearOfBillingMonth,
119         billingMonth,
120         userStateStore,
121         resourceEventStore,
122         policyStore,
123         userCreationMillis,
124         currentUserState,
125         zeroUserState,
126         defaultPolicy,
127         defaultResourcesMap,
128         accounting,
129         Just(clog))
130     }
131
132     val billingMonthStartDateCalc = new DateCalculator(yearOfBillingMonth, billingMonth)
133     val userCreationDateCalc = new DateCalculator(userCreationMillis)
134     val billingMonthStartMillis = billingMonthStartDateCalc.toMillis
135     val billingMonthStopMillis  = billingMonthStartDateCalc.copy.goEndOfThisMonth.toMillis
136
137     if(billingMonthStopMillis < userCreationMillis) {
138       // If the user did not exist for this billing month, piece of cake
139       clog.debug("User did not exist before %s. Returning %s", userCreationDateCalc, zeroUserState)
140       clog.endWith(Just(zeroUserState))
141     } else {
142       resourceEventStore.countOutOfSyncEventsForBillingPeriod(userId, billingMonthStartMillis, billingMonthStopMillis) match {
143         case Just(outOfSyncEventCount) ⇒
144           // Have out of sync, so must recompute
145           clog.debug("Found %s out of sync events, will have to (re)compute user state", outOfSyncEventCount)
146           clog.endWith(doCompute)
147         case NoVal ⇒
148           // No out of sync events, ask DB cache
149           userStateStore.findLatestUserStateForEndOfBillingMonth(userId, yearOfBillingMonth, billingMonth) match {
150             case just @ Just(userState) ⇒
151               // Found from cache
152               clog.debug("Found from cache: %s", userState)
153               clog.endWith(just)
154             case NoVal ⇒
155               // otherwise compute
156               clog.debug("No user state found from cache, will have to (re)compute")
157               clog.endWith(doCompute)
158             case failed @ Failed(_, _) ⇒
159               clog.warn("Failure while quering cache for user state: %s", failed)
160               clog.endWith(failed)
161           }
162         case failed @ Failed(_, _) ⇒
163           clog.warn("Failure while querying for out of sync events: %s", failed)
164           clog.endWith(failed)
165       }
166     }
167   }
168
169   def doFullMonthlyBilling(userId: String,
170                            yearOfBillingMonth: Int,
171                            billingMonth: Int,
172                            userStateStore: UserStateStore,
173                            resourceEventStore: ResourceEventStore,
174                            policyStore: PolicyStore,
175                            userCreationMillis: Long,
176                            currentUserState: UserState,
177                            zeroUserState: UserState,
178                            defaultPolicy: DSLPolicy,
179                            defaultResourcesMap: DSLResourcesMap,
180                            accounting: Accounting,
181                            contextualLogger: Maybe[ContextualLogger] = NoVal): Maybe[UserState] = Maybe {
182
183
184     val billingMonthStartDateCalc = new DateCalculator(yearOfBillingMonth, billingMonth)
185     val billingMonthEndDateCalc   = billingMonthStartDateCalc.copy.goEndOfThisMonth
186     val previousBillingMonthCalc = billingMonthStartDateCalc.copy.goPreviousMonth
187     val previousBillingMonth = previousBillingMonthCalc.getMonthOfYear
188     val yearOfPreviousBillingMonth = previousBillingMonthCalc.getYear
189
190     val clog = ContextualLogger.fromOther(
191       contextualLogger,
192       logger,
193       "doFullMonthlyBilling(%s-%02d)", yearOfBillingMonth, billingMonth)
194     clog.begin()
195
196     val previousBillingMonthUserStateM = findUserStateAtEndOfBillingMonth(
197       userId,
198       yearOfPreviousBillingMonth,
199       previousBillingMonth,
200       userStateStore,
201       resourceEventStore,
202       policyStore,
203       userCreationMillis,
204       currentUserState,
205       zeroUserState,
206       defaultPolicy,
207       defaultResourcesMap,
208       accounting,
209       Just(clog)
210     )
211     
212     previousBillingMonthUserStateM match {
213       case NoVal ⇒
214         null // not really... (must throw an exception here probably...)
215       case failed @ Failed(e, _) ⇒
216         throw e
217       case Just(startingUserState) ⇒
218         // This is the real deal
219
220         // This is a collection of all the latest resource events.
221         // We want these in order to correlate incoming resource events with their previous (in `occurredMillis` time)
222         // ones.
223         // Will be updated on processing the next resource event.
224         val previousResourceEvents = startingUserState.latestResourceEvents.toMutableWorker
225         clog.debug("previousResourceEvents = %s", previousResourceEvents)
226
227         val billingMonthStartMillis = billingMonthStartDateCalc.toMillis
228         val billingMonthEndMillis  = billingMonthEndDateCalc.toMillis
229
230         // Keep the working (current) user state. This will get updated as we proceed with billing for the month
231         // specified in the parameters.
232         var _workingUserState = startingUserState
233
234         // Prepare the implicit OFF resource events
235         val theImplicitOFFs = _workingUserState.implicitOFFs.toMutableWorker
236         clog.debug("theImplicitOFFs = %s", theImplicitOFFs)
237
238         /**
239          * Finds the previous resource event by checking two possible sources: a) The implicit OFF resource events and
240          * b) the explicit previous resource events. If the event is found, it is removed from the respective source.
241          *
242          * If the event is not found, then this must be for a new resource instance.
243          * (and probably then some `zero` resource event must be implied as the previous one)
244          * 
245          * @param resource
246          * @param instanceId
247          * @return
248          */
249         def findAndRemovePreviousResourceEvent(resource: String, instanceId: String): Maybe[ResourceEvent] = {
250           // implicit OFFs are checked first
251           theImplicitOFFs.findAndRemoveResourceEvent(resource, instanceId) match {
252             case just @ Just(_) ⇒
253               just
254             case NoVal ⇒
255               // explicit previous are checked second
256               previousResourceEvents.findAndRemoveResourceEvent(resource, instanceId) match {
257                 case just @ Just(_) ⇒
258                   just
259                 case noValOrFailed ⇒
260                   noValOrFailed
261               }
262             case failed ⇒
263               failed
264           }
265         }
266
267         def rcDebugInfo(rcEvent: ResourceEvent) = {
268           rcEvent.toDebugString(defaultResourcesMap, false)
269         }
270
271         // Find the actual resource events from DB
272         val allResourceEventsForMonth = resourceEventStore.findAllRelevantResourceEventsForBillingPeriod(
273           userId,
274           billingMonthStartMillis,
275           billingMonthEndMillis)
276         var _eventCounter = 0
277
278         clog.debug("resourceEventStore = %s".format(resourceEventStore))
279         clog.debug("Found %s resource events, starting processing...", allResourceEventsForMonth.size)
280         
281         for {
282           currentResourceEvent <- allResourceEventsForMonth
283         } {
284           _eventCounter = _eventCounter + 1
285           val theResource = currentResourceEvent.resource
286           val theInstanceId = currentResourceEvent.instanceId
287           val theValue = currentResourceEvent.value
288
289           clog.indent()
290           clog.debug("Processing %s", currentResourceEvent)
291           clog.debug("Friendlier %s", rcDebugInfo(currentResourceEvent))
292           clog.indent()
293
294           if(previousResourceEvents.size > 0) {
295             clog.debug("%s previousResourceEvents", previousResourceEvents.size)
296             clog.indent()
297             previousResourceEvents.foreach(ev ⇒ clog.debug("%s", rcDebugInfo(ev)))
298             clog.unindent()
299           }
300           if(theImplicitOFFs.size > 0) {
301             clog.debug("%s theImplicitOFFs", theImplicitOFFs.size)
302             clog.indent()
303             theImplicitOFFs.foreach(ev ⇒ clog.debug("%s", rcDebugInfo(ev)))
304             clog.unindent()
305           }
306
307           // Ignore the event if it is not billable (but still record it in the "previous" stuff).
308           // But to make this decision, we need the cost policy.
309           val costPolicyM = currentResourceEvent.findCostPolicyM(defaultResourcesMap)
310           costPolicyM match {
311             // We have a cost policy
312             case Just(costPolicy) ⇒
313               clog.debug("Cost policy: %s", costPolicy)
314               val isBillable = costPolicy.isBillableEventBasedOnValue(currentResourceEvent.value)
315               isBillable match {
316                 // The resource event is not billable
317                 case false ⇒
318                   clog.debug("Ignoring not billable event (%s)", currentResourceEvent.beautifyValue(defaultResourcesMap))
319
320                 // The resource event is billable
321                 case true ⇒
322                   costPolicy.needsPreviousEventForCreditAndAmountCalculation match {
323                     // We need the previous event to calculate credit & amount
324                     case true  ⇒
325                       val previousResourceEventM = findAndRemovePreviousResourceEvent(theResource, theInstanceId)
326
327                       previousResourceEventM match {
328                         // Found previous event
329                         case Just(previousResourceEvent) ⇒
330                           clog.debug("Previous %s", rcDebugInfo(previousResourceEvent))
331
332                           // A. Compute new resource instance accumulating amount
333                           //    But first ask for the current one
334                           val (previousAmountM, newAmount) = costPolicy.accumulatesAmount match {
335                             // The amount for this resource is accumulating
336                             case true  ⇒
337                               val defaultInstanceAmount = costPolicy.getResourceInstanceInitialAmount
338                               val previousAmount = _workingUserState.getResourceInstanceAmount(
339                                 theResource,
340                                 theInstanceId,
341                                 defaultInstanceAmount)
342
343                               val newAmount = costPolicy.computeNewAccumulatingAmount(previousAmount, theValue)
344                               (Just(previousAmount), newAmount)
345
346                             // The amount for this resource is not accumulating
347                             case false ⇒
348                               val newAmount = costPolicy.computeNewAccumulatingAmount(-1.0, theValue)
349                               (NoVal, newAmount)
350                           }
351
352                           // C. Compute new wallet entries
353                           /*val walletEntriesM = accounting.computeWalletEntriesForAgreement(
354                             userId,
355                             _workingUserState.credits.creditAmount,
356                             costPolicy,
357                             previousResourceEventM,
358                             previousAmountM,
359                             currentResourceEvent,
360                             defaultResourcesMap,
361                             defaultPolicy.agreements.head)*/
362
363                         // B. Compute new credit amount
364
365                         // Did not find previous event.
366                         case NoVal ⇒
367                         // So it must be the first ever, in which case we assume a previous value of zero
368
369                         // Could not find previous event
370                         case failed@Failed(_, _) ⇒
371                           clog.error(failed)
372                       }
373
374                     // We do not need the previous event to calculate credit & amount
375                     case false ⇒
376                       clog.debug("No need for previous event")
377                   }
378                   
379                   ()
380               }
381
382               // After processing, all event, billable or not update the previous state
383               previousResourceEvents.updateResourceEvent(currentResourceEvent)
384
385             // We do not have a cost policy
386             case NoVal ⇒
387               // Now, this is a matter of politics: what do we do if no policy was found?
388               clog.error("No cost policy for %s", rcDebugInfo(currentResourceEvent))
389
390             // Could not retrieve cost policy
391             case failed @ Failed(e, m) ⇒
392               clog.error("Error obtaining cost policy for %s", rcDebugInfo(currentResourceEvent))
393               clog.error(e, m)
394           }
395
396           clog.unindent()
397           clog.unindent()
398         }
399         
400
401         clog.endWith(_workingUserState)
402     }
403   }
404 }