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