diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/CatalogJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/CatalogJson.java
index 381089a..d5e6f75 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/CatalogJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/CatalogJson.java
@@ -1,6 +1,6 @@
/*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
*
* The Billing Project licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
@@ -92,37 +92,11 @@ public class CatalogJson {
}
ProductJson productJson = productMap.get(product.getName());
if (productJson == null) {
- productJson = new ProductJson(product.getCategory().toString(),
- product.getName(),
- toProductNames(product.getIncluded()),
- toProductNames(product.getAvailable()));
+ productJson = new ProductJson(product);
productMap.put(product.getName(), productJson);
}
- // Build the phases associated with this plan
- final List<PhaseJson> phases = new LinkedList<PhaseJson>();
- for (final PlanPhase phase : plan.getAllPhases()) {
- final List<PriceJson> prices = new LinkedList<PriceJson>();
- if (phase.getRecurring() != null && phase.getRecurring().getRecurringPrice() != null) {
- for (final Price price : phase.getRecurring().getRecurringPrice().getPrices()) {
- prices.add(new PriceJson(price));
- }
- }
-
- final List<PriceJson> fixedPrices = new LinkedList<PriceJson>();
- if (phase.getFixed() != null && phase.getFixed().getPrice() != null) {
- for (final Price price : phase.getFixed().getPrice().getPrices()) {
- fixedPrices.add(new PriceJson(price));
- }
- }
-
- final DurationJson durationJson = new DurationJson(phase.getDuration().getUnit(), phase.getDuration().getNumber());
- final List<UsageJson> usagesJson = buildUsagesJson(phase.getUsages());
- final PhaseJson phaseJson = new PhaseJson(phase.getPhaseType().toString(), prices, fixedPrices, durationJson, usagesJson);
- phases.add(phaseJson);
- }
-
- final PlanJson planJson = new PlanJson(plan.getName(), plan.getRecurringBillingPeriod(), phases);
+ final PlanJson planJson = new PlanJson(plan);
productJson.getPlans().add(planJson);
}
@@ -135,53 +109,7 @@ public class CatalogJson {
}
- private List<UsageJson> buildUsagesJson(final Usage[] usages) throws CurrencyValueNull {
- List<UsageJson> usagesJson = new ArrayList<UsageJson>();
- for (int i = 0; i < usages.length; i++) {
- usagesJson.add(new UsageJson(usages[i].getBillingPeriod().toString(), buildTiers(usages[i].getTiers())));
- }
- return usagesJson;
- }
-
- private List<TierJson> buildTiers(final Tier[] tiers) throws CurrencyValueNull {
- List<TierJson> tiersJson = new ArrayList<TierJson>();
- if (tiers != null && tiers.length > 0) {
- for (int i=0; i < tiers.length; i++) {
- tiersJson.add(new TierJson(buildTieredBlocks(tiers[i].getTieredBlocks()),
- buildLimits(tiers[i].getLimits()),
- buildPrices(tiers[i].getFixedPrice()),
- buildPrices(tiers[i].getRecurringPrice())));
- }
- }
- return tiersJson;
- }
-
- private List<LimitJson> buildLimits(final Limit[] limits) {
- List<LimitJson> limitsJson = new ArrayList<LimitJson>();
- if (limits != null && limits.length > 0) {
- for (int i=0; i < limits.length; i++) {
- limitsJson.add(new LimitJson(limits[i].getUnit().getName(),
- limits[i].getMax().toString(),
- limits[i].getMin().toString()));
- }
- }
- return limitsJson;
- }
-
- private List<TieredBlockJson> buildTieredBlocks(final TieredBlock[] tieredBlocks) throws CurrencyValueNull {
- List<TieredBlockJson> tieredBlocksJson = new ArrayList<TieredBlockJson>();
- if (tieredBlocks != null && tieredBlocks.length > 0) {
- for (int i=0; i < tieredBlocks.length; i++) {
- tieredBlocksJson.add(new TieredBlockJson(tieredBlocks[i].getUnit().getName(),
- tieredBlocks[i].getSize().toString(),
- tieredBlocks[i].getMax().toString(),
- buildPrices(tieredBlocks[i].getPrice())));
- }
- }
- return tieredBlocksJson;
- }
-
- private List<PriceJson> buildPrices(final InternationalPrice internationalPrice) throws CurrencyValueNull {
+ private static List<PriceJson> buildPrices(final InternationalPrice internationalPrice) throws CurrencyValueNull {
List<PriceJson> pricesJson = new ArrayList<PriceJson>();
Price[] prices = (internationalPrice != null) ? internationalPrice.getPrices() : null;
if (prices != null && prices.length > 0) {
@@ -193,16 +121,6 @@ public class CatalogJson {
return pricesJson;
}
- private List<String> toProductNames(final Collection<Product> in) {
- return Lists.transform(ImmutableList.<Product>copyOf(in),
- new Function<Product, String>() {
- @Override
- public String apply(final Product input) {
- return input.getName();
- }
- });
- }
-
public List<ProductJson> getProducts() {
return products;
}
@@ -295,8 +213,12 @@ public class CatalogJson {
this.available = available;
}
- public ProductJson(final String type, final String name, final List<String> included, final List<String> available) {
- this(type, name, new LinkedList<PlanJson>(), included, available);
+ public ProductJson(final Product product) {
+ this.type = product.getCategory().toString();
+ this.name = product.getName();
+ this.plans = new LinkedList<PlanJson>();
+ this.included = toProductNames(product.getIncluded());
+ this.available = toProductNames(product.getAvailable());
}
public String getType() {
@@ -370,6 +292,16 @@ public class CatalogJson {
result = 31 * result + (available != null ? available.hashCode() : 0);
return result;
}
+
+ private List<String> toProductNames(final Collection<Product> in) {
+ return Lists.transform(ImmutableList.<Product>copyOf(in),
+ new Function<Product, String>() {
+ @Override
+ public String apply(final Product input) {
+ return input.getName();
+ }
+ });
+ }
}
public static class PlanJson {
@@ -378,6 +310,18 @@ public class CatalogJson {
private final BillingPeriod billingPeriod;
private final List<PhaseJson> phases;
+ public PlanJson(final Plan plan) throws CurrencyValueNull {
+ final List<PhaseJson> phases = new LinkedList<PhaseJson>();
+ for (final PlanPhase phase : plan.getAllPhases()) {
+ final PhaseJson phaseJson = new PhaseJson(phase);
+ phases.add(phaseJson);
+ }
+
+ this.name = plan.getName();
+ this.billingPeriod = plan.getRecurringBillingPeriod();
+ this.phases = phases;
+ }
+
@JsonCreator
public PlanJson(@JsonProperty("name") final String name,
@JsonProperty("billingPeriod") final BillingPeriod billingPeriod,
@@ -729,6 +673,31 @@ public class CatalogJson {
private final DurationJson duration;
private final List<UsageJson> usages;
+ public PhaseJson(final PlanPhase phase) throws CurrencyValueNull {
+ final List<PriceJson> prices = new LinkedList<PriceJson>();
+ if (phase.getRecurring() != null && phase.getRecurring().getRecurringPrice() != null) {
+ for (final Price price : phase.getRecurring().getRecurringPrice().getPrices()) {
+ prices.add(new PriceJson(price));
+ }
+ }
+
+ final List<PriceJson> fixedPrices = new LinkedList<PriceJson>();
+ if (phase.getFixed() != null && phase.getFixed().getPrice() != null) {
+ for (final Price price : phase.getFixed().getPrice().getPrices()) {
+ fixedPrices.add(new PriceJson(price));
+ }
+ }
+
+ final DurationJson durationJson = new DurationJson(phase.getDuration().getUnit(), phase.getDuration().getNumber());
+ final List<UsageJson> usagesJson = buildUsagesJson(phase.getUsages());
+
+ this.type = phase.getPhaseType().toString();
+ this.prices = prices;
+ this.fixedPrices = fixedPrices;
+ this.duration = durationJson;
+ this.usages = usagesJson;
+ }
+
@JsonCreator
public PhaseJson(@JsonProperty("type") final String type,
@JsonProperty("prices") final List<PriceJson> prices,
@@ -745,15 +714,19 @@ public class CatalogJson {
public String getType() {
return type;
}
+
public List<PriceJson> getPrices() {
return prices;
}
+
public List<PriceJson> getFixedPrices() {
return fixedPrices;
}
+
public DurationJson getDuration() {
return duration;
}
+
public List<UsageJson> getUsages() {
return usages;
}
@@ -793,7 +766,7 @@ public class CatalogJson {
if (duration != null ? !duration.equals(phaseJson.duration) : phaseJson.duration != null) {
return false;
}
- if (usages != null ? !usages.equals(phaseJson.usages) : phaseJson.usages!= null) {
+ if (usages != null ? !usages.equals(phaseJson.usages) : phaseJson.usages != null) {
return false;
}
@@ -809,6 +782,52 @@ public class CatalogJson {
result = 31 * result + (usages != null ? usages.hashCode() : 0);
return result;
}
+
+ private List<UsageJson> buildUsagesJson(final Usage[] usages) throws CurrencyValueNull {
+ List<UsageJson> usagesJson = new ArrayList<UsageJson>();
+ for (int i = 0; i < usages.length; i++) {
+ usagesJson.add(new UsageJson(usages[i].getBillingPeriod().toString(), buildTiers(usages[i].getTiers())));
+ }
+ return usagesJson;
+ }
+
+ private List<TierJson> buildTiers(final Tier[] tiers) throws CurrencyValueNull {
+ List<TierJson> tiersJson = new ArrayList<TierJson>();
+ if (tiers != null && tiers.length > 0) {
+ for (int i = 0; i < tiers.length; i++) {
+ tiersJson.add(new TierJson(buildTieredBlocks(tiers[i].getTieredBlocks()),
+ buildLimits(tiers[i].getLimits()),
+ buildPrices(tiers[i].getFixedPrice()),
+ buildPrices(tiers[i].getRecurringPrice())));
+ }
+ }
+ return tiersJson;
+ }
+
+ private List<LimitJson> buildLimits(final Limit[] limits) {
+ List<LimitJson> limitsJson = new ArrayList<LimitJson>();
+ if (limits != null && limits.length > 0) {
+ for (int i = 0; i < limits.length; i++) {
+ limitsJson.add(new LimitJson(limits[i].getUnit().getName(),
+ limits[i].getMax().toString(),
+ limits[i].getMin().toString()));
+ }
+ }
+ return limitsJson;
+ }
+
+ private List<TieredBlockJson> buildTieredBlocks(final TieredBlock[] tieredBlocks) throws CurrencyValueNull {
+ List<TieredBlockJson> tieredBlocksJson = new ArrayList<TieredBlockJson>();
+ if (tieredBlocks != null && tieredBlocks.length > 0) {
+ for (int i = 0; i < tieredBlocks.length; i++) {
+ tieredBlocksJson.add(new TieredBlockJson(tieredBlocks[i].getUnit().getName(),
+ tieredBlocks[i].getSize().toString(),
+ tieredBlocks[i].getMax().toString(),
+ buildPrices(tieredBlocks[i].getPrice())));
+ }
+ }
+ return tieredBlocksJson;
+ }
}
public static class PriceJson {
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CatalogResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CatalogResource.java
index 98b2f84..c02a75e 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CatalogResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CatalogResource.java
@@ -1,7 +1,9 @@
/*
* Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
*
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
@@ -18,6 +20,7 @@ package org.killbill.billing.jaxrs.resources;
import java.util.ArrayList;
import java.util.List;
+import java.util.UUID;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
@@ -34,16 +37,30 @@ import javax.ws.rs.core.UriInfo;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
import org.killbill.billing.account.api.AccountUserApi;
import org.killbill.billing.catalog.StandaloneCatalog;
import org.killbill.billing.catalog.VersionedCatalog;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.CatalogUserApi;
+import org.killbill.billing.catalog.api.CurrencyValueNull;
import org.killbill.billing.catalog.api.Listing;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.catalog.api.PriceList;
+import org.killbill.billing.catalog.api.Product;
import org.killbill.billing.catalog.api.SimplePlanDescriptor;
import org.killbill.billing.catalog.api.StaticCatalog;
import org.killbill.billing.catalog.api.user.DefaultSimplePlanDescriptor;
+import org.killbill.billing.entitlement.api.Subscription;
+import org.killbill.billing.entitlement.api.SubscriptionApi;
+import org.killbill.billing.entitlement.api.SubscriptionApiException;
+import org.killbill.billing.entitlement.api.SubscriptionEvent;
import org.killbill.billing.jaxrs.json.CatalogJson;
+import org.killbill.billing.jaxrs.json.CatalogJson.PhaseJson;
+import org.killbill.billing.jaxrs.json.CatalogJson.PlanJson;
+import org.killbill.billing.jaxrs.json.CatalogJson.PriceListJson;
+import org.killbill.billing.jaxrs.json.CatalogJson.ProductJson;
import org.killbill.billing.jaxrs.json.PlanDetailJson;
import org.killbill.billing.jaxrs.json.SimplePlanJson;
import org.killbill.billing.jaxrs.util.Context;
@@ -73,6 +90,7 @@ import static javax.ws.rs.core.MediaType.APPLICATION_XML;
public class CatalogResource extends JaxRsResourceBase {
private final CatalogUserApi catalogUserApi;
+ private final SubscriptionApi subscriptionApi;
@Inject
public CatalogResource(final JaxrsUriBuilder uriBuilder,
@@ -82,10 +100,12 @@ public class CatalogResource extends JaxRsResourceBase {
final AccountUserApi accountUserApi,
final PaymentApi paymentApi,
final CatalogUserApi catalogUserApi,
+ final SubscriptionApi subscriptionApi,
final Clock clock,
final Context context) {
super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, paymentApi, null, clock, context);
this.catalogUserApi = catalogUserApi;
+ this.subscriptionApi = subscriptionApi;
}
@TimedResource
@@ -124,8 +144,8 @@ public class CatalogResource extends JaxRsResourceBase {
final TenantContext tenantContext = context.createContext(request);
final DateTime catalogDateVersion = requestedDate != null ?
- DATE_TIME_FORMATTER.parseDateTime(requestedDate).toDateTime(DateTimeZone.UTC) :
- null;
+ DATE_TIME_FORMATTER.parseDateTime(requestedDate).toDateTime(DateTimeZone.UTC) :
+ null;
// Yack...
final VersionedCatalog catalog = (VersionedCatalog) catalogUserApi.getCatalog(catalogName, tenantContext);
@@ -191,6 +211,136 @@ public class CatalogResource extends JaxRsResourceBase {
return Response.status(Status.OK).entity(details).build();
}
+ @TimedResource
+ @GET
+ @Path("/plan")
+ @Produces(APPLICATION_JSON)
+ @ApiOperation(value = "Retrieve plan for a given subscription and date", response = PlanJson.class)
+ @ApiResponses(value = {})
+ public Response getPlanForSubscriptionAndDate(@QueryParam("subscriptionId") final String subscriptionIdString,
+ @QueryParam("requestedDate") final String requestedDateString,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException, CurrencyValueNull {
+ verifyNonNullOrEmpty(subscriptionIdString, "Subscription id needs to be specified");
+
+ final SubscriptionEvent lastEventBeforeRequestedDate = getLastEventBeforeDate(subscriptionIdString, requestedDateString, request);
+ if (lastEventBeforeRequestedDate == null) {
+ return Response.status(Status.BAD_REQUEST).entity(String.format("%s is before the subscription start date", requestedDateString)).type("text/plain").build();
+ }
+
+ final Plan plan = lastEventBeforeRequestedDate.getNextPlan();
+ if (plan == null) {
+ // Subscription was cancelled at that point
+ return Response.status(Status.BAD_REQUEST).entity(String.format("%s is after the subscription cancel date", requestedDateString)).type("text/plain").build();
+ }
+
+ final PlanJson planJson = new PlanJson(plan);
+ return Response.status(Status.OK).entity(planJson).build();
+ }
+
+ @TimedResource
+ @GET
+ @Path("/phase")
+ @Produces(APPLICATION_JSON)
+ @ApiOperation(value = "Retrieve phase for a given subscription and date", response = PhaseJson.class)
+ @ApiResponses(value = {})
+ public Response getPhaseForSubscriptionAndDate(@QueryParam("subscriptionId") final String subscriptionIdString,
+ @QueryParam("requestedDate") final String requestedDateString,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException, CurrencyValueNull {
+ verifyNonNullOrEmpty(subscriptionIdString, "Subscription id needs to be specified");
+
+ final SubscriptionEvent lastEventBeforeRequestedDate = getLastEventBeforeDate(subscriptionIdString, requestedDateString, request);
+ if (lastEventBeforeRequestedDate == null) {
+ return Response.status(Status.BAD_REQUEST).entity(String.format("%s is before the subscription start date", requestedDateString)).type("text/plain").build();
+ }
+
+ final PlanPhase phase = lastEventBeforeRequestedDate.getNextPhase();
+ if (phase == null) {
+ // Subscription was cancelled at that point
+ return Response.status(Status.BAD_REQUEST).entity(String.format("%s is after the subscription cancel date", requestedDateString)).type("text/plain").build();
+ }
+
+ final PhaseJson phaseJson = new PhaseJson(phase);
+ return Response.status(Status.OK).entity(phaseJson).build();
+ }
+
+ @TimedResource
+ @GET
+ @Path("/product")
+ @Produces(APPLICATION_JSON)
+ @ApiOperation(value = "Retrieve product for a given subscription and date", response = ProductJson.class)
+ @ApiResponses(value = {})
+ public Response getProductForSubscriptionAndDate(@QueryParam("subscriptionId") final String subscriptionIdString,
+ @QueryParam("requestedDate") final String requestedDateString,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException {
+ verifyNonNullOrEmpty(subscriptionIdString, "Subscription id needs to be specified");
+
+ final SubscriptionEvent lastEventBeforeRequestedDate = getLastEventBeforeDate(subscriptionIdString, requestedDateString, request);
+ if (lastEventBeforeRequestedDate == null) {
+ return Response.status(Status.BAD_REQUEST).entity(String.format("%s is before the subscription start date", requestedDateString)).type("text/plain").build();
+ }
+
+ final Product product = lastEventBeforeRequestedDate.getNextProduct();
+ if (product == null) {
+ // Subscription was cancelled at that point
+ return Response.status(Status.BAD_REQUEST).entity(String.format("%s is after the subscription cancel date", requestedDateString)).type("text/plain").build();
+ }
+
+ final ProductJson productJson = new ProductJson(product);
+ return Response.status(Status.OK).entity(productJson).build();
+ }
+
+ @TimedResource
+ @GET
+ @Path("/priceList")
+ @Produces(APPLICATION_JSON)
+ @ApiOperation(value = "Retrieve priceList for a given subscription and date", response = PriceListJson.class)
+ @ApiResponses(value = {})
+ public Response getPriceListForSubscriptionAndDate(@QueryParam("subscriptionId") final String subscriptionIdString,
+ @QueryParam("requestedDate") final String requestedDateString,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException {
+ verifyNonNullOrEmpty(subscriptionIdString, "Subscription id needs to be specified");
+
+ final SubscriptionEvent lastEventBeforeRequestedDate = getLastEventBeforeDate(subscriptionIdString, requestedDateString, request);
+ if (lastEventBeforeRequestedDate == null) {
+ return Response.status(Status.BAD_REQUEST).entity(String.format("%s is before the subscription start date", requestedDateString)).type("text/plain").build();
+ }
+
+ final PriceList priceList = lastEventBeforeRequestedDate.getNextPriceList();
+ if (priceList == null) {
+ // Subscription was cancelled at that point
+ return Response.status(Status.BAD_REQUEST).entity(String.format("%s is after the subscription cancel date", requestedDateString)).type("text/plain").build();
+ }
+
+ final PriceListJson priceListJson = new PriceListJson(priceList);
+ return Response.status(Status.OK).entity(priceListJson).build();
+ }
+
+ private SubscriptionEvent getLastEventBeforeDate(final String subscriptionIdString, final String requestedDateString, final HttpServletRequest request) throws SubscriptionApiException {
+ final TenantContext tenantContext = context.createContext(request);
+ final DateTime requestedDateTime = requestedDateString != null ?
+ DATE_TIME_FORMATTER.parseDateTime(requestedDateString).toDateTime(DateTimeZone.UTC) :
+ clock.getUTCNow();
+ final LocalDate requestedDate = requestedDateTime.toLocalDate();
+
+ final Subscription subscription = subscriptionApi.getSubscriptionForEntitlementId(UUID.fromString(subscriptionIdString), tenantContext);
+ SubscriptionEvent lastEventBeforeRequestedDate = null;
+ for (final SubscriptionEvent subscriptionEvent : subscription.getSubscriptionEvents()) {
+ if (lastEventBeforeRequestedDate == null) {
+ if (subscriptionEvent.getEffectiveDate().compareTo(requestedDate) > 0) {
+ // requestedDate too far in the past, before subscription start date
+ return null;
+ }
+ lastEventBeforeRequestedDate = subscriptionEvent;
+ }
+ if (subscriptionEvent.getEffectiveDate().compareTo(requestedDate) > 0) {
+ break;
+ } else {
+ lastEventBeforeRequestedDate = subscriptionEvent;
+ }
+ }
+
+ return lastEventBeforeRequestedDate;
+ }
@TimedResource
@POST
@@ -200,11 +350,11 @@ public class CatalogResource extends JaxRsResourceBase {
@ApiOperation(value = "Upload the full catalog as XML")
@ApiResponses(value = {})
public Response addSimplePlan(final SimplePlanJson simplePlan,
- @HeaderParam(HDR_CREATED_BY) final String createdBy,
- @HeaderParam(HDR_REASON) final String reason,
- @HeaderParam(HDR_COMMENT) final String comment,
- @javax.ws.rs.core.Context final HttpServletRequest request,
- @javax.ws.rs.core.Context final UriInfo uriInfo) throws Exception {
+ @HeaderParam(HDR_CREATED_BY) final String createdBy,
+ @HeaderParam(HDR_REASON) final String reason,
+ @HeaderParam(HDR_COMMENT) final String comment,
+ @javax.ws.rs.core.Context final HttpServletRequest request,
+ @javax.ws.rs.core.Context final UriInfo uriInfo) throws Exception {
final CallContext callContext = context.createContext(createdBy, reason, comment, request);
final SimplePlanDescriptor desc = new DefaultSimplePlanDescriptor(simplePlan.getPlanId(),