11package io .github .isagroup ;
22
3+ import java .util .ArrayList ;
4+ import java .util .HashMap ;
5+ import java .util .List ;
36import java .util .Map ;
47import java .util .stream .Collectors ;
58
9+ import org .slf4j .Logger ;
10+ import org .slf4j .LoggerFactory ;
611import org .springframework .stereotype .Component ;
712import org .yaml .snakeyaml .error .YAMLException ;
813
914import io .github .isagroup .exceptions .PricingPlanEvaluationException ;
1015import io .github .isagroup .models .PricingManager ;
16+ import io .github .isagroup .models .UsageLimit ;
1117import io .github .isagroup .services .yaml .YamlUtils ;
18+ import io .github .isagroup .models .AddOn ;
19+ import io .github .isagroup .models .Feature ;
1220import io .github .isagroup .models .Plan ;
1321
1422/**
1826@ Component
1927public abstract class PricingContext {
2028
29+ private static final Logger logger = LoggerFactory .getLogger (PricingContext .class );
30+
2131 /**
2232 * Returns path of the pricing configuration YAML file.
2333 * This file should be located in the resources folder, and the path should be
@@ -84,6 +94,37 @@ public Boolean userAffectedByPricing() {
8494 */
8595 public abstract String getUserPlan ();
8696
97+ /**
98+ * This method should return a list with the name of the add-ons contracted by
99+ * the current user.
100+ * With this information, the library will be able to build the subscription of
101+ * the user
102+ * from the configuration.
103+ *
104+ * @return List<String> with the current user's contracted add-ons. Add-on names
105+ * should be the same as in the pricing configuration file.
106+ *
107+ */
108+ public abstract List <String > getUserAddOns ();
109+
110+ /**
111+ * Returns a list with the full subscription contracted by the current user
112+ * (including plans and add-ons).
113+ *
114+ * Key "plan" contains the plan name of the user.
115+ * Key "addOns" contains a list with the add-ons contracted by the user.
116+ *
117+ * @return Map<String, Object> with the current user's contracted subscription.
118+ */
119+ public final Map <String , Object > getUserSubscription () {
120+ Map <String , Object > userSubscription = new HashMap <>();
121+
122+ userSubscription .put ("plan" , this .getUserPlan ());
123+ userSubscription .put ("addOns" , this .getUserAddOns ());
124+
125+ return userSubscription ;
126+ }
127+
87128 /**
88129 * This method returns the plan context of the current user, represented by a
89130 * {@link Map}. It's used to evaluate the pricing plan.
@@ -93,18 +134,28 @@ public Boolean userAffectedByPricing() {
93134 public final Map <String , Object > getPlanContext () {
94135
95136 Plan plan = this .getPricingManager ().getPlans ().get (this .getUserPlan ());
96- Map <String , Object > planContext = plan . parseToMap ();
137+ Map <String , AddOn > addOnsMap = this . getPricingManager (). getAddOns ();
97138
98- Map <String , Object > planFeaturesContext = plan .getFeatures ().entrySet ().stream ()
99- .collect (Collectors .toMap (Map .Entry ::getKey ,
100- e -> e .getValue ().getValue () != null ? e .getValue ().getValue ()
101- : e .getValue ().getDefaultValue ()));
139+ List <AddOn > addOns = new ArrayList <>();
140+
141+ for (String addOnName : this .getUserAddOns ()) {
142+
143+ AddOn addOn = addOnsMap .get (addOnName );
144+ if (addOn != null ) {
145+ addOns .add (addOn );
146+ } else {
147+ logger .warn (
148+ "[WARNING] User add-on {} not found in the pricing configuration. It hasn't been considered in feature evaluation." ,
149+ addOnName );
150+ }
151+ }
152+
153+ Map <String , Object > planContext = new HashMap <>();
154+
155+ Map <String , Object > planFeaturesContext = computeFeatureValueMap (plan , addOns );
102156 planContext .put ("features" , planFeaturesContext );
103157
104- Map <String , Object > planUsageLimitMap = plan .getUsageLimits ().entrySet ().stream ()
105- .collect (Collectors .toMap (Map .Entry ::getKey ,
106- e -> e .getValue ().getValue () != null ? e .getValue ().getValue ()
107- : e .getValue ().getDefaultValue ()));
158+ Map <String , Object > planUsageLimitMap = computeUsageLimitValueMap (plan , addOns );
108159 planContext .put ("usageLimits" , planUsageLimitMap );
109160
110161 return planContext ;
@@ -123,4 +174,94 @@ public final PricingManager getPricingManager() {
123174 throw new PricingPlanEvaluationException ("Error while parsing YAML file" );
124175 }
125176 }
177+
178+ private final Map <String , Object > computeFeatureValueMap (Plan plan , List <AddOn > addOns ) {
179+ Map <String , Object > featureValueMap = new HashMap <>();
180+
181+ // Add plan features
182+ featureValueMap .putAll (plan .getFeatures ().entrySet ().stream ()
183+ .collect (Collectors .toMap (Map .Entry ::getKey ,
184+ e -> e .getValue ().getValue () != null ? e .getValue ().getValue ()
185+ : e .getValue ().getDefaultValue ())));
186+
187+ // Replace by add-ons features
188+ for (AddOn addOn : addOns ) {
189+ try {
190+ Map <String , Feature > addOnFeatures = addOn .getFeatures ();
191+
192+ if (addOnFeatures == null ) {
193+ continue ;
194+ }
195+
196+ featureValueMap .putAll (addOnFeatures .entrySet ().stream ()
197+ .collect (Collectors .toMap (Map .Entry ::getKey ,
198+ e -> e .getValue ().getValue ())));
199+ } catch (NullPointerException e ) {
200+ throw new PricingPlanEvaluationException ("Error while creating evaluation context. Add-on "
201+ + addOn .getName () + " do not have a value for all its features." );
202+ }
203+ }
204+
205+ return featureValueMap ;
206+ }
207+
208+ private final Map <String , Object > computeUsageLimitValueMap (Plan plan , List <AddOn > addOns ) {
209+ Map <String , Object > usageLimitMap = new HashMap <>();
210+
211+ // Add plan usage limits
212+ usageLimitMap .putAll (plan .getUsageLimits ().entrySet ().stream ()
213+ .collect (Collectors .toMap (Map .Entry ::getKey ,
214+ e -> e .getValue ().getValue () != null ? e .getValue ().getValue ()
215+ : e .getValue ().getDefaultValue ())));
216+
217+ // Replace by add-ons usage limits
218+ for (AddOn addOn : addOns ) {
219+ try {
220+
221+ Map <String , UsageLimit > addOnUsageLimits = addOn .getUsageLimits ();
222+
223+ if (addOnUsageLimits == null ) {
224+ continue ;
225+ }
226+
227+ usageLimitMap .putAll (addOnUsageLimits .entrySet ().stream ()
228+ .collect (Collectors .toMap (e -> e .getValue ().getName (),
229+ e -> e .getValue ().getValue ())));
230+ } catch (NullPointerException e ) {
231+ throw new PricingPlanEvaluationException ("Error while creating evaluation context. Add-on "
232+ + addOn .getName () + " do not have a value for all its features." );
233+ }
234+ }
235+
236+ // Extend with Add add-ons usage limits extensions
237+ for (AddOn addOn : addOns ) {
238+ try {
239+
240+ Map <String , UsageLimit > addOnUsageLimitsExtensions = addOn .getUsageLimitsExtensions ();
241+
242+ if (addOnUsageLimitsExtensions == null ) {
243+ continue ;
244+ }
245+
246+ usageLimitMap .putAll (addOnUsageLimitsExtensions .entrySet ().stream ()
247+ .collect (Collectors .toMap (e -> e .getValue ().getName (),
248+ e -> {
249+ Number existingValue = (Number ) usageLimitMap .get (e .getValue ().getName ());
250+ Number extensionValue = (Number ) e .getValue ().getValue ();
251+ if (existingValue == null || extensionValue == null ) {
252+ throw new PricingPlanEvaluationException (
253+ "Error while creating evaluation context. Usage limit extension values must be numeric and not null." );
254+ }
255+ return existingValue .doubleValue () + extensionValue .doubleValue ();
256+ })));
257+ } catch (NullPointerException e ) {
258+ throw new PricingPlanEvaluationException (
259+ "Error while creating evaluation context. It wasn't possible to extend the add-on "
260+ + addOn .getName ()
261+ + ". Please check that the usage limit that youre trying to extend actually exists in the configuration and that it's NUMERIC." );
262+ }
263+ }
264+
265+ return usageLimitMap ;
266+ }
126267}
0 commit comments