Skip to content

Commit 38f516a

Browse files
authored
5.5.0
FEAT: - Feature evaluations now consider the information declared in add-ons FIX: - availableFor list considered as inmutable due to java +16
2 parents 9074f7c + 4127d57 commit 38f516a

12 files changed

Lines changed: 416 additions & 131 deletions

File tree

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
<groupId>io.github.isa-group</groupId>
99
<artifactId>Pricing4Java</artifactId>
10-
<version>5.4.2</version>
10+
<version>5.5.0</version>
1111

1212
<name>${project.groupId}:${project.artifactId}</name>
1313
<description>A pricing driven feature toggling library for java</description>

src/main/java/io/github/isagroup/PricingContext.java

Lines changed: 150 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
package io.github.isagroup;
22

3+
import java.util.ArrayList;
4+
import java.util.HashMap;
5+
import java.util.List;
36
import java.util.Map;
47
import java.util.stream.Collectors;
58

9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
611
import org.springframework.stereotype.Component;
712
import org.yaml.snakeyaml.error.YAMLException;
813

914
import io.github.isagroup.exceptions.PricingPlanEvaluationException;
1015
import io.github.isagroup.models.PricingManager;
16+
import io.github.isagroup.models.UsageLimit;
1117
import io.github.isagroup.services.yaml.YamlUtils;
18+
import io.github.isagroup.models.AddOn;
19+
import io.github.isagroup.models.Feature;
1220
import io.github.isagroup.models.Plan;
1321

1422
/**
@@ -18,6 +26,8 @@
1826
@Component
1927
public 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
}

src/main/java/io/github/isagroup/PricingService.java

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,8 @@ public void updatePlanFromConfiguration(String previousName, Plan plan) {
243243

244244
pricingManager.setPlans(plans);
245245

246+
updateAddOnsWithNewPlan(previousName, plan.getName(), pricingManager);
247+
246248
YamlUtils.writeYaml(pricingManager, pricingContext.getConfigFilePath());
247249
}
248250

@@ -278,6 +280,7 @@ public void removePlanFromConfiguration(String name) {
278280
} else {
279281
plans.remove(name);
280282
pricingManager.setPlans(plans);
283+
removePlanFromAddOns(name, pricingManager);
281284
YamlUtils.writeYaml(pricingManager, pricingContext.getConfigFilePath());
282285
}
283286
}
@@ -631,7 +634,7 @@ private Map<String, AddOn> removeFeatureFromAddOns(String featureName, PricingMa
631634
AddOn addOn = addOns.get(addOnName);
632635
Map<String, Feature> addOnFeatures = addOn.getFeatures();
633636

634-
if (addOnFeatures.containsKey(featureName)) {
637+
if (addOnFeatures != null && addOnFeatures.containsKey(featureName)) {
635638
addOnFeatures.remove(featureName);
636639
if (addOnFeatures.isEmpty()) {
637640
addOnsToRemove.add(addOnName);
@@ -673,11 +676,53 @@ private void removeUsageLimitsFromAddOns(List<String> usageLimitsToRemove, Prici
673676

674677
for (AddOn addOn : addOns.values()) {
675678
for (String usageLimitName : usageLimitsToRemove) {
676-
addOn.getUsageLimits().remove(usageLimitName);
679+
if (addOn.getUsageLimits() != null && addOn.getUsageLimits().get(usageLimitName) != null) {
680+
addOn.getUsageLimits().remove(usageLimitName);
681+
}
682+
683+
if (addOn.getUsageLimitsExtensions() != null && addOn.getUsageLimitsExtensions().get(usageLimitName) != null){
684+
addOn.getUsageLimitsExtensions().remove(usageLimitName);
685+
}
677686
}
678687
}
679688

680689
pricingManager.setAddOns(addOns);
681690
}
682691

692+
private void removePlanFromAddOns(String planName, PricingManager pricingManager){
693+
Map<String, AddOn> addOns = pricingManager.getAddOns();
694+
695+
if (addOns == null) {
696+
return;
697+
}
698+
699+
for (AddOn addOn : addOns.values()) {
700+
if (addOn.getAvailableFor() != null && addOn.getAvailableFor().contains(planName)) {
701+
addOn.getAvailableFor().remove(planName);
702+
}
703+
704+
if (addOn.getAvailableFor().isEmpty()) {
705+
addOns.remove(addOn.getName());
706+
}
707+
}
708+
709+
pricingManager.setAddOns(addOns);
710+
}
711+
712+
private void updateAddOnsWithNewPlan(String previousName, String newName, PricingManager pricingManager){
713+
Map<String, AddOn> addOns = pricingManager.getAddOns();
714+
715+
if (addOns == null) {
716+
return;
717+
}
718+
719+
for (AddOn addOn : addOns.values()) {
720+
if (addOn.getAvailableFor() != null && addOn.getAvailableFor().contains(previousName)) {
721+
addOn.getAvailableFor().remove(previousName);
722+
addOn.getAvailableFor().add(newName);
723+
}
724+
}
725+
726+
pricingManager.setAddOns(addOns);
727+
}
683728
}

src/main/java/io/github/isagroup/services/parsing/AddOnParser.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.github.isagroup.services.parsing;
22

3+
import java.util.ArrayList;
34
import java.util.LinkedHashMap;
45
import java.util.List;
56
import java.util.Map;
@@ -124,7 +125,7 @@ private static void setAvailableFor(Map<String, Object> addOnMap, PricingManager
124125
}
125126
}
126127

127-
addOn.setAvailableFor(plansAvailableList);
128+
addOn.setAvailableFor(new ArrayList<>(plansAvailableList)); // From java 16, stream().toList() generates an immutable list
128129

129130
}
130131

0 commit comments

Comments
 (0)