From 622908c876838c0c2f716ee4873fdb565d2e1a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Sat, 1 Feb 2025 17:59:34 +0100 Subject: [PATCH] feat: inconsistent pricing calculator and transformer --- .../services/updaters/YamlUpdater.java | 4 +- .../FeatUsageLimitInfo.java | 40 ++++++++ .../InconsistentPricingReport.java | 24 +++++ .../InconsistentciesCalculator.java | 98 +++++++++++++++++++ .../InconsistentciesCalculatorTest.java | 83 ++++++++++++++++ .../pricing/inconsistent-pricing.yml | 54 ++++++++++ 6 files changed, 300 insertions(+), 3 deletions(-) create mode 100644 src/main/java/io/github/isagroup/utils/consistencychecker/FeatUsageLimitInfo.java create mode 100644 src/main/java/io/github/isagroup/utils/consistencychecker/InconsistentPricingReport.java create mode 100644 src/main/java/io/github/isagroup/utils/consistencychecker/InconsistentciesCalculator.java create mode 100644 src/test/java/io/github/isagroup/utils/consistencychecker/InconsistentciesCalculatorTest.java create mode 100644 src/test/resources/pricing/inconsistent-pricing.yml diff --git a/src/main/java/io/github/isagroup/services/updaters/YamlUpdater.java b/src/main/java/io/github/isagroup/services/updaters/YamlUpdater.java index 664cf3f..9d66ccd 100644 --- a/src/main/java/io/github/isagroup/services/updaters/YamlUpdater.java +++ b/src/main/java/io/github/isagroup/services/updaters/YamlUpdater.java @@ -23,7 +23,6 @@ public static void update(Map configFile) throws UpdateException throw new VersionException("The syntax version field of the pricing must not be null or undefined. Please ensure that the version field is present and correctly formatted"); } - Object versionField = configFile.get("syntaxVersion"); if (versionField == null) { @@ -35,8 +34,7 @@ public static void update(Map configFile) throws UpdateException } if (versionField instanceof Double || versionField instanceof String) { - Version version = Version.version(versionField); - if (updaters.get(version) == null) { + if (!Version.version(versionField).lessThan(Version.LATEST)) { return; } diff --git a/src/main/java/io/github/isagroup/utils/consistencychecker/FeatUsageLimitInfo.java b/src/main/java/io/github/isagroup/utils/consistencychecker/FeatUsageLimitInfo.java new file mode 100644 index 0000000..7c03e4b --- /dev/null +++ b/src/main/java/io/github/isagroup/utils/consistencychecker/FeatUsageLimitInfo.java @@ -0,0 +1,40 @@ +package io.github.isagroup.utils.consistencychecker; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@EqualsAndHashCode +public class FeatUsageLimitInfo { + + private String usageLimitName; + private Object usageLimitValue; + private String featName; + private Object featValue; + + public FeatUsageLimitInfo(String usageLimitName, Object usageLimitValue, String featName, Object featValue) { + this.usageLimitName = usageLimitName; + this.usageLimitValue = usageLimitValue; + this.featName = featName; + this.featValue = featValue; + } + + public boolean isFeatureEnabledUsageLimitDisabled() { + return this.featValue instanceof Boolean && this.usageLimitValue instanceof Number && (Boolean) this.featValue + && ((Number) this.usageLimitValue).intValue() == 0; + } + + public boolean isFeatureDisabledUsageLimitEnabled() { + return this.featValue instanceof Boolean && this.usageLimitValue instanceof Number && !(Boolean) this.featValue + && ((Number) this.usageLimitValue).intValue() > 0; + } + + @Override + public String toString() { + return "{\n \"ulName\": \"" + this.usageLimitName + "\",\n \"ulValue\": " + usageLimitValue + + ",\n \"fName\": \"" + featName + "\",\n \"fValue\": " + featValue + "\n}\n"; + } + +} diff --git a/src/main/java/io/github/isagroup/utils/consistencychecker/InconsistentPricingReport.java b/src/main/java/io/github/isagroup/utils/consistencychecker/InconsistentPricingReport.java new file mode 100644 index 0000000..ec3b9d8 --- /dev/null +++ b/src/main/java/io/github/isagroup/utils/consistencychecker/InconsistentPricingReport.java @@ -0,0 +1,24 @@ +package io.github.isagroup.utils.consistencychecker; + +import java.util.List; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class InconsistentPricingReport { + + private String info; + private List inconsistent; + + public InconsistentPricingReport(String info, List inconsistent) { + this.info = info; + this.inconsistent = inconsistent; + } + + @Override + public String toString() { + return "\n{\n \"info\": \"" + this.info + "\",\n \"inconsistent: " + inconsistent + "\n}\n"; + } +} diff --git a/src/main/java/io/github/isagroup/utils/consistencychecker/InconsistentciesCalculator.java b/src/main/java/io/github/isagroup/utils/consistencychecker/InconsistentciesCalculator.java new file mode 100644 index 0000000..103b0f3 --- /dev/null +++ b/src/main/java/io/github/isagroup/utils/consistencychecker/InconsistentciesCalculator.java @@ -0,0 +1,98 @@ +package io.github.isagroup.utils.consistencychecker; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import io.github.isagroup.models.Feature; +import io.github.isagroup.models.Plan; +import io.github.isagroup.models.PricingManager; + +public class InconsistentciesCalculator { + + private PricingManager pricing; + + public InconsistentciesCalculator(PricingManager pricing) { + this.pricing = pricing; + } + + public Optional getInconsistentItems() { + if (this.pricing.getUsageLimits() == null || filterInconsistentValues().isEmpty()) { + return Optional.empty(); + } + + String info = this.pricing.getSaasName() + " - " + this.pricing.getCreatedAt().getYear(); + return Optional.of(new InconsistentPricingReport(info, filterInconsistentValues())); + } + + /* + * Get a list of inconsistent features with usage limits. + * An enabled feature and disabled usage limit is inconsistent, i.e., feature = + * true, usageLimit=0 + * A disabled feature and enabled usage limit is inconsistent, i.e, feature = + * false, usageLimit > 0 + */ + private List filterInconsistentValues() { + return this.pricing.getFeatures().values().stream() + .flatMap(feature -> getFeaturesAndUsageLimitsRecords(feature).stream()) + .filter(item -> item.isFeatureDisabledUsageLimitEnabled() || item.isFeatureEnabledUsageLimitDisabled()) + .collect(Collectors.toList()); + + } + + private List getFeaturesAndUsageLimitsRecords(Feature feature) { + return this.pricing.getUsageLimits().values().stream() + .filter(usageLimit -> usageLimit.getLinkedFeatures() != null + && usageLimit.getLinkedFeatures().contains(feature.getName())) + .map(usageLimit -> new FeatUsageLimitInfo(usageLimit.getName(), + usageLimit.getDefaultValue(), feature.getName(), feature.getDefaultValue())) + .collect(Collectors.toList()); + } + + public static boolean isPlanFeatureEnabled(Plan plan, String featureName) { + return plan.getFeatures().containsKey(featureName) + && plan.getFeatures().get(featureName).getValue() instanceof Boolean + && (Boolean) plan.getFeatures().get(featureName).getValue(); + } + + public static boolean isUsageLimitUndefined(Plan plan, String usageLimitName) { + return plan.getUsageLimits() == null || !plan.getUsageLimits().containsKey(usageLimitName) + || plan.getUsageLimits().get(usageLimitName).getValue() == null; + } + + public void transformPricing(PricingManager pricing) { + + if (pricing.getPlans() == null) { + return; + } + + InconsistentciesCalculator inconsistentciesCalc = new InconsistentciesCalculator(pricing); + + if (inconsistentciesCalc.getInconsistentItems().isEmpty()) { + return; + } + + for (FeatUsageLimitInfo item : inconsistentciesCalc.getInconsistentItems().get().getInconsistent()) { + for (Plan plan : pricing.getPlans().values()) { + if (isPlanFeatureEnabled(plan, item.getFeatName()) && + isUsageLimitUndefined(plan, item.getUsageLimitName())) { + plan.getUsageLimits().get(item.getUsageLimitName()).setValue(item.getUsageLimitValue()); + } + if (item.isFeatureDisabledUsageLimitEnabled()) { + pricing.getUsageLimits().get(item.getUsageLimitName()).setDefaultValue(0); + } + + if (!isPlanFeatureEnabled(plan, item.getFeatName()) + && !isUsageLimitUndefined(plan, item.getUsageLimitName())) { + plan.getFeatures().get(item.getFeatName()).setValue(true); + } + + if (item.isFeatureEnabledUsageLimitDisabled()) { + pricing.getFeatures().get(item.getFeatName()).setDefaultValue(false); + } + } + } + + } + +} diff --git a/src/test/java/io/github/isagroup/utils/consistencychecker/InconsistentciesCalculatorTest.java b/src/test/java/io/github/isagroup/utils/consistencychecker/InconsistentciesCalculatorTest.java new file mode 100644 index 0000000..5d65283 --- /dev/null +++ b/src/test/java/io/github/isagroup/utils/consistencychecker/InconsistentciesCalculatorTest.java @@ -0,0 +1,83 @@ +package io.github.isagroup.utils.consistencychecker; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import io.github.isagroup.models.PricingManager; +import io.github.isagroup.services.yaml.YamlUtils; + +public class InconsistentciesCalculatorTest { + + @Test + @DisplayName("Given a pricing should detect enabled features - disabled usagelimits and disabled features - enabled usage limits") + void givenAPricingShouldDetectInconsistencies() { + + List expected = new ArrayList<>(); + expected.add(new FeatUsageLimitInfo("usageLimit1", 25, "feature1", false)); + expected.add(new FeatUsageLimitInfo("usageLimit2", 0, "feature2", true)); + + PricingManager pr = YamlUtils.retrieveManagerFromYaml( + Path.of("pricing", "inconsistent-pricing.yml").toString()); + InconsistentciesCalculator inconsistenciesCalc = new InconsistentciesCalculator(pr); + + if (inconsistenciesCalc.getInconsistentItems().isEmpty()) { + fail("Expected a pricing with inconsistencies"); + } + + assertEquals(expected, inconsistenciesCalc.getInconsistentItems().get().getInconsistent()); + } + + @Test + void givenPricingWithFeatureEnabledAndUsageLimitDisabledShouldTransform() { + PricingManager pricing = YamlUtils.retrieveManagerFromYaml( + Path.of("pricing", "inconsistent-pricing.yml").toString()); + InconsistentciesCalculator inconsistenciesCalc = new InconsistentciesCalculator(pricing); + + if (inconsistenciesCalc.getInconsistentItems().isEmpty()) { + fail("I should have found inconsistencies"); + } + + inconsistenciesCalc.transformPricing(pricing); + + assertTrue((Boolean) pricing.getPlans().get("ALPHA").getFeatures().get("feature2").getValue()); + assertTrue((Boolean) pricing.getPlans().get("BETA").getFeatures().get("feature2").getValue()); + assertTrue((Boolean) pricing.getPlans().get("CHARLIE").getFeatures().get("feature2").getValue()); + + assertEquals(10, pricing.getPlans().get("ALPHA").getUsageLimits().get("usageLimit2").getValue()); + assertEquals(30, pricing.getPlans().get("BETA").getUsageLimits().get("usageLimit2").getValue()); + assertEquals(50, pricing.getPlans().get("CHARLIE").getUsageLimits().get("usageLimit2").getValue()); + } + + @Test + void givenPricingWithFeatureDisabledAndUsageLimitEnabledShouldTransform() { + PricingManager pricing = YamlUtils.retrieveManagerFromYaml( + Path.of("pricing", "inconsistent-pricing.yml").toString()); + InconsistentciesCalculator inconsistenciesCalc = new InconsistentciesCalculator(pricing); + + if (inconsistenciesCalc.getInconsistentItems().isEmpty()) { + fail("I should have found inconsistencies"); + } + + inconsistenciesCalc.transformPricing(pricing); + + assertEquals(0, pricing.getUsageLimits().get("usageLimit1").getDefaultValue()); + + assertNull(pricing.getPlans().get("ALPHA").getFeatures().get("feature1").getValue()); + assertTrue((Boolean) pricing.getPlans().get("BETA").getFeatures().get("feature1").getValue()); + assertTrue((Boolean) pricing.getPlans().get("CHARLIE").getFeatures().get("feature1").getValue()); + + assertNull(pricing.getPlans().get("ALPHA").getUsageLimits().get("usageLimit1").getValue()); + assertEquals(25, pricing.getPlans().get("BETA").getUsageLimits().get("usageLimit1").getValue()); + assertEquals(50, pricing.getPlans().get("CHARLIE").getUsageLimits().get("usageLimit1").getValue()); + } + +} diff --git a/src/test/resources/pricing/inconsistent-pricing.yml b/src/test/resources/pricing/inconsistent-pricing.yml new file mode 100644 index 0000000..dc044d4 --- /dev/null +++ b/src/test/resources/pricing/inconsistent-pricing.yml @@ -0,0 +1,54 @@ +saasName: Pricing with inconsistencies +version: "2.0" +createdAt: "2024-07-02" +currency: USD +features: + feature1: + valueType: BOOLEAN + defaultValue: false + type: INFORMATION + feature2: + valueType: BOOLEAN + defaultValue: true + type: DOMAIN +usageLimits: + usageLimit1: + valueType: NUMERIC + defaultValue: 25 + type: NON_RENEWABLE + linkedFeatures: + - feature1 + usageLimit2: + valueType: NUMERIC + defaultValue: 0 + type: NON_RENEWABLE + linkedFeatures: + - feature2 +plans: + ALPHA: + price: 0 + unit: user/month + features: null + usageLimits: + usageLimit2: + value: 10 + BETA: + price: 5.99 + unit: user/month + features: + feature1: + value: true + usageLimits: + usageLimit2: + value: 30 + CHARLIE: + price: 9.99 + unit: user/month + features: + feature1: + value: true + usageLimits: + usageLimit1: + value: 50 + usageLimit2: + value: 50