killbill-aplcache
Changes
account/pom.xml 2(+1 -1)
api/pom.xml 2(+1 -1)
beatrix/pom.xml 2(+1 -1)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoiceNotifications.java 45(+45 -0)
catalog/pom.xml 2(+1 -1)
catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalogWithPriceOverride.java 33(+27 -6)
catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalogWithPriceOverride.java 52(+50 -2)
catalog/src/test/resources/catalogTest.xml 34(+32 -2)
currency/pom.xml 2(+1 -1)
entitlement/pom.xml 2(+1 -1)
entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.java 7(+4 -3)
entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlement.java 40(+35 -5)
invoice/pom.xml 2(+1 -1)
invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java 26(+23 -3)
jaxrs/pom.xml 2(+1 -1)
junction/pom.xml 2(+1 -1)
NEWS 6(+6 -0)
overdue/pom.xml 2(+1 -1)
payment/pom.xml 2(+1 -1)
pom.xml 4(+2 -2)
profiles/killbill/pom.xml 2(+1 -1)
profiles/killbill/src/main/java/org/apache/shiro/authc/pam/ModularRealmAuthenticatorWith540.java 17(+12 -5)
profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java 28(+21 -7)
profiles/killpay/pom.xml 2(+1 -1)
profiles/pom.xml 2(+1 -1)
subscription/pom.xml 2(+1 -1)
tenant/pom.xml 2(+1 -1)
usage/pom.xml 2(+1 -1)
util/pom.xml 2(+1 -1)
Details
account/pom.xml 2(+1 -1)
diff --git a/account/pom.xml b/account/pom.xml
index 0a2e7c5..084df5c 100644
--- a/account/pom.xml
+++ b/account/pom.xml
@@ -19,7 +19,7 @@
<parent>
<artifactId>killbill</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.19.1-SNAPSHOT</version>
+ <version>0.19.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-account</artifactId>
api/pom.xml 2(+1 -1)
diff --git a/api/pom.xml b/api/pom.xml
index d937a42..0f70616 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -19,7 +19,7 @@
<parent>
<artifactId>killbill</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.19.1-SNAPSHOT</version>
+ <version>0.19.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-internal-api</artifactId>
beatrix/pom.xml 2(+1 -1)
diff --git a/beatrix/pom.xml b/beatrix/pom.xml
index 95fd17f..cf06b95 100644
--- a/beatrix/pom.xml
+++ b/beatrix/pom.xml
@@ -19,7 +19,7 @@
<parent>
<artifactId>killbill</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.19.1-SNAPSHOT</version>
+ <version>0.19.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-beatrix</artifactId>
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoiceNotifications.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoiceNotifications.java
index c51b21e..1b8b3ed 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoiceNotifications.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoiceNotifications.java
@@ -22,11 +22,16 @@ import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountData;
import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.PriceListSet;
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.api.DefaultEntitlement;
+import org.killbill.billing.entitlement.api.Entitlement;
+import org.killbill.billing.payment.api.PluginProperty;
import org.killbill.billing.platform.api.KillbillConfigSource;
import org.testng.annotations.Test;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
public class TestInvoiceNotifications extends TestIntegrationBase {
@@ -63,4 +68,44 @@ public class TestInvoiceNotifications extends TestIntegrationBase {
// And then verify the invoice is correctly generated
addDaysAndCheckForCompletion(7, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
}
+
+
+ @Test(groups = "slow")
+ public void testInvoiceNotificationWithFutureSubscriptionEvents() throws Exception {
+ clock.setDay(new LocalDate(2018, 1, 31));
+
+ final AccountData accountData = getAccountData(28);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+
+ final LocalDate billingDate = new LocalDate(2018, 2, 28);
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("pistol-monthly-notrial");
+
+
+ busHandler.pushExpectedEvents(NextEvent.BLOCK);
+ final Entitlement entitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, "bundleKey", null, null, billingDate, false, true, ImmutableList.<PluginProperty>of(), callContext);
+ busHandler.assertListenerStatus();
+
+ // Move to the notification before the start date => 2018, 2, 21
+ addDaysAndCheckForCompletion(21, NextEvent.INVOICE_NOTIFICATION);
+
+ // Move to the start date => 2018, 2, 28
+ addDaysAndCheckForCompletion(7, NextEvent.CREATE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+ final LocalDate futureChangeDate = new LocalDate(2018, 3, 28);
+
+ entitlement.changePlanWithDate(new PlanPhaseSpecifier("shotgun-monthly"), null, futureChangeDate, null, callContext);
+ assertListenerStatus();
+
+ // Move to the notification before the start date => 2018, 3, 21
+ addDaysAndCheckForCompletion(21, NextEvent.INVOICE_NOTIFICATION);
+
+
+ // Move to the change date => 2018, 3, 28
+ addDaysAndCheckForCompletion(7, NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.NULL_INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+ }
+
+
}
catalog/pom.xml 2(+1 -1)
diff --git a/catalog/pom.xml b/catalog/pom.xml
index 499e1f6..a528b15 100644
--- a/catalog/pom.xml
+++ b/catalog/pom.xml
@@ -19,7 +19,7 @@
<parent>
<artifactId>killbill</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.19.1-SNAPSHOT</version>
+ <version>0.19.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-catalog</artifactId>
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalogWithPriceOverride.java b/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalogWithPriceOverride.java
index f78480d..4729e6d 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalogWithPriceOverride.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalogWithPriceOverride.java
@@ -19,6 +19,7 @@ package org.killbill.billing.catalog;
import java.util.regex.Matcher;
+import org.killbill.billing.ErrorCode;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.CatalogApiException;
@@ -68,7 +69,6 @@ public class StandaloneCatalogWithPriceOverride extends StandaloneCatalog implem
return internalCallContextFactory;
}
-
@Override
public Plan createOrFindCurrentPlan(final PlanSpecifier spec, final PlanPhasePriceOverridesWithCallContext overrides) throws CatalogApiException {
final Plan defaultPlan = super.createOrFindCurrentPlan(spec, null);
@@ -86,8 +86,10 @@ public class StandaloneCatalogWithPriceOverride extends StandaloneCatalog implem
public DefaultPlan findCurrentPlan(final String planName) throws CatalogApiException {
final Matcher m = DefaultPriceOverride.CUSTOM_PLAN_NAME_PATTERN.matcher(planName);
if (m.matches()) {
- final InternalTenantContext internalTenantContext = createInternalTenantContext();
- return priceOverride.getOverriddenPlan(planName, this, internalTenantContext);
+ final DefaultPlan plan = maybeGetOverriddenPlan(planName);
+ if (plan != null) {
+ return plan;
+ }
}
return super.findCurrentPlan(planName);
}
@@ -102,13 +104,32 @@ public class StandaloneCatalogWithPriceOverride extends StandaloneCatalog implem
final String planName = DefaultPlanPhase.planName(phaseName);
final Matcher m = DefaultPriceOverride.CUSTOM_PLAN_NAME_PATTERN.matcher(planName);
if (m.matches()) {
- final InternalTenantContext internalTenantContext = createInternalTenantContext();
- final Plan plan = priceOverride.getOverriddenPlan(planName, this, internalTenantContext);
- return plan.findPhase(phaseName);
+ final DefaultPlan plan = maybeGetOverriddenPlan(planName);
+ if (plan != null) {
+ return plan.findPhase(phaseName);
+ }
}
return super.findCurrentPhase(phaseName);
}
+ private DefaultPlan maybeGetOverriddenPlan(final String planName) throws CatalogApiException {
+ final InternalTenantContext internalTenantContext = createInternalTenantContext();
+
+ try {
+ return priceOverride.getOverriddenPlan(planName, this, internalTenantContext);
+ } catch (final RuntimeException e) {
+ if (e.getCause() == null ||
+ e.getCause().getCause() == null ||
+ !(e.getCause().getCause() instanceof CatalogApiException) ||
+ ((CatalogApiException) e.getCause().getCause()).getCode() != ErrorCode.CAT_NO_SUCH_PLAN.getCode()) {
+ throw e;
+ } else {
+ // Otherwise, ambiguous name? See https://github.com/killbill/killbill/issues/842.
+ return null;
+ }
+ }
+ }
+
private InternalTenantContext createInternalTenantContext() {
return internalCallContextFactory.createInternalTenantContext(tenantRecordId, null);
}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java b/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java
index 585e2b0..448942f 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java
@@ -1,6 +1,6 @@
/*
- * 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,10 +31,12 @@ import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.catalog.api.MutableStaticCatalog;
import org.killbill.billing.catalog.api.PhaseType;
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.ProductCategory;
import org.killbill.billing.catalog.api.SimplePlanDescriptor;
+import org.killbill.billing.catalog.api.StaticCatalog;
import org.killbill.billing.catalog.api.TimeUnit;
import org.killbill.billing.catalog.api.user.DefaultSimplePlanDescriptor;
import org.killbill.xmlloader.XMLLoader;
@@ -62,6 +64,37 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
assertEquals(catalog.getCurrentPlans().size(), 0);
}
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/842")
+ public void testCreateAmbiguousPlan() throws CatalogApiException {
+ final DateTime now = clock.getUTCNow();
+ final SimplePlanDescriptor desc = new DefaultSimplePlanDescriptor("foo-monthly-12345", "Foo", ProductCategory.BASE, Currency.EUR, BigDecimal.TEN, BillingPeriod.MONTHLY, 0, TimeUnit.UNLIMITED, ImmutableList.<String>of());
+
+ final CatalogUpdater catalogUpdater = new CatalogUpdater(now, desc.getCurrency());
+ catalogUpdater.addSimplePlanDescriptor(desc);
+ final StandaloneCatalog catalog = catalogUpdater.getCatalog();
+
+ assertEquals(catalog.getCurrentPlans().size(), 1);
+
+ final StaticCatalog standaloneCatalogWithPriceOverride = new StandaloneCatalogWithPriceOverride(catalog,
+ priceOverride,
+ internalCallContext.getTenantRecordId(),
+ internalCallContextFactory);
+
+ final Plan plan = catalog.findCurrentPlan("foo-monthly-12345");
+ assertEquals(plan.getName(), "foo-monthly-12345");
+
+ // Verify PriceOverride logic
+ final Plan plan2 = standaloneCatalogWithPriceOverride.findCurrentPlan("foo-monthly-12345");
+ assertEquals(plan2.getName(), "foo-monthly-12345");
+
+ final PlanPhase planPhase = catalog.findCurrentPhase("foo-monthly-12345-evergreen");
+ assertEquals(planPhase.getName(), "foo-monthly-12345-evergreen");
+
+ // Verify PriceOverride logic
+ final PlanPhase phase2 = standaloneCatalogWithPriceOverride.findCurrentPhase("foo-monthly-12345-evergreen");
+ assertEquals(phase2.getName(), "foo-monthly-12345-evergreen");
+ }
+
@Test(groups = "fast")
public void testAddNoTrialPlanOnFirstCatalog() throws CatalogApiException {
@@ -102,7 +135,6 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
assertEquals(priceList.getPlans().iterator().next().getName(), "foo-monthly");
}
-
@Test(groups = "fast")
public void testAddTrialPlanOnFirstCatalog() throws CatalogApiException {
@@ -149,8 +181,6 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
assertEquals(priceList.getPlans().iterator().next().getName(), "foo-monthly");
}
-
-
@Test(groups = "fast")
public void testAddPlanOnExistingCatalog() throws Exception {
@@ -185,8 +215,6 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
assertEquals(priceList.getPlans().size(), 4);
}
-
-
@Test(groups = "fast")
public void testAddExistingPlanWithNewCurrency() throws Exception {
final StandaloneCatalog originalCatalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarBasic.xml").toExternalForm(), StandaloneCatalog.class);
@@ -231,7 +259,6 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
SimplePlanDescriptor desc = new DefaultSimplePlanDescriptor("standard-monthly", "Standard", ProductCategory.BASE, Currency.EUR, BigDecimal.TEN, BillingPeriod.MONTHLY, 0, TimeUnit.DAYS, ImmutableList.<String>of());
addBadSimplePlanDescriptor(catalogUpdater, desc);
-
// Existing Plan has a 30 days trial => try different trial length
desc = new DefaultSimplePlanDescriptor("standard-monthly", "Standard", ProductCategory.BASE, Currency.EUR, BigDecimal.TEN, BillingPeriod.MONTHLY, 14, TimeUnit.DAYS, ImmutableList.<String>of());
addBadSimplePlanDescriptor(catalogUpdater, desc);
@@ -257,11 +284,9 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
addBadSimplePlanDescriptor(catalogUpdater, desc);
}
-
@Test(groups = "fast")
public void testPlanWithNonFinalFixedTermPhase() throws Exception {
-
final StandaloneCatalog catalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarBasic.xml").toExternalForm(), StandaloneCatalog.class);
final MutableStaticCatalog mutableCatalog = new DefaultMutableStaticCatalog(catalog);
@@ -272,7 +297,6 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
newProduct.initialize((StandaloneCatalog) mutableCatalog, null);
mutableCatalog.addProduct(newProduct);
-
final DefaultPlanPhase trialPhase = new DefaultPlanPhase();
trialPhase.setPhaseType(PhaseType.TRIAL);
trialPhase.setDuration(new DefaultDuration().setUnit(TimeUnit.DAYS).setNumber(14));
@@ -284,24 +308,22 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
fixedTermPhase.setDuration(new DefaultDuration().setUnit(TimeUnit.MONTHS).setNumber(3));
fixedTermPhase.setRecurring(new DefaultRecurring().setBillingPeriod(BillingPeriod.MONTHLY).setRecurringPrice(new DefaultInternationalPrice().setPrices(new DefaultPrice[]{new DefaultPrice().setCurrency(Currency.USD).setValue(BigDecimal.TEN)})));
-
final DefaultPlanPhase evergreenPhase = new DefaultPlanPhase();
evergreenPhase.setPhaseType(PhaseType.EVERGREEN);
evergreenPhase.setDuration(new DefaultDuration().setUnit(TimeUnit.MONTHS).setNumber(1));
evergreenPhase.setRecurring(new DefaultRecurring().setBillingPeriod(BillingPeriod.MONTHLY).setRecurringPrice(new DefaultInternationalPrice().setPrices(new DefaultPrice[]{new DefaultPrice().setCurrency(Currency.USD).setValue(BigDecimal.TEN)})));
-
final DefaultPlan newPlan = new DefaultPlan();
newPlan.setName("something-with-fixed-term");
newPlan.setPriceListName(DefaultPriceListSet.DEFAULT_PRICELIST_NAME);
newPlan.setProduct(newProduct);
- newPlan.setInitialPhases(new DefaultPlanPhase[] {trialPhase, fixedTermPhase});
+ newPlan.setInitialPhases(new DefaultPlanPhase[]{trialPhase, fixedTermPhase});
newPlan.setFinalPhase(fixedTermPhase);
mutableCatalog.addPlan(newPlan);
newPlan.initialize((StandaloneCatalog) mutableCatalog, new URI("dummy"));
final String newCatalogStr = XMLWriter.writeXML((StandaloneCatalog) mutableCatalog, StandaloneCatalog.class);
- final StandaloneCatalog newCatalog = XMLLoader.getObjectFromStream(new URI("dummy"), new ByteArrayInputStream(newCatalogStr.getBytes(Charset.forName("UTF-8"))), StandaloneCatalog.class);
+ final StandaloneCatalog newCatalog = XMLLoader.getObjectFromStream(new URI("dummy"), new ByteArrayInputStream(newCatalogStr.getBytes(Charset.forName("UTF-8"))), StandaloneCatalog.class);
final DefaultPlan targetPlan = newCatalog.findCurrentPlan("something-with-fixed-term");
Assert.assertEquals(targetPlan.getInitialPhases().length, 2);
@@ -309,8 +331,6 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
}
-
-
@Test(groups = "fast")
public void testVerifyXML() throws Exception {
@@ -559,7 +579,6 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
System.err.println(catalogUpdater.getCatalogXML());
}
-
private StandaloneCatalog enhanceOriginalCatalogForInvalidTestCases(final String catalogName) throws Exception {
final StandaloneCatalog catalog = XMLLoader.getObjectFromString(Resources.getResource(catalogName).toExternalForm(), StandaloneCatalog.class);
@@ -592,7 +611,6 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
mutableCatalog.addPlan(newPlan1);
newPlan1.initialize((StandaloneCatalog) mutableCatalog, new URI("dummy"));
-
final DefaultProduct newProduct2 = new DefaultProduct();
newProduct2.setName("SuperDynamic");
newProduct2.setCatagory(ProductCategory.BASE);
@@ -605,7 +623,6 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
fixedterm2.setDuration(new DefaultDuration().setUnit(TimeUnit.MONTHS).setNumber(3));
fixedterm2.setRecurring(new DefaultRecurring().setBillingPeriod(BillingPeriod.MONTHLY).setRecurringPrice(new DefaultInternationalPrice().setPrices(new DefaultPrice[]{new DefaultPrice().setCurrency(Currency.USD).setValue(BigDecimal.TEN)})));
-
final DefaultPlan newPlan2 = new DefaultPlan();
newPlan2.setName("superdynamic-fixedterm");
newPlan2.setPriceListName(DefaultPriceListSet.DEFAULT_PRICELIST_NAME);
@@ -614,12 +631,10 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
mutableCatalog.addPlan(newPlan2);
newPlan2.initialize((StandaloneCatalog) mutableCatalog, new URI("dummy"));
-
final String newCatalogStr = XMLWriter.writeXML((StandaloneCatalog) mutableCatalog, StandaloneCatalog.class);
return XMLLoader.getObjectFromStream(new URI("dummy"), new ByteArrayInputStream(newCatalogStr.getBytes(Charset.forName("UTF-8"))), StandaloneCatalog.class);
}
-
private void addBadSimplePlanDescriptor(final CatalogUpdater catalogUpdater, final SimplePlanDescriptor desc) {
try {
catalogUpdater.addSimplePlanDescriptor(desc);
@@ -628,4 +643,4 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
assertEquals(e.getCode(), ErrorCode.CAT_FAILED_SIMPLE_PLAN_VALIDATION.getCode());
}
}
-}
\ No newline at end of file
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalogWithPriceOverride.java b/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalogWithPriceOverride.java
index 2a70ca6..571e023 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalogWithPriceOverride.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalogWithPriceOverride.java
@@ -1,6 +1,6 @@
/*
- * 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
@@ -17,19 +17,67 @@
package org.killbill.billing.catalog;
+import java.math.BigDecimal;
+import java.util.regex.Matcher;
+
import org.killbill.billing.ErrorCode;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
+import org.killbill.billing.catalog.api.PlanPhasePriceOverridesWithCallContext;
import org.killbill.billing.catalog.api.PlanSpecifier;
import org.killbill.billing.catalog.api.StaticCatalog;
+import org.killbill.billing.catalog.api.UsagePriceOverride;
+import org.killbill.billing.catalog.override.DefaultPriceOverride;
import org.killbill.xmlloader.XMLLoader;
+import org.mockito.Mockito;
import org.testng.Assert;
import org.testng.annotations.Test;
+import com.google.common.collect.ImmutableList;
import com.google.common.io.Resources;
public class TestStandaloneCatalogWithPriceOverride extends CatalogTestSuiteWithEmbeddedDB {
+ @Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/842")
+ public void testCreateAmbiguousPlan() throws Exception {
+ final StandaloneCatalog catalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarAdvanced.xml").toExternalForm(), StandaloneCatalog.class);
+ final StaticCatalog standaloneCatalogWithPriceOverride = new StandaloneCatalogWithPriceOverride(catalog,
+ priceOverride,
+ internalCallContext.getTenantRecordId(),
+ internalCallContextFactory);
+
+ // Create ambiguous plan name
+ final PlanSpecifier spec = new PlanSpecifier("standard-monthly-67890");
+ final PlanPhasePriceOverridesWithCallContext overrides = Mockito.mock(PlanPhasePriceOverridesWithCallContext.class);
+ Mockito.when(overrides.getCallContext()).thenReturn(callContext);
+ final PlanPhasePriceOverride override = new DefaultPlanPhasePriceOverride("standard-monthly-evergreen", Currency.USD, null, BigDecimal.ONE, ImmutableList.<UsagePriceOverride>of());
+ Mockito.when(overrides.getOverrides()).thenReturn(ImmutableList.of(override));
+ final Plan plan = standaloneCatalogWithPriceOverride.createOrFindCurrentPlan(spec, overrides);
+ Assert.assertTrue(plan.getName().startsWith("standard-monthly-67890-"));
+ final Matcher m = DefaultPriceOverride.CUSTOM_PLAN_NAME_PATTERN.matcher(plan.getName());
+ Assert.assertTrue(m.matches());
+
+ // From the catalog
+ Assert.assertNotNull(catalog.findCurrentPlan("standard-monthly"));
+ Assert.assertNotNull(standaloneCatalogWithPriceOverride.findCurrentPlan("standard-monthly"));
+
+ // Created on the fly
+ try {
+ catalog.findCurrentPlan(plan.getName());
+ Assert.fail();
+ } catch (final CatalogApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.CAT_NO_SUCH_PLAN.getCode());
+ }
+ Assert.assertNotNull(standaloneCatalogWithPriceOverride.findCurrentPlan("standard-monthly-1"));
+
+ // From the catalog
+ Assert.assertNotNull(catalog.findCurrentPlan("standard-monthly-12345"));
+ Assert.assertNotNull(standaloneCatalogWithPriceOverride.findCurrentPlan("standard-monthly-12345"));
+ }
+
@Test(groups = "slow")
public void testCreatePlanNoProduct() throws Exception {
final StandaloneCatalog catalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarAdvanced.xml").toExternalForm(), StandaloneCatalog.class);
catalog/src/test/resources/catalogTest.xml 34(+32 -2)
diff --git a/catalog/src/test/resources/catalogTest.xml b/catalog/src/test/resources/catalogTest.xml
index f8d9f93..e4969b1 100644
--- a/catalog/src/test/resources/catalogTest.xml
+++ b/catalog/src/test/resources/catalogTest.xml
@@ -1,8 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
~ 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:
~
@@ -42,6 +44,9 @@
</units>
<products>
+ <product name="Knife">
+ <category>STANDALONE</category>
+ </product>
<product name="Blowdart">
<category>BASE</category>
</product>
@@ -191,6 +196,31 @@
</rules>
<plans>
+ <plan name="knife-monthly-notrial">
+ <product>Knife</product>
+ <finalPhase type="EVERGREEN">
+ <duration>
+ <unit>UNLIMITED</unit>
+ </duration>
+ <recurring>
+ <billingPeriod>MONTHLY</billingPeriod>
+ <recurringPrice>
+ <price>
+ <currency>USD</currency>
+ <value>29.95</value>
+ </price>
+ <price>
+ <currency>EUR</currency>
+ <value>29.95</value>
+ </price>
+ <price>
+ <currency>GBP</currency>
+ <value>29.95</value>
+ </price>
+ </recurringPrice>
+ </recurring>
+ </finalPhase>
+ </plan>
<plan name="blowdart-monthly-notrial">
<product>Blowdart</product>
<finalPhase type="EVERGREEN">
@@ -216,7 +246,6 @@
</recurring>
</finalPhase>
</plan>
-
<plan name="pistol-monthly-notrial">
<product>Pistol</product>
<finalPhase type="EVERGREEN">
@@ -1373,6 +1402,7 @@
</childPriceList>
<childPriceList name="notrial">
<plans>
+ <plan>knife-monthly-notrial</plan>
<plan>blowdart-monthly-notrial</plan>
<plan>pistol-monthly-notrial</plan>
</plans>
diff --git a/catalog/src/test/resources/SpyCarAdvanced.xml b/catalog/src/test/resources/SpyCarAdvanced.xml
index a08c425..bfc21c4 100644
--- a/catalog/src/test/resources/SpyCarAdvanced.xml
+++ b/catalog/src/test/resources/SpyCarAdvanced.xml
@@ -247,6 +247,39 @@
</recurring>
</finalPhase>
</plan>
+ <plan name="standard-monthly-12345">
+ <product>Standard</product>
+ <finalPhase type="EVERGREEN">
+ <duration>
+ <unit>UNLIMITED</unit>
+ </duration>
+ <recurring>
+ <billingPeriod>MONTHLY</billingPeriod>
+ <recurringPrice>
+ <price>
+ <currency>GBP</currency>
+ <value>75.00</value>
+ </price>
+ <price>
+ <currency>EUR</currency>
+ <value>85.00</value>
+ </price>
+ <price>
+ <currency>USD</currency>
+ <value>100.00</value>
+ </price>
+ <price>
+ <currency>JPY</currency>
+ <value>10.00</value>
+ </price>
+ <price>
+ <currency>BTC</currency>
+ <value>0.1</value>
+ </price>
+ </recurringPrice>
+ </recurring>
+ </finalPhase>
+ </plan>
<plan name="sports-monthly">
<product>Sports</product>
<initialPhases>
@@ -967,5 +1000,10 @@
<plan>cia-sports-monthly</plan>
</plans>
</childPriceList>
+ <childPriceList name="ambiguous">
+ <plans>
+ <plan>standard-monthly-12345</plan>
+ </plans>
+ </childPriceList>
</priceLists>
</catalog>
currency/pom.xml 2(+1 -1)
diff --git a/currency/pom.xml b/currency/pom.xml
index 6430f67..e494bd3 100644
--- a/currency/pom.xml
+++ b/currency/pom.xml
@@ -19,7 +19,7 @@
<parent>
<artifactId>killbill</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.19.1-SNAPSHOT</version>
+ <version>0.19.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-currency</artifactId>
entitlement/pom.xml 2(+1 -1)
diff --git a/entitlement/pom.xml b/entitlement/pom.xml
index fd73092..a194d47 100644
--- a/entitlement/pom.xml
+++ b/entitlement/pom.xml
@@ -19,7 +19,7 @@
<parent>
<artifactId>killbill</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.19.1-SNAPSHOT</version>
+ <version>0.19.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-entitlement</artifactId>
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.java b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.java
index 7c84dac..878f1a0 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.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
@@ -111,7 +111,8 @@ public class DefaultEventsStream implements EventsStream {
@Nullable final SubscriptionBaseBundle bundle,
@Nullable final SubscriptionBase baseSubscription,
@Nullable final SubscriptionBase subscription) {
- for (final Object object : new Object[]{account, bundle, baseSubscription, subscription}) {
+ // baseSubscription can be null for STANDALONE products (https://github.com/killbill/killbill/issues/840)
+ for (final Object object : new Object[]{account, bundle, subscription}) {
Preconditions.checkNotNull(object,
"accountId='%s', bundleId='%s', baseSubscriptionId='%s', subscriptionId='%s'",
account != null ? account.getId() : null,
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlement.java b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlement.java
index c5cc731..937f095 100644
--- a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlement.java
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlement.java
@@ -24,13 +24,11 @@ import org.killbill.billing.ErrorCode;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.api.TestApiListener.NextEvent;
-import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.catalog.api.PlanSpecifier;
import org.killbill.billing.catalog.api.PriceListSet;
-import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.EntitlementTestSuiteWithEmbeddedDB;
import org.killbill.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
import org.killbill.billing.entitlement.api.Entitlement.EntitlementSourceType;
@@ -140,6 +138,39 @@ public class TestDefaultEntitlement extends EntitlementTestSuiteWithEmbeddedDB {
assertEquals(entitlement3.getState(), EntitlementState.ACTIVE);
}
+ @Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/840")
+ public void testUncancelEntitlementFor_STANDALONE_Product() throws AccountApiException, EntitlementApiException {
+ final LocalDate initialDate = new LocalDate(2013, 8, 7);
+ clock.setDay(initialDate);
+
+ final Account account = createAccount(getAccountData(7));
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Knife", BillingPeriod.MONTHLY, "notrial", null);
+
+ // Create entitlement and check each field
+ testListener.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK);
+ final Entitlement entitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, account.getExternalKey(), null, null, null, false, false, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+ assertEquals(entitlement.getState(), EntitlementState.ACTIVE);
+
+ clock.addDays(5);
+
+ final LocalDate cancelDate = new LocalDate(clock.getUTCToday().plusDays(1));
+ entitlement.cancelEntitlementWithDate(cancelDate, true, ImmutableList.<PluginProperty>of(), callContext);
+
+ final Entitlement entitlement2 = entitlementApi.getEntitlementForId(entitlement.getId(), callContext);
+ assertEquals(entitlement2.getState(), EntitlementState.ACTIVE);
+ assertEquals(entitlement2.getEffectiveEndDate(), cancelDate);
+
+ testListener.pushExpectedEvents(NextEvent.UNCANCEL);
+ entitlement2.uncancelEntitlement(ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ clock.addDays(1);
+ final Entitlement entitlement3 = entitlementApi.getEntitlementForId(entitlement.getId(), callContext);
+ assertEquals(entitlement3.getState(), EntitlementState.ACTIVE);
+ }
+
@Test(groups = "slow")
public void testCancelWithEntitlementPolicyEOTAndNOCTD() throws AccountApiException, EntitlementApiException, SubscriptionApiException {
final LocalDate initialDate = new LocalDate(2013, 8, 7);
@@ -283,11 +314,10 @@ public class TestDefaultEntitlement extends EntitlementTestSuiteWithEmbeddedDB {
}
@Test(groups = "slow")
- public void testEntitlementChangePlanOnPendingEntitlement() throws AccountApiException, EntitlementApiException {
+ public void testEntitlementChangePlanOnPendingEntitlement() throws AccountApiException, EntitlementApiException {
final LocalDate initialDate = new LocalDate(2013, 8, 7);
clock.setDay(initialDate);
-
final LocalDate startDate = initialDate.plusDays(10);
final Account account = accountApi.createAccount(getAccountData(7), callContext);
@@ -316,7 +346,7 @@ public class TestDefaultEntitlement extends EntitlementTestSuiteWithEmbeddedDB {
entitlement.changePlanWithDate(spec2, ImmutableList.<PlanPhasePriceOverride>of(), startDate, ImmutableList.<PluginProperty>of(), callContext);
- testListener.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK);
+ testListener.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK);
clock.addDays(10);
assertListenerStatus();
invoice/pom.xml 2(+1 -1)
diff --git a/invoice/pom.xml b/invoice/pom.xml
index bba2c29..4dc832f 100644
--- a/invoice/pom.xml
+++ b/invoice/pom.xml
@@ -19,7 +19,7 @@
<parent>
<artifactId>killbill</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.19.1-SNAPSHOT</version>
+ <version>0.19.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-invoice</artifactId>
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceItemGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceItemGenerator.java
index 826a1b5..c5db403 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceItemGenerator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceItemGenerator.java
@@ -48,6 +48,7 @@ public abstract class InvoiceItemGenerator {
private final UUID accountId;
private final String type;
private final Logger delegate;
+ private final boolean enabled;
private StringBuilder logStringBuilder = null;
@@ -56,17 +57,18 @@ public abstract class InvoiceItemGenerator {
this.accountId = accountId;
this.type = type;
this.delegate = delegate;
+ this.enabled = delegate.isDebugEnabled();
}
public void append(final Object event, final Collection<InvoiceItem> items) {
- if (items.isEmpty()) {
+ if (!enabled || items.isEmpty()) {
return;
}
append(event, items.toArray(new InvoiceItem[items.size()]));
}
public void append(final Object event, final InvoiceItem... items) {
- if (items.length == 0) {
+ if (!enabled || items.length == 0) {
return;
}
@@ -80,8 +82,8 @@ public abstract class InvoiceItemGenerator {
}
public void logItems() {
- if (logStringBuilder != null) {
- delegate.info(getLogStringBuilder().toString());
+ if (enabled && logStringBuilder != null) {
+ delegate.debug(getLogStringBuilder().toString());
}
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
index fabf9c7..808e0c5 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -48,10 +48,13 @@ import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
import org.killbill.billing.entitlement.api.SubscriptionEventType;
import org.killbill.billing.events.BusInternalEvent;
import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
import org.killbill.billing.events.InvoiceNotificationInternalEvent;
+import org.killbill.billing.events.RequestedSubscriptionInternalEvent;
+import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications.FutureAccountNotificationsBuilder;
import org.killbill.billing.invoice.api.DefaultInvoiceService;
import org.killbill.billing.invoice.api.DryRunArguments;
import org.killbill.billing.invoice.api.DryRunType;
@@ -107,6 +110,7 @@ import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
+import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
@@ -168,6 +172,56 @@ public class InvoiceDispatcher {
this.parkedAccountsManager = parkedAccountsManager;
}
+
+ public void processSubscriptionStartRequestedDate(final RequestedSubscriptionInternalEvent transition, final InternalCallContext context) {
+
+ final long dryRunNotificationTime = invoiceConfig.getDryRunNotificationSchedule(context).getMillis();
+ final boolean isInvoiceNotificationEnabled = dryRunNotificationTime > 0;
+ if (!isInvoiceNotificationEnabled) {
+ return;
+ }
+
+ final UUID accountId;
+ try {
+ accountId = subscriptionApi.getAccountIdFromSubscriptionId(transition.getSubscriptionId(), context);
+
+
+ } catch (final SubscriptionBaseApiException e) {
+ log.warn("Failed handling SubscriptionBase change.",
+ new InvoiceApiException(ErrorCode.INVOICE_NO_ACCOUNT_ID_FOR_SUBSCRIPTION_ID, transition.getSubscriptionId().toString()));
+ return;
+ }
+
+ try {
+
+ final BillingEventSet billingEvents = billingApi.getBillingEventsForAccountAndUpdateAccountBCD(accountId, null, context);
+ if (billingEvents.isEmpty()) {
+ return;
+ }
+
+ final FutureAccountNotificationsBuilder notificationsBuilder = new FutureAccountNotificationsBuilder();
+ populateNextFutureDryRunNotificationDate(billingEvents, notificationsBuilder, context);
+
+ final ImmutableAccountData account = accountApi.getImmutableAccountDataById(accountId, context);
+
+ commitInvoiceAndSetFutureNotifications(account, null, notificationsBuilder.build(), context);
+
+ } catch (final SubscriptionBaseApiException e) {
+ log.warn("Failed handling SubscriptionBase change.",
+ new InvoiceApiException(ErrorCode.INVOICE_NO_ACCOUNT_ID_FOR_SUBSCRIPTION_ID, transition.getSubscriptionId().toString()));
+ } catch (final AccountApiException e) {
+ log.warn("Failed to retrieve BillingEvents for accountId='{}'", accountId, e);
+ } catch (final CatalogApiException e) {
+ log.warn("Failed to retrieve BillingEvents for accountId='{}'", accountId, e);
+ }
+
+
+
+
+ }
+
+
+
public void processSubscriptionForInvoiceGeneration(final EffectiveSubscriptionInternalEvent transition,
final InternalCallContext context) throws InvoiceApiException {
final UUID subscriptionId = transition.getSubscriptionId();
@@ -579,8 +633,18 @@ public class InvoiceDispatcher {
return generator.generateInvoice(account, billingEvents, existingInvoices, targetInvoiceId, targetDate, account.getCurrency(), context);
}
+
+
private FutureAccountNotifications createNextFutureNotificationDate(final InvoiceWithMetadata invoiceWithMetadata, final BillingEventSet billingEvents, final InternalCallContext context) {
+ final FutureAccountNotificationsBuilder notificationsBuilder = new FutureAccountNotificationsBuilder();
+ populateNextFutureNotificationDate(invoiceWithMetadata, notificationsBuilder);
+ populateNextFutureDryRunNotificationDate(billingEvents, notificationsBuilder, context);
+ return notificationsBuilder.build();
+ }
+
+
+ private void populateNextFutureNotificationDate(final InvoiceWithMetadata invoiceWithMetadata, final FutureAccountNotificationsBuilder notificationsBuilder) {
final Map<LocalDate, Set<UUID>> notificationListForTrigger = new HashMap<LocalDate, Set<UUID>>();
for (final UUID subscriptionId : invoiceWithMetadata.getPerSubscriptionFutureNotificationDates().keySet()) {
@@ -609,6 +673,13 @@ public class InvoiceDispatcher {
}
}
}
+ notificationsBuilder.setNotificationListForTrigger(notificationListForTrigger);
+ }
+
+ private void populateNextFutureDryRunNotificationDate(final BillingEventSet billingEvents, final FutureAccountNotificationsBuilder notificationsBuilder, final InternalCallContext context) {
+
+
+ final Map<LocalDate, Set<UUID>> notificationListForTrigger = notificationsBuilder.getNotificationListForTrigger();
final long dryRunNotificationTime = invoiceConfig.getDryRunNotificationSchedule(context).getMillis();
final boolean isInvoiceNotificationEnabled = dryRunNotificationTime > 0;
@@ -625,12 +696,12 @@ public class InvoiceDispatcher {
subscriptionsForDryRunDates.addAll(notificationListForTrigger.get(curDate));
}
- final Map<UUID, DateTime> upcomingPhasesForSubscriptions = isInvoiceNotificationEnabled ?
+ final Map<UUID, DateTime> upcomingTransitionsForSubscriptions = isInvoiceNotificationEnabled ?
getNextTransitionsForSubscriptions(billingEvents) :
ImmutableMap.<UUID, DateTime>of();
- for (UUID curId : upcomingPhasesForSubscriptions.keySet()) {
- final LocalDate curDryRunDate = context.toLocalDate(upcomingPhasesForSubscriptions.get(curId).minus(dryRunNotificationTime));
+ for (UUID curId : upcomingTransitionsForSubscriptions.keySet()) {
+ final LocalDate curDryRunDate = context.toLocalDate(upcomingTransitionsForSubscriptions.get(curId).minus(dryRunNotificationTime));
Set<UUID> subscriptionsForDryRunDates = notificationListForDryRun.get(curDryRunDate);
if (subscriptionsForDryRunDates == null) {
subscriptionsForDryRunDates = new HashSet<UUID>();
@@ -639,10 +710,11 @@ public class InvoiceDispatcher {
subscriptionsForDryRunDates.add(curId);
}
}
-
- return new FutureAccountNotifications(notificationListForTrigger, notificationListForDryRun);
+ notificationsBuilder.setNotificationListForDryRun(notificationListForDryRun);
}
+
+
private List<InvoiceItemModelDao> transformToInvoiceModelDao(final List<InvoiceItem> invoiceItems) {
return Lists.transform(invoiceItems,
new Function<InvoiceItem, InvoiceItemModelDao>() {
@@ -778,6 +850,37 @@ public class InvoiceDispatcher {
public Map<LocalDate, Set<UUID>> getNotificationsForDryRun() {
return notificationListForDryRun;
}
+
+
+
+ public static class FutureAccountNotificationsBuilder {
+
+ private Map<LocalDate, Set<UUID>> notificationListForTrigger;
+ private Map<LocalDate, Set<UUID>> notificationListForDryRun;
+
+ public FutureAccountNotificationsBuilder() {
+ }
+
+ public void setNotificationListForTrigger(final Map<LocalDate, Set<UUID>> notificationListForTrigger) {
+ this.notificationListForTrigger = notificationListForTrigger;
+ }
+
+ public void setNotificationListForDryRun(final Map<LocalDate, Set<UUID>> notificationListForDryRun) {
+ this.notificationListForDryRun = notificationListForDryRun;
+ }
+
+ public Map<LocalDate, Set<UUID>> getNotificationListForTrigger() {
+ return MoreObjects.firstNonNull(notificationListForTrigger, ImmutableMap.<LocalDate, Set<UUID>>of());
+ }
+
+ public Map<LocalDate, Set<UUID>> getNotificationListForDryRun() {
+ return MoreObjects.firstNonNull(notificationListForDryRun, ImmutableMap.<LocalDate, Set<UUID>>of());
+ }
+
+ public FutureAccountNotifications build() {
+ return new FutureAccountNotifications(getNotificationListForTrigger(), getNotificationListForDryRun());
+ }
+ }
}
private List<LocalDate> getUpcomingInvoiceCandidateDates(final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications,
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
index bf7e879..eee8749 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
@@ -29,6 +29,7 @@ import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.events.BlockingTransitionInternalEvent;
import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
import org.killbill.billing.events.InvoiceCreationInternalEvent;
+import org.killbill.billing.events.RequestedSubscriptionInternalEvent;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceInternalApi;
import org.killbill.billing.invoice.api.InvoiceListenerService;
@@ -155,6 +156,15 @@ public class InvoiceListener extends RetryableService implements InvoiceListener
}
}
});
+ subscriberQueueHandler.subscribe(RequestedSubscriptionInternalEvent.class,
+ new SubscriberAction<RequestedSubscriptionInternalEvent>() {
+ @Override
+ public void run(final RequestedSubscriptionInternalEvent event) {
+ final InternalCallContext context = internalCallContextFactory.createInternalCallContext(event.getSearchKey2(), event.getSearchKey1(), "SubscriptionBaseTransition", CallOrigin.INTERNAL, UserType.SYSTEM, event.getUserToken());
+ dispatcher.processSubscriptionStartRequestedDate(event, context);
+ }
+ });
+
this.retryableSubscriber = new RetryableSubscriber(clock, this, subscriberQueueHandler);
}
@@ -190,6 +200,14 @@ public class InvoiceListener extends RetryableService implements InvoiceListener
retryableSubscriber.handleEvent(event);
}
+ @AllowConcurrentEvents
+ @Subscribe
+ public void handleSubscriptionTransition(final RequestedSubscriptionInternalEvent event) {
+ retryableSubscriber.handleEvent(event);
+ }
+
+
+
public void handleNextBillingDateEvent(final UUID subscriptionId, final DateTime eventDateTime, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
final InternalCallContext context = internalCallContextFactory.createInternalCallContext(tenantRecordId, accountRecordId, "Next Billing Date", CallOrigin.INTERNAL, UserType.SYSTEM, userToken);
try {
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();
jaxrs/pom.xml 2(+1 -1)
diff --git a/jaxrs/pom.xml b/jaxrs/pom.xml
index 7b9ead4..54cb60e 100644
--- a/jaxrs/pom.xml
+++ b/jaxrs/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>killbill</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.19.1-SNAPSHOT</version>
+ <version>0.19.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-jaxrs</artifactId>
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/AdminResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AdminResource.java
index f85946b..6e84d68 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AdminResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AdminResource.java
@@ -1,6 +1,6 @@
/*
- * 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
@@ -17,6 +17,7 @@
package org.killbill.billing.jaxrs.resources;
+import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
@@ -300,8 +301,13 @@ public class AdminResource extends JaxRsResourceBase {
generator.close();
} finally {
// In case the client goes away (IOException), make sure to close the underlying DB connection
- while (iterator.hasNext()) {
- iterator.next();
+ if (tags instanceof Closeable) {
+ ((Closeable) tags).close();
+ } else {
+ // TODO 0.20.x (https://github.com/killbill/killbill/issues/558)
+ while (iterator.hasNext()) {
+ iterator.next();
+ }
}
}
}
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(),
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
index 0fc62db..8a45b83 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * 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
@@ -18,6 +18,7 @@
package org.killbill.billing.jaxrs.resources;
+import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigDecimal;
@@ -349,8 +350,13 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
generator.close();
} finally {
// In case the client goes away (IOException), make sure to close the underlying DB connection
- while (iterator.hasNext()) {
- iterator.next();
+ if (entities instanceof Closeable) {
+ ((Closeable) entities).close();
+ } else {
+ // TODO 0.20.x (https://github.com/killbill/killbill/issues/558)
+ while (iterator.hasNext()) {
+ iterator.next();
+ }
}
}
}
junction/pom.xml 2(+1 -1)
diff --git a/junction/pom.xml b/junction/pom.xml
index b3f3200..0283447 100644
--- a/junction/pom.xml
+++ b/junction/pom.xml
@@ -19,7 +19,7 @@
<parent>
<artifactId>killbill</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.19.1-SNAPSHOT</version>
+ <version>0.19.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-junction</artifactId>
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
overdue/pom.xml 2(+1 -1)
diff --git a/overdue/pom.xml b/overdue/pom.xml
index 290cd8a..4fc17ea 100644
--- a/overdue/pom.xml
+++ b/overdue/pom.xml
@@ -19,7 +19,7 @@
<parent>
<artifactId>killbill</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.19.1-SNAPSHOT</version>
+ <version>0.19.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-overdue</artifactId>
payment/pom.xml 2(+1 -1)
diff --git a/payment/pom.xml b/payment/pom.xml
index 1a79089..ef02c20 100644
--- a/payment/pom.xml
+++ b/payment/pom.xml
@@ -19,7 +19,7 @@
<parent>
<artifactId>killbill</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.19.1-SNAPSHOT</version>
+ <version>0.19.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-payment</artifactId>
pom.xml 4(+2 -2)
diff --git a/pom.xml b/pom.xml
index bccfe28..ff1d489 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,10 +21,10 @@
<parent>
<artifactId>killbill-oss-parent</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.141.20</version>
+ <version>0.141.25</version>
</parent>
<artifactId>killbill</artifactId>
- <version>0.19.1-SNAPSHOT</version>
+ <version>0.19.2-SNAPSHOT</version>
<packaging>pom</packaging>
<name>killbill</name>
<description>Library for managing recurring subscriptions and the associated billing</description>
profiles/killbill/pom.xml 2(+1 -1)
diff --git a/profiles/killbill/pom.xml b/profiles/killbill/pom.xml
index e5b514a..3793501 100644
--- a/profiles/killbill/pom.xml
+++ b/profiles/killbill/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>killbill-profiles</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.19.1-SNAPSHOT</version>
+ <version>0.19.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-profiles-killbill</artifactId>
diff --git a/profiles/killbill/src/main/java/org/apache/shiro/authc/pam/ModularRealmAuthenticatorWith540.java b/profiles/killbill/src/main/java/org/apache/shiro/authc/pam/ModularRealmAuthenticatorWith540.java
index 14c5eda..3595863 100644
--- a/profiles/killbill/src/main/java/org/apache/shiro/authc/pam/ModularRealmAuthenticatorWith540.java
+++ b/profiles/killbill/src/main/java/org/apache/shiro/authc/pam/ModularRealmAuthenticatorWith540.java
@@ -1,6 +1,6 @@
/*
- * Copyright 2015 Groupon, Inc
- * Copyright 2015 The Billing Project, LLC
+ * Copyright 2015-2018 Groupon, Inc
+ * Copyright 2015-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
@@ -18,6 +18,7 @@
package org.apache.shiro.authc.pam;
import java.util.Collection;
+import java.util.LinkedList;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
@@ -26,13 +27,19 @@ import org.killbill.billing.server.security.FirstSuccessfulStrategyWith540;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-// See https://issues.apache.org/jira/browse/SHIRO-540
+/**
+ * Fix for https://issues.apache.org/jira/browse/SHIRO-540
+ * Support for additional realms non injected
+ */
public class ModularRealmAuthenticatorWith540 extends ModularRealmAuthenticator {
private static final Logger log = LoggerFactory.getLogger(ModularRealmAuthenticator.class);
- public ModularRealmAuthenticatorWith540(final ModularRealmAuthenticator delegate) {
- setRealms(delegate.getRealms());
+ public ModularRealmAuthenticatorWith540(final Collection<Realm> realmsFromShiroIni, final ModularRealmAuthenticator delegate) {
+ // Note: order matters (the first successful match will win)
+ final Collection<Realm> realms = new LinkedList<Realm>(realmsFromShiroIni);
+ realms.addAll(delegate.getRealms());
+ setRealms(realms);
setAuthenticationStrategy(delegate.getAuthenticationStrategy());
}
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java
index eaf9e55..d7ce566 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.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
@@ -18,6 +18,9 @@
package org.killbill.billing.server.modules;
+import java.util.Collection;
+import java.util.LinkedList;
+
import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
@@ -25,8 +28,10 @@ import javax.servlet.http.HttpServletRequest;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.authc.pam.ModularRealmAuthenticatorWith540;
+import org.apache.shiro.authz.ModularRealmAuthorizer;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.guice.web.ShiroWebModuleWith435;
+import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
@@ -38,9 +43,9 @@ import org.killbill.billing.server.security.KillBillWebSessionManager;
import org.killbill.billing.server.security.KillbillJdbcTenantRealm;
import org.killbill.billing.util.config.definition.RbacConfig;
import org.killbill.billing.util.glue.EhcacheShiroManagerProvider;
-import org.killbill.billing.util.glue.IniRealmProvider;
import org.killbill.billing.util.glue.JDBCSessionDaoProvider;
import org.killbill.billing.util.glue.KillBillShiroModule;
+import org.killbill.billing.util.glue.RealmsFromShiroIniProvider;
import org.killbill.billing.util.security.shiro.dao.JDBCSessionDao;
import org.killbill.billing.util.security.shiro.realm.KillBillJdbcRealm;
import org.killbill.billing.util.security.shiro.realm.KillBillJndiLdapRealm;
@@ -82,8 +87,6 @@ public class KillBillShiroWebModule extends ShiroWebModuleWith435 {
final RbacConfig config = new ConfigurationObjectFactory(configSource).build(RbacConfig.class);
bind(RbacConfig.class).toInstance(config);
- // Note: order matters (the first successful match will win, see below)
- bindRealm().toProvider(IniRealmProvider.class).asEagerSingleton();
bindRealm().to(KillBillJdbcRealm.class).asEagerSingleton();
if (KillBillShiroModule.isLDAPEnabled()) {
bindRealm().to(KillBillJndiLdapRealm.class).asEagerSingleton();
@@ -133,7 +136,7 @@ public class KillBillShiroWebModule extends ShiroWebModuleWith435 {
}
}
- private static final class DefaultWebSecurityManagerTypeListener implements TypeListener {
+ private final class DefaultWebSecurityManagerTypeListener implements TypeListener {
@Override
public <I> void hear(final TypeLiteral<I> typeLiteral, final TypeEncounter<I> typeEncounter) {
@@ -141,10 +144,21 @@ public class KillBillShiroWebModule extends ShiroWebModuleWith435 {
@Override
public void afterInjection(final Object o) {
final DefaultWebSecurityManager webSecurityManager = (DefaultWebSecurityManager) o;
+
+ // Other realms have been injected by Guice (bindRealm().toInstance(...) makes Guice throw a ClassCastException?!)
+ final Collection<Realm> realmsFromShiroIni = RealmsFromShiroIniProvider.get(configSource);
+
+ if (webSecurityManager.getAuthorizer() instanceof ModularRealmAuthorizer) {
+ final ModularRealmAuthorizer modularRealmAuthorizer = (ModularRealmAuthorizer) webSecurityManager.getAuthorizer();
+ final Collection<Realm> realms = new LinkedList<Realm>(realmsFromShiroIni);
+ realms.addAll(modularRealmAuthorizer.getRealms());
+ modularRealmAuthorizer.setRealms(realms);
+ }
+
if (webSecurityManager.getAuthenticator() instanceof ModularRealmAuthenticator) {
final ModularRealmAuthenticator authenticator = (ModularRealmAuthenticator) webSecurityManager.getAuthenticator();
authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategyWith540());
- webSecurityManager.setAuthenticator(new ModularRealmAuthenticatorWith540(authenticator));
+ webSecurityManager.setAuthenticator(new ModularRealmAuthenticatorWith540(realmsFromShiroIni, authenticator));
}
}
});
profiles/killpay/pom.xml 2(+1 -1)
diff --git a/profiles/killpay/pom.xml b/profiles/killpay/pom.xml
index 25446b9..ea189b8 100644
--- a/profiles/killpay/pom.xml
+++ b/profiles/killpay/pom.xml
@@ -20,7 +20,7 @@
<parent>
<artifactId>killbill-profiles</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.19.1-SNAPSHOT</version>
+ <version>0.19.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-profiles-killpay</artifactId>
profiles/pom.xml 2(+1 -1)
diff --git a/profiles/pom.xml b/profiles/pom.xml
index 910bf30..4f18c54 100644
--- a/profiles/pom.xml
+++ b/profiles/pom.xml
@@ -19,7 +19,7 @@
<parent>
<artifactId>killbill</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.19.1-SNAPSHOT</version>
+ <version>0.19.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-profiles</artifactId>
subscription/pom.xml 2(+1 -1)
diff --git a/subscription/pom.xml b/subscription/pom.xml
index 979df0a..bfff77c 100644
--- a/subscription/pom.xml
+++ b/subscription/pom.xml
@@ -19,7 +19,7 @@
<parent>
<artifactId>killbill</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.19.1-SNAPSHOT</version>
+ <version>0.19.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-subscription</artifactId>
tenant/pom.xml 2(+1 -1)
diff --git a/tenant/pom.xml b/tenant/pom.xml
index 9c42d0b..5812005 100644
--- a/tenant/pom.xml
+++ b/tenant/pom.xml
@@ -19,7 +19,7 @@
<parent>
<artifactId>killbill</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.19.1-SNAPSHOT</version>
+ <version>0.19.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-tenant</artifactId>
usage/pom.xml 2(+1 -1)
diff --git a/usage/pom.xml b/usage/pom.xml
index 866aef7..6124f01 100644
--- a/usage/pom.xml
+++ b/usage/pom.xml
@@ -19,7 +19,7 @@
<parent>
<artifactId>killbill</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.19.1-SNAPSHOT</version>
+ <version>0.19.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-usage</artifactId>
util/pom.xml 2(+1 -1)
diff --git a/util/pom.xml b/util/pom.xml
index f7a100f..486618c 100644
--- a/util/pom.xml
+++ b/util/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>killbill</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.19.1-SNAPSHOT</version>
+ <version>0.19.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-util</artifactId>
diff --git a/util/src/main/java/org/killbill/billing/util/cache/EhCacheBasedCacheController.java b/util/src/main/java/org/killbill/billing/util/cache/EhCacheBasedCacheController.java
index 7f059fc..fcb43c1 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/EhCacheBasedCacheController.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/EhCacheBasedCacheController.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2012 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
@@ -128,7 +128,8 @@ public class EhCacheBasedCacheController<K, V> implements CacheController<K, V>
try {
value = baseCacheLoader.compute(key, cacheLoaderArgument);
} catch (final Exception e) {
- logger.warn("Unable to compute cached value for key='{}' and cacheLoaderArgument='{}'", key, cacheLoaderArgument, e);
+ // Remove noisy log (might be expected, see https://github.com/killbill/killbill/issues/842)
+ //logger.warn("Unable to compute cached value for key='{}' and cacheLoaderArgument='{}'", key, cacheLoaderArgument, e);
throw new RuntimeException(e);
}
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/DefaultPaginationSqlDaoHelper.java b/util/src/main/java/org/killbill/billing/util/entity/dao/DefaultPaginationSqlDaoHelper.java
index 574d3b0..d359a19 100644
--- a/util/src/main/java/org/killbill/billing/util/entity/dao/DefaultPaginationSqlDaoHelper.java
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/DefaultPaginationSqlDaoHelper.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2014 Ning, Inc.
- * 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
@@ -18,6 +18,8 @@
package org.killbill.billing.util.entity.dao;
+import java.io.Closeable;
+import java.io.IOException;
import java.util.Iterator;
import javax.annotation.Nullable;
@@ -26,9 +28,13 @@ import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.util.entity.DefaultPagination;
import org.killbill.billing.util.entity.Entity;
import org.killbill.billing.util.entity.Pagination;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class DefaultPaginationSqlDaoHelper {
+ private static final Logger logger = LoggerFactory.getLogger(DefaultPaginationSqlDaoHelper.class);
+
// Number large enough so that small installations have access to an accurate count
// but small enough to not impact very large deployments
// TODO Should this be configurable per tenant?
@@ -66,14 +72,35 @@ public class DefaultPaginationSqlDaoHelper {
// We usually always want to wrap our queries in an EntitySqlDaoTransactionWrapper... except here.
// Since we want to stream the results out, we don't want to auto-commit when this method returns.
final EntitySqlDao<M, E> sqlDao = transactionalSqlDao.onDemandForStreamingResults(sqlDaoClazz);
- // The count to get maxNbRecords can be expensive on very large datasets. As a heuristic to check how large that number is,
- // we retrieve 1 record at offset SIMPLE_PAGINATION_THRESHOLD (pretty fast). If we've found a record, that means the count is larger
- // than this threshold and we don't issue the full count query
final Long maxNbRecords;
- if (context == null || paginationIteratorBuilder.build((S) sqlDao, SIMPLE_PAGINATION_THRESHOLD, 1L, ordering, context).hasNext()) {
+ if (context == null) {
maxNbRecords = null;
} else {
- maxNbRecords = sqlDao.getCount(context);
+ // The count to get maxNbRecords can be expensive on very large datasets. As a heuristic to check how large that number is,
+ // we retrieve 1 record at offset SIMPLE_PAGINATION_THRESHOLD (pretty fast). If we've found a record, that means the count is larger
+ // than this threshold and we don't issue the full count query
+ final Iterator<M> simplePaginationIterator = paginationIteratorBuilder.build((S) sqlDao, SIMPLE_PAGINATION_THRESHOLD, 1L, ordering, context);
+ final boolean veryLargeDataSet = simplePaginationIterator.hasNext();
+
+ // Make sure to free resources (https://github.com/killbill/killbill/issues/853)
+ if (simplePaginationIterator instanceof Closeable) {
+ // Always the case with the current implementation (simplePaginationIterator is a org.skife.jdbi.v2.ResultIterator)
+ try {
+ ((Closeable) simplePaginationIterator).close();
+ } catch (final IOException e) {
+ logger.warn("Unable to close iterator", e);
+ }
+ } else {
+ while (simplePaginationIterator.hasNext()) {
+ simplePaginationIterator.next();
+ }
+ }
+
+ if (veryLargeDataSet) {
+ maxNbRecords = null;
+ } else {
+ maxNbRecords = sqlDao.getCount(context);
+ }
}
final Iterator<M> results = paginationIteratorBuilder.build((S) sqlDao, offset, limit, ordering, context);
diff --git a/util/src/main/java/org/killbill/billing/util/entity/DefaultPagination.java b/util/src/main/java/org/killbill/billing/util/entity/DefaultPagination.java
index bdd5e16..7ea60aa 100644
--- a/util/src/main/java/org/killbill/billing/util/entity/DefaultPagination.java
+++ b/util/src/main/java/org/killbill/billing/util/entity/DefaultPagination.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:
*
@@ -16,6 +18,8 @@
package org.killbill.billing.util.entity;
+import java.io.Closeable;
+import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
@@ -25,7 +29,7 @@ import javax.annotation.Nullable;
import com.google.common.collect.ImmutableList;
// Assumes the original offset starts at zero.
-public class DefaultPagination<T> implements Pagination<T> {
+public class DefaultPagination<T> implements Pagination<T>, Closeable {
private final Long currentOffset;
private final Long limit;
@@ -75,6 +79,18 @@ public class DefaultPagination<T> implements Pagination<T> {
}
@Override
+ public void close() throws IOException {
+ if (delegateIterator instanceof Closeable) {
+ // Always the case with the current implementation (delegateIterator is a org.skife.jdbi.v2.ResultIterator)
+ ((Closeable) delegateIterator).close();
+ } else {
+ while (delegateIterator.hasNext()) {
+ delegateIterator.next();
+ }
+ }
+ }
+
+ @Override
public Iterator<T> iterator() {
return delegateIterator;
}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroModule.java b/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroModule.java
index c103789..154bc1d 100644
--- a/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroModule.java
+++ b/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroModule.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 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
@@ -21,10 +21,13 @@ package org.killbill.billing.util.glue;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.guice.ShiroModule;
import org.apache.shiro.mgt.SecurityManager;
+import org.apache.shiro.realm.Realm;
+import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.session.mgt.DefaultSessionManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.killbill.billing.platform.api.KillbillConfigSource;
import org.killbill.billing.util.config.definition.RbacConfig;
+import org.killbill.billing.util.config.definition.SecurityConfig;
import org.killbill.billing.util.security.shiro.dao.JDBCSessionDao;
import org.killbill.billing.util.security.shiro.realm.KillBillJdbcRealm;
import org.killbill.billing.util.security.shiro.realm.KillBillJndiLdapRealm;
@@ -32,6 +35,7 @@ import org.killbill.billing.util.security.shiro.realm.KillBillOktaRealm;
import org.skife.config.ConfigSource;
import org.skife.config.ConfigurationObjectFactory;
+import com.google.inject.Provider;
import com.google.inject.binder.AnnotatedBindingBuilder;
// For Kill Bill library only.
@@ -42,7 +46,6 @@ public class KillBillShiroModule extends ShiroModule {
public static final String KILLBILL_OKTA_PROPERTY = "killbill.server.okta";
public static final String KILLBILL_RBAC_PROPERTY = "killbill.server.rbac";
-
public static boolean isLDAPEnabled() {
return Boolean.parseBoolean(System.getProperty(KILLBILL_LDAP_PROPERTY, "false"));
}
@@ -70,11 +73,25 @@ public class KillBillShiroModule extends ShiroModule {
}).build(RbacConfig.class);
bind(RbacConfig.class).toInstance(config);
- bindRealm().toProvider(IniRealmProvider.class).asEagerSingleton();
+ final ConfigSource skifeConfigSource = new ConfigSource() {
+ @Override
+ public String getString(final String propertyName) {
+ return configSource.getString(propertyName);
+ }
+ };
+
+ bind(RbacConfig.class).toInstance(config);
+
+ final Provider<IniRealm> iniRealmProvider = RealmsFromShiroIniProvider.getIniRealmProvider(skifeConfigSource);
+ // Hack for Kill Bill library to work around weird Guice ClassCastException when using
+ // bindRealm().toInstance(...) -- this means we don't support custom realms when embedding Kill Bill
+ bindRealm().toProvider(iniRealmProvider).asEagerSingleton();
configureJDBCRealm();
configureLDAPRealm();
+
+ configureOktaRealm();
}
protected void configureJDBCRealm() {
diff --git a/util/src/main/java/org/killbill/billing/util/tag/DefaultTagDefinition.java b/util/src/main/java/org/killbill/billing/util/tag/DefaultTagDefinition.java
index 77db66f..3d0877e 100644
--- a/util/src/main/java/org/killbill/billing/util/tag/DefaultTagDefinition.java
+++ b/util/src/main/java/org/killbill/billing/util/tag/DefaultTagDefinition.java
@@ -156,7 +156,11 @@ public class DefaultTagDefinition extends EntityBase implements TagDefinition {
throw new IllegalStateException(String.format("ControlTag id %s does not seem to exist", id));
}
- private static List<ObjectType> toObjectTypes(final String input) {
+ private static List<ObjectType> toObjectTypes(@Nullable final String input) {
+ if (input == null) {
+ return ImmutableList.copyOf(ObjectType.values());
+ }
+
return ImmutableList.copyOf(Iterables.transform(SPLITTER.splitToList(input), new Function<String, ObjectType>() {
@Override
public ObjectType apply(final String input) {