killbill-memoizeit
Merge remote-tracking branch 'origin/master' into work-for-release-0.19.x Signed-off-by: …
1/28/2018 11:25:00 PM
Changes
invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java 26(+23 -3)
NEWS 6(+6 -0)
Details
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java
index d194140..aeb8483 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java
@@ -124,9 +124,18 @@ public class SubscriptionItemTree {
Preconditions.checkState(!isBuilt);
for (final InvoiceItem item : pendingItemAdj) {
- final Item fullyAdjustedItem = root.addAdjustment(item, targetInvoiceId);
- if (fullyAdjustedItem != null) {
- existingFullyAdjustedItems.add(fullyAdjustedItem);
+ // If the linked item was ignored, ignore this adjustment too
+ final InvoiceItem ignoredLinkedItem = Iterables.tryFind(existingIgnoredItems, new Predicate<InvoiceItem>() {
+ @Override
+ public boolean apply(final InvoiceItem input) {
+ return input.getId().equals(item.getLinkedItemId());
+ }
+ }).orNull();
+ if (ignoredLinkedItem == null) {
+ final Item fullyAdjustedItem = root.addAdjustment(item, targetInvoiceId);
+ if (fullyAdjustedItem != null) {
+ existingFullyAdjustedItems.add(fullyAdjustedItem);
+ }
}
}
pendingItemAdj.clear();
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java b/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java
index 52b0e08..9f7868a 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 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
@@ -31,6 +31,7 @@ import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.callcontext.DefaultCallContext;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.invoice.InvoiceTestSuiteWithEmbeddedDB;
+import org.killbill.billing.invoice.TestInvoiceHelper.DryRunFutureDateArguments;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItem;
@@ -236,7 +237,22 @@ public class TestDefaultInvoiceUserApi extends InvoiceTestSuiteWithEmbeddedDB {
}
@Test(groups = "slow")
- public void testAdjustPartialInvoiceItem() throws Exception {
+ public void testAdjustPartialRecurringInvoiceItem() throws Exception {
+ testAdjustPartialInvoiceItem(true);
+ }
+
+ @Test(groups = "slow", description = "https://github.com/killbill/killbill/pull/831")
+ public void testAdjustPartialFixedInvoiceItem() throws Exception {
+ testAdjustPartialInvoiceItem(false);
+ }
+
+ private void testAdjustPartialInvoiceItem(final boolean recurring) throws Exception {
+ final Account account = invoiceUtil.createAccount(callContext);
+ final UUID accountId = account.getId();
+ final BigDecimal fixedPrice = recurring ? null : BigDecimal.ONE;
+ final BigDecimal recurringPrice = !recurring ? null : BigDecimal.ONE;
+ final UUID invoiceId = invoiceUtil.generateRegularInvoice(account, fixedPrice, recurringPrice, null, callContext);
+
final InvoiceItem invoiceItem = invoiceUserApi.getInvoice(invoiceId, callContext).getInvoiceItems().get(0);
// Verify we picked a non zero item
Assert.assertEquals(invoiceItem.getAmount().compareTo(BigDecimal.ZERO), 1);
@@ -268,6 +284,10 @@ public class TestDefaultInvoiceUserApi extends InvoiceTestSuiteWithEmbeddedDB {
// Verify the adjusted account balance
final BigDecimal adjustedAccountBalance = invoiceUserApi.getAccountBalance(accountId, callContext);
Assert.assertEquals(adjustedAccountBalance, adjustedInvoiceBalance);
+
+ // Verify future invoice generation
+ invoiceUtil.generateInvoice(account.getId(), null, new DryRunFutureDateArguments(), internalCallContext);
+ // Invoice may or may not be generated, but there is no exception
}
@Test(groups = "slow")
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
index b1846ee..bc084c3 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 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
@@ -76,7 +76,6 @@ import org.killbill.billing.subscription.api.SubscriptionBase;
import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
-import org.killbill.billing.util.cache.CacheControllerDispatcher;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.config.definition.InvoiceConfig;
@@ -197,6 +196,12 @@ public class TestInvoiceHelper {
}
public UUID generateRegularInvoice(final Account account, final LocalDate targetDate, final CallContext callContext) throws Exception {
+ final BigDecimal fixedPrice = null;
+ final BigDecimal recurringPrice = BigDecimal.ONE;
+ return generateRegularInvoice(account, fixedPrice, recurringPrice, targetDate, callContext);
+ }
+
+ public UUID generateRegularInvoice(final Account account, final BigDecimal fixedPrice, final BigDecimal recurringPrice, final LocalDate targetDate, final CallContext callContext) throws Exception {
final SubscriptionBase subscription = Mockito.mock(SubscriptionBase.class);
Mockito.when(subscription.getId()).thenReturn(UUID.randomUUID());
Mockito.when(subscription.getBundleId()).thenReturn(new UUID(0L, 0L));
@@ -205,26 +210,21 @@ public class TestInvoiceHelper {
final PlanPhase planPhase = MockPlanPhase.create1USDMonthlyEvergreen();
final DateTime effectiveDate = new DateTime().minusDays(1);
final Currency currency = Currency.USD;
- final BigDecimal fixedPrice = null;
events.add(createMockBillingEvent(account, subscription, effectiveDate, plan, planPhase,
- fixedPrice, BigDecimal.ONE, currency, BillingPeriod.MONTHLY, 1,
+ fixedPrice, recurringPrice, currency, BillingPeriod.MONTHLY, 1,
BillingMode.IN_ADVANCE, "", 1L, SubscriptionBaseTransitionType.CREATE));
Mockito.when(billingApi.getBillingEventsForAccountAndUpdateAccountBCD(Mockito.<UUID>any(), Mockito.<DryRunArguments>any(), Mockito.<InternalCallContext>any())).thenReturn(events);
- final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi,
- invoiceDao, internalCallContextFactory, invoicePluginDispatcher, locker, busService.getBus(),
- notificationQueueService, invoiceConfig, clock, parkedAccountsManager);
+ final InternalCallContext context = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
- Invoice invoice = dispatcher.processAccountFromNotificationOrBusEvent(account.getId(), targetDate, new DryRunFutureDateArguments(), internalCallContext);
+ Invoice invoice = generateInvoice(account.getId(), targetDate, new DryRunFutureDateArguments(), context);
Assert.assertNotNull(invoice);
- final InternalCallContext context = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
-
List<InvoiceModelDao> invoices = invoiceDao.getInvoicesByAccount(context);
Assert.assertEquals(invoices.size(), 0);
- invoice = dispatcher.processAccountFromNotificationOrBusEvent(account.getId(), targetDate, null, context);
+ invoice = generateInvoice(account.getId(), targetDate, null, context);
Assert.assertNotNull(invoice);
invoices = invoiceDao.getInvoicesByAccount(context);
@@ -233,6 +233,14 @@ public class TestInvoiceHelper {
return invoice.getId();
}
+ public Invoice generateInvoice(final UUID accountId, @Nullable final LocalDate targetDate, @Nullable final DryRunArguments dryRunArguments, final InternalCallContext internalCallContext) throws InvoiceApiException {
+ final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi,
+ invoiceDao, internalCallContextFactory, invoicePluginDispatcher, locker, busService.getBus(),
+ notificationQueueService, invoiceConfig, clock, parkedAccountsManager);
+
+ return dispatcher.processAccountFromNotificationOrBusEvent(accountId, targetDate, dryRunArguments, internalCallContext);
+ }
+
public SubscriptionBase createSubscription() throws SubscriptionBaseApiException {
final UUID uuid = UUID.randomUUID();
final UUID bundleId = UUID.randomUUID();
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 d5d36bf..55a4375 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
@@ -103,38 +103,11 @@ public class CatalogJson {
}
ProductJson productJson = productMap.get(product.getName());
if (productJson == null) {
- productJson = new ProductJson(product.getCategory().toString(),
- product.getName(),
- product.getPrettyName(),
- 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.getPrettyName(), plan.getRecurringBillingPeriod(), phases);
+ final PlanJson planJson = new PlanJson(plan);
productJson.getPlans().add(planJson);
}
@@ -147,53 +120,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) {
@@ -205,16 +132,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;
}
@@ -368,8 +285,13 @@ public class CatalogJson {
this.available = available;
}
- public ProductJson(final String type, final String name, final String prettyName, final List<String> included, final List<String> available) {
- this(type, name, prettyName, new LinkedList<PlanJson>(), included, available);
+ public ProductJson(final Product product) {
+ this.type = product.getCategory().toString();
+ this.name = product.getName();
+ this.prettyName = product.getPrettyName();
+ this.plans = new LinkedList<PlanJson>();
+ this.included = toProductNames(product.getIncluded());
+ this.available = toProductNames(product.getAvailable());
}
public String getType() {
@@ -448,6 +370,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 {
@@ -457,6 +389,19 @@ 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.prettyName = plan.getPrettyName();
+ this.billingPeriod = plan.getRecurringBillingPeriod();
+ this.phases = phases;
+ }
+
@JsonCreator
public PlanJson(@JsonProperty("name") final String name,
@JsonProperty("prettyName") final String prettyName,
@@ -815,6 +760,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,
@@ -831,15 +801,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;
}
@@ -879,7 +853,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;
}
@@ -895,6 +869,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 4eea0e9..c68ce54 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;
@@ -35,16 +38,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;
@@ -78,6 +95,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,
@@ -87,10 +105,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
@@ -129,8 +149,8 @@ public class CatalogResource extends JaxRsResourceBase {
final TenantContext tenantContext = context.createTenantContextNoAccountId(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);
@@ -196,6 +216,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.createTenantContextNoAccountId(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
@@ -205,11 +355,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.createCallContextNoAccountId(createdBy, reason, comment, request);
final SimplePlanDescriptor desc = new DefaultSimplePlanDescriptor(simplePlan.getPlanId(),
NEWS 6(+6 -0)
diff --git a/NEWS b/NEWS
index 9b7028a..74cd426 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,9 @@
+0.18.16
+ See https://github.com/killbill/killbill/releases/tag/killbill-0.18.16
+
+0.18.15
+ See https://github.com/killbill/killbill/releases/tag/killbill-0.18.15
+
0.18.14
See https://github.com/killbill/killbill/releases/tag/killbill-0.18.14