Finally, returning the fully computed charge slots
[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
39 import com.ckkloverdos.maybe.{Failed, NoVal, Just, Maybe}
40 import gr.grnet.aquarium.util.date.MutableDateCalc
41 import gr.grnet.aquarium.logic.accounting.dsl.{DSLResourcesMap, DSLPolicy}
42 import gr.grnet.aquarium.logic.events.ResourceEvent
43 import gr.grnet.aquarium.store.{PolicyStore, UserStateStore, ResourceEventStore}
44 import gr.grnet.aquarium.util.{ContextualLogger, Loggable}
45 import gr.grnet.aquarium.logic.accounting.Accounting
46 import gr.grnet.aquarium.logic.accounting.algorithm.SimpleCostPolicyAlgorithmCompiler
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       ImplicitlyIssuedResourceEventsSnapshot(List(), now),
62       Nil, Nil,
63       LatestResourceEventsSnapshot(List(), now),
64       0L, 0L,
65       ActiveStateSnapshot(false, now),
66       CreditSnapshot(0, now),
67       AgreementSnapshot(Agreement(agreementName, now) :: 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         ImplicitlyIssuedResourceEventsSnapshot(List(), now),
82         Nil, Nil,
83         LatestResourceEventsSnapshot(List(), now),
84         0L, 0L,
85         ActiveStateSnapshot(false, now),
86         CreditSnapshot(0, now),
87         AgreementSnapshot(Agreement(agreementName, now) :: Nil, now),
88         RolesSnapshot(List(), now),
89         OwnedResourcesSnapshot(List(), now)
90       )
91     }
92
93   def findUserStateAtEndOfBillingMonth(userId: String,
94                                        billingMonthInfo: BillingMonthInfo,
95                                        userStateStore: UserStateStore,
96                                        resourceEventStore: ResourceEventStore,
97                                        policyStore: PolicyStore,
98                                        userCreationMillis: Long,
99                                        currentUserState: UserState,
100                                        zeroUserState: UserState, 
101                                        defaultPolicy: DSLPolicy,
102                                        defaultResourcesMap: DSLResourcesMap,
103                                        accounting: Accounting,
104                                        contextualLogger: Maybe[ContextualLogger] = NoVal): Maybe[UserState] = {
105
106     val clog = ContextualLogger.fromOther(
107       contextualLogger,
108       logger,
109       "findUserStateAtEndOfBillingMonth(%s)", billingMonthInfo)
110     clog.begin()
111
112     def doCompute: Maybe[UserState] = {
113       clog.debug("Computing full month billing")
114       doFullMonthlyBilling(
115         userId,
116         billingMonthInfo,
117         userStateStore,
118         resourceEventStore,
119         policyStore,
120         userCreationMillis,
121         currentUserState,
122         zeroUserState,
123         defaultPolicy,
124         defaultResourcesMap,
125         accounting,
126         Just(clog))
127     }
128
129     val userCreationDateCalc = new MutableDateCalc(userCreationMillis)
130     val billingMonthStartMillis = billingMonthInfo.startMillis
131     val billingMonthStopMillis  = billingMonthInfo.stopMillis
132
133     if(billingMonthStopMillis < userCreationMillis) {
134       // If the user did not exist for this billing month, piece of cake
135       clog.debug("User did not exist before %s. Returning %s", userCreationDateCalc, zeroUserState)
136       clog.endWith(Just(zeroUserState))
137     } else {
138       // Ask DB cache for the latest known user state for this billing period
139       val latestUserStateM = userStateStore.findLatestUserStateForEndOfBillingMonth(
140         userId,
141         billingMonthInfo.year,
142         billingMonthInfo.month)
143
144       latestUserStateM match {
145         case NoVal ⇒
146           // Not found, must compute
147           clog.debug("No user state found from cache, will have to (re)compute")
148           clog.endWith(doCompute)
149           
150         case failed @ Failed(_, _) ⇒
151           clog.warn("Failure while quering cache for user state: %s", failed)
152           clog.endWith(failed)
153
154         case Just(latestUserState) ⇒
155           // Found a "latest" user state but need to see if it is indeed the true and one latest.
156           // For this reason, we must count the events again.
157          val latestStateOOSEventsCounter = latestUserState.billingPeriodOutOfSyncResourceEventsCounter
158          val actualOOSEventsCounterM = resourceEventStore.countOutOfSyncEventsForBillingPeriod(
159            userId,
160            billingMonthStartMillis,
161            billingMonthStopMillis)
162
163          actualOOSEventsCounterM match {
164            case NoVal ⇒
165              val errMsg = "No counter computed for out of sync events. Should at least be zero."
166              clog.warn(errMsg)
167              clog.endWith(Failed(new Exception(errMsg)))
168
169            case failed @ Failed(_, _) ⇒
170              clog.warn("Failure while querying for out of sync events: %s", failed)
171              clog.endWith(failed)
172
173            case Just(actualOOSEventsCounter) ⇒
174              val counterDiff = actualOOSEventsCounter - latestStateOOSEventsCounter
175              counterDiff match {
176                // ZERO, we are OK!
177                case 0 ⇒
178                  latestUserStateM
179
180                // We had more, so must recompute
181                case n if n > 0 ⇒
182                  clog.debug(
183                    "Found %s out of sync events (%s more), will have to (re)compute user state", actualOOSEventsCounter, n)
184                  clog.endWith(doCompute)
185
186                // We had less????
187                case n if n < 0 ⇒
188                  val errMsg = "Found %s out of sync events (%s less). DB must be inconsistent".format(actualOOSEventsCounter, n)
189                  clog.warn(errMsg)
190                  clog.endWith(Failed(new Exception(errMsg)))
191              }
192          }
193       }
194     }
195   }
196
197   def doFullMonthlyBilling(userId: String,
198                            billingMonthInfo: BillingMonthInfo,
199                            userStateStore: UserStateStore,
200                            resourceEventStore: ResourceEventStore,
201                            policyStore: PolicyStore,
202                            userCreationMillis: Long,
203                            currentUserState: UserState,
204                            zeroUserState: UserState,
205                            defaultPolicy: DSLPolicy,
206                            defaultResourcesMap: DSLResourcesMap,
207                            accounting: Accounting,
208                            contextualLogger: Maybe[ContextualLogger] = NoVal): Maybe[UserState] = Maybe {
209
210     val clog = ContextualLogger.fromOther(
211       contextualLogger,
212       logger,
213       "doFullMonthlyBilling(%s)", billingMonthInfo)
214     clog.begin()
215
216     val previousBillingMonthUserStateM = findUserStateAtEndOfBillingMonth(
217       userId,
218       billingMonthInfo.previousMonth,
219       userStateStore,
220       resourceEventStore,
221       policyStore,
222       userCreationMillis,
223       currentUserState,
224       zeroUserState,
225       defaultPolicy,
226       defaultResourcesMap,
227       accounting,
228       Just(clog)
229     )
230     
231     previousBillingMonthUserStateM match {
232       case NoVal ⇒
233         null // not really... (must throw an exception here probably...)
234       case failed @ Failed(e, _) ⇒
235         throw e
236       case Just(startingUserState) ⇒
237         // This is the real deal
238
239         // This is a collection of all the latest resource events.
240         // We want these in order to correlate incoming resource events with their previous (in `occurredMillis` time)
241         // ones.
242         // Will be updated on processing the next resource event.
243         val previousResourceEvents = startingUserState.latestResourceEventsSnapshot.toMutableWorker
244         clog.debug("previousResourceEvents = %s", previousResourceEvents)
245
246         val billingMonthStartMillis = billingMonthInfo.startMillis
247         val billingMonthEndMillis = billingMonthInfo.stopMillis
248
249         // Keep the working (current) user state. This will get updated as we proceed with billing for the month
250         // specified in the parameters.
251         var _workingUserState = startingUserState
252
253         // Prepare the implicit OFF resource events
254         // (we keep the terminology for historical reasons)
255         val theImplicitOFFs = _workingUserState.implicitlyTerminatedSnapshot.toMutableWorker
256         clog.debug("theImplicitOFFs = %s", theImplicitOFFs)
257
258         /**
259          * Finds the previous resource event by checking two possible sources: a) The implicit OFF resource events and
260          * b) the explicit previous resource events. If the event is found, it is removed from the respective source.
261          *
262          * If the event is not found, then this must be for a new resource instance.
263          * (and probably then some `zero` resource event must be implied as the previous one)
264          * 
265          * @param resource
266          * @param instanceId
267          * @return
268          */
269         def findAndRemovePreviousResourceEvent(resource: String, instanceId: String): Maybe[ResourceEvent] = {
270           // implicit OFFs are checked first
271           theImplicitOFFs.findAndRemoveResourceEvent(resource, instanceId) match {
272             case just @ Just(_) ⇒
273               just
274             case NoVal ⇒
275               // explicit previous are checked second
276               previousResourceEvents.findAndRemoveResourceEvent(resource, instanceId) match {
277                 case just @ Just(_) ⇒
278                   just
279                 case noValOrFailed ⇒
280                   noValOrFailed
281               }
282             case failed ⇒
283               failed
284           }
285         }
286
287         def rcDebugInfo(rcEvent: ResourceEvent) = {
288           rcEvent.toDebugString(defaultResourcesMap, false)
289         }
290
291         // Find the actual resource events from DB
292         val allResourceEventsForMonth = resourceEventStore.findAllRelevantResourceEventsForBillingPeriod(
293           userId,
294           billingMonthStartMillis,
295           billingMonthEndMillis)
296         var _eventCounter = 0
297
298         clog.debug("resourceEventStore = %s".format(resourceEventStore))
299         clog.debug("Found %s resource events, starting processing...", allResourceEventsForMonth.size)
300         
301         for {
302           currentResourceEvent <- allResourceEventsForMonth
303         } {
304           _eventCounter = _eventCounter + 1
305           val theResource = currentResourceEvent.safeResource
306           val theInstanceId = currentResourceEvent.safeInstanceId
307           val theValue = currentResourceEvent.value
308
309           clog.indent()
310           clog.debug("Processing %s", currentResourceEvent)
311           clog.debug("Friendlier %s", rcDebugInfo(currentResourceEvent))
312           clog.indent()
313
314           if(previousResourceEvents.size > 0) {
315             clog.debug("%s previousResourceEvents", previousResourceEvents.size)
316             clog.indent()
317             previousResourceEvents.foreach(ev ⇒ clog.debug("%s", rcDebugInfo(ev)))
318             clog.unindent()
319           }
320           if(theImplicitOFFs.size > 0) {
321             clog.debug("%s theImplicitOFFs", theImplicitOFFs.size)
322             clog.indent()
323             theImplicitOFFs.foreach(ev ⇒ clog.debug("%s", rcDebugInfo(ev)))
324             clog.unindent()
325           }
326
327           // Ignore the event if it is not billable (but still record it in the "previous" stuff).
328           // But to make this decision, first we need the resource definition (and its cost policy).
329           val resourceDefM = defaultResourcesMap.findResourceM(theResource)
330           resourceDefM match {
331             // We have a resource (and thus a cost policy)
332             case Just(resourceDef) ⇒
333               val costPolicy = resourceDef.costPolicy
334               clog.debug("Cost policy: %s", costPolicy)
335               val isBillable = costPolicy.isBillableEventBasedOnValue(theValue)
336               isBillable match {
337                 // The resource event is not billable
338                 case false ⇒
339                   clog.debug("Ignoring not billable event %s", rcDebugInfo(currentResourceEvent))
340
341                 // The resource event is billable
342                 case true ⇒
343                   // Find the previous event.
344                   // This is (potentially) needed to calculate new credit amount and new resource instance amount
345                   val previousResourceEventM = findAndRemovePreviousResourceEvent(theResource, theInstanceId)
346                   clog.debug("PreviousM %s", previousResourceEventM.map(rcDebugInfo(_)))
347                   val defaultInitialAmount = costPolicy.getResourceInstanceInitialAmount
348                   val oldAmount = _workingUserState.getResourceInstanceAmount(theResource, theInstanceId, defaultInitialAmount)
349                   val oldCredits = _workingUserState.creditsSnapshot.creditAmount
350
351                   // A. Compute new resource instance accumulating amount
352                   val newAmount = costPolicy.computeNewAccumulatingAmount(oldAmount, theValue)
353
354                   // B. Compute new wallet entries
355                   val alltimeAgreements = _workingUserState.agreementsSnapshot.agreementsByTimeslot
356
357                   accounting.computeChargeChunks(
358                     previousResourceEventM,
359                     currentResourceEvent,
360                     oldCredits,
361                     oldAmount,
362                     newAmount,
363                     resourceDef,
364                     defaultResourcesMap,
365                     alltimeAgreements,
366                     SimpleCostPolicyAlgorithmCompiler
367                   )
368
369                   // C. Compute new credit amount (based on the wallet entries)
370                   // Maybe this can be consolidated inthe previous step (B)
371
372
373                   ()
374               }
375
376               // After processing, all event, billable or not update the previous state
377               previousResourceEvents.updateResourceEvent(currentResourceEvent)
378
379             // We do not have a resource (and no cost policy)
380             case NoVal ⇒
381               // Now, this is a matter of politics: what do we do if no policy was found?
382               clog.error("No cost policy for %s", rcDebugInfo(currentResourceEvent))
383
384             // Could not retrieve resource (unlikely to happen)
385             case failed @ Failed(e, m) ⇒
386               clog.error("Error obtaining cost policy for %s", rcDebugInfo(currentResourceEvent))
387               clog.error(e, m)
388           }
389
390           clog.unindent()
391           clog.unindent()
392         }
393         
394
395         clog.endWith(_workingUserState)
396     }
397   }
398 }