diff --git a/examples/src/main/java/io/github/pgmarc/space/examples/SingleService.java b/examples/src/main/java/io/github/pgmarc/space/examples/SingleService.java index c9ff45b..2eee104 100644 --- a/examples/src/main/java/io/github/pgmarc/space/examples/SingleService.java +++ b/examples/src/main/java/io/github/pgmarc/space/examples/SingleService.java @@ -69,9 +69,10 @@ public static void main(String[] args) throws IOException { // Updating (novating) the contract to version of 2025 SubscriptionUpdateRequest upReq = SubscriptionUpdateRequest.builder() - .service("WireMock", "2025") + .startService("WireMock", "2025") .plan("Enterprise") - .add(); + .endService() + .build(); Subscription updatedSubscription = client.contracts().updateContractByUserId(userId, upReq); System.out.println(updatedSubscription); diff --git a/space-client/src/main/java/io/github/pgmarc/space/FeaturesEndpoint.java b/space-client/src/main/java/io/github/pgmarc/space/FeaturesEndpoint.java index a103730..e744fdf 100644 --- a/space-client/src/main/java/io/github/pgmarc/space/FeaturesEndpoint.java +++ b/space-client/src/main/java/io/github/pgmarc/space/FeaturesEndpoint.java @@ -3,7 +3,8 @@ import io.github.pgmarc.space.deserializers.ErrorDeserializer; import io.github.pgmarc.space.deserializers.FeatureEvaluationDeserializer; import io.github.pgmarc.space.exceptions.SpaceApiException; -import io.github.pgmarc.space.features.Consumption; +import io.github.pgmarc.space.features.Revert; +import io.github.pgmarc.space.features.UsageLimitConsumption; import io.github.pgmarc.space.features.FeatureEvaluationResult; import io.github.pgmarc.space.serializers.ConsumptionSerializer; import okhttp3.*; @@ -56,11 +57,11 @@ public FeatureEvaluationResult evaluate(String userId, String service, String fe return res; } - public FeatureEvaluationResult evaluateOptimistically(String userId, String service, String featureId, Consumption consumption) + public FeatureEvaluationResult evaluateOptimistically(String userId, String service, String featureId, UsageLimitConsumption usageLimitConsumption) throws IOException { HttpUrl url = this.baseUrl.newBuilder().addEncodedPathSegment(userId) .addEncodedPathSegment(formatFeatureId(service, featureId)).build(); - RequestBody body = RequestBody.create(consumptionSerializer.toJson(consumption).toString(), JSON); + RequestBody body = RequestBody.create(consumptionSerializer.toJson(usageLimitConsumption).toString(), JSON); Request request = new Request(url, requiredHeaders ,"POST" , body); FeatureEvaluationResult res = null; @@ -77,20 +78,6 @@ public FeatureEvaluationResult evaluateOptimistically(String userId, String serv return res; } - public enum Revert { - OLDEST_VALUE(false), - NEWEST_VALUE(true); - - private final boolean latest; - - Revert(boolean latest) { - this.latest = latest; - } - - public boolean isLatest() { - return latest; - } - } public boolean revert(String userId, String service, String feature, Revert revert) throws IOException { HttpUrl url = this.baseUrl.newBuilder().addEncodedPathSegment(userId) @@ -118,7 +105,6 @@ public String generatePricingTokenForUser(String userId) throws IOException { .addPathSegment("pricing-token").build(); Request request = new Request(url, requiredHeaders ,"POST" , RequestBody.EMPTY); - FeatureEvaluationResult res = null; try (Response response = client.newCall(request).execute()) { JSONObject jsonResponse = new JSONObject(response.body().string()); if (!response.isSuccessful()) { diff --git a/space-client/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java b/space-client/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java deleted file mode 100644 index a256724..0000000 --- a/space-client/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java +++ /dev/null @@ -1,111 +0,0 @@ -package io.github.pgmarc.space.contracts; - -import java.time.Duration; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.Objects; -import java.util.Optional; - -public final class BillingPeriod { - - private final ZonedDateTime startDate; - private final ZonedDateTime endDate; - private Duration renewalDays; - - private BillingPeriod(ZonedDateTime startDate, ZonedDateTime endDate, Duration renewalDays) { - this.startDate = startDate; - this.endDate = endDate; - this.renewalDays = renewalDays; - } - - LocalDateTime getStartDate() { - return startDate.toLocalDateTime(); - } - - LocalDateTime getEndDate() { - return endDate.toLocalDateTime(); - } - - Duration getDuration() { - return renewalDays; - } - - boolean isActive(ZonedDateTime date) { - return (startDate.isEqual(date) || startDate.isBefore(date)) && - (endDate.isAfter(date) || endDate.isEqual(date)); - } - - boolean isExpired(ZonedDateTime date) { - return startDate.isBefore(date) && endDate.isBefore(date); - } - - boolean isAutoRenewable() { - return renewalDays != null; - } - - Optional getRenewalDate() { - return Optional.ofNullable(isAutoRenewable() ? endDate.plus(renewalDays).toLocalDateTime() : null); - } - - public void setRenewalDays(Duration renewalDays) { - validateRenewalDays(renewalDays); - this.renewalDays = renewalDays; - } - - private static void validateRenewalDays(Duration renewalDays) { - if (renewalDays != null && renewalDays.toDays() <= 0) { - throw new IllegalArgumentException("your subscription cannot expire in less than one day"); - } - } - - public static BillingPeriod of(ZonedDateTime startDate, ZonedDateTime endDate) { - return of(startDate, endDate, null); - } - - public static BillingPeriod of(ZonedDateTime startDate, ZonedDateTime endDate, Duration renewalDays) { - Objects.requireNonNull(startDate, "startDate must not be null"); - Objects.requireNonNull(endDate, "endDate must not be null"); - if (startDate.isAfter(endDate)) { - throw new IllegalStateException("startDate is after endDate"); - } - validateRenewalDays(renewalDays); - return new BillingPeriod(startDate, endDate, renewalDays); - } - - public enum Keys { - - START_DATE("startDate"), - END_DATE("endDate"), - AUTORENEW("autoRenew"), - RENEWAL_DAYS("renewalDays"); - - private final String name; - - Keys(String name) { - this.name = name; - } - - @Override - public String toString() { - return name; - } - } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) return false; - BillingPeriod that = (BillingPeriod) o; - return Objects.equals(startDate, that.startDate) && Objects.equals(endDate, that.endDate) && Objects.equals(renewalDays, that.renewalDays); - } - - @Override - public int hashCode() { - return Objects.hash(startDate, endDate, renewalDays); - } - - @Override - public String toString() { - return "From " + startDate + " to " + endDate + ", renews in " + renewalDays; - } -} diff --git a/space-client/src/main/java/io/github/pgmarc/space/contracts/Subscription.java b/space-client/src/main/java/io/github/pgmarc/space/contracts/Subscription.java index 362fb11..2796ac8 100644 --- a/space-client/src/main/java/io/github/pgmarc/space/contracts/Subscription.java +++ b/space-client/src/main/java/io/github/pgmarc/space/contracts/Subscription.java @@ -1,9 +1,6 @@ package io.github.pgmarc.space.contracts; -import java.time.Duration; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; +import java.time.*; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -19,42 +16,49 @@ public final class Subscription { private final UserContact userContact; private final Map services; - private final BillingPeriod billingPeriod; + private final ZonedDateTime startDate; + private final ZonedDateTime endDate; + private final Period renewalPeriod; private final List history; private final Map> usageLevels; private Subscription(Builder builder) { this.userContact = builder.userContact; - this.billingPeriod = builder.billingPeriod; + this.startDate = builder.startDate; + this.endDate = builder.endDate; + this.renewalPeriod = builder.renewalPeriod; this.services = Collections.unmodifiableMap(builder.services); this.history = Collections.unmodifiableList(builder.history); this.usageLevels = Collections.unmodifiableMap(builder.usageLevels); } - public static Builder builder(UserContact userContact, BillingPeriod billingPeriod, + public static Builder builder(UserContact userContact, ZonedDateTime startDate, ZonedDateTime endDate, Service service) { - return new Builder(userContact, billingPeriod).subscribe(service); + return new Builder(userContact, startDate, endDate).subscribe(service); } - public static Builder builder(UserContact userContact, BillingPeriod billingPeriod, - Collection services) { - return new Builder(userContact, billingPeriod).subscribeAll(services); + public static Builder builder(UserContact userContact, ZonedDateTime startDate, ZonedDateTime endDate, Collection services) { + return new Builder(userContact, startDate, endDate).subscribeAll(services); } public LocalDateTime getStartDate() { - return billingPeriod.getStartDate(); + return startDate.toLocalDateTime(); } public LocalDateTime getEndDate() { - return billingPeriod.getEndDate(); + return endDate.toLocalDateTime(); } - public Optional getRenewalDuration() { - return Optional.of(billingPeriod.getDuration()); + public Optional getRenewalPeriod() { + return Optional.of(renewalPeriod); } public boolean isAutoRenewable() { - return billingPeriod.isAutoRenewable(); + return renewalPeriod != null; + } + + public Optional getRenewalDate() { + return Optional.ofNullable(isAutoRenewable() ? endDate.plus(renewalPeriod).toLocalDateTime() : null); } /** @@ -64,7 +68,10 @@ public boolean isAutoRenewable() { */ public boolean isActive(LocalDateTime date) { Objects.requireNonNull(date, "date must not be null"); - return billingPeriod.isActive(ZonedDateTime.of(date, ZoneId.of("UTC"))); + ZonedDateTime utcDate = ZonedDateTime.of(date, ZoneId.of("UTC")); + + return (startDate.isEqual(utcDate) || startDate.isBefore(utcDate)) && + (endDate.isAfter(utcDate) || endDate.isEqual(utcDate)); } /** @@ -73,12 +80,10 @@ public boolean isActive(LocalDateTime date) { */ public boolean isExpired(LocalDateTime date) { Objects.requireNonNull(date, "date must not be null"); - return billingPeriod.isExpired(ZonedDateTime.of(date, ZoneId.of("UTC"))); + ZonedDateTime utcDate = ZonedDateTime.of(date, ZoneId.of("UTC")); + return startDate.isBefore(utcDate) && endDate.isBefore(utcDate); } - public Optional getRenewalDate() { - return billingPeriod.getRenewalDate(); - } public String getUserId() { return userContact.getUserId(); @@ -114,7 +119,7 @@ public enum Keys { CONTRACTED_SERVICES("contractedServices"), SUBSCRIPTION_PLANS("subscriptionPlans"), SUBSCRIPTION_ADDONS("subscriptionAddOns"), - USAGE_LEVEL("usageLevel"), + USAGE_LEVELS("usageLevels"), HISTORY("history"); private final String name; @@ -131,19 +136,37 @@ public String toString() { public static final class Builder { - private final BillingPeriod billingPeriod; + private final ZonedDateTime startDate; + private final ZonedDateTime endDate; private final UserContact userContact; private final Map services = new HashMap<>(); private final List history = new ArrayList<>(); private final Map> usageLevels = new HashMap<>(); + private Period renewalPeriod; - private Builder(UserContact userContact, BillingPeriod billingPeriod) { - this.billingPeriod = billingPeriod; + private Builder(UserContact userContact, ZonedDateTime startDate, ZonedDateTime endDate) { + this.startDate = startDate; + this.endDate = endDate; this.userContact = userContact; } - public Builder renewIn(Duration renewalDays) { - this.billingPeriod.setRenewalDays(renewalDays); + public Builder renewIn(Period renewalPeriod) { + this.renewalPeriod = renewalPeriod; + return this; + } + + public Builder renewInDays(int days) { + this.renewalPeriod = Period.ofDays(days); + return this; + } + + public Builder renewInMonths(int months) { + this.renewalPeriod = Period.ofMonths(months); + return this; + } + + public Builder renewInYears(int years) { + this.renewalPeriod = Period.ofYears(years); return this; } @@ -174,20 +197,39 @@ public Builder addUsageLevels(Map> usageLevels) return this; } + private void validateSubscriptionInterval() { + Objects.requireNonNull(startDate, "start date must not be null"); + Objects.requireNonNull(endDate, "end date must not be null"); + if (startDate.isAfter(endDate)) { + throw new IllegalStateException("startDate is after endDate"); + } + } + + private void validateRenewalPeriod() { + if (renewalPeriod != null && renewalPeriod.isZero()) { + throw new IllegalArgumentException("your renewal period must not be zero"); + } + + if (renewalPeriod != null && renewalPeriod.isNegative()) { + throw new IllegalStateException("renewal period must not be negative"); + } + } + public Subscription build() { - Objects.requireNonNull(billingPeriod, "billingPeriod must not be null"); Objects.requireNonNull(userContact, "userContact must not be null"); + validateSubscriptionInterval(); + validateRenewalPeriod(); return new Subscription(this); } } public static final class Snapshot { - private final LocalDateTime starDateTime; - private final LocalDateTime endDateTime; + private final ZonedDateTime starDateTime; + private final ZonedDateTime endDateTime; private final Map services; - private Snapshot(LocalDateTime startDateTime, LocalDateTime endDateTime, + private Snapshot(ZonedDateTime startDateTime, ZonedDateTime endDateTime, Map services) { this.starDateTime = startDateTime; this.endDateTime = endDateTime; @@ -195,11 +237,11 @@ private Snapshot(LocalDateTime startDateTime, LocalDateTime endDateTime, } public LocalDateTime getStartDate() { - return starDateTime; + return starDateTime.toLocalDateTime(); } public LocalDateTime getEndDate() { - return endDateTime; + return endDateTime.toLocalDateTime(); } public Map getServices() { @@ -210,7 +252,7 @@ public Optional getService(String name) { return Optional.ofNullable(services.get(name)); } - public static Snapshot of(LocalDateTime startDateTime, LocalDateTime endDateTime, + public static Snapshot of(ZonedDateTime startDateTime, ZonedDateTime endDateTime, Map services) { return new Snapshot(startDateTime, endDateTime, services); } diff --git a/space-client/src/main/java/io/github/pgmarc/space/contracts/SubscriptionUpdateRequest.java b/space-client/src/main/java/io/github/pgmarc/space/contracts/SubscriptionUpdateRequest.java index f117600..05b71e8 100644 --- a/space-client/src/main/java/io/github/pgmarc/space/contracts/SubscriptionUpdateRequest.java +++ b/space-client/src/main/java/io/github/pgmarc/space/contracts/SubscriptionUpdateRequest.java @@ -1,48 +1,91 @@ package io.github.pgmarc.space.contracts; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; +import java.util.*; -public class SubscriptionUpdateRequest { +public final class SubscriptionUpdateRequest { - private final Set services = new HashSet<>(); - private Service.Builder serviceBuilder; - - private SubscriptionUpdateRequest() { + private final Set services; + private SubscriptionUpdateRequest(Builder builder) { + services = builder.services; } public Set getServices() { - return Set.copyOf(services); + return Collections.unmodifiableSet(services); } - public static SubscriptionUpdateRequest builder() { - return new SubscriptionUpdateRequest(); + public static Builder builder() { + return new Builder(); } - public SubscriptionUpdateRequest service(String name, String version) { - this.serviceBuilder = Service.builder(name, version); - return this; - } + public static final class Builder { - public SubscriptionUpdateRequest plan(String plan) { - Objects.requireNonNull(serviceBuilder, "you call service first"); - serviceBuilder.plan(plan); - return this; - } + private final Set services = new HashSet<>(); + private Service.Builder serviceBuilder; - public SubscriptionUpdateRequest addOn(String name, long quantity) { - Objects.requireNonNull(serviceBuilder, "you call service first"); - this.serviceBuilder.addOn(name, quantity); - return this; - } + public Builder subscribe(Service service) { + this.services.add(Objects.requireNonNull(service, "service must not be null")); + return this; + } - public SubscriptionUpdateRequest add() { - Objects.requireNonNull(serviceBuilder, "you call service first"); - this.services.add(serviceBuilder.build()); - this.serviceBuilder = null; - return this; - } + public Builder subscribeAll(Collection services) { + Objects.requireNonNull(services, "services must not be null"); + if (services.isEmpty()) { + throw new IllegalArgumentException("services must not be empty"); + } + this.services.addAll(services); + return this; + } + + public Builder startService(String name, String version) { + if (isServiceBuilderAlive()) { + throw new IllegalStateException("you must build a service before creating another"); + } + this.serviceBuilder = Service.builder(name, version); + return this; + } + + public Builder plan(String plan) { + validateServiceBuilderCalled("you must call 'newService' before setting a plan: " + plan); + serviceBuilder.plan(plan); + return this; + } + public Builder addOn(String addOnName, long quantity) { + validateServiceBuilderCalled("you must call 'newService' before setting an add-on: " + addOnName); + serviceBuilder.addOn(addOnName, quantity); + return this; + } + + public Builder endService() { + validateServiceBuilderCalled("you must call 'newService' before adding a service"); + services.add(serviceBuilder.build()); + destroyServiceBuilder(); + return this; + } + + public SubscriptionUpdateRequest build() { + if (isServiceBuilderAlive()) { + throw new IllegalStateException("finish the creation of your service by calling endService"); + } + if (services.isEmpty()) { + throw new IllegalStateException("you have to be subscribed al least to one service"); + } + return new SubscriptionUpdateRequest(this); + } + + private boolean isServiceBuilderAlive() { + return serviceBuilder != null; + } + + private void validateServiceBuilderCalled(String message) { + if (!isServiceBuilderAlive()) { + throw new IllegalStateException(message); + } + } + + private void destroyServiceBuilder() { + this.serviceBuilder = null; + } + } } diff --git a/space-client/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java b/space-client/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java index b351d67..b980b43 100644 --- a/space-client/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java +++ b/space-client/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java @@ -9,15 +9,15 @@ public final class UsageLevel { private final String name; private final double consumed; - private ZonedDateTime resetTimestamp; + private final ZonedDateTime resetTimestamp; public enum Keys { CONSUMED("consumed"), - RESET_TIMESTAMP("resetTimestamp"); + RESET_TIMESTAMP("resetTimeStamp"); private final String name; - private Keys(String name) { + Keys(String name) { this.name = name; } @@ -64,4 +64,16 @@ public static UsageLevel of(String name, double consumed, ZonedDateTime resetTim validateUsageLevel(name, consumed); return new UsageLevel(name, consumed, resetTimestamp); } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + UsageLevel that = (UsageLevel) o; + return Double.compare(consumed, that.consumed) == 0 && Objects.equals(name, that.name) && Objects.equals(resetTimestamp, that.resetTimestamp); + } + + @Override + public int hashCode() { + return Objects.hash(name, consumed, resetTimestamp); + } } diff --git a/space-client/src/main/java/io/github/pgmarc/space/deserializers/BillingPeriodDeserializer.java b/space-client/src/main/java/io/github/pgmarc/space/deserializers/BillingPeriodDeserializer.java deleted file mode 100644 index 457c4cd..0000000 --- a/space-client/src/main/java/io/github/pgmarc/space/deserializers/BillingPeriodDeserializer.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.github.pgmarc.space.deserializers; - -import java.time.Duration; -import java.time.ZonedDateTime; -import java.util.Objects; - -import org.json.JSONObject; - -import io.github.pgmarc.space.contracts.BillingPeriod; - -class BillingPeriodDeserializer implements JsonDeserializable { - - @Override - public BillingPeriod fromJson(JSONObject json) { - - Objects.requireNonNull(json, "billing period json must not be null"); - ZonedDateTime start = ZonedDateTime.parse(json.getString(BillingPeriod.Keys.START_DATE.toString())); - ZonedDateTime end = ZonedDateTime.parse(json.getString(BillingPeriod.Keys.END_DATE.toString())); - Duration renewalDays = null; - if (json.has(BillingPeriod.Keys.RENEWAL_DAYS.toString())) { - renewalDays = Duration.ofDays(json.getLong(BillingPeriod.Keys.RENEWAL_DAYS.toString())); - } - - return BillingPeriod.of(start, end, renewalDays); - - } - -} diff --git a/space-client/src/main/java/io/github/pgmarc/space/deserializers/SnapshotsDeserializer.java b/space-client/src/main/java/io/github/pgmarc/space/deserializers/SnapshotsDeserializer.java index 6f02dee..8f82c68 100644 --- a/space-client/src/main/java/io/github/pgmarc/space/deserializers/SnapshotsDeserializer.java +++ b/space-client/src/main/java/io/github/pgmarc/space/deserializers/SnapshotsDeserializer.java @@ -1,13 +1,12 @@ package io.github.pgmarc.space.deserializers; -import java.time.OffsetDateTime; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; import org.json.JSONArray; import org.json.JSONObject; -import io.github.pgmarc.space.contracts.BillingPeriod; import io.github.pgmarc.space.contracts.Subscription; import io.github.pgmarc.space.contracts.Subscription.Snapshot; @@ -25,13 +24,12 @@ public List fromJson(JSONObject json) { List res = new ArrayList<>(); for (int i = 0; i < history.length(); i++) { JSONObject snapshot = history.getJSONObject(i); - OffsetDateTime startUtc = OffsetDateTime - .parse(snapshot.getString(BillingPeriod.Keys.START_DATE.toString())); - OffsetDateTime end = OffsetDateTime - .parse(snapshot.getString(BillingPeriod.Keys.END_DATE.toString())); - res.add(Snapshot.of(startUtc.toLocalDateTime(), end.toLocalDateTime(), servicesDeserializer.fromJson(snapshot))); + ZonedDateTime startUtc = ZonedDateTime + .parse(snapshot.getString("startDate")); + ZonedDateTime endUtc = ZonedDateTime + .parse(snapshot.getString("endDate")); + res.add(Snapshot.of(startUtc, endUtc, servicesDeserializer.fromJson(snapshot))); } return res; } - } diff --git a/space-client/src/main/java/io/github/pgmarc/space/deserializers/SubscriptionDeserializer.java b/space-client/src/main/java/io/github/pgmarc/space/deserializers/SubscriptionDeserializer.java index 6ae231b..d34549c 100644 --- a/space-client/src/main/java/io/github/pgmarc/space/deserializers/SubscriptionDeserializer.java +++ b/space-client/src/main/java/io/github/pgmarc/space/deserializers/SubscriptionDeserializer.java @@ -1,11 +1,11 @@ package io.github.pgmarc.space.deserializers; +import java.time.ZonedDateTime; import java.util.List; import java.util.Map; import org.json.JSONObject; -import io.github.pgmarc.space.contracts.BillingPeriod; import io.github.pgmarc.space.contracts.UsageLevel; import io.github.pgmarc.space.contracts.UserContact; import io.github.pgmarc.space.contracts.Service; @@ -14,7 +14,6 @@ public final class SubscriptionDeserializer implements JsonDeserializable { - private final BillingPeriodDeserializer billingSerializer = new BillingPeriodDeserializer(); private final UserContactDeserializer userContactDeserializer = new UserContactDeserializer(); private final UsageLevelDeserializer usageLevelDeserializer = new UsageLevelDeserializer(); private final ServicesDeserializer servicesDeserializer = new ServicesDeserializer(); @@ -22,14 +21,26 @@ public final class SubscriptionDeserializer implements JsonDeserializable> usageLevels = usageLevelDeserializer.fromJson( - json.getJSONObject(Subscription.Keys.USAGE_LEVEL.toString())); + json.getJSONObject(Subscription.Keys.USAGE_LEVELS.toString())); Map services = servicesDeserializer.fromJson(json); + + JSONObject billingPeriod = json.getJSONObject(Subscription.Keys.BILLING_PERIOD.toString()); + ZonedDateTime start = ZonedDateTime.parse(billingPeriod.getString("startDate")); + ZonedDateTime end = ZonedDateTime.parse(billingPeriod.getString("endDate")); + List history = historyDeserializer.fromJson(json); - return Subscription.builder(userContact, billingPeriod, services.values()) - .addUsageLevels(usageLevels).addSnapshots(history).build(); + Subscription.Builder builder = Subscription.builder(userContact, start, end, services.values()) + .addUsageLevels(usageLevels) + .addSnapshots(history); + + int renewalDays = billingPeriod.optInt("renewalDays", -1); + if (renewalDays > 0) { + builder.renewInDays(renewalDays); + } + + return builder.build(); } } diff --git a/space-client/src/main/java/io/github/pgmarc/space/features/Consumption.java b/space-client/src/main/java/io/github/pgmarc/space/features/Consumption.java deleted file mode 100644 index 1fd25b2..0000000 --- a/space-client/src/main/java/io/github/pgmarc/space/features/Consumption.java +++ /dev/null @@ -1,89 +0,0 @@ -package io.github.pgmarc.space.features; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; - -public class Consumption { - - private final Set> items; - - private Consumption(Builder builder) { - this.items = Collections.unmodifiableSet(builder.items); - } - - public Set> getConsumption() { - return items; - } - - public static Builder builder() { - return new Builder(); - } - - public static class Item { - private final String serviceName; - private final String usageLimit; - private final T quantity; - - private Item(String serviceName, String usageLimit, T quantity) { - this.serviceName = serviceName; - this.usageLimit = usageLimit; - this.quantity = quantity; - } - - public String getServiceName() { - return serviceName; - } - - public String getUsageLimit() { - return usageLimit; - } - - public T getQuantity() { - return quantity; - } - } - - public static class Builder { - - private final Set> items = new HashSet<>(); - - private void validateFeatureId(String service, String usageLimit) { - Objects.requireNonNull(service, "service name must not be null"); - Objects.requireNonNull(usageLimit, "usage limit name must not be null"); - } - - public Builder addInt(String serviceName, String usageLimit, int quantity) { - validateFeatureId(serviceName, usageLimit); - this.items.add(new Item<>(serviceName, usageLimit, quantity)); - return this; - } - - public Builder addLong(String serviceName, String usageLimit, long quantity) { - validateFeatureId(serviceName, usageLimit); - this.items.add(new Item<>(serviceName, usageLimit, quantity)); - return this; - } - - public Builder addFloat(String serviceName, String usageLimit, float quantity) { - validateFeatureId(serviceName, usageLimit); - this.items.add(new Item<>(serviceName, usageLimit, quantity)); - return this; - } - - public Builder addDouble(String serviceName, String usageLimit, double quantity) { - validateFeatureId(serviceName, usageLimit); - this.items.add(new Item<>(serviceName, usageLimit, quantity)); - return this; - } - - public Consumption build() { - if (items.isEmpty()) { - throw new IllegalStateException("consumption must not be empty"); - } - return new Consumption(this); - } - - } -} diff --git a/space-client/src/main/java/io/github/pgmarc/space/features/FeatureEvaluationResult.java b/space-client/src/main/java/io/github/pgmarc/space/features/FeatureEvaluationResult.java index 615b802..9f95e27 100644 --- a/space-client/src/main/java/io/github/pgmarc/space/features/FeatureEvaluationResult.java +++ b/space-client/src/main/java/io/github/pgmarc/space/features/FeatureEvaluationResult.java @@ -39,7 +39,7 @@ public static FeatureEvaluationResult of(boolean available, Map qu return new FeatureEvaluationResult(available, quotas); } - public final static class Usage { + public static final class Usage { private final Number used; private final Number limit; diff --git a/space-client/src/main/java/io/github/pgmarc/space/features/Revert.java b/space-client/src/main/java/io/github/pgmarc/space/features/Revert.java new file mode 100644 index 0000000..da74758 --- /dev/null +++ b/space-client/src/main/java/io/github/pgmarc/space/features/Revert.java @@ -0,0 +1,16 @@ +package io.github.pgmarc.space.features; + +public enum Revert { + OLDEST_VALUE(false), + NEWEST_VALUE(true); + + private final boolean latest; + + Revert(boolean latest) { + this.latest = latest; + } + + public boolean isLatest() { + return latest; + } +} diff --git a/space-client/src/main/java/io/github/pgmarc/space/features/UsageLimitConsumption.java b/space-client/src/main/java/io/github/pgmarc/space/features/UsageLimitConsumption.java new file mode 100644 index 0000000..778cfc8 --- /dev/null +++ b/space-client/src/main/java/io/github/pgmarc/space/features/UsageLimitConsumption.java @@ -0,0 +1,96 @@ +package io.github.pgmarc.space.features; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +public final class UsageLimitConsumption { + + private final String service; + private final Set> items; + + private UsageLimitConsumption(Builder builder) { + service = builder.service; + items = Collections.unmodifiableSet(builder.items); + } + + public String getService() { + return service; + } + + public Set> getConsumption() { + return items; + } + + public static Builder builder(String service) { + Objects.requireNonNull(service, "service name must not be null"); + return new Builder(service); + } + + public static final class Builder { + + private final String service; + private final Set> items = new HashSet<>(); + + private Builder(String service) { + this.service = service.toLowerCase(); + } + + private void validateFeatureId(String usageLimit) { + Objects.requireNonNull(usageLimit, "usage limit name must not be null"); + } + + public Builder addInt(String usageLimit, int quantity) { + validateFeatureId(usageLimit); + this.items.add(new Item<>(usageLimit, quantity)); + return this; + } + + public Builder addLong(String usageLimit, long quantity) { + validateFeatureId(usageLimit); + this.items.add(new Item<>(usageLimit, quantity)); + return this; + } + + public Builder addFloat(String usageLimit, float quantity) { + validateFeatureId(usageLimit); + this.items.add(new Item<>(usageLimit, quantity)); + return this; + } + + public Builder addDouble(String usageLimit, double quantity) { + validateFeatureId(usageLimit); + this.items.add(new Item<>(usageLimit, quantity)); + return this; + } + + public UsageLimitConsumption build() { + if (service.isBlank()) { + throw new IllegalStateException("service name must not be blank"); + } + if (items.isEmpty()) { + throw new IllegalStateException("usage limits consumption must not be empty"); + } + return new UsageLimitConsumption(this); + } + } + + public static final class Item { + private final String usageLimit; + private final T quantity; + + private Item(String usageLimit, T quantity) { + this.usageLimit = usageLimit; + this.quantity = quantity; + } + + public String getUsageLimit() { + return usageLimit; + } + + public T getQuantity() { + return quantity; + } + } +} diff --git a/space-client/src/main/java/io/github/pgmarc/space/serializers/ConsumptionSerializer.java b/space-client/src/main/java/io/github/pgmarc/space/serializers/ConsumptionSerializer.java index 7597928..2a51696 100644 --- a/space-client/src/main/java/io/github/pgmarc/space/serializers/ConsumptionSerializer.java +++ b/space-client/src/main/java/io/github/pgmarc/space/serializers/ConsumptionSerializer.java @@ -1,22 +1,20 @@ package io.github.pgmarc.space.serializers; -import io.github.pgmarc.space.features.Consumption; +import io.github.pgmarc.space.features.UsageLimitConsumption; import org.json.JSONObject; -import java.util.stream.Collectors; +public final class ConsumptionSerializer implements JsonSerializable { -public final class ConsumptionSerializer implements JsonSerializable { - - private static String formatConsumptionKey(String serviceName, String usageLimitName) { - return serviceName.toLowerCase() + "-" + usageLimitName; + private static String formatUsageLimitConsumptionKey(String serviceName, String usageLimitName) { + return serviceName + "-" + usageLimitName; } @Override - public JSONObject toJson(Consumption consumption) { + public JSONObject toJson(UsageLimitConsumption usageLimitConsumption) { JSONObject jsonObject = new JSONObject(); - for (Consumption.Item item: consumption.getConsumption()) { - jsonObject.put(formatConsumptionKey(item.getServiceName(), item.getUsageLimit()), item.getQuantity()); + for (UsageLimitConsumption.Item item: usageLimitConsumption.getConsumption()) { + jsonObject.put(formatUsageLimitConsumptionKey(usageLimitConsumption.getService(), item.getUsageLimit()), item.getQuantity()); } return jsonObject; diff --git a/space-client/src/main/java/io/github/pgmarc/space/serializers/SubscriptionRequestSerializer.java b/space-client/src/main/java/io/github/pgmarc/space/serializers/SubscriptionRequestSerializer.java index 545e0a3..1d71f71 100644 --- a/space-client/src/main/java/io/github/pgmarc/space/serializers/SubscriptionRequestSerializer.java +++ b/space-client/src/main/java/io/github/pgmarc/space/serializers/SubscriptionRequestSerializer.java @@ -5,7 +5,6 @@ import org.json.JSONObject; import io.github.pgmarc.space.contracts.AddOn; -import io.github.pgmarc.space.contracts.BillingPeriod; import io.github.pgmarc.space.contracts.Service; import io.github.pgmarc.space.contracts.Subscription; import io.github.pgmarc.space.contracts.SubscriptionRequest; @@ -19,14 +18,14 @@ public JSONObject toJson(SubscriptionRequest object) { JSONObject json = new JSONObject() .put(Subscription.Keys.USER_CONTACT.toString(), userContact(object.getUserContact())) .put(Subscription.Keys.BILLING_PERIOD.toString(), - Map.of(BillingPeriod.Keys.AUTORENEW.toString(), object.getRenewalDays() != null)) + Map.of("autoRenew", object.getRenewalDays() != null)) .put(Subscription.Keys.CONTRACTED_SERVICES.toString(), contractedServices(object.getServices())) .put(Subscription.Keys.SUBSCRIPTION_PLANS.toString(), subscriptionPlans(object.getServices())) .put(Subscription.Keys.SUBSCRIPTION_ADDONS.toString(), subscriptionAddOns(object.getServices())); if (object.getRenewalDays() != null) { json.getJSONObject(Subscription.Keys.BILLING_PERIOD.toString()) - .put(BillingPeriod.Keys.RENEWAL_DAYS.toString(), object.getRenewalDays().toDays()); + .put("renewalDays", object.getRenewalDays().toDays()); } return json; diff --git a/space-client/src/test/java/io/github/pgmarc/space/BaseEndpointTest.java b/space-client/src/test/java/io/github/pgmarc/space/BaseEndpointTest.java deleted file mode 100644 index 81adfa1..0000000 --- a/space-client/src/test/java/io/github/pgmarc/space/BaseEndpointTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.github.pgmarc.space; - -import com.github.tomakehurst.wiremock.junit5.WireMockExtension; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.extension.RegisterExtension; - -import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; - -public class BaseEndpointTest { - - protected static final String TEST_API_KEY = "prueba"; - protected static final OkHttpClient httpClient = new OkHttpClient.Builder().build(); - protected static HttpUrl url; - - @RegisterExtension - protected static WireMockExtension wm = WireMockExtension.newInstance() - .options(wireMockConfig().dynamicPort().globalTemplating(true)) - .build(); - - @BeforeAll - static void setUp() { - url = new HttpUrl.Builder().scheme("http").host("localhost").port(wm.getPort()).build(); - } - - -} diff --git a/space-client/src/test/java/io/github/pgmarc/space/ContractsEndpointTest.java b/space-client/src/test/java/io/github/pgmarc/space/ContractsEndpointTest.java index 61b8d1c..5183632 100644 --- a/space-client/src/test/java/io/github/pgmarc/space/ContractsEndpointTest.java +++ b/space-client/src/test/java/io/github/pgmarc/space/ContractsEndpointTest.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.time.Duration; +import java.time.Period; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -25,24 +26,25 @@ class ContractsEndpointTest { private static final String TEST_API_KEY = "prueba"; - private final OkHttpClient httpClient = new OkHttpClient.Builder().build(); - private static HttpUrl url; - private final ContractsEndpoint endpoint = new ContractsEndpoint(httpClient, url, TEST_API_KEY); + private static final OkHttpClient httpClient = new OkHttpClient.Builder().build(); + private static ContractsEndpoint endpoint; @RegisterExtension static WireMockExtension wm = WireMockExtension.newInstance() - .options(wireMockConfig().dynamicPort().globalTemplating(true)) - .build(); + .options(wireMockConfig().globalTemplating(true)) + .build(); @BeforeAll static void setUp() { - url = new HttpUrl.Builder().scheme("http").host("localhost").port(wm.getPort()).build(); + HttpUrl url = new HttpUrl.Builder().scheme("http").host("localhost").port(wm.getPort()).build(); + endpoint = new ContractsEndpoint(httpClient, url, TEST_API_KEY); } @Test void givenASubscriptionShouldBeCreated() { String userId = "01c36d29-0d6a-4b41-83e9-8c6d9310c508"; + int renewalDays = 45; wm.stubFor(post(urlEqualTo("/contracts")) .withHeader("x-api-key", equalTo(TEST_API_KEY)) @@ -53,7 +55,7 @@ void givenASubscriptionShouldBeCreated() { .withRequestBody(matchingJsonPath("$.contractedServices")) .withRequestBody(matchingJsonPath("$.subscriptionPlans")) .withRequestBody(matchingJsonPath("$.subscriptionAddOns")) - .willReturn( + .willReturn( created() .withHeader("Content-Type", "application/json") .withBodyFile("addContracts-response.hbs"))); @@ -68,13 +70,13 @@ void givenASubscriptionShouldBeCreated() { SubscriptionRequest subReq = SubscriptionRequest.builder(userContact) .renewIn(Duration.ofDays(45)) .startService("zoom", "2025") - .plan("ENTERPRISE") - .addOn("extraSeats", 2) - .addOn("hugeMeetings", 1) + .plan("ENTERPRISE") + .addOn("extraSeats", 2) + .addOn("hugeMeetings", 1) .endService() .startService("petclinic", "2024") - .plan("GOLD") - .addOn("petsAdoptionCentre", 1) + .plan("GOLD") + .addOn("petsAdoptionCentre", 1) .endService() .build(); @@ -84,7 +86,7 @@ void givenASubscriptionShouldBeCreated() { subscription = endpoint.addContract(subReq); assertThat(subscription.getServices()).isEqualTo(subReq.getServices()); assertThat(subscription.getUserId()).isEqualTo(userId); - assertThat(subscription.getRenewalDuration()).isPresent().hasValue(Duration.ofDays(45)); + assertThat(subscription.getRenewalPeriod()).hasValue(Period.ofDays(45)); assertThat(subscription.getHistory()).isEmpty(); } catch (IOException e) { fail(); @@ -109,7 +111,7 @@ void givenRequestWithNoApiKeyShouldThrow() { SubscriptionRequest subReq = SubscriptionRequest.builder(userContact) .startService("err", "v1") - .plan("Error") + .plan("Error") .endService() .build(); @@ -183,9 +185,10 @@ void givenAnUserIdAndServicesShouldUpdateSubscription() { .withBodyFile("getContractById-response.json"))); SubscriptionUpdateRequest subscription = SubscriptionUpdateRequest.builder() - .service("petclinic", "v1") - .plan("GOLD") - .add(); + .startService("petclinic", "v1") + .plan("GOLD") + .endService() + .build(); Subscription sub; try { sub = endpoint.updateContractByUserId(userId, subscription); diff --git a/space-client/src/test/java/io/github/pgmarc/space/FeaturesEndpointTest.java b/space-client/src/test/java/io/github/pgmarc/space/FeaturesEndpointTest.java index 777a4c4..b2a50e8 100644 --- a/space-client/src/test/java/io/github/pgmarc/space/FeaturesEndpointTest.java +++ b/space-client/src/test/java/io/github/pgmarc/space/FeaturesEndpointTest.java @@ -1,8 +1,12 @@ package io.github.pgmarc.space; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import io.github.pgmarc.space.features.Consumption; +import io.github.pgmarc.space.features.Revert; +import io.github.pgmarc.space.features.UsageLimitConsumption; import io.github.pgmarc.space.features.FeatureEvaluationResult; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -13,12 +17,15 @@ import static org.assertj.core.api.Assertions.*; @WireMockTest -class FeaturesEndpointTest extends BaseEndpointTest { +class FeaturesEndpointTest { + protected static final String TEST_API_KEY = "prueba"; + protected static final OkHttpClient httpClient = new OkHttpClient.Builder().build(); private static FeaturesEndpoint endpoint; @BeforeAll - static void setup() { + static void setup(WireMockRuntimeInfo wmRuntimeInfo) { + HttpUrl url = new HttpUrl.Builder().scheme("http").host("localhost").port(wmRuntimeInfo.getHttpPort()).build(); endpoint = new FeaturesEndpoint(httpClient, url, TEST_API_KEY); } @@ -28,7 +35,7 @@ void givenSimpleFeatureIdShouldEvaluate() { String userId = "e8e053c5-fd2b-4e4c-85a0-f1a52f0da72e"; String featureId = "petclinic-featureA"; - wm.stubFor(post(urlPathTemplate("/features/{userId}/{featureId}")) + stubFor(post(urlPathTemplate("/features/{userId}/{featureId}")) .withHeader("x-api-key", equalTo("prueba")) .withPathParam("userId", equalTo(userId)) .withPathParam("featureId", equalTo(featureId)) @@ -53,7 +60,7 @@ void givenConsumptionShouldEvaluateOptimistically() { String userId = "e8e053c5-fd2b-4e4c-85a0-f1a52f0da72e"; String featureId = "petclinic-featureA"; - wm.stubFor(post(urlPathTemplate("/features/{userId}/{featureId}")) + stubFor(post(urlPathTemplate("/features/{userId}/{featureId}")) .withHeader("x-api-key", equalTo("prueba")) .withPathParam("userId", equalTo(userId)) .withPathParam("featureId", equalTo(featureId)) @@ -67,8 +74,8 @@ void givenConsumptionShouldEvaluateOptimistically() { String usageLimit = "featureALimit"; try { - Consumption consumption = Consumption.builder().addInt(service, usageLimit, 100).build(); - FeatureEvaluationResult res = endpoint.evaluateOptimistically(userId, service, feature, consumption); + UsageLimitConsumption usageLimitConsumption = UsageLimitConsumption.builder(service).addInt(usageLimit, 100).build(); + FeatureEvaluationResult res = endpoint.evaluateOptimistically(userId, service, feature, usageLimitConsumption); assertThat(res.isAvailable()).isTrue(); assertThat(res.getConsumed(usageLimit)).hasValue(100); assertThat(res.getLimit(usageLimit)).hasValue(500); @@ -82,7 +89,7 @@ void givenRevertNewestCallShouldCompleteSucessfully() { String userId = "e8e053c5-fd2b-4e4c-85a0-f1a52f0da72e"; String featureId = "petclinic-featureA"; - wm.stubFor(post(urlPathTemplate("/features/{userId}/{featureId}")) + stubFor(post(urlPathTemplate("/features/{userId}/{featureId}")) .withHeader("x-api-key", equalTo("prueba")) .withRequestBody(absent()) .withPathParam("userId", equalTo(userId)) @@ -96,7 +103,7 @@ void givenRevertNewestCallShouldCompleteSucessfully() { String feature = "featureA"; try { - assertThat(endpoint.revert(userId, service, feature, FeaturesEndpoint.Revert.NEWEST_VALUE)) + assertThat(endpoint.revert(userId, service, feature, Revert.NEWEST_VALUE)) .isTrue(); } catch (IOException e) { fail(); @@ -108,7 +115,7 @@ void givenRevertOldestCallShouldCompleteSucessfully() { String userId = "e8e053c5-fd2b-4e4c-85a0-f1a52f0da72e"; String featureId = "petclinic-featureA"; - wm.stubFor(post(urlPathTemplate("/features/{userId}/{featureId}")) + stubFor(post(urlPathTemplate("/features/{userId}/{featureId}")) .withHeader("x-api-key", equalTo("prueba")) .withRequestBody(absent()) .withPathParam("userId", equalTo(userId)) @@ -122,7 +129,7 @@ void givenRevertOldestCallShouldCompleteSucessfully() { String feature = "featureA"; try { - assertThat(endpoint.revert(userId, service, feature, FeaturesEndpoint.Revert.OLDEST_VALUE)) + assertThat(endpoint.revert(userId, service, feature, Revert.OLDEST_VALUE)) .isTrue(); } catch (IOException e) { fail(); @@ -134,7 +141,7 @@ void getPricingTokenByUserId() { String userId = "e8e053c5-fd2b-4e4c-85a0-f1a52f0da72e"; - wm.stubFor(post(urlPathTemplate("/features/{userId}/pricing-token")) + stubFor(post(urlPathTemplate("/features/{userId}/pricing-token")) .withHeader("x-api-key", equalTo("prueba")) .withRequestBody(absent()) .withPathParam("userId", equalTo(userId)) diff --git a/space-client/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java b/space-client/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java deleted file mode 100644 index 9f8e214..0000000 --- a/space-client/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java +++ /dev/null @@ -1,47 +0,0 @@ -package io.github.pgmarc.space.contracts; - -import static org.assertj.core.api.Assertions.*; - -import java.time.Duration; -import java.time.LocalDateTime; -import java.time.ZonedDateTime; - -import org.junit.jupiter.api.Test; - -class BillingPeriodTest { - - private final ZonedDateTime start = ZonedDateTime.parse("2025-08-15T00:00:00Z"); - private final ZonedDateTime end = start.plusDays(30); - - @Test - void givenZeroRenewalDaysShouldThrow() { - - BillingPeriod period = BillingPeriod.of(start, end); - Duration duration = Duration.ofHours(12); - - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> period.setRenewalDays(duration)) - .withMessage("your subscription cannot expire in less than one day"); - } - - @Test - void givenStartDateAfterEndDateShouldThrow() { - - ZonedDateTime endDate = start.minusDays(1); - - assertThatExceptionOfType(IllegalStateException.class) - .isThrownBy(() -> BillingPeriod.of(start, endDate)) - .withMessage("startDate is after endDate"); - } - - @Test - void givenRenewableDateShouldBeRenowable() { - - int days = 30; - BillingPeriod billingPeriod = BillingPeriod.of(start, end); - billingPeriod.setRenewalDays(Duration.ofDays(days)); - - assertThat(billingPeriod.getDuration().toDays()).isEqualTo(days); - assertThat(billingPeriod.getRenewalDate()).isPresent().hasValue(end.plusDays(30).toLocalDateTime()); - } -} diff --git a/space-client/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java b/space-client/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java index 985e2fc..2f29fdc 100644 --- a/space-client/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java +++ b/space-client/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java @@ -3,9 +3,7 @@ import static org.assertj.core.api.Assertions.*; import java.time.Duration; -import java.time.ZonedDateTime; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class SubscriptionRequestTest { @@ -13,16 +11,6 @@ class SubscriptionRequestTest { private static final UserContact TEST_USER_CONTACT = UserContact.builder("123456789", "alexdoe") .build(); - private BillingPeriod billingPeriod; - - @BeforeEach - void setUp() { - ZonedDateTime start = ZonedDateTime.parse("2025-08-15T00:00:00Z"); - ZonedDateTime end = start.plusDays(30); - billingPeriod = BillingPeriod.of(start, end); - billingPeriod.setRenewalDays(Duration.ofDays(30)); - } - @Test void givenMultipleServicesInSubscriptionShouldCreate() { @@ -122,7 +110,7 @@ void givenNoEndServiceShouldThrow() { } @Test - void foo() { + void givenEmptySubscriptionRequestShouldThrow() { SubscriptionRequest.Builder builder = SubscriptionRequest.builder(TEST_USER_CONTACT); diff --git a/space-client/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java b/space-client/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java index 29fe1c2..95bb911 100644 --- a/space-client/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java +++ b/space-client/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java @@ -2,21 +2,21 @@ import static org.assertj.core.api.Assertions.*; - -import org.eclipse.jetty.server.Authentication; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import java.time.Duration; import java.time.LocalDateTime; +import java.time.Period; import java.time.ZonedDateTime; import java.util.Map; import java.util.Set; -public class SubscriptionTest { +class SubscriptionTest { private static final UserContact TEST_CONTACT = UserContact.builder("123456789", "alex").build(); + private static final ZonedDateTime START = ZonedDateTime.parse("2025-08-15T00:00:00Z"); + private static final ZonedDateTime END = START.plusDays(30); private static final Service TEST_SERVICE = Service.builder("petclinic", "2025").plan("GOLD").build(); @Test @@ -25,28 +25,33 @@ void givenSubscriptionShould() { String userId = "123456789"; String username = "alex"; UserContact contact = UserContact.builder(userId, username).build(); - ZonedDateTime start = ZonedDateTime.parse("2025-08-28T00:00:00Z"); - ZonedDateTime end = ZonedDateTime.parse("2025-08-29T00:00:00Z"); + String serviceName = "petclinic"; - BillingPeriod billingPeriod = BillingPeriod.of(start, end, Duration.ofDays(45)); Service service = Service.builder(serviceName, "2025").plan("GOLD").build(); ZonedDateTime snapshotStart = ZonedDateTime.parse("2024-01-01T00:00:00Z"); ZonedDateTime snapshotEnd = ZonedDateTime.parse("2025-01-01T00:00:00Z"); Service snapshotService = Service.builder(serviceName, "2024").plan("FREE").build(); - Subscription.Snapshot snapshot1 = Subscription.Snapshot.of(snapshotStart.toLocalDateTime(), - snapshotEnd.toLocalDateTime(), Map.of("petclinic", snapshotService)); + Subscription.Snapshot snapshot1 = Subscription.Snapshot.of(snapshotStart, + snapshotEnd, Map.of("petclinic", snapshotService)); Set history = Set.of(snapshot1); - Subscription subscription = Subscription.builder(contact, billingPeriod, service) + ZonedDateTime start = ZonedDateTime.parse("2025-08-28T00:00:00Z"); + ZonedDateTime end = ZonedDateTime.parse("2025-08-29T00:00:00Z"); + int renewalDays = 45; + Period renewalPeriod = Period.ofDays(renewalDays); + + Subscription subscription = Subscription.builder(contact, start, end, service) + .renewInDays(renewalDays) .addSnapshots(history).build(); assertThat(subscription.getUsername()).isEqualTo(username); assertThat(subscription.getStartDate()).isEqualTo(start.toLocalDateTime()); assertThat(subscription.getEndDate()).isEqualTo(end.toLocalDateTime()); assertThat(subscription.isAutoRenewable()).isTrue(); - assertThat(subscription.getRenewalDate()).hasValue(end.plusDays(45).toLocalDateTime()); + assertThat(subscription.getRenewalPeriod()).hasValue(renewalPeriod); + assertThat(subscription.getRenewalDate()).hasValue(end.plusDays(renewalDays).toLocalDateTime()); assertThat(subscription.getService(serviceName)).hasValue(service); assertThat(subscription.getHistory()).contains(snapshot1); assertThat(subscription.getHistory().get(0).getStartDate()).isEqualTo(snapshotStart.toLocalDateTime()); @@ -55,13 +60,34 @@ void givenSubscriptionShould() { assertThat(subscription.getHistory().get(0).getService(serviceName)).hasValue(snapshotService); } + @Test + void givenRenewalPeriodInDaysShouldCreate() { + + int renewalDays = 45; + Subscription subscription = Subscription.builder(TEST_CONTACT, START, END, TEST_SERVICE) + .renewInDays(renewalDays) + .build(); + + assertThat(subscription.getRenewalPeriod()).hasValue(Period.ofDays(renewalDays)); + + } + + @Test + void givenYearlySubscriptionShouldCreate() { + int years = 1; + Subscription subscription = Subscription.builder(TEST_CONTACT, START, END, TEST_SERVICE) + .renewInYears(years) + .build(); + + assertThat(subscription.getRenewalPeriod()).hasValue(Period.ofYears(years)); + } + @Test void givenDateAfterSubscriptionShouldBeExpired() { ZonedDateTime start = ZonedDateTime.parse("2025-01-01T00:00:00Z"); ZonedDateTime end = ZonedDateTime.parse("2025-02-01T00:00:00Z"); - BillingPeriod period = BillingPeriod.of(start, end); - Subscription subscription = Subscription.builder(TEST_CONTACT, period, TEST_SERVICE).build(); + Subscription subscription = Subscription.builder(TEST_CONTACT, start, end, TEST_SERVICE).build(); LocalDateTime dateToCheck = ZonedDateTime.parse("2025-02-01T00:00:01Z").toLocalDateTime(); assertThat(subscription.isExpired(dateToCheck)).isTrue(); @@ -79,11 +105,68 @@ void givenDateInsideSubscriptionShouldBeActive(String utcDate) { ZonedDateTime start = ZonedDateTime.parse("2025-01-01T00:00:00Z"); ZonedDateTime end = ZonedDateTime.parse("2025-02-01T00:00:00Z"); - BillingPeriod period = BillingPeriod.of(start, end); - Subscription subscription = Subscription.builder(TEST_CONTACT, period, TEST_SERVICE).build(); + Subscription subscription = Subscription.builder(TEST_CONTACT, start, end, TEST_SERVICE).build(); LocalDateTime dateToCheck = ZonedDateTime.parse(utcDate).toLocalDateTime(); assertThat(subscription.isExpired(dateToCheck)).isFalse(); assertThat(subscription.isActive(dateToCheck)).isTrue(); } + + + @Test + void givenNegativePeriodShouldThrow() { + + Subscription.Builder builder = Subscription.builder(TEST_CONTACT, START, END, TEST_SERVICE) + .renewIn(Period.ofDays(-1)); + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(builder::build) + .withMessage("renewal period must not be negative"); + } + + @Test + void givenStartDateAfterEndDateShouldThrow() { + + ZonedDateTime endDate = START.minusDays(1); + Subscription.Builder builder = Subscription.builder(TEST_CONTACT, START, endDate, TEST_SERVICE); + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(builder::build) + .withMessage("startDate is after endDate"); + } + + @Test + void givenRenewablePeriodSubscriptionShouldBeRenowable() { + + int months = 1; + Period period = Period.ofMonths(months); + + Subscription subscription = Subscription.builder(TEST_CONTACT, START, END, TEST_SERVICE) + .renewInMonths(months) + .build(); + + assertThat(subscription.isAutoRenewable()).isTrue(); + assertThat(subscription.getRenewalPeriod()).hasValue(period); + assertThat(subscription.getRenewalDate()).hasValue(END.plus(period).toLocalDateTime()); + } + + @Test + void givenNullStartDateShouldThrow() { + + Subscription.Builder builder = Subscription.builder(TEST_CONTACT, null, END, TEST_SERVICE); + + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(builder::build) + .withMessage("start date must not be null"); + } + + @Test + void givenNullEmdDateShouldThrow() { + + Subscription.Builder builder = Subscription.builder(TEST_CONTACT, START, null, TEST_SERVICE); + + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(builder::build) + .withMessage("end date must not be null"); + } } diff --git a/space-client/src/test/java/io/github/pgmarc/space/contracts/SubscriptionUpdateRequestTest.java b/space-client/src/test/java/io/github/pgmarc/space/contracts/SubscriptionUpdateRequestTest.java new file mode 100644 index 0000000..1aee6ca --- /dev/null +++ b/space-client/src/test/java/io/github/pgmarc/space/contracts/SubscriptionUpdateRequestTest.java @@ -0,0 +1,78 @@ +package io.github.pgmarc.space.contracts; + +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +class SubscriptionUpdateRequestTest { + + @Test + void givenParametersShouldCreateSubscriptionUpdateRequest() { + + String serviceName = "Petclinic"; + String version = "v3"; + String plan = "PLATINUM"; + String addOn = "petLover"; + int quantity = 1; + Service petclinic = Service.builder(serviceName, version) + .plan(plan) + .addOn(addOn, quantity) + .build(); + + SubscriptionUpdateRequest upReq = SubscriptionUpdateRequest.builder() + .startService(serviceName, version) + .plan(plan) + .addOn(addOn, quantity) + .endService() + .build(); + + assertThat(upReq.getServices()).contains(petclinic); + } + + + @Test + void givenMultipleServicesInSubscriptionShouldCreate() { + + String service1Name = "Petclinic"; + String service2Name = "Petclinic Labs"; + + Service service1 = Service.builder(service1Name, "v1").plan("GOLD").build(); + Service service2 = Service.builder(service2Name, "v2").plan("PLATINUM").build(); + + SubscriptionUpdateRequest sub = SubscriptionUpdateRequest.builder() + .subscribe(service1) + .subscribe(service2) + .build(); + + assertThat(sub.getServices()).contains(service1, service2); + } + + @Test + void givenServiceCollectionShouldCreate() { + + String service1Name = "Petclinic"; + String service2Name = "Petclinic Labs"; + + Service service1 = Service.builder(service1Name, "v1").plan("GOLD").build(); + Service service2 = Service.builder(service2Name, "v2").plan("PLATINUM").build(); + Set services = Set.of(service1, service2); + + SubscriptionUpdateRequest sub = SubscriptionUpdateRequest.builder() + .subscribeAll(services) + .build(); + + assertThat(sub.getServices()).contains(service1, service2); + } + + @Test + void givenEmptyCollectionOfServicesShouldThrow() { + + SubscriptionUpdateRequest.Builder builder = SubscriptionUpdateRequest.builder(); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> builder.subscribeAll(Set.of())) + .withMessage("services must not be empty"); + } +} diff --git a/space-client/src/test/java/io/github/pgmarc/space/deserializers/BillingPeriodSerializerTest.java b/space-client/src/test/java/io/github/pgmarc/space/deserializers/BillingPeriodSerializerTest.java deleted file mode 100644 index 0211f85..0000000 --- a/space-client/src/test/java/io/github/pgmarc/space/deserializers/BillingPeriodSerializerTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.github.pgmarc.space.deserializers; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.time.Duration; -import java.time.ZonedDateTime; - -import org.json.JSONObject; -import org.junit.jupiter.api.Test; - -import io.github.pgmarc.space.contracts.BillingPeriod; - -class BillingPeriodSerializerTest { - - private final BillingPeriodDeserializer serializer = new BillingPeriodDeserializer(); - - - - @Test - void givenJsonShouldCreateBillingPeriod() { - - String startUtc = "2024-08-20T12:00Z"; - String endUtc = "2025-08-20T12:00Z"; - ZonedDateTime start = ZonedDateTime.parse(startUtc); - ZonedDateTime end = ZonedDateTime.parse(endUtc); - BillingPeriod expected = BillingPeriod.of(start, end); - expected.setRenewalDays(Duration.ofDays(30)); - - JSONObject input = new JSONObject().put("startDate", startUtc) - .put("endDate", endUtc).put("autoRenew", true).put("renewalDays", 30L); - - assertEquals(expected, serializer.fromJson(input)); - } - - @Test - void giveNullJsonShouldThrow() { - - Exception ex = assertThrows(NullPointerException.class, () -> serializer.fromJson(null)); - assertEquals("billing period json must not be null", ex.getMessage()); - } - -} diff --git a/space-client/src/test/java/io/github/pgmarc/space/deserializers/SubscriptionSerializerTest.java b/space-client/src/test/java/io/github/pgmarc/space/deserializers/SubscriptionSerializerTest.java index 4601d9e..4dec0b4 100644 --- a/space-client/src/test/java/io/github/pgmarc/space/deserializers/SubscriptionSerializerTest.java +++ b/space-client/src/test/java/io/github/pgmarc/space/deserializers/SubscriptionSerializerTest.java @@ -1,16 +1,20 @@ package io.github.pgmarc.space.deserializers; import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertEquals; +import java.time.ZonedDateTime; import java.util.List; import java.util.Map; +import io.github.pgmarc.space.contracts.UsageLevel; import org.json.JSONObject; import org.junit.jupiter.api.Test; import io.github.pgmarc.space.contracts.Subscription; +import static org.assertj.core.api.Assertions.*; + + class SubscriptionSerializerTest { private final SubscriptionDeserializer serializer = new SubscriptionDeserializer(); @@ -18,6 +22,10 @@ class SubscriptionSerializerTest { @Test void givenSubscriptionAsJsonShouldCreateSubscription() { + String maxVisits = "maxVisits"; + double visitsConsumed = 5.0; + String maxVisitsResetUtc = "2025-07-31T00:00:00Z"; + JSONObject input = new JSONObject(Map.of( "id", "68050bd09890322c57842f6f", "userContact", Map.of( @@ -32,14 +40,14 @@ void givenSubscriptionAsJsonShouldCreateSubscription() { "endDate", "2025-12-31T00:00:00Z", "autoRenew", true, "renewalDays", 365), - "usageLevel", Map.of( + "usageLevels", Map.of( "zoom", Map.of( "maxSeats", Map.of("consumed", 10)), "petclinic", Map.of( "maxPets", Map.of("consumed", 2), "maxVisits", Map.of( - "consumed", 5, - "resetTimeStamp", "2025-07-31T00:00:00Z"))), + "consumed", visitsConsumed, + "resetTimeStamp", maxVisitsResetUtc))), "contractedServices", Map.of( "zoom", "2025", "petclinic", "2024"), @@ -70,10 +78,16 @@ void givenSubscriptionAsJsonShouldCreateSubscription() { "petsAdoptionCentre", 1)))))); Subscription actual = serializer.fromJson(input); + assertAll( - () -> assertEquals(1, actual.getHistory().size()), - () -> assertEquals(2, actual.getUsageLevels().size()), - () -> assertEquals(2, actual.getServicesMap().size())); + () -> assertThat(actual.getHistory()).hasSize(1), + () -> assertThat(actual.getUsageLevels()).hasSize(2), + () -> assertThat(actual.getServicesMap()).hasSize(2)); + + ZonedDateTime maxVisitsExpiration = ZonedDateTime.parse(maxVisitsResetUtc); + + UsageLevel actualLevel = actual.getUsageLevels().get("petclinic").get(maxVisits); + assertThat(actualLevel).isEqualTo(UsageLevel.of(maxVisits,visitsConsumed, maxVisitsExpiration)); } diff --git a/space-client/src/test/java/io/github/pgmarc/space/features/UsageLimitConsumptionTest.java b/space-client/src/test/java/io/github/pgmarc/space/features/UsageLimitConsumptionTest.java new file mode 100644 index 0000000..d352385 --- /dev/null +++ b/space-client/src/test/java/io/github/pgmarc/space/features/UsageLimitConsumptionTest.java @@ -0,0 +1,53 @@ +package io.github.pgmarc.space.features; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +class UsageLimitConsumptionTest { + + @Test + void givenConsumptionServiceNameMustBeLowerCase() { + + String service = "Petclinic"; + UsageLimitConsumption usageLimitConsumption = UsageLimitConsumption.builder(service) + .addInt("intLimit", Integer.MAX_VALUE) + .addLong("longLimit", Long.MAX_VALUE) + .addFloat("floatLimit", Float.MAX_VALUE) + .addDouble("doubleLimit", Double.MAX_VALUE) + .build(); + assertThat(usageLimitConsumption.getService()).isLowerCase(); + assertThat(usageLimitConsumption.getConsumption()).isNotEmpty(); + } + + @Test + void givenNullServiceNameShouldThrow() { + + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> UsageLimitConsumption.builder(null)) + .withMessage("service name must not be null"); + } + + @Test + void givenEmptyServiceNameShouldThrow() { + + UsageLimitConsumption.Builder builder = UsageLimitConsumption.builder(""); + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(builder::build) + .withMessage("service name must not be blank"); + } + + @Test + void givenEmptyUsageLimitConsumptionShouldThrow() { + + UsageLimitConsumption.Builder builder = UsageLimitConsumption.builder("Petclinic"); + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(builder::build) + .withMessage("usage limits consumption must not be empty"); + } + + + +} diff --git a/space-client/src/test/java/io/github/pgmarc/space/serializers/ConsumptionSerializerTest.java b/space-client/src/test/java/io/github/pgmarc/space/serializers/UsageLimitConsumptionSerializerTest.java similarity index 72% rename from space-client/src/test/java/io/github/pgmarc/space/serializers/ConsumptionSerializerTest.java rename to space-client/src/test/java/io/github/pgmarc/space/serializers/UsageLimitConsumptionSerializerTest.java index 4f99b2c..f087bad 100644 --- a/space-client/src/test/java/io/github/pgmarc/space/serializers/ConsumptionSerializerTest.java +++ b/space-client/src/test/java/io/github/pgmarc/space/serializers/UsageLimitConsumptionSerializerTest.java @@ -1,12 +1,12 @@ package io.github.pgmarc.space.serializers; -import io.github.pgmarc.space.features.Consumption; +import io.github.pgmarc.space.features.UsageLimitConsumption; import org.json.JSONObject; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.*; -class ConsumptionSerializerTest { +class UsageLimitConsumptionSerializerTest { private final ConsumptionSerializer serializer = new ConsumptionSerializer(); @@ -19,20 +19,20 @@ void givenConsumptionShouldSerialize() { float barConsumption = 0.1f; double consumedSeconds = (double) Float.MAX_VALUE + 1; - Consumption consumption = Consumption.builder() - .addInt(service, "maxPets", petsRegistered) - .addLong(service, "fooLimit", fooConsumption) - .addDouble(service, "maxSeconds", consumedSeconds) - .addFloat(service, "barLimit", barConsumption) + UsageLimitConsumption usageLimitConsumption = UsageLimitConsumption.builder(service) + .addInt( "maxPets", petsRegistered) + .addLong( "fooLimit", fooConsumption) + .addDouble("maxSeconds", consumedSeconds) + .addFloat("barLimit", barConsumption) .build(); - assertThatNoException().isThrownBy(() -> serializer.toJson(consumption)); + assertThatNoException().isThrownBy(() -> serializer.toJson(usageLimitConsumption)); String intUsageLimit = "petclinic-maxPets"; String longUsageLimit = "petclinic-fooLimit"; String floatUsageLimit = "petclinic-barLimit"; String doubleUsageLimit = "petclinic-maxSeconds"; - JSONObject consumptionPayload = serializer.toJson(consumption); + JSONObject consumptionPayload = serializer.toJson(usageLimitConsumption); assertThat(consumptionPayload.keySet()).contains(intUsageLimit, longUsageLimit, floatUsageLimit, doubleUsageLimit); assertThat(consumptionPayload.getInt(intUsageLimit)).isEqualTo(petsRegistered); diff --git a/space-client/src/test/resources/__files/addContracts-response.hbs b/space-client/src/test/resources/__files/addContracts-response.hbs index 82cbcea..735ddfe 100644 --- a/space-client/src/test/resources/__files/addContracts-response.hbs +++ b/space-client/src/test/resources/__files/addContracts-response.hbs @@ -7,7 +7,7 @@ "autoRenew": true, "renewalDays": {{jsonPath request.body '$.billingPeriod.renewalDays'}} }, - "usageLevel": { + "usageLevels": { "zoom": { "maxSeats": { "consumed": 10 diff --git a/space-client/src/test/resources/__files/getContractById-response.json b/space-client/src/test/resources/__files/getContractById-response.json index 6c950bd..9ad4e20 100644 --- a/space-client/src/test/resources/__files/getContractById-response.json +++ b/space-client/src/test/resources/__files/getContractById-response.json @@ -10,7 +10,7 @@ "autoRenew": false, "renewalDays": 30 }, - "usageLevel": { + "usageLevels": { "zoom": { "maxSeats": { "consumed": 10