Implement Once behavior with the new scheme. Refactor in the process
[aquarium] / src / main / scala / gr / grnet / aquarium / charging / ChargingBehaviorSkeleton.scala
1 /*
2  * Copyright 2011-2012 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.charging
37
38 import scala.collection.immutable
39 import scala.collection.mutable
40
41 import gr.grnet.aquarium.event.model.resource.ResourceEventModel
42 import gr.grnet.aquarium.{Aquarium, AquariumInternalError}
43 import gr.grnet.aquarium.policy.{FullPriceTable, EffectivePriceTable, UserAgreementModel, ResourceType}
44 import gr.grnet.aquarium.util._
45 import gr.grnet.aquarium.util.date.TimeHelpers
46 import gr.grnet.aquarium.charging.wallet.WalletEntry
47 import gr.grnet.aquarium.computation.{TimeslotComputations, BillingMonthInfo}
48 import gr.grnet.aquarium.charging.state.{WorkingResourceInstanceChargingState, WorkingResourcesChargingState, AgreementHistoryModel}
49 import gr.grnet.aquarium.logic.accounting.dsl.Timeslot
50 import gr.grnet.aquarium.store.PolicyStore
51
52 /**
53  * A charging behavior indicates how charging for a resource will be done
54  * wrt the various states a resource can be.
55  *
56  * @author Christos KK Loverdos <loverdos@gmail.com>
57  */
58
59 abstract class ChargingBehaviorSkeleton(
60     final val selectorLabelsHierarchy: List[String]
61 ) extends ChargingBehavior with Loggable {
62
63   protected def hrs(millis: Double) = {
64     val hours = millis / 1000 / 60 / 60
65     val roundedHours = hours
66     roundedHours
67   }
68
69   protected def rcDebugInfo(rcEvent: ResourceEventModel) = {
70     rcEvent.toDebugString
71   }
72
73   protected def newWorkingResourceInstanceChargingState() = {
74     new WorkingResourceInstanceChargingState(
75       mutable.Map(),
76       Nil,
77       Nil,
78       0.0,
79       0.0,
80       0.0,
81       0.0
82     )
83   }
84
85   final protected def ensureWorkingState(
86       workingResourcesChargingState: WorkingResourcesChargingState,
87       resourceEvent: ResourceEventModel
88   ) {
89     ensureResourcesChargingStateDetails(workingResourcesChargingState.details)
90     ensureResourceInstanceChargingState(workingResourcesChargingState, resourceEvent)
91   }
92
93   protected def ensureResourcesChargingStateDetails(
94     details: mutable.Map[String, Any]
95   ) {}
96
97   protected def ensureResourceInstanceChargingState(
98       workingResourcesChargingState: WorkingResourcesChargingState,
99       resourceEvent: ResourceEventModel
100   ) {
101
102     val instanceID = resourceEvent.instanceID
103     val stateOfResourceInstance = workingResourcesChargingState.stateOfResourceInstance
104
105     stateOfResourceInstance.get(instanceID) match {
106       case None ⇒
107         stateOfResourceInstance(instanceID) = newWorkingResourceInstanceChargingState()
108
109       case _ ⇒
110     }
111   }
112
113   protected def computeWalletEntriesForNewEvent(
114       resourceEvent: ResourceEventModel,
115       resourceType: ResourceType,
116       billingMonthInfo: BillingMonthInfo,
117       totalCredits: Double,
118       referenceTimeslot: Timeslot,
119       agreementByTimeslot: immutable.SortedMap[Timeslot, UserAgreementModel],
120       workingResourcesChargingStateDetails: mutable.Map[String, Any],
121       workingResourceInstanceChargingState: WorkingResourceInstanceChargingState,
122       policyStore: PolicyStore,
123       walletEntryRecorder: WalletEntry ⇒ Unit
124   ): (Int, Double) = {
125
126     val userID = resourceEvent.userID
127     val resourceEventDetails = resourceEvent.details
128
129     var _oldTotalCredits = totalCredits
130
131     var _newAccumulatingAmount = computeNewAccumulatingAmount(workingResourceInstanceChargingState, resourceEventDetails)
132     // It will also update the old one inside the data structure.
133     workingResourceInstanceChargingState.setNewAccumulatingAmount(_newAccumulatingAmount)
134
135     val policyByTimeslot = policyStore.loadAndSortPoliciesWithin(
136       referenceTimeslot.from.getTime,
137       referenceTimeslot.to.getTime
138     )
139
140     val effectivePriceTableSelector: FullPriceTable ⇒ EffectivePriceTable = fullPriceTable ⇒ {
141       this.selectEffectivePriceTable(
142         fullPriceTable,
143         workingResourcesChargingStateDetails,
144         workingResourceInstanceChargingState,
145         resourceEvent,
146         referenceTimeslot,
147         totalCredits
148       )
149     }
150
151     val initialChargeslots = TimeslotComputations.computeInitialChargeslots(
152       referenceTimeslot,
153       policyByTimeslot,
154       agreementByTimeslot,
155       effectivePriceTableSelector
156     )
157
158     val fullChargeslots = initialChargeslots.map {
159       case chargeslot@Chargeslot(startMillis, stopMillis, unitPrice, _, _) ⇒
160         val timeDeltaMillis = stopMillis - startMillis
161
162         val (creditsToSubtract, explanation) = this.computeCreditsToSubtract(
163           workingResourceInstanceChargingState,
164           _oldTotalCredits, // FIXME ??? Should recalculate ???
165           timeDeltaMillis,
166           unitPrice
167         )
168
169         val newChargeslot = chargeslot.copyWithCreditsToSubtract(creditsToSubtract, explanation)
170         newChargeslot
171     }
172
173     if(fullChargeslots.length == 0) {
174       throw new AquariumInternalError("No chargeslots computed for resource event %s".format(resourceEvent.id))
175     }
176
177     val sumOfCreditsToSubtract = fullChargeslots.map(_.creditsToSubtract).sum
178     val newTotalCredits = _oldTotalCredits - sumOfCreditsToSubtract
179
180     val newWalletEntry = WalletEntry(
181       userID,
182       sumOfCreditsToSubtract,
183       _oldTotalCredits,
184       newTotalCredits,
185       TimeHelpers.nowMillis(),
186       referenceTimeslot,
187       billingMonthInfo.year,
188       billingMonthInfo.month,
189       fullChargeslots,
190       resourceEvent :: workingResourceInstanceChargingState.previousEvents,
191       resourceType,
192       resourceEvent.isSynthetic
193     )
194
195     logger.debug("newWalletEntry = {}", newWalletEntry.toJsonString)
196
197     walletEntryRecorder.apply(newWalletEntry)
198
199     (1, newTotalCredits)
200   }
201
202
203   def selectEffectivePriceTable(
204       fullPriceTable: FullPriceTable,
205       workingChargingBehaviorDetails: mutable.Map[String, Any],
206       workingResourceInstanceChargingState: WorkingResourceInstanceChargingState,
207       currentResourceEvent: ResourceEventModel,
208       referenceTimeslot: Timeslot,
209       totalCredits: Double
210   ): EffectivePriceTable = {
211
212     val selectorPath = computeSelectorPath(
213       workingChargingBehaviorDetails,
214       workingResourceInstanceChargingState,
215       currentResourceEvent,
216       referenceTimeslot,
217       totalCredits
218     )
219
220     fullPriceTable.effectivePriceTableOfSelectorForResource(selectorPath, currentResourceEvent.safeResource)
221   }
222
223   /**
224    * A generic implementation for charging a resource event.
225    * TODO: Ditch this in favor of completely ahdoc behaviors.
226    *
227    * @return The number of wallet entries recorded and the new total credits
228    */
229   def processResourceEvent(
230       aquarium: Aquarium,
231       currentResourceEvent: ResourceEventModel,
232       resourceType: ResourceType,
233       billingMonthInfo: BillingMonthInfo,
234       workingResourcesChargingState: WorkingResourcesChargingState,
235       userAgreements: AgreementHistoryModel,
236       totalCredits: Double,
237       walletEntryRecorder: WalletEntry ⇒ Unit
238   ): (Int, Double) = {
239     (0,0)
240
241     /*val currentResourceEventDebugInfo = rcDebugInfo(currentResourceEvent)
242
243     val isBillable = this.isBillableEvent(currentResourceEvent)
244     val retval = if(!isBillable) {
245       // The resource event is not billable.
246       Debug(logger, "Ignoring not billable %s", currentResourceEventDebugInfo)
247       (0, totalCredits)
248     } else {
249       // The resource event is billable.
250       // Find the previous event if needed.
251       // This is (potentially) needed to calculate new credit amount and new resource instance amount
252       if(this.needsPreviousEventForCreditAndAmountCalculation) {
253         if(previousResourceEventOpt.isDefined) {
254           val previousResourceEvent = previousResourceEventOpt.get
255           val previousValue = previousResourceEvent.value
256
257           Debug(logger, "I have previous event %s", previousResourceEvent.toDebugString)
258
259           computeChargeslots(
260             chargingData,
261             previousResourceEventOpt,
262             currentResourceEvent,
263             billingMonthInfo,
264             Timeslot(previousResourceEvent.occurredMillis, currentResourceEvent.occurredMillis),
265             resourceType,
266             userAgreements.agreementByTimeslot,
267             previousValue,
268             totalCredits,
269             aquarium.policyStore,
270             walletEntryRecorder
271           )
272         } else {
273           // We do not have the needed previous event, so this must be the first resource event of its kind, ever.
274           // Let's see if we can create a dummy previous event.
275           val actualFirstEvent = currentResourceEvent
276
277           // FIXME: Why && ?
278           if(this.isBillableFirstEvent(actualFirstEvent) && this.mustGenerateDummyFirstEvent) {
279             Debug(logger, "First event of its kind %s", currentResourceEventDebugInfo)
280
281             val dummyFirst = this.constructDummyFirstEventFor(currentResourceEvent, billingMonthInfo.monthStartMillis)
282             Debug(logger, "Dummy first event %s", dummyFirst.toDebugString)
283
284             val previousResourceEvent = dummyFirst
285             val previousValue = previousResourceEvent.value
286
287             computeChargeslots(
288               chargingData,
289               Some(previousResourceEvent),
290               currentResourceEvent,
291               billingMonthInfo,
292               Timeslot(previousResourceEvent.occurredMillis, currentResourceEvent.occurredMillis),
293               resourceType,
294               userAgreements.agreementByTimeslot,
295               previousValue,
296               totalCredits,
297               aquarium.policyStore,
298               walletEntryRecorder
299             )
300           } else {
301             Debug(logger, "Ignoring first event of its kind %s", currentResourceEventDebugInfo)
302             // userStateWorker.updateIgnored(currentResourceEvent)
303             (0, totalCredits)
304           }
305         }
306       } else {
307         // No need for previous event. One event does it all.
308         computeChargeslots(
309           chargingData,
310           None,
311           currentResourceEvent,
312           billingMonthInfo,
313           Timeslot(currentResourceEvent.occurredMillis, currentResourceEvent.occurredMillis + 1),
314           resourceType,
315           userAgreements.agreementByTimeslot,
316           0.0,
317           totalCredits,
318           aquarium.policyStore,
319           walletEntryRecorder
320         )
321       }
322     }
323
324     retval*/
325   }
326
327
328   /**
329    * Given the charging state of a resource instance and the details of the incoming message, compute the new
330    * accumulating amount.
331    */
332   def computeNewAccumulatingAmount(
333       workingResourceInstanceChargingState: WorkingResourceInstanceChargingState,
334       eventDetails: Map[String, String]
335   ): Double
336
337
338   def constructDummyFirstEventFor(actualFirst: ResourceEventModel, newOccurredMillis: Long): ResourceEventModel = {
339
340     val newDetails = Map(
341       ResourceEventModel.Names.details_aquarium_is_synthetic   -> "true",
342       ResourceEventModel.Names.details_aquarium_is_dummy_first -> "true",
343       ResourceEventModel.Names.details_aquarium_reference_event_id -> actualFirst.id,
344       ResourceEventModel.Names.details_aquarium_reference_event_id_in_store -> actualFirst.stringIDInStoreOrEmpty
345     )
346
347     actualFirst.withDetailsAndValue(newDetails, 0.0, newOccurredMillis)
348   }
349 }