killbill-memoizeit
Changes
account/pom.xml 2(+1 -1)
account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java 13(+6 -7)
api/pom.xml 2(+1 -1)
beatrix/pom.xml 2(+1 -1)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java 148(+148 -0)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java 48(+48 -0)
catalog/pom.xml 2(+1 -1)
currency/pom.xml 2(+1 -1)
entitlement/pom.xml 2(+1 -1)
invoice/pom.xml 2(+1 -1)
invoice/src/main/resources/org/killbill/billing/invoice/migration/V20160908172551__multiple_refunds_254.sql 1(+1 -0)
jaxrs/pom.xml 2(+1 -1)
junction/pom.xml 2(+1 -1)
NEWS 9(+9 -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/killbill/billing/server/config/MultiTenantNotificationConfig.java 24(+11 -13)
profiles/killpay/pom.xml 2(+1 -1)
profiles/pom.xml 2(+1 -1)
subscription/pom.xml 2(+1 -1)
subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java 77(+72 -5)
subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java 10(+10 -0)
subscription/src/main/java/org/killbill/billing/subscription/engine/addon/AddonUtils.java 16(+16 -0)
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 5b17600..17a4076 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.17.3-SNAPSHOT</version>
+ <version>0.17.5-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-account</artifactId>
diff --git a/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java b/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java
index 829e000..c9cc74a 100644
--- a/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java
+++ b/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java
@@ -157,17 +157,16 @@ public class DefaultAccountInternalApi extends DefaultAccountApiBase implements
return accountModelDao;
}
- private int getBCDInternal(final UUID accountId, final InternalTenantContext context) {
- final Integer bcd = accountDao.getAccountBCD(accountId, context);
- return bcd != null ? bcd : DefaultMutableAccountData.DEFAULT_BILLING_CYCLE_DAY_LOCAL;
- }
-
private CacheLoaderArgument createBCDCacheLoaderArgument(final InternalTenantContext context) {
final AccountBCDCacheLoader.LoaderCallback loaderCallback = new AccountBCDCacheLoader.LoaderCallback() {
@Override
public Object loadAccountBCD(final UUID accountId, final InternalTenantContext context) {
- int bcd = getBCDInternal(accountId, context);
- return new Integer(bcd);
+ Object result = accountDao.getAccountBCD(accountId, context);
+ if (result != null) {
+ // If the value is 0, then account BCD was not set so we don't want to create a cache entry
+ result = result.equals(DefaultMutableAccountData.DEFAULT_BILLING_CYCLE_DAY_LOCAL) ? null : result;
+ }
+ return result;
}
};
final Object[] args = new Object[1];
api/pom.xml 2(+1 -1)
diff --git a/api/pom.xml b/api/pom.xml
index 9d3539e..63f28a3 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.17.3-SNAPSHOT</version>
+ <version>0.17.5-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-internal-api</artifactId>
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
index 234f9f3..fada42e 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
@@ -89,7 +89,7 @@ public interface SubscriptionBaseInternalApi {
public List<EffectiveSubscriptionInternalEvent> getBillingTransitions(SubscriptionBase subscription, InternalTenantContext context);
- public DateTime getDryRunChangePlanEffectiveDate(SubscriptionBase subscription, PlanSpecifier spec, DateTime requestedDate, BillingActionPolicy policy, InternalTenantContext context) throws SubscriptionBaseApiException;
+ public DateTime getDryRunChangePlanEffectiveDate(SubscriptionBase subscription, PlanSpecifier spec, DateTime requestedDate, BillingActionPolicy policy, List<PlanPhasePriceOverride> overrides, InternalCallContext context) throws SubscriptionBaseApiException, CatalogApiException;
public List<EntitlementAOStatusDryRun> getDryRunChangePlanStatus(UUID subscriptionId, @Nullable String baseProductName,
DateTime requestedDate, InternalTenantContext context) throws SubscriptionBaseApiException;
beatrix/pom.xml 2(+1 -1)
diff --git a/beatrix/pom.xml b/beatrix/pom.xml
index da2ef43..3b272b8 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.17.3-SNAPSHOT</version>
+ <version>0.17.5-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-beatrix</artifactId>
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
index beb7246..7b6b8da 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
@@ -46,13 +46,16 @@ import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.api.InvoicePayment;
import org.killbill.billing.invoice.model.ExternalChargeInvoiceItem;
+import org.killbill.billing.overdue.config.DefaultOverdueConfig;
import org.killbill.billing.overdue.wrapper.OverdueWrapper;
import org.killbill.billing.payment.api.Payment;
import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.xmlloader.XMLLoader;
import org.testng.Assert;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableList;
+import com.google.common.io.Resources;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
@@ -949,6 +952,151 @@ public class TestOverdueIntegration extends TestOverdueBase {
checkODState(OverdueWrapper.CLEAR_STATE_NAME);
}
+ @Test(groups = "slow", description = "Test overdue state with number of unpaid invoices condition")
+ public void testOverdueStateWithNumberOfUnpaidInvoicesCondition() throws Exception {
+ // 2012-05-01T00:03:42.000Z
+ clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
+
+ final DefaultOverdueConfig config = XMLLoader.getObjectFromString(Resources.getResource("overdueWithNumberOfUnpaidInvoicesCondition.xml").toExternalForm(), DefaultOverdueConfig.class);
+ overdueConfigCache.loadDefaultOverdueConfig(config);
+
+ setupAccount();
+
+ paymentPlugin.makeAllInvoicesFailWithError(true);
+
+ createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", productName, ProductCategory.BASE,
+ term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // 2012-05-31 => DAY 30 have to get out of trial before first payment
+ addMonthsAndCheckForCompletion(1, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ // Verify that number of unpaid invoices is 1
+ Assert.assertEquals(invoiceUserApi.getUnpaidInvoicesByAccountId(account.getId(), clock.getUTCToday(), callContext).size(), 1);
+ // Should still be in clear state
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
+
+ // Add 1 month
+ addMonthsAndCheckForCompletion(1, NextEvent.INVOICE, NextEvent.BLOCK, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ // Verify that number of unpaid invoices is 2
+ Assert.assertEquals(invoiceUserApi.getUnpaidInvoicesByAccountId(account.getId(), clock.getUTCToday(), callContext).size(), 2);
+ // Now we should be in OD1
+ checkODState("OD1");
+
+ // Add 1 month
+ addMonthsAndCheckForCompletion(1, NextEvent.INVOICE, NextEvent.BLOCK, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+ NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+ NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ // Verify that number of unpaid invoices is 3
+ Assert.assertEquals(invoiceUserApi.getUnpaidInvoicesByAccountId(account.getId(), clock.getUTCToday(), callContext).size(), 3);
+ // Now we should be in OD2
+ checkODState("OD2");
+
+ // Add 1 month
+ addMonthsAndCheckForCompletion(1, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+ NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+ NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+ NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ // Verify that number of unpaid invoices is 4
+ Assert.assertEquals(invoiceUserApi.getUnpaidInvoicesByAccountId(account.getId(), clock.getUTCToday(), callContext).size(), 4);
+ // We should still be in OD2
+ checkODState("OD2");
+
+ // Add 1 month
+ addMonthsAndCheckForCompletion(1, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+ NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+ NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+ NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+ NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR, NextEvent.TAG, NextEvent.BLOCK);
+ // Verify that number of unpaid invoices is 5
+ Assert.assertEquals(invoiceUserApi.getUnpaidInvoicesByAccountId(account.getId(), clock.getUTCToday(), callContext).size(), 5);
+ // Now we should be in OD3
+ checkODState("OD3");
+
+ // Get all unpaid invoices and pay them to clear the overdue state
+ paymentPlugin.makeAllInvoicesFailWithError(false);
+ List<Invoice> unpaidInvoices = getUnpaidInvoicesOrderFromRecent();
+ createPaymentAndCheckForCompletion(account, unpaidInvoices.get(0), NextEvent.BLOCK, NextEvent.TAG, NextEvent.NULL_INVOICE, NextEvent.NULL_INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ createPaymentAndCheckForCompletion(account, unpaidInvoices.get(1), NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ createPaymentAndCheckForCompletion(account, unpaidInvoices.get(2), NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.BLOCK);
+ createPaymentAndCheckForCompletion(account, unpaidInvoices.get(3), NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.BLOCK);
+ createPaymentAndCheckForCompletion(account, unpaidInvoices.get(4), NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+ // We should be clear now
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
+ }
+
+ @Test(groups = "slow", description = "Test overdue state with total unpaid invoice balance condition")
+ public void testOverdueStateWithTotalUnpaidInvoiceBalanceCondition() throws Exception {
+ // 2012-05-01T00:03:42.000Z
+ clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
+
+ final DefaultOverdueConfig config = XMLLoader.getObjectFromString(Resources.getResource("overdueWithTotalUnpaidInvoiceBalanceCondition.xml").toExternalForm(), DefaultOverdueConfig.class);
+ overdueConfigCache.loadDefaultOverdueConfig(config);
+
+ setupAccount();
+
+ paymentPlugin.makeAllInvoicesFailWithError(true);
+
+ createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", productName, ProductCategory.BASE,
+ term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // 2012-05-31 => DAY 30 have to get out of trial before first payment
+ addMonthsAndCheckForCompletion(1, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+
+ // Amount balance should be USD 249.95
+ assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(BigDecimal.valueOf(249.95)), 0);
+ // Should still be in clear state
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
+
+ // Add 1 month
+ addMonthsAndCheckForCompletion(1, NextEvent.INVOICE, NextEvent.BLOCK, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ // Amount balance should be USD 499.90
+ assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(BigDecimal.valueOf(499.90)), 0);
+ // Now we should be in OD1
+ checkODState("OD1");
+
+ // Add 1 month
+ addMonthsAndCheckForCompletion(1, NextEvent.INVOICE, NextEvent.BLOCK, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+ NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+ NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ // Amount balance should be USD 749.85
+ assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(BigDecimal.valueOf(749.85)), 0);
+ // Now we should be in OD2
+ checkODState("OD2");
+
+ // Add 1 month
+ addMonthsAndCheckForCompletion(1, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+ NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+ NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+ NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ // Amount balance should be USD 999.80
+ assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(BigDecimal.valueOf(999.80)), 0);
+ // We should still be in OD2
+ checkODState("OD2");
+
+ // Add 1 month
+ addMonthsAndCheckForCompletion(1, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+ NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+ NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+ NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+ NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR, NextEvent.TAG, NextEvent.BLOCK);
+ // Amount balance should be USD 1249.75
+ assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(BigDecimal.valueOf(1249.75)), 0);
+ // Now we should be in OD3
+ checkODState("OD3");
+
+ // Get all unpaid invoices and pay them to clear the overdue state
+ paymentPlugin.makeAllInvoicesFailWithError(false);
+ List<Invoice> unpaidInvoices = getUnpaidInvoicesOrderFromRecent();
+ createPaymentAndCheckForCompletion(account, unpaidInvoices.get(0), NextEvent.BLOCK, NextEvent.TAG, NextEvent.NULL_INVOICE, NextEvent.NULL_INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ createPaymentAndCheckForCompletion(account, unpaidInvoices.get(1), NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ createPaymentAndCheckForCompletion(account, unpaidInvoices.get(2), NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.BLOCK);
+ createPaymentAndCheckForCompletion(account, unpaidInvoices.get(3), NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.BLOCK);
+ createPaymentAndCheckForCompletion(account, unpaidInvoices.get(4), NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+ // We should be clear now
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME);
+ }
+
private void allowPaymentsAndResetOverdueToClearByPayingAllUnpaidInvoices(final boolean extraPayment) {
// Reset plugin so payments should now succeed
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java
index 7e1b7b3..4635782 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java
@@ -49,6 +49,7 @@ import org.killbill.billing.catalog.api.user.DefaultSimplePlanDescriptor;
import org.killbill.billing.entitlement.api.Entitlement;
import org.killbill.billing.entitlement.api.EntitlementApiException;
import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.payment.api.PaymentMethodPlugin;
import org.killbill.billing.payment.api.PluginProperty;
@@ -223,7 +224,54 @@ public class TestIntegrationWithCatalogUpdate extends TestIntegrationBase {
invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, testCallContext);
assertEquals(invoices.size(), 3);
assertEquals(invoices.get(2).getChargedAmount().compareTo(new BigDecimal("9.00")), 0); // 10 (recurring) - 1 (repair)
+ }
+
+
+
+ // Use custom plan definition to create a THIRTY_DAYS plan with no trial and test issue #598
+ @Test(groups = "slow")
+ public void testWithThirtyDaysPlan() throws Exception {
+
+ // Create a per-tenant catalog with one plan
+ final SimplePlanDescriptor desc1 = new DefaultSimplePlanDescriptor("thirty-monthly", "Thirty", ProductCategory.BASE, account.getCurrency(), BigDecimal.TEN, BillingPeriod.THIRTY_DAYS, 0, TimeUnit.UNLIMITED, ImmutableList.<String>of());
+ catalogUserApi.addSimplePlan(desc1, init, testCallContext);
+ StaticCatalog catalog = catalogUserApi.getCurrentCatalog("dummy", testCallContext);
+ assertEquals(catalog.getCurrentPlans().length, 1);
+
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("thirty-monthly", null);
+ createEntitlement(spec, null, true);
+
+ List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, testCallContext);
+ assertEquals(invoices.size(), 1);
+ assertEquals(invoices.get(0).getChargedAmount().compareTo(BigDecimal.TEN), 0);
+ assertEquals(invoices.get(0).getInvoiceItems().size(), 1);
+
+ final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 6, 1), new LocalDate(2016, 7, 1), InvoiceItemType.RECURRING, BigDecimal.TEN));
+ invoiceChecker.checkInvoiceNoAudits(invoices.get(0), callContext, expectedInvoices);
+
+ int invoiceSize = 2;
+ LocalDate startDate = new LocalDate(2016, 7, 1);
+ for (int i = 0; i < 14; i++) {
+
+ expectedInvoices.clear();
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ LocalDate endDate = startDate.plusDays(30);
+ invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, testCallContext);
+ assertEquals(invoices.size(), invoiceSize);
+
+ expectedInvoices.add(new ExpectedInvoiceItemCheck(startDate, endDate, InvoiceItemType.RECURRING, BigDecimal.TEN));
+ invoiceChecker.checkInvoiceNoAudits(invoices.get(invoices.size() - 1), callContext, expectedInvoices);
+
+ startDate = endDate;
+ invoiceSize++;
+ }
}
private Entitlement createEntitlement(final String planName, final boolean expectPayment) throws EntitlementApiException {
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java
index 695e480..17d84b4 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java
@@ -248,6 +248,110 @@ public class TestSubscription extends TestIntegrationBase {
}
@Test(groups = "slow")
+ public void testCreateSubscriptionWithAddOnsWithLimitException() throws Exception {
+ final LocalDate initialDate = new LocalDate(2015, 10, 1);
+ clock.setDay(initialDate);
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+ final PlanPhaseSpecifier baseSpec = new PlanPhaseSpecifier("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+ final PlanPhaseSpecifier addOnSpec1 = new PlanPhaseSpecifier("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+ final PlanPhaseSpecifier addOnSpec2 = new PlanPhaseSpecifier("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+ final PlanPhaseSpecifier addOnSpec3 = new PlanPhaseSpecifier("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+
+ final String externalKey = "baseExternalKey";
+ EntitlementSpecifier baseEntitlementSpecifier = new DefaultEntitlementSpecifier(baseSpec, null);
+ EntitlementSpecifier addOnEntitlementSpecifier1 = new DefaultEntitlementSpecifier(addOnSpec1, null);
+ EntitlementSpecifier addOnEntitlementSpecifier2 = new DefaultEntitlementSpecifier(addOnSpec2, null);
+ EntitlementSpecifier addOnEntitlementSpecifier3 = new DefaultEntitlementSpecifier(addOnSpec3, null);
+
+ final List<EntitlementSpecifier> specifierList = new ArrayList<EntitlementSpecifier>();
+ specifierList.add(baseEntitlementSpecifier);
+ specifierList.add(addOnEntitlementSpecifier1);
+ specifierList.add(addOnEntitlementSpecifier2);
+ specifierList.add(addOnEntitlementSpecifier3);
+
+ // Trying to add the third add_on with the same plan should throw an exception (the limit is 2 for this plan)
+ try {
+ entitlementApi.createBaseEntitlementWithAddOns(account.getId(), externalKey, specifierList, initialDate, initialDate, false, ImmutableList.<PluginProperty>of(), callContext);
+ } catch (final EntitlementApiException e) {
+ assertEquals(e.getCode(), ErrorCode.SUB_CREATE_AO_MAX_PLAN_ALLOWED_BY_BUNDLE.getCode());
+ }
+ }
+
+ @Test(groups = "slow")
+ public void testCreateBaseSubscriptionAndAddOnsWithLimitException() throws Exception {
+ final LocalDate initialDate = new LocalDate(2015, 10, 1);
+ clock.setDay(initialDate);
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+ final PlanPhaseSpecifier addOnSpec1 = new PlanPhaseSpecifier("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+ final PlanPhaseSpecifier addOnSpec2 = new PlanPhaseSpecifier("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+ final PlanPhaseSpecifier addOnSpec3 = new PlanPhaseSpecifier("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+
+ // Create base subscription
+ final Entitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), account.getExternalKey(), "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ assertNotNull(baseEntitlement);
+
+ // Create first add_on subscription
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ entitlementApi.addEntitlement(baseEntitlement.getBundleId(), addOnSpec1, null, initialDate, initialDate, false, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ // Create second add_on subscription with the same plan
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ entitlementApi.addEntitlement(baseEntitlement.getBundleId(), addOnSpec2, null, initialDate, initialDate, false, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ // Trying to add the third add_on with the same plan should throw an exception (the limit is 2 for this plan)
+ try {
+ entitlementApi.addEntitlement(baseEntitlement.getBundleId(), addOnSpec3, null, initialDate, initialDate, false, ImmutableList.<PluginProperty>of(), callContext);
+ } catch (final EntitlementApiException e) {
+ assertEquals(e.getCode(), ErrorCode.SUB_CREATE_AO_MAX_PLAN_ALLOWED_BY_BUNDLE.getCode());
+ }
+ }
+
+ @Test(groups = "slow")
+ public void testChangePlanWithLimitException() throws Exception {
+ final LocalDate initialDate = new LocalDate(2015, 10, 1);
+ clock.setDay(initialDate);
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+ final PlanPhaseSpecifier addOnSpec1 = new PlanPhaseSpecifier("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+ final PlanPhaseSpecifier addOnSpec2 = new PlanPhaseSpecifier("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+ final PlanPhaseSpecifier addOnSpec3 = new PlanPhaseSpecifier("Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+
+ // Create base subscription
+ final Entitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), account.getExternalKey(), "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ assertNotNull(baseEntitlement);
+
+ // Create first add_on subscription
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ entitlementApi.addEntitlement(baseEntitlement.getBundleId(), addOnSpec1, null, initialDate, initialDate, false, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ // Create second add_on subscription with the same plan
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ entitlementApi.addEntitlement(baseEntitlement.getBundleId(), addOnSpec2, null, initialDate, initialDate, false, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ // Create third add_on subscription with another plan
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ Entitlement addOn3 = entitlementApi.addEntitlement(baseEntitlement.getBundleId(), addOnSpec3, null, initialDate, initialDate, false, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ // Trying to change the plan of the third add_on to 'Laser-Scope' plan, should throw an exception (the limit is 2 for this plan)
+ try {
+ final PlanPhaseSpecifier addOnSpecChangedPlan = new PlanPhaseSpecifier("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+ addOn3.changePlan(addOnSpecChangedPlan, null, ImmutableList.<PluginProperty>of(), callContext);
+ } catch (final EntitlementApiException e) {
+ assertEquals(e.getCode(), ErrorCode.SUB_CHANGE_AO_MAX_PLAN_ALLOWED_BY_BUNDLE.getCode());
+ }
+ }
+
+ @Test(groups = "slow")
public void testCancelFutureSubscriptionWithPolicy() throws Exception {
final LocalDate initialDate = new LocalDate(2015, 9, 1);
clock.setDay(initialDate);
diff --git a/beatrix/src/test/resources/overdueWithNumberOfUnpaidInvoicesCondition.xml b/beatrix/src/test/resources/overdueWithNumberOfUnpaidInvoicesCondition.xml
new file mode 100644
index 0000000..798eea1
--- /dev/null
+++ b/beatrix/src/test/resources/overdueWithNumberOfUnpaidInvoicesCondition.xml
@@ -0,0 +1,53 @@
+<!--
+ ~ Copyright 2016 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
+ ~ License. You may obtain a copy of the License at:
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ ~ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ ~ License for the specific language governing permissions and limitations
+ ~ under the License.
+ -->
+
+<overdueConfig>
+ <accountOverdueStates>
+ <state name="OD3">
+ <condition>
+ <numberOfUnpaidInvoicesEqualsOrExceeds>5</numberOfUnpaidInvoicesEqualsOrExceeds>
+ </condition>
+ <externalMessage>Reached OD3</externalMessage>
+ <blockChanges>true</blockChanges>
+ <disableEntitlementAndChangesBlocked>true</disableEntitlementAndChangesBlocked>
+ <autoReevaluationInterval>
+ <unit>DAYS</unit><number>5</number>
+ </autoReevaluationInterval>
+ </state>
+ <state name="OD2">
+ <condition>
+ <numberOfUnpaidInvoicesEqualsOrExceeds>3</numberOfUnpaidInvoicesEqualsOrExceeds>
+ </condition>
+ <externalMessage>Reached OD2</externalMessage>
+ <blockChanges>true</blockChanges>
+ <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>
+ <autoReevaluationInterval>
+ <unit>DAYS</unit><number>5</number>
+ </autoReevaluationInterval>
+ </state>
+ <state name="OD1">
+ <condition>
+ <numberOfUnpaidInvoicesEqualsOrExceeds>2</numberOfUnpaidInvoicesEqualsOrExceeds>
+ </condition>
+ <externalMessage>Reached OD1</externalMessage>
+ <blockChanges>true</blockChanges>
+ <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>
+ <autoReevaluationInterval>
+ <unit>DAYS</unit><number>5</number>
+ </autoReevaluationInterval>
+ </state>
+ </accountOverdueStates>
+</overdueConfig>
diff --git a/beatrix/src/test/resources/overdueWithTotalUnpaidInvoiceBalanceCondition.xml b/beatrix/src/test/resources/overdueWithTotalUnpaidInvoiceBalanceCondition.xml
new file mode 100644
index 0000000..b50d999
--- /dev/null
+++ b/beatrix/src/test/resources/overdueWithTotalUnpaidInvoiceBalanceCondition.xml
@@ -0,0 +1,53 @@
+<!--
+ ~ Copyright 2016 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
+ ~ License. You may obtain a copy of the License at:
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ ~ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ ~ License for the specific language governing permissions and limitations
+ ~ under the License.
+ -->
+
+<overdueConfig>
+ <accountOverdueStates>
+ <state name="OD3">
+ <condition>
+ <totalUnpaidInvoiceBalanceEqualsOrExceeds>1200.0</totalUnpaidInvoiceBalanceEqualsOrExceeds>
+ </condition>
+ <externalMessage>Reached OD3</externalMessage>
+ <blockChanges>true</blockChanges>
+ <disableEntitlementAndChangesBlocked>true</disableEntitlementAndChangesBlocked>
+ <autoReevaluationInterval>
+ <unit>DAYS</unit><number>5</number>
+ </autoReevaluationInterval>
+ </state>
+ <state name="OD2">
+ <condition>
+ <totalUnpaidInvoiceBalanceEqualsOrExceeds>600.0</totalUnpaidInvoiceBalanceEqualsOrExceeds>
+ </condition>
+ <externalMessage>Reached OD2</externalMessage>
+ <blockChanges>true</blockChanges>
+ <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>
+ <autoReevaluationInterval>
+ <unit>DAYS</unit><number>5</number>
+ </autoReevaluationInterval>
+ </state>
+ <state name="OD1">
+ <condition>
+ <totalUnpaidInvoiceBalanceEqualsOrExceeds>260.0</totalUnpaidInvoiceBalanceEqualsOrExceeds>
+ </condition>
+ <externalMessage>Reached OD1</externalMessage>
+ <blockChanges>true</blockChanges>
+ <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>
+ <autoReevaluationInterval>
+ <unit>DAYS</unit><number>5</number>
+ </autoReevaluationInterval>
+ </state>
+ </accountOverdueStates>
+</overdueConfig>
catalog/pom.xml 2(+1 -1)
diff --git a/catalog/pom.xml b/catalog/pom.xml
index dd6f964..1f72d70 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.17.3-SNAPSHOT</version>
+ <version>0.17.5-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-catalog</artifactId>
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheOverriddenPlanCache.java b/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheOverriddenPlanCache.java
index 5e94a51..3b54252 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheOverriddenPlanCache.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheOverriddenPlanCache.java
@@ -75,6 +75,11 @@ public class EhCacheOverriddenPlanCache implements OverriddenPlanCache {
return (DefaultPlan) cacheController.get(planName, argument);
}
+ @Override
+ public void addDryRunPlan(final String planName, final Plan plan) {
+ cacheController.putIfAbsent(planName, plan);
+ }
+
private DefaultPlan loadOverriddenPlan(final String planName, final StaticCatalog catalog, final InternalTenantContext context) throws CatalogApiException {
final Matcher m = DefaultPriceOverride.CUSTOM_PLAN_NAME_PATTERN.matcher(planName);
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/caching/OverriddenPlanCache.java b/catalog/src/main/java/org/killbill/billing/catalog/caching/OverriddenPlanCache.java
index f0f8178..f888dc8 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/caching/OverriddenPlanCache.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/caching/OverriddenPlanCache.java
@@ -20,9 +20,12 @@ package org.killbill.billing.catalog.caching;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.DefaultPlan;
import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.StaticCatalog;
public interface OverriddenPlanCache {
DefaultPlan getOverriddenPlan(final String planName, final StaticCatalog catalog, final InternalTenantContext context) throws CatalogApiException;
+
+ void addDryRunPlan(final String planName, final Plan plan);
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/CatalogUpdater.java b/catalog/src/main/java/org/killbill/billing/catalog/CatalogUpdater.java
index 684e21e..dd0dcfd 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/CatalogUpdater.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/CatalogUpdater.java
@@ -175,7 +175,17 @@ public class CatalogUpdater {
if (desc.getProductCategory() == ProductCategory.ADD_ON) {
for (final String bp : desc.getAvailableBaseProducts()) {
- catalog.addProductAvailableAO(getExistingProduct(bp), product);
+ final DefaultProduct targetBasePlan = getExistingProduct(bp);
+ boolean found = false;
+ for (DefaultProduct cur : targetBasePlan.getAvailable()) {
+ if (cur.getName().equals(product.getName())) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ catalog.addProductAvailableAO(getExistingProduct(bp), product);
+ }
}
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
index 885b7cb..856fd91 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
@@ -76,7 +76,7 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements
//No other value is allowed for Tiered ADDONS
//A value of -1 means unlimited
@XmlElement(required = false)
- private Integer plansAllowedInBundle = 1;
+ private Integer plansAllowedInBundle = -1;
private String priceListName;
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/override/DefaultPriceOverride.java b/catalog/src/main/java/org/killbill/billing/catalog/override/DefaultPriceOverride.java
index c8165b5..86b8739 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/override/DefaultPriceOverride.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/override/DefaultPriceOverride.java
@@ -18,8 +18,11 @@
package org.killbill.billing.catalog.override;
import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Pattern;
+import javax.annotation.Nullable;
+
import org.joda.time.DateTime;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.callcontext.InternalCallContext;
@@ -43,6 +46,8 @@ import com.google.inject.Inject;
public class DefaultPriceOverride implements PriceOverride {
+ private static final AtomicLong DRY_RUN_PLAN_IDX = new AtomicLong(0);
+
public static final Pattern CUSTOM_PLAN_NAME_PATTERN = Pattern.compile("(.*)-(\\d+)$");
private final CatalogOverrideDao overrideDao;
@@ -55,7 +60,7 @@ public class DefaultPriceOverride implements PriceOverride {
}
@Override
- public DefaultPlan getOrCreateOverriddenPlan(final Plan parentPlan, final DateTime catalogEffectiveDate, final List<PlanPhasePriceOverride> overrides, final InternalCallContext context) throws CatalogApiException {
+ public DefaultPlan getOrCreateOverriddenPlan(final Plan parentPlan, final DateTime catalogEffectiveDate, final List<PlanPhasePriceOverride> overrides, @Nullable final InternalCallContext context) throws CatalogApiException {
final PlanPhasePriceOverride[] resolvedOverride = new PlanPhasePriceOverride[parentPlan.getAllPhases().length];
int index = 0;
@@ -97,9 +102,17 @@ public class DefaultPriceOverride implements PriceOverride {
}
}
- final CatalogOverridePlanDefinitionModelDao overriddenPlan = overrideDao.getOrCreateOverridePlanDefinition(parentPlan.getName(), catalogEffectiveDate, resolvedOverride, context);
- final String planName = new StringBuffer(parentPlan.getName()).append("-").append(overriddenPlan.getRecordId()).toString();
+ final String planName;
+ if (context != null) {
+ final CatalogOverridePlanDefinitionModelDao overriddenPlan = overrideDao.getOrCreateOverridePlanDefinition(parentPlan.getName(), catalogEffectiveDate, resolvedOverride, context);
+ planName = new StringBuffer(parentPlan.getName()).append("-").append(overriddenPlan.getRecordId()).toString();
+ } else {
+ planName = new StringBuffer(parentPlan.getName()).append("-dryrun-").append(DRY_RUN_PLAN_IDX.incrementAndGet()).toString();
+ }
final DefaultPlan result = new DefaultPlan(planName, (DefaultPlan) parentPlan, resolvedOverride);
+ if (context == null) {
+ overriddenPlanCache.addDryRunPlan(planName, result);
+ }
return result;
}
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 8a458c4..fbffc1f 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalogWithPriceOverride.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalogWithPriceOverride.java
@@ -78,7 +78,7 @@ public class StandaloneCatalogWithPriceOverride extends StandaloneCatalog implem
return defaultPlan;
}
- final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContextWithoutAccountRecordId(overrides.getCallContext());
+ final InternalCallContext internalCallContext = overrides.getCallContext() != null ? internalCallContextFactory.createInternalCallContextWithoutAccountRecordId(overrides.getCallContext()) : null;
return priceOverride.getOrCreateOverriddenPlan(defaultPlan, CatalogDateHelper.toUTCDateTime(getEffectiveDate()), overrides.getOverrides(), internalCallContext);
}
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 c101cd0..6f2af55 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java
@@ -383,7 +383,7 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
" </recurring>\n" +
" <usages/>\n" +
" </finalPhase>\n" +
- " <plansAllowedInBundle>1</plansAllowedInBundle>\n" +
+ " <plansAllowedInBundle>-1</plansAllowedInBundle>\n" +
" </plan>\n" +
" <plan name=\"sports-monthly\">\n" +
" <product>Sports</product>\n" +
@@ -428,7 +428,7 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
" </recurring>\n" +
" <usages/>\n" +
" </finalPhase>\n" +
- " <plansAllowedInBundle>1</plansAllowedInBundle>\n" +
+ " <plansAllowedInBundle>-1</plansAllowedInBundle>\n" +
" </plan>\n" +
" <plan name=\"super-monthly\">\n" +
" <product>Super</product>\n" +
@@ -473,7 +473,7 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
" </recurring>\n" +
" <usages/>\n" +
" </finalPhase>\n" +
- " <plansAllowedInBundle>1</plansAllowedInBundle>\n" +
+ " <plansAllowedInBundle>-1</plansAllowedInBundle>\n" +
" </plan>\n" +
" <plan name=\"dynamic-annual\">\n" +
" <product>Dynamic</product>\n" +
@@ -510,7 +510,7 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
" </recurring>\n" +
" <usages/>\n" +
" </finalPhase>\n" +
- " <plansAllowedInBundle>1</plansAllowedInBundle>\n" +
+ " <plansAllowedInBundle>-1</plansAllowedInBundle>\n" +
" </plan>\n" +
" </plans>\n" +
" <priceLists>\n" +
diff --git a/catalog/src/test/resources/catalogTest.xml b/catalog/src/test/resources/catalogTest.xml
index a1cdec3..670abb5 100644
--- a/catalog/src/test/resources/catalogTest.xml
+++ b/catalog/src/test/resources/catalogTest.xml
@@ -838,6 +838,7 @@
</recurringPrice>
</recurring>
</finalPhase>
+ <plansAllowedInBundle>2</plansAllowedInBundle>
</plan>
<plan name="cleaning-monthly">
<product>Cleaning</product>
currency/pom.xml 2(+1 -1)
diff --git a/currency/pom.xml b/currency/pom.xml
index 149fd67..c39bb22 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.17.3-SNAPSHOT</version>
+ <version>0.17.5-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 8fe8984..992df8e 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.17.3-SNAPSHOT</version>
+ <version>0.17.5-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-entitlement</artifactId>
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
index 3e5fa05..1f84737 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
@@ -35,6 +35,7 @@ import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.PlanPhase;
import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
@@ -559,9 +560,11 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
final DateTime effectiveChangeDate;
try {
- effectiveChangeDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), spec, null, null, context);
+ effectiveChangeDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), spec, null, null, overrides, context);
} catch (final SubscriptionBaseApiException e) {
throw new EntitlementApiException(e, e.getCode(), e.getMessage());
+ } catch (final CatalogApiException e) {
+ throw new EntitlementApiException(e, e.getCode(), e.getMessage());
}
try {
@@ -624,9 +627,11 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
final DateTime resultingEffectiveDate;
try {
- resultingEffectiveDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), spec, effectiveChangeDate, null, context);
+ resultingEffectiveDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), spec, effectiveChangeDate, null, overrides, context);
} catch (final SubscriptionBaseApiException e) {
throw new EntitlementApiException(e, e.getCode(), e.getMessage());
+ } catch (final CatalogApiException e) {
+ throw new EntitlementApiException(e, e.getCode(), e.getMessage());
}
try {
@@ -689,9 +694,11 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
final DateTime effectiveChangeDate;
try {
- effectiveChangeDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), spec, null, actionPolicy, context);
+ effectiveChangeDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), spec, null, actionPolicy, overrides, context);
} catch (final SubscriptionBaseApiException e) {
throw new EntitlementApiException(e, e.getCode(), e.getMessage());
+ } catch (final CatalogApiException e) {
+ throw new EntitlementApiException(e, e.getCode(), e.getMessage());
}
try {
invoice/pom.xml 2(+1 -1)
diff --git a/invoice/pom.xml b/invoice/pom.xml
index f1795d3..68088ab 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.17.3-SNAPSHOT</version>
+ <version>0.17.5-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-invoice</artifactId>
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java b/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java
index 65f339f..79a0a45 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java
@@ -17,14 +17,13 @@
package org.killbill.billing.invoice.config;
-import java.lang.reflect.Method;
-
import javax.inject.Inject;
import javax.inject.Named;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.glue.InvoiceModule;
import org.killbill.billing.util.config.definition.InvoiceConfig;
+import org.killbill.billing.util.config.definition.KillbillConfig;
import org.killbill.billing.util.config.tenant.CacheConfig;
import org.killbill.billing.util.config.tenant.MultiTenantConfigBase;
import org.skife.config.TimeSpan;
@@ -46,8 +45,7 @@ public class MultiTenantInvoiceConfig extends MultiTenantConfigBase implements I
@Override
public int getNumberOfMonthsInFuture(final InternalTenantContext tenantContext) {
- final Method method = new Object(){}.getClass().getEnclosingMethod();
- final String result = getStringTenantConfig(method.getName(), tenantContext);
+ final String result = getStringTenantConfig("getNumberOfMonthsInFuture", tenantContext);
if (result != null) {
return Integer.parseInt(result);
}
@@ -61,8 +59,7 @@ public class MultiTenantInvoiceConfig extends MultiTenantConfigBase implements I
@Override
public TimeSpan getDryRunNotificationSchedule(final InternalTenantContext tenantContext) {
- final Method method = new Object(){}.getClass().getEnclosingMethod();
- final String result = getStringTenantConfig(method.getName(), tenantContext);
+ final String result = getStringTenantConfig("getDryRunNotificationSchedule", tenantContext);
if (result != null) {
return new TimeSpan(result);
}
@@ -76,8 +73,7 @@ public class MultiTenantInvoiceConfig extends MultiTenantConfigBase implements I
@Override
public int getMaxRawUsagePreviousPeriod(final InternalTenantContext tenantContext) {
- final Method method = new Object(){}.getClass().getEnclosingMethod();
- final String result = getStringTenantConfig(method.getName(), tenantContext);
+ final String result = getStringTenantConfig("getMaxRawUsagePreviousPeriod", tenantContext);
if (result != null) {
return Integer.parseInt(result);
}
@@ -95,11 +91,7 @@ public class MultiTenantInvoiceConfig extends MultiTenantConfigBase implements I
}
@Override
- protected Method getConfigStaticMethod(final String methodName) {
- try {
- return InvoiceConfig.class.getMethod(methodName, InternalTenantContext.class);
- } catch (final NoSuchMethodException e) {
- throw new RuntimeException(e);
- }
+ protected Class<? extends KillbillConfig> getConfigClass() {
+ return InvoiceConfig.class;
}
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java
index 846b057..8a75ef0 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java
@@ -32,12 +32,14 @@ public class BillingIntervalDetail {
private final int billingCycleDay;
private final BillingPeriod billingPeriod;
private final BillingMode billingMode;
+ private final boolean isMonthBased;
// First date after the startDate aligned with the BCD
private LocalDate firstBillingCycleDate;
// Date up to which we should bill
private LocalDate effectiveEndDate;
private LocalDate lastBillingCycleDate;
+
public BillingIntervalDetail(final LocalDate startDate,
final LocalDate endDate,
final LocalDate targetDate,
@@ -54,6 +56,7 @@ public class BillingIntervalDetail {
}
this.billingPeriod = billingPeriod;
this.billingMode = billingMode;
+ this.isMonthBased = (billingPeriod.getPeriod().getMonths() | billingPeriod.getPeriod().getYears()) > 0;
computeAll();
}
@@ -67,7 +70,7 @@ public class BillingIntervalDetail {
public LocalDate getFutureBillingDateFor(final int nbPeriod) {
final LocalDate proposedDate = InvoiceDateUtils.advanceByNPeriods(firstBillingCycleDate, billingPeriod, nbPeriod);
- return alignProposedBillCycleDate(proposedDate, billingCycleDay);
+ return alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
}
public LocalDate getLastBillingCycleDate() {
@@ -76,7 +79,7 @@ public class BillingIntervalDetail {
public LocalDate getNextBillingCycleDate() {
final LocalDate proposedDate = lastBillingCycleDate != null ? lastBillingCycleDate.plus(billingPeriod.getPeriod()) : firstBillingCycleDate;
- final LocalDate nextBillingCycleDate = alignProposedBillCycleDate(proposedDate, billingCycleDay);
+ final LocalDate nextBillingCycleDate = alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
return nextBillingCycleDate;
}
@@ -106,7 +109,7 @@ public class BillingIntervalDetail {
while (proposedDate.isBefore(startDate)) {
proposedDate = proposedDate.plus(billingPeriod.getPeriod());
}
- firstBillingCycleDate = alignProposedBillCycleDate(proposedDate, billingCycleDay);
+ firstBillingCycleDate = alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
}
private void calculateEffectiveEndDate() {
@@ -138,7 +141,7 @@ public class BillingIntervalDetail {
nextProposedDate = InvoiceDateUtils.advanceByNPeriods(firstBillingCycleDate, billingPeriod, numberOfPeriods);
numberOfPeriods += 1;
}
- proposedDate = alignProposedBillCycleDate(proposedDate, billingCycleDay);
+ proposedDate = alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
// We honor the endDate as long as it does not go beyond our targetDate (by construction this cannot be after the nextProposedDate neither.
if (endDate != null && !endDate.isAfter(targetDate)) {
@@ -168,7 +171,7 @@ public class BillingIntervalDetail {
proposedDate = InvoiceDateUtils.advanceByNPeriods(firstBillingCycleDate, billingPeriod, numberOfPeriods);
numberOfPeriods += 1;
}
- proposedDate = alignProposedBillCycleDate(proposedDate, billingCycleDay);
+ proposedDate = alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
// The proposedDate is greater to our endDate => return it
if (endDate != null && endDate.isBefore(proposedDate)) {
@@ -196,7 +199,7 @@ public class BillingIntervalDetail {
// Our proposed date is billingCycleDate prior to the effectiveEndDate
proposedDate = proposedDate.minus(billingPeriod.getPeriod());
- proposedDate = alignProposedBillCycleDate(proposedDate, billingCycleDay);
+ proposedDate = alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
if (proposedDate.isBefore(firstBillingCycleDate)) {
// Make sure not to go too far in the past
@@ -209,7 +212,11 @@ public class BillingIntervalDetail {
//
// We start from a billCycleDate
//
- public static LocalDate alignProposedBillCycleDate(final LocalDate proposedDate, final int billingCycleDay) {
+ private static LocalDate alignProposedBillCycleDate(final LocalDate proposedDate, final int billingCycleDay, final boolean isMonthBased) {
+ // billingCycleDay alignment only makes sense for month based BillingPeriod (MONTHLY, QUARTERLY, BIANNUAL, ANNUAL)
+ if (!isMonthBased) {
+ return proposedDate;
+ }
final int lastDayOfMonth = proposedDate.dayOfMonth().getMaximumValue();
int proposedBillCycleDate = proposedDate.getDayOfMonth();
if (proposedBillCycleDate < billingCycleDay && billingCycleDay <= lastDayOfMonth) {
diff --git a/invoice/src/main/resources/org/killbill/billing/invoice/migration/V20160908172551__multiple_refunds_254.sql b/invoice/src/main/resources/org/killbill/billing/invoice/migration/V20160908172551__multiple_refunds_254.sql
new file mode 100644
index 0000000..e5628d6
--- /dev/null
+++ b/invoice/src/main/resources/org/killbill/billing/invoice/migration/V20160908172551__multiple_refunds_254.sql
@@ -0,0 +1 @@
+drop index idx_invoice_payments on invoice_payments;
\ No newline at end of file
jaxrs/pom.xml 2(+1 -1)
diff --git a/jaxrs/pom.xml b/jaxrs/pom.xml
index d477452..5e2bb13 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.17.3-SNAPSHOT</version>
+ <version>0.17.5-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 112f24b..79a2b8b 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
@@ -947,7 +947,7 @@ public class CatalogJson {
this.number = number;
}
- public DurationJson(final Duration duration) throws CurrencyValueNull {
+ public DurationJson(final Duration duration) {
this(duration.getUnit(), duration.getNumber());
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueConditionJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueConditionJson.java
new file mode 100644
index 0000000..048a3c5
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueConditionJson.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 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
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.math.BigDecimal;
+
+import org.killbill.billing.jaxrs.json.CatalogJson.DurationJson;
+import org.killbill.billing.overdue.api.OverdueCondition;
+import org.killbill.billing.overdue.config.DefaultDuration;
+import org.killbill.billing.overdue.config.DefaultOverdueCondition;
+import org.killbill.billing.payment.api.PaymentResponse;
+import org.killbill.billing.util.tag.ControlTagType;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class OverdueConditionJson {
+
+ private final DurationJson timeSinceEarliestUnpaidInvoiceEqualsOrExceeds;
+ private final ControlTagType controlTagInclusion;
+ private final ControlTagType controlTagExclusion;
+ private final Integer numberOfUnpaidInvoicesEqualsOrExceeds;
+ private final PaymentResponse[] responseForLastFailedPayment;
+ private final BigDecimal totalUnpaidInvoiceBalanceEqualsOrExceeds;
+
+
+ @JsonCreator
+ public OverdueConditionJson(@JsonProperty("timeSinceEarliestUnpaidInvoiceEqualsOrExceeds") final DurationJson timeSinceEarliestUnpaidInvoiceEqualsOrExceeds,
+ @JsonProperty("controlTagInclusion") final ControlTagType controlTagInclusion,
+ @JsonProperty("controlTagExclusion") final ControlTagType controlTagExclusion,
+ @JsonProperty("numberOfUnpaidInvoicesEqualsOrExceeds") final Integer numberOfUnpaidInvoicesEqualsOrExceeds,
+ @JsonProperty("responseForLastFailedPayment") final PaymentResponse[] responseForLastFailedPayment,
+ @JsonProperty("totalUnpaidInvoiceBalanceEqualsOrExceeds") final BigDecimal totalUnpaidInvoiceBalanceEqualsOrExceeds) {
+ this.timeSinceEarliestUnpaidInvoiceEqualsOrExceeds = timeSinceEarliestUnpaidInvoiceEqualsOrExceeds;
+ this.controlTagInclusion = controlTagInclusion;
+ this.controlTagExclusion = controlTagExclusion;
+ this.numberOfUnpaidInvoicesEqualsOrExceeds = numberOfUnpaidInvoicesEqualsOrExceeds;
+ this.responseForLastFailedPayment = responseForLastFailedPayment;
+ this.totalUnpaidInvoiceBalanceEqualsOrExceeds = totalUnpaidInvoiceBalanceEqualsOrExceeds;
+ }
+
+ public OverdueConditionJson(final OverdueCondition overdueCondition) {
+ this.timeSinceEarliestUnpaidInvoiceEqualsOrExceeds = new DurationJson(overdueCondition.getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds());
+ this.controlTagInclusion = overdueCondition.getInclusionControlTagType();
+ this.controlTagExclusion = overdueCondition.getExclusionControlTagType();
+ this.numberOfUnpaidInvoicesEqualsOrExceeds = overdueCondition.getNumberOfUnpaidInvoicesEqualsOrExceeds();
+ this.responseForLastFailedPayment = overdueCondition.getResponseForLastFailedPaymentIn();
+ this.totalUnpaidInvoiceBalanceEqualsOrExceeds = overdueCondition.getTotalUnpaidInvoiceBalanceEqualsOrExceeds();
+ }
+
+ public DurationJson getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds() {
+ return timeSinceEarliestUnpaidInvoiceEqualsOrExceeds;
+ }
+
+ public ControlTagType getControlTagInclusion() {
+ return controlTagInclusion;
+ }
+
+ public ControlTagType getControlTagExclusion() {
+ return controlTagExclusion;
+ }
+
+ public Integer getNumberOfUnpaidInvoicesEqualsOrExceeds() {
+ return numberOfUnpaidInvoicesEqualsOrExceeds;
+ }
+
+ public PaymentResponse[] getResponseForLastFailedPayment() {
+ return responseForLastFailedPayment;
+ }
+
+ public BigDecimal getTotalUnpaidInvoiceBalanceEqualsOrExceeds() {
+ return totalUnpaidInvoiceBalanceEqualsOrExceeds;
+ }
+
+ @Override
+ public String toString() {
+ return "OverdueConditionJson{" +
+ "timeSinceEarliestUnpaidInvoiceEqualsOrExceeds=" + timeSinceEarliestUnpaidInvoiceEqualsOrExceeds +
+ ", controlTagInclusion=" + controlTagInclusion +
+ ", controlTagExclusion=" + controlTagExclusion +
+ ", numberOfUnpaidInvoicesEqualsOrExceeds=" + numberOfUnpaidInvoicesEqualsOrExceeds +
+ ", responseForLastFailedPayment=" + responseForLastFailedPayment +
+ ", totalUnpaidInvoiceBalanceEqualsOrExceeds=" + totalUnpaidInvoiceBalanceEqualsOrExceeds +
+ '}';
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof OverdueConditionJson)) {
+ return false;
+ }
+
+ final OverdueConditionJson that = (OverdueConditionJson) o;
+
+ if (timeSinceEarliestUnpaidInvoiceEqualsOrExceeds != null ? !timeSinceEarliestUnpaidInvoiceEqualsOrExceeds.equals(that.timeSinceEarliestUnpaidInvoiceEqualsOrExceeds) : that.timeSinceEarliestUnpaidInvoiceEqualsOrExceeds != null) {
+ return false;
+ }
+ if (controlTagInclusion != that.controlTagInclusion) {
+ return false;
+ }
+ if (controlTagExclusion != that.controlTagExclusion) {
+ return false;
+ }
+ if (numberOfUnpaidInvoicesEqualsOrExceeds != that.numberOfUnpaidInvoicesEqualsOrExceeds) {
+ return false;
+ }
+ if (responseForLastFailedPayment != that.responseForLastFailedPayment) {
+ return false;
+ }
+ return totalUnpaidInvoiceBalanceEqualsOrExceeds == that.totalUnpaidInvoiceBalanceEqualsOrExceeds;
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = timeSinceEarliestUnpaidInvoiceEqualsOrExceeds != null ? timeSinceEarliestUnpaidInvoiceEqualsOrExceeds.hashCode() : 0;
+ result = 31 * result + (controlTagInclusion != null ? controlTagInclusion.hashCode() : 0);
+ result = 31 * result + (controlTagExclusion != null ? controlTagExclusion.hashCode() : 0);
+ result = 31 * result + (numberOfUnpaidInvoicesEqualsOrExceeds != null ? numberOfUnpaidInvoicesEqualsOrExceeds.hashCode() : 0);
+ result = 31 * result + (responseForLastFailedPayment != null ? responseForLastFailedPayment.hashCode() : 0);
+ result = 31 * result + (totalUnpaidInvoiceBalanceEqualsOrExceeds != null ? totalUnpaidInvoiceBalanceEqualsOrExceeds.hashCode() : 0);
+ return result;
+ }
+
+ public static DefaultOverdueCondition toOverdueCondition(final OverdueConditionJson input) {
+ final DefaultOverdueCondition result = new DefaultOverdueCondition();
+ if (input.getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds() != null) {
+ result.setTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds(new DefaultDuration().setUnit(input.getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds().getUnit()).setNumber(input.getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds().getNumber()));
+ }
+ result.setControlTagInclusion(input.getControlTagInclusion());
+ result.setControlTagExclusion(input.getControlTagExclusion());
+ result.setNumberOfUnpaidInvoicesEqualsOrExceeds(input.getNumberOfUnpaidInvoicesEqualsOrExceeds());
+ result.setResponseForLastFailedPayment(input.getResponseForLastFailedPayment());
+ result.setTotalUnpaidInvoiceBalanceEqualsOrExceeds(input.getTotalUnpaidInvoiceBalanceEqualsOrExceeds());
+
+ return result;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueJson.java
new file mode 100644
index 0000000..c2284be
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueJson.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 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
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.List;
+
+import org.killbill.billing.catalog.api.CurrencyValueNull;
+import org.killbill.billing.catalog.api.TimeUnit;
+import org.killbill.billing.overdue.api.OverdueApiException;
+import org.killbill.billing.overdue.api.OverdueConfig;
+import org.killbill.billing.overdue.api.OverdueState;
+import org.killbill.billing.overdue.config.DefaultDuration;
+import org.killbill.billing.overdue.config.DefaultOverdueConfig;
+import org.killbill.billing.overdue.config.DefaultOverdueState;
+import org.killbill.billing.overdue.config.DefaultOverdueStatesAccount;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+public class OverdueJson {
+
+ private final Integer initialReevaluationIntervalDays;
+ private final List<OverdueStateConfigJson> overdueStates;
+
+ @JsonCreator
+ public OverdueJson(@JsonProperty("initialReevaluationInterval") final Integer initialReevaluationInterval,
+ @JsonProperty("overdueStates") final List<OverdueStateConfigJson> overdueStates) {
+ this.initialReevaluationIntervalDays = initialReevaluationInterval;
+ this.overdueStates = overdueStates;
+ }
+
+ public OverdueJson(final OverdueConfig overdueConfig) {
+ this.initialReevaluationIntervalDays = overdueConfig.getOverdueStatesAccount().getInitialReevaluationInterval() != null ?
+ overdueConfig.getOverdueStatesAccount().getInitialReevaluationInterval().getDays() : null;
+ this.overdueStates = ImmutableList.copyOf(Iterables.transform(ImmutableList.copyOf(overdueConfig.getOverdueStatesAccount().getStates()), new Function<OverdueState, OverdueStateConfigJson>() {
+ @Override
+ public OverdueStateConfigJson apply(final OverdueState input) {
+ return new OverdueStateConfigJson(input);
+ }
+ }));
+ }
+
+ public Integer getInitialReevaluationInterval() {
+ return initialReevaluationIntervalDays;
+ }
+
+ public List<OverdueStateConfigJson> getOverdueStates() {
+ return overdueStates;
+ }
+
+ @Override
+ public String toString() {
+ return "OverdueJson{" +
+ "initialReevaluationIntervalDays=" + initialReevaluationIntervalDays +
+ ", overdueStates=" + overdueStates +
+ '}';
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof OverdueJson)) {
+ return false;
+ }
+
+ final OverdueJson that = (OverdueJson) o;
+
+ if (initialReevaluationIntervalDays != null ? !initialReevaluationIntervalDays.equals(that.initialReevaluationIntervalDays) : that.initialReevaluationIntervalDays != null) {
+ return false;
+ }
+ return overdueStates != null ? overdueStates.equals(that.overdueStates) : that.overdueStates == null;
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = initialReevaluationIntervalDays != null ? initialReevaluationIntervalDays.hashCode() : 0;
+ result = 31 * result + (overdueStates != null ? overdueStates.hashCode() : 0);
+ return result;
+ }
+
+ public static OverdueConfig toOverdueConfigWithValidation(final OverdueJson input) {
+ final DefaultOverdueConfig result = new DefaultOverdueConfig();
+ final DefaultOverdueStatesAccount overdueStateAccount = new DefaultOverdueStatesAccount();
+ result.setOverdueStates(overdueStateAccount);
+
+ final DefaultOverdueState [] states = new DefaultOverdueState[input.getOverdueStates().size()];
+ int i = 0;
+
+
+ int prevTimeSinceEarliestUnpaidInvoice = -1;
+ for (final OverdueStateConfigJson cur : input.getOverdueStates()) {
+
+ Preconditions.checkNotNull(cur.getName());
+
+ // We only support timeSinceEarliestUnpaidInvoiceEqualsOrExceeds condition (see #611)
+ Preconditions.checkNotNull(cur.getCondition());
+ Preconditions.checkNotNull(cur.getCondition().getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds());
+ Preconditions.checkNotNull(cur.getCondition().getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds().getUnit());
+ Preconditions.checkState(cur.getCondition().getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds().getUnit() == TimeUnit.DAYS);
+ Preconditions.checkState(cur.getCondition().getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds().getNumber() > 0);
+
+ final DefaultOverdueState state = new DefaultOverdueState();
+ state.setName(cur.getName());
+ state.setExternalMessage(cur.getExternalMessage());
+ state.setBlockChanges(cur.getBlockChanges());
+ state.setDisableEntitlement(cur.getDisableEntitlement());
+ state.setSubscriptionCancellationPolicy(cur.getSubscriptionCancellationPolicy());
+ state.setClearState(cur.isClearState());
+ state.setAutoReevaluationInterval(computeReevaluationInterval(cur.getAutoReevaluationIntervalDays(), prevTimeSinceEarliestUnpaidInvoice, cur.getCondition().getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds().getNumber()));
+ state.setCondition(OverdueConditionJson.toOverdueCondition(cur.getCondition()));
+ states[i++] = state;
+
+ prevTimeSinceEarliestUnpaidInvoice = cur.getCondition().getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds().getNumber();
+ }
+ overdueStateAccount.setAccountOverdueStates(states);
+ overdueStateAccount.setInitialReevaluationInterval(computeReevaluationInterval(null, prevTimeSinceEarliestUnpaidInvoice, 0));
+ return result;
+ }
+
+ // Unless the user knows what it's doing (inputReevaluationInterval != null), for time based condition we set the reevaluation interval to match the transition to the next state
+ private static DefaultDuration computeReevaluationInterval(final Integer inputReevaluationInterval, int prevTimeSinceEarliestUnpaidInvoice, int curTimeSinceEarliestUnpaidInvoice) {
+ if (inputReevaluationInterval != null && inputReevaluationInterval > 0) {
+ return new DefaultDuration().setUnit(TimeUnit.DAYS).setNumber(inputReevaluationInterval);
+ }
+
+ if (prevTimeSinceEarliestUnpaidInvoice == -1) {
+ return null;
+ }
+
+ Preconditions.checkState(prevTimeSinceEarliestUnpaidInvoice - curTimeSinceEarliestUnpaidInvoice > 0);
+
+ return new DefaultDuration().setUnit(TimeUnit.DAYS).setNumber(prevTimeSinceEarliestUnpaidInvoice - curTimeSinceEarliestUnpaidInvoice);
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueStateConfigJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueStateConfigJson.java
new file mode 100644
index 0000000..ab06260
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueStateConfigJson.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 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
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import org.killbill.billing.overdue.api.OverdueApiException;
+import org.killbill.billing.overdue.api.OverdueCancellationPolicy;
+import org.killbill.billing.overdue.api.OverdueState;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class OverdueStateConfigJson {
+
+ private final String name;
+ private final Boolean isClearState;
+ private final OverdueConditionJson condition;
+ private final String externalMessage;
+ private final Boolean blockChanges;
+ private final Boolean disableEntitlement;
+ private final OverdueCancellationPolicy subscriptionCancellationPolicy;
+ private final Integer autoReevaluationIntervalDays;
+
+ @JsonCreator
+ public OverdueStateConfigJson(@JsonProperty("name") final String name,
+ @JsonProperty("isClearState") final Boolean isClearState,
+ @JsonProperty("condition") final OverdueConditionJson condition,
+ @JsonProperty("externalMessage") final String externalMessage,
+ @JsonProperty("blockChanges") final Boolean blockChanges,
+ @JsonProperty("disableEntitlement") final Boolean disableEntitlement,
+ @JsonProperty("subscriptionCancellationPolicy") final OverdueCancellationPolicy subscriptionCancellationPolicy,
+ @JsonProperty("autoReevaluationIntervalDays") final Integer autoReevaluationInterval) {
+ this.name = name;
+ this.isClearState = isClearState;
+ this.condition = condition;
+ this.externalMessage = externalMessage;
+ this.blockChanges = blockChanges;
+ this.disableEntitlement = disableEntitlement;
+ this.subscriptionCancellationPolicy = subscriptionCancellationPolicy;
+ this.autoReevaluationIntervalDays = autoReevaluationInterval;
+ }
+
+ public OverdueStateConfigJson(final OverdueState input) {
+ this.name = input.getName();
+ this.isClearState = input.isClearState();
+ this.condition = input.getOverdueCondition() != null ? new OverdueConditionJson(input.getOverdueCondition()) : null;
+ this.externalMessage = input.getExternalMessage();
+ this.blockChanges = input.isBlockChanges();
+ this.disableEntitlement = input.isDisableEntitlementAndChangesBlocked();
+ this.subscriptionCancellationPolicy = input.getOverdueCancellationPolicy();
+ Integer tmpAutoReevaluationIntervalDays = null;
+ try {
+ tmpAutoReevaluationIntervalDays = input.getAutoReevaluationInterval().getDays();
+ } catch (final OverdueApiException e) {
+ } finally {
+ this.autoReevaluationIntervalDays = tmpAutoReevaluationIntervalDays;
+ }
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @JsonProperty("isClearState")
+ public Boolean isClearState() {
+ return isClearState;
+ }
+
+ public OverdueConditionJson getCondition() {
+ return condition;
+ }
+
+ public String getExternalMessage() {
+ return externalMessage;
+ }
+
+ public Boolean getBlockChanges() {
+ return blockChanges;
+ }
+
+ public Boolean getDisableEntitlement() {
+ return disableEntitlement;
+ }
+
+ public OverdueCancellationPolicy getSubscriptionCancellationPolicy() {
+ return subscriptionCancellationPolicy;
+ }
+
+ public Integer getAutoReevaluationIntervalDays() {
+ return autoReevaluationIntervalDays;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof OverdueStateConfigJson)) {
+ return false;
+ }
+
+ final OverdueStateConfigJson that = (OverdueStateConfigJson) o;
+
+ if (name != null ? !name.equals(that.name) : that.name != null) {
+ return false;
+ }
+ if (isClearState != null ? !isClearState.equals(that.isClearState) : that.isClearState != null) {
+ return false;
+ }
+ if (condition != null ? !condition.equals(that.condition) : that.condition != null) {
+ return false;
+ }
+ if (externalMessage != null ? !externalMessage.equals(that.externalMessage) : that.externalMessage != null) {
+ return false;
+ }
+ if (blockChanges != null ? !blockChanges.equals(that.blockChanges) : that.blockChanges != null) {
+ return false;
+ }
+ if (disableEntitlement != null ? !disableEntitlement.equals(that.disableEntitlement) : that.disableEntitlement != null) {
+ return false;
+ }
+ if (subscriptionCancellationPolicy != that.subscriptionCancellationPolicy) {
+ return false;
+ }
+ return autoReevaluationIntervalDays != null ? autoReevaluationIntervalDays.equals(that.autoReevaluationIntervalDays) : that.autoReevaluationIntervalDays == null;
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = name != null ? name.hashCode() : 0;
+ result = 31 * result + (isClearState != null ? isClearState.hashCode() : 0);
+ result = 31 * result + (condition != null ? condition.hashCode() : 0);
+ result = 31 * result + (externalMessage != null ? externalMessage.hashCode() : 0);
+ result = 31 * result + (blockChanges != null ? blockChanges.hashCode() : 0);
+ result = 31 * result + (disableEntitlement != null ? disableEntitlement.hashCode() : 0);
+ result = 31 * result + (subscriptionCancellationPolicy != null ? subscriptionCancellationPolicy.hashCode() : 0);
+ result = 31 * result + (autoReevaluationIntervalDays != null ? autoReevaluationIntervalDays.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "OverdueStateConfigJson{" +
+ "name='" + name + '\'' +
+ ", isClearState=" + isClearState +
+ ", condition=" + condition +
+ ", externalMessage='" + externalMessage + '\'' +
+ ", blockChanges=" + blockChanges +
+ ", disableEntitlement=" + disableEntitlement +
+ ", subscriptionCancellationPolicy=" + subscriptionCancellationPolicy +
+ ", autoReevaluationIntervalDays=" + autoReevaluationIntervalDays +
+ '}';
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/OverdueResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/OverdueResource.java
index a30a0cf..534d07e 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/OverdueResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/OverdueResource.java
@@ -33,9 +33,11 @@ import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.billing.jaxrs.json.OverdueJson;
import org.killbill.billing.jaxrs.util.Context;
import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
import org.killbill.billing.overdue.api.OverdueApi;
+import org.killbill.billing.overdue.api.OverdueConfig;
import org.killbill.billing.overdue.config.DefaultOverdueConfig;
import org.killbill.billing.payment.api.PaymentApi;
import org.killbill.billing.util.api.AuditUserApi;
@@ -54,6 +56,7 @@ import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponses;
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.APPLICATION_XML;
@Singleton
@@ -80,7 +83,7 @@ public class OverdueResource extends JaxRsResourceBase {
@TimedResource
@GET
@Produces(APPLICATION_XML)
- @ApiOperation(value = "Retrieve the full catalog as XML", response = String.class, hidden = true)
+ @ApiOperation(value = "Retrieve the overdue config as XML", response = String.class, hidden = true)
@ApiResponses(value = {})
public Response getOverdueConfigXml(@javax.ws.rs.core.Context final HttpServletRequest request) throws Exception {
final TenantContext tenantContext = context.createContext(request);
@@ -106,4 +109,38 @@ public class OverdueResource extends JaxRsResourceBase {
overdueApi.uploadOverdueConfig(overdueXML, callContext);
return uriBuilder.buildResponse(uriInfo, OverdueResource.class, null, null);
}
+
+ @TimedResource
+ @GET
+ @Produces(APPLICATION_JSON)
+ @ApiOperation(value = "Retrieve the overdue config as JSON" , response = OverdueJson.class)
+ @ApiResponses(value = {})
+ public Response getOverdueConfigJson(@javax.ws.rs.core.Context final HttpServletRequest request) throws Exception {
+ final TenantContext tenantContext = context.createContext(request);
+ final OverdueConfig overdueConfig = overdueApi.getOverdueConfig(tenantContext);
+ final OverdueJson result = new OverdueJson(overdueConfig);
+ return Response.status(Status.OK).entity(result).build();
+ }
+
+
+
+ @TimedResource
+ @POST
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ @ApiOperation(value = "Upload the full overdue config as JSON")
+ @ApiResponses(value = {})
+ public Response uploadOverdueConfigJson(final OverdueJson overdueJson,
+ @HeaderParam(HDR_CREATED_BY) final String createdBy,
+ @HeaderParam(HDR_REASON) final String reason,
+ @HeaderParam(HDR_COMMENT) final String comment,
+ @javax.ws.rs.core.Context final HttpServletRequest request,
+ @javax.ws.rs.core.Context final UriInfo uriInfo) throws Exception {
+ final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+
+ final OverdueConfig overdueConfig = OverdueJson.toOverdueConfigWithValidation(overdueJson);
+ overdueApi.uploadOverdueConfig(overdueConfig, callContext);
+ return uriBuilder.buildResponse(uriInfo, OverdueResource.class, null, null);
+ }
+
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/JaxrsUriBuilder.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/JaxrsUriBuilder.java
index 7a6b6aa..ee09c1a 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/JaxrsUriBuilder.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/JaxrsUriBuilder.java
@@ -17,6 +17,7 @@
package org.killbill.billing.jaxrs.util;
import java.net.URI;
+import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nullable;
@@ -33,6 +34,10 @@ import org.killbill.billing.util.config.definition.JaxrsConfig;
public class JaxrsUriBuilder {
private final JaxrsConfig jaxrsConfig;
+ private final Map<Class, UriBuilder> classToUriBuilder = new HashMap<Class, UriBuilder>();
+ private final Map<String, UriBuilder> classAndMethodToUriBuilder = new HashMap<String, UriBuilder>();
+ private final Map<String, UriBuilder> pathAndClassToUriBuilder = new HashMap<String, UriBuilder>();
+ private final Map<String, UriBuilder> pathClassAndMethodToUriBuilder = new HashMap<String, UriBuilder>();
@Inject
public JaxrsUriBuilder(JaxrsConfig jaxrsConfig) {
@@ -53,8 +58,8 @@ public class JaxrsUriBuilder {
if (jaxrsConfig.isJaxrsLocationFullUrl()) {
uriBuilder.scheme(uriInfo.getAbsolutePath().getScheme())
- .host(uriInfo.getAbsolutePath().getHost())
- .port(uriInfo.getAbsolutePath().getPort());
+ .host(uriInfo.getAbsolutePath().getHost())
+ .port(uriInfo.getAbsolutePath().getPort());
}
return objectId != null ? uriBuilder.build(objectId) : uriBuilder.build();
}
@@ -92,15 +97,74 @@ public class JaxrsUriBuilder {
private UriBuilder getUriBuilder(final String path, final Class<? extends JaxrsResource> theClassMaybeEnhanced, @Nullable final String getMethodName) {
final Class theClass = getNonEnhancedClass(theClassMaybeEnhanced);
- return getMethodName != null ? UriBuilder.fromPath(path.equals("/") ? path.substring(1) : path).path(theClass).path(theClass, getMethodName) :
- UriBuilder.fromPath(path).path(theClass);
+ return getMethodName != null ? fromPath(path.equals("/") ? path.substring(1) : path, theClass, getMethodName) : fromPath(path, theClass);
+ }
+
+ private UriBuilder fromPath(final String path, final Class theClass, final String getMethodName) {
+ final String key = path + theClass.getName() + getMethodName;
+
+ UriBuilder uriBuilder = pathClassAndMethodToUriBuilder.get(key);
+ if (uriBuilder == null) {
+ synchronized (pathClassAndMethodToUriBuilder) {
+ uriBuilder = pathClassAndMethodToUriBuilder.get(key);
+ if (uriBuilder == null) {
+ uriBuilder = fromPath(path, theClass).path(theClass, getMethodName);
+ pathClassAndMethodToUriBuilder.put(key, uriBuilder);
+ }
+ }
+ }
+ return uriBuilder.clone();
+ }
+
+ private UriBuilder fromPath(final String path, final Class theClass) {
+ final String key = path + theClass.getName();
+
+ UriBuilder uriBuilder = pathAndClassToUriBuilder.get(key);
+ if (uriBuilder == null) {
+ synchronized (pathAndClassToUriBuilder) {
+ uriBuilder = pathAndClassToUriBuilder.get(key);
+ if (uriBuilder == null) {
+ uriBuilder = UriBuilder.fromPath(path).path(theClass);
+ pathAndClassToUriBuilder.put(key, uriBuilder);
+ }
+ }
+ }
+ return uriBuilder.clone();
}
private UriBuilder getUriBuilder(final Class<? extends JaxrsResource> theClassMaybeEnhanced, @Nullable final String getMethodName) {
final Class theClass = getNonEnhancedClass(theClassMaybeEnhanced);
- return getMethodName != null ? UriBuilder.fromResource(theClass).path(theClass, getMethodName) :
- UriBuilder.fromResource(theClass);
+ return getMethodName != null ? fromResource(theClass, getMethodName) : fromResource(theClass);
+ }
+ private UriBuilder fromResource(final Class theClass, final String getMethodName) {
+ final String key = theClass.getName() + getMethodName;
+
+ UriBuilder uriBuilder = classAndMethodToUriBuilder.get(key);
+ if (uriBuilder == null) {
+ synchronized (classAndMethodToUriBuilder) {
+ uriBuilder = classAndMethodToUriBuilder.get(key);
+ if (uriBuilder == null) {
+ uriBuilder = fromResource(theClass).path(theClass, getMethodName);
+ classAndMethodToUriBuilder.put(key, uriBuilder);
+ }
+ }
+ }
+ return uriBuilder.clone();
+ }
+
+ private UriBuilder fromResource(final Class theClass) {
+ UriBuilder uriBuilder = classToUriBuilder.get(theClass);
+ if (uriBuilder == null) {
+ synchronized (classToUriBuilder) {
+ uriBuilder = classToUriBuilder.get(theClass);
+ if (uriBuilder == null) {
+ uriBuilder = UriBuilder.fromResource(theClass);
+ classToUriBuilder.put(theClass, uriBuilder);
+ }
+ }
+ }
+ return uriBuilder.clone();
}
private Class getNonEnhancedClass(final Class<? extends JaxrsResource> theClassMaybeEnhanced) {
junction/pom.xml 2(+1 -1)
diff --git a/junction/pom.xml b/junction/pom.xml
index 0f5af56..e18f08a 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.17.3-SNAPSHOT</version>
+ <version>0.17.5-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-junction</artifactId>
NEWS 9(+9 -0)
diff --git a/NEWS b/NEWS
index a9b9b12..06725dc 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,9 @@
+0.17.4
+ See https://github.com/killbill/killbill/releases/tag/killbill-0.17.4
+
+0.17.3
+ See https://github.com/killbill/killbill/releases/tag/killbill-0.17.3
+
0.17.2
See https://github.com/killbill/killbill/releases/tag/killbill-0.17.2
@@ -7,6 +13,9 @@
0.17.0
See https://github.com/killbill/killbill/releases/tag/killbill-0.17.0
+0.16.8
+ See https://github.com/killbill/killbill/releases/tag/killbill-0.16.8
+
0.16.7
See https://github.com/killbill/killbill/releases/tag/killbill-0.16.7
overdue/pom.xml 2(+1 -1)
diff --git a/overdue/pom.xml b/overdue/pom.xml
index f247210..8b07ded 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.17.3-SNAPSHOT</version>
+ <version>0.17.5-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-overdue</artifactId>
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/api/DefaultOverdueApi.java b/overdue/src/main/java/org/killbill/billing/overdue/api/DefaultOverdueApi.java
index 580e4f9..05a52c9 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/api/DefaultOverdueApi.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/api/DefaultOverdueApi.java
@@ -21,6 +21,7 @@ import java.util.UUID;
import javax.inject.Inject;
+import org.killbill.billing.ErrorCode;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.entitlement.api.BlockingState;
import org.killbill.billing.entitlement.api.BlockingStateType;
@@ -36,6 +37,7 @@ import org.killbill.billing.tenant.api.TenantUserApi;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.xmlloader.XMLWriter;
public class DefaultOverdueApi implements OverdueApi {
@@ -77,6 +79,16 @@ public class DefaultOverdueApi implements OverdueApi {
}
@Override
+ public void uploadOverdueConfig(final OverdueConfig overdueConfig, final CallContext callContext) throws OverdueApiException {
+ try {
+ final String overdueXML = XMLWriter.writeXML((DefaultOverdueConfig) overdueConfig, DefaultOverdueConfig.class);
+ uploadOverdueConfig(overdueXML, callContext);
+ } catch (final Exception e) {
+ throw new OverdueApiException(ErrorCode.OVERDUE_INVALID_FOR_TENANT, callContext.getTenantId());
+ }
+ }
+
+ @Override
public OverdueState getOverdueStateFor(final UUID accountId, final TenantContext tenantContext) throws OverdueApiException {
final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(accountId, tenantContext);
final BlockingState blockingStateForService = blockingInternalApi.getBlockingStateForService(accountId, BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalTenantContext);
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java b/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java
index cf2ea4f..2d01711 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java
@@ -288,7 +288,7 @@ public class OverdueStateApplicator {
}
private boolean blockChanges(final OverdueState nextOverdueState) {
- return nextOverdueState.isBlockChanges();
+ return nextOverdueState.isBlockChanges() || nextOverdueState.isDisableEntitlementAndChangesBlocked();
}
private boolean blockBilling(final OverdueState nextOverdueState) {
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultDuration.java b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultDuration.java
index d1d73c2..49396ea 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultDuration.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultDuration.java
@@ -111,12 +111,12 @@ public class DefaultDuration extends ValidatingConfig<DefaultOverdueConfig> impl
return errors;
}
- protected DefaultDuration setUnit(final TimeUnit unit) {
+ public DefaultDuration setUnit(final TimeUnit unit) {
this.unit = unit;
return this;
}
- protected DefaultDuration setNumber(final Integer number) {
+ public DefaultDuration setNumber(final Integer number) {
this.number = number;
return this;
}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueCondition.java b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueCondition.java
index 3cca103..a4e4b51 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueCondition.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueCondition.java
@@ -157,6 +157,30 @@ public class DefaultOverdueCondition extends ValidatingConfig<DefaultOverdueConf
return controlTagExclusion;
}
+ public void setNumberOfUnpaidInvoicesEqualsOrExceeds(final Integer numberOfUnpaidInvoicesEqualsOrExceeds) {
+ this.numberOfUnpaidInvoicesEqualsOrExceeds = numberOfUnpaidInvoicesEqualsOrExceeds;
+ }
+
+ public void setTotalUnpaidInvoiceBalanceEqualsOrExceeds(final BigDecimal totalUnpaidInvoiceBalanceEqualsOrExceeds) {
+ this.totalUnpaidInvoiceBalanceEqualsOrExceeds = totalUnpaidInvoiceBalanceEqualsOrExceeds;
+ }
+
+ public void setTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds(final DefaultDuration timeSinceEarliestUnpaidInvoiceEqualsOrExceeds) {
+ this.timeSinceEarliestUnpaidInvoiceEqualsOrExceeds = timeSinceEarliestUnpaidInvoiceEqualsOrExceeds;
+ }
+
+ public void setResponseForLastFailedPayment(final PaymentResponse[] responseForLastFailedPayment) {
+ this.responseForLastFailedPayment = responseForLastFailedPayment;
+ }
+
+ public void setControlTagInclusion(final ControlTagType controlTagInclusion) {
+ this.controlTagInclusion = controlTagInclusion;
+ }
+
+ public void setControlTagExclusion(final ControlTagType controlTagExclusion) {
+ this.controlTagExclusion = controlTagExclusion;
+ }
+
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("DefaultOverdueCondition{");
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueState.java b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueState.java
index 9d33ddd..1068745 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueState.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueState.java
@@ -98,7 +98,7 @@ public class DefaultOverdueState extends ValidatingConfig<DefaultOverdueConfig>
@Override
public boolean isBlockChanges() {
- return blockChanges || disableEntitlement;
+ return blockChanges;
}
@Override
@@ -119,22 +119,26 @@ public class DefaultOverdueState extends ValidatingConfig<DefaultOverdueConfig>
return autoReevaluationInterval.toJodaPeriod();
}
- protected DefaultOverdueState setName(final String name) {
+ public void setAutoReevaluationInterval(final DefaultDuration autoReevaluationInterval) {
+ this.autoReevaluationInterval = autoReevaluationInterval;
+ }
+
+ public DefaultOverdueState setName(final String name) {
this.name = name;
return this;
}
- protected DefaultOverdueState setClearState(final boolean isClearState) {
+ public DefaultOverdueState setClearState(final boolean isClearState) {
this.isClearState = isClearState;
return this;
}
- protected DefaultOverdueState setExternalMessage(final String externalMessage) {
+ public DefaultOverdueState setExternalMessage(final String externalMessage) {
this.externalMessage = externalMessage;
return this;
}
- protected DefaultOverdueState setDisableEntitlement(final boolean cancel) {
+ public DefaultOverdueState setDisableEntitlement(final boolean cancel) {
this.disableEntitlement = cancel;
return this;
}
@@ -144,12 +148,12 @@ public class DefaultOverdueState extends ValidatingConfig<DefaultOverdueConfig>
return this;
}
- protected DefaultOverdueState setBlockChanges(final boolean cancel) {
+ public DefaultOverdueState setBlockChanges(final boolean cancel) {
this.blockChanges = cancel;
return this;
}
- protected DefaultOverdueState setCondition(final DefaultOverdueCondition condition) {
+ public DefaultOverdueState setCondition(final DefaultOverdueCondition condition) {
this.condition = condition;
return this;
}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueStatesAccount.java b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueStatesAccount.java
index 0bd4e63..004fd2d 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueStatesAccount.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueStatesAccount.java
@@ -45,12 +45,12 @@ public class DefaultOverdueStatesAccount extends DefaultOverdueStateSet implemen
return initialReevaluationInterval.toJodaPeriod();
}
- protected DefaultOverdueStatesAccount setAccountOverdueStates(final DefaultOverdueState[] accountOverdueStates) {
+ public DefaultOverdueStatesAccount setAccountOverdueStates(final DefaultOverdueState[] accountOverdueStates) {
this.accountOverdueStates = accountOverdueStates;
return this;
}
- protected DefaultOverdueStatesAccount setInitialReevaluationInterval(final DefaultDuration initialReevaluationInterval) {
+ public DefaultOverdueStatesAccount setInitialReevaluationInterval(final DefaultDuration initialReevaluationInterval) {
this.initialReevaluationInterval = initialReevaluationInterval;
return this;
}
diff --git a/overdue/src/test/resources/OverdueConfig3.xml b/overdue/src/test/resources/OverdueConfig3.xml
new file mode 100644
index 0000000..ddba32f
--- /dev/null
+++ b/overdue/src/test/resources/OverdueConfig3.xml
@@ -0,0 +1,71 @@
+<!--
+ ~ Copyright 2014-2016 Groupon, Inc
+ ~ Copyright 2014-2016 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
+ ~ License. You may obtain a copy of the License at:
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ ~ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ ~ License for the specific language governing permissions and limitations
+ ~ under the License.
+ -->
+
+<overdueConfig>
+ <accountOverdueStates>
+ <state name="OD4">
+ <condition>
+ <numberOfUnpaidInvoicesEqualsOrExceeds>5</numberOfUnpaidInvoicesEqualsOrExceeds>
+ <controlTagInclusion>AUTO_PAY_OFF</controlTagInclusion>
+ </condition>
+ <externalMessage>Reached OD3</externalMessage>
+ <blockChanges>true</blockChanges>
+ <disableEntitlementAndChangesBlocked>true</disableEntitlementAndChangesBlocked>
+ <autoReevaluationInterval>
+ <unit>DAYS</unit><number>5</number>
+ </autoReevaluationInterval>
+ </state>
+ <state name="OD3">
+ <condition>
+ <responseForLastFailedPaymentIn>
+ <response>INVALID_CARD</response>
+ <response>LOST_OR_STOLEN_CARD</response>
+ </responseForLastFailedPaymentIn>
+ </condition>
+ <externalMessage>Reached OD3</externalMessage>
+ <blockChanges>true</blockChanges>
+ <disableEntitlementAndChangesBlocked>true</disableEntitlementAndChangesBlocked>
+ <autoReevaluationInterval>
+ <unit>DAYS</unit><number>5</number>
+ </autoReevaluationInterval>
+ </state>
+ <state name="OD2">
+ <condition>
+ <totalUnpaidInvoiceBalanceEqualsOrExceeds>5.00</totalUnpaidInvoiceBalanceEqualsOrExceeds>
+ </condition>
+ <externalMessage>Reached OD2</externalMessage>
+ <blockChanges>true</blockChanges>
+ <disableEntitlementAndChangesBlocked>true</disableEntitlementAndChangesBlocked>
+ <autoReevaluationInterval>
+ <unit>DAYS</unit><number>5</number>
+ </autoReevaluationInterval>
+ </state>
+ <state name="OD1">
+ <condition>
+ <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>
+ <unit>DAYS</unit><number>30</number>
+ </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>
+ </condition>
+ <externalMessage>Reached OD1</externalMessage>
+ <blockChanges>true</blockChanges>
+ <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>
+ <autoReevaluationInterval>
+ <unit>DAYS</unit><number>5</number>
+ </autoReevaluationInterval>
+ </state>
+ </accountOverdueStates>
+</overdueConfig>
payment/pom.xml 2(+1 -1)
diff --git a/payment/pom.xml b/payment/pom.xml
index d47ebd3..f2d255f 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.17.3-SNAPSHOT</version>
+ <version>0.17.5-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-payment</artifactId>
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
index 1d3d1b2..5e44397 100644
--- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
@@ -466,7 +466,6 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
checkNotNullParameter(currency, "currency");
}
checkNotNullParameter(paymentId, "paymentId");
- checkNotNullParameter(paymentTransactionExternalKey, "paymentTransactionExternalKey");
checkNotNullParameter(properties, "plugin properties");
final String transactionType = TransactionType.REFUND.name();
@@ -731,7 +730,6 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
public Payment createChargebackReversal(final Account account, final UUID paymentId, final String paymentTransactionExternalKey, final CallContext callContext) throws PaymentApiException {
checkNotNullParameter(account, "account");
checkNotNullParameter(paymentId, "paymentId");
- checkNotNullParameter(paymentTransactionExternalKey, "paymentTransactionExternalKey");
final String transactionType = TransactionType.CHARGEBACK.name();
Payment payment = null;
@@ -770,7 +768,6 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
checkNotNullParameter(account, "account");
checkNotNullParameter(paymentId, "paymentId");
- checkNotNullParameter(paymentTransactionExternalKey, "paymentTransactionExternalKey");
final String transactionType = TransactionType.CHARGEBACK.name();
Payment payment = null;
diff --git a/payment/src/main/java/org/killbill/billing/payment/config/MultiTenantPaymentConfig.java b/payment/src/main/java/org/killbill/billing/payment/config/MultiTenantPaymentConfig.java
index bbf365f..6e56c52 100644
--- a/payment/src/main/java/org/killbill/billing/payment/config/MultiTenantPaymentConfig.java
+++ b/payment/src/main/java/org/killbill/billing/payment/config/MultiTenantPaymentConfig.java
@@ -17,7 +17,6 @@
package org.killbill.billing.payment.config;
-import java.lang.reflect.Method;
import java.util.List;
import javax.inject.Inject;
@@ -25,6 +24,7 @@ import javax.inject.Named;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.payment.glue.PaymentModule;
+import org.killbill.billing.util.config.definition.KillbillConfig;
import org.killbill.billing.util.config.definition.PaymentConfig;
import org.killbill.billing.util.config.tenant.CacheConfig;
import org.killbill.billing.util.config.tenant.MultiTenantConfigBase;
@@ -48,11 +48,9 @@ public class MultiTenantPaymentConfig extends MultiTenantConfigBase implements P
@Override
public List<Integer> getPaymentFailureRetryDays(@Param("dummy") final InternalTenantContext tenantContext) {
- // There is no good way to achieve that in java; this solution is expensive (we could consider hardcoding the method name each time instead)
- final Method method = new Object() {}.getClass().getEnclosingMethod();
- final String result = getStringTenantConfig(method.getName(), tenantContext);
+ final String result = getStringTenantConfig("getPaymentFailureRetryDays", tenantContext);
if (result != null) {
- return convertToListInteger(result, method.getName());
+ return convertToListInteger(result, "getPaymentFailureRetryDays");
}
return getPaymentFailureRetryDays();
}
@@ -64,8 +62,7 @@ public class MultiTenantPaymentConfig extends MultiTenantConfigBase implements P
@Override
public int getPluginFailureInitialRetryInSec(@Param("dummy") final InternalTenantContext tenantContext) {
- final Method method = new Object() {}.getClass().getEnclosingMethod();
- final String result = getStringTenantConfig(method.getName(), tenantContext);
+ final String result = getStringTenantConfig("getPluginFailureInitialRetryInSec", tenantContext);
if (result != null) {
return Integer.parseInt(result);
}
@@ -79,9 +76,7 @@ public class MultiTenantPaymentConfig extends MultiTenantConfigBase implements P
@Override
public int getPluginFailureRetryMultiplier(@Param("dummy") final InternalTenantContext tenantContext) {
- final Method method = new Object() {}.getClass().getEnclosingMethod();
-
- final String result = getStringTenantConfig(method.getName(), tenantContext);
+ final String result = getStringTenantConfig("getPluginFailureRetryMultiplier", tenantContext);
if (result != null) {
return Integer.parseInt(result);
}
@@ -95,11 +90,9 @@ public class MultiTenantPaymentConfig extends MultiTenantConfigBase implements P
@Override
public List<TimeSpan> getIncompleteTransactionsRetries(@Param("dummy") final InternalTenantContext tenantContext) {
- final Method method = new Object() {}.getClass().getEnclosingMethod();
-
- final String result = getStringTenantConfig(method.getName(), tenantContext);
+ final String result = getStringTenantConfig("getIncompleteTransactionsRetries", tenantContext);
if (result != null) {
- return convertToListTimeSpan(result, method.getName());
+ return convertToListTimeSpan(result, "getIncompleteTransactionsRetries");
}
return getIncompleteTransactionsRetries();
}
@@ -111,9 +104,7 @@ public class MultiTenantPaymentConfig extends MultiTenantConfigBase implements P
@Override
public int getPluginFailureRetryMaxAttempts(@Param("dummy") final InternalTenantContext tenantContext) {
- final Method method = new Object() {}.getClass().getEnclosingMethod();
-
- final String result = getStringTenantConfig(method.getName(), tenantContext);
+ final String result = getStringTenantConfig("getPluginFailureRetryMaxAttempts", tenantContext);
if (result != null) {
return Integer.parseInt(result);
}
@@ -127,11 +118,9 @@ public class MultiTenantPaymentConfig extends MultiTenantConfigBase implements P
@Override
public List<String> getPaymentControlPluginNames(@Param("dummy") final InternalTenantContext tenantContext) {
- final Method method = new Object() {}.getClass().getEnclosingMethod();
-
- final String result = getStringTenantConfig(method.getName(), tenantContext);
+ final String result = getStringTenantConfig("getPaymentControlPluginNames", tenantContext);
if (result != null) {
- return convertToListString(result, method.getName());
+ return convertToListString(result, "getPaymentControlPluginNames");
}
return getPaymentControlPluginNames();
}
@@ -167,11 +156,7 @@ public class MultiTenantPaymentConfig extends MultiTenantConfigBase implements P
}
@Override
- protected Method getConfigStaticMethod(final String methodName) {
- try {
- return PaymentConfig.class.getMethod(methodName, InternalTenantContext.class);
- } catch (final NoSuchMethodException e) {
- throw new RuntimeException(e);
- }
+ protected Class<? extends KillbillConfig> getConfigClass() {
+ return PaymentConfig.class;
}
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
index e8981f9..ab61ca5 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
@@ -55,8 +55,8 @@ import org.killbill.billing.payment.api.TransactionType;
import org.killbill.billing.payment.core.janitor.IncompletePaymentTransactionTask;
import org.killbill.billing.payment.core.sm.PaymentAutomatonDAOHelper;
import org.killbill.billing.payment.core.sm.PaymentAutomatonRunner;
-import org.killbill.billing.payment.dao.PaymentAttemptModelDao;
import org.killbill.billing.payment.core.sm.PaymentStateContext;
+import org.killbill.billing.payment.dao.PaymentAttemptModelDao;
import org.killbill.billing.payment.dao.PaymentDao;
import org.killbill.billing.payment.dao.PaymentModelDao;
import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
@@ -456,6 +456,11 @@ public class PaymentProcessor extends ProcessorBase {
throw new PaymentApiException(ErrorCode.PAYMENT_DIFFERENT_ACCOUNT_ID, paymentStateContext.getPaymentId());
}
+ if (paymentStateContext.getPaymentTransactionExternalKey() != null) {
+ final List<PaymentTransactionModelDao> allPaymentTransactionsForKey = daoHelper.getPaymentDao().getPaymentTransactionsByExternalKey(paymentStateContext.getPaymentTransactionExternalKey(), internalCallContext);
+ runSanityOnTransactionExternalKey(allPaymentTransactionsForKey, paymentStateContext, internalCallContext);
+ }
+
if (paymentStateContext.getTransactionId() != null || paymentStateContext.getPaymentTransactionExternalKey() != null) {
// If a transaction id or key is passed, we are maybe completing an existing transaction (unless a new key was provided)
final List<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment = daoHelper.getPaymentDao().getTransactionsForPayment(paymentStateContext.getPaymentId(), paymentStateContext.getInternalCallContext());
@@ -499,6 +504,31 @@ public class PaymentProcessor extends ProcessorBase {
return getPayment(nonNullPaymentId, true, false, properties, callContext, internalCallContext);
}
+ private void runSanityOnTransactionExternalKey(final Iterable<PaymentTransactionModelDao> allPaymentTransactionsForKey,
+ final PaymentStateContext paymentStateContext,
+ final InternalCallContext internalCallContext) throws PaymentApiException {
+ for (final PaymentTransactionModelDao paymentTransactionModelDao : allPaymentTransactionsForKey) {
+ // Sanity: verify we don't already have a successful transaction for that key (chargeback reversals are a bit special, it's the only transaction type we can revert)
+ if (paymentTransactionModelDao.getTransactionExternalKey().equals(paymentStateContext.getPaymentTransactionExternalKey()) &&
+ paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.SUCCESS &&
+ paymentTransactionModelDao.getTransactionType() != TransactionType.CHARGEBACK) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS, paymentStateContext.getPaymentTransactionExternalKey());
+ }
+
+ // Sanity: don't share keys across accounts
+ if (!paymentTransactionModelDao.getAccountRecordId().equals(internalCallContext.getAccountRecordId())) {
+ UUID accountId;
+ try {
+ accountId = accountInternalApi.getAccountByRecordId(paymentTransactionModelDao.getAccountRecordId(), internalCallContext).getId();
+ } catch (final AccountApiException e) {
+ log.warn("Unable to retrieve account", e);
+ accountId = null;
+ }
+ throw new PaymentApiException(ErrorCode.PAYMENT_TRANSACTION_DIFFERENT_ACCOUNT_ID, accountId);
+ }
+ }
+ }
+
private PaymentTransactionModelDao findTransactionToCompleteAndRunSanityChecks(final PaymentModelDao paymentModelDao,
final Iterable<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment,
final PaymentStateContext paymentStateContext,
@@ -524,26 +554,6 @@ public class PaymentProcessor extends ProcessorBase {
throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, "transactionType", String.format("%s doesn't match existing transaction type %s", paymentStateContext.getTransactionType(), paymentTransactionModelDao.getTransactionType()));
}
- // Sanity: verify we don't already have a successful transaction for that key (chargeback reversals are a bit special, it's the only transaction type we can revert)
- if (paymentTransactionModelDao.getTransactionExternalKey().equals(paymentStateContext.getPaymentTransactionExternalKey()) &&
- paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.SUCCESS &&
- paymentTransactionModelDao.getTransactionType() != TransactionType.CHARGEBACK) {
- throw new PaymentApiException(ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS, paymentStateContext.getPaymentTransactionExternalKey());
- }
-
- // Sanity: don't share keys across accounts
- if (paymentTransactionModelDao.getTransactionExternalKey().equals(paymentStateContext.getPaymentTransactionExternalKey()) &&
- !paymentTransactionModelDao.getAccountRecordId().equals(internalCallContext.getAccountRecordId())) {
- UUID accountId;
- try {
- accountId = accountInternalApi.getAccountByRecordId(paymentModelDao.getAccountRecordId(), internalCallContext).getId();
- } catch (final AccountApiException e) {
- log.warn("Unable to retrieve account", e);
- accountId = null;
- }
- throw new PaymentApiException(ErrorCode.PAYMENT_TRANSACTION_DIFFERENT_ACCOUNT_ID, accountId);
- }
-
// UNKNOWN transactions are potential candidates, we'll invoke the Janitor first though
if (paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.PENDING || paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.UNKNOWN) {
completionCandidates.add(paymentTransactionModelDao);
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
index 16a43e7..e141506 100644
--- a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
@@ -2008,7 +2008,7 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
createPayment(TransactionType.CAPTURE, authorization.getId(), null, authKey, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
Assert.fail();
} catch (final PaymentApiException e) {
- Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_PARAMETER.getCode());
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
}
try {
@@ -2080,7 +2080,7 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
createPayment(TransactionType.AUTHORIZE, null, null, captureKey, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
Assert.fail();
} catch (final PaymentApiException e) {
- Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_PARAMETER.getCode());
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
}
try {
@@ -2098,6 +2098,44 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(capturedPayment2.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
Assert.assertEquals(capturedPayment2.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
Assert.assertEquals(capturedPayment2.getTransactions().get(2).getTransactionStatus(), TransactionStatus.SUCCESS);
+
+ final String refundKey = UUID.randomUUID().toString();
+ final Payment refundedPayment = createPayment(TransactionType.REFUND, authorization.getId(), null, refundKey, BigDecimal.ONE, PaymentPluginStatus.PROCESSED);
+ Assert.assertEquals(refundedPayment.getTransactions().size(), 4);
+ Assert.assertEquals(refundedPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(refundedPayment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(refundedPayment.getTransactions().get(2).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(refundedPayment.getTransactions().get(3).getTransactionStatus(), TransactionStatus.SUCCESS);
+
+ // Second payment
+
+ final String auth2Key = UUID.randomUUID().toString();
+ final Payment authorization2 = createPayment(TransactionType.AUTHORIZE, null, null, auth2Key, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ assertNotNull(authorization2);
+ Assert.assertEquals(authorization2.getTransactions().size(), 1);
+ Assert.assertEquals(authorization2.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+
+ try {
+ // Capture with an existing successful transaction external key should fail
+ createPayment(TransactionType.CAPTURE, authorization2.getId(), null, captureKey, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
+ }
+
+ final String capture2Key = UUID.randomUUID().toString();
+ final Payment capture2 = createPayment(TransactionType.CAPTURE, authorization2.getId(), null, capture2Key, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.assertEquals(capture2.getTransactions().size(), 2);
+ Assert.assertEquals(capture2.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ Assert.assertEquals(capture2.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
+
+ try {
+ // Refund with an existing successful transaction external key should fail
+ createPayment(TransactionType.REFUND, authorization2.getId(), null, refundKey, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+ Assert.fail();
+ } catch (final PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
+ }
}
@Test(groups = "slow")
@@ -2301,6 +2339,14 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
paymentTransactionExternalKey,
pluginProperties,
callContext);
+ case REFUND:
+ return paymentApi.createRefund(account,
+ paymentId,
+ amount,
+ amount == null ? null : account.getCurrency(),
+ paymentTransactionExternalKey,
+ pluginProperties,
+ callContext);
default:
Assert.fail();
return null;
pom.xml 4(+2 -2)
diff --git a/pom.xml b/pom.xml
index 8c91c40..0927ec9 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.127-SNAPSHOT</version>
+ <version>0.129-SNAPSHOT</version>
</parent>
<artifactId>killbill</artifactId>
- <version>0.17.3-SNAPSHOT</version>
+ <version>0.17.5-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 fc683f3..c5a14ad 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.17.3-SNAPSHOT</version>
+ <version>0.17.5-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-profiles-killbill</artifactId>
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/config/MultiTenantNotificationConfig.java b/profiles/killbill/src/main/java/org/killbill/billing/server/config/MultiTenantNotificationConfig.java
index cfea29b..08ffd8a 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/config/MultiTenantNotificationConfig.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/config/MultiTenantNotificationConfig.java
@@ -18,10 +18,13 @@
package org.killbill.billing.server.config;
import java.lang.reflect.Method;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.payment.glue.PaymentModule;
+import org.killbill.billing.util.config.definition.KillbillConfig;
import org.killbill.billing.util.config.definition.NotificationConfig;
import org.killbill.billing.util.config.tenant.CacheConfig;
import org.killbill.billing.util.config.tenant.MultiTenantConfigBase;
@@ -33,6 +36,7 @@ import com.google.inject.name.Named;
public class MultiTenantNotificationConfig extends MultiTenantConfigBase implements NotificationConfig {
+ private final Map<String, Method> methodsCache = new HashMap<String, Method>();
private final NotificationConfig staticConfig;
@Inject
@@ -42,27 +46,21 @@ public class MultiTenantNotificationConfig extends MultiTenantConfigBase impleme
}
@Override
- protected Method getConfigStaticMethod(final String methodName) {
- try {
- return NotificationConfig.class.getMethod(methodName, InternalTenantContext.class);
- } catch (final NoSuchMethodException e) {
- throw new RuntimeException(e);
- }
- }
-
- @Override
public List<TimeSpan> getPushNotificationsRetries() {
return staticConfig.getPushNotificationsRetries();
}
@Override
public List<TimeSpan> getPushNotificationsRetries(@Param("dummy") final InternalTenantContext tenantContext) {
- final Method method = new Object() {}.getClass().getEnclosingMethod();
-
- final String result = getStringTenantConfig(method.getName(), tenantContext);
+ final String result = getStringTenantConfig("getPushNotificationsRetries", tenantContext);
if (result != null) {
- return convertToListTimeSpan(result, method.getName());
+ return convertToListTimeSpan(result, "getPushNotificationsRetries");
}
return getPushNotificationsRetries();
}
+
+ @Override
+ protected Class<? extends KillbillConfig> getConfigClass() {
+ return NotificationConfig.class;
+ }
}
diff --git a/profiles/killbill/src/main/resources/killbill-server.properties b/profiles/killbill/src/main/resources/killbill-server.properties
index 19f583a..3c2b298 100644
--- a/profiles/killbill/src/main/resources/killbill-server.properties
+++ b/profiles/killbill/src/main/resources/killbill-server.properties
@@ -22,7 +22,7 @@ org.killbill.jruby.context.version=RUBY2_0
# KILLBILL GENERIC PROPERTIES
#
# Database config
-org.killbill.dao.url=jdbc:mysql://127.0.0.1:3306/killbill_0_17_x
+org.killbill.dao.url=jdbc:mysql://127.0.0.1:3306/killbill
org.killbill.dao.user=root
org.killbill.dao.password=root
org.killbill.dao.logLevel=DEBUG
@@ -61,7 +61,7 @@ org.killbill.tenant.broadcast.rate=1s
# PLUGIN SPECIFIC PROPERTIES
#
# Database config (OSGI plugins)
-org.killbill.billing.osgi.dao.url=jdbc:mysql://127.0.0.1:3306/killbill_0_17_x
+org.killbill.billing.osgi.dao.url=jdbc:mysql://127.0.0.1:3306/killbill
org.killbill.billing.osgi.dao.user=root
org.killbill.billing.osgi.dao.password=root
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java
index f2f8f2c..6b13996 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java
@@ -22,11 +22,11 @@ import java.math.BigDecimal;
import java.util.Comparator;
import java.util.List;
+import org.killbill.billing.client.KillBillClientException;
import org.killbill.billing.client.model.Account;
import org.killbill.billing.client.model.Invoice;
import org.killbill.billing.client.model.InvoicePayment;
import org.killbill.billing.client.model.Invoices;
-import org.killbill.billing.client.model.Payment;
import org.killbill.billing.client.model.Tags;
import org.killbill.billing.util.tag.ControlTagType;
import org.testng.Assert;
@@ -42,9 +42,9 @@ public class TestOverdue extends TestJaxrsBase {
@Test(groups = "slow", description = "Upload and retrieve a per tenant overdue config")
public void testMultiTenantOverdueConfig() throws Exception {
final String overdueConfigPath = Resources.getResource("overdue.xml").getPath();
- killBillClient.uploadXMLOverdueConfig(overdueConfigPath, createdBy, reason, comment);
+ killBillClient.uploadXMLOverdueConfig(overdueConfigPath, requestOptions);
- final String overdueConfig = killBillClient.getXMLOverdueConfig();
+ final String overdueConfig = killBillClient.getXMLOverdueConfig(requestOptions);
Assert.assertNotNull(overdueConfig);
}
@@ -54,29 +54,29 @@ public class TestOverdue extends TestJaxrsBase {
final Account accountJson = createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice();
// Get the invoices
- final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId());
+ final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), requestOptions);
// 2 invoices but look for the non zero dollar one
assertEquals(invoices.size(), 2);
// We're still clear - see the configuration
- Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJson.getAccountId()).getIsClearState());
+ Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getIsClearState());
clock.addDays(30);
crappyWaitForLackOfProperSynchonization();
- Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId()).getName(), "OD1");
+ Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getName(), "OD1");
clock.addDays(10);
crappyWaitForLackOfProperSynchonization();
- Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId()).getName(), "OD2");
+ Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getName(), "OD2");
clock.addDays(10);
crappyWaitForLackOfProperSynchonization();
- Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId()).getName(), "OD3");
+ Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getName(), "OD3");
// Post external payments, paying the most recent invoice first: this is to avoid a race condition where
// a refresh overdue notification kicks in after the first payment, which makes the account goes CLEAR and
// triggers an AUTO_INVOICE_OFF tag removal (hence adjustment of the other invoices balance).
- final Invoices invoicesForAccount = killBillClient.getInvoicesForAccount(accountJson.getAccountId());
+ final Invoices invoicesForAccount = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), requestOptions);
final List<Invoice> mostRecentInvoiceFirst = Ordering.<Invoice>from(new Comparator<Invoice>() {
@Override
public int compare(final Invoice invoice1, final Invoice invoice2) {
@@ -90,7 +90,7 @@ public class TestOverdue extends TestJaxrsBase {
invoicePayment.setPurchasedAmount(invoice.getAmount());
invoicePayment.setAccountId(accountJson.getAccountId());
invoicePayment.setTargetInvoiceId(invoice.getInvoiceId());
- killBillClient.createInvoicePayment(invoicePayment, true, createdBy, reason, comment);
+ killBillClient.createInvoicePayment(invoicePayment, true, requestOptions);
}
}
@@ -98,7 +98,7 @@ public class TestOverdue extends TestJaxrsBase {
crappyWaitForLackOfProperSynchonization();
// Verify we're in clear state
- Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJson.getAccountId()).getIsClearState());
+ Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getIsClearState());
}
@Test(groups = "slow", description = "Allow overdue condition by control tag defined in overdue config xml file")
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java
index 0baeb5e..16f4c35 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java
@@ -237,7 +237,7 @@ public class TestPayment extends TestJaxrsBase {
.withComment(comment)
.withQueryParams(queryParams).build();
- Payments payments = killBillClient.searchPayments("1", 0L, 100L, AuditLevel.NONE, inputOptions);
+ Payments payments = killBillClient.searchPayments("", 0L, 100L, AuditLevel.NONE, inputOptions);
Assert.assertNotNull(payments.get(0).getPaymentAttempts());
Assert.assertEquals(payments.get(0).getPaymentAttempts().get(0).getStateName(), "RETRIED");
profiles/killpay/pom.xml 2(+1 -1)
diff --git a/profiles/killpay/pom.xml b/profiles/killpay/pom.xml
index 27999e1..b71057c 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.17.3-SNAPSHOT</version>
+ <version>0.17.5-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 b2e5cfe..6b436d0 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.17.3-SNAPSHOT</version>
+ <version>0.17.5-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 9998aa4..091fbeb 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.17.3-SNAPSHOT</version>
+ <version>0.17.5-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-subscription</artifactId>
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
index dbf1446..fcc771d 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
@@ -37,7 +37,6 @@ import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingAlignment;
-import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Catalog;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.CatalogService;
@@ -170,6 +169,18 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
}
final DefaultSubscriptionBase baseSubscription = (DefaultSubscriptionBase) dao.getBaseSubscription(bundleId, context);
+
+ // verify the number of subscriptions (of the same kind) allowed per bundle
+ if (ProductCategory.ADD_ON.toString().equalsIgnoreCase(plan.getProduct().getCategory().toString())) {
+ if (plan.getPlansAllowedInBundle() != -1
+ && plan.getPlansAllowedInBundle() > 0
+ && addonUtils.countExistingAddOnsWithSamePlanName(getSubscriptionsForBundle(bundleId, null, context), plan.getName())
+ >= plan.getPlansAllowedInBundle()) {
+ // a new ADD_ON subscription of the same plan can't be added because it has reached its limit by bundle
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_AO_MAX_PLAN_ALLOWED_BY_BUNDLE, plan.getName());
+ }
+ }
+
final DateTime bundleStartDate = getBundleStartDateWithSanity(bundleId, baseSubscription, plan, effectiveDate, context);
return apiService.createPlan(new SubscriptionBuilder()
.setId(UUIDs.randomUUID())
@@ -201,6 +212,8 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
}
boolean first = true;
+ final List<SubscriptionBase> subscriptionsForBundle = getSubscriptionsForBundle(bundleId, null, context);
+
for (EntitlementSpecifier entitlement : entitlements) {
final PlanPhaseSpecifier spec = entitlement.getPlanPhaseSpecifier();
@@ -221,6 +234,18 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
}
}
+ // verify the number of subscriptions (of the same kind) allowed per bundle and the existing ones
+ if (ProductCategory.ADD_ON.toString().equalsIgnoreCase(plan.getProduct().getCategory().toString())) {
+ if (plan.getPlansAllowedInBundle() != -1 && plan.getPlansAllowedInBundle() > 0) {
+ int existingAddOnsWithSamePlanName = addonUtils.countExistingAddOnsWithSamePlanName(subscriptionsForBundle, plan.getName());
+ int currentAddOnsWithSamePlanName = countCurrentAddOnsWithSamePlanName(entitlements, catalog, plan.getName(), effectiveDate, callContext);
+ if ((existingAddOnsWithSamePlanName + currentAddOnsWithSamePlanName) > plan.getPlansAllowedInBundle()) {
+ // a new ADD_ON subscription of the same plan can't be added because it has reached its limit by bundle
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_AO_MAX_PLAN_ALLOWED_BY_BUNDLE, plan.getName());
+ }
+ }
+ }
+
SubscriptionSpecifier subscription = new SubscriptionSpecifier();
subscription.setRealPriceList(plan.getPriceListName());
subscription.setEffectiveDate(effectiveDate);
@@ -250,6 +275,25 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
}
}
+ private int countCurrentAddOnsWithSamePlanName(final Iterable<EntitlementSpecifier> entitlements,
+ final Catalog catalog, final String planName,
+ final DateTime effectiveDate, final CallContext callContext) throws CatalogApiException {
+ int countCurrentAddOns = 0;
+ for (EntitlementSpecifier entitlement : entitlements) {
+ final PlanPhaseSpecifier spec = entitlement.getPlanPhaseSpecifier();
+ final PlanPhasePriceOverridesWithCallContext overridesWithContext =
+ new DefaultPlanPhasePriceOverridesWithCallContext(entitlement.getOverrides(), callContext);
+ final Plan plan = catalog.createOrFindPlan(spec, overridesWithContext, effectiveDate);
+
+ if (plan.getName().equalsIgnoreCase(planName)
+ && plan.getProduct().getCategory() != null
+ && ProductCategory.ADD_ON.equals(plan.getProduct().getCategory())) {
+ countCurrentAddOns++;
+ }
+ }
+ return countCurrentAddOns;
+ }
+
@Override
public void cancelBaseSubscriptions(final Iterable<SubscriptionBase> subscriptions, final BillingActionPolicy policy, final InternalCallContext context) throws SubscriptionBaseApiException {
apiService.cancelWithPolicyNoValidation(Iterables.<SubscriptionBase, DefaultSubscriptionBase>transform(subscriptions,
@@ -464,8 +508,26 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
final PlanSpecifier spec,
final DateTime requestedDateWithMs,
final BillingActionPolicy requestedPolicy,
- final InternalTenantContext context) throws SubscriptionBaseApiException {
+ final List<PlanPhasePriceOverride> overrides,
+ final InternalCallContext context) throws SubscriptionBaseApiException, CatalogApiException {
final TenantContext tenantContext = internalCallContextFactory.createTenantContext(context);
+ final CallContext callContext = internalCallContextFactory.createCallContext(context);
+
+ // verify the number of subscriptions (of the same kind) allowed per bundle
+ final Catalog catalog = catalogService.getFullCatalog(true, true, context);
+ final DateTime now = clock.getUTCNow();
+ final DateTime effectiveDate = (requestedDateWithMs != null) ? DefaultClock.truncateMs(requestedDateWithMs) : now;
+ final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(overrides, callContext);
+ final Plan plan = catalog.createOrFindPlan(spec, overridesWithContext, effectiveDate);
+ if (ProductCategory.ADD_ON.toString().equalsIgnoreCase(plan.getProduct().getCategory().toString())) {
+ if (plan.getPlansAllowedInBundle() != -1
+ && plan.getPlansAllowedInBundle() > 0
+ && addonUtils.countExistingAddOnsWithSamePlanName(getSubscriptionsForBundle(subscription.getBundleId(), null, context), plan.getName())
+ >= plan.getPlansAllowedInBundle()) {
+ // the plan can be changed to the new value, because it has reached its limit by bundle
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_AO_MAX_PLAN_ALLOWED_BY_BUNDLE, plan.getName());
+ }
+ }
return apiService.dryRunChangePlan((DefaultSubscriptionBase) subscription, spec, requestedDateWithMs, requestedPolicy, tenantContext);
}
@@ -534,13 +596,18 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
List<SubscriptionBaseEvent> dryRunEvents = null;
try {
final PlanPhaseSpecifier inputSpec = dryRunArguments.getPlanPhaseSpecifier();
+ final boolean isInputSpecNullOrEmpty = inputSpec == null ||
+ (inputSpec.getPlanName() == null && inputSpec.getProductName() == null && inputSpec.getBillingPeriod() == null);
final Catalog catalog = catalogService.getFullCatalog(true, true, context);
- final PlanPhasePriceOverridesWithCallContext overridesWithContext = null; // TODO not supported to dryRun with custom price
- final Plan plan = (inputSpec != null && inputSpec.getProductName() != null && inputSpec.getBillingPeriod() != null) ?
- catalog.createOrFindPlan(inputSpec, overridesWithContext, utcNow) : null;
+ // Create an overridesWithContext with a null context to indicate this is dryRun and no price overriden plan should be created.
+ final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(dryRunArguments.getPlanPhasePriceOverrides(), null);
+ final Plan plan = isInputSpecNullOrEmpty ?
+ null :
+ catalog.createOrFindPlan(inputSpec, overridesWithContext, utcNow);
final TenantContext tenantContext = internalCallContextFactory.createTenantContext(context);
+
if (dryRunArguments != null) {
switch (dryRunArguments.getAction()) {
case START_BILLING:
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
index cffcd6b..0fee918 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
@@ -394,6 +394,16 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(overrides, context);
final Plan newPlan = catalogService.getFullCatalog(true, true, internalCallContext).createOrFindPlan(spec, overridesWithContext, effectiveDate, subscription.getStartDate());
+ if (ProductCategory.ADD_ON.toString().equalsIgnoreCase(newPlan.getProduct().getCategory().toString())) {
+ if (newPlan.getPlansAllowedInBundle() != -1
+ && newPlan.getPlansAllowedInBundle() > 0
+ && addonUtils.countExistingAddOnsWithSamePlanName(dao.getSubscriptions(subscription.getBundleId(), null, internalCallContext), newPlan.getName())
+ >= newPlan.getPlansAllowedInBundle()) {
+ // the plan can be changed to the new value, because it has reached its limit by bundle
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_AO_MAX_PLAN_ALLOWED_BY_BUNDLE, newPlan.getName());
+ }
+ }
+
if (newPlan.getProduct().getCategory() != subscription.getCategory()) {
throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_INVALID, subscription.getId());
}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/addon/AddonUtils.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/addon/AddonUtils.java
index 3a55506..7098709 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/addon/AddonUtils.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/addon/AddonUtils.java
@@ -16,6 +16,8 @@
package org.killbill.billing.subscription.engine.addon;
+import java.util.List;
+
import org.joda.time.DateTime;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.callcontext.InternalTenantContext;
@@ -23,7 +25,9 @@ import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.CatalogService;
import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.Product;
+import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.subscription.api.SubscriptionBase;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
@@ -121,4 +125,16 @@ public class AddonUtils {
}
return false;
}
+
+ public int countExistingAddOnsWithSamePlanName(final List<SubscriptionBase> subscriptionsForBundle, final String planName) {
+ int countExistingAddOns = 0;
+ for (SubscriptionBase subscription : subscriptionsForBundle) {
+ if (subscription.getCurrentPlan().getName().equalsIgnoreCase(planName)
+ && subscription.getLastActiveProduct().getCategory() != null
+ && ProductCategory.ADD_ON.equals(subscription.getLastActiveProduct().getCategory())) {
+ countExistingAddOns++;
+ }
+ }
+ return countExistingAddOns;
+ }
}
tenant/pom.xml 2(+1 -1)
diff --git a/tenant/pom.xml b/tenant/pom.xml
index 6b10bda..0dea385 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.17.3-SNAPSHOT</version>
+ <version>0.17.5-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 594fe85..4b86981 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.17.3-SNAPSHOT</version>
+ <version>0.17.5-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 524e002..c1ab5a4 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.17.3-SNAPSHOT</version>
+ <version>0.17.5-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-util</artifactId>
diff --git a/util/src/main/java/org/killbill/billing/util/config/tenant/MultiTenantConfigBase.java b/util/src/main/java/org/killbill/billing/util/config/tenant/MultiTenantConfigBase.java
index 87f0b0a..39562ee 100644
--- a/util/src/main/java/org/killbill/billing/util/config/tenant/MultiTenantConfigBase.java
+++ b/util/src/main/java/org/killbill/billing/util/config/tenant/MultiTenantConfigBase.java
@@ -18,9 +18,12 @@
package org.killbill.billing.util.config.tenant;
import java.lang.reflect.Method;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.config.definition.KillbillConfig;
import org.skife.config.Config;
import org.skife.config.Separator;
import org.skife.config.TimeSpan;
@@ -31,6 +34,7 @@ import com.google.common.collect.Iterables;
public abstract class MultiTenantConfigBase {
+ private final Map<String, Method> methodsCache = new HashMap<String, Method>();
protected final CacheConfig cacheConfig;
private final static Function<String, Integer> INT_CONVERTER = new Function<String, Integer>() {
@@ -107,5 +111,23 @@ public abstract class MultiTenantConfigBase {
return ImmutableList.copyOf(value.split(separator == null ? Separator.DEFAULT : separator.value()));
}
- protected abstract Method getConfigStaticMethod(final String methodName);
+ protected Method getConfigStaticMethod(final String methodName) {
+ Method method = methodsCache.get(methodName);
+ if (method == null) {
+ synchronized (methodsCache) {
+ method = methodsCache.get(methodName);
+ if (method == null) {
+ try {
+ method = getConfigClass().getMethod(methodName, InternalTenantContext.class);
+ methodsCache.put(methodName, method);
+ } catch (final NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ }
+ return method;
+ }
+
+ protected abstract Class<? extends KillbillConfig> getConfigClass();
}
diff --git a/util/src/main/java/org/killbill/billing/util/security/AnnotationHierarchicalResolver.java b/util/src/main/java/org/killbill/billing/util/security/AnnotationHierarchicalResolver.java
index 3973932..7ae7360 100644
--- a/util/src/main/java/org/killbill/billing/util/security/AnnotationHierarchicalResolver.java
+++ b/util/src/main/java/org/killbill/billing/util/security/AnnotationHierarchicalResolver.java
@@ -18,6 +18,7 @@ package org.killbill.billing.util.security;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
+import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
@@ -26,13 +27,26 @@ import org.apache.shiro.aop.MethodInvocation;
public class AnnotationHierarchicalResolver implements AnnotationResolver {
+ private final Map<String, Annotation> methodToAnnotation = new HashMap<String, Annotation>();
+
@Override
public Annotation getAnnotation(final MethodInvocation mi, final Class<? extends Annotation> clazz) {
return getAnnotationFromMethod(mi.getMethod(), clazz);
}
public Annotation getAnnotationFromMethod(final Method method, final Class<? extends Annotation> clazz) {
- return findAnnotation(method, clazz);
+ final String key = method.toString();
+ Annotation annotation = methodToAnnotation.get(key);
+ if (annotation == null) {
+ synchronized (methodToAnnotation) {
+ annotation = methodToAnnotation.get(key);
+ if (annotation == null) {
+ annotation = findAnnotation(method, clazz);
+ methodToAnnotation.put(key, annotation);
+ }
+ }
+ }
+ return annotation;
}
// The following comes from spring-core (AnnotationUtils) to handle annotations on interfaces